Re-implement abbreviations as a built-in

Prior to this change, abbreviations were stored as fish variables, often
universal. However we intend to add additional features to abbreviations
which would be very awkward to shoe-horn into variables.

Re-implement abbreviations using a builtin, managing them internally.

Existing abbreviations stored in universal variables are still imported,
for compatibility. However new abbreviations will need to be added to a
function. A follow-up commit will add it.

Now that abbr is a built-in, remove the abbr function; but leave the
abbr.fish file so that stale files from past installs do not override
the abbr builtin.
This commit is contained in:
ridiculousfish 2022-04-01 12:05:27 -07:00
parent 635cc3ee8d
commit 1402bae7f4
20 changed files with 608 additions and 339 deletions

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

@ -64,17 +64,16 @@ Examples
::
abbr -a -g gco git checkout
abbr -a gco git checkout
Add a new abbreviation where ``gco`` will be replaced with ``git checkout`` global to the current shell.
Add a new abbreviation where ``gco`` will be replaced with ``git checkout``.
This abbreviation will not be automatically visible to other shells unless the same command is run in those shells (such as when executing the commands in config.fish).
::
abbr -a -U l less
abbr -a l less
Add a new abbreviation where ``l`` will be replaced with ``less`` universal to all shells.
Note that you omit the **-U** since it is the default.
::

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.

62
src/abbrs.cpp Normal file
View File

@ -0,0 +1,62 @@
#include "config.h" // IWYU pragma: keep
#include "abbrs.h"
#include "env.h"
#include "global_safety.h"
#include "wcstringutil.h"
static relaxed_atomic_t<uint64_t> k_abbrs_next_order{0};
abbreviation_t::abbreviation_t(wcstring replacement, abbrs_position_t position, bool from_universal)
: replacement(std::move(replacement)),
position(position),
from_universal(from_universal),
order(++k_abbrs_next_order) {}
acquired_lock<abbrs_map_t> abbrs_get_map() {
static owning_lock<std::unordered_map<wcstring, abbreviation_t>> abbrs;
return abbrs.acquire();
}
maybe_t<wcstring> abbrs_expand(const wcstring &token, abbrs_position_t position) {
auto abbrs = abbrs_get_map();
auto iter = abbrs->find(token);
maybe_t<wcstring> result{};
if (iter != abbrs->end()) {
const abbreviation_t &abbr = iter->second;
// Expand only if the positions are "compatible."
if (abbr.position == position || abbr.position == abbrs_position_t::anywhere) {
result = abbr.replacement;
}
}
return result;
}
wcstring_list_t abbrs_get_keys() {
auto abbrs = abbrs_get_map();
wcstring_list_t keys;
keys.reserve(abbrs->size());
for (const auto &kv : *abbrs) {
keys.push_back(kv.first);
}
return keys;
}
void abbrs_import_from_uvars(const std::unordered_map<wcstring, env_var_t> &uvars) {
auto abbrs = abbrs_get_map();
const wchar_t *const prefix = L"_fish_abbr_";
size_t prefix_len = wcslen(prefix);
wcstring name;
const bool from_universal = true;
for (const auto &kv : uvars) {
if (string_prefixes_string(prefix, kv.first)) {
wcstring escaped_name = kv.first.substr(prefix_len);
if (unescape_string(escaped_name, &name, unescape_flags_t{}, STRING_STYLE_VAR)) {
wcstring replacement = join_strings(kv.second.as_list(), L' ');
abbrs->emplace(name, abbreviation_t{std::move(replacement),
abbrs_position_t::command, from_universal});
}
}
}
}

53
src/abbrs.h Normal file
View File

