Merge pull request #9313 from ridiculousfish/mega-abbr

Enhances abbreviations with extra features
- global abbreviations
- trigger on regex match as alternative to literal match
- the ability to expand abbreviations with a user-defined function  
- the ability to set cursor position after expansion
This commit is contained in:
Johannes Altmanninger 2022-12-12 23:56:11 +01:00 committed by GitHub
commit c120305b8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1435 additions and 512 deletions

View file

@ -28,12 +28,23 @@ Notable improvements and fixes
- It is now possible to specify multiple scopes for ``set -e`` and all of the named variables present in any of the specified scopes will be erased. This makes it possible to remove all instances of a variable in all scopes (``set -efglU foo``) in one go (:issue:`7711`).
- A possible stack overflow when recursively evaluating substitutions has been fixed (:issue:`9302`).
- `status current-commandline` has been added and retrieves the entirety of the currently executing commandline when called from a function during execution, allowing easier job introspection (:issue:`8905`).
- Abbrevations are more flexible:
- They may optionally replace tokens anywhere on the command line, instead of only commands
- Matching tokens may be described using a regular expression instead of a literal word
- The replacement text may be produced by a fish function, instead of a literal word
- They may position the cursor anywhere in the expansion, instead of at the end
See the documentation for more.
Deprecations and removed features
---------------------------------
- The difference between ``\xAB`` and ``\XAB`` has been removed. Before, ``\x`` would do the same thing as ``\X`` except that it would error if the value was larger than "7f" (127 in decimal, the highest ASCII value) (:issue:`9247`, :issue:`1352`).
- The ``fish_git_prompt`` will now only turn on features if the corresponding boolean variable has been set to a true value (of "1", "yes" or "true") instead of just checking if it is defined. This allows specifically turning features *off* without having to erase variables, e.g. via universal variables. If you have defined a variable to a different value and expect it to count as true, you need to change it (:issue:`9274`).
For example, ``set -g __fish_git_prompt_show_informative_status 0`` previously would have enabled informative status (because any value would have done so), now it turns it off.
- Abbreviations are no longer stored in universal variables. Existing universal abbreviations are still imported, but new abbreviations should be added to ``config.fish``.
- The short option ``-r`` for abbreviations has changed from ``rename`` to ``regex``, for consistency with ``string``.
Scripting improvements
----------------------

View file

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

View file

@ -8,96 +8,136 @@ Synopsis
.. synopsis::
abbr --add [SCOPE] WORD EXPANSION
abbr --erase WORD ...
abbr --rename [SCOPE] OLD_WORD NEW_WORD
abbr --add NAME [--position command | anywhere] [-r | --regex PATTERN]
[--set-cursor[=MARKER]]
[-f | --function] EXPANSION
abbr --erase NAME ...
abbr --rename OLD_WORD NEW_WORD
abbr --show
abbr --list
abbr --query WORD ...
abbr --query NAME ...
Description
-----------
``abbr`` manages abbreviations - user-defined words that are replaced with longer phrases after they are entered.
``abbr`` manages abbreviations - user-defined words that are replaced with longer phrases when entered.
.. note::
Only typed-in commands use abbreviations. Abbreviations are not expanded in scripts.
For example, a frequently-run command like ``git checkout`` can be abbreviated to ``gco``.
After entering ``gco`` and pressing :kbd:`Space` or :kbd:`Enter`, the full text ``git checkout`` will appear in the command line.
Options
-------
An abbreviation may match a literal word, or it may match a pattern given by a regular expression. When an abbreviation matches a word, that word is replaced by new text, called its *expansion*. This expansion may be a fixed new phrase, or it can be dynamically created via a fish function. This expansion occurs after pressing space or enter.
The following options are available:
Combining these features, it is possible to create custom syntaxes, where a regular expression recognizes matching tokens, and the expansion function interprets them. See the `Examples`_ section.
**-a** *WORD* *EXPANSION* or **--add** *WORD* *EXPANSION*
Adds a new abbreviation, causing *WORD* to be expanded to *EXPANSION*
Abbreviations may be added to :ref:`config.fish <configuration>`.
**-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 abbreviations in a manner suitable for import and export
**-l** or **--list**
Lists all abbreviated words
"add" subcommand
--------------------
**-e** *WORD* or **--erase** *WORD* ...
Erase the given abbreviations
.. synopsis::
**-q** or **--query**
Return 0 (true) if one of the *WORD* is an abbreviation.
abbr [-a | --add] NAME [--position command | anywhere] [-r | --regex PATTERN]
[--set-cursor[=MARKER]] [-f | --function] EXPANSION
**-h** or **--help**
Displays help about using this command.
``abbr --add`` creates a new abbreviation. With no other options, the string **NAME** is replaced by **EXPANSION**.
In addition, when adding or renaming abbreviations, one of the following **SCOPE** options can be used:
With **--position command**, the abbreviation will only expand when it is positioned as a command, not as an argument to another command. With **--position anywhere** the abbreviation may expand anywhere in the command line. The default is **command**.
**-g** or **--global**
Use a global variable
With **--regex**, the abbreviation matches using the regular expression given by **PATTERN**, instead of the literal **NAME**. The pattern is interpreted using PCRE2 syntax and must match the entire token. If multiple abbreviations match the same token, the last abbreviation added is used.
**-U** or **--universal**
Use a universal variable (default)
With **--set-cursor=MARKER**, the cursor is moved to the first occurrence of **MARKER** in the expansion. The **MARKER** value is erased. The **MARKER** may be omitted (i.e. simply ``--set-cursor``), in which case it defaults to ``%``.
With **-f** or **--function**, **EXPANSION** is treated as the name of a fish function instead of a literal replacement. When the abbreviation matches, the function will be called with the matching token as an argument. If the function's exit status is 0 (success), the token will be replaced by the function's output; otherwise the token will be left unchanged.
See the "Internals" section for more on them.
Examples
--------
########
::
abbr -a -g gco git checkout
abbr --add gco 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).
Add a new abbreviation where ``gco`` will be replaced with ``git checkout``.
::
abbr -a -U l less
abbr -a --position anywhere -- -C --color
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.
Add a new abbreviation where ``-C`` will be replaced with ``--color``. The ``--`` allows ``-C`` to be treated as the name of the abbreviation, instead of an option.
::
abbr -r gco gch
abbr -a L --position anywhere --set-cursor "% | less"
Add a new abbreviation where ``L`` will be replaced with ``| less``, placing the cursor before the pipe.
Renames an existing abbreviation from ``gco`` to ``gch``.
::
abbr -e gco
function last_history_item
echo $history[1]
end
abbr -a !! --position anywhere --function last_history_item
Erase the ``gco`` abbreviation.
This first creates a function ``last_history_item`` which outputs the last entered command. It then adds an abbreviation which replaces ``!!`` with the result of calling this function. Taken together, this is similar to the ``!!`` history expansion feature of bash.
::
ssh another_host abbr -s | source
function vim_edit
echo vim $argv
end
abbr -a vim_edit_texts --position command --regex ".+\.txt" --function vim_edit
Import the abbreviations defined on another_host over SSH.
This first creates a function ``vim_edit`` which prepends ``vim`` before its argument. It then adds an abbreviation which matches commands ending in ``.txt``, and replaces the command with the result of calling this function. This allows text files to be "executed" as a command to open them in vim, similar to the "suffix alias" feature in zsh.
Internals
---------
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.
::
abbr 4DIRS --set-cursor ! "$(string join \n -- 'for dir in */' 'cd $dir' '!' 'cd ..' 'end')"
This creates an abbreviation "4DIRS" which expands to a multi-line loop "template." The template enters each directory and then leaves it. The cursor is positioned ready to enter the command to run in each directory, at the location of the ``!``, which is itself erased.
Other subcommands
--------------------
::
abbr --rename OLD_NAME NEW_NAME
Renames an abbreviation, from *OLD_NAME* to *NEW_NAME*
::
abbr [-s | --show]
Show all abbreviations in a manner suitable for import and export
::
abbr [-l | --list]
Prints the names of all abbreviation
::
abbr [-e | --erase] NAME
Erases the abbreviation with the given name
::
abbr -q or --query [NAME...]
Return 0 (true) if one of the *NAME* is an abbreviation.
::
abbr -h or --help
Displays help for the `abbr` command.
Abbreviations created with the **--universal** flag will be visible to other fish sessions, whilst **--global** will be limited to the current session.

