// String expansion functions. These functions perform several kinds of parameter expansion.
// IWYU pragma: no_include <cstddef>
#include "config.h"

#include <errno.h>
#include <pwd.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>

#ifdef HAVE_SYS_SYSCTL_H
#include <sys/sysctl.h>  // IWYU pragma: keep
#endif
#ifdef SunOS
#include <procfs.h>
#endif
#if __APPLE__
#include <sys/proc.h>
#else
#include <dirent.h>
#include <sys/stat.h>
#endif

#include <algorithm>
#include <functional>
#include <memory>  // IWYU pragma: keep
#include <type_traits>
#include <vector>

#include "common.h"
#include "complete.h"
#include "env.h"
#include "exec.h"
#include "expand.h"
#include "fallback.h"  // IWYU pragma: keep
#include "iothread.h"
#include "parse_constants.h"
#include "parse_util.h"
#include "path.h"
#include "proc.h"
#include "util.h"
#include "wildcard.h"
#include "wutil.h"  // IWYU pragma: keep
#ifdef KERN_PROCARGS2
#else
#include "tokenizer.h"
#endif

/// Description for child process.
#define COMPLETE_CHILD_PROCESS_DESC _(L"Child process")

/// Description for non-child process.
#define COMPLETE_PROCESS_DESC _(L"Process")

/// Description for long job.
#define COMPLETE_JOB_DESC _(L"Job")

/// Description for short job. The job command is concatenated.
#define COMPLETE_JOB_DESC_VAL _(L"Job: %ls")

/// Description for the shells own pid.
#define COMPLETE_SELF_DESC _(L"Shell process")

/// Description for the shells own pid.
#define COMPLETE_LAST_DESC _(L"Last background job")

/// String in process expansion denoting ourself.
#define SELF_STR L"self"

/// String in process expansion denoting last background job.
#define LAST_STR L"last"

/// Characters which make a string unclean if they are the first character of the string. See \c
/// expand_is_clean().
#define UNCLEAN_FIRST L"~%"
/// Unclean characters. See \c expand_is_clean().
#define UNCLEAN L"$*?\\\"'({})"

static void remove_internal_separator(wcstring *s, bool conv);

/// Test if the specified argument is clean, i.e. it does not contain any tokens which need to be
/// expanded or otherwise altered. Clean strings can be passed through expand_string and expand_one
/// without changing them. About two thirds of all strings are clean, so skipping expansion on them
/// actually does save a small amount of time, since it avoids multiple memory allocations during
/// the expansion process.
///
/// \param in the string to test
static bool expand_is_clean(const wcstring &in) {
    if (in.empty()) return true;

    // Test characters that have a special meaning in the first character position.
    if (wcschr(UNCLEAN_FIRST, in.at(0)) != NULL) return false;

    // Test characters that have a special meaning in any character position.
    return in.find_first_of(UNCLEAN) == wcstring::npos;
}

/// Append a syntax error to the given error list.
static void append_syntax_error(parse_error_list_t *errors, size_t source_start, const wchar_t *fmt,
                                ...) {
    if (errors != NULL) {
        parse_error_t error;
        error.source_start = source_start;
        error.source_length = 0;
        error.code = parse_error_syntax;

        va_list va;
        va_start(va, fmt);
        error.text = vformat_string(fmt, va);
        va_end(va);

        errors->push_back(error);
    }
}

/// Append a cmdsub error to the given error list.
static void append_cmdsub_error(parse_error_list_t *errors, size_t source_start, const wchar_t *fmt,
                                ...) {
    if (errors != NULL) {
        parse_error_t error;
        error.source_start = source_start;
        error.source_length = 0;
        error.code = parse_error_cmdsubst;

        va_list va;
        va_start(va, fmt);
        error.text = vformat_string(fmt, va);
        va_end(va);

        errors->push_back(error);
    }
}

/// Return the environment variable value for the string starting at \c in.
static env_var_t expand_var(const wchar_t *in) {
    if (!in) return env_var_t::missing_var();
    return env_get_string(in);
}

/// Test if the specified string does not contain character which can not be used inside a quoted
/// string.
static int is_quotable(const wchar_t *str) {
    switch (*str) {
        case 0: {
            return 1;
        }
        case L'\n':
        case L'\t':
        case L'\r':
        case L'\b':
        case L'\e': {
            return 0;
        }
        default: { return is_quotable(str + 1); }
    }
    return 0;
}

static int is_quotable(const wcstring &str) { return is_quotable(str.c_str()); }

wcstring expand_escape_variable(const wcstring &in) {
    wcstring_list_t lst;
    wcstring buff;

    tokenize_variable_array(in, lst);

    size_t size = lst.size();
    if (size == 0) {
        buff.append(L"''");
    } else if (size == 1) {
        const wcstring &el = lst.at(0);

        if (el.find(L' ') != wcstring::npos && is_quotable(el)) {
            buff.append(L"'");
            buff.append(el);
            buff.append(L"'");
        } else {
            buff.append(escape_string(el, 1));
        }
    } else {
        for (size_t j = 0; j < lst.size(); j++) {
            const wcstring &el = lst.at(j);
            if (j) buff.append(L"  ");

            if (is_quotable(el)) {
                buff.append(L"'");
                buff.append(el);
                buff.append(L"'");
            } else {
                buff.append(escape_string(el, 1));
            }
        }
    }
    return buff;
}

/// Tests if all characters in the wide string are numeric.
static int iswnumeric(const wchar_t *n) {
    for (; *n; n++) {
        if (*n < L'0' || *n > L'9') {
            return 0;
        }
    }
    return 1;
}

/// See if the process described by \c proc matches the commandline \c cmd.
static bool match_pid(const wcstring &cmd, const wchar_t *proc, size_t *offset) {
    // Test for a direct match. If the proc string is empty (e.g. the user tries to complete against
    // %), then return an offset pointing at the base command. That ensures that you don't see a
    // bunch of dumb paths when completing against all processes.
    if (proc[0] != L'\0' && wcsncmp(cmd.c_str(), proc, wcslen(proc)) == 0) {
        if (offset) *offset = 0;
        return true;
    }

    // Get the command to match against. We're only interested in the last path component.
    const wcstring base_cmd = wbasename(cmd);

    bool result = string_prefixes_string(proc, base_cmd);
    // It's a match. Return the offset within the full command.
    if (result && offset) *offset = cmd.size() - base_cmd.size();
    return result;
}

/// Helper class for iterating over processes. The names returned have been unescaped (e.g. may
/// include spaces).
#ifdef KERN_PROCARGS2

// BSD / OS X process completions.

class process_iterator_t {
    std::vector<pid_t> pids;
    size_t idx;

    wcstring name_for_pid(pid_t pid);

   public:
    process_iterator_t();
    bool next_process(wcstring *str, pid_t *pid);
};

