From 340de731726701d992ee4ec915dc41bdaf9d728c Mon Sep 17 00:00:00 2001 From: Fabian Homborg Date: Sat, 29 Aug 2020 21:54:13 +0200 Subject: [PATCH] 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. --- doc_src/cmds/fish_command_not_found.rst | 76 +++++++++++++++++++ doc_src/cmds/function.rst | 2 - share/config.fish | 6 +- .../functions/__fish_config_interactive.fish | 60 +-------------- share/functions/fish_command_not_found.fish | 61 +++++++++++++++ src/parse_execution.cpp | 35 ++++++++- tests/checks/command-not-found.fish | 24 ++++++ 7 files changed, 199 insertions(+), 65 deletions(-) create mode 100644 doc_src/cmds/fish_command_not_found.rst create mode 100644 share/functions/fish_command_not_found.fish create mode 100644 tests/checks/command-not-found.fish diff --git a/doc_src/cmds/fish_command_not_found.rst b/doc_src/cmds/fish_command_not_found.rst new file mode 100644 index 000000000..cb1d90dfb --- /dev/null +++ b/doc_src/cmds/fish_command_not_found.rst @@ -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 ` 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 diff --git a/doc_src/cmds/function.rst b/doc_src/cmds/function.rst index e89ff4257..99a093a28 100644 --- a/doc_src/cmds/function.rst +++ b/doc_src/cmds/function.rst @@ -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. diff --git a/share/config.fish b/share/config.fish index ee9990ffd..050df3102 100644 --- a/share/config.fish +++ b/share/config.fish @@ -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 diff --git a/share/functions/__fish_config_interactive.fish b/share/functions/__fish_config_interactive.fish index e01aae8f0..137fa9369 100644 --- a/share/functions/__fish_config_interactive.fish +++ b/share/functions/__fish_config_interactive.fish @@ -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. diff --git a/share/functions/fish_command_not_found.fish b/share/functions/fish_command_not_found.fish new file mode 100644 index 000000000..8e878695c --- /dev/null +++ b/share/functions/fish_command_not_found.fish @@ -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 diff --git a/src/parse_execution.cpp b/src/parse_execution.cpp index f9dcf2ec8..3425b4ee3 100644 --- a/src/parse_execution.cpp +++ b/src/parse_execution.cpp @@ -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()); } } diff --git a/tests/checks/command-not-found.fish b/tests/checks/command-not-found.fish new file mode 100644 index 000000000..c4c409ac8 --- /dev/null +++ b/tests/checks/command-not-found.fish @@ -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