View file

@ -1,10 +1,18 @@
# "add" is implicit.
set __fish_abbr_not_add_cond '__fish_seen_subcommand_from --query --rename --erase --show --list --help'
set __fish_abbr_add_cond 'not __fish_seen_subcommand_from --query --rename --erase --show --list --help'
complete -c abbr -f
complete -c abbr -f -s a -l add -d 'Add abbreviation'
complete -c abbr -f -s q -l query -d 'Check if an abbreviation exists'
complete -c abbr -f -s r -l rename -d 'Rename an abbreviation' -xa '(abbr --list)'
complete -c abbr -f -s e -l erase -d 'Erase abbreviation' -xa '(abbr --list)'
complete -c abbr -f -s s -l show -d 'Print all abbreviations'
complete -c abbr -f -s l -l list -d 'Print all abbreviation names'
complete -c abbr -f -s g -l global -d 'Store abbreviation in a global variable'
complete -c abbr -f -s U -l universal -d 'Store abbreviation in a universal variable'
complete -c abbr -f -s h -l help -d Help
complete -c abbr -f -n $__fish_abbr_not_add_cond -s a -l add -d 'Add abbreviation'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s q -l query -d 'Check if an abbreviation exists'
complete -c abbr -f -n $__fish_abbr_not_add_cond -l rename -d 'Rename an abbreviation' -xa '(abbr --list)'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s e -l erase -d 'Erase abbreviation' -xa '(abbr --list)'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s s -l show -d 'Print all abbreviations'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s l -l list -d 'Print all abbreviation names'
complete -c abbr -f -n $__fish_abbr_not_add_cond -s h -l help -d Help
complete -c abbr -f -n $__fish_abbr_add_cond -s p -l position -a 'command anywhere' -d 'Expand only as a command, or anywhere' -x
complete -c abbr -f -n $__fish_abbr_add_cond -s f -l function -d 'Treat value as a fish function'
complete -c abbr -f -n $__fish_abbr_add_cond -s r -l regex -d 'Match a regular expression' -x
complete -c abbr -f -n $__fish_abbr_add_cond -l set-cursor -d 'Position the cursor at % in the output'

View file

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

134
src/abbrs.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "config.h" // IWYU pragma: keep
#include "abbrs.h"
#include "env.h"
#include "global_safety.h"
#include "wcstringutil.h"
abbreviation_t::abbreviation_t(wcstring name, wcstring key, wcstring replacement,
abbrs_position_t position, bool from_universal)
: name(std::move(name)),
key(std::move(key)),
replacement(std::move(replacement)),
position(position),
from_universal(from_universal) {}
bool abbreviation_t::matches_position(abbrs_position_t position) const {
return this->position == abbrs_position_t::anywhere || this->position == position;
}
bool abbreviation_t::matches(const wcstring &token, abbrs_position_t position) const {
if (!this->matches_position(position)) {
return false;
}
if (this->is_regex()) {
return this->regex->match(token).has_value();
} else {
return this->key == token;
}
}
acquired_lock<abbrs_set_t> abbrs_get_set() {
static owning_lock<abbrs_set_t> abbrs;
return abbrs.acquire();
}
abbrs_replacer_list_t abbrs_set_t::match(const wcstring &token, abbrs_position_t position) const {
abbrs_replacer_list_t result{};
// Later abbreviations take precedence so walk backwards.
for (auto it = abbrs_.rbegin(); it != abbrs_.rend(); ++it) {
const abbreviation_t &abbr = *it;
if (abbr.matches(token, position)) {
result.push_back(abbrs_replacer_t{abbr.replacement, abbr.replacement_is_function,
abbr.set_cursor_marker});
}
}
return result;
}
bool abbrs_set_t::has_match(const wcstring &token, abbrs_position_t position) const {
for (const auto &abbr : abbrs_) {
if (abbr.matches(token, position)) {
return true;
}
}
return false;
}
void abbrs_set_t::add(abbreviation_t &&abbr) {
assert(!abbr.name.empty() && "Invalid name");
bool inserted = used_names_.insert(abbr.name).second;
if (!inserted) {
// Name was already used, do a linear scan to find it.
auto where = std::find_if(abbrs_.begin(), abbrs_.end(), [&](const abbreviation_t &other) {
return other.name == abbr.name;
});
assert(where != abbrs_.end() && "Abbreviation not found though its name was present");
abbrs_.erase(where);
}
abbrs_.push_back(std::move(abbr));
}
void abbrs_set_t::rename(const wcstring &old_name, const wcstring &new_name) {
bool erased = this->used_names_.erase(old_name) > 0;
bool inserted = this->used_names_.insert(new_name).second;
assert(erased && inserted && "Old name not found or new name already present");
(void)erased;
(void)inserted;
for (auto &abbr : abbrs_) {
if (abbr.name == old_name) {
abbr.name = new_name;
break;
}
}
}
bool abbrs_set_t::erase(const wcstring &name) {
bool erased = this->used_names_.erase(name) > 0;
if (!erased) {
return false;
}
for (auto it = abbrs_.begin(); it != abbrs_.end(); ++it) {
if (it->name == name) {
abbrs_.erase(it);
return true;
}
}
assert(false && "Unable to find named abbreviation");
return false;
}
void abbrs_set_t::import_from_uvars(const std::unordered_map<wcstring, env_var_t> &uvars) {
const wchar_t *const prefix = L"_fish_abbr_";
size_t prefix_len = wcslen(prefix);
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);
wcstring name;
if (unescape_string(escaped_name, &name, unescape_flags_t{}, STRING_STYLE_VAR)) {
wcstring key = name;
wcstring replacement = join_strings(kv.second.as_list(), L' ');
this->add(abbreviation_t{std::move(name), std::move(key), std::move(replacement),
abbrs_position_t::command, from_universal});
}
}
}
}
// static
abbrs_replacement_t abbrs_replacement_t::from(source_range_t range, wcstring text,
const abbrs_replacer_t &replacer) {
abbrs_replacement_t result{};
result.range = range;
result.text = std::move(text);
if (replacer.set_cursor_marker.has_value()) {
size_t pos = result.text.find(*replacer.set_cursor_marker);
if (pos != wcstring::npos) {
result.text.erase(pos, replacer.set_cursor_marker->size());
result.cursor = pos + range.start;
}
}
return result;
}

149
src/abbrs.h Normal file
View file