wcstring process_iterator_t::name_for_pid(pid_t pid) {
    wcstring result;
    int mib[4], maxarg = 0, numArgs = 0;
    size_t size = 0;
    char *args = NULL, *stringPtr = NULL;

    mib[0] = CTL_KERN;
    mib[1] = KERN_ARGMAX;

    size = sizeof(maxarg);
    if (sysctl(mib, 2, &maxarg, &size, NULL, 0) == -1) {
        return result;
    }

    args = (char *)malloc(maxarg);
    if (args == NULL) {  // cppcheck-suppress memleak
        return result;
    }

    mib[0] = CTL_KERN;
    mib[1] = KERN_PROCARGS2;
    mib[2] = pid;

    size = (size_t)maxarg;
    if (sysctl(mib, 3, args, &size, NULL, 0) == -1) {
        free(args);
        return result;
    }

    memcpy(&numArgs, args, sizeof(numArgs));
    stringPtr = args + sizeof(numArgs);
    result = str2wcstring(stringPtr);
    free(args);
    return result;
}

bool process_iterator_t::next_process(wcstring *out_str, pid_t *out_pid) {
    wcstring name;
    pid_t pid = 0;
    bool result = false;
    while (idx < pids.size()) {
        pid = pids.at(idx++);
        name = name_for_pid(pid);
        if (!name.empty()) {
            result = true;
            break;
        }
    }
    if (result) {
        *out_str = name;
        *out_pid = pid;
    }
    return result;
}

process_iterator_t::process_iterator_t() : idx(0) {
    int err;
    struct kinfo_proc *result;
    bool done;
    static const int name[] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0};
    // Declaring name as const requires us to cast it when passing it to sysctl because the
    // prototype doesn't include the const modifier.
    size_t length;

    // We start by calling sysctl with result == NULL and length == 0. That will succeed, and set
    // length to the appropriate length. We then allocate a buffer of that size and call sysctl
    // again with that buffer.  If that succeeds, we're done.  If that fails with ENOMEM, we have to
    // throw away our buffer and loop.  Note that the loop causes use to call sysctl with NULL
    // again; this is necessary because the ENOMEM failure case sets length to the amount of data
    // returned, not the amount of data that could have been returned.
    result = NULL;
    done = false;
    do {
        assert(result == NULL);

        // Call sysctl with a NULL buffer.
        length = 0;
        err = sysctl((int *)name, (sizeof(name) / sizeof(*name)) - 1, NULL, &length, NULL, 0);
        if (err == -1) {
            err = errno;
        }

        // Allocate an appropriately sized buffer based on the results from the previous call.
        if (err == 0) {
            result = (struct kinfo_proc *)malloc(length);
            if (result == NULL) {
                err = ENOMEM;
            }
        }

        // Call sysctl again with the new buffer.  If we get an ENOMEM error, toss away our buffer
        // and start again.
        if (err == 0) {
            err = sysctl((int *)name, (sizeof(name) / sizeof(*name)) - 1, result, &length, NULL, 0);
            if (err == -1) {
                err = errno;
            }
            if (err == 0) {
                done = true;
            } else if (err == ENOMEM) {
                assert(result != NULL);
                free(result);
                result = NULL;
                err = 0;
            }
        }
    } while (err == 0 && !done);

    // Clean up and establish post conditions.
    if (err == 0 && result != NULL) {
        for (size_t idx = 0; idx < length / sizeof(struct kinfo_proc); idx++)
            pids.push_back(result[idx].kp_proc.p_pid);
    }

    if (result) free(result);
}

#else

/// /proc style process completions.
class process_iterator_t {
    DIR *dir;

   public:
    process_iterator_t();
    ~process_iterator_t();

    bool next_process(wcstring *out_str, pid_t *out_pid);
};

process_iterator_t::process_iterator_t(void) { dir = opendir("/proc"); }

process_iterator_t::~process_iterator_t(void) {
    if (dir) closedir(dir);
}

bool process_iterator_t::next_process(wcstring *out_str, pid_t *out_pid) {
    wcstring cmd;
    pid_t pid = 0;
    while (cmd.empty()) {
        wcstring name;
        if (!dir || !wreaddir(dir, name)) break;
        if (!iswnumeric(name.c_str())) continue;

        wcstring path = wcstring(L"/proc/") + name;
        struct stat buf;
        if (wstat(path, &buf)) continue;

        if (buf.st_uid != getuid()) continue;

        // Remember the pid.
        pid = fish_wcstoi(name.c_str());
        if (errno || pid < 0) {
            debug(1, _(L"Unexpected failure to convert pid '%ls' to integer\n"), name.c_str());
        }

        // The 'cmdline' file exists, it should contain the commandline.
        FILE *cmdfile;
        if ((cmdfile = wfopen(path + L"/cmdline", "r"))) {
            wcstring full_command_line;
            fgetws2(&full_command_line, cmdfile);

            // The command line needs to be escaped.
            cmd = tok_first(full_command_line);
        }
#ifdef SunOS
        else if ((cmdfile = wfopen(path + L"/psinfo", "r"))) {
            psinfo_t info;
            if (fread(&info, sizeof(info), 1, cmdfile)) {
                // The filename is unescaped.
                cmd = str2wcstring(info.pr_fname);
            }
        }
#endif
        if (cmdfile) fclose(cmdfile);
    }

    bool result = !cmd.empty();
    if (result) {
        *out_str = cmd;
        *out_pid = pid;
    }
    return result;
}

#endif

