/// Functions related to tab-completion. /// /// These functions are used for storing and retrieving tab-completion data, as well as for /// performing tab-completion. /// #include "config.h" // IWYU pragma: keep #include "complete.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "autoload.h" #include "builtin.h" #include "common.h" #include "env.h" #include "exec.h" #include "expand.h" #include "fallback.h" // IWYU pragma: keep #include "function.h" #include "history.h" #include "iothread.h" #include "parse_constants.h" #include "parse_util.h" #include "parser.h" #include "parser_keywords.h" #include "path.h" #include "proc.h" #include "reader.h" #include "util.h" #include "wcstringutil.h" #include "wildcard.h" #include "wutil.h" // IWYU pragma: keep // Completion description strings, mostly for different types of files, such as sockets, block // devices, etc. // // There are a few more completion description strings defined in expand.c. Maybe all completion // description strings should be defined in the same file? /// Description for ~USER completion. #define COMPLETE_USER_DESC _(L"Home for %ls") /// Description for short variables. The value is concatenated to this description. #define COMPLETE_VAR_DESC_VAL _(L"Variable: %ls") /// Description for abbreviations. #define ABBR_DESC _(L"Abbreviation: %ls") /// The special cased translation macro for completions. The empty string needs to be special cased, /// since it can occur, and should not be translated. (Gettext returns the version information as /// the response). #ifdef HAVE_GETTEXT static const wchar_t *C_(const wcstring &s) { return s.empty() ? L"" : wgettext(s.c_str()).c_str(); } #else static const wcstring &C_(const wcstring &s) { return s; } #endif /// Struct describing a completion option entry. /// /// If option is empty, the comp field must not be empty and contains a list of arguments to the /// command. /// /// The type field determines how the option is to be interpreted: either empty (args_only) or /// short, single-long ("old") or double-long ("GNU"). An invariant is that the option is empty if /// and only if the type is args_only. /// /// If option is non-empty, it specifies a switch for the command. If \c comp is also not empty, it /// contains a list of non-switch arguments that may only follow directly after the specified /// switch. namespace { struct complete_entry_opt_t { // Text of the option (like 'foo'). wcstring option; // Type of the option: args-oly, short, single_long, or double_long. complete_option_type_t type; // Arguments to the option. wcstring comp; // Description of the completion. wcstring desc; // Conditions under which to use the option. wcstring_list_t condition; // Determines how completions should be performed on the argument after the switch. completion_mode_t result_mode; // Completion flags. complete_flags_t flags; wcstring localized_desc() const { return C_(desc); } size_t expected_dash_count() const { switch (this->type) { case option_type_args_only: return 0; case option_type_short: case option_type_single_long: return 1; case option_type_double_long: return 2; } DIE("unreachable"); } }; /// Last value used in the order field of completion_entry_t. static relaxed_atomic_t k_complete_order{0}; /// Struct describing a command completion. using option_list_t = std::forward_list; class completion_entry_t { public: /// List of all options. option_list_t options; /// Order for when this completion was created. This aids in outputting completions sorted by /// time. const unsigned int order; /// Getters for option list. const option_list_t &get_options() const { return options; } /// Adds an option. void add_option(complete_entry_opt_t &&opt) { options.push_front(std::move(opt)); } /// Remove all completion options in the specified entry that match the specified short / long /// option strings. Returns true if it is now empty and should be deleted, false if it's not /// empty. bool remove_option(const wcstring &option, complete_option_type_t type) { this->options.remove_if([&](const complete_entry_opt_t &opt) { return opt.option == option && opt.type == type; }); return this->options.empty(); } completion_entry_t() : order(++k_complete_order) {} }; } // namespace /// Set of all completion entries. Keyed by the command name, and whether it is a path. using completion_key_t = std::pair; using completion_entry_map_t = std::map; static owning_lock s_completion_map; /// Completion "wrapper" support. The map goes from wrapping-command to wrapped-command-list. using wrapper_map_t = std::unordered_map; static owning_lock wrapper_map; description_func_t const_desc(const wcstring &s) { return [=](const wcstring &ignored) { UNUSED(ignored); return s; }; } /// Clear the COMPLETE_AUTO_SPACE flag, and set COMPLETE_NO_SPACE appropriately depending on the /// suffix of the string. static complete_flags_t resolve_auto_space(const wcstring &comp, complete_flags_t flags) { complete_flags_t new_flags = flags; if (flags & COMPLETE_AUTO_SPACE) { new_flags &= ~COMPLETE_AUTO_SPACE; size_t len = comp.size(); if (len > 0 && (std::wcschr(L"/=@:.,-", comp.at(len - 1)) != nullptr)) new_flags |= COMPLETE_NO_SPACE; } return new_flags; } /// completion_t functions. Note that the constructor resolves flags! completion_t::completion_t(wcstring comp, wcstring desc, string_fuzzy_match_t mat, complete_flags_t flags_val) : completion(std::move(comp)), description(std::move(desc)), match(mat), flags(resolve_auto_space(completion, flags_val)) {} completion_t::completion_t(const completion_t &) = default; completion_t::completion_t(completion_t &&) = default; completion_t &completion_t::operator=(const completion_t &) = default; completion_t &completion_t::operator=(completion_t &&) = default; completion_t::~completion_t() = default; __attribute__((always_inline)) static inline bool natural_compare_completions( const completion_t &a, const completion_t &b) { // For this to work, stable_sort must be used because results aren't interchangeable. if (a.flags & b.flags & COMPLETE_DONT_SORT) { // Both completions are from a source with the --keep-order flag. return false; } return wcsfilecmp(a.completion.c_str(), b.completion.c_str()) < 0; } void completion_t::prepend_token_prefix(const wcstring &prefix) { if (this->flags & COMPLETE_REPLACES_TOKEN) { this->completion.insert(0, prefix); } } bool completion_receiver_t::add(completion_t &&comp) { if (this->completions_.size() >= limit_) { return false; } this->completions_.push_back(std::move(comp)); return true; } bool completion_receiver_t::add(wcstring &&comp) { return this->add(std::move(comp), wcstring{}); } bool completion_receiver_t::add(wcstring &&comp, wcstring desc, complete_flags_t flags, string_fuzzy_match_t match) { return this->add(completion_t(std::move(comp), std::move(desc), match, flags)); } bool completion_receiver_t::add_list(completion_list_t &&lst) { size_t total_size = lst.size() + this->size(); if (total_size < this->size() || total_size > limit_) { return false; } if (completions_.empty()) { completions_ = std::move(lst); } else { completions_.reserve(completions_.size() + lst.size()); std::move(lst.begin(), lst.end(), std::back_inserter(completions_)); } return true; } completion_list_t completion_receiver_t::take() { completion_list_t res{}; std::swap(res, this->completions_); return res; } completion_receiver_t completion_receiver_t::subreceiver() const { size_t remaining_capacity = limit_ < size() ? 0 : limit_ - size(); return completion_receiver_t(remaining_capacity); } // If these functions aren't force inlined, it is actually faster to call // stable_sort twice rather than to iterate once performing all comparisons in one go! __attribute__((always_inline)) static inline bool compare_completions_by_duplicate_arguments( const completion_t &a, const completion_t &b) { bool ad = a.flags & COMPLETE_DUPLICATES_ARGUMENT; bool bd = b.flags & COMPLETE_DUPLICATES_ARGUMENT; return ad < bd; } __attribute__((always_inline)) static inline bool compare_completions_by_tilde( const completion_t &a, const completion_t &b) { if (a.completion.empty() || b.completion.empty()) { return false; } return ((a.completion.back() == L'~') < (b.completion.back() == L'~')); } /// Unique the list of completions, without perturbing their order. static void unique_completions_retaining_order(completion_list_t *comps) { std::unordered_set seen; seen.reserve(comps->size()); auto pred = [&seen](const completion_t &c) { // Remove (return true) if insertion fails. bool inserted = seen.insert(c.completion).second; return !inserted; }; comps->erase(std::remove_if(comps->begin(), comps->end(), pred), comps->end()); } void completions_sort_and_prioritize(completion_list_t *comps, completion_request_options_t flags) { if (comps->empty()) return; // Find the best rank. uint32_t best_rank = UINT32_MAX; for (const auto &comp : *comps) { best_rank = std::min(best_rank, comp.rank()); } // Throw out completions of worse ranks. comps->erase(std::remove_if(comps->begin(), comps->end(), [=](const completion_t &comp) { return comp.rank() > best_rank; }), comps->end()); // Deduplicate both sorted and unsorted results. unique_completions_retaining_order(comps); // Sort, provided COMPLETE_DONT_SORT isn't set. // Here we do not pass suppress_exact, so that exact matches appear first. stable_sort(comps->begin(), comps->end(), [&](const completion_t &a, const completion_t &b) { return a.rank() < b.rank() || natural_compare_completions(a, b); }); // Lastly, if this is for an autosuggestion, prefer to avoid completions that duplicate // arguments, and penalize files that end in tilde - they're frequently autosave files from e.g. // emacs. Also prefer samecase to smartcase. if (flags.autosuggestion) { stable_sort(comps->begin(), comps->end(), [](const completion_t &a, const completion_t &b) { if (a.match.case_fold != b.match.case_fold) { return a.match.case_fold < b.match.case_fold; } return compare_completions_by_duplicate_arguments(a, b) || compare_completions_by_tilde(a, b); }); } } namespace { /// Class representing an attempt to compute completions. class completer_t { /// The operation context for this completion. const operation_context_t &ctx; /// Flags associated with the completion request. const completion_request_options_t flags; /// The output completions. completion_receiver_t completions; /// Commands which we would have tried to load, if we had a parser. wcstring_list_t needs_load; /// Table of completions conditions that have already been tested and the corresponding test /// results. using condition_cache_t = std::unordered_map; condition_cache_t condition_cache; bool try_complete_variable(const wcstring &str); bool try_complete_user(const wcstring &str); bool complete_param_for_command(const wcstring &cmd_orig, const wcstring &popt, const wcstring &str, bool use_switches, bool *out_do_file); void complete_param_expand(const wcstring &str, bool do_file, bool handle_as_special_cd = false); void complete_cmd(const wcstring &str); /// Attempt to complete an abbreviation for the given string. void complete_abbr(const wcstring &cmd); void complete_from_args(const wcstring &str, const wcstring &args, const wcstring &desc, complete_flags_t flags); void complete_cmd_desc(const wcstring &str); bool complete_variable(const wcstring &str, size_t start_offset); bool condition_test(const wcstring &condition); bool condition_test(const wcstring_list_t &conditions); void complete_strings(const wcstring &wc_escaped, const description_func_t &desc_func, const completion_list_t &possible_comp, complete_flags_t flags); expand_flags_t expand_flags() const { expand_flags_t result{}; if (flags.autosuggestion) result |= expand_flag::skip_cmdsubst; if (flags.fuzzy_match) result |= expand_flag::fuzzy_match; if (flags.descriptions) result |= expand_flag::gen_descriptions; return result; } // Bag of data to support expanding a command's arguments using custom completions, including // the wrap chain. struct custom_arg_data_t { explicit custom_arg_data_t(wcstring_list_t *vars) : var_assignments(vars) { assert(vars); } // The unescaped argument before the argument which is being completed, or empty if none. wcstring previous_argument{}; // The unescaped argument which is being completed, or empty if none. wcstring current_argument{}; // Whether a -- has been encountered, which suppresses options. bool had_ddash{false}; // Whether to perform file completions. // This is an "out" parameter of the wrap chain walk: if any wrapped command suppresses file // completions this gets set to false. bool do_file{true}; // Depth in the wrap chain. size_t wrap_depth{0}; // The list of variable assignments: escaped strings of the form VAR=VAL. // This may be temporarily appended to as we explore the wrap chain. // When completing, variable assignments are really set in a local scope. wcstring_list_t *var_assignments; // The set of wrapped commands which we have visited, and so should not be explored again. std::set visited_wrapped_commands{}; }; void complete_custom(const wcstring &cmd, const wcstring &cmdline, custom_arg_data_t *ad); void walk_wrap_chain(const wcstring &cmd, const wcstring &cmdline, source_range_t cmdrange, custom_arg_data_t *ad); cleanup_t apply_var_assignments(const wcstring_list_t &var_assignments); bool empty() const { return completions.empty(); } void escape_opening_brackets(const wcstring &argument); void mark_completions_duplicating_arguments(const wcstring &cmd, const wcstring &prefix, const std::vector &args); public: completer_t(const operation_context_t &ctx, completion_request_options_t f) : ctx(ctx), flags(f), completions(ctx.expansion_limit) {} void perform_for_commandline(wcstring cmdline); completion_list_t acquire_completions() { return completions.take(); } wcstring_list_t acquire_needs_load() { return std::move(needs_load); } }; // Autoloader for completions. static owning_lock completion_autoloader{autoload_t(L"fish_complete_path")}; /// Test if the specified script returns zero. The result is cached, so that if multiple completions /// use the same condition, it needs only be evaluated once. condition_cache_clear must be called /// after a completion run to make sure that there are no stale completions. bool completer_t::condition_test(const wcstring &condition) { if (condition.empty()) { // std::fwprintf( stderr, L"No condition specified\n" ); return true; } if (!ctx.parser) { return false; } bool test_res; auto cached_entry = condition_cache.find(condition); if (cached_entry == condition_cache.end()) { // Compute new value and reinsert it. test_res = (0 == exec_subshell(condition, *ctx.parser, false /* don't apply exit status */)); condition_cache[condition] = test_res; } else { // Use the old value. test_res = cached_entry->second; } return test_res; } bool completer_t::condition_test(const wcstring_list_t &conditions) { for (const auto &c : conditions) { if (!condition_test(c)) return false; } return true; } /// Find the full path and commandname from a command string 'str'. static void parse_cmd_string(const wcstring &str, wcstring *path, wcstring *cmd, const environment_t &vars) { auto path_result = path_try_get_path(str, vars); bool found = (path_result.err == 0); *path = std::move(path_result.path); // Resolve commands that use relative paths because we compare full paths with "complete -p". if (found && !str.empty() && str.at(0) != L'/') { if (auto full_path = wrealpath(*path)) { path->assign(full_path.acquire()); } } // Make sure the path is not included in the command. size_t last_slash = str.find_last_of(L'/'); if (last_slash != wcstring::npos) { *cmd = str.substr(last_slash + 1); } else { *cmd = str; } } /// Copy any strings in possible_comp which have the specified prefix to the /// completer's completion array. The prefix may contain wildcards. The output /// will consist of completion_t structs. /// /// There are three ways to specify descriptions for each completion. Firstly, /// if a description has already been added to the completion, it is _not_ /// replaced. Secondly, if the desc_func function is specified, use it to /// determine a dynamic completion. Thirdly, if none of the above are available, /// the desc string is used as a description. /// /// @param wc_escaped /// the prefix, possibly containing wildcards. The wildcard should not have /// been unescaped, i.e. '*' should be used for any string, not the /// ANY_STRING character. /// @param desc_func /// the function that generates a description for those completions without an /// embedded description /// @param possible_comp /// the list of possible completions to iterate over /// @param flags /// The flags void completer_t::complete_strings(const wcstring &wc_escaped, const description_func_t &desc_func, const completion_list_t &possible_comp, complete_flags_t flags) { wcstring tmp = wc_escaped; if (!expand_one(tmp, this->expand_flags() | expand_flag::skip_cmdsubst | expand_flag::skip_wildcards, ctx)) return; const wcstring wc = parse_util_unescape_wildcards(tmp); for (const auto &comp : possible_comp) { const wcstring &comp_str = comp.completion; if (!comp_str.empty()) { wildcard_complete(comp_str, wc.c_str(), desc_func, &this->completions, this->expand_flags(), flags); } } } /// If command to complete is short enough, substitute the description with the whatis information /// for the executable. void completer_t::complete_cmd_desc(const wcstring &str) { if (!ctx.parser) return; wcstring cmd; size_t pos = str.find_last_of(L'/'); if (pos != std::string::npos) { if (pos + 1 > str.length()) return; cmd = wcstring(str, pos + 1); } else { cmd = str; } // Using apropos with a single-character search term produces far to many results - require at // least two characters if we don't know the location of the whatis-database. if (cmd.length() < 2) return; if (wildcard_has(cmd)) { return; } bool skip = true; for (const auto &c : completions.get_list()) { if (c.completion.empty() || (c.completion.back() != L'/')) { skip = false; break; } } if (skip) { return; } wcstring lookup_cmd(L"__fish_describe_command "); lookup_cmd.append(escape_string(cmd)); // First locate a list of possible descriptions using a single call to apropos or a direct // search if we know the location of the whatis database. This can take some time on slower // systems with a large set of manuals, but it should be ok since apropos is only called once. wcstring_list_t list; (void)exec_subshell(lookup_cmd, *ctx.parser, list, false /* don't apply exit status */); // Then discard anything that is not a possible completion and put the result into a // hashtable with the completion as key and the description as value. std::unordered_map lookup; // A typical entry is the command name, followed by a tab, followed by a description. for (const wcstring &elstr : list) { // Skip keys that are too short. if (elstr.size() < cmd.size()) continue; // Skip cases without a tab, or without a description, or bizarre cases where the tab is // part of the command. size_t tab_idx = elstr.find(L'\t'); if (tab_idx == wcstring::npos || tab_idx + 1 >= elstr.size() || tab_idx < cmd.size()) continue; // Make the key. This is the stuff after the command. // For example: // elstr = lsmod // cmd = ls // key = mod // Note an empty key is common and natural, if 'cmd' were already valid. wcstring key(elstr, cmd.size(), tab_idx - cmd.size()); wcstring val(elstr, tab_idx + 1); assert(!val.empty() && "tab index should not have been at the end."); // And once again I make sure the first character is uppercased because I like it that // way, and I get to decide these things. val.at(0) = towupper(val.at(0)); lookup.emplace(std::move(key), std::move(val)); } // Then do a lookup on every completion and if a match is found, change to the new // description. for (auto &completion : completions.get_list()) { const wcstring &el = completion.completion; auto new_desc_iter = lookup.find(el); if (new_desc_iter != lookup.end()) completion.description = new_desc_iter->second; } } /// Returns a description for the specified function, or an empty string if none. static wcstring complete_function_desc(const wcstring &fn) { if (auto props = function_get_props(fn)) { return props->description; } return wcstring{}; } /// Complete the specified command name. Search for executables in the path, executables defined /// using an absolute path, functions, builtins and directories for implicit cd commands. /// /// \param str_cmd the command string to find completions for void completer_t::complete_cmd(const wcstring &str_cmd) { completion_list_t possible_comp; // Append all possible executables expand_result_t result = expand_string( str_cmd, &this->completions, this->expand_flags() | expand_flag::special_for_command | expand_flag::for_completions | expand_flag::preserve_home_tildes | expand_flag::executables_only, ctx); if (result == expand_result_t::cancel) { return; } if (result == expand_result_t::ok && this->flags.descriptions) { this->complete_cmd_desc(str_cmd); } // We don't really care if this succeeds or fails. If it succeeds this->completions will be // updated with choices for the user. expand_result_t ignore = // Append all matching directories expand_string(str_cmd, &this->completions, this->expand_flags() | expand_flag::for_completions | expand_flag::preserve_home_tildes | expand_flag::directories_only, ctx); UNUSED(ignore); if (str_cmd.empty() || (str_cmd.find(L'/') == wcstring::npos && str_cmd.at(0) != L'~')) { bool include_hidden = !str_cmd.empty() && str_cmd.at(0) == L'_'; wcstring_list_t names = function_get_names(include_hidden); for (wcstring &name : names) { // Append all known matching functions append_completion(&possible_comp, std::move(name)); } this->complete_strings(str_cmd, complete_function_desc, possible_comp, 0); possible_comp.clear(); // Append all matching builtins builtin_get_names(&possible_comp); this->complete_strings(str_cmd, builtin_get_desc, possible_comp, 0); } } void completer_t::complete_abbr(const wcstring &cmd) { std::map abbrs = get_abbreviations(ctx.vars); completion_list_t possible_comp; possible_comp.reserve(abbrs.size()); for (const auto &kv : abbrs) { possible_comp.emplace_back(kv.first); } auto desc_func = [&](const wcstring &key) { auto iter = abbrs.find(key); assert(iter != abbrs.end() && "Abbreviation not found"); return format_string(ABBR_DESC, iter->second.c_str()); }; this->complete_strings(cmd, desc_func, possible_comp, COMPLETE_NO_SPACE); } /// Evaluate the argument list (as supplied by complete -a) and insert any /// return matching completions. Matching is done using @c /// copy_strings_with_prefix, meaning the completion may contain wildcards. /// Logically, this is not always the right thing to do, but I have yet to come /// up with a case where this matters. /// /// @param str /// The string to complete. /// @param args /// The list of option arguments to be evaluated. /// @param desc /// Description of the completion /// @param flags /// The flags /// void completer_t::complete_from_args(const wcstring &str, const wcstring &args, const wcstring &desc, complete_flags_t flags) { const bool is_autosuggest = this->flags.autosuggestion; bool saved_interactive = false; statuses_t status; if (ctx.parser) { saved_interactive = ctx.parser->libdata().is_interactive; ctx.parser->libdata().is_interactive = false; status = ctx.parser->get_last_statuses(); } expand_flags_t eflags{}; if (is_autosuggest) { eflags |= expand_flag::skip_cmdsubst; } completion_list_t possible_comp = parser_t::expand_argument_list(args, eflags, ctx); if (ctx.parser) { ctx.parser->libdata().is_interactive = saved_interactive; ctx.parser->set_last_statuses(status); } this->complete_strings(escape_string(str), const_desc(desc), possible_comp, flags); } static size_t leading_dash_count(const wchar_t *str) { size_t cursor = 0; while (str[cursor] == L'-') { cursor++; } return cursor; } /// Match a parameter. static bool param_match(const complete_entry_opt_t *e, const wchar_t *optstr) { bool result = false; if (e->type != option_type_args_only) { size_t dashes = leading_dash_count(optstr); result = (dashes == e->expected_dash_count() && e->option == &optstr[dashes]); } return result; } /// Test if a string is an option with an argument, like --color=auto or -I/usr/include. static const wchar_t *param_match2(const complete_entry_opt_t *e, const wchar_t *optstr) { // We may get a complete_entry_opt_t with no options if it's just arguments. if (e->option.empty()) { return nullptr; } // Verify leading dashes. size_t cursor = leading_dash_count(optstr); if (cursor != e->expected_dash_count()) { return nullptr; } // Verify options match. if (!string_prefixes_string(e->option, &optstr[cursor])) { return nullptr; } cursor += e->option.length(); // Short options are like -DNDEBUG. Long options are like --color=auto. So check for an equal // sign for long options. assert(e->type != option_type_short); if (optstr[cursor] != L'=') { return nullptr; } cursor += 1; return &optstr[cursor]; } /// Parses a token of short options plus one optional parameter like /// '-xzPARAM', where x and z are short options. /// /// Returns the position of the last option character (e.g. the position of z which is 2). /// Everything after that is assumed to be part of the parameter. /// Returns wcstring::npos if there is no valid short option. static size_t short_option_pos(const wcstring &arg, const option_list_t &options) { if (arg.size() <= 1 || leading_dash_count(arg.c_str()) != 1) { return wcstring::npos; } for (size_t pos = 1; pos < arg.size(); pos++) { wchar_t arg_char = arg.at(pos); const complete_entry_opt_t *match = nullptr; for (const complete_entry_opt_t &o : options) { if (o.type == option_type_short && o.option.at(0) == arg_char) { match = &o; break; } } if (match == nullptr) { // The first character after the dash is not a valid option. if (pos == 1) return wcstring::npos; return pos - 1; } if (match->result_mode.requires_param) { return pos; } } return arg.size() - 1; } /// complete_param: Given a command, find completions for the argument str of command cmd_orig with /// previous option popt. If file completions should be disabled, then mark *out_do_file as false. /// /// \return true if successful, false if there's an error. /// /// Examples in format (cmd, popt, str): /// /// echo hello world -> ("echo", "world", "") /// echo hello world -> ("echo", "hello", "world") /// bool completer_t::complete_param_for_command(const wcstring &cmd_orig, const wcstring &popt, const wcstring &str, bool use_switches, bool *out_do_file) { bool use_files = true, has_force = false; wcstring cmd, path; parse_cmd_string(cmd_orig, &path, &cmd, ctx.vars); // Don't use cmd_orig here for paths. It's potentially pathed, // so that command might exist, but the completion script // won't be using it. bool cmd_exists = builtin_exists(cmd) || function_exists_no_autoload(cmd) || path_get_path(cmd, ctx.vars).has_value(); if (!cmd_exists) { // Do not load custom completions if the command does not exist // This prevents errors caused during the execution of completion providers for // tools that do not exist. Applies to both manual completions ("cm", "cmd ") // and automatic completions ("gi" autosuggestion provider -> git) FLOG(complete, "Skipping completions for non-existent command"); } else if (ctx.parser) { complete_load(cmd, *ctx.parser); } else if (!completion_autoloader.acquire()->has_attempted_autoload(cmd)) { needs_load.push_back(cmd); } // Make a list of lists of all options that we care about. std::vector all_options; { auto completion_map = s_completion_map.acquire(); for (const auto &kv : *completion_map) { const completion_key_t &key = kv.first; bool cmd_is_path = key.second; const wcstring &match = cmd_is_path ? path : cmd; if (wildcard_match(match, key.first)) { // Copy all of their options into our list. all_options.push_back(kv.second.get_options()); // Oof, this is a lot of copying } } } // Now release the lock and test each option that we captured above. We have to do this outside // the lock because callouts (like the condition) may add or remove completions. See issue 2. for (const option_list_t &options : all_options) { size_t short_opt_pos = short_option_pos(str, options); bool last_option_requires_param = false, use_common = true; if (use_switches) { if (str[0] == L'-') { // Check if we are entering a combined option and argument (like --color=auto or // -I/usr/include). for (const complete_entry_opt_t &o : options) { const wchar_t *arg; if (o.type == option_type_short) { if (short_opt_pos == wcstring::npos) continue; if (o.option.at(0) != str.at(short_opt_pos)) continue; last_option_requires_param = o.result_mode.requires_param; arg = str.c_str() + short_opt_pos + 1; } else { arg = param_match2(&o, str.c_str()); } if (arg != nullptr && this->condition_test(o.condition)) { if (o.result_mode.requires_param) use_common = false; if (o.result_mode.no_files) use_files = false; if (o.result_mode.force_files) has_force = true; complete_from_args(arg, o.comp, o.localized_desc(), o.flags); } } } else if (popt[0] == L'-') { // Set to true if we found a matching old-style switch. // Here we are testing the previous argument, // to see how we should complete the current argument bool old_style_match = false; // If we are using old style long options, check for them first. for (const complete_entry_opt_t &o : options) { if (o.type == option_type_single_long && param_match(&o, popt.c_str()) && this->condition_test(o.condition)) { old_style_match = true; if (o.result_mode.requires_param) use_common = false; if (o.result_mode.no_files) use_files = false; if (o.result_mode.force_files) has_force = true; complete_from_args(str, o.comp, o.localized_desc(), o.flags); } } // No old style option matched, or we are not using old style options. We check if // any short (or gnu style) options do. if (!old_style_match) { size_t prev_short_opt_pos = short_option_pos(popt, options); for (const complete_entry_opt_t &o : options) { // Gnu-style options with _optional_ arguments must be specified as a single // token, so that it can be differed from a regular argument. // Here we are testing the previous argument for a GNU-style match, // to see how we should complete the current argument if (!o.result_mode.requires_param) continue; bool match = false; if (o.type == option_type_short) { match = prev_short_opt_pos != wcstring::npos && // Only if the option was the last char in the token, // i.e. there is no parameter yet. prev_short_opt_pos + 1 == popt.size() && o.option.at(0) == popt.at(prev_short_opt_pos); } else if (o.type == option_type_double_long) { match = param_match(&o, popt.c_str()); } if (match && this->condition_test(o.condition)) { if (o.result_mode.requires_param) use_common = false; if (o.result_mode.no_files) use_files = false; if (o.result_mode.force_files) has_force = true; complete_from_args(str, o.comp, o.localized_desc(), o.flags); } } } } } if (!use_common) { continue; } // Now we try to complete an option itself for (const complete_entry_opt_t &o : options) { // If this entry is for the base command, check if any of the arguments match. if (!this->condition_test(o.condition)) continue; if (o.option.empty()) { use_files = use_files && (!(o.result_mode.no_files)); has_force = has_force || o.result_mode.force_files; complete_from_args(str, o.comp, o.localized_desc(), o.flags); } if (!use_switches || str.empty()) { continue; } // Check if the short style option matches. if (o.type == option_type_short) { wchar_t optchar = o.option.at(0); if (short_opt_pos == wcstring::npos) { // str has no short option at all (but perhaps it is the // prefix of a single long option). // Only complete short options if there is no character after the dash. if (str != L"-") continue; } else { // Only complete when the last short option has no parameter yet.. if (short_opt_pos + 1 != str.size()) continue; // .. and it does not require one .. if (last_option_requires_param) continue; // .. and the option is not already there. if (str.find(optchar) != wcstring::npos) continue; } // It's a match. wcstring desc = o.localized_desc(); // Append a short-style option if (!this->completions.add(wcstring{o.option}, std::move(desc), 0)) { return false; } } // Check if the long style option matches. if (o.type != option_type_single_long && o.type != option_type_double_long) { continue; } wcstring whole_opt(o.expected_dash_count(), L'-'); whole_opt.append(o.option); if (whole_opt.length() < str.length()) { continue; } int match = string_prefixes_string(str, whole_opt); if (!match) { bool match_no_case = wcsncasecmp(str.c_str(), whole_opt.c_str(), str.length()) == 0; if (!match_no_case) { continue; } } int has_arg = 0; // does this switch have any known arguments int req_arg = 0; // does this switch _require_ an argument size_t offset = 0; complete_flags_t flags = 0; if (match) { offset = str.length(); } else { flags = COMPLETE_REPLACES_TOKEN; } has_arg = !o.comp.empty(); req_arg = o.result_mode.requires_param; if (o.type == option_type_double_long && (has_arg && !req_arg)) { // Optional arguments to a switch can only be handled using the '=', so we add it as // a completion. By default we avoid using '=' and instead rely on '--switch // switch-arg', since it is more commonly supported by homebrew getopt-like // functions. wcstring completion = format_string(L"%ls=", whole_opt.c_str() + offset); // Append a long-style option with a mandatory trailing equal sign if (!this->completions.add(std::move(completion), C_(o.desc), flags | COMPLETE_NO_SPACE)) { return false; } } // Append a long-style option if (!this->completions.add(whole_opt.substr(offset), C_(o.desc), flags)) { return false; } } } if (has_force) { *out_do_file = true; } else if (!use_files) { *out_do_file = false; } return true; } /// Perform generic (not command-specific) expansions on the specified string. void completer_t::complete_param_expand(const wcstring &str, bool do_file, bool handle_as_special_cd) { if (ctx.check_cancel()) return; expand_flags_t flags = this->expand_flags() | expand_flag::skip_cmdsubst | expand_flag::for_completions | expand_flag::preserve_home_tildes; if (!do_file) flags |= expand_flag::skip_wildcards; if (handle_as_special_cd && do_file) { if (this->flags.autosuggestion) { flags |= expand_flag::special_for_cd_autosuggestion; } flags |= expand_flag::directories_only; flags |= expand_flag::special_for_cd; } // Squelch file descriptions per issue #254. if (this->flags.autosuggestion || do_file) flags.clear(expand_flag::gen_descriptions); // We have the following cases: // // --foo=bar => expand just bar // -foo=bar => expand just bar // foo=bar => expand the whole thing, and also just bar // // We also support colon separator (#2178). If there's more than one, prefer the last one. size_t sep_index = str.find_last_of(L"=:"); bool complete_from_separator = (sep_index != wcstring::npos); bool complete_from_start = !complete_from_separator || !string_prefixes_string(L"-", str); if (complete_from_separator) { // FIXME: This just cuts the token, // so any quoting or braces gets lost. // See #4954. const wcstring sep_string = wcstring(str, sep_index + 1); completion_list_t local_completions; if (expand_string(sep_string, &local_completions, flags, ctx) == expand_result_t::error) { FLOGF(complete, L"Error while expanding string '%ls'", sep_string.c_str()); } // Any COMPLETE_REPLACES_TOKEN will also stomp the separator. We need to "repair" them by // inserting our separator and prefix. const wcstring prefix_with_sep = wcstring(str, 0, sep_index + 1); for (completion_t &comp : local_completions) { comp.prepend_token_prefix(prefix_with_sep); } if (!this->completions.add_list(std::move(local_completions))) { return; } } if (complete_from_start) { // Don't do fuzzy matching for files if the string begins with a dash (issue #568). We could // consider relaxing this if there was a preceding double-dash argument. if (string_prefixes_string(L"-", str)) flags.clear(expand_flag::fuzzy_match); if (expand_string(str, &this->completions, flags, ctx) == expand_result_t::error) { FLOGF(complete, L"Error while expanding string '%ls'", str.c_str()); } } } /// Complete the specified string as an environment variable. /// \return true if this was a variable, so we should stop completion. bool completer_t::complete_variable(const wcstring &str, size_t start_offset) { const wchar_t *const whole_var = str.c_str(); const wchar_t *var = &whole_var[start_offset]; size_t varlen = str.length() - start_offset; bool res = false; for (const wcstring &env_name : ctx.vars.get_names(0)) { bool anchor_start = !this->flags.fuzzy_match; maybe_t match = string_fuzzy_match_string(var, env_name, anchor_start); if (!match) continue; wcstring comp; complete_flags_t flags = 0; if (!match->requires_full_replacement()) { // Take only the suffix. comp.append(env_name.c_str() + varlen); } else { comp.append(whole_var, start_offset); comp.append(env_name); flags = COMPLETE_REPLACES_TOKEN | COMPLETE_DONT_ESCAPE; } wcstring desc; if (this->flags.descriptions) { if (this->flags.autosuggestion) { // $history can be huge, don't put all of it in the completion description; see // #6288. if (env_name == L"history") { std::shared_ptr history = history_t::with_name(history_session_id(ctx.vars)); for (size_t i = 1; i < history->size() && desc.size() < 64; i++) { if (i > 1) desc += L' '; desc += expand_escape_string(history->item_at_index(i).str()); } } else { // Can't use ctx.vars here, it could be any variable. auto var = ctx.vars.get(env_name); if (!var) continue; wcstring value = expand_escape_variable(*var); desc = format_string(COMPLETE_VAR_DESC_VAL, value.c_str()); } } } // Append matching environment variables // TODO: need to propagate overflow here. ignore_result(this->completions.add(std::move(comp), std::move(desc), flags, *match)); res = true; } return res; } bool completer_t::try_complete_variable(const wcstring &str) { enum { e_unquoted, e_single_quoted, e_double_quoted } mode = e_unquoted; const size_t len = str.size(); // Get the position of the dollar heading a (possibly empty) run of valid variable characters. // npos means none. size_t variable_start = wcstring::npos; for (size_t in_pos = 0; in_pos < len; in_pos++) { wchar_t c = str.at(in_pos); if (!valid_var_name_char(c)) { // This character cannot be in a variable, reset the dollar. variable_start = -1; } switch (c) { case L'\\': { in_pos++; break; } case L'$': { if (mode == e_unquoted || mode == e_double_quoted) { variable_start = in_pos; } break; } case L'\'': { if (mode == e_single_quoted) { mode = e_unquoted; } else if (mode == e_unquoted) { mode = e_single_quoted; } break; } case L'"': { if (mode == e_double_quoted) { mode = e_unquoted; } else if (mode == e_unquoted) { mode = e_double_quoted; } break; } default: { break; // all other chars ignored here } } } // Now complete if we have a variable start. Note the variable text may be empty; in that case // don't generate an autosuggestion, but do allow tab completion. bool allow_empty = !this->flags.autosuggestion; bool text_is_empty = (variable_start == len); bool result = false; if (variable_start != wcstring::npos && (allow_empty || !text_is_empty)) { result = this->complete_variable(str, variable_start + 1); } return result; } /// Try to complete the specified string as a username. This is used by ~USER type expansion. /// /// \return false if unable to complete, true otherwise bool completer_t::try_complete_user(const wcstring &str) { #ifndef HAVE_GETPWENT // The getpwent() function does not exist on Android. A Linux user on Android isn't // really a user - each installed app gets an UID assigned. Listing all UID:s is not // possible without root access, and doing a ~USER type expansion does not make sense // since every app is sandboxed and can't access eachother. return false; #else const wchar_t *cmd = str.c_str(); const wchar_t *first_char = cmd; if (*first_char != L'~' || std::wcschr(first_char, L'/')) return false; const wchar_t *user_name = first_char + 1; const wchar_t *name_end = std::wcschr(user_name, L'~'); if (name_end) return false; double start_time = timef(); bool result = false; size_t name_len = str.length() - 1; static std::mutex s_setpwent_lock; scoped_lock locker(s_setpwent_lock); setpwent(); // cppcheck-suppress getpwentCalled while (struct passwd *pw = getpwent()) { if (ctx.check_cancel()) { break; } const wcstring pw_name_str = str2wcstring(pw->pw_name); const wchar_t *pw_name = pw_name_str.c_str(); if (std::wcsncmp(user_name, pw_name, name_len) == 0) { wcstring desc = format_string(COMPLETE_USER_DESC, pw_name); // Append a user name. // TODO: propagate overflow? ignore_result( this->completions.add(&pw_name[name_len], std::move(desc), COMPLETE_NO_SPACE)); result = true; } else if (wcsncasecmp(user_name, pw_name, name_len) == 0) { wcstring name = format_string(L"~%ls", pw_name); wcstring desc = format_string(COMPLETE_USER_DESC, pw_name); // Append a user name ignore_result(this->completions.add( std::move(name), std::move(desc), COMPLETE_REPLACES_TOKEN | COMPLETE_DONT_ESCAPE | COMPLETE_NO_SPACE)); result = true; } // If we've spent too much time (more than 200 ms) doing this give up. if (timef() - start_time > 0.2) break; } endpwent(); return result; #endif } // If we have variable assignments, attempt to apply them in our parser. As soon as the return // value goes out of scope, the variables will be removed from the parser. cleanup_t completer_t::apply_var_assignments(const wcstring_list_t &var_assignments) { if (!ctx.parser || var_assignments.empty()) return cleanup_t{[] {}}; env_stack_t &vars = ctx.parser->vars(); assert(&vars == &ctx.vars && "Don't know how to tab complete with a parser but a different variable set"); // clone of parse_execution_context_t::apply_variable_assignments. // Crucially do NOT expand subcommands: // VAR=(launch_missiles) cmd // should not launch missiles. // Note we also do NOT send --on-variable events. const expand_flags_t expand_flags = expand_flag::skip_cmdsubst; const block_t *block = ctx.parser->push_block(block_t::variable_assignment_block()); for (const wcstring &var_assign : var_assignments) { maybe_t equals_pos = variable_assignment_equals_pos(var_assign); assert(equals_pos && "All variable assignments should have equals position"); const wcstring variable_name = var_assign.substr(0, *equals_pos); const wcstring expression = var_assign.substr(*equals_pos + 1); completion_list_t expression_expanded; auto expand_ret = expand_string(expression, &expression_expanded, expand_flags, ctx); // If expansion succeeds, set the value; if it fails (e.g. it has a cmdsub) set an empty // value anyways. wcstring_list_t vals; if (expand_ret == expand_result_t::ok) { for (auto &completion : expression_expanded) { vals.emplace_back(std::move(completion.completion)); } } ctx.parser->vars().set(variable_name, ENV_LOCAL | ENV_EXPORT, std::move(vals)); if (ctx.check_cancel()) break; } return cleanup_t([=] { ctx.parser->pop_block(block); }); } // Complete a command by invoking user-specified completions. void completer_t::complete_custom(const wcstring &cmd, const wcstring &cmdline, custom_arg_data_t *ad) { if (ctx.check_cancel()) return; bool is_autosuggest = this->flags.autosuggestion; // Perhaps set a transient commandline so that custom completions // buitin_commandline will refer to the wrapped command. But not if // we're doing autosuggestions. maybe_t remove_transient{}; bool wants_transient = (ad->wrap_depth > 0 || ad->var_assignments) && !is_autosuggest; if (wants_transient) { ctx.parser->libdata().transient_commandlines.push_back(cmdline); remove_transient.emplace([=] { ctx.parser->libdata().transient_commandlines.pop_back(); }); } // Maybe apply variable assignments. cleanup_t restore_vars{apply_var_assignments(*ad->var_assignments)}; if (ctx.check_cancel()) return; // Invoke any custom completions for this command. (void)complete_param_for_command(cmd, ad->previous_argument, ad->current_argument, !ad->had_ddash, &ad->do_file); } static bool expand_command_token(const operation_context_t &ctx, wcstring &cmd_tok) { // TODO: we give up if the first token expands to more than one argument. We could handle // that case by propagating arguments. // Also we could expand wildcards. return expand_one(cmd_tok, {expand_flag::skip_cmdsubst, expand_flag::skip_wildcards}, ctx, nullptr); } // Invoke command-specific completions given by \p arg_data. // Then, for each target wrapped by the given command, update the command // line with that target and invoke this recursively. // The command whose completions to use is given by \p cmd. The full command line is given by \p // cmdline and the command's range in it is given by \p cmdrange. Note: the command range // may have a different length than the command itself, because the command is unescaped (i.e. // quotes removed). void completer_t::walk_wrap_chain(const wcstring &cmd, const wcstring &cmdline, source_range_t cmdrange, custom_arg_data_t *ad) { // Limit our recursion depth. This prevents cycles in the wrap chain graph from overflowing. if (ad->wrap_depth > 24) return; if (ctx.cancel_checker()) return; // Extract command from the command line and invoke the receiver with it. complete_custom(cmd, cmdline, ad); wcstring_list_t targets = complete_get_wrap_targets(cmd); scoped_push saved_depth(&ad->wrap_depth, ad->wrap_depth + 1); for (const wcstring &wt : targets) { // We may append to the variable assignment list; ensure we restore it. const size_t saved_var_count = ad->var_assignments->size(); cleanup_t restore_vars([=] { assert(ad->var_assignments->size() >= saved_var_count && "Should not delete var assignments"); ad->var_assignments->resize(saved_var_count); }); // Separate the wrap target into any variable assignments VAR=... and the command itself. wcstring wrapped_command; tokenizer_t tokenizer(wt.c_str(), 0); size_t wrapped_command_offset_in_wt = wcstring::npos; while (auto tok = tokenizer.next()) { wcstring tok_src = tok->get_source(wt); if (variable_assignment_equals_pos(tok_src)) { ad->var_assignments->push_back(std::move(tok_src)); } else { wrapped_command_offset_in_wt = tok->offset; wrapped_command = std::move(tok_src); expand_command_token(ctx, wrapped_command); break; } } // Skip this wrapped command if empty, or if we've seen it before. if (wrapped_command.empty() || !ad->visited_wrapped_commands.insert(wrapped_command).second) { continue; } // Construct a fake command line containing the wrap target. wcstring faux_commandline = cmdline; faux_commandline.replace(cmdrange.start, cmdrange.length, wt); // Recurse with our new command and command line. source_range_t faux_source_range{uint32_t(cmdrange.start + wrapped_command_offset_in_wt), uint32_t(wrapped_command.size())}; walk_wrap_chain(wrapped_command, faux_commandline, faux_source_range, ad); } } /// If the argument contains a '[' typed by the user, completion by appending to the argument might /// produce an invalid token (#5831). /// /// Check if there is any unescaped, unquoted '['; if yes, make the completions replace the entire /// argument instead of appending, so '[' will be escaped. void completer_t::escape_opening_brackets(const wcstring &argument) { bool have_unquoted_unescaped_bracket = false; wchar_t quote = L'\0'; bool escaped = false; for (wchar_t c : argument) { have_unquoted_unescaped_bracket |= (c == L'[') && !quote && !escaped; if (escaped) { escaped = false; } else if (c == L'\\') { escaped = true; } else if (c == L'\'' || c == L'"') { if (quote == c) { // Closing a quote. quote = L'\0'; } else if (quote == L'\0') { // Opening a quote. quote = c; } } } if (!have_unquoted_unescaped_bracket) return; // Since completion_apply_to_command_line will escape the completion, we need to provide an // unescaped version. wcstring unescaped_argument; if (!unescape_string(argument, &unescaped_argument, UNESCAPE_INCOMPLETE)) return; for (completion_t &comp : completions.get_list()) { if (comp.flags & COMPLETE_REPLACES_TOKEN) continue; comp.flags |= COMPLETE_REPLACES_TOKEN; comp.flags |= COMPLETE_DONT_ESCAPE_TILDES; // See #9073. // We are grafting a completion that is expected to be escaped later. This will break // if the original completion doesn't want escaping. Happily, this is only the case // for username completion and variable name completion. They shouldn't end up here // anyway because they won't contain '['. if (comp.flags & COMPLETE_DONT_ESCAPE) { FLOG(warning, L"unexpected completion flag"); } comp.completion = unescaped_argument + comp.completion; } } /// Set the DUPLICATES_ARG flag in any completion that duplicates an argument. void completer_t::mark_completions_duplicating_arguments(const wcstring &cmd, const wcstring &prefix, const std::vector &args) { // Get all the arguments, unescaped, into an array that we're going to bsearch. wcstring_list_t arg_strs; for (const auto &arg : args) { wcstring argstr = arg.get_source(cmd); wcstring argstr_unesc; if (unescape_string(argstr, &argstr_unesc, UNESCAPE_DEFAULT)) { arg_strs.push_back(std::move(argstr_unesc)); } } std::sort(arg_strs.begin(), arg_strs.end()); wcstring comp_str; for (completion_t &comp : completions.get_list()) { comp_str = comp.completion; if (!(comp.flags & COMPLETE_REPLACES_TOKEN)) { comp_str.insert(0, prefix); } if (std::binary_search(arg_strs.begin(), arg_strs.end(), comp_str)) { comp.flags |= COMPLETE_DUPLICATES_ARGUMENT; } } } void completer_t::perform_for_commandline(wcstring cmdline) { // Limit recursion, in case a user-defined completion has cycles, or the completion for "x" // wraps "A=B x" (#3474, #7344). No need to do that when there is no parser: this happens only // for autosuggestions where we don't evaluate command substitutions or variable assignments. if (ctx.parser) { if (ctx.parser->libdata().complete_recursion_level >= 24) { FLOGF(error, _(L"completion reached maximum recursion depth, possible cycle?"), cmdline.c_str()); return; } ++ctx.parser->libdata().complete_recursion_level; }; cleanup_t decrement{[this]() { if (ctx.parser) --ctx.parser->libdata().complete_recursion_level; }}; const size_t cursor_pos = cmdline.size(); const bool is_autosuggest = flags.autosuggestion; // Find the process to operate on. The cursor may be past it (#1261), so backtrack // until we know we're no longer in a space. But the space may actually be part of the // argument (#2477). size_t position_in_statement = cursor_pos; while (position_in_statement > 0 && cmdline.at(position_in_statement - 1) == L' ') { position_in_statement--; } // Get all the arguments. std::vector tokens; parse_util_process_extent(cmdline.c_str(), position_in_statement, nullptr, nullptr, &tokens); size_t actual_token_count = tokens.size(); // Hack: fix autosuggestion by removing prefixing "and"s #6249. if (is_autosuggest) { while (!tokens.empty() && parser_keywords_is_subcommand(tokens.front().get_source(cmdline))) tokens.erase(tokens.begin()); } // Consume variable assignments in tokens strictly before the cursor. // This is a list of (escaped) strings of the form VAR=VAL. wcstring_list_t var_assignments; for (const tok_t &tok : tokens) { if (tok.location_in_or_at_end_of_source_range(cursor_pos)) break; wcstring tok_src = tok.get_source(cmdline); if (!variable_assignment_equals_pos(tok_src)) break; var_assignments.push_back(std::move(tok_src)); } tokens.erase(tokens.begin(), tokens.begin() + var_assignments.size()); // Empty process (cursor is after one of ;, &, |, \n, &&, || modulo whitespace). if (tokens.empty()) { // Don't autosuggest anything based on the empty string (generalizes #1631). if (is_autosuggest) return; complete_cmd(L""); complete_abbr(L""); return; } wcstring *effective_cmdline, effective_cmdline_buf; if (tokens.size() == actual_token_count) { effective_cmdline = &cmdline; } else { effective_cmdline_buf.assign(cmdline, tokens.front().offset, wcstring::npos); effective_cmdline = &effective_cmdline_buf; } const tok_t &cmd_tok = tokens.front(); const tok_t &cur_tok = tokens.back(); // Since fish does not currently support redirect in command position, we return here. if (cmd_tok.type != token_type_t::string) return; if (cur_tok.type == token_type_t::error) return; for (const auto &tok : tokens) { // If there was an error, it was in the last token. assert(tok.type == token_type_t::string || tok.type == token_type_t::redirect); } // If we are completing a variable name or a tilde expansion user name, we do that and // return. No need for any other completions. const wcstring current_token = cur_tok.get_source(cmdline); if (cur_tok.location_in_or_at_end_of_source_range(cursor_pos)) { if (try_complete_variable(current_token) || try_complete_user(current_token)) { return; } } if (cmd_tok.location_in_or_at_end_of_source_range(cursor_pos)) { maybe_t equal_sign_pos = variable_assignment_equals_pos(current_token); if (equal_sign_pos) { complete_param_expand(current_token, true /* do_file */); return; } // Complete command filename. complete_cmd(current_token); complete_abbr(current_token); return; } // See whether we are in an argument, in a redirection or in the whitespace in between. bool in_redirection = cur_tok.type == token_type_t::redirect; bool had_ddash = false; wcstring current_argument, previous_argument; if (cur_tok.type == token_type_t::string && cur_tok.location_in_or_at_end_of_source_range(position_in_statement)) { // If the cursor is in whitespace, then the "current" argument is empty and the // previous argument is the matching one. But if the cursor was in or at the end // of the argument, then the current argument is the matching one, and the // previous argument is the one before it. bool cursor_in_whitespace = !cur_tok.location_in_or_at_end_of_source_range(cursor_pos); if (cursor_in_whitespace) { current_argument.clear(); previous_argument = current_token; } else { current_argument = current_token; if (tokens.size() >= 2) { tok_t prev_tok = tokens.at(tokens.size() - 2); if (prev_tok.type == token_type_t::string) previous_argument = prev_tok.get_source(cmdline); in_redirection = prev_tok.type == token_type_t::redirect; } } // Check to see if we have a preceding double-dash. for (size_t i = 0; i < tokens.size() - 1; i++) { if (tokens.at(i).get_source(cmdline) == L"--") { had_ddash = true; break; } } } bool do_file = false, handle_as_special_cd = false; if (in_redirection) { do_file = true; } else { // Try completing as an argument. custom_arg_data_t arg_data{&var_assignments}; arg_data.had_ddash = had_ddash; source_offset_t bias = cmdline.size() - effective_cmdline->size(); source_range_t command_range = {cmd_tok.offset - bias, cmd_tok.length}; wcstring exp_command = cmd_tok.get_source(cmdline); bool unescaped = expand_command_token(ctx, exp_command) && unescape_string(previous_argument, &arg_data.previous_argument, UNESCAPE_DEFAULT) && unescape_string(current_argument, &arg_data.current_argument, UNESCAPE_INCOMPLETE); if (unescaped) { // Have to walk over the command and its entire wrap chain. If any command // disables do_file, then they all do. walk_wrap_chain(exp_command, *effective_cmdline, command_range, &arg_data); do_file = arg_data.do_file; // If we're autosuggesting, and the token is empty, don't do file suggestions. if (is_autosuggest && arg_data.current_argument.empty()) { do_file = false; } } // Hack. If we're cd, handle it specially (issue #1059, others). handle_as_special_cd = (exp_command == L"cd") || arg_data.visited_wrapped_commands.count(L"cd"); } // Maybe apply variable assignments. cleanup_t restore_vars{apply_var_assignments(var_assignments)}; if (ctx.check_cancel()) return; // This function wants the unescaped string. complete_param_expand(current_argument, do_file, handle_as_special_cd); // Escape '[' in the argument before completing it. escape_opening_brackets(current_argument); // Lastly mark any completions that appear to already be present in arguments. mark_completions_duplicating_arguments(cmdline, current_token, tokens); } } // namespace /// Create a new completion entry. void append_completion(completion_list_t *completions, wcstring comp, wcstring desc, complete_flags_t flags, string_fuzzy_match_t match) { completions->emplace_back(std::move(comp), std::move(desc), match, flags); } void complete_add(const wcstring &cmd, bool cmd_is_path, const wcstring &option, complete_option_type_t option_type, completion_mode_t result_mode, wcstring_list_t condition, const wchar_t *comp, const wchar_t *desc, complete_flags_t flags) { // option should be empty iff the option type is arguments only. assert(option.empty() == (option_type == option_type_args_only)); // Lock the lock that allows us to edit the completion entry list. auto completion_map = s_completion_map.acquire(); completion_entry_t &c = (*completion_map)[std::make_pair(cmd, cmd_is_path)]; // Create our new option. complete_entry_opt_t opt; opt.option = option; opt.type = option_type; opt.result_mode = result_mode; if (comp) opt.comp = comp; opt.condition = std::move(condition); if (desc) opt.desc = desc; opt.flags = flags; c.add_option(std::move(opt)); } void complete_remove(const wcstring &cmd, bool cmd_is_path, const wcstring &option, complete_option_type_t type) { auto completion_map = s_completion_map.acquire(); auto iter = completion_map->find(std::make_pair(cmd, cmd_is_path)); if (iter != completion_map->end()) { bool delete_it = iter->second.remove_option(option, type); if (delete_it) { completion_map->erase(iter); } } } void complete_remove_all(const wcstring &cmd, bool cmd_is_path) { auto completion_map = s_completion_map.acquire(); completion_map->erase(std::make_pair(cmd, cmd_is_path)); } completion_list_t complete(const wcstring &cmd_with_subcmds, completion_request_options_t flags, const operation_context_t &ctx, wcstring_list_t *out_needs_loads) { // Determine the innermost subcommand. const wchar_t *cmdsubst_begin, *cmdsubst_end; parse_util_cmdsubst_extent(cmd_with_subcmds.c_str(), cmd_with_subcmds.size(), &cmdsubst_begin, &cmdsubst_end); assert(cmdsubst_begin != nullptr && cmdsubst_end != nullptr && cmdsubst_end >= cmdsubst_begin); wcstring cmd = wcstring(cmdsubst_begin, cmdsubst_end - cmdsubst_begin); completer_t completer(ctx, flags); completer.perform_for_commandline(std::move(cmd)); if (out_needs_loads) { *out_needs_loads = completer.acquire_needs_load(); } return completer.acquire_completions(); } /// Print the short switch \c opt, and the argument \c arg to the specified /// wcstring, but only if \c argument isn't an empty string. static void append_switch(wcstring &out, wchar_t opt, const wcstring &arg) { if (arg.empty()) return; append_format(out, L" -%lc %ls", opt, escape_string(arg).c_str()); } static void append_switch(wcstring &out, const wcstring &opt, const wcstring &arg) { if (arg.empty()) return; append_format(out, L" --%ls %ls", opt.c_str(), escape_string(arg).c_str()); } static void append_switch(wcstring &out, wchar_t opt) { append_format(out, L" -%lc", opt); } static void append_switch(wcstring &out, const wcstring &opt) { append_format(out, L" --%ls", opt.c_str()); } static wcstring completion2string(const completion_key_t &key, const complete_entry_opt_t &o) { const wcstring &cmd = key.first; bool is_path = key.second; wcstring out = L"complete"; if (o.flags & COMPLETE_DONT_SORT) append_switch(out, L'k'); if (o.result_mode.no_files && o.result_mode.requires_param) { append_switch(out, L"exclusive"); } else if (o.result_mode.no_files) { append_switch(out, L"no-files"); } else if (o.result_mode.force_files) { append_switch(out, L"force-files"); } else if (o.result_mode.requires_param) { append_switch(out, L"require-parameter"); } if (is_path) append_switch(out, L'p', cmd); else { out.append(L" "); out.append(escape_string(cmd)); } switch (o.type) { case option_type_args_only: { break; } case option_type_short: { append_switch(out, L's', wcstring(1, o.option.at(0))); break; } case option_type_single_long: case option_type_double_long: { append_switch(out, o.type == option_type_single_long ? L'o' : L'l', o.option); break; } } append_switch(out, L'd', C_(o.desc)); append_switch(out, L'a', o.comp); for (const auto &c : o.condition) { append_switch(out, L'n', c); } out.append(L"\n"); return out; } bool complete_load(const wcstring &cmd, parser_t &parser) { bool loaded_new = false; // We have to load this as a function, since it may define a --wraps or signature. // See issue #2466. if (function_load(cmd, parser)) { // We autoloaded something; check if we have a --wraps. loaded_new |= complete_get_wrap_targets(cmd).size() > 0; } // It's important to NOT hold the lock around completion loading. // We need to take the lock to decide what to load, drop it to perform the load, then reacquire // it. // Note we only look at the global fish_function_path and fish_complete_path. maybe_t path_to_load = completion_autoloader.acquire()->resolve_command(cmd, env_stack_t::globals()); if (path_to_load) { autoload_t::perform_autoload(*path_to_load, parser); completion_autoloader.acquire()->mark_autoload_finished(cmd); loaded_new = true; } return loaded_new; } /// Use by the bare `complete`, loaded completions are printed out as commands wcstring complete_print(const wcstring &cmd) { wcstring out; // Get references to our completions and sort them by order. auto completions = s_completion_map.acquire(); using comp_ref_t = std::reference_wrapper; std::vector completion_refs(completions->begin(), completions->end()); std::sort(completion_refs.begin(), completion_refs.end(), [](comp_ref_t a, comp_ref_t b) { return a.get().second.order < b.get().second.order; }); for (const comp_ref_t &cr : completion_refs) { const completion_key_t &key = cr.get().first; const completion_entry_t &entry = cr.get().second; if (!cmd.empty() && key.first != cmd) continue; const option_list_t &options = entry.get_options(); for (const complete_entry_opt_t &o : options) { out.append(completion2string(key, o)); } } // Append wraps. auto locked_wrappers = wrapper_map.acquire(); for (const auto &entry : *locked_wrappers) { const wcstring &src = entry.first; if (!cmd.empty() && src != cmd) continue; for (const wcstring &target : entry.second) { out.append(L"complete "); out.append(escape_string(src)); append_switch(out, L"wraps", target); out.append(L"\n"); } } return out; } void complete_invalidate_path() { // TODO: here we unload all completions for commands that are loaded by the autoloader. We also // unload any completions that the user may specified on the command line. We should in // principle track those completions loaded by the autoloader alone. wcstring_list_t cmds = completion_autoloader.acquire()->get_autoloaded_commands(); for (const wcstring &cmd : cmds) { complete_remove_all(cmd, false /* not a path */); } } /// Add a new target that wraps a command. Example: __fish_XYZ (function) wraps XYZ (target). bool complete_add_wrapper(const wcstring &command, const wcstring &new_target) { if (command.empty() || new_target.empty()) { return false; } // If the command and the target are the same, // there's no point in following the wrap-chain because we'd only complete the same thing. // TODO: This should maybe include full cycle detection. if (command == new_target) return false; auto locked_map = wrapper_map.acquire(); wrapper_map_t &wraps = *locked_map; wcstring_list_t *targets = &wraps[command]; // If it's already present, we do nothing. if (!contains(*targets, new_target)) { targets->push_back(new_target); } return true; } bool complete_remove_wrapper(const wcstring &command, const wcstring &target_to_remove) { if (command.empty() || target_to_remove.empty()) { return false; } auto locked_map = wrapper_map.acquire(); wrapper_map_t &wraps = *locked_map; bool result = false; auto current_targets_iter = wraps.find(command); if (current_targets_iter != wraps.end()) { wcstring_list_t *targets = ¤t_targets_iter->second; auto where = std::find(targets->begin(), targets->end(), target_to_remove); if (where != targets->end()) { targets->erase(where); result = true; } } return result; } wcstring_list_t complete_get_wrap_targets(const wcstring &command) { if (command.empty()) { return {}; } auto locked_map = wrapper_map.acquire(); wrapper_map_t &wraps = *locked_map; auto iter = wraps.find(command); if (iter == wraps.end()) return {}; return iter->second; }