@ -0,0 +1,149 @@
// Support for abbreviations.
//
#ifndef FISH_ABBRS_H
#define FISH_ABBRS_H
#include <unordered_map>
#include <unordered_set>
#include "common.h"
#include "maybe.h"
#include "parse_constants.h"
#include "re.h"
class env_var_t;
/// 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 {
// Abbreviation name. This is unique within the abbreviation set.
// This is used as the token to match unless we have a regex.
wcstring name{};
/// The key (recognized token) - either a literal or a regex pattern.
wcstring key{};
/// If set, use this regex to recognize tokens.
/// If unset, the key is to be interpreted literally.
/// Note that the fish interface enforces that regexes match the entire token;
/// we accomplish this by surrounding the regex in ^ and $.
maybe_t<re::regex_t> regex{};
/// Replacement string.
wcstring replacement{};
/// If set, the replacement is a function name.
bool replacement_is_function{};
/// Expansion position.
abbrs_position_t position{abbrs_position_t::command};
/// If set, then move the cursor to the first instance of this string in the expansion.
maybe_t<wcstring> set_cursor_marker{};
/// Mark if we came from a universal variable.
bool from_universal{};
// \return true if this is a regex abbreviation.
bool is_regex() const { return this->regex.has_value(); }
// \return true if we match a token at a given position.
bool matches(const wcstring &token, abbrs_position_t position) const;
// Construct from a name, a key which matches a token, a replacement token, a position, and
// whether we are derived from a universal variable.
explicit abbreviation_t(wcstring name, wcstring key, wcstring replacement,
abbrs_position_t position = abbrs_position_t::command,
bool from_universal = false);
abbreviation_t() = default;
private:
// \return if we expand in a given position.
bool matches_position(abbrs_position_t position) const;
};
/// The result of an abbreviation expansion.
struct abbrs_replacer_t {
/// The string to use to replace the incoming token, either literal or as a function name.
wcstring replacement;
/// If true, treat 'replacement' as the name of a function.
bool is_function;
/// If set, the cursor should be moved to the first instance of this string in the expansion.
maybe_t<wcstring> set_cursor_marker;
};
using abbrs_replacer_list_t = std::vector<abbrs_replacer_t>;
/// A helper type for replacing a range in a string.
struct abbrs_replacement_t {
/// The original range of the token in the command line.
source_range_t range{};
/// The string to replace with.
wcstring text{};
/// The new cursor location, or none to use the default.
/// This is relative to the original range.
maybe_t<size_t> cursor{};
/// Construct a replacement from a replacer.
/// The \p range is the range of the text matched by the replacer in the command line.
/// The text is passed in separately as it may be the output of the replacer's function.
static abbrs_replacement_t from(source_range_t range, wcstring text,
const abbrs_replacer_t &replacer);
};
class abbrs_set_t {
public:
/// \return the list of replacers for an input token, in priority order.
/// The \p position is given to describe where the token was found.
abbrs_replacer_list_t match(const wcstring &token, abbrs_position_t position) const;
/// \return whether we would have at least one replacer for a given token.
bool has_match(const wcstring &token, abbrs_position_t position) const;
/// Add an abbreviation. Any abbreviation with the same name is replaced.
void add(abbreviation_t &&abbr);
/// Rename an abbreviation. This asserts that the old name is used, and the new name is not; the
/// caller should check these beforehand with has_name().
void rename(const wcstring &old_name, const wcstring &new_name);
/// Erase an abbreviation by name.
/// \return true if erased, false if not found.
bool erase(const wcstring &name);
/// \return true if we have an abbreviation with the given name.
bool has_name(const wcstring &name) const { return used_names_.count(name) > 0; }
/// \return a reference to the abbreviation list.
const std::vector<abbreviation_t> &list() const { return abbrs_; }
/// Import from a universal variable set.
void import_from_uvars(const std::unordered_map<wcstring, env_var_t> &uvars);
private:
/// List of abbreviations, in definition order.
std::vector<abbreviation_t> abbrs_{};
/// Set of used abbrevation names.
/// This is to avoid a linear scan when adding new abbreviations.
std::unordered_set<wcstring> used_names_;
};
/// \return the global mutable set of abbreviations.
acquired_lock<abbrs_set_t> abbrs_get_set();
/// \return the list of replacers for an input token, in priority order, using the global set.
/// The \p position is given to describe where the token was found.
inline abbrs_replacer_list_t abbrs_match(const wcstring &token, abbrs_position_t position) {
return abbrs_get_set()->match(token, position);
}
#endif

View file

@ -29,6 +29,7 @@
#include <memory>
#include <string>
#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")},

403
src/builtins/abbr.cpp Normal file
View file