/// The following function is invoked on the main thread, because the job list is not thread safe.
/// It should search the job list for something matching the given proc, and then return true to
/// stop the search, false to continue it.
static bool find_job(const wchar_t *proc, expand_flags_t flags,
                     std::vector<completion_t> *completions) {
    ASSERT_IS_MAIN_THREAD();

    bool found = false;
    // If we are not doing tab completion, we first check for the single '%' character, because an
    // empty string will pass the numeric check below. But if we are doing tab completion, we want
    // all of the job IDs as completion options, not just the last job backgrounded, so we pass this
    // first block in favor of the second.
    if (wcslen(proc) == 0 && !(flags & EXPAND_FOR_COMPLETIONS)) {
        // This is an empty job expansion: '%'. It expands to the last job backgrounded.
        job_iterator_t jobs;
        while (const job_t *j = jobs.next()) {
            if (!j->command_is_empty()) {
                append_completion(completions, to_string<long>(j->pgid));
                break;
            }
        }
        // You don't *really* want to flip a coin between killing the last process backgrounded and
        // all processes, do you? Let's not try other match methods with the solo '%' syntax.
        found = true;
    } else if (iswnumeric(proc)) {
        // This is a numeric job string, like '%2'.
        if (flags & EXPAND_FOR_COMPLETIONS) {
            job_iterator_t jobs;
            while (const job_t *j = jobs.next()) {
                wchar_t jid[16];
                if (j->command_is_empty()) continue;

                swprintf(jid, 16, L"%d", j->job_id);

                if (wcsncmp(proc, jid, wcslen(proc)) == 0) {
                    wcstring desc_buff = format_string(COMPLETE_JOB_DESC_VAL, j->command_wcstr());
                    append_completion(completions, jid + wcslen(proc), desc_buff, 0);
                }
            }
        } else {
            int jid = fish_wcstoi(proc);
            if (!errno && jid > 0) {
                const job_t *j = job_get(jid);
                if (j && !j->command_is_empty()) {
                    append_completion(completions, to_string<long>(j->pgid));
                }
            }
        }
        // Stop here so you can't match a random process name when you're just trying to use job
        // control.
        found = true;
    }

    if (found) {
        return found;
    }

    job_iterator_t jobs;
    while (const job_t *j = jobs.next()) {
        if (j->command_is_empty()) continue;

        size_t offset = 0;
        if (match_pid(j->command(), proc, &offset)) {
            if (flags & EXPAND_FOR_COMPLETIONS) {
                append_completion(completions, j->command_wcstr() + offset + wcslen(proc),
                                  COMPLETE_JOB_DESC, 0);
            } else {
                append_completion(completions, to_string<long>(j->pgid));
                found = 1;
            }
        }
    }

    if (found) {
        return found;
    }

    jobs.reset();
    while (const job_t *j = jobs.next()) {
        if (j->command_is_empty()) continue;
        for (const process_ptr_t &p : j->processes) {
            if (p->actual_cmd.empty()) continue;

            size_t offset = 0;
            if (match_pid(p->actual_cmd, proc, &offset)) {
                if (flags & EXPAND_FOR_COMPLETIONS) {
                    append_completion(completions, wcstring(p->actual_cmd, offset + wcslen(proc)),
                                      COMPLETE_CHILD_PROCESS_DESC, 0);
                } else {
                    append_completion(completions, to_string<long>(p->pid), L"", 0);
                    found = 1;
                }
            }
        }
    }

    return found;
}

/// Searches for a job with the specified job id, or a job or process which has the string \c proc
/// as a prefix of its commandline. Appends the name of the process as a completion in 'out'.
///
/// Otherwise, any job matching the specified string is matched, and the job pgid is returned. If no
/// job matches, all child processes are searched. If no child processes match, and <tt>fish</tt>
/// can understand the contents of the /proc filesystem, all the users processes are searched for
/// matches.
static void find_process(const wchar_t *proc, expand_flags_t flags,
                         std::vector<completion_t> *out) {
    if (!(flags & EXPAND_SKIP_JOBS)) {
        bool found = false;
        iothread_perform_on_main([&]() { found = find_job(proc, flags, out); });
        if (found) {
            return;
        }
    }

    // Iterate over all processes.
    wcstring process_name;
    pid_t process_pid;
    process_iterator_t iterator;
    while (iterator.next_process(&process_name, &process_pid)) {
        size_t offset = 0;
        if (match_pid(process_name, proc, &offset)) {
            if (flags & EXPAND_FOR_COMPLETIONS) {
                append_completion(out, process_name.c_str() + offset + wcslen(proc),
                                  COMPLETE_PROCESS_DESC, 0);
            } else {
                append_completion(out, to_string<long>(process_pid));
            }
        }
    }
}

/// Process id expansion.
static bool expand_pid(const wcstring &instr_with_sep, expand_flags_t flags,
                       std::vector<completion_t> *out, parse_error_list_t *errors) {
    // Hack. If there's no INTERNAL_SEP and no PROCESS_EXPAND, then there's nothing to do. Check out
    // this "null terminated string."
    const wchar_t some_chars[] = {INTERNAL_SEPARATOR, PROCESS_EXPAND, L'\0'};
    if (instr_with_sep.find_first_of(some_chars) == wcstring::npos) {
        // Nothing to do.
        append_completion(out, instr_with_sep);
        return true;
    }

    // expand_string calls us with internal separators in instr...sigh.
    wcstring instr = instr_with_sep;
    remove_internal_separator(&instr, false);

    if (instr.empty() || instr.at(0) != PROCESS_EXPAND) {
        // Not a process expansion.
        append_completion(out, instr);
        return true;
    }

    const wchar_t *const in = instr.c_str();

    // We know we are a process expansion now.
    assert(in[0] == PROCESS_EXPAND);

    if (flags & EXPAND_FOR_COMPLETIONS) {
        if (wcsncmp(in + 1, SELF_STR, wcslen(in + 1)) == 0) {
            append_completion(out, &SELF_STR[wcslen(in + 1)], COMPLETE_SELF_DESC, 0);
        } else if (wcsncmp(in + 1, LAST_STR, wcslen(in + 1)) == 0) {
            append_completion(out, &LAST_STR[wcslen(in + 1)], COMPLETE_LAST_DESC, 0);
        }
    } else {
        if (wcscmp((in + 1), SELF_STR) == 0) {
            append_completion(out, to_string<long>(getpid()));
            return true;
        }
        if (wcscmp((in + 1), LAST_STR) == 0) {
            if (proc_last_bg_pid > 0) {
                append_completion(out, to_string<long>(proc_last_bg_pid));
            }
            return true;
        }
    }

    // This is sort of crummy - find_process doesn't return any indication of success, so instead we
    // check to see if it inserted any completions.
    const size_t prev_count = out->size();
    find_process(in + 1, flags, out);

    if (prev_count == out->size() && !(flags & EXPAND_FOR_COMPLETIONS)) {
        // We failed to find anything.
        append_syntax_error(errors, 1, FAILED_EXPANSION_PROCESS_ERR_MSG,
                            escape_string(in + 1, ESCAPE_NO_QUOTED).c_str());
        return false;
    }

    return true;
}

