From 17dff8c569a4bbbe88d23c77e043574d15804093 Mon Sep 17 00:00:00 2001 From: Kurtis Rader Date: Sun, 21 May 2017 22:22:55 -0700 Subject: [PATCH] rewrite `abbr` function Rewrite the `abbr` function to store each abbreviation in a separate variable. This greatly improves the efficiency. For the common case it is 5x faster. For pathological cases it is upwards of 100x faster. Most people should be able to unconditionally define abbreviations in their config.fish without a noticable slow down. Fixes #4048 --- CHANGELOG.md | 1 + doc_src/abbr.txt | 39 +- .../functions/__fish_config_interactive.fish | 9 + share/functions/abbr.fish | 397 +++++++++--------- share/functions/abbr_old.fish | 206 +++++++++ src/env.cpp | 34 +- src/expand.cpp | 56 +-- src/expand.h | 3 +- src/fish_tests.cpp | 20 +- tests/abbr.err | 29 +- tests/abbr.in | 52 +-- tests/abbr.out | 35 +- tests/functions.in | 2 +- 13 files changed, 561 insertions(+), 322 deletions(-) create mode 100644 share/functions/abbr_old.fish diff --git a/CHANGELOG.md b/CHANGELOG.md index 340e7f25f..e055f52a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This section is for changes merged to the `major` branch that are not also merge ## Other significant changes - `read` now requires at least one var name (#4220). +- `abbr` has been reimplemented to be faster. This means the old `fish_user_abbreviations` variable is ignored (#4048). # fish 2.7b1 diff --git a/doc_src/abbr.txt b/doc_src/abbr.txt index c1fd717ee..18e209c2a 100644 --- a/doc_src/abbr.txt +++ b/doc_src/abbr.txt @@ -2,54 +2,57 @@ \subsection abbr-synopsis Synopsis \fish{synopsis} -abbr --add word phrase... -abbr --rename word new_word +abbr --add [SCOPE] WORD EXPANSION +abbr --erase word +abbr --rename [SCOPE] OLD_WORD NEW_WORD abbr --show abbr --list -abbr --erase word \endfish \subsection abbr-description Description -`abbr` manipulates the list of abbreviations that fish will expand. +Abbreviations are user-defined character sequences or words that are replaced with longer phrases after they are entered. For example, a frequently-run command such as `git checkout` can be abbreviated to `gco`. After entering `gco` and pressing @key{Space} or @key{Enter}, the full text `git checkout` will appear in the command line. The `abbr` command manipulates those abbreviations. -Abbreviations are user-defined character sequences or words that are replaced with longer phrases after they are entered. For example, a frequently-run command such as `git checkout` can be abbreviated to `gco`. After entering `gco` and pressing @key{Space} or @key{Enter}, the full text `git checkout` will appear in the command line. +Each abbreviation is stored in its own global or universal variable. The name consists of the prefix `_fish_abbr_` followed by the WORD after being transformed by `string escape style=var`. The WORD cannot contain a space but all other characters are legal. -Abbreviations are stored in a variable named `fish_user_abbreviations`. This is automatically created as a universal variable the first time an abbreviation is created. If you want your abbreviations to be private to a particular fish session you can put the following in your *~/.config/fish/config.fish* file before you define your first abbrevation: +Defining an abbreviation with global scope is slightly faster than universal scope (which is the default). But in general you'll only want to use the global scope when defining abbreviations in a startup script like `~/.config/fish/config.fish` like this: \fish if status --is-interactive - set -g fish_user_abbreviations - abbr --add first 'echo my first abbreviation' - abbr --add second 'echo my second abbreviation' + abbr --add --global first 'echo my first abbreviation' + abbr --add --global second 'echo my second abbreviation' + abbr --add --global gco git checkout # etcetera end \endfish -You can create abbreviations directly on the command line and they will be saved automatically and made visible to other fish sessions if `fish_user_abbreviations` is a universal variable. If you keep the variable as universal, `abbr --add` statements in config.fish will do nothing but slow down startup slightly. +You can create abbreviations interactively and they will be visible to other fish sessions if you use the `-U` or `--universal` flag or don't explicitly specify the scope and the abbreviation isn't already defined with global scope. If you want it to be visible only to the current shell use the `-g` or `--global` flag. \subsection abbr-options Options -The following parameters are available: +The following options are available: -- `-a WORD PHRASE` or `--add WORD PHRASE` Adds a new abbreviation, causing WORD to be expanded to PHRASE. +- `-a WORD EXPANSION` or `--add WORD EXPANSION` Adds a new abbreviation, causing WORD to be expanded to PHRASE. You can optionally specify `-g` or `--global` to avoid the overhead of universal variables at the expense of not having the definition being immediately visible to other fish shells that are already running. If you don't specify global scope it default to universal. For clarity you can also specify `-U` or `--universal`. -- `-r WORD NEW_WORD` or `--rename WORD NEW_WORD` Renames an abbreviation, from WORD to NEW_WORD. +- `-r OLD_WORD NEW_WORD` or `--rename OLD_WORD NEW_WORD` Renames an abbreviation, from OLD_WORD to NEW_WORD. -- `-s` or `--show` Show all abbreviated words and their expanded phrases in a manner suitable for export and import. +- `-s` or `--show` Show all abbreviations in a manner suitable for export and import. - `-l` or `--list` Lists all abbreviated words. - `-e WORD` or `--erase WORD` Erase the abbreviation WORD. -Note: fish version 2.1 supported `-a WORD=PHRASE`. This syntax is now deprecated but will still be converted. - \subsection abbr-example Examples \fish -abbr -a gco git checkout +abbr -a -g gco git checkout \endfish -Add a new abbreviation where `gco` will be replaced with `git checkout`. +Add a new abbreviation where `gco` will be replaced with `git checkout` global to the current shell. 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). + +\fish +abbr -a -U l less +\endfish +Add a new abbreviation where `l` will be replaced with `less` univeral so all shells. Note that you omit the `-U` since it is the default. \fish abbr -r gco gch diff --git a/share/functions/__fish_config_interactive.fish b/share/functions/__fish_config_interactive.fish index 437605022..5d8c92ff7 100644 --- a/share/functions/__fish_config_interactive.fish +++ b/share/functions/__fish_config_interactive.fish @@ -4,6 +4,15 @@ # This function is called by the __fish_on_interactive function, which is defined in config.fish. # function __fish_config_interactive -d "Initializations that should be performed when entering interactive mode" + if not set -q __fish_init_3_x + # Perform transitions relevant to going from fish 2.x to 3.x. + + # Migrate old universal abbreviations to the new scheme. + abbr_old | source + + set -U __fish_init_3_x + end + # Make sure this function is only run once. if set -q __fish_config_interactive_done return diff --git a/share/functions/abbr.fish b/share/functions/abbr.fish index 23a2d08e3..65ed16914 100644 --- a/share/functions/abbr.fish +++ b/share/functions/abbr.fish @@ -1,206 +1,201 @@ -function abbr --description "Manage abbreviations" - # parse arguments - set -l mode - set -l mode_flag # the flag that was specified, for better errors - set -l mode_arg - set -l needs_arg no - while set -q argv[1] - set -l new_mode - switch $argv[1] - case '-h' '--help' - __fish_print_help abbr - return 0 - case '-a' '--add' - set new_mode add - set needs_arg multi - case '-r' '--rename' - set new_mode rename - set needs_arg double - case '-e' '--erase' - set new_mode erase - set needs_arg single - case '-l' '--list' - set new_mode list - case '-s' '--show' - set new_mode show - case '--' - set -e argv[1] - break - case '-*' - printf ( _ "%s: invalid option -- %s\n" ) abbr $argv[1] >&2 - return 1 - case '*' - break - end - if test -n "$mode" -a -n "$new_mode" - # we're trying to set two different modes - printf ( _ "%s: %s cannot be specified along with %s\n" ) abbr $argv[1] $mode_flag >&2 - return 1 - end - set mode $new_mode - set mode_flag $argv[1] - set -e argv[1] - end +function abbr --description "Manage abbreviations using new fish 3.0 scheme." + set -l options --stop-nonopt --exclusive 'a,r,e,l,s' --exclusive 'g,U' + set options $options 'h/help' 'a/add' 'r/rename' 'e/erase' 'l/list' 's/show' + set options $options 'g/global' 'U/universal' - # If run with no options, treat it like --add if we have an argument, or - # --show if we do not have an argument - if not set -q mode[1] - if set -q argv[1] - set mode add - set needs_arg multi - else - set mode show - end - end + argparse -n $cmd $options -- $argv + or return - if test $needs_arg = single - set mode_arg $argv[1] - set needs_arg no - set -e argv[1] - else if test $needs_arg = double - # Pull the two parameters from argv. - # * leave argv non-empty, if there are more than two arguments - # * leave needs_arg set to double if there is not enough arguments - if set -q argv[1] - set param1 $argv[1] - set -e argv[1] - if set -q argv[1] - set param2 $argv[1] - set needs_arg no - set -e argv[1] - end - end - else if test $needs_arg = multi - set mode_arg $argv - set needs_arg no - set -e argv - end - if test $needs_arg != no - printf ( _ "%s: option requires an argument -- %s\n" ) abbr $mode_flag >&2 - return 1 - end - - # none of our modes want any excess arguments - if set -q argv[1] - printf ( _ "%s: Unexpected argument -- %s\n" ) abbr $argv[1] >&2 - return 1 - end - - switch $mode - case 'add' - # Convert from old "key=value" syntax - # TODO: This should be removed later - if not set -q mode_arg[2] - and string match -qr '^[^ ]+=' -- $mode_arg - set mode_arg (string split "=" -- $mode_arg) - end - - # Bail out early if the exact abbr is already in - set -q fish_user_abbreviations - and contains -- "$mode_arg" $fish_user_abbreviations - and return 0 - - set -l key $mode_arg[1] - set -e mode_arg[1] - set -l value "$mode_arg" - # Because we later store "$key $value", there can't be any spaces in the key - if string match -q "* *" -- $key - printf ( _ "%s: abbreviation cannot have spaces in the key\n" ) abbr >&2 - return 1 - end - if test -z "$value" - printf ( _ "%s: abbreviation must have a value\n" ) abbr >&2 - return 1 - end - if set -l idx (__fish_abbr_get_by_key $key) - # erase the existing abbreviation - set -e fish_user_abbreviations[$idx] - end - if not set -q fish_user_abbreviations - # initialize as a universal variable, so we can skip the -U later - # and therefore work properly if someone sets this as a global variable - set -U fish_user_abbreviations - end - set fish_user_abbreviations $fish_user_abbreviations "$key $value" - return 0 - - case 'rename' - set -l old_name $param1 - set -l new_name $param2 - - # if the target name already exists, throw an error - if set -l idx (__fish_abbr_get_by_key $new_name) - printf ( _ "%s: abbreviation '%s' already exists, cannot rename\n" ) abbr $new_name >&2 - return 2 - end - - # Because we later store "$key $value", there can't be any spaces in the key - if string match -q "* *" -- $new_name - printf ( _ "%s: abbreviation cannot have spaces in the key\n" ) abbr >&2 - return 1 - end - - set -l idx (__fish_abbr_get_by_key $old_name) - or begin - printf ( _ "%s: no such abbreviation '%s'\n" ) abbr $old_name >&2 - return 2 - end - - set -l value (string split " " -m 1 -- $fish_user_abbreviations[$idx])[2] - set fish_user_abbreviations[$idx] "$new_name $value" - return 0 - - case 'erase' - if set -l idx (__fish_abbr_get_by_key $mode_arg) - set -e fish_user_abbreviations[$idx] - return 0 - else - printf ( _ "%s: no such abbreviation '%s'\n" ) abbr $mode_arg >&2 - return 2 - end - - case 'show' - for i in $fish_user_abbreviations - set -l opt_double_dash - set -l kv (string split " " -m 1 -- $i) - set -l key $kv[1] - set -l value $kv[2] - - # Check to see if either key or value has a leading dash - # If so, we need to write -- - string match -q -- '-*' $key $value - and set opt_double_dash '--' - echo abbr $opt_double_dash (string escape -- $key $value) - end - return 0 - - case 'list' - for i in $fish_user_abbreviations - set -l key (string split " " -m 1 -- $i)[1] - printf "%s\n" $key - end - return 0 - end -end - -function __fish_abbr_get_by_key - if not set -q argv[1] - echo "__fish_abbr_get_by_key: expected one argument, got none" >&2 - return 2 - end - - set -q fish_user_abbreviations - or return 1 - - # Going through all entries is still quicker than calling `seq` - set -l keys - for kv in $fish_user_abbreviations - # If this does not match, we have screwed up before and the error should be reported - set keys $keys (string split " " -m 1 -- $kv)[1] - end - if set -l idx (contains -i -- $argv[1] $keys) - echo $idx + if set -q _flag_help + __fish_print_help abbr return 0 end - return 1 + + # 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] + 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] + __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 + 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 + if set -q argv[2] + printf ( _ "%s %s: Expected one argument\n" ) abbr --erase >&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_name (string escape -- $abbr_name) + if string match -q "* *" -- $abbr_old_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) + + if not set -q $abbr_var_name + printf ( _ "%s %s: No abbreviation named %s\n" ) abbr --erase $escaped_name >&2 + return 121 + end + + set -e $abbr_var_name +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 diff --git a/share/functions/abbr_old.fish b/share/functions/abbr_old.fish new file mode 100644 index 000000000..c43f344ea --- /dev/null +++ b/share/functions/abbr_old.fish @@ -0,0 +1,206 @@ +function abbr_old --description "Manage abbreviations using old fish 2.x scheme." + # parse arguments + set -l mode + set -l mode_flag # the flag that was specified, for better errors + set -l mode_arg + set -l needs_arg no + while set -q argv[1] + set -l new_mode + switch $argv[1] + case '-h' '--help' + __fish_print_help abbr + return 0 + case '-a' '--add' + set new_mode add + set needs_arg multi + case '-r' '--rename' + set new_mode rename + set needs_arg double + case '-e' '--erase' + set new_mode erase + set needs_arg single + case '-l' '--list' + set new_mode list + case '-s' '--show' + set new_mode show + case '--' + set -e argv[1] + break + case '-*' + printf ( _ "%s: invalid option -- %s\n" ) abbr $argv[1] >&2 + return 1 + case '*' + break + end + if test -n "$mode" -a -n "$new_mode" + # we're trying to set two different modes + printf ( _ "%s: %s cannot be specified along with %s\n" ) abbr $argv[1] $mode_flag >&2 + return 1 + end + set mode $new_mode + set mode_flag $argv[1] + set -e argv[1] + end + + # If run with no options, treat it like --add if we have an argument, or + # --show if we do not have an argument + if not set -q mode[1] + if set -q argv[1] + set mode add + set needs_arg multi + else + set mode show + end + end + + if test $needs_arg = single + set mode_arg $argv[1] + set needs_arg no + set -e argv[1] + else if test $needs_arg = double + # Pull the two parameters from argv. + # * leave argv non-empty, if there are more than two arguments + # * leave needs_arg set to double if there is not enough arguments + if set -q argv[1] + set param1 $argv[1] + set -e argv[1] + if set -q argv[1] + set param2 $argv[1] + set needs_arg no + set -e argv[1] + end + end + else if test $needs_arg = multi + set mode_arg $argv + set needs_arg no + set -e argv + end + if test $needs_arg != no + printf ( _ "%s: option requires an argument -- %s\n" ) abbr $mode_flag >&2 + return 1 + end + + # none of our modes want any excess arguments + if set -q argv[1] + printf ( _ "%s: Unexpected argument -- %s\n" ) abbr $argv[1] >&2 + return 1 + end + + switch $mode + case 'add' + # Convert from old "key=value" syntax + # TODO: This should be removed later + if not set -q mode_arg[2] + and string match -qr '^[^ ]+=' -- $mode_arg + set mode_arg (string split "=" -- $mode_arg) + end + + # Bail out early if the exact abbr is already in + set -q fish_user_abbreviations + and contains -- "$mode_arg" $fish_user_abbreviations + and return 0 + + set -l key $mode_arg[1] + set -e mode_arg[1] + set -l value "$mode_arg" + # Because we later store "$key $value", there can't be any spaces in the key + if string match -q "* *" -- $key + printf ( _ "%s: abbreviation cannot have spaces in the key\n" ) abbr >&2 + return 1 + end + if test -z "$value" + printf ( _ "%s: abbreviation must have a value\n" ) abbr >&2 + return 1 + end + if set -l idx (__fish_abbr_get_by_key $key) + # erase the existing abbreviation + set -e fish_user_abbreviations[$idx] + end + if not set -q fish_user_abbreviations + # initialize as a universal variable, so we can skip the -U later + # and therefore work properly if someone sets this as a global variable + set -U fish_user_abbreviations + end + set fish_user_abbreviations $fish_user_abbreviations "$key $value" + return 0 + + case 'rename' + set -l old_name $param1 + set -l new_name $param2 + + # if the target name already exists, throw an error + if set -l idx (__fish_abbr_get_by_key $new_name) + printf ( _ "%s: abbreviation '%s' already exists, cannot rename\n" ) abbr $new_name >&2 + return 2 + end + + # Because we later store "$key $value", there can't be any spaces in the key + if string match -q "* *" -- $new_name + printf ( _ "%s: abbreviation cannot have spaces in the key\n" ) abbr >&2 + return 1 + end + + set -l idx (__fish_abbr_get_by_key $old_name) + or begin + printf ( _ "%s: no such abbreviation '%s'\n" ) abbr $old_name >&2 + return 2 + end + + set -l value (string split " " -m 1 -- $fish_user_abbreviations[$idx])[2] + set fish_user_abbreviations[$idx] "$new_name $value" + return 0 + + case 'erase' + if set -l idx (__fish_abbr_get_by_key $mode_arg) + set -e fish_user_abbreviations[$idx] + return 0 + else + printf ( _ "%s: no such abbreviation '%s'\n" ) abbr $mode_arg >&2 + return 2 + end + + case 'show' + for i in $fish_user_abbreviations + set -l opt_double_dash + set -l kv (string split " " -m 1 -- $i) + set -l key $kv[1] + set -l value $kv[2] + + # Check to see if either key or value has a leading dash + # If so, we need to write -- + string match -q -- '-*' $key $value + and set opt_double_dash '--' + echo abbr $opt_double_dash (string escape -- $key $value) + end + return 0 + + case 'list' + for i in $fish_user_abbreviations + set -l key (string split " " -m 1 -- $i)[1] + printf "%s\n" $key + end + return 0 + end +end + +function __fish_abbr_get_by_key + if not set -q argv[1] + echo "__fish_abbr_get_by_key: expected one argument, got none" >&2 + return 2 + end + + set -q fish_user_abbreviations + or return 1 + + # Going through all entries is still quicker than calling `seq` + set -l keys + for kv in $fish_user_abbreviations + # If this does not match, we have screwed up before and the error should be reported + set keys $keys (string split " " -m 1 -- $kv)[1] + end + if set -l idx (contains -i -- $argv[1] $keys) + echo $idx + return 0 + end + return 1 +end diff --git a/src/env.cpp b/src/env.cpp index 78512c23e..0fd675539 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -41,6 +41,7 @@ #include "env.h" #include "env_universal_common.h" #include "event.h" +#include "expand.h" #include "fallback.h" // IWYU pragma: keep #include "fish_version.h" #include "history.h" @@ -187,7 +188,7 @@ void var_stack_t::push(bool new_scope) { // i.e. not if it's just `begin; end` or "--no-scope-shadowing". if (new_scope) { if (!(top_node == this->global_env)) { - for (auto& var : top_node->env) { + for (auto &var : top_node->env) { if (var.second.exportv) { // This should copy var node->env.insert(var); @@ -603,7 +604,7 @@ static bool variable_is_colon_delimited_var(const wcstring &str) { } /// React to modifying the given variable. -static void react_to_variable_change(const wcstring &key) { +static void react_to_variable_change(const wchar_t *op, const wcstring &key) { // Don't do any of this until `env_init()` has run. We only want to do this in response to // variables set by the user; e.g., in a script like *config.fish* or interactively or as part // of loading the universal variables for the first time. @@ -631,37 +632,36 @@ static void react_to_variable_change(const wcstring &key) { env_set_read_limit(); } else if (key == L"FISH_HISTORY") { reader_change_history(history_session_id().c_str()); + } else if (wcsncmp(key.c_str(), L"_fish_abbr_", wcslen(L"_fish_abbr_")) == 0) { + update_abbr_cache(op, key); } } /// Universal variable callback function. This function makes sure the proper events are triggered /// when an event occurs. static void universal_callback(fish_message_type_t type, const wchar_t *name) { - const wchar_t *str = NULL; + const wchar_t *op; switch (type) { case SET: case SET_EXPORT: { - str = L"SET"; + op = L"SET"; break; } case ERASE: { - str = L"ERASE"; + op = L"ERASE"; break; } } - if (str) { - vars_stack().mark_changed_exported(); + react_to_variable_change(op, name); + vars_stack().mark_changed_exported(); - event_t ev = event_t::variable_event(name); - ev.arguments.push_back(L"VARIABLE"); - ev.arguments.push_back(str); - ev.arguments.push_back(name); - event_fire(&ev); - } - - if (name) react_to_variable_change(name); + event_t ev = event_t::variable_event(name); + ev.arguments.push_back(L"VARIABLE"); + ev.arguments.push_back(op); + ev.arguments.push_back(name); + event_fire(&ev); } /// Make sure the PATH variable contains something. @@ -1130,7 +1130,7 @@ int env_set(const wcstring &key, const wchar_t *val, env_mode_flags_t var_mode) event_fire(&ev); // debug( 1, L"env_set: return from event firing" ); - react_to_variable_change(key); + react_to_variable_change(L"SET", key); return ENV_OK; } @@ -1203,7 +1203,7 @@ int env_remove(const wcstring &key, int var_mode) { if (is_exported) vars_stack().mark_changed_exported(); } - react_to_variable_change(key); + react_to_variable_change(L"ERASE", key); return !erased; } diff --git a/src/expand.cpp b/src/expand.cpp index e00a1bff0..347f0c3c3 100644 --- a/src/expand.cpp +++ b/src/expand.cpp @@ -1572,37 +1572,39 @@ bool fish_xdm_login_hack_hack_hack_hack(std::vector *cmds, int argc return result; } +std::map abbreviations; +void update_abbr_cache(const wchar_t *op, const wcstring varname) { + wcstring abbr; + if (!unescape_string(varname.substr(wcslen(L"_fish_abbr_")), &abbr, 0, STRING_STYLE_VAR)) { + debug(1, L"Abbreviation var '%ls' is not correctly encoded, ignoring it.", varname.c_str()); + return; + } + abbreviations.erase(abbr); + if (wcscmp(op, L"ERASE") != 0) { + const env_var_t expansion = env_get_string(varname); + if (!expansion.missing_or_empty()) { + abbreviations.emplace(std::make_pair(abbr, expansion)); + } + } +} + bool expand_abbreviation(const wcstring &src, wcstring *output) { if (src.empty()) return false; - // Get the abbreviations. Return false if we have none. - env_var_t abbrs = env_get_string(USER_ABBREVIATIONS_VARIABLE_NAME); - if (abbrs.missing_or_empty()) return false; + auto abbr = abbreviations.find(src); + if (abbr == abbreviations.end()) return false; + if (output != NULL) output->assign(abbr->second); + return true; - bool result = false; - std::vector abbrsv; - tokenize_variable_array(abbrs, abbrsv); - for (auto abbr : abbrsv) { - // Abbreviation is expected to be of the form 'foo=bar' or 'foo bar'. Parse out the first = - // or space. Silently skip on failure (no equals, or equals at the end or beginning). Try to - // avoid copying any strings until we are sure this is a match. - size_t equals_pos = abbr.find(L'='); - size_t space_pos = abbr.find(L' '); - size_t separator = mini(equals_pos, space_pos); - if (separator == wcstring::npos || separator == 0 || separator + 1 == abbr.size()) continue; - - // Find the character just past the end of the command. Walk backwards, skipping spaces. - size_t cmd_end = separator; - while (cmd_end > 0 && iswspace(abbr.at(cmd_end - 1))) cmd_end--; - - // See if this command matches. - if (abbr.compare(0, cmd_end, src) == 0) { - // Success. Set output to everything past the end of the string. - if (output != NULL) output->assign(abbr, separator + 1, wcstring::npos); - - result = true; - break; +#if 0 + for (auto abbr : abbreviations) { + if (src == abbr.first) { + // We found a matching abbreviation. Set output to the expansion. + if (output != NULL) output->assign(abbr.second); + return true; } } - return result; + + return false; +#endif } diff --git a/src/expand.h b/src/expand.h index a904090e7..10fc88df5 100644 --- a/src/expand.h +++ b/src/expand.h @@ -135,11 +135,10 @@ wcstring replace_home_directory_with_tilde(const wcstring &str); /// Abbreviation support. Expand src as an abbreviation, returning true if one was found, false if /// not. If result is not-null, returns the abbreviation by reference. -#define USER_ABBREVIATIONS_VARIABLE_NAME L"fish_user_abbreviations" +void update_abbr_cache(const wchar_t *op, const wcstring varnam); bool expand_abbreviation(const wcstring &src, wcstring *output); // Terrible hacks bool fish_xdm_login_hack_hack_hack_hack(std::vector *cmds, int argc, const char *const *argv); - #endif diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index 49390d2ba..0702efb9c 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -1610,21 +1610,15 @@ static void test_fuzzy_match(void) { static void test_abbreviations(void) { say(L"Testing abbreviations"); - - const wchar_t *abbreviations = - L"gc=git checkout" ARRAY_SEP_STR - L"foo=" ARRAY_SEP_STR - L"gc=something else" ARRAY_SEP_STR - L"=" ARRAY_SEP_STR - L"=foo" ARRAY_SEP_STR - L"foo" ARRAY_SEP_STR - L"foo=bar" ARRAY_SEP_STR - L"gx git checkout"; - env_push(true); - int ret = env_set(USER_ABBREVIATIONS_VARIABLE_NAME, abbreviations, ENV_LOCAL); - if (ret != 0) err(L"Unable to set abbreviation variable"); + const std::vector> abbreviations = { + {L"gc", L"git checkout"}, {L"foo", L"bar"}, {L"gx", L"git checkout"}, + }; + for (auto it : abbreviations) { + int ret = env_set(L"_fish_abbr_" + it.first, it.second.c_str(), ENV_LOCAL); + if (ret != 0) err(L"Unable to set abbreviation variable"); + } wcstring result; if (expand_abbreviation(L"", &result)) err(L"Unexpected success with empty abbreviation"); diff --git a/tests/abbr.err b/tests/abbr.err index 83f97ab79..ef1f24017 100644 --- a/tests/abbr.err +++ b/tests/abbr.err @@ -1,7 +1,22 @@ -abbr: no such abbreviation 'NOT_AN_ABBR' -abbr: abbreviation cannot have spaces in the key -abbr: no such abbreviation '__abbr6' -abbr: abbreviation cannot have spaces in the key -abbr: option requires an argument -- -r -abbr: Unexpected argument -- __abbr10 -abbr: abbreviation '__abbr12' already exists, cannot rename +# Test basic add and list of __abbr1 +# Erasing one that doesn't exist should do nothing +abbr --erase: No abbreviation named NOT_AN_ABBR +# Adding existing __abbr1 should be idempotent +# Replacing __abbr1 definition +# __abbr1 -s and --show tests +# Test erasing __abbr1 +# Ensure we escape special characters on output +# Ensure we handle leading dashes in abbreviation names properly +# Test that an abbr word containing spaces is rejected +abbr --add: Abbreviation 'a b c' cannot have spaces in the word +# Test renaming +# Test renaming a nonexistent abbreviation +abbr --rename: No abbreviation named __abbr6 +# Test renaming to a abbreviation with spaces +abbr --rename: Abbreviation 'g h i' cannot have spaces in the word +# Test renaming without arguments +abbr --rename: Requires exactly two arguments +# Test renaming with too many arguments +abbr --rename: Requires exactly two arguments +# Test renaming to existing abbreviation +abbr --rename: Abbreviation __abbr12 already exists, cannot rename __abbr11 diff --git a/tests/abbr.in b/tests/abbr.in index 26b421171..8a0f11646 100644 --- a/tests/abbr.in +++ b/tests/abbr.in @@ -1,44 +1,42 @@ -# Test basic add and list +echo '# Test basic add and list of __abbr1' | tee /dev/stderr abbr __abbr1 alpha beta gamma abbr | grep __abbr1 -# Erasing one that doesn't exist should do nothing +echo '# Erasing one that doesn\'t exist should do nothing' | tee /dev/stderr abbr --erase NOT_AN_ABBR abbr | grep __abbr1 -# Adding existing one should be idempotent +echo '# Adding existing __abbr1 should be idempotent' | tee /dev/stderr abbr __abbr1 alpha beta gamma abbr | grep __abbr1 -# Replacing +echo '# Replacing __abbr1 definition' | tee /dev/stderr abbr __abbr1 delta abbr | grep __abbr1 -# -s and --show tests +echo '# __abbr1 -s and --show tests' | tee /dev/stderr abbr -s | grep __abbr1 abbr --show | grep __abbr1 -# Test erasing +echo '# Test erasing __abbr1' | tee /dev/stderr abbr -e __abbr1 abbr | grep __abbr1 -# Ensure we escape special characters on output +echo '# Ensure we escape special characters on output' | tee /dev/stderr abbr '~__abbr2' '$xyz' abbr | grep __abbr2 abbr -e '~__abbr2' -# Ensure we handle leading dashes in abbreviation names properly +echo '# Ensure we handle leading dashes in abbreviation names properly' | tee /dev/stderr abbr -- '--__abbr3' 'xyz' abbr | grep __abbr3 abbr -e -- '--__abbr3' -# Ensure we are not recognizing later "=" as separators -abbr d2 env a=b banana -abbr -l | string match -q d2; or echo "= test failed" +echo '# Test that an abbr word containing spaces is rejected' | tee /dev/stderr +abbr "a b c" "d e f" +abbr | grep 'a b c' -abbr "a b c" "d e f"; or true - -# Test renaming +echo '# Test renaming' | tee /dev/stderr abbr __abbr4 omega abbr | grep __abbr5 abbr -r __abbr4 __abbr5 @@ -46,26 +44,28 @@ abbr | grep __abbr5 abbr -e __abbr5 abbr | grep __abbr4 -# Test renaming a nonexistent abbreviation -abbr -r __abbr6 __abbr; or true +echo '# Test renaming a nonexistent abbreviation' | tee /dev/stderr +abbr -r __abbr6 __abbr -# Test renaming to a abbreviation with spaces +echo '# Test renaming to a abbreviation with spaces' | tee /dev/stderr abbr __abbr4 omega -abbr -r __abbr4 "g h i"; or true +abbr -r __abbr4 "g h i" abbr -e __abbr4 -# Test renaming without arguments +echo '# Test renaming without arguments' | tee /dev/stderr abbr __abbr7 omega -abbr -r __abbr7; or true +abbr -r __abbr7 -# Test renaming with too many arguments +echo '# Test renaming with too many arguments' | tee /dev/stderr abbr __abbr8 omega -abbr -r __abbr8 __abbr9 __abbr10; or true +abbr -r __abbr8 __abbr9 __abbr10 abbr | grep __abbr8 -abbr | grep __abbr9; or true -abbr | grep __abbr10; or true +abbr | grep __abbr9 +abbr | grep __abbr10 -# Test renaming to existing abbreviation +echo '# Test renaming to existing abbreviation' | tee /dev/stderr abbr __abbr11 omega11 abbr __abbr12 omega12 -abbr -r __abbr11 __abbr12; or true +abbr -r __abbr11 __abbr12 + +true # the last `abbr` command is expected to fail -- don't let that cause a test failure diff --git a/tests/abbr.out b/tests/abbr.out index 168fbfcee..224b7cb4b 100644 --- a/tests/abbr.out +++ b/tests/abbr.out @@ -1,10 +1,25 @@ -abbr __abbr1 'alpha beta gamma' -abbr __abbr1 'alpha beta gamma' -abbr __abbr1 'alpha beta gamma' -abbr __abbr1 delta -abbr __abbr1 delta -abbr __abbr1 delta -abbr '~__abbr2' '$xyz' -abbr -- --__abbr3 xyz -abbr __abbr5 omega -abbr __abbr8 omega +# Test basic add and list of __abbr1 +abbr -a -U -- __abbr1 'alpha beta gamma' +# Erasing one that doesn't exist should do nothing +abbr -a -U -- __abbr1 'alpha beta gamma' +# Adding existing __abbr1 should be idempotent +abbr -a -U -- __abbr1 'alpha beta gamma' +# Replacing __abbr1 definition +abbr -a -U -- __abbr1 delta +# __abbr1 -s and --show tests +abbr -a -U -- __abbr1 delta +abbr -a -U -- __abbr1 delta +# Test erasing __abbr1 +# Ensure we escape special characters on output +abbr -a -U -- '~__abbr2' '$xyz' +# Ensure we handle leading dashes in abbreviation names properly +abbr -a -U -- --__abbr3 xyz +# Test that an abbr word containing spaces is rejected +# Test renaming +abbr -a -U -- __abbr5 omega +# Test renaming a nonexistent abbreviation +# Test renaming to a abbreviation with spaces +# Test renaming without arguments +# Test renaming with too many arguments +abbr -a -U -- __abbr8 omega +# Test renaming to existing abbreviation diff --git a/tests/functions.in b/tests/functions.in index 4e2f03433..d1eae6e64 100644 --- a/tests/functions.in +++ b/tests/functions.in @@ -46,7 +46,7 @@ or not string match -q '*/share/functions/abbr.fish' $x[1] or test $x[2] != autoloaded or test $x[3] != 1 or test $x[4] != scope-shadowing -or test $x[5] != 'Manage abbreviations' +or test $x[5] != 'Manage abbreviations using new fish 3.0 scheme.' echo "Unexpected output for 'functions -v -D abbr': $x" >&2 end