@ -0,0 +1,403 @@
// Implementation of the read builtin.
#include "config.h" // IWYU pragma: keep
#include <termios.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cwchar>
#include <memory>
#include <numeric>
#include <string>
#include <vector>
#include "../abbrs.h"
#include "../builtin.h"
#include "../common.h"
#include "../env.h"
#include "../io.h"
#include "../re.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{};
bool function{};
maybe_t<wcstring> regex_pattern;
maybe_t<abbrs_position_t> position{};
maybe_t<wcstring> set_cursor_marker{};
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;
}
if (!add && regex_pattern.has_value()) {
streams.err.append_format(_(L"%ls: --regex option requires --add\n"), CMD);
return false;
}
if (!add && function) {
streams.err.append_format(_(L"%ls: --function option requires --add\n"), CMD);
return false;
}
if (!add && set_cursor_marker.has_value()) {
streams.err.append_format(_(L"%ls: --set-cursor option requires --add\n"), CMD);
return false;
}
if (set_cursor_marker.has_value() && set_cursor_marker->empty()) {
streams.err.append_format(_(L"%ls: --set-cursor argument cannot be empty\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_set();
wcstring_list_t comps{};
for (const auto &abbr : abbrs->list()) {
comps.clear();
comps.push_back(L"abbr -a");
if (abbr.from_universal) comps.push_back(L"-U");
comps.push_back(L"--");
// Literal abbreviations have the name and key as the same.
// Regex abbreviations have a pattern separate from the name.
comps.push_back(escape_string(abbr.name));
if (abbr.is_regex()) {
comps.push_back(L"--regex");
comps.push_back(escape_string(abbr.key));
}
if (abbr.set_cursor_marker.has_value()) {
comps.push_back(L"--set-cursor=" + escape_string(*abbr.set_cursor_marker));
}
if (abbr.replacement_is_function) {
comps.push_back(L"--function");
}
comps.push_back(escape_string(abbr.replacement));
wcstring result = join_strings(comps, L' ');
result.push_back(L'\n');
streams.out.append(result);
}
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_set();
for (const auto &abbr : abbrs->list()) {
wcstring name = escape_string(abbr.name);
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_set();
if (!abbrs->has_name(old_name)) {
streams.err.append_format(_(L"%ls %ls: No abbreviation named %ls\n"), CMD, subcmd,
old_name.c_str());
return STATUS_CMD_ERROR;
}
if (abbrs->has_name(new_name)) {
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;
}
abbrs->rename(old_name, new_name);
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_set();
for (const auto &arg : opts.args) {
if (abbrs->has_name(arg)) {
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;
}
maybe_t<re::regex_t> regex;
wcstring key;
if (!opts.regex_pattern.has_value()) {
// The name plays double-duty as the token to replace.
key = name;
} else {
key = *opts.regex_pattern;
re::re_error_t error{};
// Compile the regex as given; if that succeeds then wrap it in our ^$ so it matches the
// entire token.
if (!re::regex_t::try_compile(*opts.regex_pattern, re::flags_t{}, &error)) {
streams.err.append_format(_(L"%ls: Regular expression compile error: %ls\n"), CMD,
error.message().c_str());
streams.err.append_format(L"%ls: %ls\n", CMD, opts.regex_pattern->c_str());
streams.err.append_format(L"%ls: %*ls\n", CMD, static_cast<int>(error.offset), L"^");
return STATUS_INVALID_ARGS;
}
wcstring anchored = re::make_anchored(*opts.regex_pattern);
regex = re::regex_t::try_compile(anchored, re::flags_t{}, &error);
assert(regex.has_value() && "Anchored compilation should have succeeded");
}
wcstring replacement;
for (auto iter = opts.args.begin() + 1; iter != opts.args.end(); ++iter) {
if (!replacement.empty()) replacement.push_back(L' ');
replacement.append(*iter);
}
// Abbreviation function names disallow spaces.
// This is to prevent accidental usage of e.g. `--function 'string replace'`
if (opts.function &&
(!valid_func_name(replacement) || replacement.find(L' ') != wcstring::npos)) {
streams.err.append_format(_(L"%ls: Invalid function name: %ls\n"), CMD,
replacement.c_str());
return STATUS_INVALID_ARGS;
}
abbrs_position_t position = opts.position ? *opts.position : abbrs_position_t::command;
// Note historically we have allowed overwriting existing abbreviations.
abbreviation_t abbr{std::move(name), std::move(key), std::move(replacement), position};
abbr.regex = std::move(regex);
abbr.replacement_is_function = opts.function;
abbr.set_cursor_marker = opts.set_cursor_marker;
abbrs_get_set()->add(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_set();
for (const auto &arg : opts.args) {
if (!abbrs->erase(arg)) {
result = ENV_NOT_FOUND;
}
}
return result;
}
} // namespace
maybe_t<int> 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, SET_CURSOR_SHORT, RENAME_SHORT };
// 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"-afr:seqgUh";
static const struct woption long_options[] = {
{L"add", no_argument, 'a'}, {L"position", required_argument, 'p'},
{L"regex", required_argument, 'r'}, {L"set-cursor", optional_argument, SET_CURSOR_SHORT},
{L"function", no_argument, 'f'}, {L"rename", no_argument, RENAME_SHORT},
{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:
// 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'\n"
L"Position must be one of: command, anywhere.\n"),
CMD, w.woptarg);
return STATUS_INVALID_ARGS;
}
break;
}
case 'r': {
if (opts.regex_pattern.has_value()) {
streams.err.append_format(_(L"%ls: Cannot specify multiple regex patterns\n"),
CMD);
return STATUS_INVALID_ARGS;
}
opts.regex_pattern = w.woptarg;
break;
}
case SET_CURSOR_SHORT: {
if (opts.set_cursor_marker.has_value()) {
streams.err.append_format(
_(L"%ls: Cannot specify multiple set-cursor options\n"), CMD);
return STATUS_INVALID_ARGS;
}
// The default set-cursor indicator is '%'.
opts.set_cursor_marker = w.woptarg ? w.woptarg : L"%";
break;
}
case 'f':
opts.function = true;
break;
case RENAME_SHORT:
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;
}

11
src/builtins/abbr.h Normal file
View file

@ -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<int> builtin_abbr(parser_t &parser, io_streams_t &streams, const wchar_t **argv);
#endif

View file

@ -24,6 +24,7 @@
#include <unordered_set>
#include <utility>
#include "abbrs.h"
#include "autoload.h"
#include "builtin.h"
#include "common.h"
@ -668,16 +669,23 @@ void completer_t::complete_cmd(const wcstring &str_cmd) {
}
void completer_t::complete_abbr(const wcstring &cmd) {
std::map<wcstring, wcstring> abbrs = get_abbreviations(ctx.vars);
// Copy the list of names and descriptions so as not to hold the lock across the call to
// complete_strings.
completion_list_t possible_comp;
possible_comp.reserve(abbrs.size());
for (const auto &kv : abbrs) {
possible_comp.emplace_back(kv.first);
std::unordered_map<wcstring, wcstring> descs;
{
auto abbrs = abbrs_get_set();
for (const auto &abbr : abbrs->list()) {
if (!abbr.is_regex()) {
possible_comp.emplace_back(abbr.key);
descs[abbr.key] = abbr.replacement;
}
}
}
auto desc_func = [&](const wcstring &key) {
auto iter = abbrs.find(key);
assert(iter != abbrs.end() && "Abbreviation not found");
auto iter = descs.find(key);
assert(iter != descs.end() && "Abbreviation not found");
return format_string(ABBR_DESC, iter->second.c_str());
};
this->complete_strings(cmd, desc_func, possible_comp, COMPLETE_NO_SPACE);

View file

@ -18,6 +18,7 @@
#include <utility>
#include <vector>
#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_get_set()->import_from_uvars(table);
}
}

View file

@ -1326,31 +1326,3 @@ bool fish_xdm_login_hack_hack_hack_hack(std::vector<std::string> *cmds, int argc
}
return result;
}
maybe_t<wcstring> 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<wcstring, wcstring> get_abbreviations(const environment_t &vars) {
const wcstring prefix = L"_fish_abbr_";
auto names = vars.get_names(0);
std::map<wcstring, wcstring> 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;
}

View file

@ -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<wcstring> 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<wcstring, wcstring> get_abbreviations(const environment_t &vars);
// Terrible hacks
bool fish_xdm_login_hack_hack_hack_hack(std::vector<std::string> *cmds, int argc,
const char *const *argv);

View file