/// Parse an array slicing specification Returns 0 on success. If a parse error occurs, returns the
/// index of the bad token. Note that 0 can never be a bad index because the string always starts
/// with [.
static size_t parse_slice(const wchar_t *in, wchar_t **end_ptr, std::vector<long> &idx,
                          std::vector<size_t> &source_positions, size_t array_size) {
    const long size = (long)array_size;
    size_t pos = 1;  // skip past the opening square bracket

    while (1) {
        while (iswspace(in[pos]) || (in[pos] == INTERNAL_SEPARATOR)) pos++;
        if (in[pos] == L']') {
            pos++;
            break;
        }

        const size_t i1_src_pos = pos;
        const wchar_t *end;
        long tmp = fish_wcstol(&in[pos], &end);
        // We don't test `*end` as is typically done because we expect it to not be the null char.
        // Ignore the case of errno==-1 because it means the end char wasn't the null char.
        if (errno > 0) {
            return pos;
        }
        // debug( 0, L"Push idx %d", tmp );

        long i1 = tmp > -1 ? tmp : size + tmp + 1;
        pos = end - in;
        while (in[pos] == INTERNAL_SEPARATOR) pos++;
        if (in[pos] == L'.' && in[pos + 1] == L'.') {
            pos += 2;
            while (in[pos] == INTERNAL_SEPARATOR) pos++;

            const size_t number_start = pos;
            long tmp1 = fish_wcstol(&in[pos], &end);
            // Ignore the case of errno==-1 because it means the end char wasn't the null char.
            if (errno > 0) {
                return pos;
            }
            pos = end - in;

            // debug( 0, L"Push range %d %d", tmp, tmp1 );
            long i2 = tmp1 > -1 ? tmp1 : size + tmp1 + 1;
            // Clamp to array size, but only when doing a range,
            // and only when just one is too high.
            if (i1 > size && i2 > size) {
                continue;
            }
            i1 = i1 < size ? i1 : size;
            i2 = i2 < size ? i2 : size;
            // debug( 0, L"Push range idx %d %d", i1, i2 );
            short direction = i2 < i1 ? -1 : 1;
            for (long jjj = i1; jjj * direction <= i2 * direction; jjj += direction) {
                // debug(0, L"Expand range [subst]: %i\n", jjj);
                idx.push_back(jjj);
                source_positions.push_back(number_start);
            }
            continue;
        }

        // debug( 0, L"Push idx %d", tmp );
        idx.push_back(i1);
        source_positions.push_back(i1_src_pos);
    }

    if (end_ptr) {
        *end_ptr = (wchar_t *)(in + pos);
    }

    return 0;
}

/// Expand all environment variables in the string *ptr.
///
/// This function is slow, fragile and complicated. There are lots of little corner cases, like
/// $$foo should do a double expansion, $foo$bar should not double expand bar, etc. Also, it's easy
/// to accidentally leak memory on array out of bounds errors an various other situations. All in
/// all, this function should be rewritten, split out into multiple logical units and carefully
/// tested. After that, it can probably be optimized to do fewer memory allocations, fewer string
/// scans and overall just less work. But until that happens, don't edit it unless you know exactly
/// what you are doing, and do proper testing afterwards.
///
/// This function operates on strings backwards, starting at last_idx.
///
/// Note: last_idx is considered to be where it previously finished procesisng. This means it
/// actually starts operating on last_idx-1. As such, to process a string fully, pass string.size()
/// as last_idx instead of string.size()-1.
static int expand_variables(const wcstring &instr, std::vector<completion_t> *out, long last_idx,
                            parse_error_list_t *errors) {
    const size_t insize = instr.size();

    // last_idx may be 1 past the end of the string, but no further.
    assert(last_idx >= 0 && (size_t)last_idx <= insize);

    if (last_idx == 0) {
        append_completion(out, instr);
        return true;
    }

    bool is_ok = true;
    bool empty = false;

    wcstring var_tmp;

    // List of indexes.
    std::vector<long> var_idx_list;

    // Parallel array of source positions of each index in the variable list.
    std::vector<size_t> var_pos_list;

    // CHECK( out, 0 );

    for (long i = last_idx - 1; (i >= 0) && is_ok && !empty; i--) {
        const wchar_t c = instr.at(i);
        if (c != VARIABLE_EXPAND && c != VARIABLE_EXPAND_SINGLE) {
            continue;
        }

        long var_len;
        int is_single = (c == VARIABLE_EXPAND_SINGLE);
        size_t start_pos = i + 1;
        size_t stop_pos = start_pos;

        while (stop_pos < insize) {
            const wchar_t nc = instr.at(stop_pos);
            if (nc == VARIABLE_EXPAND_EMPTY) {
                stop_pos++;
                break;
            }
            if (!valid_var_name_char(nc)) break;

            stop_pos++;
        }

        // fwprintf(stdout, L"Stop for '%c'\n", in[stop_pos]);
        var_len = stop_pos - start_pos;

        if (var_len == 0) {
            if (errors) {
                parse_util_expand_variable_error(instr, 0 /* global_token_pos */, i, errors);
            }

            is_ok = false;
            break;
        }

        var_tmp.append(instr, start_pos, var_len);
        env_var_t var_val;
        if (var_len == 1 && var_tmp[0] == VARIABLE_EXPAND_EMPTY) {
            var_val = env_var_t::missing_var();
        } else {
            var_val = expand_var(var_tmp.c_str());
        }

        if (!var_val.missing()) {
            int all_vars = 1;
            wcstring_list_t var_item_list;

            if (is_ok) {
                tokenize_variable_array(var_val, var_item_list);

                const size_t slice_start = stop_pos;
                if (slice_start < insize && instr.at(slice_start) == L'[') {
                    wchar_t *slice_end;
                    size_t bad_pos;
                    all_vars = 0;
                    const wchar_t *in = instr.c_str();
                    bad_pos = parse_slice(in + slice_start, &slice_end, var_idx_list, var_pos_list,
                                          var_item_list.size());
                    if (bad_pos != 0) {
                        append_syntax_error(errors, stop_pos + bad_pos, L"Invalid index value");
                        is_ok = false;
                        break;
                    }
                    stop_pos = (slice_end - in);
                }

                if (!all_vars) {
                    wcstring_list_t string_values(var_idx_list.size());
                    size_t k = 0;
                    for (size_t j = 0; j < var_idx_list.size(); j++) {
                        long tmp = var_idx_list.at(j);
                        // Check that we are within array bounds. If not, skip the element. Note:
                        // Negative indices (`echo $foo[-1]`) are already converted to positive ones
                        // here, So tmp < 1 means it's definitely not in.
                        if ((size_t)tmp > var_item_list.size() || tmp < 1) {
                            continue;
                        }
                        // Replace each index in var_idx_list inplace with the string value
                        // at the specified index.
                        string_values.at(k++) = var_item_list.at(tmp - 1);
                    }

                    // string_values is the new var_item_list. Resize to remove invalid elements.
                    string_values.resize(k);
                    var_item_list = std::move(string_values);
                }
            }

            if (!is_ok) {
                return is_ok;
            }

            if (is_single) {
                wcstring res(instr, 0, i);
                if (i > 0) {
                    if (instr.at(i - 1) != VARIABLE_EXPAND_SINGLE) {
                        res.push_back(INTERNAL_SEPARATOR);
                    } else if (var_item_list.empty() || var_item_list.front().empty()) {
                        // First expansion is empty, but we need to recursively expand.
                        res.push_back(VARIABLE_EXPAND_EMPTY);
                    }
                }

                for (size_t j = 0; j < var_item_list.size(); j++) {
                    const wcstring &next = var_item_list.at(j);
                    if (is_ok) {
                        if (j != 0) res.append(L" ");
                        res.append(next);
                    }
                }
                assert(stop_pos <= insize);
                res.append(instr, stop_pos, insize - stop_pos);
                is_ok &= expand_variables(res, out, i, errors);
            } else {
                for (size_t j = 0; j < var_item_list.size(); j++) {
                    const wcstring &next = var_item_list.at(j);
                    if (is_ok && i == 0 && stop_pos == insize) {
                        append_completion(out, next);
                    } else {
                        if (is_ok) {
                            wcstring new_in;
                            new_in.append(instr, 0, i);

                            if (i > 0) {
                                if (instr.at(i - 1) != VARIABLE_EXPAND) {
                                    new_in.push_back(INTERNAL_SEPARATOR);
                                } else if (next.empty()) {
                                    new_in.push_back(VARIABLE_EXPAND_EMPTY);
                                }
                            }
                            assert(stop_pos <= insize);
                            new_in.append(next);
                            new_in.append(instr, stop_pos, insize - stop_pos);
                            is_ok &= expand_variables(new_in, out, i, errors);
                        }
                    }
                }
            }

            return is_ok;
        }

        // Even with no value, we still need to parse out slice syntax. Behave as though we
        // had 1 value, so $foo[1] always works.
        const size_t slice_start = stop_pos;
        if (slice_start < insize && instr.at(slice_start) == L'[') {
            const wchar_t *in = instr.c_str();
            wchar_t *slice_end;
            size_t bad_pos;

            bad_pos = parse_slice(in + slice_start, &slice_end, var_idx_list, var_pos_list, 1);
            if (bad_pos != 0) {
                append_syntax_error(errors, stop_pos + bad_pos, L"Invalid index value");
                is_ok = 0;
                return is_ok;
            }
            stop_pos = (slice_end - in);
        }

        // Expand a non-existing variable.
        if (c == VARIABLE_EXPAND) {
            // Regular expansion, i.e. expand this argument to nothing.
            empty = true;
        } else {
            // Expansion to single argument.
            wcstring res;
            res.append(instr, 0, i);
            if (i > 0 && instr.at(i - 1) == VARIABLE_EXPAND_SINGLE) {
                res.push_back(VARIABLE_EXPAND_EMPTY);
            }
            assert(stop_pos <= insize);
            res.append(instr, stop_pos, insize - stop_pos);

            is_ok &= expand_variables(res, out, i, errors);
            return is_ok;
        }
    }

    if (!empty) {
        append_completion(out, instr);
    }

    return is_ok;
}