@ -0,0 +1,53 @@
// Support for abbreviations.
//
#ifndef FISH_ABBRS_H
#define FISH_ABBRS_H
#include <unordered_map>
#include "common.h"
#include "maybe.h"
/// Controls where in the command line abbreviations may expand.
enum class abbrs_position_t : uint8_t {
command, // expand in command position
anywhere, // expand in any token
};
struct abbreviation_t {
// Replacement string.
wcstring replacement{};
// Expansion position.
abbrs_position_t position{abbrs_position_t::command};
// Mark if we came from a universal variable.
bool from_universal{};
// A monotone key to allow reconstructing definition order.
uint64_t order{};
explicit abbreviation_t(wcstring replacement,
abbrs_position_t position = abbrs_position_t::command,
bool from_universal = false);
abbreviation_t() = default;
};
using abbrs_map_t = std::unordered_map<wcstring, abbreviation_t>;
/// \return the mutable map of abbreviations, keyed by name.
acquired_lock<abbrs_map_t> abbrs_get_map();
/// \return the replacement value for a abbreviation token, if any.
/// The \p position is given to describe where the token was found.
maybe_t<wcstring> abbrs_expand(const wcstring &token, abbrs_position_t position);
/// \return the list of abbreviation keys.
wcstring_list_t abbrs_get_keys();
/// Import any abbreviations from universal variables.
class env_var_t;
void abbrs_import_from_uvars(const std::unordered_map<wcstring, env_var_t> &uvars);
#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")},

327
src/builtins/abbr.cpp Normal file
View File