@ -46,6 +46,7 @@
#include <sanitizer/lsan_interface.h>
#endif
#include "abbrs.h"
#include "ast.h"
#include "autoload.h"
#include "builtin.h"
@ -2466,84 +2467,116 @@ 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<std::pair<const wcstring, const wcstring>> 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 literal_abbr = [](const wchar_t *name, const wchar_t *repl,
abbrs_position_t pos = abbrs_position_t::command) {
return abbreviation_t(name, name /* key */, repl, pos);
};
auto abbrs = abbrs_get_set();
abbrs->add(literal_abbr(L"gc", L"git checkout"));
abbrs->add(literal_abbr(L"foo", L"bar"));
abbrs->add(literal_abbr(L"gx", L"git checkout"));
abbrs->add(literal_abbr(L"yin", 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");
// Helper to expand an abbreviation, enforcing we have no more than one result.
auto abbr_expand_1 = [](const wcstring &token, abbrs_position_t pos) -> maybe_t<wcstring> {
auto result = abbrs_match(token, pos);
if (result.size() > 1) {
err(L"abbreviation expansion for %ls returned more than 1 result", token.c_str());
}
if (result.empty()) {
return none();
}
return result.front().replacement;
};
auto mresult = expand_abbreviation(L"gc", vars);
auto cmd = abbrs_position_t::command;
if (abbr_expand_1(L"", cmd)) err(L"Unexpected success with empty abbreviation");
if (abbr_expand_1(L"nothing", cmd)) err(L"Unexpected success with missing abbreviation");
auto mresult = abbr_expand_1(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 = abbr_expand_1(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<wcstring> result;
auto expand_abbreviation_in_command = [](const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars) -> maybe_t<wcstring> {
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<wcstring> {
if (auto replacement = reader_expand_abbreviation_at_cursor(cmdline, cursor_pos,
parser_t::principal_parser())) {
wcstring cmdline_expanded = cmdline;
std::vector<highlight_spec_t> colors{cmdline_expanded.size()};
apply_edit(&cmdline_expanded, &colors, *edit);
apply_edit(&cmdline_expanded, &colors, edit_t{replacement->range, replacement->text});
return cmdline_expanded;
}
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());
}
// Renaming works.
{
auto abbrs = abbrs_get_set();
do_test(!abbrs->has_name(L"gcc"));
do_test(abbrs->has_name(L"gc"));
abbrs->rename(L"gc", L"gcc");
do_test(abbrs->has_name(L"gcc"));
do_test(!abbrs->has_name(L"gc"));
do_test(!abbrs->erase(L"gc"));
do_test(abbrs->erase(L"gcc"));
do_test(!abbrs->erase(L"gcc"));
}
}
/// Test path functions.
@ -3502,17 +3535,16 @@ 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_set()->add(abbreviation_t(L"somename", 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);
// Abbreviations should not have a space after them.
do_test(completions.at(1).completion == L"zero");
do_test((completions.at(1).flags & COMPLETE_NO_SPACE) != 0);
abbrs_get_set()->erase(L"testabbrsonetwothreezero");
// Test wraps.
do_test(comma_join(complete_get_wrap_targets(L"wrapper1")).empty());
@ -6435,6 +6467,10 @@ void test_maybe() {
do_test(std::is_copy_assignable<maybe_t<noncopyable>>::value == false);
do_test(std::is_copy_constructible<maybe_t<noncopyable>>::value == false);
// We can construct a maybe_t from noncopyable things.
maybe_t<noncopyable> nmt{make_unique<int>(42)};
do_test(**nmt == 42);
maybe_t<std::string> c1{"abc"};
maybe_t<std::string> c2 = c1;
do_test(c1.value() == "abc");
@ -6442,6 +6478,11 @@ void test_maybe() {
c2 = c1;
do_test(c1.value() == "abc");
do_test(c2.value() == "abc");
do_test(c2.value_or("derp") == "abc");
do_test(c2.value_or("derp") == "abc");
c2.reset();
do_test(c2.value_or("derp") == "derp");
}
void test_layout_cache() {
@ -6858,6 +6899,21 @@ static void test_re_basic() {
}
do_test(join_strings(matches, L',') == L"AA,CC,11");
do_test(join_strings(captures, L',') == L"A,C,1");
// Test make_anchored
re = regex_t::try_compile(make_anchored(L"ab(.+?)"));
do_test(re.has_value());
do_test(!re->match(L""));
do_test(!re->match(L"ab"));
do_test((re->match(L"abcd") == match_range_t{0, 4}));
do_test((re->match(L"abcdefghij") == match_range_t{0, 10}));
re = regex_t::try_compile(make_anchored(L"(a+)|(b+)"));
do_test(re.has_value());
do_test(!re->match(L""));
do_test(!re->match(L"aabb"));
do_test((re->match(L"aaaa") == match_range_t{0, 4}));
do_test((re->match(L"bbbb") == match_range_t{0, 4}));
}
static void test_re_reset() {

View file

@ -109,6 +109,8 @@ class category_list_t {
category_t path{L"path", L"Searching/using paths"};
category_t screen{L"screen", L"Screen repaints"};
category_t abbrs{L"abbrs", L"Abbreviation expansion"};
};
/// The class responsible for logging.

View file

@ -16,6 +16,7 @@
#include <unordered_set>
#include <utility>
#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_get_set()->has_match(cmd, abbrs_position_t::command);
// Regular commands
if (!is_valid && command_ok) is_valid = path_get_path(cmd, vars).has_value();

View file

@ -209,6 +209,14 @@ class maybe_t : private maybe_detail::conditionally_copyable_t<T> {
// Transfer the value to the caller.
T acquire() { return impl_.acquire(); }
// Return (a copy of) our value, or the given value if we are empty.
T value_or(T v) const {
if (this->has_value()) {
return this->value();
}
return v;
}
// Clear the value.
void reset() { impl_.reset(); }

View file

@ -130,6 +130,11 @@ maybe_t<match_range_t> regex_t::match(match_data_t &md, const wcstring &subject)
return match_range_t{ovector[0], ovector[1]};
}
maybe_t<match_range_t> regex_t::match(const wcstring &subject) const {
match_data_t md = this->prepare();
return this->match(md, subject);
}
maybe_t<match_range_t> regex_t::group(const match_data_t &md, size_t group_idx) const {
if (group_idx >= md.max_capture || group_idx >= pcre2_get_ovector_count(get_md(md.data))) {
return none();
@ -288,3 +293,13 @@ regex_t::regex_t(adapters::bytecode_ptr_t &&code) : code_(std::move(code)) {
}
wcstring re_error_t::message() const { return message_for_code(this->code); }
wcstring re::make_anchored(wcstring pattern) {
// PATTERN -> ^(:?PATTERN)$.
const wchar_t *prefix = L"^(?:";
const wchar_t *suffix = L")$";
pattern.reserve(pattern.size() + wcslen(prefix) + wcslen(suffix));
pattern.insert(0, prefix);
pattern.append(suffix);
return pattern;
}

View file

@ -111,6 +111,9 @@ class regex_t : noncopyable_t {
/// \return a range on a successful match, none on no match.
maybe_t<match_range_t> match(match_data_t &md, const wcstring &subject) const;
/// A convenience function which calls prepare() for you.
maybe_t<match_range_t> match(const wcstring &subject) const;
/// \return the matched range for an indexed or named capture group. 0 means the entire match.
maybe_t<match_range_t> group(const match_data_t &md, size_t group_idx) const;
maybe_t<match_range_t> group(const match_data_t &md, const wcstring &name) const;
@ -145,5 +148,10 @@ class regex_t : noncopyable_t {
adapters::bytecode_ptr_t code_;
};
/// Adjust a pattern so that it is anchored at both beginning and end.
/// This is a workaround for the fact that PCRE2_ENDANCHORED is unavailable on pre-2017 PCRE2
/// (e.g. 10.21, on Xenial).
wcstring make_anchored(wcstring pattern);
} // namespace re
#endif

View file

@ -44,6 +44,7 @@
#include <set>
#include <type_traits>
#include "abbrs.h"
#include "ast.h"
#include "color.h"
#include "common.h"
@ -787,8 +788,8 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
/// Do what we need to do whenever our pager selection changes.
void pager_selection_changed();
/// Expand abbreviations at the current cursor position, minus backtrack_amt.
bool expand_abbreviation_as_necessary(size_t cursor_backtrack);
/// Expand abbreviations at the current cursor position, minus cursor_backtrack.
bool expand_abbreviation_at_cursor(size_t cursor_backtrack);
/// \return true if the command line has changed and repainting is needed. If \p colors is not
/// null, then also return true if the colors have changed.
@ -871,8 +872,18 @@ class reader_data_t : public std::enable_shared_from_this<reader_data_t> {
void handle_readline_command(readline_cmd_t cmd, readline_loop_state_t &rls);
// Handle readline_cmd_t::execute. This may mean inserting a newline if the command is
// unfinished.
void handle_execute(readline_loop_state_t &rls);
// unfinished. It may also set 'finished' and 'cmd' inside the rls.
// \return true on success, false if we got an error, in which case the caller should fire the
// error event.
bool handle_execute(readline_loop_state_t &rls);
// Add the current command line contents to history.
void add_to_history() const;
// Expand abbreviations before execution.
// Replace the command line with any abbreviations as needed.
// \return the test result, which may be incomplete to insert a newline, or an error.
parser_test_error_bits_t expand_for_execute();
void clear_pager();
void select_completion_in_direction(selection_motion_t dir,
@ -1346,80 +1357,135 @@ void reader_data_t::pager_selection_changed() {
}
}
/// Expand abbreviations at the given cursor position. Does NOT inspect 'data'.
maybe_t<edit_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.
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);
assert(cmdsub_begin != nullptr && cmdsub_begin >= buff);
assert(cmdsub_end != nullptr && cmdsub_end >= cmdsub_begin);
// Determine the offset of this command substitution.
const size_t subcmd_offset = cmdsub_begin - buff;
const wcstring subcmd = wcstring(cmdsub_begin, cmdsub_end - cmdsub_begin);
const size_t subcmd_cursor_pos = cursor_pos - subcmd_offset;
// Parse this subcmd.
using namespace ast;
auto ast =
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<decorated_statement_t>();
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;
}
/// Expand an abbreviation replacer, which may mean running its function.
/// \return the replacement, or none to skip it. This may run fish script!
maybe_t<abbrs_replacement_t> expand_replacer(source_range_t range, const wcstring &token,
const abbrs_replacer_t &repl, parser_t &parser) {
if (!repl.is_function) {
// Literal replacement cannot fail.
FLOGF(abbrs, L"Expanded literal abbreviation <%ls> -> <%ls>", token.c_str(),
repl.replacement.c_str());
return abbrs_replacement_t::from(range, repl.replacement, repl);
}
// Now if we found a command node, expand it.
maybe_t<edit_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));
wcstring cmd = escape_string(repl.replacement);
cmd.push_back(L' ');
cmd.append(escape_string(token));
scoped_push<bool> not_interactive(&parser.libdata().is_interactive, false);
wcstring_list_t outputs{};
int ret = exec_subshell(cmd, parser, outputs, false /* not apply_exit_status */);
if (ret != STATUS_CMD_OK) {
return none();
}
wcstring result = join_strings(outputs, L'\n');
FLOGF(abbrs, L"Expanded function abbreviation <%ls> -> <%ls>", token.c_str(), result.c_str());
return abbrs_replacement_t::from(range, std::move(result), repl);
}
// Extract all the token ranges in \p str, along with whether they are an undecorated command.
// Tokens containing command substitutions are skipped; this ensures tokens are non-overlapping.
struct positioned_token_t {
source_range_t range;
bool is_cmd;
};
static std::vector<positioned_token_t> extract_tokens(const wcstring &str) {
using namespace ast;
parse_tree_flags_t ast_flags = parse_flag_continue_after_error |
parse_flag_accept_incomplete_tokens |
parse_flag_leave_unterminated;
auto ast = ast::ast_t::parse(str, ast_flags);
// Helper to check if a node is the command portion of an undecorated statement.
auto is_command = [&](const node_t *node) {
for (const node_t *cursor = node; cursor; cursor = cursor->parent) {
if (const auto *stmt = cursor->try_as<decorated_statement_t>()) {
if (!stmt->opt_decoration && node == &stmt->command) {
return true;
}
}
}
return false;
};
wcstring cmdsub_contents;
std::vector<positioned_token_t> result;
traversal_t tv = ast.walk();
while (const node_t *node = tv.next()) {
// We are only interested in leaf nodes with source.
if (node->category != category_t::leaf) continue;
source_range_t r = node->source_range();
if (r.length == 0) continue;
// If we have command subs, then we don't include this token; instead we recurse.
bool has_cmd_subs = false;
size_t cmdsub_cursor = r.start, cmdsub_start = 0, cmdsub_end = 0;
while (parse_util_locate_cmdsubst_range(str, &cmdsub_cursor, &cmdsub_contents,
&cmdsub_start, &cmdsub_end,
true /* accept incomplete */) > 0) {
if (cmdsub_start >= r.end()) {
break;
}
has_cmd_subs = true;
for (positioned_token_t t : extract_tokens(cmdsub_contents)) {
// cmdsub_start is the open paren; the contents start one after it.
t.range.start += static_cast<source_offset_t>(cmdsub_start + 1);
result.push_back(t);
}
}
if (!has_cmd_subs) {
// Common case of no command substitutions in this leaf node.
result.push_back(positioned_token_t{r, is_command(node)});
}
}
return result;
}
/// Expand abbreviations at the given cursor position.
/// \return the replacement. This does NOT inspect the current reader data.
maybe_t<abbrs_replacement_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline,
size_t cursor_pos,
parser_t &parser) {
// Find the token containing the cursor. Usually users edit from the end, so walk backwards.
const auto tokens = extract_tokens(cmdline);
auto iter = std::find_if(tokens.rbegin(), tokens.rend(), [&](const positioned_token_t &t) {
return t.range.contains_inclusive(cursor_pos);
});
if (iter == tokens.rend()) {
return none();
}
source_range_t range = iter->range;
abbrs_position_t position =
iter->is_cmd ? abbrs_position_t::command : abbrs_position_t::anywhere;
wcstring token_str = cmdline.substr(range.start, range.length);
auto replacers = abbrs_match(token_str, position);
for (const auto &replacer : replacers) {
if (auto replacement = expand_replacer(range, token_str, replacer, parser)) {
return replacement;
}
}
return none();
}
/// Expand abbreviations at the current cursor position, minus the given cursor backtrack. This may
/// change the command line but does NOT repaint it. This is to allow the caller to coalesce
/// repaints.
bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) {
bool reader_data_t::expand_abbreviation_at_cursor(size_t cursor_backtrack) {
bool result = false;
editable_line_t *el = active_edit_line();
if (conf.expand_abbrev_ok && el == &command_line) {
// Try expanding abbreviations.
this->update_commandline_state();
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())) {
push_edit(el, std::move(*edit));
update_buff_pos(el);
if (auto replacement =
reader_expand_abbreviation_at_cursor(el->text(), cursor_pos, this->parser())) {
push_edit(el, edit_t{replacement->range, std::move(replacement->text)});
update_buff_pos(el, replacement->cursor);
result = true;
}
}
@ -3590,7 +3656,10 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
break;
}
case rl::execute: {
this->handle_execute(rls);
if (!this->handle_execute(rls)) {
event_fire_generic(parser(), L"fish_posterror", {command_line.text()});
screen.reset_abandoning_line(termsize_last().width);
}
break;
}
@ -4117,7 +4186,7 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
}
case rl::expand_abbr: {
if (expand_abbreviation_as_necessary(1)) {
if (expand_abbreviation_at_cursor(1)) {
inputter.function_set_status(true);
} else {
inputter.function_set_status(false);
@ -4164,13 +4233,69 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat
}
}
void reader_data_t::handle_execute(readline_loop_state_t &rls) {
void reader_data_t::add_to_history() const {
if (!history || conf.in_silent_mode) {
return;
}
// Historical behavior is to trim trailing spaces, unless escape (#7661).
wcstring text = command_line.text();
while (!text.empty() && text.back() == L' ' &&
count_preceding_backslashes(text, text.size() - 1) % 2 == 0) {
text.pop_back();
}
// Remove ephemeral items - even if the text is empty.
history->remove_ephemeral_items();
if (!text.empty()) {
// Mark this item as ephemeral if there is a leading space (#615).
history_persistence_mode_t mode;
if (text.front() == L' ') {
// Leading spaces are ephemeral (#615).
mode = history_persistence_mode_t::ephemeral;
} else if (in_private_mode(this->vars())) {
// Private mode means in-memory only.
mode = history_persistence_mode_t::memory;
} else {
mode = history_persistence_mode_t::disk;
}
history_t::add_pending_with_file_detection(history, text, this->vars().snapshot(), mode);
}
}
parser_test_error_bits_t reader_data_t::expand_for_execute() {
// Expand abbreviations at the cursor.
// The first expansion is "user visible" and enters into history.
editable_line_t *el = &command_line;
parser_test_error_bits_t test_res = 0;
// Syntax check before expanding abbreviations. We could consider relaxing this: a string may be
// syntactically invalid but become valid after expanding abbreviations.
if (conf.syntax_check_ok) {
test_res = reader_shell_test(parser(), el->text());
if (test_res & PARSER_TEST_ERROR) return test_res;
}
// Exec abbreviations at the cursor.
// Note we want to expand abbreviations even if incomplete.
if (expand_abbreviation_at_cursor(0)) {
// Trigger syntax highlighting as we are likely about to execute this command.
this->super_highlight_me_plenty();
if (conf.syntax_check_ok) {
test_res = reader_shell_test(parser(), el->text());
}
}
return test_res;
}
bool reader_data_t::handle_execute(readline_loop_state_t &rls) {
// Evaluate. If the current command is unfinished, or if the charater is escaped
// using a backslash, insert a newline.
// If the user hits return while navigating the pager, it only clears the pager.
if (is_navigating_pager_contents()) {
clear_pager();
return;
return true;
}
// Delete any autosuggestion.
@ -4203,77 +4328,24 @@ void reader_data_t::handle_execute(readline_loop_state_t &rls) {
// If the conditions are met, insert a new line at the position of the cursor.
if (continue_on_next_line) {
insert_char(el, L'\n');
return;
return true;
}
// See if this command is valid.
parser_test_error_bits_t command_test_result = 0;
if (conf.syntax_check_ok) {
command_test_result = reader_shell_test(parser(), el->text());
}
if (command_test_result == 0 || command_test_result == PARSER_TEST_INCOMPLETE) {
// This command is valid, but an abbreviation may make it invalid. If so, we
// will have to test again.
if (expand_abbreviation_as_necessary(0)) {
// Trigger syntax highlighting as we are likely about to execute this command.
this->super_highlight_me_plenty();
if (conf.syntax_check_ok) {
command_test_result = reader_shell_test(parser(), el->text());
}
}
}
if (command_test_result == 0) {
// Finished command, execute it. Don't add items in silent mode (#7230).
wcstring text = command_line.text();
if (text.empty()) {
// Here the user just hit return. Make a new prompt, don't remove ephemeral
// items.
rls.finished = true;
return;
}
// Historical behavior is to trim trailing spaces.
// However, escaped spaces ('\ ') should not be trimmed (#7661)
// This can be done by counting pre-trailing '\'
// If there's an odd number, this must be an escaped space.
while (!text.empty() && text.back() == L' ' &&
count_preceding_backslashes(text, text.size() - 1) % 2 == 0) {
text.pop_back();
}
if (history && !conf.in_silent_mode) {
// Remove ephemeral items - even if the text is empty
history->remove_ephemeral_items();
if (!text.empty()) {
// Mark this item as ephemeral if there is a leading space (#615).
history_persistence_mode_t mode;
if (text.front() == L' ') {
// Leading spaces are ephemeral (#615).
mode = history_persistence_mode_t::ephemeral;
} else if (in_private_mode(this->vars())) {
// Private mode means in-memory only.
mode = history_persistence_mode_t::memory;
} else {
mode = history_persistence_mode_t::disk;
}
history_t::add_pending_with_file_detection(history, text, this->vars().snapshot(),
mode);
}
}
rls.finished = true;
update_buff_pos(&command_line, command_line.size());
} else if (command_test_result == PARSER_TEST_INCOMPLETE) {
// We are incomplete, continue editing.
// Expand the command line in preparation for execution.
// to_exec is the command to execute; the command line itself has the command for history.
parser_test_error_bits_t test_res = this->expand_for_execute();
if (test_res & PARSER_TEST_ERROR) {
return false;
} else if (test_res & PARSER_TEST_INCOMPLETE) {
insert_char(el, L'\n');
} else {
// Result must be some combination including an error. The error message will
// already be printed, all we need to do is repaint.
event_fire_generic(parser(), L"fish_posterror", {el->text()});
screen.reset_abandoning_line(termsize_last().width);
return true;
}
assert(test_res == 0);
this->add_to_history();
rls.finished = true;
update_buff_pos(&command_line, command_line.size());
return true;
}
maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
@ -4487,7 +4559,6 @@ maybe_t<wcstring> reader_data_t::readline(int nchars_or_0) {
}
outputter_t::stdoutput().set_color(rgb_color_t::reset(), rgb_color_t::reset());
}
return rls.finished ? maybe_t<wcstring>{command_line.text()} : none();
}

View file

@ -42,6 +42,9 @@ struct edit_t {
explicit edit_t(size_t offset, size_t length, wcstring replacement)
: offset(offset), length(length), replacement(std::move(replacement)) {}
explicit edit_t(source_range_t range, wcstring replacement)
: edit_t(range.start, range.length, std::move(replacement)) {}
/// Used for testing.
bool operator==(const edit_t &other) const;
};
@ -261,10 +264,14 @@ bool fish_is_unwinding_for_exit();
wcstring combine_command_and_autosuggestion(const wcstring &cmdline,
const wcstring &autosuggestion);
/// 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<edit_t> reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars);
/// Expand at most one abbreviation at the given cursor position, updating the position if the
/// abbreviation wants to move the cursor. Use the parser to run any abbreviations which want
/// function calls. \return none if no abbreviations were expanded, otherwise the resulting
/// replacement.
struct abbrs_replacement_t;
maybe_t<abbrs_replacement_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline,
size_t cursor_pos,
parser_t &parser);
/// Apply a completion string. Exposed for testing only.
wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags,

View file

@ -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);
}

View file

@ -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<size_t>::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];