/// Perform bracket expansion.
static expand_error_t expand_brackets(const wcstring &instr, expand_flags_t flags,
                                      std::vector<completion_t> *out, parse_error_list_t *errors) {
    bool syntax_error = false;
    int bracket_count = 0;

    const wchar_t *bracket_begin = NULL, *bracket_end = NULL;
    const wchar_t *last_sep = NULL;

    const wchar_t *item_begin;
    size_t length_preceding_brackets, length_following_brackets, tot_len;

    const wchar_t *const in = instr.c_str();

    // Locate the first non-nested bracket pair.
    for (const wchar_t *pos = in; (*pos) && !syntax_error; pos++) {
        switch (*pos) {
            case BRACKET_BEGIN: {
                if (bracket_count == 0) bracket_begin = pos;
                bracket_count++;
                break;
            }
            case BRACKET_END: {
                bracket_count--;
                if (bracket_count < 0) {
                    syntax_error = true;
                } else if (bracket_count == 0) {
                    bracket_end = pos;
                }
                break;
            }
            case BRACKET_SEP: {
                if (bracket_count == 1) last_sep = pos;
                break;
            }
            default: {
                break;  // we ignore all other characters here
            }
        }
    }

    if (bracket_count > 0) {
        if (!(flags & EXPAND_FOR_COMPLETIONS)) {
            syntax_error = true;
        } else {
            // The user hasn't typed an end bracket yet; make one up and append it, then expand
            // that.
            wcstring mod;
            if (last_sep) {
                mod.append(in, bracket_begin - in + 1);
                mod.append(last_sep + 1);
                mod.push_back(BRACKET_END);
            } else {
                mod.append(in);
                mod.push_back(BRACKET_END);
            }

            // Note: this code looks very fishy, apparently it has never worked.
            return expand_brackets(mod, 1, out, errors);
        }
    }

    if (syntax_error) {
        append_syntax_error(errors, SOURCE_LOCATION_UNKNOWN, _(L"Mismatched brackets"));
        return EXPAND_ERROR;
    }

    if (bracket_begin == NULL) {
        append_completion(out, instr);
        return EXPAND_OK;
    }

    length_preceding_brackets = (bracket_begin - in);
    length_following_brackets = wcslen(bracket_end) - 1;
    tot_len = length_preceding_brackets + length_following_brackets;
    item_begin = bracket_begin + 1;
    for (const wchar_t *pos = (bracket_begin + 1); true; pos++) {
        if (bracket_count == 0 && ((*pos == BRACKET_SEP) || (pos == bracket_end))) {
            assert(pos >= item_begin);
            size_t item_len = pos - item_begin;

            wcstring whole_item;
            whole_item.reserve(tot_len + item_len + 2);
            whole_item.append(in, length_preceding_brackets);
            whole_item.append(item_begin, item_len);
            whole_item.append(bracket_end + 1);
            expand_brackets(whole_item, flags, out, errors);

            item_begin = pos + 1;
            if (pos == bracket_end) break;
        }

        if (*pos == BRACKET_BEGIN) {
            bracket_count++;
        }

        if (*pos == BRACKET_END) {
            bracket_count--;
        }
    }
    return EXPAND_OK;
}

