Call "fish_command_not_found" if a command wasn't found

Previously, when a command wasn't found, fish would emit the
"fish_command_not_found" *event*.

This was annoying as it was hard to override (the code ended up
checking for a function called `__fish_command_not_found_handler`
anyway!), the setup was ugly,
and it's useless - there is no use case for multiple command-not-found handlers.

Instead, let's just call a function `fish_command_not_found` if it
exists, or print the default message otherwise.

The event is completely removed, but because a missing event is not an error
(MEISNAE in C++-speak) this isn't an issue.

Note that, for backwards-compatibility, we still keep the default
handler function around even tho the new one is hard-coded in C++.

Also, if we detect a previous handler, the new handler just calls it.

This way, the backwards-compatible way to install a custom handler is:

```fish
function __fish_command_not_found_handler --on-event fish_command_not_found
    # do a little dance, make a little love, get down tonight
end
```

and the new hotness is

```fish
function fish_command_not_found
    # do the thing
end
```

Fixes #7293.
This commit is contained in:
Fabian Homborg 2020-08-29 21:54:13 +02:00
parent d1dab22691
commit 340de73172
7 changed files with 199 additions and 65 deletions

View file

@ -0,0 +1,76 @@
.. _cmd-fish_cmd_not_found:
fish_command_not_found - what to do when a command wasn't found
===============================================================
Synopsis
--------
::
function fish_command_not_found
...
end
Description
-----------
When fish tries to execute a command and can't find it, it invokes this function.
It can print a message to tell you about it, and it often also checks for a missing package that would include the command.
Fish ships multiple handlers for various operating systems and chooses from them when this function is loaded,
or you can define your own.
It receives the full commandline as one argument per token, so $argv[1] contains the missing command.
When you leave ``fish_command_not_found`` undefined (e.g. by adding an empty function file) or explicitly call ``__fish_default_command_not_found_handler``, fish will just print a simple error.
Example
-------
A simple handler:
::
function fish_command_not_found
echo Did not find command $argv[1]
end
> flounder
Did not find command flounder
Or the handler for OpenSUSE's command-not-found::
function fish_command_not_found
/usr/bin/command-not-found $argv[1]
end
Or the simple default handler::
function fish_command_not_found
__fish_default_command_not_found_handler $argv
end
Backwards compatibility
-----------------------
This command was introduced in fish 3.2.0. Previous versions of fish used the "fish_command_not_found" :ref:`event <event>` instead.
To define a handler that works in older versions of fish as well, define it the old way::
function __fish_command_not_found_handler --on-event fish_command_not_found
echo COMMAND WAS NOT FOUND MY FRIEND $argv[1]
end
in which case fish will define a ``fish_command_not_found`` that calls it,
or define a wrapper::
function fish_command_not_found
echo "G'day mate, could not find your command: $argv"
end
function __fish_command_not_found_handler --on-event fish_command_not_found
fish_command_not_found $argv
end

View file

@ -50,8 +50,6 @@ By using one of the event handler switches, a function can be made to run automa
- ``fish_prompt``, which is emitted whenever a new fish prompt is about to be displayed.
- ``fish_command_not_found``, which is emitted whenever a command lookup failed.
- ``fish_preexec``, which is emitted right before executing an interactive command. The commandline is passed as the first parameter. Not emitted if command is empty.
- ``fish_posterror``, which is emitted right after executing a command with syntax errors. The commandline is passed as the first parameter.

View file

@ -26,9 +26,9 @@ function __fish_default_command_not_found_handler
end
if not status --is-interactive
# Hook up the default as the principal command_not_found handler
# in case we are not interactive
function __fish_command_not_found_handler --on-event fish_command_not_found
# Hook up the default as the command_not_found handler
# if we are not interactive to avoid custom handlers.
function fish_command_not_found --on-event fish_command_not_found
__fish_default_command_not_found_handler $argv
end
end

View file