View file

@ -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
@ -49,40 +56,40 @@ abbr | grep 'a b c'
# Test renaming
abbr __abbr4 omega
abbr | grep __abbr5
abbr -r __abbr4 __abbr5
abbr --rename __abbr4 __abbr5
abbr | grep __abbr5
# CHECK: abbr -a -U -- __abbr5 omega
# CHECK: abbr -a -- __abbr5 omega
abbr -e __abbr5
abbr | grep __abbr4
# Test renaming a nonexistent abbreviation
abbr -r __abbr6 __abbr
abbr --rename __abbr6 __abbr
# CHECKERR: abbr --rename: No abbreviation named __abbr6
# Test renaming to a abbreviation with spaces
abbr __abbr4 omega
abbr -r __abbr4 "g h i"
abbr --rename __abbr4 "g h i"
# CHECKERR: abbr --rename: Abbreviation 'g h i' cannot have spaces in the word
abbr -e __abbr4
# Test renaming without arguments
abbr __abbr7 omega
abbr -r __abbr7
abbr --rename __abbr7
# CHECKERR: abbr --rename: Requires exactly two arguments
# Test renaming with too many arguments
abbr __abbr8 omega
abbr -r __abbr8 __abbr9 __abbr10
abbr --rename __abbr8 __abbr9 __abbr10
# CHECKERR: abbr --rename: Requires exactly two arguments
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
abbr __abbr12 omega12
abbr -r __abbr11 __abbr12
abbr --rename __abbr11 __abbr12
# CHECKERR: abbr --rename: Abbreviation __abbr12 already exists, cannot rename __abbr11
abbr __abbr-with-dashes omega
@ -106,3 +113,57 @@ 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
abbr --query banana --function
echo $status
# CHECKERR: abbr: --function option requires --add
# CHECK: 2
abbr --add peach --function invalid/function/name
echo $status
# CHECKERR: abbr: Invalid function name: invalid/function/name
# CHECK: 2
# Function names cannot contain spaces, to prevent confusion with fish script.
abbr --add peach --function 'no space allowed'
echo $status
# CHECKERR: abbr: Invalid function name: no space allowed
# CHECK: 2
# Erase all abbreviations
abbr --erase (abbr --list)
abbr --show
# Should be no output
abbr --add nonregex_name foo
abbr --add regex_name --regex 'A[0-9]B' bar
abbr --show
# CHECK: abbr -a -- nonregex_name foo
# CHECK: abbr -a -- regex_name --regex 'A[0-9]B' bar
abbr --erase (abbr --list)
abbr --add bogus --position never stuff
# CHECKERR: abbr: Invalid position 'never'
# CHECKERR: Position must be one of: command, anywhere.
abbr --add bogus --position anywhere --position command stuff
# CHECKERR: abbr: Cannot specify multiple positions