/// Perform cmdsubst expansion.
static int expand_cmdsubst(const wcstring &input, std::vector<completion_t> *out_list,
                           parse_error_list_t *errors) {
    wchar_t *paran_begin = 0, *paran_end = 0;
    std::vector<wcstring> sub_res;
    size_t i, j;
    wchar_t *tail_begin = 0;

    const wchar_t *const in = input.c_str();

    int parse_ret;
    switch (parse_ret = parse_util_locate_cmdsubst(in, &paran_begin, &paran_end, false)) {
        case -1: {
            append_syntax_error(errors, SOURCE_LOCATION_UNKNOWN, L"Mismatched parenthesis");
            return 0;
        }
        case 0: {
            append_completion(out_list, input);
            return 1;
        }
        case 1: {
            break;
        }
        default: {
            DIE("unhandled parse_ret value");
            break;
        }
    }

    const wcstring subcmd(paran_begin + 1, paran_end - paran_begin - 1);

    if (exec_subshell(subcmd, sub_res, true /* do apply exit status */) == -1) {
        append_cmdsub_error(errors, SOURCE_LOCATION_UNKNOWN,
                            L"Unknown error while evaulating command substitution");
        return 0;
    }

    tail_begin = paran_end + 1;
    if (*tail_begin == L'[') {
        std::vector<long> slice_idx;
        std::vector<size_t> slice_source_positions;
        const wchar_t *const slice_begin = tail_begin;
        wchar_t *slice_end;
        size_t bad_pos;

        bad_pos =
            parse_slice(slice_begin, &slice_end, slice_idx, slice_source_positions, sub_res.size());
        if (bad_pos != 0) {
            append_syntax_error(errors, slice_begin - in + bad_pos, L"Invalid index value");
            return 0;
        }

        wcstring_list_t sub_res2;
        tail_begin = slice_end;
        for (i = 0; i < slice_idx.size(); i++) {
            long idx = slice_idx.at(i);
            if ((size_t)idx > sub_res.size() || idx < 1) {
                continue;
            }
            idx = idx - 1;

            sub_res2.push_back(sub_res.at(idx));
            // debug( 0, L"Pushing item '%ls' with index %d onto sliced result", al_get(
            // sub_res, idx ), idx );
            // sub_res[idx] = 0; // ??
        }
        sub_res = sub_res2;
    }

    // Recursively call ourselves to expand any remaining command substitutions. The result of this
    // recursive call using the tail of the string is inserted into the tail_expand array list
    std::vector<completion_t> tail_expand;
    expand_cmdsubst(tail_begin, &tail_expand, errors);  // TODO: offset error locations

    // Combine the result of the current command substitution with the result of the recursive tail
    // expansion.
    for (i = 0; i < sub_res.size(); i++) {
        const wcstring &sub_item = sub_res.at(i);
        const wcstring sub_item2 = escape_string(sub_item, 1);

        wcstring whole_item;

        for (j = 0; j < tail_expand.size(); j++) {
            whole_item.clear();
            const wcstring &tail_item = tail_expand.at(j).completion;

            // sb_append_substring( &whole_item, in, len1 );
            whole_item.append(in, paran_begin - in);

            // sb_append_char( &whole_item, INTERNAL_SEPARATOR );
            whole_item.push_back(INTERNAL_SEPARATOR);

            // sb_append_substring( &whole_item, sub_item2, item_len );
            whole_item.append(sub_item2);

            // sb_append_char( &whole_item, INTERNAL_SEPARATOR );
            whole_item.push_back(INTERNAL_SEPARATOR);

            // sb_append( &whole_item, tail_item );
            whole_item.append(tail_item);

            // al_push( out, whole_item.buff );
            append_completion(out_list, whole_item);
        }
    }

    return 1;
}

// Given that input[0] is HOME_DIRECTORY or tilde (ugh), return the user's name. Return the empty
// string if it is just a tilde. Also return by reference the index of the first character of the
// remaining part of the string (e.g. the subsequent slash).
static wcstring get_home_directory_name(const wcstring &input, size_t *out_tail_idx) {
    const wchar_t *const in = input.c_str();
    assert(in[0] == HOME_DIRECTORY || in[0] == L'~');
    size_t tail_idx;

    const wchar_t *name_end = wcschr(in, L'/');
    if (name_end) {
        tail_idx = name_end - in;
    } else {
        tail_idx = wcslen(in);
    }
    *out_tail_idx = tail_idx;
    return input.substr(1, tail_idx - 1);
}

/// Attempts tilde expansion of the string specified, modifying it in place.
static void expand_home_directory(wcstring &input) {
    if (!input.empty() && input.at(0) == HOME_DIRECTORY) {
        size_t tail_idx;
        wcstring username = get_home_directory_name(input, &tail_idx);

        bool tilde_error = false;
        env_var_t home;
        if (username.empty()) {
            // Current users home directory.
            home = env_get_string(L"HOME");
            // If home is either missing or empty,
            // treat it like an empty list.
            // $HOME is defined to be a _path_,
            // and those cannot be empty.
            //
            // We do not expand a string-empty var differently,
            // because that results in bogus paths
            // - ~/foo turns into /foo.
            if (home.missing_or_empty()) {
                input = ENV_NULL;
                return;
            }
            tail_idx = 1;
        } else {
            // Some other users home directory.
            std::string name_cstr = wcs2string(username);
            struct passwd userinfo;
            struct passwd *result;
            char buf[8192];
            int retval = getpwnam_r(name_cstr.c_str(), &userinfo, buf, sizeof(buf), &result);
            if (retval || !result) {
                tilde_error = true;
            } else {
                home = str2wcstring(userinfo.pw_dir);
            }
        }

        wchar_t *realhome = wrealpath(home, NULL);

        if (!tilde_error && realhome) {
            input.replace(input.begin(), input.begin() + tail_idx, realhome);
        } else {
            input[0] = L'~';
        }
        free((void *)realhome);
    }
}

void expand_tilde(wcstring &input) {
    // Avoid needless COW behavior by ensuring we use const at.
    const wcstring &tmp = input;
    if (!tmp.empty() && tmp.at(0) == L'~') {
        input.at(0) = HOME_DIRECTORY;
        expand_home_directory(input);
    }
}

static void unexpand_tildes(const wcstring &input, std::vector<completion_t> *completions) {
    // If input begins with tilde, then try to replace the corresponding string in each completion
    // with the tilde. If it does not, there's nothing to do.
    if (input.empty() || input.at(0) != L'~') return;

    // We only operate on completions that replace their contents. If we don't have any, we're done.
    // In particular, empty vectors are common.
    bool has_candidate_completion = false;
    for (size_t i = 0; i < completions->size(); i++) {
        if (completions->at(i).flags & COMPLETE_REPLACES_TOKEN) {
            has_candidate_completion = true;
            break;
        }
    }
    if (!has_candidate_completion) return;

    size_t tail_idx;
    wcstring username_with_tilde = L"~";
    username_with_tilde.append(get_home_directory_name(input, &tail_idx));

    // Expand username_with_tilde.
    wcstring home = username_with_tilde;
    expand_tilde(home);

    // Now for each completion that starts with home, replace it with the username_with_tilde.
    for (size_t i = 0; i < completions->size(); i++) {
        completion_t &comp = completions->at(i);
        if ((comp.flags & COMPLETE_REPLACES_TOKEN) &&
            string_prefixes_string(home, comp.completion)) {
            comp.completion.replace(0, home.size(), username_with_tilde);

            // And mark that our tilde is literal, so it doesn't try to escape it.
            comp.flags |= COMPLETE_DONT_ESCAPE_TILDES;
        }
    }
}

// If the given path contains the user's home directory, replace that with a tilde. We don't try to
// be smart about case insensitivity, etc.
wcstring replace_home_directory_with_tilde(const wcstring &str) {
    // Only absolute paths get this treatment.
    wcstring result = str;
    if (string_prefixes_string(L"/", result)) {
        wcstring home_directory = L"~";
        expand_tilde(home_directory);
        if (!string_suffixes_string(L"/", home_directory)) {
            home_directory.push_back(L'/');
        }

        // Now check if the home_directory prefixes the string.
        if (string_prefixes_string(home_directory, result)) {
            // Success
            result.replace(0, home_directory.size(), L"~/");
        }
    }
    return result;
}