@ -0,0 +1,327 @@
// 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 "../wcstringutil.h"
#include "../wgetopt.h"
#include "../wutil.h"
namespace {
static const wchar_t *const CMD = L"abbr";
struct abbr_options_t {
bool add{};
bool rename{};
bool show{};
bool list{};
bool erase{};
bool query{};
maybe_t<abbrs_position_t> position{};
wcstring_list_t args;
bool validate(io_streams_t &streams) {
// Duplicate options?
wcstring_list_t cmds;
if (add) cmds.push_back(L"add");
if (rename) cmds.push_back(L"rename");
if (show) cmds.push_back(L"show");
if (list) cmds.push_back(L"list");
if (erase) cmds.push_back(L"erase");
if (query) cmds.push_back(L"query");
if (cmds.size() > 1) {
streams.err.append_format(_(L"%ls: Cannot combine options %ls\n"), CMD,
join_strings(cmds, L", ").c_str());
return false;
}
// If run with no options, treat it like --add if we have arguments,
// or --show if we do not have any arguments.
if (cmds.empty()) {
show = args.empty();
add = !args.empty();
}
if (!add && position.has_value()) {
streams.err.append_format(_(L"%ls: --position option requires --add\n"), CMD);
return false;
}
return true;
}
};
// Print abbreviations in a fish-script friendly way.
static int abbr_show(const abbr_options_t &, io_streams_t &streams) {
const auto abbrs = abbrs_get_map();
for (const auto &kv : *abbrs) {
wcstring name = escape_string(kv.first);
const auto &abbr = kv.second;
wcstring value = escape_string(abbr.replacement);
const wchar_t *scope = (abbr.from_universal ? L"-U " : L"");
streams.out.append_format(L"abbr -a %ls-- %ls %ls\n", scope, name.c_str(), value.c_str());
}
return STATUS_CMD_OK;
}
// Print the list of abbreviation names.
static int abbr_list(const abbr_options_t &opts, io_streams_t &streams) {
const wchar_t *const subcmd = L"--list";
if (opts.args.size() > 0) {
streams.err.append_format(_(L"%ls %ls: Unexpected argument -- '%ls'\n"), CMD, subcmd,
opts.args.front().c_str());
return STATUS_INVALID_ARGS;
}
const auto abbrs = abbrs_get_map();
for (const auto &kv : *abbrs) {
wcstring name = escape_string(kv.first);
name.push_back(L'\n');
streams.out.append(name);
}
return STATUS_CMD_OK;
}
// Rename an abbreviation, deleting any existing one with the given name.
static int abbr_rename(const abbr_options_t &opts, io_streams_t &streams) {
const wchar_t *const subcmd = L"--rename";
if (opts.args.size() != 2) {
streams.err.append_format(_(L"%ls %ls: Requires exactly two arguments\n"), CMD, subcmd);
return STATUS_INVALID_ARGS;
}
const wcstring &old_name = opts.args[0];
const wcstring &new_name = opts.args[1];
if (old_name.empty() || new_name.empty()) {
streams.err.append_format(_(L"%ls %ls: Name cannot be empty\n"), CMD, subcmd);
return STATUS_INVALID_ARGS;
}
if (std::any_of(new_name.begin(), new_name.end(), iswspace)) {
streams.err.append_format(
_(L"%ls %ls: Abbreviation '%ls' cannot have spaces in the word\n"), CMD, subcmd,
new_name.c_str());
return STATUS_INVALID_ARGS;
}
auto abbrs = abbrs_get_map();
auto old_iter = abbrs->find(old_name);
if (old_iter == abbrs->end()) {
streams.err.append_format(_(L"%ls %ls: No abbreviation named %ls\n"), CMD, subcmd,
old_name.c_str());
return STATUS_CMD_ERROR;
}
// Cannot overwrite an abbreviation.
auto new_iter = abbrs->find(new_name);
if (new_iter != abbrs->end()) {
streams.err.append_format(
_(L"%ls %ls: Abbreviation %ls already exists, cannot rename %ls\n"), CMD, subcmd,
new_name.c_str(), old_name.c_str());
return STATUS_INVALID_ARGS;
}
abbreviation_t abbr = std::move(old_iter->second);
abbrs->erase(old_iter);
bool inserted = abbrs->insert(std::make_pair(std::move(new_name), std::move(abbr))).second;
assert(inserted && "Should have successfully inserted");
(void)inserted;
return STATUS_CMD_OK;
}
// Test if any args is an abbreviation.
static int abbr_query(const abbr_options_t &opts, io_streams_t &) {
// Return success if any of our args matches an abbreviation.
const auto abbrs = abbrs_get_map();
for (const auto &arg : opts.args) {
if (abbrs->find(arg) != abbrs->end()) {
return STATUS_CMD_OK;
}
}
return STATUS_CMD_ERROR;
}
// Add a named abbreviation.
static int abbr_add(const abbr_options_t &opts, io_streams_t &streams) {
const wchar_t *const subcmd = L"--add";
if (opts.args.size() < 2) {
streams.err.append_format(_(L"%ls %ls: Requires at least two arguments\n"), CMD, subcmd);
return STATUS_INVALID_ARGS;
}
const wcstring &name = opts.args[0];
if (name.empty()) {
streams.err.append_format(_(L"%ls %ls: Name cannot be empty\n"), CMD, subcmd);
return STATUS_INVALID_ARGS;
}
if (std::any_of(name.begin(), name.end(), iswspace)) {
streams.err.append_format(
_(L"%ls %ls: Abbreviation '%ls' cannot have spaces in the word\n"), CMD, subcmd,
name.c_str());
return STATUS_INVALID_ARGS;
}
abbreviation_t abbr{L""};
for (auto iter = opts.args.begin() + 1; iter != opts.args.end(); ++iter) {
if (!abbr.replacement.empty()) abbr.replacement.push_back(L' ');
abbr.replacement.append(*iter);
}
if (opts.position) {
abbr.position = *opts.position;
}
// Note historically we have allowed overwriting existing abbreviations.
auto abbrs = abbrs_get_map();
(*abbrs)[name] = std::move(abbr);
return STATUS_CMD_OK;
}
// Erase the named abbreviations.
static int abbr_erase(const abbr_options_t &opts, io_streams_t &) {
if (opts.args.empty()) {
// This has historically been a silent failure.
return STATUS_CMD_ERROR;
}
// Erase each. If any is not found, return ENV_NOT_FOUND which is historical.
int result = STATUS_CMD_OK;
auto abbrs = abbrs_get_map();
for (const auto &arg : opts.args) {
auto iter = abbrs->find(arg);
if (iter == abbrs->end()) {
result = ENV_NOT_FOUND;
} else {
abbrs->erase(iter);
}
}
return result;
}
} // namespace
maybe_t<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 };
// Note the leading '-' causes wgetopter to return arguments in order, instead of permuting
// them. We need this behavior for compatibility with pre-builtin abbreviations where options
// could be given literally, for example `abbr e emacs -nw`.
static const wchar_t *const short_options = L"-arseqgUh";
static const struct woption long_options[] = {{L"add", no_argument, 'a'},
{L"position", required_argument, 'p'},
{L"rename", no_argument, 'r'},
{L"erase", no_argument, 'e'},
{L"query", no_argument, 'q'},
{L"show", no_argument, 's'},
{L"list", no_argument, 'l'},
{L"global", no_argument, 'g'},
{L"universal", no_argument, 'U'},
{L"help", no_argument, 'h'},
{}};
int argc = builtin_count_args(argv);
int opt;
wgetopter_t w;
bool unrecognized_options_are_args = false;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
switch (opt) {
case NON_OPTION_ARGUMENT:
// Non-option argument.
// If --add is specified (or implied by specifying no other commands), all
// unrecognized options after the *second* non-option argument are considered part
// of the abbreviation expansion itself, rather than options to the abbr command.
// For example, `abbr e emacs -nw` works, because `-nw` occurs after the second
// non-option, and --add is implied.
opts.args.push_back(w.woptarg);
if (opts.args.size() >= 2 &&
!(opts.rename || opts.show || opts.list || opts.erase || opts.query)) {
unrecognized_options_are_args = true;
}
break;
case 'a':
opts.add = true;
break;
case 'p': {
if (opts.position.has_value()) {
streams.err.append_format(_(L"%ls: Cannot specify multiple positions\n"), CMD);
return STATUS_INVALID_ARGS;
}
if (!wcscmp(w.woptarg, L"command")) {
opts.position = abbrs_position_t::command;
} else if (!wcscmp(w.woptarg, L"anywhere")) {
opts.position = abbrs_position_t::anywhere;
} else {
streams.err.append_format(_(L"%ls: Invalid position '%ls'\nPosition must be "
L"one of: command, anywhere.\n"),
CMD, w.woptarg);
return STATUS_INVALID_ARGS;
}
break;
}
case 'r':
opts.rename = true;
break;
case 'e':
opts.erase = true;
break;
case 'q':
opts.query = true;
break;
case 's':
opts.show = true;
break;
case 'l':
opts.list = true;
break;
case 'g':
case 'U':
// Kept for backwards compatibility but ignored.
break;
case 'h': {
builtin_print_help(parser, streams, cmd);
return STATUS_CMD_OK;
}
case '?': {
if (unrecognized_options_are_args) {
opts.args.push_back(argv[w.woptind - 1]);
} else {
builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
}
}
}
opts.args.insert(opts.args.end(), argv + w.woptind, argv + argc);
if (!opts.validate(streams)) {
return STATUS_INVALID_ARGS;
}
if (opts.add) return abbr_add(opts, streams);
if (opts.show) return abbr_show(opts, streams);
if (opts.list) return abbr_list(opts, streams);
if (opts.rename) return abbr_rename(opts, streams);
if (opts.erase) return abbr_erase(opts, streams);
if (opts.query) return abbr_query(opts, streams);
// validate() should error or ensure at least one path is set.
DIE("unreachable");
return STATUS_INVALID_ARGS;
}

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,7 +669,8 @@ 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 map, so we don't hold the lock across the call to complete_strings.
abbrs_map_t abbrs = *abbrs_get_map();
completion_list_t possible_comp;
possible_comp.reserve(abbrs.size());
for (const auto &kv : abbrs) {
@ -678,7 +680,7 @@ void completer_t::complete_abbr(const wcstring &cmd) {
auto desc_func = [&](const wcstring &key) {
auto iter = abbrs.find(key);
assert(iter != abbrs.end() && "Abbreviation not found");
return format_string(ABBR_DESC, iter->second.c_str());
return format_string(ABBR_DESC, iter->second.replacement.c_str());
};
this->complete_strings(cmd, desc_func, possible_comp, COMPLETE_NO_SPACE);
}

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_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,34 +2467,31 @@ static void test_ifind_fuzzy() {
static void test_abbreviations() {
say(L"Testing abbreviations");
auto &vars = parser_t::principal_parser().vars();
vars.push(true);
const std::vector<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 abbrs = abbrs_get_map();
abbrs->emplace(L"gc", L"git checkout");
abbrs->emplace(L"foo", L"bar");
abbrs->emplace(L"gx", L"git checkout");
abbrs->emplace(L"yin", abbreviation_t(L"yang", abbrs_position_t::anywhere));
}
if (expand_abbreviation(L"", vars)) err(L"Unexpected success with empty abbreviation");
if (expand_abbreviation(L"nothing", vars)) err(L"Unexpected success with missing abbreviation");
auto cmd = abbrs_position_t::command;
if (abbrs_expand(L"", cmd)) err(L"Unexpected success with empty abbreviation");
if (abbrs_expand(L"nothing", cmd)) err(L"Unexpected success with missing abbreviation");
auto mresult = expand_abbreviation(L"gc", vars);
auto mresult = abbrs_expand(L"gc", cmd);
if (!mresult) err(L"Unexpected failure with gc abbreviation");
if (*mresult != L"git checkout") err(L"Wrong abbreviation result for gc");
mresult = expand_abbreviation(L"foo", vars);
mresult = abbrs_expand(L"foo", cmd);
if (!mresult) err(L"Unexpected failure with foo abbreviation");
if (*mresult != L"bar") err(L"Wrong abbreviation result for foo");
maybe_t<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 edit = reader_expand_abbreviation_at_cursor(cmdline, cursor_pos)) {
wcstring cmdline_expanded = cmdline;
std::vector<highlight_spec_t> colors{cmdline_expanded.size()};
apply_edit(&cmdline_expanded, &colors, *edit);
@ -2501,49 +2499,55 @@ static void test_abbreviations() {
}
return none_t();
};
result = expand_abbreviation_in_command(L"just a command", 3, vars);
result = expand_abbreviation_in_command(L"just a command", 3);
if (result) err(L"Command wrongly expanded on line %ld", (long)__LINE__);
result = expand_abbreviation_in_command(L"gc somebranch", 0, vars);
result = expand_abbreviation_in_command(L"gc somebranch", 0);
if (!result) err(L"Command not expanded on line %ld", (long)__LINE__);
result = expand_abbreviation_in_command(L"gc somebranch", const_strlen(L"gc"), vars);
result = expand_abbreviation_in_command(L"gc somebranch", const_strlen(L"gc"));
if (!result) err(L"gc not expanded");
if (result != L"git checkout somebranch")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
// Space separation.
result = expand_abbreviation_in_command(L"gx somebranch", const_strlen(L"gc"), vars);
result = expand_abbreviation_in_command(L"gx somebranch", const_strlen(L"gc"));
if (!result) err(L"gx not expanded");
if (result != L"git checkout somebranch")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
result = expand_abbreviation_in_command(L"echo hi ; gc somebranch",
const_strlen(L"echo hi ; g"), vars);
result =
expand_abbreviation_in_command(L"echo hi ; gc somebranch", const_strlen(L"echo hi ; g"));
if (!result) err(L"gc not expanded on line %ld", (long)__LINE__);
if (result != L"echo hi ; git checkout somebranch")
err(L"gc incorrectly expanded on line %ld", (long)__LINE__);
result = expand_abbreviation_in_command(L"echo (echo (echo (echo (gc ",
const_strlen(L"echo (echo (echo (echo (gc"), vars);
const_strlen(L"echo (echo (echo (echo (gc"));
if (!result) err(L"gc not expanded on line %ld", (long)__LINE__);
if (result != L"echo (echo (echo (echo (git checkout ")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
// If commands should be expanded.
result = expand_abbreviation_in_command(L"if gc", const_strlen(L"if gc"), vars);
result = expand_abbreviation_in_command(L"if gc", const_strlen(L"if gc"));
if (!result) err(L"gc not expanded on line %ld", (long)__LINE__);
if (result != L"if git checkout")
err(L"gc incorrectly expanded on line %ld to '%ls'", (long)__LINE__, result->c_str());
// Others should not be.
result = expand_abbreviation_in_command(L"of gc", const_strlen(L"of gc"), vars);
result = expand_abbreviation_in_command(L"of gc", const_strlen(L"of gc"));
if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__);
// Others should not be.
result = expand_abbreviation_in_command(L"command gc", const_strlen(L"command gc"), vars);
result = expand_abbreviation_in_command(L"command gc", const_strlen(L"command gc"));
if (result) err(L"gc incorrectly expanded on line %ld", (long)__LINE__);
vars.pop();
// yin/yang expands everywhere.
result = expand_abbreviation_in_command(L"command yin", const_strlen(L"command yin"));
if (!result) err(L"gc not expanded on line %ld", (long)__LINE__);
if (result != L"command yang") {
err(L"command yin incorrectly expanded on line %ld to '%ls'", (long)__LINE__,
result->c_str());
}
}
/// Test path functions.
@ -3502,11 +3506,9 @@ static void test_complete() {
completions.clear();
// Test abbreviations.
auto &pvars = parser_t::principal_parser().vars();
function_add(L"testabbrsonetwothreefour", func_props);
int ret = pvars.set_one(L"_fish_abbr_testabbrsonetwothreezero", ENV_LOCAL, L"expansion");
abbrs_get_map()->emplace(L"testabbrsonetwothreezero", L"expansion");
completions = complete(L"testabbrsonetwothree", {}, parser->context());
do_test(ret == 0);
do_test(completions.size() == 2);
do_test(completions.at(0).completion == L"four");
do_test((completions.at(0).flags & COMPLETE_NO_SPACE) == 0);

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_expand(cmd, abbrs_position_t::command).has_value();
// Regular commands
if (!is_valid && command_ok) is_valid = path_get_path(cmd, vars).has_value();

View File

@ -44,6 +44,7 @@
#include <set>
#include <type_traits>
#include "abbrs.h"
#include "ast.h"
#include "color.h"
#include "common.h"
@ -1347,10 +1348,8 @@ void reader_data_t::pager_selection_changed() {
}
/// Expand abbreviations at the given cursor position. Does NOT inspect 'data'.
maybe_t<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.
maybe_t<edit_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos) {
// Get the surrounding command substitution.
const wchar_t *const buff = cmdline.c_str();
const wchar_t *cmdsub_begin = nullptr, *cmdsub_end = nullptr;
parse_util_cmdsubst_extent(buff, cursor_pos, &cmdsub_begin, &cmdsub_end);
@ -1369,39 +1368,44 @@ maybe_t<edit_t> reader_expand_abbreviation_in_command(const wcstring &cmdline, s
ast_t::parse(subcmd, parse_flag_continue_after_error | parse_flag_accept_incomplete_tokens |
parse_flag_leave_unterminated);
// Look for plain statements where the cursor is at the end of the command.
const ast::string_t *matching_cmd_node = nullptr;
for (const node_t &n : ast) {
const auto *stmt = n.try_as<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;
// Find a leaf node where the cursor is at its end.
const node_t *leaf = nullptr;
traversal_t tv = ast.walk();
while (const node_t *node = tv.next()) {
if (node->category == category_t::leaf) {
auto r = node->try_source_range();
if (r && r->start <= subcmd_cursor_pos && subcmd_cursor_pos <= r->end()) {
leaf = node;
break;
}
}
}
if (!leaf) {
return none();
}
// Now if we found a command node, expand it.
// We found the leaf node before the cursor.
// Decide if this leaf is in "command position:" it is the command part of an undecorated
// statement.
bool leaf_is_command = false;
for (const node_t *cursor = leaf; cursor; cursor = cursor->parent) {
if (const auto *stmt = cursor->try_as<decorated_statement_t>()) {
if (!stmt->opt_decoration && leaf == &stmt->command) {
leaf_is_command = true;
break;
}
}
}
abbrs_position_t expand_position =
leaf_is_command ? abbrs_position_t::command : abbrs_position_t::anywhere;
// Now we can expand the abbreviation.
maybe_t<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)) {
if (auto abbreviation = abbrs_expand(leaf->source(subcmd), expand_position)) {
// There was an abbreviation! Replace the token in the full command. Maintain the
// relative position of the cursor.
source_range_t r = matching_cmd_node->source_range();
result = edit_t(subcmd_offset + r.start, r.length, std::move(*abbreviation));
}
source_range_t r = leaf->source_range();
result = edit_t(subcmd_offset + r.start, r.length, abbreviation.acquire());
}
return result;
}
@ -1417,7 +1421,7 @@ bool reader_data_t::expand_abbreviation_as_necessary(size_t cursor_backtrack) {
// Try expanding abbreviations.
size_t cursor_pos = el->position() - std::min(el->position(), cursor_backtrack);
if (auto edit = reader_expand_abbreviation_in_command(el->text(), cursor_pos, vars())) {
if (auto edit = reader_expand_abbreviation_at_cursor(el->text(), cursor_pos)) {
push_edit(el, std::move(*edit));
update_buff_pos(el);
result = true;

View File

@ -263,8 +263,7 @@ wcstring combine_command_and_autosuggestion(const wcstring &cmdline,
/// Expand abbreviations at the given cursor position. Exposed for testing purposes only.
/// \return none if no abbreviations were expanded, otherwise the new command line.
maybe_t<edit_t> reader_expand_abbreviation_in_command(const wcstring &cmdline, size_t cursor_pos,
const environment_t &vars);
maybe_t<edit_t> reader_expand_abbreviation_at_cursor(const wcstring &cmdline, size_t cursor_pos);
/// Apply a completion string. Exposed for testing only.
wcstring completion_apply_to_command_line(const wcstring &val_str, complete_flags_t flags,

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
@ -51,7 +58,7 @@ abbr __abbr4 omega
abbr | grep __abbr5
abbr -r __abbr4 __abbr5
abbr | grep __abbr5
# CHECK: abbr -a -U -- __abbr5 omega
# CHECK: abbr -a -- __abbr5 omega
abbr -e __abbr5
abbr | grep __abbr4
@ -77,7 +84,7 @@ abbr -r __abbr8 __abbr9 __abbr10
abbr | grep __abbr8
abbr | grep __abbr9
abbr | grep __abbr10
# CHECK: abbr -a -U -- __abbr8 omega
# CHECK: abbr -a -- __abbr8 omega
# Test renaming to existing abbreviation
abbr __abbr11 omega11
@ -106,3 +113,22 @@ echo $status
abbr -q banana __abbr8 foobar
echo $status
# CHECK: 0
abbr --add grape --position nowhere juice
echo $status
# CHECKERR: abbr: Invalid position 'nowhere'
# CHECKERR: Position must be one of: command, anywhere.
# CHECK: 2
abbr --add grape --position anywhere juice
echo $status
# CHECK: 0
abbr --add grape --position command juice
echo $status
# CHECK: 0
abbr --query banana --position anywhere
echo $status
# CHECKERR: abbr: --position option requires --add
# CHECK: 2

View File

@ -19,13 +19,13 @@ sendline(r"""bind '?' 'echo "<$(commandline)>"; commandline ""'; """)
expect_prompt()
# Basic test.
# Default abbreviations expand only in command position.
sendline(r"abbr alpha beta")
expect_prompt()
send(r"alpha ?")
expect_str(r"<beta >")
# Default abbreviations expand only in command position.
send(r"echo alpha ?")
expect_str(r"<echo alpha >")
@ -50,3 +50,12 @@ 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")
send(r"alpha ?")
expect_str(r"<beta2 >")
send(r"echo alpha ?")
expect_str(r"<echo beta2 >")