@ -258,63 +258,9 @@ function __fish_config_interactive -d "Initializations that should be performed
__update_cwd_osc # Run once because we might have already inherited a PWD from an old tab
end
### Command-not-found handlers
# This can be overridden by defining a new __fish_command_not_found_handler function
if not type -q __fish_command_not_found_handler
# Read the OS/Distro from /etc/os-release.
# This has a "ID=" line that defines the exact distribution,
# and an "ID_LIKE=" line that defines what it is derived from or otherwise like.
# For our purposes, we use both.
set -l os
if test -r /etc/os-release
set os (string match -r '^ID(?:_LIKE)?\s*=.*' < /etc/os-release | \
string replace -r '^ID(?:_LIKE)?\s*=(.*)' '$1' | string trim -c '\'"' | string split " ")
end
# First check if we are on OpenSUSE since SUSE's handler has no options
# but the same name and path as Ubuntu's.
if contains -- suse $os || contains -- sles $os && type -q command-not-found
function __fish_command_not_found_handler --on-event fish_command_not_found
/usr/bin/command-not-found $argv[1]
end
# Check for Fedora's handler
else if test -f /usr/libexec/pk-command-not-found
function __fish_command_not_found_handler --on-event fish_command_not_found
/usr/libexec/pk-command-not-found $argv[1]
end
# Check in /usr/lib, this is where modern Ubuntus place this command
else if test -f /usr/lib/command-not-found
function __fish_command_not_found_handler --on-event fish_command_not_found
/usr/lib/command-not-found -- $argv[1]
end
# Check for NixOS handler
else if test -f /run/current-system/sw/bin/command-not-found
function __fish_command_not_found_handler --on-event fish_command_not_found
/run/current-system/sw/bin/command-not-found $argv
end
# Ubuntu Feisty places this command in the regular path instead
else if type -q command-not-found
function __fish_command_not_found_handler --on-event fish_command_not_found
command-not-found -- $argv[1]
end
# pkgfile is an optional, but official, package on Arch Linux
# it ships with example handlers for bash and zsh, so we'll follow that format
else if type -p -q pkgfile
function __fish_command_not_found_handler --on-event fish_command_not_found
set -l __packages (pkgfile --binaries --verbose -- $argv[1] 2>/dev/null)
if test $status -eq 0
printf "%s may be found in the following packages:\n" "$argv[1]"
printf " %s\n" $__packages
else
__fish_default_command_not_found_handler $argv[1]
end
end
# Use standard fish command not found handler otherwise
else
function __fish_command_not_found_handler --on-event fish_command_not_found
__fish_default_command_not_found_handler $argv[1]
end
end
# For backwards-compatibility - the event doesn't exist anymore so it's harmless.
function __fish_command_not_found_handler --on-event fish_command_not_found
fish_command_not_found $argv
end
# Bump this whenever some code below needs to run once when upgrading to a new version.

View file

@ -0,0 +1,61 @@
### Command-not-found handlers
# This can be overridden by defining a new fish_command_not_found function
# Read the OS/Distro from /etc/os-release.
# This has a "ID=" line that defines the exact distribution,
# and an "ID_LIKE=" line that defines what it is derived from or otherwise like.
# For our purposes, we use both.
set -l os
if test -r /etc/os-release
set os (string match -r '^ID(?:_LIKE)?\s*=.*' < /etc/os-release | \
string replace -r '^ID(?:_LIKE)?\s*=(.*)' '$1' | string trim -c '\'"' | string split " ")
end
# If an old handler already exists, defer to that.
if functions -q __fish_command_not_found_handler
function fish_command_not_found
# The fish_command_not_found event was removed in fish 3.2.0,
# and future versions of fish will just call a function called "fish_command_not_found".
# You have defined a custom handler, we suggest renaming it to "fish_command_not_found".
__fish_command_not_found_handler $argv
end
# First check if we are on OpenSUSE since SUSE's handler has no options
# but the same name and path as Ubuntu's.
else if contains -- suse $os || contains -- sles $os && type -q command-not-found
function fish_command_not_found
/usr/bin/command-not-found $argv[1]
end
# Check for Fedora's handler
else if test -f /usr/libexec/pk-command-not-found
function fish_command_not_found
/usr/libexec/pk-command-not-found $argv[1]
end
# Check in /usr/lib, where Ubuntu places this command
else if test -f /usr/lib/command-not-found
function fish_command_not_found
/usr/lib/command-not-found -- $argv[1]
end
# Check for NixOS handler
else if test -f /run/current-system/sw/bin/command-not-found
function fish_command_not_found
/run/current-system/sw/bin/command-not-found $argv
end
# Ubuntu Feisty places this command in the regular path instead
else if type -q command-not-found
function fish_command_not_found
command-not-found -- $argv[1]
end
# pkgfile is an optional, but official, package on Arch Linux
# it ships with example handlers for bash and zsh, so we'll follow that format
else if type -p -q pkgfile
function fish_command_not_found
set -l __packages (pkgfile --binaries --verbose -- $argv[1] 2>/dev/null)
if test $status -eq 0
printf "%s may be found in the following packages:\n" "$argv[1]"
printf " %s\n" $__packages
else
__fish_default_command_not_found_handler $argv[1]
end
end
end
# Use standard fish command not found handler otherwise