/// Remove any internal separators. Also optionally convert wildcard characters to regular
/// equivalents. This is done to support EXPAND_SKIP_WILDCARDS.
static void remove_internal_separator(wcstring *str, bool conv) {
    // Remove all instances of INTERNAL_SEPARATOR.
    str->erase(std::remove(str->begin(), str->end(), (wchar_t)INTERNAL_SEPARATOR), str->end());

    // If conv is true, replace all instances of ANY_CHAR with '?', ANY_STRING with '*',
    // ANY_STRING_RECURSIVE with '*'.
    if (conv) {
        for (size_t idx = 0; idx < str->size(); idx++) {
            switch (str->at(idx)) {
                case ANY_CHAR: {
                    str->at(idx) = L'?';
                    break;
                }
                case ANY_STRING:
                case ANY_STRING_RECURSIVE: {
                    str->at(idx) = L'*';
                    break;
                }
                default: {
                    break;  // we ignore all other characters
                }
            }
        }
    }
}

/// A stage in string expansion is represented as a function that takes an input and returns a list
/// of output (by reference). We get flags and errors. It may return an error; if so expansion
/// halts.
typedef expand_error_t (*expand_stage_t)(const wcstring &input,           //!OCLINT(unused param)
                                         std::vector<completion_t> *out,  //!OCLINT(unused param)
                                         expand_flags_t flags,            //!OCLINT(unused param)
                                         parse_error_list_t *errors);     //!OCLINT(unused param)

static expand_error_t expand_stage_cmdsubst(const wcstring &input, std::vector<completion_t> *out,
                                            expand_flags_t flags, parse_error_list_t *errors) {
    expand_error_t result = EXPAND_OK;
    if (EXPAND_SKIP_CMDSUBST & flags) {
        wchar_t *begin, *end;
        if (parse_util_locate_cmdsubst(input.c_str(), &begin, &end, true) == 0) {
            append_completion(out, input);
        } else {
            append_cmdsub_error(errors, SOURCE_LOCATION_UNKNOWN,
                                L"Command substitutions not allowed");
            result = EXPAND_ERROR;
        }
    } else {
        int cmdsubst_ok = expand_cmdsubst(input, out, errors);
        if (!cmdsubst_ok) {
            result = EXPAND_ERROR;
        }
    }
    return result;
}

static expand_error_t expand_stage_variables(const wcstring &input, std::vector<completion_t> *out,
                                             expand_flags_t flags, parse_error_list_t *errors) {
    // We accept incomplete strings here, since complete uses expand_string to expand incomplete
    // strings from the commandline.
    wcstring next;
    unescape_string(input, &next, UNESCAPE_SPECIAL | UNESCAPE_INCOMPLETE);

    if (EXPAND_SKIP_VARIABLES & flags) {
        for (size_t i = 0; i < next.size(); i++) {
            if (next.at(i) == VARIABLE_EXPAND) {
                next[i] = L'$';
            }
        }
        append_completion(out, next);
    } else {
        if (!expand_variables(next, out, next.size(), errors)) {
            return EXPAND_ERROR;
        }
    }
    return EXPAND_OK;
}

static expand_error_t expand_stage_brackets(const wcstring &input, std::vector<completion_t> *out,
                                            expand_flags_t flags, parse_error_list_t *errors) {
    return expand_brackets(input, flags, out, errors);
}

static expand_error_t expand_stage_home_and_pid(const wcstring &input,
                                                std::vector<completion_t> *out,
                                                expand_flags_t flags, parse_error_list_t *errors) {
    wcstring next = input;

    if (!(EXPAND_SKIP_HOME_DIRECTORIES & flags)) {
        expand_home_directory(next);
    }

    if (flags & EXPAND_FOR_COMPLETIONS) {
        if (!next.empty() && next.at(0) == PROCESS_EXPAND) {
            expand_pid(next, flags, out, NULL);
            return EXPAND_OK;
        }
        append_completion(out, next);
    } else if (!expand_pid(next, flags, out, errors)) {
        return EXPAND_ERROR;
    }
    return EXPAND_OK;
}

static expand_error_t expand_stage_wildcards(const wcstring &input, std::vector<completion_t> *out,
                                             expand_flags_t flags, parse_error_list_t *errors) {
    UNUSED(errors);
    expand_error_t result = EXPAND_OK;
    wcstring path_to_expand = input;

    remove_internal_separator(&path_to_expand, flags & EXPAND_SKIP_WILDCARDS);
    const bool has_wildcard = wildcard_has(path_to_expand, true /* internal, i.e. ANY_CHAR */);

    if (has_wildcard && (flags & EXECUTABLES_ONLY)) {
        ;  // don't do wildcard expansion for executables, see issue #785
    } else if (((flags & EXPAND_FOR_COMPLETIONS) && (!(flags & EXPAND_SKIP_WILDCARDS))) ||
               has_wildcard) {
        // We either have a wildcard, or we don't have a wildcard but we're doing completion
        // expansion (so we want to get the completion of a file path). Note that if
        // EXPAND_SKIP_WILDCARDS is set, we stomped wildcards in remove_internal_separator above, so
        // there actually aren't any.
        //
        // So we're going to treat this input as a file path. Compute the "working directories",
        // which may be CDPATH if the special flag is set.
        const wcstring working_dir = env_get_pwd_slash();
        wcstring_list_t effective_working_dirs;
        bool for_cd = static_cast<bool>(flags & EXPAND_SPECIAL_FOR_CD);
        bool for_command = static_cast<bool>(flags & EXPAND_SPECIAL_FOR_COMMAND);
        if (!for_cd && !for_command) {
            // Common case.
            effective_working_dirs.push_back(working_dir);
        } else {
            // Either EXPAND_SPECIAL_FOR_COMMAND or EXPAND_SPECIAL_FOR_CD. We can handle these
            // mostly the same. There's the following differences:
            //
            // 1. An empty CDPATH should be treated as '.', but an empty PATH should be left empty
            // (no commands can be found). Also, an empty element in either is treated as '.' for
            // consistency with POSIX shells. Note that we rely on the latter by having called
            // `munge_colon_delimited_array()` for these special env vars. Thus we do not
            // special-case them here.
            //
            // 2. PATH is only "one level," while CDPATH is multiple levels. That is, input like
            // 'foo/bar' should resolve against CDPATH, but not PATH.
            //
            // In either case, we ignore the path if we start with ./ or /. Also ignore it if we are
            // doing command completion and we contain a slash, per IEEE 1003.1, chapter 8 under
            // PATH.
            if (string_prefixes_string(L"/", path_to_expand) ||
                string_prefixes_string(L"./", path_to_expand) ||
                string_prefixes_string(L"../", path_to_expand) ||
                (for_command && path_to_expand.find(L'/') != wcstring::npos)) {
                effective_working_dirs.push_back(working_dir);
            } else {
                // Get the PATH/CDPATH and cwd. Perhaps these should be passed in. An empty CDPATH
                // implies just the current directory, while an empty PATH is left empty.
                env_var_t paths = env_get_string(for_cd ? L"CDPATH" : L"PATH");
                if (paths.missing_or_empty()) paths = for_cd ? L"." : L"";

                // Tokenize it into path names.
                std::vector<wcstring> pathsv;
                tokenize_variable_array(paths, pathsv);
                for (auto next_path : pathsv) {
                    effective_working_dirs.push_back(
                        path_apply_working_directory(next_path, working_dir));
                }
            }
        }

        result = EXPAND_WILDCARD_NO_MATCH;
        std::vector<completion_t> expanded;
        for (size_t wd_idx = 0; wd_idx < effective_working_dirs.size(); wd_idx++) {
            int local_wc_res = wildcard_expand_string(
                path_to_expand, effective_working_dirs.at(wd_idx), flags, &expanded);
            if (local_wc_res > 0) {
                // Something matched,so overall we matched.
                result = EXPAND_WILDCARD_MATCH;
            } else if (local_wc_res < 0) {
                // Cancellation
                result = EXPAND_ERROR;
                break;
            }
        }

        std::sort(expanded.begin(), expanded.end(), completion_t::is_naturally_less_than);
        out->insert(out->end(), expanded.begin(), expanded.end());
    } else {
        // Can't fully justify this check. I think it's that SKIP_WILDCARDS is used when completing
        // to mean don't do file expansions, so if we're not doing file expansions, just drop this
        // completion on the floor.
        if (!(flags & EXPAND_FOR_COMPLETIONS)) {
            append_completion(out, path_to_expand);
        }
    }
    return result;
}

