/// Insert all builtin names into list. void builtin_get_names(std::vector<completion_t> *list) { assert(list != NULL); list->reserve(list->size() + BUILTIN_COUNT); for (size_t i = 0; i < BUILTIN_COUNT; i++) { append_completion(list, builtin_datas[i].name); } }
void add_expansion_result(const wcstring &result) { // This function is only for the non-completions case. assert(!(this->flags & expand_flag::for_completions)); if (this->completion_set.insert(result).second) { append_completion(this->resolved_completions, result); this->did_add = true; } }
void add_expansion_result(const wcstring &result) { // This function is only for the non-completions case. assert(!static_cast<bool>(this->flags & EXPAND_FOR_COMPLETIONS)); //!OCLINT(multiple unary operator) if (this->completion_set.insert(result).second) { append_completion(this->resolved_completions, result); this->did_add = true; } }
/** Appends a completion to the completion list, if the string is missing from the set. */ static void insert_completion_if_missing(const wcstring &str, std::vector<completion_t> &out, std::set<wcstring> &completion_set) { if (completion_set.insert(str).second) append_completion(out, str); }
/** Matches the string against the wildcard, and if the wildcard is a possible completion of the string, the remainder of the string is inserted into the out vector. */ static bool wildcard_complete_internal(const wcstring &orig, const wchar_t *str, const wchar_t *wc, bool is_first, const wchar_t *desc, wcstring (*desc_func)(const wcstring &), std::vector<completion_t> &out, int flags ) { if( !wc || ! str || orig.empty()) { debug( 2, L"Got null string on line %d of file %s", __LINE__, __FILE__ ); return 0; } if( *wc == 0 && ( (str[0] != L'.') || (!is_first)) ) { wcstring out_completion; wcstring out_desc = (desc ? desc : L""); if( flags & COMPLETE_NO_CASE ) { out_completion = orig; } else { out_completion = str; } size_t complete_sep_loc = out_completion.find(PROG_COMPLETE_SEP); if (complete_sep_loc != wcstring::npos) { /* This completion has an embedded description, do not use the generic description */ out_desc.assign(out_completion, complete_sep_loc + 1, out_completion.size() - complete_sep_loc - 1); out_completion.resize(complete_sep_loc); } else { if( desc_func ) { /* A description generating function is specified, call it. If it returns something, use that as the description. */ wcstring func_desc = desc_func( orig ); if (! func_desc.empty()) out_desc = func_desc; } } /* Note: out_completion may be empty if the completion really is empty, e.g. tab-completing 'foo' when a file 'foo' exists. */ append_completion(out, out_completion, out_desc, flags); return true; } if( *wc == ANY_STRING ) { bool res=false; /* Ignore hidden file */ if( is_first && str[0] == L'.' ) return false; /* Try all submatches */ do { res = wildcard_complete_internal( orig, str, wc+1, 0, desc, desc_func, out, flags ); if (res) break; } while (*str++ != 0); return res; } else if( *wc == ANY_CHAR ) { return wildcard_complete_internal( orig, str+1, wc+1, 0, desc, desc_func, out, flags ); } else if( *wc == *str ) { return wildcard_complete_internal( orig, str+1, wc+1, 0, desc, desc_func, out, flags ); } else if( towlower(*wc) == towlower(*str) ) { return wildcard_complete_internal( orig, str+1, wc+1, 0, desc, desc_func, out, flags | COMPLETE_NO_CASE ); } return false; }
/** Matches the string against the wildcard, and if the wildcard is a possible completion of the string, the remainder of the string is inserted into the out vector. */ static bool wildcard_complete_internal(const wcstring &orig, const wchar_t *str, const wchar_t *wc, bool is_first, const wchar_t *desc, wcstring(*desc_func)(const wcstring &), std::vector<completion_t> *out, expand_flags_t expand_flags, complete_flags_t flags) { if (!wc || ! str || orig.empty()) { debug(2, L"Got null string on line %d of file %s", __LINE__, __FILE__); return 0; } bool has_match = false; string_fuzzy_match_t fuzzy_match(fuzzy_match_exact); const bool at_end_of_wildcard = (*wc == L'\0'); const wchar_t *completion_string = NULL; // Hack hack hack // Implement EXPAND_FUZZY_MATCH by short-circuiting everything if there are no remaining wildcards if ((expand_flags & EXPAND_FUZZY_MATCH) && ! at_end_of_wildcard && ! wildcard_has(wc, true)) { string_fuzzy_match_t local_fuzzy_match = string_fuzzy_match_string(wc, str); if (local_fuzzy_match.type != fuzzy_match_none) { has_match = true; fuzzy_match = local_fuzzy_match; /* If we're not a prefix or exact match, then we need to replace the token. Note that in this case we're not going to call ourselves recursively, so these modified flags won't "leak" except into the completion. */ if (match_type_requires_full_replacement(local_fuzzy_match.type)) { flags |= COMPLETE_REPLACES_TOKEN; completion_string = orig.c_str(); } else { /* Since we are not replacing the token, be careful to only store the part of the string after the wildcard */ size_t wc_len = wcslen(wc); assert(wcslen(str) >= wc_len); completion_string = str + wcslen(wc); } } } /* Maybe we satisfied the wildcard normally */ if (! has_match) { bool file_has_leading_dot = (is_first && str[0] == L'.'); if (at_end_of_wildcard && ! file_has_leading_dot) { has_match = true; if (flags & COMPLETE_REPLACES_TOKEN) { completion_string = orig.c_str(); } else { completion_string = str; } } } if (has_match) { /* Wildcard complete */ assert(completion_string != NULL); wcstring out_completion = completion_string; wcstring out_desc = (desc ? desc : L""); size_t complete_sep_loc = out_completion.find(PROG_COMPLETE_SEP); if (complete_sep_loc != wcstring::npos) { /* This completion has an embedded description, do not use the generic description */ out_desc.assign(out_completion, complete_sep_loc + 1, out_completion.size() - complete_sep_loc - 1); out_completion.resize(complete_sep_loc); } else { if (desc_func && !(expand_flags & EXPAND_NO_DESCRIPTIONS)) { /* A description generating function is specified, call it. If it returns something, use that as the description. */ wcstring func_desc = desc_func(orig); if (! func_desc.empty()) out_desc = func_desc; } } /* Note: out_completion may be empty if the completion really is empty, e.g. tab-completing 'foo' when a file 'foo' exists. */ append_completion(out, out_completion, out_desc, flags, fuzzy_match); return true; } if (*wc == ANY_STRING) { bool res=false; /* Ignore hidden file */ if (is_first && str[0] == L'.') return false; /* Try all submatches */ for (size_t i=0; str[i] != L'\0'; i++) { const size_t before_count = out->size(); if (wildcard_complete_internal(orig, str + i, wc+1, false, desc, desc_func, out, expand_flags, flags)) { res = true; /* #929: if the recursive call gives us a prefix match, just stop. This is sloppy - what we really want to do is say, once we've seen a match of a particular type, ignore all matches of that type further down the string, such that the wildcard produces the "minimal match." */ bool has_prefix_match = false; const size_t after_count = out->size(); for (size_t j = before_count; j < after_count; j++) { if (out->at(j).match.type <= fuzzy_match_prefix) { has_prefix_match = true; break; } } if (has_prefix_match) break; } } return res; } else if (*wc == ANY_CHAR || *wc == *str) { return wildcard_complete_internal(orig, str+1, wc+1, false, desc, desc_func, out, expand_flags, flags); } else if (towlower(*wc) == towlower(*str)) { return wildcard_complete_internal(orig, str+1, wc+1, false, desc, desc_func, out, expand_flags, flags | COMPLETE_REPLACES_TOKEN); } return false; }
static int expand_variables_internal(parser_t &parser, wchar_t * const in, std::vector<completion_t> &out, long last_idx) { int is_ok= 1; int empty=0; wcstring var_tmp; std::vector<long> var_idx_list; // CHECK( out, 0 ); for (long i=last_idx; (i>=0) && is_ok && !empty; i--) { const wchar_t c = in[i]; if ((c == VARIABLE_EXPAND) || (c == VARIABLE_EXPAND_SINGLE)) { long start_pos = i+1; long stop_pos; long var_len; int is_single = (c==VARIABLE_EXPAND_SINGLE); stop_pos = start_pos; while (1) { if (!(in[stop_pos ])) break; if (!(iswalnum(in[stop_pos]) || (wcschr(L"_", in[stop_pos])!= 0))) break; stop_pos++; } /* printf( "Stop for '%c'\n", in[stop_pos]);*/ var_len = stop_pos - start_pos; if (var_len == 0) { expand_variable_error(parser, in, stop_pos-1, -1); is_ok = 0; break; } var_tmp.append(in + start_pos, var_len); env_var_t 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.c_str(), var_item_list); if (in[stop_pos] == L'[') { wchar_t *slice_end; all_vars=0; if (parse_slice(in + stop_pos, &slice_end, var_idx_list, var_item_list.size())) { parser.error(SYNTAX_ERROR, -1, L"Invalid index value"); is_ok = 0; break; } stop_pos = (slice_end-in); } if (!all_vars) { wcstring_list_t string_values(var_idx_list.size()); 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, truncate the list to exit. */ if (tmp < 1 || (size_t)tmp > var_item_list.size()) { parser.error(SYNTAX_ERROR, -1, ARRAY_BOUNDS_ERR); is_ok=0; var_idx_list.resize(j); break; } else { /* Replace each index in var_idx_list inplace with the string value at the specified index */ //al_set( var_idx_list, j, wcsdup((const wchar_t *)al_get( &var_item_list, tmp-1 ) ) ); string_values.at(j) = var_item_list.at(tmp-1); } } // string_values is the new var_item_list var_item_list.swap(string_values); } } if (is_ok) { if (is_single) { in[i]=0; wcstring res = in; res.push_back(INTERNAL_SEPARATOR); 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); } } res.append(in + stop_pos); is_ok &= expand_variables2(parser, res, out, i); } 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) && (!in[stop_pos])) { append_completion(out, next); } else { if (is_ok) { wcstring new_in; if (start_pos > 0) new_in.append(in, start_pos - 1); // at this point new_in.size() is start_pos - 1 if (start_pos>1 && new_in[start_pos-2]!=VARIABLE_EXPAND) { new_in.push_back(INTERNAL_SEPARATOR); } new_in.append(next); new_in.append(in + stop_pos); is_ok &= expand_variables2(parser, new_in, out, i); } } } } } return is_ok; } else { /* Expand a non-existing variable */ if (c == VARIABLE_EXPAND) { /* Regular expansion, i.e. expand this argument to nothing */ empty = 1; } else { /* Expansion to single argument. */ wcstring res; in[i] = 0; res.append(in); res.append(in + stop_pos); is_ok &= expand_variables2(parser, res, out, i); return is_ok; } } } } if (!empty) { append_completion(out, in); } return is_ok; }
/** Process id expansion */ static int expand_pid(const wcstring &instr_with_sep, expand_flags_t flags, std::vector<completion_t> &out) { /* 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) { append_completion(out, instr); return 1; } const wchar_t * const in = instr.c_str(); if (flags & ACCEPT_INCOMPLETE) { 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 1; } if (wcscmp((in+1), LAST_STR)==0) { if (proc_last_bg_pid > 0) { append_completion(out, to_string<long>(proc_last_bg_pid)); } return 1; } } size_t prev = out.size(); if (!find_process(in+1, flags, out)) return 0; if (prev == out.size()) { if (!(flags & ACCEPT_INCOMPLETE)) { return 0; } } return 1; }
static int find_process(const wchar_t *proc, expand_flags_t flags, std::vector<completion_t> &out) { int found = 0; if (!(flags & EXPAND_SKIP_JOBS)) { ASSERT_IS_MAIN_THREAD(); const job_t *j; if (iswnumeric(proc) || (wcslen(proc)==0)) { /* This is a numeric job string, like '%2' */ if (flags & ACCEPT_INCOMPLETE) { job_iterator_t jobs; while ((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(out, jid+wcslen(proc), desc_buff, 0); } } } else { int jid; wchar_t *end; errno = 0; jid = fish_wcstoi(proc, &end, 10); if (jid > 0 && !errno && !*end) { j = job_get(jid); if ((j != 0) && (j->command_wcstr() != 0)) { { append_completion(out, to_string<long>(j->pgid)); found = 1; } } } } } if (found) return 1; job_iterator_t jobs; while ((j = jobs.next())) { if (j->command_is_empty()) continue; size_t offset; if (match_pid(j->command(), proc, flags, &offset)) { if (flags & ACCEPT_INCOMPLETE) { append_completion(out, j->command_wcstr() + offset + wcslen(proc), COMPLETE_JOB_DESC, 0); } else { append_completion(out, to_string<long>(j->pgid)); found = 1; } } } if (found) { return 1; } jobs.reset(); while ((j = jobs.next())) { process_t *p; if (j->command_is_empty()) continue; for (p=j->first_process; p; p=p->next) { if (p->actual_cmd.empty()) continue; size_t offset; if (match_pid(p->actual_cmd, proc, flags, &offset)) { if (flags & ACCEPT_INCOMPLETE) { append_completion(out, wcstring(p->actual_cmd, offset + wcslen(proc)), COMPLETE_CHILD_PROCESS_DESC, 0); } else { append_completion(out, to_string<long>(p->pid), L"", 0); found = 1; } } } } if (found) { return 1; } } /* 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; if (match_pid(process_name, proc, flags, &offset)) { if (flags & ACCEPT_INCOMPLETE) { append_completion(out, process_name.c_str() + offset + wcslen(proc), COMPLETE_PROCESS_DESC, 0); } else { append_completion(out, to_string<long>(process_pid)); } } } return 1; }
/** Perform bracket expansion */ static int expand_brackets(parser_t &parser, const wcstring &instr, int flags, std::vector<completion_t> &out) { 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; } } } if (bracket_count > 0) { if (!(flags & ACCEPT_INCOMPLETE)) { 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); } return expand_brackets(parser, mod, 1, out); } } if (syntax_error) { parser.error(SYNTAX_ERROR, -1, _(L"Mismatched brackets")); return 0; } if (bracket_begin == NULL) { append_completion(out, instr); return 1; } 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) { if ((*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(parser, whole_item, flags, out); item_begin = pos+1; if (pos == bracket_end) break; } } if (*pos == BRACKET_BEGIN) { bracket_count++; } if (*pos == BRACKET_END) { bracket_count--; } } return 1; }
/// Matches the string against the wildcard, and if the wildcard is a possible completion of the /// string, the remainder of the string is inserted into the out vector. /// /// We ignore ANY_STRING_RECURSIVE here. The consequence is that you cannot tab complete ** /// wildcards. This is historic behavior. static bool wildcard_complete_internal(const wchar_t *str, const wchar_t *wc, const wc_complete_pack_t ¶ms, complete_flags_t flags, std::vector<completion_t> *out, bool is_first_call = false) { assert(str != NULL); assert(wc != NULL); // Maybe early out for hidden files. We require that the wildcard match these exactly (i.e. a // dot); ANY_STRING not allowed. if (is_first_call && str[0] == L'.' && wc[0] != L'.') { return false; } // Locate the next wildcard character position, e.g. ANY_CHAR or ANY_STRING. const size_t next_wc_char_pos = wildcard_find(wc); // Maybe we have no more wildcards at all. This includes the empty string. if (next_wc_char_pos == wcstring::npos) { string_fuzzy_match_t match = string_fuzzy_match_string(wc, str); // If we're allowing fuzzy match, any match is OK. Otherwise we require a prefix match. bool match_acceptable; if (params.expand_flags & EXPAND_FUZZY_MATCH) { match_acceptable = match.type != fuzzy_match_none; } else { match_acceptable = match_type_shares_prefix(match.type); } if (!match_acceptable || out == NULL) { return match_acceptable; } // Wildcard complete. bool full_replacement = match_type_requires_full_replacement(match.type) || (flags & COMPLETE_REPLACES_TOKEN); // If we are not replacing the token, be careful to only store the part of the string after // the wildcard. assert(!full_replacement || std::wcslen(wc) <= std::wcslen(str)); wcstring out_completion = full_replacement ? params.orig : str + std::wcslen(wc); wcstring out_desc = resolve_description(params.orig, &out_completion, params.expand_flags, params.desc_func); // Note: out_completion may be empty if the completion really is empty, e.g. tab-completing // 'foo' when a file 'foo' exists. complete_flags_t local_flags = flags | (full_replacement ? COMPLETE_REPLACES_TOKEN : 0); append_completion(out, out_completion, out_desc, local_flags, match); return match_acceptable; } else if (next_wc_char_pos > 0) { // Here we have a non-wildcard prefix. Note that we don't do fuzzy matching for stuff before // a wildcard, so just do case comparison and then recurse. if (std::wcsncmp(str, wc, next_wc_char_pos) == 0) { // Normal match. return wildcard_complete_internal(str + next_wc_char_pos, wc + next_wc_char_pos, params, flags, out); } if (wcsncasecmp(str, wc, next_wc_char_pos) == 0) { // Case insensitive match. return wildcard_complete_internal(str + next_wc_char_pos, wc + next_wc_char_pos, params, flags | COMPLETE_REPLACES_TOKEN, out); } return false; // no match } // Our first character is a wildcard. assert(next_wc_char_pos == 0); switch (wc[0]) { case ANY_CHAR: { if (str[0] == L'\0') { return false; } return wildcard_complete_internal(str + 1, wc + 1, params, flags, out); } case ANY_STRING: { // Hackish. If this is the last character of the wildcard, then just complete with // the empty string. This fixes cases like "f*<tab>" -> "f*o". if (wc[1] == L'\0') { return wildcard_complete_internal(L"", L"", params, flags, out); } // Try all submatches. Issue #929: if the recursive call gives us a prefix match, // just stop. This is sloppy - what we really want to do is say, once we've seen a // match of a particular type, ignore all matches of that type further down the // string, such that the wildcard produces the "minimal match.". bool has_match = false; for (size_t i = 0; str[i] != L'\0'; i++) { const size_t before_count = out ? out->size() : 0; if (wildcard_complete_internal(str + i, wc + 1, params, flags, out)) { // We found a match. has_match = true; // If out is NULL, we don't care about the actual matches. If out is not // NULL but we have a prefix match, stop there. if (out == NULL || has_prefix_match(out, before_count)) { break; } } } return has_match; } case ANY_STRING_RECURSIVE: { // We don't even try with this one. return false; } default: { DIE("unreachable code reached"); break; } } DIE("unreachable code reached"); }
/// Perform brace expansion. static expand_error_t expand_braces(const wcstring &instr, expand_flags_t flags, std::vector<completion_t> *out, parse_error_list_t *errors) { bool syntax_error = false; int brace_count = 0; const wchar_t *brace_begin = NULL, *brace_end = NULL; const wchar_t *last_sep = NULL; const wchar_t *item_begin; size_t length_preceding_braces, length_following_braces, tot_len; const wchar_t *const in = instr.c_str(); // Locate the first non-nested brace pair. for (const wchar_t *pos = in; (*pos) && !syntax_error; pos++) { switch (*pos) { case BRACE_BEGIN: { if (brace_count == 0) brace_begin = pos; brace_count++; break; } case BRACE_END: { brace_count--; if (brace_count < 0) { syntax_error = true; } else if (brace_count == 0) { brace_end = pos; } break; } case BRACE_SEP: { if (brace_count == 1) last_sep = pos; break; } default: { break; // we ignore all other characters here } } } if (brace_count > 0) { if (!(flags & EXPAND_FOR_COMPLETIONS)) { syntax_error = true; } else { // The user hasn't typed an end brace yet; make one up and append it, then expand // that. wcstring mod; if (last_sep) { mod.append(in, brace_begin - in + 1); mod.append(last_sep + 1); mod.push_back(BRACE_END); } else { mod.append(in); mod.push_back(BRACE_END); } // Note: this code looks very fishy, apparently it has never worked. return expand_braces(mod, 1, out, errors); } } // Expand a literal "{}" to itself because it is useless otherwise, // and this eases e.g. `find -exec {}`. See #1109. if (brace_begin + 1 == brace_end) { wcstring newstr = instr; newstr.at(brace_begin - in) = L'{'; newstr.at(brace_end - in) = L'}'; return expand_braces(newstr, flags, out, errors); } if (syntax_error) { append_syntax_error(errors, SOURCE_LOCATION_UNKNOWN, _(L"Mismatched braces")); return EXPAND_ERROR; } if (brace_begin == NULL) { append_completion(out, instr); return EXPAND_OK; } length_preceding_braces = (brace_begin - in); length_following_braces = wcslen(brace_end) - 1; tot_len = length_preceding_braces + length_following_braces; item_begin = brace_begin + 1; for (const wchar_t *pos = (brace_begin + 1); true; pos++) { if (brace_count == 0 && ((*pos == BRACE_SEP) || (pos == brace_end))) { assert(pos >= item_begin); size_t item_len = pos - item_begin; wcstring item = wcstring(item_begin, item_len); item = trim(item, (const wchar_t[]) { BRACE_SPACE, L'\0' }); for (auto &c : item) { if (c == BRACE_SPACE) { c = ' '; } } wcstring whole_item; whole_item.reserve(tot_len + item_len + 2); whole_item.append(in, length_preceding_braces); whole_item.append(item.begin(), item.end()); whole_item.append(brace_end + 1); expand_braces(whole_item, flags, out, errors); item_begin = pos + 1; if (pos == brace_end) break; }
/// 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 bool expand_variables(const wcstring &instr, std::vector<completion_t> *out, size_t 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 <= insize && "Invalid last_idx"); if (last_idx == 0) { append_completion(out, instr); return true; } // Locate the last VARIABLE_EXPAND or VARIABLE_EXPAND_SINGLE bool is_single = false; size_t varexp_char_idx = last_idx; while (varexp_char_idx--) { const wchar_t c = instr.at(varexp_char_idx); if (c == VARIABLE_EXPAND || c == VARIABLE_EXPAND_SINGLE) { is_single = (c == VARIABLE_EXPAND_SINGLE); break; } } if (varexp_char_idx >= instr.size()) { // No variable expand char, we're done. append_completion(out, instr); return true; } // Get the variable name. const size_t var_name_start = varexp_char_idx + 1; size_t var_name_stop = var_name_start; while (var_name_stop < insize) { const wchar_t nc = instr.at(var_name_stop); if (nc == VARIABLE_EXPAND_EMPTY) { var_name_stop++; break; } if (!valid_var_name_char(nc)) break; var_name_stop++; } assert(var_name_stop >= var_name_start && "Bogus variable name indexes"); const size_t var_name_len = var_name_stop - var_name_start; // It's an error if the name is empty. if (var_name_len == 0) { if (errors) { parse_util_expand_variable_error(instr, 0 /* global_token_pos */, varexp_char_idx, errors); } return false; } // Get the variable name as a string, then try to get the variable from env. const wcstring var_name(instr, var_name_start, var_name_len); // Do a dirty hack to make sliced history fast (#4650). We expand from either a variable, or a // history_t. Note that "history" is read only in env.cpp so it's safe to special-case it in // this way (it cannot be shadowed, etc). history_t *history = nullptr; maybe_t<env_var_t> var{}; if (var_name == L"history") { // We do this only on the main thread, matching env.cpp. if (is_main_thread()) { history = reader_get_history(); } } else if (var_name != wcstring{VARIABLE_EXPAND_EMPTY}) { var = env_get(var_name); } // Parse out any following slice. // Record the end of the variable name and any following slice. size_t var_name_and_slice_stop = var_name_stop; bool all_values = true; const size_t slice_start = var_name_stop; // List of indexes, and parallel array of source positions of each index in the variable list. std::vector<long> var_idx_list; std::vector<size_t> var_pos_list; if (slice_start < insize && instr.at(slice_start) == L'[') { all_values = false; const wchar_t *in = instr.c_str(); wchar_t *slice_end; // If a variable is missing, behave as though we have one value, so that $var[1] always // works. size_t effective_val_count = 1; if (var) { effective_val_count = var->as_list().size(); } else if (history) { effective_val_count = history->size(); } size_t bad_pos = parse_slice(in + slice_start, &slice_end, var_idx_list, var_pos_list, effective_val_count); if (bad_pos != 0) { append_syntax_error(errors, slice_start + bad_pos, L"Invalid index value"); return false; } var_name_and_slice_stop = (slice_end - in); } if (!var && !history) { // Expanding a non-existent variable. if (!is_single) { // Normal expansions of missing variables successfully expand to nothing. return true; } else { // Expansion to single argument. // Replace the variable name and slice with VARIABLE_EXPAND_EMPTY. wcstring res(instr, 0, varexp_char_idx); if (!res.empty() && res.back() == VARIABLE_EXPAND_SINGLE) { res.push_back(VARIABLE_EXPAND_EMPTY); } res.append(instr, var_name_and_slice_stop, wcstring::npos); return expand_variables(res, out, varexp_char_idx, errors); } } // Ok, we have a variable or a history. Let's expand it. // Start by respecting the sliced elements. assert((var || history) && "Should have variable or history here"); wcstring_list_t var_item_list; if (all_values) { if (history) { history->get_history(var_item_list); } else { var->to_list(var_item_list); } } else { // We have to respect the slice. if (history) { // Ask history to map indexes to item strings. // Note this may have missing entries for out-of-bounds. auto item_map = history->items_at_indexes(var_idx_list); for (long item_index : var_idx_list) { auto iter = item_map.find(item_index); if (iter != item_map.end()) { var_item_list.push_back(iter->second); } } } else { const wcstring_list_t &all_var_items = var->as_list(); for (long item_index : var_idx_list) { // 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. // Note we are 1-based. if (item_index >= 1 && size_t(item_index) <= all_var_items.size()) { var_item_list.push_back(all_var_items.at(item_index - 1)); } } } } if (is_single) { wcstring res(instr, 0, varexp_char_idx); if (!res.empty()) { if (res.back() != 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); } } // Append all entries in var_item_list, separated by spaces. // Remove the last space. if (!var_item_list.empty()) { for (const wcstring &item : var_item_list) { res.append(item); res.push_back(L' '); } res.pop_back(); } res.append(instr, var_name_and_slice_stop, wcstring::npos); return expand_variables(res, out, varexp_char_idx, errors); } else { // Normal cartesian-product expansion. for (const wcstring &item : var_item_list) { if (varexp_char_idx == 0 && var_name_and_slice_stop == insize) { append_completion(out, item); } else { wcstring new_in(instr, 0, varexp_char_idx); if (!new_in.empty()) { if (new_in.back() != VARIABLE_EXPAND) { new_in.push_back(INTERNAL_SEPARATOR); } else if (item.empty()) { new_in.push_back(VARIABLE_EXPAND_EMPTY); } } new_in.append(item); new_in.append(instr, var_name_and_slice_stop, wcstring::npos); if (!expand_variables(new_in, out, varexp_char_idx, errors)) { return false; } } } } return true; }