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
This commit is contained in:
Kurtis Rader 2017-05-21 22:22:55 -07:00
parent 7e0833c1e0
commit 17dff8c569
13 changed files with 561 additions and 322 deletions

View file

@ -12,6 +12,7 @@ This section is for changes merged to the `major` branch that are not also merge
## Other significant changes ## Other significant changes
- `read` now requires at least one var name (#4220). - `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 # fish 2.7b1

View file

@ -2,54 +2,57 @@
\subsection abbr-synopsis Synopsis \subsection abbr-synopsis Synopsis
\fish{synopsis} \fish{synopsis}
abbr --add word phrase... abbr --add [SCOPE] WORD EXPANSION
abbr --rename word new_word abbr --erase word
abbr --rename [SCOPE] OLD_WORD NEW_WORD
abbr --show abbr --show
abbr --list abbr --list
abbr --erase word
\endfish \endfish
\subsection abbr-description Description \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 \fish
if status --is-interactive if status --is-interactive
set -g fish_user_abbreviations abbr --add --global first 'echo my first abbreviation'
abbr --add first 'echo my first abbreviation' abbr --add --global second 'echo my second abbreviation'
abbr --add second 'echo my second abbreviation' abbr --add --global gco git checkout
# etcetera # etcetera
end end
\endfish \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 <a href="tutorial.html#tut_startup">config.fish</a> 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 \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. - `-l` or `--list` Lists all abbreviated words.
- `-e WORD` or `--erase WORD` Erase the abbreviation WORD. - `-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 \subsection abbr-example Examples
\fish \fish
abbr -a gco git checkout abbr -a -g gco git checkout
\endfish \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 \fish
abbr -r gco gch abbr -r gco gch

View file

@ -4,6 +4,15 @@
# This function is called by the __fish_on_interactive function, which is defined in config.fish. # 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" 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. # Make sure this function is only run once.
if set -q __fish_config_interactive_done if set -q __fish_config_interactive_done
return return

View file

@ -1,206 +1,201 @@
function abbr --description "Manage abbreviations" function abbr --description "Manage abbreviations using new fish 3.0 scheme."
# parse arguments set -l options --stop-nonopt --exclusive 'a,r,e,l,s' --exclusive 'g,U'
set -l mode set options $options 'h/help' 'a/add' 'r/rename' 'e/erase' 'l/list' 's/show'
set -l mode_flag # the flag that was specified, for better errors set options $options 'g/global' 'U/universal'
set -l mode_arg
set -l needs_arg no argparse -n $cmd $options -- $argv
while set -q argv[1] or return
set -l new_mode
switch $argv[1] if set -q _flag_help
case '-h' '--help'
__fish_print_help abbr __fish_print_help abbr
return 0 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 end
# If run with no options, treat it like --add if we have an argument, or # If run with no options, treat it like --add if we have arguments, or
# --show if we do not have an argument # --show if we do not have any arguments.
if not set -q mode[1] 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] if set -q argv[1]
set mode add set _flag_add --add
set needs_arg multi
else else
set mode show set _flag_show --show
end end
end end
if test $needs_arg = single set -l abbr_scope
set mode_arg $argv[1] if set -q _flag_global
set needs_arg no set abbr_scope --global
set -e argv[1] else if set -q _flag_universal
else if test $needs_arg = double set abbr_scope --universal
# 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
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
else if test $needs_arg = multi end
set mode_arg $argv
set needs_arg no function __fish_abbr_add --no-scope-shadowing
set -e argv if not set -q argv[2]
end printf ( _ "%s %s: Requires at least two arguments\n" ) abbr --add >&2
if test $needs_arg != no
printf ( _ "%s: option requires an argument -- %s\n" ) abbr $mode_flag >&2
return 1 return 1
end end
# none of our modes want any excess arguments # Because of the way abbreviations are expanded there can't be any spaces in the key.
if set -q argv[1] set -l abbr_name $argv[1]
printf ( _ "%s: Unexpected argument -- %s\n" ) abbr $argv[1] >&2 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 return 1
end end
switch $mode set -l abbr_val "$argv[2..-1]"
case 'add' set -l abbr_var_name _fish_abbr_(string escape --style=var -- $abbr_name)
# Convert from old "key=value" syntax
# TODO: This should be removed later if not set -q $abbr_var_name
if not set -q mode_arg[2] # We default to the universal scope if the user didn't explicitly specify a scope and the
and string match -qr '^[^ ]+=' -- $mode_arg # abbreviation isn't already defined.
set mode_arg (string split "=" -- $mode_arg) set -q abbr_scope[1]
or set abbr_scope --universal
end end
true # make sure the next `set` command doesn't leak the previous status
set $abbr_scope $abbr_var_name $abbr_val
end
# Bail out early if the exact abbr is already in function __fish_abbr_erase --no-scope-shadowing
set -q fish_user_abbreviations if set -q argv[2]
and contains -- "$mode_arg" $fish_user_abbreviations printf ( _ "%s %s: Expected one argument\n" ) abbr --erase >&2
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 return 1
end end
if test -z "$value"
printf ( _ "%s: abbreviation must have a value\n" ) abbr >&2 # 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 return 1
end 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 abbr_var_name _fish_abbr_(string escape --style=var -- $abbr_name)
set -l old_name $param1
set -l new_name $param2
# if the target name already exists, throw an error if not set -q $abbr_var_name
if set -l idx (__fish_abbr_get_by_key $new_name) printf ( _ "%s %s: No abbreviation named %s\n" ) abbr --erase $escaped_name >&2
printf ( _ "%s: abbreviation '%s' already exists, cannot rename\n" ) abbr $new_name >&2 return 121
return 2
end end
# Because we later store "$key $value", there can't be any spaces in the key 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 if string match -q "* *" -- $new_name
printf ( _ "%s: abbreviation cannot have spaces in the key\n" ) abbr >&2 set -l msg ( _ "%s %s: Abbreviation %s cannot have spaces in the word\n" )
printf $msg abbr --rename $escaped_new_name >&2
return 1 return 1
end end
set -l idx (__fish_abbr_get_by_key $old_name) set -l old_var_name _fish_abbr_(string escape --style=var -- $old_name)
or begin set -l new_var_name _fish_abbr_(string escape --style=var -- $new_name)
printf ( _ "%s: no such abbreviation '%s'\n" ) abbr $old_name >&2
return 2 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 end
set -l value (string split " " -m 1 -- $fish_user_abbreviations[$idx])[2] set -l old_var_val $$old_var_name
set fish_user_abbreviations[$idx] "$new_name $value"
return 0
case 'erase' if not set -q abbr_scope[1]
if set -l idx (__fish_abbr_get_by_key $mode_arg) # User isn't forcing the scope so use the existing scope.
set -e fish_user_abbreviations[$idx] if set -ql $old_var_name
return 0 set abbr_scope --global
else else
printf ( _ "%s: no such abbreviation '%s'\n" ) abbr $mode_arg >&2 set abbr_scope --universal
return 2 end
end end
case 'show' set -e $old_var_name
for i in $fish_user_abbreviations set $abbr_scope $new_var_name $old_var_val
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 end
function __fish_abbr_get_by_key function __fish_abbr_list --no-scope-shadowing
if not set -q argv[1] if set -q argv[1]
echo "__fish_abbr_get_by_key: expected one argument, got none" >&2 printf ( _ "%s %s: Unexpected argument -- '%s'\n" ) abbr --erase $argv[1] >&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 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 end

View file

@ -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

View file

@ -41,6 +41,7 @@
#include "env.h" #include "env.h"
#include "env_universal_common.h" #include "env_universal_common.h"
#include "event.h" #include "event.h"
#include "expand.h"
#include "fallback.h" // IWYU pragma: keep #include "fallback.h" // IWYU pragma: keep
#include "fish_version.h" #include "fish_version.h"
#include "history.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". // i.e. not if it's just `begin; end` or "--no-scope-shadowing".
if (new_scope) { if (new_scope) {
if (!(top_node == this->global_env)) { if (!(top_node == this->global_env)) {
for (auto& var : top_node->env) { for (auto &var : top_node->env) {
if (var.second.exportv) { if (var.second.exportv) {
// This should copy var // This should copy var
node->env.insert(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. /// 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 // 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 // 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. // 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(); env_set_read_limit();
} else if (key == L"FISH_HISTORY") { } else if (key == L"FISH_HISTORY") {
reader_change_history(history_session_id().c_str()); 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 /// Universal variable callback function. This function makes sure the proper events are triggered
/// when an event occurs. /// when an event occurs.
static void universal_callback(fish_message_type_t type, const wchar_t *name) { static void universal_callback(fish_message_type_t type, const wchar_t *name) {
const wchar_t *str = NULL; const wchar_t *op;
switch (type) { switch (type) {
case SET: case SET:
case SET_EXPORT: { case SET_EXPORT: {
str = L"SET"; op = L"SET";
break; break;
} }
case ERASE: { case ERASE: {
str = L"ERASE"; op = L"ERASE";
break; break;
} }
} }
if (str) { react_to_variable_change(op, name);
vars_stack().mark_changed_exported(); vars_stack().mark_changed_exported();
event_t ev = event_t::variable_event(name); event_t ev = event_t::variable_event(name);
ev.arguments.push_back(L"VARIABLE"); ev.arguments.push_back(L"VARIABLE");
ev.arguments.push_back(str); ev.arguments.push_back(op);
ev.arguments.push_back(name); ev.arguments.push_back(name);
event_fire(&ev); event_fire(&ev);
}
if (name) react_to_variable_change(name);
} }
/// Make sure the PATH variable contains something. /// 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); event_fire(&ev);
// debug( 1, L"env_set: return from event firing" ); // debug( 1, L"env_set: return from event firing" );
react_to_variable_change(key); react_to_variable_change(L"SET", key);
return ENV_OK; return ENV_OK;
} }
@ -1203,7 +1203,7 @@ int env_remove(const wcstring &key, int var_mode) {
if (is_exported) vars_stack().mark_changed_exported(); if (is_exported) vars_stack().mark_changed_exported();
} }
react_to_variable_change(key); react_to_variable_change(L"ERASE", key);
return !erased; return !erased;
} }

View file

@ -1572,37 +1572,39 @@ bool fish_xdm_login_hack_hack_hack_hack(std::vector<std::string> *cmds, int argc
return result; return result;
} }
std::map<const wcstring, const wcstring> 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) { bool expand_abbreviation(const wcstring &src, wcstring *output) {
if (src.empty()) return false; if (src.empty()) return false;
// Get the abbreviations. Return false if we have none. auto abbr = abbreviations.find(src);
env_var_t abbrs = env_get_string(USER_ABBREVIATIONS_VARIABLE_NAME); if (abbr == abbreviations.end()) return false;
if (abbrs.missing_or_empty()) return false; if (output != NULL) output->assign(abbr->second);
return true;
bool result = false; #if 0
std::vector<wcstring> abbrsv; for (auto abbr : abbreviations) {
tokenize_variable_array(abbrs, abbrsv); if (src == abbr.first) {
for (auto abbr : abbrsv) { // We found a matching abbreviation. Set output to the expansion.
// Abbreviation is expected to be of the form 'foo=bar' or 'foo bar'. Parse out the first = if (output != NULL) output->assign(abbr.second);
// or space. Silently skip on failure (no equals, or equals at the end or beginning). Try to return true;
// avoid copying any strings until we are sure this is a match.
size_t equals_pos = abbr.find(L'=');
size_t space_pos = abbr.find(L' ');
size_t separator = mini(equals_pos, space_pos);
if (separator == wcstring::npos || separator == 0 || separator + 1 == abbr.size()) continue;
// Find the character just past the end of the command. Walk backwards, skipping spaces.
size_t cmd_end = separator;
while (cmd_end > 0 && iswspace(abbr.at(cmd_end - 1))) cmd_end--;
// See if this command matches.
if (abbr.compare(0, cmd_end, src) == 0) {
// Success. Set output to everything past the end of the string.
if (output != NULL) output->assign(abbr, separator + 1, wcstring::npos);
result = true;
break;
} }
} }
return result;
return false;
#endif
} }

View file

@ -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 /// 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. /// 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); bool expand_abbreviation(const wcstring &src, wcstring *output);
// Terrible hacks // Terrible hacks
bool fish_xdm_login_hack_hack_hack_hack(std::vector<std::string> *cmds, int argc, bool fish_xdm_login_hack_hack_hack_hack(std::vector<std::string> *cmds, int argc,
const char *const *argv); const char *const *argv);
#endif #endif

View file

@ -1610,21 +1610,15 @@ static void test_fuzzy_match(void) {
static void test_abbreviations(void) { static void test_abbreviations(void) {
say(L"Testing abbreviations"); 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); env_push(true);
int ret = env_set(USER_ABBREVIATIONS_VARIABLE_NAME, abbreviations, ENV_LOCAL); const std::vector<std::pair<const wcstring, const wcstring>> 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"); if (ret != 0) err(L"Unable to set abbreviation variable");
}
wcstring result; wcstring result;
if (expand_abbreviation(L"", &result)) err(L"Unexpected success with empty abbreviation"); if (expand_abbreviation(L"", &result)) err(L"Unexpected success with empty abbreviation");

View file

@ -1,7 +1,22 @@
abbr: no such abbreviation 'NOT_AN_ABBR' # Test basic add and list of __abbr1
abbr: abbreviation cannot have spaces in the key # Erasing one that doesn't exist should do nothing
abbr: no such abbreviation '__abbr6' abbr --erase: No abbreviation named NOT_AN_ABBR
abbr: abbreviation cannot have spaces in the key # Adding existing __abbr1 should be idempotent
abbr: option requires an argument -- -r # Replacing __abbr1 definition
abbr: Unexpected argument -- __abbr10 # __abbr1 -s and --show tests
abbr: abbreviation '__abbr12' already exists, cannot rename # 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

View file

@ -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 __abbr1 alpha beta gamma
abbr | grep __abbr1 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 --erase NOT_AN_ABBR
abbr | grep __abbr1 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 __abbr1 alpha beta gamma
abbr | grep __abbr1 abbr | grep __abbr1
# Replacing echo '# Replacing __abbr1 definition' | tee /dev/stderr
abbr __abbr1 delta abbr __abbr1 delta
abbr | grep __abbr1 abbr | grep __abbr1
# -s and --show tests echo '# __abbr1 -s and --show tests' | tee /dev/stderr
abbr -s | grep __abbr1 abbr -s | grep __abbr1
abbr --show | grep __abbr1 abbr --show | grep __abbr1
# Test erasing echo '# Test erasing __abbr1' | tee /dev/stderr
abbr -e __abbr1 abbr -e __abbr1
abbr | grep __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 '~__abbr2' '$xyz'
abbr | grep __abbr2 abbr | grep __abbr2
abbr -e '~__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 -- '--__abbr3' 'xyz'
abbr | grep __abbr3 abbr | grep __abbr3
abbr -e -- '--__abbr3' abbr -e -- '--__abbr3'
# Ensure we are not recognizing later "=" as separators echo '# Test that an abbr word containing spaces is rejected' | tee /dev/stderr
abbr d2 env a=b banana abbr "a b c" "d e f"
abbr -l | string match -q d2; or echo "= test failed" abbr | grep 'a b c'
abbr "a b c" "d e f"; or true echo '# Test renaming' | tee /dev/stderr
# Test renaming
abbr __abbr4 omega abbr __abbr4 omega
abbr | grep __abbr5 abbr | grep __abbr5
abbr -r __abbr4 __abbr5 abbr -r __abbr4 __abbr5
@ -46,26 +44,28 @@ abbr | grep __abbr5
abbr -e __abbr5 abbr -e __abbr5
abbr | grep __abbr4 abbr | grep __abbr4
# Test renaming a nonexistent abbreviation echo '# Test renaming a nonexistent abbreviation' | tee /dev/stderr
abbr -r __abbr6 __abbr; or true 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 __abbr4 omega
abbr -r __abbr4 "g h i"; or true abbr -r __abbr4 "g h i"
abbr -e __abbr4 abbr -e __abbr4
# Test renaming without arguments echo '# Test renaming without arguments' | tee /dev/stderr
abbr __abbr7 omega 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 __abbr8 omega
abbr -r __abbr8 __abbr9 __abbr10; or true abbr -r __abbr8 __abbr9 __abbr10
abbr | grep __abbr8 abbr | grep __abbr8
abbr | grep __abbr9; or true abbr | grep __abbr9
abbr | grep __abbr10; or true abbr | grep __abbr10
# Test renaming to existing abbreviation echo '# Test renaming to existing abbreviation' | tee /dev/stderr
abbr __abbr11 omega11 abbr __abbr11 omega11
abbr __abbr12 omega12 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

View file

@ -1,10 +1,25 @@
abbr __abbr1 'alpha beta gamma' # Test basic add and list of __abbr1
abbr __abbr1 'alpha beta gamma' abbr -a -U -- __abbr1 'alpha beta gamma'
abbr __abbr1 'alpha beta gamma' # Erasing one that doesn't exist should do nothing
abbr __abbr1 delta abbr -a -U -- __abbr1 'alpha beta gamma'
abbr __abbr1 delta # Adding existing __abbr1 should be idempotent
abbr __abbr1 delta abbr -a -U -- __abbr1 'alpha beta gamma'
abbr '~__abbr2' '$xyz' # Replacing __abbr1 definition
abbr -- --__abbr3 xyz abbr -a -U -- __abbr1 delta
abbr __abbr5 omega # __abbr1 -s and --show tests
abbr __abbr8 omega 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

View file

@ -46,7 +46,7 @@ or not string match -q '*/share/functions/abbr.fish' $x[1]
or test $x[2] != autoloaded or test $x[2] != autoloaded
or test $x[3] != 1 or test $x[3] != 1
or test $x[4] != scope-shadowing 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 echo "Unexpected output for 'functions -v -D abbr': $x" >&2
end end