View file

@ -24,23 +24,23 @@ functions -D f2
# ==========
# Verify that `functions --details` works as expected when given the name of a
# function that could be autoloaded but isn't currently loaded.
set x (functions -D abbr)
set x (functions -D vared)
if test (count $x) -ne 1
or not string match -q '*/share/functions/abbr.fish' "$x"
echo "Unexpected output for 'functions -D abbr': $x" >&2
or not string match -q '*/share/functions/vared.fish' "$x"
echo "Unexpected output for 'functions -D vared': $x" >&2
end
# ==========
# Verify that `functions --verbose --details` works as expected when given the name of a
# function that was autoloaded.
set x (functions -v -D abbr)
set x (functions -v -D vared)
if test (count $x) -ne 5
or not string match -q '*/share/functions/abbr.fish' $x[1]
or not string match -q '*/share/functions/vared.fish' $x[1]
or test $x[2] != autoloaded
or test $x[3] != 1
or test $x[3] != 6
or test $x[4] != scope-shadowing
or test $x[5] != 'Manage abbreviations'
echo "Unexpected output for 'functions -v -D abbr': $x" >&2
or test $x[5] != 'Edit variable value'
echo "Unexpected output for 'functions -v -D vared': $x" >&2
end
# ==========

156
tests/pexpects/abbrs.py Normal file
View file