View file

@ -749,10 +749,39 @@ end_execution_reason_t parse_execution_context_t::handle_command_not_found(
event_args.insert(event_args.begin(), cmd_str);
}
event_fire_generic(*parser, L"fish_command_not_found", &event_args);
wcstring buffer;
wcstring error;
// Here we want to report an error (so it shows a backtrace), but with no text.
return this->report_error(STATUS_CMD_UNKNOWN, statement, L"");
// Redirect to stderr
auto io = io_chain_t{};
io.append_from_specs({redirection_spec_t{STDOUT_FILENO, redirection_mode_t::fd, L"2"}}, L"");
if (function_exists(L"fish_command_not_found", *parser)) {
buffer = L"fish_command_not_found";
for (const wcstring &arg : event_args) {
buffer.push_back(L' ');
buffer.append(escape_string(arg, ESCAPE_ALL));
}
auto prev_statuses = parser->get_last_statuses();
event_t event(event_type_t::generic);
event.desc.str_param1 = L"fish_command_not_found";
block_t *b = parser->push_block(block_t::event_block(event));
parser->eval(buffer, io);
parser->pop_block(b);
parser->set_last_statuses(std::move(prev_statuses));
} else {
// If we have no handler, just print it as a normal error.
error = _(L"Unknown command:");
if (!event_args.empty()) {
error.push_back(L' ');
error.append(escape_string(event_args[0], ESCAPE_ALL));
}
}
// Here we want to report an error (so it shows a backtrace).
// If the handler printed text, that's already shown, so error will be empty.
return this->report_error(STATUS_CMD_UNKNOWN, statement, error.c_str());
}
}

View file

@ -0,0 +1,24 @@
# RUN: %fish -C 'set -g fish %fish' %s
set -g PATH
$fish -c "nonexistent-command-1234 banana rama"
#CHECKERR: fish: Unknown command: nonexistent-command-1234
#CHECKERR: fish:
#CHECKERR: nonexistent-command-1234 banana rama
#CHECKERR: ^
$fish -C 'function fish_command_not_found; echo cmd-not-found; end' -ic "nonexistent-command-1234 1 2 3 4"
#CHECKERR: cmd-not-found
#CHECKERR: fish:
#CHECKERR: nonexistent-command-1234 1 2 3 4
#CHECKERR: ^
$fish -C 'function fish_command_not_found; echo command-not-found $argv; end' -c "nonexistent-command-abcd foo bar baz"
#CHECKERR: command-not-found nonexistent-command-abcd foo bar baz
#CHECKERR: fish:
#CHECKERR: nonexistent-command-abcd foo bar baz
#CHECKERR: ^
$fish -C 'functions --erase fish_command_not_found' -c 'nonexistent-command apple friday'
#CHECKERR: fish: Unknown command: nonexistent-command
#CHECKERR: nonexistent-command apple friday
#CHECKERR: ^
exit 0