expand_error_t expand_string(const wcstring &input, std::vector<completion_t> *out_completions,
                             expand_flags_t flags, parse_error_list_t *errors) {
    // Early out. If we're not completing, and there's no magic in the input, we're done.
    if (!(flags & EXPAND_FOR_COMPLETIONS) && expand_is_clean(input)) {
        append_completion(out_completions, input);
        return EXPAND_OK;
    }

    // Our expansion stages.
    const expand_stage_t stages[] = {expand_stage_cmdsubst, expand_stage_variables,
                                     expand_stage_brackets, expand_stage_home_and_pid,
                                     expand_stage_wildcards};

    // Load up our single initial completion.
    std::vector<completion_t> completions, output_storage;
    append_completion(&completions, input);

    expand_error_t total_result = EXPAND_OK;
    for (size_t stage_idx = 0;
         total_result != EXPAND_ERROR && stage_idx < sizeof stages / sizeof *stages; stage_idx++) {
        for (size_t i = 0; total_result != EXPAND_ERROR && i < completions.size(); i++) {
            const wcstring &next = completions.at(i).completion;
            expand_error_t this_result = stages[stage_idx](next, &output_storage, flags, errors);
            // If this_result was no match, but total_result is that we have a match, then don't
            // change it.
            if (!(this_result == EXPAND_WILDCARD_NO_MATCH &&
                  total_result == EXPAND_WILDCARD_MATCH)) {
                total_result = this_result;
            }
        }

        // Output becomes our next stage's input.
        completions.swap(output_storage);
        output_storage.clear();
    }

    if (total_result != EXPAND_ERROR) {
        // Hack to un-expand tildes (see #647).
        if (!(flags & EXPAND_SKIP_HOME_DIRECTORIES)) {
            unexpand_tildes(input, &completions);
        }
        out_completions->insert(out_completions->end(), completions.begin(), completions.end());
    }
    return total_result;
}

bool expand_one(wcstring &string, expand_flags_t flags, parse_error_list_t *errors) {
    std::vector<completion_t> completions;

    if (!(flags & EXPAND_FOR_COMPLETIONS) && expand_is_clean(string)) {
        return true;
    }

    if (expand_string(string, &completions, flags | EXPAND_NO_DESCRIPTIONS, errors) &&
        completions.size() == 1) {
        string = completions.at(0).completion;
        return true;
    }
    return false;
}

// https://github.com/fish-shell/fish-shell/issues/367
//
// With them the Seed of Wisdom did I sow,
// And with my own hand labour'd it to grow:
// And this was all the Harvest that I reap'd---
// "I came like Water, and like Wind I go."

static std::string escape_single_quoted_hack_hack_hack_hack(const char *str) {
    std::string result;
    size_t len = strlen(str);
    result.reserve(len + 2);
    result.push_back('\'');
    for (size_t i = 0; i < len; i++) {
        char c = str[i];
        // Escape backslashes and single quotes only.
        if (c == '\\' || c == '\'') result.push_back('\\');
        result.push_back(c);
    }
    result.push_back('\'');
    return result;
}

bool fish_xdm_login_hack_hack_hack_hack(std::vector<std::string> *cmds, int argc,
                                        const char *const *argv) {
    if (!cmds || cmds->size() != 1) {
        return false;
    }

    bool result = false;
    const std::string &cmd = cmds->at(0);
    if (cmd == "exec \"${@}\"" || cmd == "exec \"$@\"") {
        // We're going to construct a new command that starts with exec, and then has the
        // remaining arguments escaped.
        std::string new_cmd = "exec";
        for (int i = 1; i < argc; i++) {
            const char *arg = argv[i];
            if (arg) {
                new_cmd.push_back(' ');
                new_cmd.append(escape_single_quoted_hack_hack_hack_hack(arg));
            }
        }

        cmds->at(0) = new_cmd;
        result = true;
    }
    return result;
}

bool expand_abbreviation(const wcstring &src, wcstring *output) {
    if (src.empty()) return false;

    // Get the abbreviations. Return false if we have none.
    env_var_t abbrs = env_get_string(USER_ABBREVIATIONS_VARIABLE_NAME);
    if (abbrs.missing_or_empty()) return false;

    bool result = false;
    std::vector<wcstring> abbrsv;
    tokenize_variable_array(abbrs, abbrsv);
    for (auto abbr : abbrsv) {
        // Abbreviation is expected to be of the form 'foo=bar' or 'foo bar'. Parse out the first =
        // or space. Silently skip on failure (no equals, or equals at the end or beginning). Try to
        // avoid copying any strings until we are sure this is a match.
        size_t equals_pos = abbr.find(L'=');
        size_t space_pos = abbr.find(L' ');
        size_t separator = mini(equals_pos, space_pos);
        if (separator == wcstring::npos || separator == 0 || separator + 1 == abbr.size()) continue;

        // Find the character just past the end of the command. Walk backwards, skipping spaces.
        size_t cmd_end = separator;
        while (cmd_end > 0 && iswspace(abbr.at(cmd_end - 1))) cmd_end--;

        // See if this command matches.
        if (abbr.compare(0, cmd_end, src) == 0) {
            // Success. Set output to everything past the end of the string.
            if (output != NULL) output->assign(abbr, separator + 1, wcstring::npos);

            result = true;
            break;
        }
    }
    return result;
}
