diff --git a/CMakeLists.txt b/CMakeLists.txt index 5dbfd8844..08d7c54e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ endif() # List of sources for builtin functions. set(FISH_BUILTIN_SRCS - src/builtin.cpp src/builtins/argparse.cpp + src/builtin.cpp src/builtins/abbr.cpp src/builtins/argparse.cpp src/builtins/bg.cpp src/builtins/bind.cpp src/builtins/block.cpp src/builtins/builtin.cpp src/builtins/cd.cpp src/builtins/command.cpp src/builtins/commandline.cpp src/builtins/complete.cpp src/builtins/contains.cpp @@ -107,9 +107,9 @@ set(FISH_BUILTIN_SRCS # List of other sources. set(FISH_SRCS - src/ast.cpp src/autoload.cpp src/color.cpp src/common.cpp src/complete.cpp src/env.cpp - src/env_dispatch.cpp src/env_universal_common.cpp src/event.cpp src/exec.cpp - src/expand.cpp src/fallback.cpp src/fd_monitor.cpp src/fish_version.cpp + src/ast.cpp src/abbrs.cpp src/autoload.cpp src/color.cpp src/common.cpp src/complete.cpp + src/env.cpp src/env_dispatch.cpp src/env_universal_common.cpp src/event.cpp + src/exec.cpp src/expand.cpp src/fallback.cpp src/fd_monitor.cpp src/fish_version.cpp src/flog.cpp src/function.cpp src/future_feature_flags.cpp src/highlight.cpp src/history.cpp src/history_file.cpp src/input.cpp src/input_common.cpp src/io.cpp src/iothread.cpp src/job_group.cpp src/kill.cpp diff --git a/doc_src/cmds/abbr.rst b/doc_src/cmds/abbr.rst index 2f0682698..17c65319c 100644 --- a/doc_src/cmds/abbr.rst +++ b/doc_src/cmds/abbr.rst @@ -64,17 +64,16 @@ Examples :: - abbr -a -g gco git checkout + abbr -a gco git checkout -Add a new abbreviation where ``gco`` will be replaced with ``git checkout`` global to the current shell. +Add a new abbreviation where ``gco`` will be replaced with ``git checkout``. This abbreviation will not be automatically visible to other shells unless the same command is run in those shells (such as when executing the commands in config.fish). :: - abbr -a -U l less + abbr -a l less Add a new abbreviation where ``l`` will be replaced with ``less`` universal to all shells. -Note that you omit the **-U** since it is the default. :: diff --git a/share/functions/abbr.fish b/share/functions/abbr.fish index add6cc763..f972f4c5c 100644 --- a/share/functions/abbr.fish +++ b/share/functions/abbr.fish @@ -1,210 +1,3 @@ -function abbr --description "Manage abbreviations" - set -l options --stop-nonopt --exclusive 'a,r,e,l,s,q' --exclusive 'g,U' - set -a options h/help a/add r/rename e/erase l/list s/show q/query - set -a options g/global U/universal - - argparse -n abbr $options -- $argv - or return - - if set -q _flag_help - __fish_print_help abbr - return 0 - end - - # If run with no options, treat it like --add if we have arguments, or - # --show if we do not have any arguments. - set -l _flag_add - set -l _flag_show - if not set -q _flag_add[1] - and not set -q _flag_rename[1] - and not set -q _flag_erase[1] - and not set -q _flag_list[1] - and not set -q _flag_show[1] - and not set -q _flag_query[1] - if set -q argv[1] - set _flag_add --add - else - set _flag_show --show - end - end - - set -l abbr_scope - if set -q _flag_global - set abbr_scope --global - else if set -q _flag_universal - set abbr_scope --universal - end - - if set -q _flag_add[1] - __fish_abbr_add $argv - return - else if set -q _flag_erase[1] - set -q argv[1]; or return 1 - __fish_abbr_erase $argv - return - else if set -q _flag_rename[1] - __fish_abbr_rename $argv - return - else if set -q _flag_list[1] - __fish_abbr_list $argv - return - else if set -q _flag_show[1] - __fish_abbr_show $argv - return - else if set -q _flag_query[1] - # "--query": Check if abbrs exist. - # If we don't have an argument, it's an automatic failure. - set -q argv[1]; or return 1 - set -l escaped _fish_abbr_(string escape --style=var -- $argv) - # We return 0 if any arg exists, whereas `set -q` returns the number of undefined arguments. - # But we should be consistent with `type -q` and `command -q`. - for var in $escaped - set -q $var; and return 0 - end - return 1 - else - printf ( _ "%s: Could not figure out what to do!\n" ) abbr >&2 - return 127 - end -end - -function __fish_abbr_add --no-scope-shadowing - if not set -q argv[2] - printf ( _ "%s %s: Requires at least two arguments\n" ) abbr --add >&2 - return 1 - end - - # Because of the way abbreviations are expanded there can't be any spaces in the key. - set -l abbr_name $argv[1] - set -l escaped_abbr_name (string escape -- $abbr_name) - if string match -q "* *" -- $abbr_name - set -l msg ( _ "%s %s: Abbreviation %s cannot have spaces in the word\n" ) - printf $msg abbr --add $escaped_abbr_name >&2 - return 1 - end - - set -l abbr_val "$argv[2..-1]" - set -l abbr_var_name _fish_abbr_(string escape --style=var -- $abbr_name) - - if not set -q $abbr_var_name - # We default to the universal scope if the user didn't explicitly specify a scope and the - # abbreviation isn't already defined. - set -q abbr_scope[1] - or set abbr_scope --universal - end - true # make sure the next `set` command doesn't leak the previous status - set $abbr_scope $abbr_var_name $abbr_val -end - -function __fish_abbr_erase --no-scope-shadowing - set -l ret 0 - set -l abbr_var_names - for abbr_name in $argv - # Because of the way abbreviations are expanded there can't be any spaces in the key. - set -l escaped_name (string escape -- $abbr_name) - if string match -q "* *" -- $abbr_name - set -l msg ( _ "%s %s: Abbreviation %s cannot have spaces in the word\n" ) - printf $msg abbr --erase $escaped_name >&2 - return 1 - end - - set -l abbr_var_name _fish_abbr_(string escape --style=var -- $abbr_name) - - set -a abbr_var_names $abbr_var_name - end - # And then erase them all in one go. - # Our return value is that of `set -e`. - set -e $abbr_var_names -end - -function __fish_abbr_rename --no-scope-shadowing - if test (count $argv) -ne 2 - printf ( _ "%s %s: Requires exactly two arguments\n" ) abbr --rename >&2 - return 1 - end - - set -l old_name $argv[1] - set -l new_name $argv[2] - set -l escaped_old_name (string escape -- $old_name) - set -l escaped_new_name (string escape -- $new_name) - if string match -q "* *" -- $old_name - set -l msg ( _ "%s %s: Abbreviation %s cannot have spaces in the word\n" ) - printf $msg abbr --rename $escaped_old_name >&2 - return 1 - end - if string match -q "* *" -- $new_name - set -l msg ( _ "%s %s: Abbreviation %s cannot have spaces in the word\n" ) - printf $msg abbr --rename $escaped_new_name >&2 - return 1 - end - - set -l old_var_name _fish_abbr_(string escape --style=var -- $old_name) - set -l new_var_name _fish_abbr_(string escape --style=var -- $new_name) - - if not set -q $old_var_name - printf ( _ "%s %s: No abbreviation named %s\n" ) abbr --rename $escaped_old_name >&2 - return 1 - end - if set -q $new_var_name - set -l msg ( _ "%s %s: Abbreviation %s already exists, cannot rename %s\n" ) - printf $msg abbr --rename $escaped_new_name $escaped_old_name >&2 - return 1 - end - - set -l old_var_val $$old_var_name - - if not set -q abbr_scope[1] - # User isn't forcing the scope so use the existing scope. - if set -ql $old_var_name - set abbr_scope --global - else - set abbr_scope --universal - end - end - - set -e $old_var_name - set $abbr_scope $new_var_name $old_var_val -end - -function __fish_abbr_list --no-scope-shadowing - if set -q argv[1] - printf ( _ "%s %s: Unexpected argument -- '%s'\n" ) abbr --erase $argv[1] >&2 - return 1 - end - - for var_name in (set --names) - string match -q '_fish_abbr_*' $var_name - or continue - - set -l abbr_name (string unescape --style=var (string sub -s 12 $var_name)) - echo $abbr_name - end -end - -function __fish_abbr_show --no-scope-shadowing - if set -q argv[1] - printf ( _ "%s %s: Unexpected argument -- '%s'\n" ) abbr --erase $argv[1] >&2 - return 1 - end - - for var_name in (set --names) - string match -q '_fish_abbr_*' $var_name - or continue - - set -l abbr_var_name $var_name - set -l abbr_name (string unescape --style=var -- (string sub -s 12 $abbr_var_name)) - set -l abbr_name (string escape --style=script -- $abbr_name) - set -l abbr_val $$abbr_var_name - set -l abbr_val (string escape --style=script -- $abbr_val) - - if set -ql $abbr_var_name - printf 'abbr -a %s -- %s %s\n' -l $abbr_name $abbr_val - end - if set -qg $abbr_var_name - printf 'abbr -a %s -- %s %s\n' -g $abbr_name $abbr_val - end - if set -qU $abbr_var_name - printf 'abbr -a %s -- %s %s\n' -U $abbr_name $abbr_val - end - end -end +# This file intentionally left blank. +# This is provided to overwrite existing abbr.fish files, so that any abbr +# function retained from past fish releases does not override the abbr builtin. diff --git a/src/abbrs.cpp b/src/abbrs.cpp new file mode 100644 index 000000000..435f0830a --- /dev/null +++ b/src/abbrs.cpp @@ -0,0 +1,62 @@ +#include "config.h" // IWYU pragma: keep + +#include "abbrs.h" + +#include "env.h" +#include "global_safety.h" +#include "wcstringutil.h" + +static relaxed_atomic_t k_abbrs_next_order{0}; + +abbreviation_t::abbreviation_t(wcstring replacement, abbrs_position_t position, bool from_universal) + : replacement(std::move(replacement)), + position(position), + from_universal(from_universal), + order(++k_abbrs_next_order) {} + +acquired_lock abbrs_get_map() { + static owning_lock> abbrs; + return abbrs.acquire(); +} + +maybe_t abbrs_expand(const wcstring &token, abbrs_position_t position) { + auto abbrs = abbrs_get_map(); + auto iter = abbrs->find(token); + maybe_t result{}; + if (iter != abbrs->end()) { + const abbreviation_t &abbr = iter->second; + // Expand only if the positions are "compatible." + if (abbr.position == position || abbr.position == abbrs_position_t::anywhere) { + result = abbr.replacement; + } + } + return result; +} + +wcstring_list_t abbrs_get_keys() { + auto abbrs = abbrs_get_map(); + wcstring_list_t keys; + keys.reserve(abbrs->size()); + for (const auto &kv : *abbrs) { + keys.push_back(kv.first); + } + return keys; +} + +void abbrs_import_from_uvars(const std::unordered_map &uvars) { + auto abbrs = abbrs_get_map(); + const wchar_t *const prefix = L"_fish_abbr_"; + size_t prefix_len = wcslen(prefix); + wcstring name; + const bool from_universal = true; + for (const auto &kv : uvars) { + if (string_prefixes_string(prefix, kv.first)) { + wcstring escaped_name = kv.first.substr(prefix_len); + if (unescape_string(escaped_name, &name, unescape_flags_t{}, STRING_STYLE_VAR)) { + wcstring replacement = join_strings(kv.second.as_list(), L' '); + abbrs->emplace(name, abbreviation_t{std::move(replacement), + abbrs_position_t::command, from_universal}); + } + } + } +} diff --git a/src/abbrs.h b/src/abbrs.h new file mode 100644 index 000000000..4ab06f557 --- /dev/null +++ b/src/abbrs.h @@ -0,0 +1,53 @@ +// Support for abbreviations. +// +#ifndef FISH_ABBRS_H +#define FISH_ABBRS_H + +#include + +#include "common.h" +#include "maybe.h" + +/// Controls where in the command line abbreviations may expand. +enum class abbrs_position_t : uint8_t { + command, // expand in command position + anywhere, // expand in any token +}; + +struct abbreviation_t { + // Replacement string. + wcstring replacement{}; + + // Expansion position. + abbrs_position_t position{abbrs_position_t::command}; + + // Mark if we came from a universal variable. + bool from_universal{}; + + // A monotone key to allow reconstructing definition order. + uint64_t order{}; + + explicit abbreviation_t(wcstring replacement, + abbrs_position_t position = abbrs_position_t::command, + bool from_universal = false); + + abbreviation_t() = default; +}; + +using abbrs_map_t = std::unordered_map; + +/// \return the mutable map of abbreviations, keyed by name. +acquired_lock abbrs_get_map(); + +/// \return the replacement value for a abbreviation token, if any. +/// The \p position is given to describe where the token was found. +maybe_t abbrs_expand(const wcstring &token, abbrs_position_t position); + +/// \return the list of abbreviation keys. +wcstring_list_t abbrs_get_keys(); + +/// Import any abbreviations from universal variables. +class env_var_t; +void abbrs_import_from_uvars(const std::unordered_map &uvars); + +#endif diff --git a/src/builtin.cpp b/src/builtin.cpp index e9b8c61a1..8b8a0cba8 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -29,6 +29,7 @@ #include #include +#include "builtins/abbr.h" #include "builtins/argparse.h" #include "builtins/bg.h" #include "builtins/bind.h" @@ -354,6 +355,7 @@ static constexpr builtin_data_t builtin_datas[] = { {L":", &builtin_true, N_(L"Return a successful result")}, {L"[", &builtin_test, N_(L"Test a condition")}, {L"_", &builtin_gettext, N_(L"Translate a string")}, + {L"abbr", &builtin_abbr, N_(L"Manage generics")}, {L"and", &builtin_generic, N_(L"Run command if last command succeeded")}, {L"argparse", &builtin_argparse, N_(L"Parse options in fish script")}, {L"begin", &builtin_generic, N_(L"Create a block of code")}, diff --git a/src/builtins/abbr.cpp b/src/builtins/abbr.cpp new file mode 100644 index 000000000..cf0420f37 --- /dev/null +++ b/src/builtins/abbr.cpp @@ -0,0 +1,327 @@ +// Implementation of the read builtin. +#include "config.h" // IWYU pragma: keep + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../abbrs.h" +#include "../builtin.h" +#include "../common.h" +#include "../env.h" +#include "../io.h" +#include "../wcstringutil.h" +#include "../wgetopt.h" +#include "../wutil.h" + +namespace { + +static const wchar_t *const CMD = L"abbr"; + +struct abbr_options_t { + bool add{}; + bool rename{}; + bool show{}; + bool list{}; + bool erase{}; + bool query{}; + maybe_t position{}; + + wcstring_list_t args; + + bool validate(io_streams_t &streams) { + // Duplicate options? + wcstring_list_t cmds; + if (add) cmds.push_back(L"add"); + if (rename) cmds.push_back(L"rename"); + if (show) cmds.push_back(L"show"); + if (list) cmds.push_back(L"list"); + if (erase) cmds.push_back(L"erase"); + if (query) cmds.push_back(L"query"); + if (cmds.size() > 1) { + streams.err.append_format(_(L"%ls: Cannot combine options %ls\n"), CMD, + join_strings(cmds, L", ").c_str()); + return false; + } + + // If run with no options, treat it like --add if we have arguments, + // or --show if we do not have any arguments. + if (cmds.empty()) { + show = args.empty(); + add = !args.empty(); + } + + if (!add && position.has_value()) { + streams.err.append_format(_(L"%ls: --position option requires --add\n"), CMD); + return false; + } + return true; + } +}; + +// Print abbreviations in a fish-script friendly way. +static int abbr_show(const abbr_options_t &, io_streams_t &streams) { + const auto abbrs = abbrs_get_map(); + for (const auto &kv : *abbrs) { + wcstring name = escape_string(kv.first); + const auto &abbr = kv.second; + wcstring value = escape_string(abbr.replacement); + const wchar_t *scope = (abbr.from_universal ? L"-U " : L""); + streams.out.append_format(L"abbr -a %ls-- %ls %ls\n", scope, name.c_str(), value.c_str()); + } + return STATUS_CMD_OK; +} + +// Print the list of abbreviation names. +static int abbr_list(const abbr_options_t &opts, io_streams_t &streams) { + const wchar_t *const subcmd = L"--list"; + if (opts.args.size() > 0) { + streams.err.append_format(_(L"%ls %ls: Unexpected argument -- '%ls'\n"), CMD, subcmd, + opts.args.front().c_str()); + return STATUS_INVALID_ARGS; + } + const auto abbrs = abbrs_get_map(); + for (const auto &kv : *abbrs) { + wcstring name = escape_string(kv.first); + name.push_back(L'\n'); + streams.out.append(name); + } + return STATUS_CMD_OK; +} + +// Rename an abbreviation, deleting any existing one with the given name. +static int abbr_rename(const abbr_options_t &opts, io_streams_t &streams) { + const wchar_t *const subcmd = L"--rename"; + if (opts.args.size() != 2) { + streams.err.append_format(_(L"%ls %ls: Requires exactly two arguments\n"), CMD, subcmd); + return STATUS_INVALID_ARGS; + } + const wcstring &old_name = opts.args[0]; + const wcstring &new_name = opts.args[1]; + if (old_name.empty() || new_name.empty()) { + streams.err.append_format(_(L"%ls %ls: Name cannot be empty\n"), CMD, subcmd); + return STATUS_INVALID_ARGS; + } + + if (std::any_of(new_name.begin(), new_name.end(), iswspace)) { + streams.err.append_format( + _(L"%ls %ls: Abbreviation '%ls' cannot have spaces in the word\n"), CMD, subcmd, + new_name.c_str()); + return STATUS_INVALID_ARGS; + } + auto abbrs = abbrs_get_map(); + + auto old_iter = abbrs->find(old_name); + if (old_iter == abbrs->end()) { + streams.err.append_format(_(L"%ls %ls: No abbreviation named %ls\n"), CMD, subcmd, + old_name.c_str()); + return STATUS_CMD_ERROR; + } + + // Cannot overwrite an abbreviation. + auto new_iter = abbrs->find(new_name); + if (new_iter != abbrs->end()) { + streams.err.append_format( + _(L"%ls %ls: Abbreviation %ls already exists, cannot rename %ls\n"), CMD, subcmd, + new_name.c_str(), old_name.c_str()); + return STATUS_INVALID_ARGS; + } + + abbreviation_t abbr = std::move(old_iter->second); + abbrs->erase(old_iter); + bool inserted = abbrs->insert(std::make_pair(std::move(new_name), std::move(abbr))).second; + assert(inserted && "Should have successfully inserted"); + (void)inserted; + return STATUS_CMD_OK; +} + +// Test if any args is an abbreviation. +static int abbr_query(const abbr_options_t &opts, io_streams_t &) { + // Return success if any of our args matches an abbreviation. + const auto abbrs = abbrs_get_map(); + for (const auto &arg : opts.args) { + if (abbrs->find(arg) != abbrs->end()) { + return STATUS_CMD_OK; + } + } + return STATUS_CMD_ERROR; +} + +// Add a named abbreviation. +static int abbr_add(const abbr_options_t &opts, io_streams_t &streams) { + const wchar_t *const subcmd = L"--add"; + if (opts.args.size() < 2) { + streams.err.append_format(_(L"%ls %ls: Requires at least two arguments\n"), CMD, subcmd); + return STATUS_INVALID_ARGS; + } + const wcstring &name = opts.args[0]; + if (name.empty()) { + streams.err.append_format(_(L"%ls %ls: Name cannot be empty\n"), CMD, subcmd); + return STATUS_INVALID_ARGS; + } + if (std::any_of(name.begin(), name.end(), iswspace)) { + streams.err.append_format( + _(L"%ls %ls: Abbreviation '%ls' cannot have spaces in the word\n"), CMD, subcmd, + name.c_str()); + return STATUS_INVALID_ARGS; + } + abbreviation_t abbr{L""}; + for (auto iter = opts.args.begin() + 1; iter != opts.args.end(); ++iter) { + if (!abbr.replacement.empty()) abbr.replacement.push_back(L' '); + abbr.replacement.append(*iter); + } + if (opts.position) { + abbr.position = *opts.position; + } + + // Note historically we have allowed overwriting existing abbreviations. + auto abbrs = abbrs_get_map(); + (*abbrs)[name] = std::move(abbr); + return STATUS_CMD_OK; +} + +// Erase the named abbreviations. +static int abbr_erase(const abbr_options_t &opts, io_streams_t &) { + if (opts.args.empty()) { + // This has historically been a silent failure. + return STATUS_CMD_ERROR; + } + + // Erase each. If any is not found, return ENV_NOT_FOUND which is historical. + int result = STATUS_CMD_OK; + auto abbrs = abbrs_get_map(); + for (const auto &arg : opts.args) { + auto iter = abbrs->find(arg); + if (iter == abbrs->end()) { + result = ENV_NOT_FOUND; + } else { + abbrs->erase(iter); + } + } + return result; +} + +} // namespace + +maybe_t builtin_abbr(parser_t &parser, io_streams_t &streams, const wchar_t **argv) { + const wchar_t *cmd = argv[0]; + abbr_options_t opts; + // Note 1 is returned by wgetopt to indicate a non-option argument. + enum { NON_OPTION_ARGUMENT = 1 }; + + // Note the leading '-' causes wgetopter to return arguments in order, instead of permuting + // them. We need this behavior for compatibility with pre-builtin abbreviations where options + // could be given literally, for example `abbr e emacs -nw`. + static const wchar_t *const short_options = L"-arseqgUh"; + static const struct woption long_options[] = {{L"add", no_argument, 'a'}, + {L"position", required_argument, 'p'}, + {L"rename", no_argument, 'r'}, + {L"erase", no_argument, 'e'}, + {L"query", no_argument, 'q'}, + {L"show", no_argument, 's'}, + {L"list", no_argument, 'l'}, + {L"global", no_argument, 'g'}, + {L"universal", no_argument, 'U'}, + {L"help", no_argument, 'h'}, + {}}; + + int argc = builtin_count_args(argv); + int opt; + wgetopter_t w; + bool unrecognized_options_are_args = false; + while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) { + switch (opt) { + case NON_OPTION_ARGUMENT: + // Non-option argument. + // If --add is specified (or implied by specifying no other commands), all + // unrecognized options after the *second* non-option argument are considered part + // of the abbreviation expansion itself, rather than options to the abbr command. + // For example, `abbr e emacs -nw` works, because `-nw` occurs after the second + // non-option, and --add is implied. + opts.args.push_back(w.woptarg); + if (opts.args.size() >= 2 && + !(opts.rename || opts.show || opts.list || opts.erase || opts.query)) { + unrecognized_options_are_args = true; + } + break; + case 'a': + opts.add = true; + break; + case 'p': { + if (opts.position.has_value()) { + streams.err.append_format(_(L"%ls: Cannot specify multiple positions\n"), CMD); + return STATUS_INVALID_ARGS; + } + if (!wcscmp(w.woptarg, L"command")) { + opts.position = abbrs_position_t::command; + } else if (!wcscmp(w.woptarg, L"anywhere")) { + opts.position = abbrs_position_t::anywhere; + } else { + streams.err.append_format(_(L"%ls: Invalid position '%ls'\nPosition must be " + L"one of: command, anywhere.\n"), + CMD, w.woptarg); + return STATUS_INVALID_ARGS; + } + break; + } + case 'r': + opts.rename = true; + break; + case 'e': + opts.erase = true; + break; + case 'q': + opts.query = true; + break; + case 's': + opts.show = true; + break; + case 'l': + opts.list = true; + break; + case 'g': + case 'U': + // Kept for backwards compatibility but ignored. + break; + case 'h': { + builtin_print_help(parser, streams, cmd); + return STATUS_CMD_OK; + } + case '?': { + if (unrecognized_options_are_args) { + opts.args.push_back(argv[w.woptind - 1]); + } else { + builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]); + return STATUS_INVALID_ARGS; + } + } + } + } + opts.args.insert(opts.args.end(), argv + w.woptind, argv + argc); + if (!opts.validate(streams)) { + return STATUS_INVALID_ARGS; + } + + if (opts.add) return abbr_add(opts, streams); + if (opts.show) return abbr_show(opts, streams); + if (opts.list) return abbr_list(opts, streams); + if (opts.rename) return abbr_rename(opts, streams); + if (opts.erase) return abbr_erase(opts, streams); + if (opts.query) return abbr_query(opts, streams); + + // validate() should error or ensure at least one path is set. + DIE("unreachable"); + return STATUS_INVALID_ARGS; +} \ No newline at end of file diff --git a/src/builtins/abbr.h b/src/builtins/abbr.h new file mode 100644 index 000000000..4a1dc883b --- /dev/null +++ b/src/builtins/abbr.h @@ -0,0 +1,11 @@ +// Prototypes for executing builtin_abbr function. +#ifndef FISH_BUILTIN_ABBR_H +#define FISH_BUILTIN_ABBR_H + +#include "../maybe.h" + +class parser_t; +struct io_streams_t; + +maybe_t builtin_abbr(parser_t &parser, io_streams_t &streams, const wchar_t **argv); +#endif diff --git a/src/complete.cpp b/src/complete.cpp index 5cfab9712..c1ef81b2e 100644 --- a/src/complete.cpp +++ b/src/complete.cpp @@ -24,6 +24,7 @@ #include #include +#include "abbrs.h" #include "autoload.h" #include "builtin.h" #include "common.h" @@ -668,7 +669,8 @@ void completer_t::complete_cmd(const wcstring &str_cmd) { } void completer_t::complete_abbr(const wcstring &cmd) { - std::map abbrs = get_abbreviations(ctx.vars); + // Copy the map, so we don't hold the lock across the call to complete_strings. + abbrs_map_t abbrs = *abbrs_get_map(); completion_list_t possible_comp; possible_comp.reserve(abbrs.size()); for (const auto &kv : abbrs) { @@ -678,7 +680,7 @@ void completer_t::complete_abbr(const wcstring &cmd) { 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()); + return format_string(ABBR_DESC, iter->second.replacement.c_str()); }; this->complete_strings(cmd, desc_func, possible_comp, COMPLETE_NO_SPACE); } diff --git a/src/env.cpp b/src/env.cpp index 282935dbb..2da05949d 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -18,6 +18,7 @@ #include #include +#include "abbrs.h" #include "common.h" #include "env_dispatch.h" #include "env_universal_common.h" @@ -452,6 +453,10 @@ void env_init(const struct config_paths_t *paths, bool do_uvars, bool default_pa vars.globals().remove(name, ENV_GLOBAL | ENV_EXPORT); } } + + // Import any abbreviations from uvars. + // Note we do not dynamically react to changes. + abbrs_import_from_uvars(table); } } diff --git a/src/expand.cpp b/src/expand.cpp index 02ceda44e..e04077d2b 100644 --- a/src/expand.cpp +++ b/src/expand.cpp @@ -1326,31 +1326,3 @@ bool fish_xdm_login_hack_hack_hack_hack(std::vector *cmds, int argc } return result; } - -maybe_t expand_abbreviation(const wcstring &src, const environment_t &vars) { - if (src.empty()) return none(); - - wcstring esc_src = escape_string(src, 0, STRING_STYLE_VAR); - if (esc_src.empty()) { - return none(); - } - wcstring var_name = L"_fish_abbr_" + esc_src; - if (auto var_value = vars.get(var_name)) { - return var_value->as_string(); - } - return none(); -} - -std::map get_abbreviations(const environment_t &vars) { - const wcstring prefix = L"_fish_abbr_"; - auto names = vars.get_names(0); - std::map result; - for (const wcstring &name : names) { - if (string_prefixes_string(prefix, name)) { - wcstring key; - unescape_string(name.substr(prefix.size()), &key, UNESCAPE_DEFAULT, STRING_STYLE_VAR); - result[key] = vars.get(name)->as_string(); - } - } - return result; -} diff --git a/src/expand.h b/src/expand.h index 5649abcb4..22f7a37b9 100644 --- a/src/expand.h +++ b/src/expand.h @@ -203,14 +203,6 @@ void expand_tilde(wcstring &input, const environment_t &vars); /// Perform the opposite of tilde expansion on the string, which is modified in place. wcstring replace_home_directory_with_tilde(const wcstring &str, const environment_t &vars); -/// Abbreviation support. Expand src as an abbreviation, returning the expanded form if found, -/// none() if not. -maybe_t expand_abbreviation(const wcstring &src, const environment_t &vars); - -/// \return a snapshot of all abbreviations as a map abbreviation->expansion. -/// The abbreviations are unescaped, i.e. they may not be valid variable identifiers (#6166). -std::map get_abbreviations(const environment_t &vars); - // Terrible hacks bool fish_xdm_login_hack_hack_hack_hack(std::vector *cmds, int argc, const char *const *argv); diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index 75dabd33a..07f79271b 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -46,6 +46,7 @@ #include #endif +#include "abbrs.h" #include "ast.h" #include "autoload.h" #include "builtin.h" @@ -2466,34 +2467,31 @@ static void test_ifind_fuzzy() { static void test_abbreviations() { say(L"Testing abbreviations"); - auto &vars = parser_t::principal_parser().vars(); - vars.push(true); - const std::vector> abbreviations = { - {L"gc", L"git checkout"}, - {L"foo", L"bar"}, - {L"gx", L"git checkout"}, - }; - for (const auto &kv : abbreviations) { - int ret = vars.set_one(L"_fish_abbr_" + kv.first, ENV_LOCAL, kv.second); - if (ret != 0) err(L"Unable to set abbreviation variable"); + { + auto abbrs = abbrs_get_map(); + abbrs->emplace(L"gc", L"git checkout"); + abbrs->emplace(L"foo", L"bar"); + abbrs->emplace(L"gx", L"git checkout"); + abbrs->emplace(L"yin", abbreviation_t(L"yang", abbrs_position_t::anywhere)); } - if (expand_abbreviation(L"", vars)) err(L"Unexpected success with empty abbreviation"); - if (expand_abbreviation(L"nothing", vars)) err(L"Unexpected success with missing abbreviation"); + auto cmd = abbrs_position_t::command; + if (abbrs_expand(L"", cmd)) err(L"Unexpected success with empty abbreviation"); + if (abbrs_expand(L"nothing", cmd)) err(L"Unexpected success with missing abbreviation"); - auto mresult = expand_abbreviation(L"gc", vars); + auto mresult = abbrs_expand(L"gc", cmd); if (!mresult) err(L"Unexpected failure with gc abbreviation"); if (*mresult != L"git checkout") err(L"Wrong abbreviation result for gc"); - mresult = expand_abbreviation(L"foo", vars); + mresult = abbrs_expand(L"foo", cmd); if (!mresult) err(L"Unexpected failure with foo abbreviation"); if (*mresult != L"bar") err(L"Wrong abbreviation result for foo"); maybe_t result; - auto expand_abbreviation_in_command = [](const wcstring &cmdline, size_t cursor_pos, - const environment_t &vars) -> maybe_t { - if (auto edit = reader_expand_abbreviation_in_command(cmdline, cursor_pos, vars)) { + auto expand_abbreviation_in_command = [](const wcstring &cmdline, + size_t cursor_pos) -> maybe_t { + if (auto edit = reader_expand_abbreviation_at_cursor(cmdline, cursor_pos)) { wcstring cmdline_expanded = cmdline; std::vector colors{cmdline_expanded.size()}; apply_edit(&cmdline_expanded, &colors, *edit); @@ -2501,49 +2499,55 @@ static void test_abbreviations() { } return none_t(); }; - result = expand_abbreviation_in_command(L"just a command", 3, vars); + result = expand_abbreviation_in_command(L"just a command", 3); if (result) err(L"Command wrongly expanded on line %ld", (long)__LINE__); - result = expand_abbreviation_in_command(L"gc somebranch", 0, vars); + result = expand_abbreviation_in_command(L"gc somebranch", 0); if (!result) err(L"Command not expanded on line %ld", (long)__LINE__); - result = expand_abbreviation_in_command(L"gc somebranch", const_strlen(L"gc"), vars); + result = expand_abbreviation_in_command(L"gc somebranch", const_strlen(L"gc")); if (!result) err(L"gc not expanded"); if (result != L"git checkout somebranch") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); // Space separation. - result = expand_abbreviation_in_command(L"gx somebranch", const_strlen(L"gc"), vars); + result = expand_abbreviation_in_command(L"gx somebranch", const_strlen(L"gc")); if (!result) err(L"gx not expanded"); if (result != L"git checkout somebranch") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); - result = expand_abbreviation_in_command(L"echo hi ; gc somebranch", - const_strlen(L"echo hi ; g"), vars); + result = + expand_abbreviation_in_command(L"echo hi ; gc somebranch", const_strlen(L"echo hi ; g")); if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); if (result != L"echo hi ; git checkout somebranch") err(L"gc incorrectly expanded on line %ld", (long)__LINE__); result = expand_abbreviation_in_command(L"echo (echo (echo (echo (gc ", - const_strlen(L"echo (echo (echo (echo (gc"), vars); + const_strlen(L"echo (echo (echo (echo (gc")); if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); if (result != L"echo (echo (echo (echo (git checkout ") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); // If commands should be expanded. - result = expand_abbreviation_in_command(L"if gc", const_strlen(L"if gc"), vars); + result = expand_abbreviation_in_command(L"if gc", const_strlen(L"if gc")); if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); if (result != L"if git checkout") err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str()); // Others should not be. - result = expand_abbreviation_in_command(L"of gc", const_strlen(L"of gc"), vars); + result = expand_abbreviation_in_command(L"of gc", const_strlen(L"of gc")); if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__); // Others should not be. - result = expand_abbreviation_in_command(L"command gc", const_strlen(L"command gc"), vars); + result = expand_abbreviation_in_command(L"command gc", const_strlen(L"command gc")); if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__); - vars.pop(); + // yin/yang expands everywhere. + result = expand_abbreviation_in_command(L"command yin", const_strlen(L"command yin")); + if (!result) err(L"gc not expanded on line %ld", (long)__LINE__); + if (result != L"command yang") { + err(L"command yin incorrectly expanded on line %ld to '%ls'", (long)__LINE__, + result->c_str()); + } } /// Test path functions. @@ -3502,11 +3506,9 @@ static void test_complete() { completions.clear(); // Test abbreviations. - auto &pvars = parser_t::principal_parser().vars(); function_add(L"testabbrsonetwothreefour", func_props); - int ret = pvars.set_one(L"_fish_abbr_testabbrsonetwothreezero", ENV_LOCAL, L"expansion"); + abbrs_get_map()->emplace(L"testabbrsonetwothreezero", L"expansion"); completions = complete(L"testabbrsonetwothree", {}, parser->context()); - do_test(ret == 0); do_test(completions.size() == 2); do_test(completions.at(0).completion == L"four"); do_test((completions.at(0).flags & COMPLETE_NO_SPACE) == 0); diff --git a/src/highlight.cpp b/src/highlight.cpp index 7645573b3..680cd9466 100644 --- a/src/highlight.cpp +++ b/src/highlight.cpp @@ -16,6 +16,7 @@ #include #include +#include "abbrs.h" #include "ast.h" #include "builtin.h" #include "color.h" @@ -1334,7 +1335,8 @@ static bool command_is_valid(const wcstring &cmd, enum statement_decoration_t de if (!is_valid && function_ok) is_valid = function_exists_no_autoload(cmd); // Abbreviations - if (!is_valid && abbreviation_ok) is_valid = expand_abbreviation(cmd, vars).has_value(); + if (!is_valid && abbreviation_ok) + is_valid = abbrs_expand(cmd, abbrs_position_t::command).has_value(); // Regular commands if (!is_valid && command_ok) is_valid = path_get_path(cmd, vars).has_value(); diff --git a/src/reader.cpp b/src/reader.cpp index 691850c97..1a3b8ee09 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -44,6 +44,7 @@ #include #include +#include "abbrs.h" #include "ast.h" #include "color.h" #include "common.h" @@ -1347,10 +1348,8 @@ void reader_data_t::pager_selection_changed() { } /// Expand abbreviations at the given cursor position. Does NOT inspect 'data'. -maybe_t reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos, - const environment_t &vars) { - // See if we are at "command position". Get the surrounding command substitution, and get the - // extent of the first token. +maybe_t reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos) { + // Get the surrounding command substitution. const wchar_t *const buff = cmdline.c_str(); const wchar_t *cmdsub_begin = nullptr, *cmdsub_end = nullptr; parse_util_cmdsubst_extent(buff, cursor_pos, &cmdsub_begin, &cmdsub_end); @@ -1369,40 +1368,45 @@ maybe_t reader_expand_abbreviation_in_command(const wcstring &cmdline, s ast_t::parse(subcmd, parse_flag_continue_after_error | parse_flag_accept_incomplete_tokens | parse_flag_leave_unterminated); - // Look for plain statements where the cursor is at the end of the command. - const ast::string_t *matching_cmd_node = nullptr; - for (const node_t &n : ast) { - const auto *stmt = n.try_as(); - if (!stmt) continue; - - // Skip if we have a decoration. - if (stmt->opt_decoration) continue; - - // See if the command's source range range contains our cursor, including at the end. - auto msource = stmt->command.try_source_range(); - if (!msource) continue; - - // Now see if its source range contains our cursor, including at the end. - if (subcmd_cursor_pos >= msource->start && - subcmd_cursor_pos <= msource->start + msource->length) { - // Success! - matching_cmd_node = &stmt->command; - break; + // Find a leaf node where the cursor is at its end. + const node_t *leaf = nullptr; + traversal_t tv = ast.walk(); + while (const node_t *node = tv.next()) { + if (node->category == category_t::leaf) { + auto r = node->try_source_range(); + if (r && r->start <= subcmd_cursor_pos && subcmd_cursor_pos <= r->end()) { + leaf = node; + break; + } } } + if (!leaf) { + return none(); + } - // Now if we found a command node, expand it. - maybe_t result{}; - if (matching_cmd_node) { - assert(!matching_cmd_node->unsourced && "Should not be unsourced"); - const wcstring token = matching_cmd_node->source(subcmd); - if (auto abbreviation = expand_abbreviation(token, vars)) { - // There was an abbreviation! Replace the token in the full command. Maintain the - // relative position of the cursor. - source_range_t r = matching_cmd_node->source_range(); - result = edit_t(subcmd_offset + r.start, r.length, std::move(*abbreviation)); + // We found the leaf node before the cursor. + // Decide if this leaf is in "command position:" it is the command part of an undecorated + // statement. + bool leaf_is_command = false; + for (const node_t *cursor = leaf; cursor; cursor = cursor->parent) { + if (const auto *stmt = cursor->try_as()) { + if (!stmt->opt_decoration && leaf == &stmt->command) { + leaf_is_command = true; + break; + } } } + abbrs_position_t expand_position = + leaf_is_command ? abbrs_position_t::command : abbrs_position_t::anywhere; + + // Now we can expand the abbreviation. + maybe_t result{}; + if (auto abbreviation = abbrs_expand(leaf->source(subcmd), expand_position)) { + // There was an abbreviation! Replace the token in the full command. Maintain the + // relative position of the cursor. + source_range_t r = leaf->source_range(); + result = edit_t(subcmd_offset + r.start, r.length, abbreviation.acquire()); + } return result; } @@ -1417,7 +1421,7 @@ bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) { // Try expanding abbreviations. size_t cursor_pos = el->position() - std::min(el->position(), cursor_backtrack); - if (auto edit = reader_expand_abbreviation_in_command(el->text(), cursor_pos, vars())) { + if (auto edit = reader_expand_abbreviation_at_cursor(el->text(), cursor_pos)) { push_edit(el, std::move(*edit)); update_buff_pos(el); result = true; diff --git a/src/reader.h b/src/reader.h index 7df5f1d27..284fb91a1 100644 --- a/src/reader.h +++ b/src/reader.h @@ -263,8 +263,7 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline, /// Expand abbreviations at the given cursor position. Exposed for testing purposes only. /// \return none if no abbreviations were expanded, otherwise the new command line. -maybe_t reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos, - const environment_t &vars); +maybe_t reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos); /// Apply a completion string. Exposed for testing only. wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags, diff --git a/src/wcstringutil.cpp b/src/wcstringutil.cpp index 2fa6412ff..d4a6810ee 100644 --- a/src/wcstringutil.cpp +++ b/src/wcstringutil.cpp @@ -286,12 +286,12 @@ wcstring_list_t split_string_tok(const wcstring &val, const wcstring &seps, size return out; } -wcstring join_strings(const wcstring_list_t &vals, wchar_t sep) { +static wcstring join_strings_impl(const wcstring_list_t &vals, const wchar_t *sep, size_t seplen) { if (vals.empty()) return wcstring{}; // Reserve the size we will need. // count-1 separators, plus the length of all strings. - size_t size = vals.size() - 1; + size_t size = (vals.size() - 1) * seplen; for (const wcstring &s : vals) { size += s.size(); } @@ -302,7 +302,7 @@ wcstring join_strings(const wcstring_list_t &vals, wchar_t sep) { bool first = true; for (const wcstring &s : vals) { if (!first) { - result.push_back(sep); + result.append(sep, seplen); } result.append(s); first = false; @@ -310,6 +310,14 @@ wcstring join_strings(const wcstring_list_t &vals, wchar_t sep) { return result; } +wcstring join_strings(const wcstring_list_t &vals, wchar_t c) { + return join_strings_impl(vals, &c, 1); +} + +wcstring join_strings(const wcstring_list_t &vals, const wchar_t *sep) { + return join_strings_impl(vals, sep, wcslen(sep)); +} + void wcs2string_bad_char(wchar_t wc) { FLOGF(char_encoding, L"Wide character U+%4X has no narrow representation", wc); } diff --git a/src/wcstringutil.h b/src/wcstringutil.h index 4a768c0bd..566b3f931 100644 --- a/src/wcstringutil.h +++ b/src/wcstringutil.h @@ -137,8 +137,9 @@ wcstring_list_t split_string(const wcstring &val, wchar_t sep); wcstring_list_t split_string_tok(const wcstring &val, const wcstring &seps, size_t max_results = std::numeric_limits::max()); -/// Join a list of strings by a separator character. +/// Join a list of strings by a separator character or string. wcstring join_strings(const wcstring_list_t &vals, wchar_t sep); +wcstring join_strings(const wcstring_list_t &vals, const wchar_t *sep); inline wcstring to_string(long x) { wchar_t buff[64]; diff --git a/tests/checks/abbr.fish b/tests/checks/abbr.fish index edb25a8d6..9eb0d02e8 100644 --- a/tests/checks/abbr.fish +++ b/tests/checks/abbr.fish @@ -1,29 +1,36 @@ #RUN: %fish %s + +# Universal abbreviations are imported. +set -U _fish_abbr_cuckoo somevalue +set fish (status fish-path) +$fish -c abbr +# CHECK: abbr -a -U -- cuckoo somevalue + # Test basic add and list of __abbr1 abbr __abbr1 alpha beta gamma abbr | grep __abbr1 -# CHECK: abbr -a -U -- __abbr1 'alpha beta gamma' +# CHECK: abbr -a -- __abbr1 'alpha beta gamma' # Erasing one that doesn\'t exist should do nothing abbr --erase NOT_AN_ABBR abbr | grep __abbr1 -# CHECK: abbr -a -U -- __abbr1 'alpha beta gamma' +# CHECK: abbr -a -- __abbr1 'alpha beta gamma' # Adding existing __abbr1 should be idempotent abbr __abbr1 alpha beta gamma abbr | grep __abbr1 -# CHECK: abbr -a -U -- __abbr1 'alpha beta gamma' +# CHECK: abbr -a -- __abbr1 'alpha beta gamma' # Replacing __abbr1 definition abbr __abbr1 delta abbr | grep __abbr1 -# CHECK: abbr -a -U -- __abbr1 delta +# CHECK: abbr -a -- __abbr1 delta # __abbr1 -s and --show tests abbr -s | grep __abbr1 abbr --show | grep __abbr1 -# CHECK: abbr -a -U -- __abbr1 delta -# CHECK: abbr -a -U -- __abbr1 delta +# CHECK: abbr -a -- __abbr1 delta +# CHECK: abbr -a -- __abbr1 delta # Test erasing __abbr1 abbr -e __abbr1 @@ -32,13 +39,13 @@ abbr | grep __abbr1 # Ensure we escape special characters on output abbr '~__abbr2' '$xyz' abbr | grep __abbr2 -# CHECK: abbr -a -U -- '~__abbr2' '$xyz' +# CHECK: abbr -a -- '~__abbr2' '$xyz' abbr -e '~__abbr2' # Ensure we handle leading dashes in abbreviation names properly abbr -- --__abbr3 xyz abbr | grep __abbr3 -# CHECK: abbr -a -U -- --__abbr3 xyz +# CHECK: abbr -a -- --__abbr3 xyz abbr -e -- --__abbr3 # Test that an abbr word containing spaces is rejected @@ -51,7 +58,7 @@ abbr __abbr4 omega abbr | grep __abbr5 abbr -r __abbr4 __abbr5 abbr | grep __abbr5 -# CHECK: abbr -a -U -- __abbr5 omega +# CHECK: abbr -a -- __abbr5 omega abbr -e __abbr5 abbr | grep __abbr4 @@ -77,7 +84,7 @@ abbr -r __abbr8 __abbr9 __abbr10 abbr | grep __abbr8 abbr | grep __abbr9 abbr | grep __abbr10 -# CHECK: abbr -a -U -- __abbr8 omega +# CHECK: abbr -a -- __abbr8 omega # Test renaming to existing abbreviation abbr __abbr11 omega11 @@ -106,3 +113,22 @@ echo $status abbr -q banana __abbr8 foobar echo $status # CHECK: 0 + +abbr --add grape --position nowhere juice +echo $status +# CHECKERR: abbr: Invalid position 'nowhere' +# CHECKERR: Position must be one of: command, anywhere. +# CHECK: 2 + +abbr --add grape --position anywhere juice +echo $status +# CHECK: 0 + +abbr --add grape --position command juice +echo $status +# CHECK: 0 + +abbr --query banana --position anywhere +echo $status +# CHECKERR: abbr: --position option requires --add +# CHECK: 2 diff --git a/tests/pexpects/abbrs.py b/tests/pexpects/abbrs.py index 8e215f3df..41364e274 100644 --- a/tests/pexpects/abbrs.py +++ b/tests/pexpects/abbrs.py @@ -19,13 +19,13 @@ sendline(r"""bind '?' 'echo "<$(commandline)>"; commandline ""'; """) expect_prompt() # Basic test. +# Default abbreviations expand only in command position. sendline(r"abbr alpha beta") expect_prompt() send(r"alpha ?") expect_str(r"") -# Default abbreviations expand only in command position. send(r"echo alpha ?") expect_str(r"") @@ -50,3 +50,12 @@ sendline(r"echo )") expect_str(r"Unexpected ')' for unopened parenthesis") send(r"?") expect_str(r"") + +# Support position anywhere. +sendline(r"abbr alpha --position anywhere beta2") + +send(r"alpha ?") +expect_str(r"") + +send(r"echo alpha ?") +expect_str(r"")