@ -0,0 +1,156 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
import re
sp = SpawnedProc()
send, sendline, sleep, expect_prompt, expect_re, expect_str = (
sp.send,
sp.sendline,
sp.sleep,
sp.expect_prompt,
sp.expect_re,
sp.expect_str,
)
expect_prompt()
# ? prints the command line, bracketed by <>, then clears it.
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"<beta >")
send(r"echo alpha ?")
expect_str(r"<echo alpha >")
# Abbreviation expansions may have multiple words.
sendline(r"abbr --add emacsnw emacs -nw")
expect_prompt()
send(r"emacsnw ?")
expect_str(r"<emacs -nw >")
# Regression test that abbreviations still expand in incomplete positions.
sendline(r"""abbr --erase (abbr --list)""")
expect_prompt()
sendline(r"""abbr --add foo echo bar""")
expect_prompt()
sendline(r"if foo")
expect_str(r"if echo bar")
sendline(r"end")
expect_prompt(r"bar")
# Regression test that 'echo (' doesn't hang.
sendline(r"echo )")
expect_str(r"Unexpected ')' for unopened parenthesis")
send(r"?")
expect_str(r"<echo )>")
# Support position anywhere.
sendline(r"abbr alpha --position anywhere beta2")
expect_prompt()
send(r"alpha ?")
expect_str(r"<beta2 >")
send(r"echo alpha ?")
expect_str(r"<echo beta2 >")
# Support regex.
sendline(r"abbr alpha --regex 'A[0-9]+Z' beta3")
expect_prompt()
send(r"A123Z ?")
expect_str(r"<beta3 >")
send(r"AZ ?")
expect_str(r"<AZ >")
send(r"QA123Z ?") # fails as the regex must match the entire string
expect_str(r"<QA123Z >")
send(r"A0000000000000000000009Z ?")
expect_str(r"<beta3 >")
# Support functions. Here anything in between @@ is uppercased, except 'nope'.
sendline(
r"""function uppercaser
string match --quiet '*nope*' $argv[1] && return 1
string trim --chars @ $argv | string upper
end
""".strip()
)
expect_prompt()
sendline(r"abbr uppercaser --regex '@.+@' --function uppercaser")
expect_prompt()
send(r"@abc@ ?")
expect_str(r"<ABC >")
send(r"@nope@ ?")
expect_str(r"<@nope@ >")
sendline(r"abbr --erase uppercaser")
expect_prompt()
# -f works as shorthand for --function.
sendline(r"abbr uppercaser2 --regex '@.+@' -f uppercaser")
expect_prompt()
send(r"@abc@ ?")
expect_str(r"<ABC >")
send(r"@nope@ ?")
expect_str(r"<@nope@ >")
sendline(r"abbr --erase uppercaser2")
expect_prompt()
# Abbreviations which cause the command line to become incomplete or invalid
# are visibly expanded.
sendline(r"abbr openparen '(' --position anywhere")
expect_prompt()
sendline(r"abbr closeparen ')' --position anywhere")
expect_prompt()
sendline(r"echo openparen")
expect_str(r"echo (")
send(r"?")
expect_str(r"<echo (>")
sendline(r"echo closeparen")
expect_str(r"echo )")
send(r"?")
expect_str(r"<echo )>")
sendline(r"echo openparen seq 5 closeparen") # expands on enter
expect_prompt(r"1 2 3 4 5")
sendline(r"echo openparen seq 5 closeparen ") # expands on space
expect_prompt(r"1 2 3 4 5")
# Verify that 'commandline' is accurate.
# Abbreviation functions cannot usefully change the command line, but they can read it.
sendline(
r"""function check_cmdline
set -g last_cmdline (commandline)
set -g last_cursor (commandline --cursor)
false
end
""".strip()
)
expect_prompt()
sendline(r"abbr check_cmdline --regex '@.+@' --function check_cmdline")
expect_prompt()
send(r"@abc@ ?")
expect_str(r"<@abc@ >")
sendline(r"echo $last_cursor : $last_cmdline")
expect_prompt(r"6 : @abc@ ")
# Test cursor positioning.
sendline(r"""abbr --erase (abbr --list) """)
expect_prompt()
sendline(r"""abbr LLL --position anywhere --set-cursor 'abc%ghi'""")
expect_prompt()
send(r"""echo LLL def?""")
expect_str(r"<echo abcdefghi >")
sendline(r"""abbr --erase (abbr --list) """)
expect_prompt()
sendline(r"""abbr LLL --position anywhere --set-cursor=!HERE! '!HERE! | less'""")
expect_prompt()
send(r"""echo LLL derp?""")
expect_str(r"<echo derp | less >")