Bash: Handling Command Not Found

""

After a recent O/S version upgrade (to openSUSE 11.2) I noticed that bash started being a bit more intelligent when I did something stupid: it started giving me a useful error message when I typed the name of a command that wasn't in my PATH but that was in an "sbin" directory. My reaction at the time was "huh, that's nice", but today I decided I needed a bit more information.

As an example of this behavior, if I type ifconfig while not logged in as root I get the get the following:

$ ifconfig
Absolute path to 'ifconfig' is '/sbin/ifconfig', so running it may require superuser privileges (eg. root).

Which is certainly more useful than a "command not found" message.

Turns out that this capability is a standard feature of bash. From bash's man page:

... A full search of the directories in PATH is performed only if the command is not found in the hash table. If the search is unsuccessful, the shell searches for a defined shell function named command_not_found_handle. If that function exists, it is invoked with the original command and the original command's arguments as its arguments, and the function's exit status becomes the exit status of the shell. If that function is not defined, the shell prints an error message and returns an exit status of 127.

I'm not sure if that's a new feature of bash or if it's just something that's recently implemented in openSUSE.

A quick grep in /etc discovered where it was happening. The function itself is in /etc/bash_command_not_found and that function gets included (if it exists) in your bash session via /etc/bash.bashrc.

The function itself is not that complex but there are a couple of useful tidbits that I wanted to point out as a sidebar. The following code determines if the invoking shell was executed from Midnight Commander or is taking input from a pipe:

    # do not run when inside Midnight Commander or within a Pipe
    if test -n "$MC_SID" -o ! -t 1 ; then
        echo $"$1: command not found"
        return 127
    fi

And the following determines if the invoking shell is a sub-shell:

    # do not run when within a subshell
    read pid cmd state ppid pgrp session tty_nr tpgid rest  < /proc/self/stat
    if test $$ -eq $tpgid ; then
        echo "$1: command not found"
        return 127
    fi

End of sidebar.

At the end of the function there's some code that uses /usr/bin/command-not-found (a python script) to lookup commands in installable packages (via zypper), but you need to set an environment variable (COMMAND_NOT_FOUND_AUTO) to activate it, and of course you need to have python installed. To test this try the following:

$ export COMMAND_NOT_FOUND_AUTO=1
$ pascal
pascal: command not found
$  gcj
The program 'gcj' can be found in the following package:
  * gcc-java [ path: /usr/bin/gcj, repository: zypp (repo-oss) ]

Try installing with:
    sudo zypper install gcc-java

Since this command-not-found functionality is handled by a bash function, you can of course replace the system installed function (if one exists on your system) with one of your own design. All you need is to include it in your .bashrc script. The entire version of openSUSE's script follows:

command_not_found_handle() {

    export TEXTDOMAIN=command-not-found

    local cmd state rest
    local -i pid ppid pgrp session tty_nr tpgid

    # do not run when inside Midnight Commander or within a Pipe
    if test -n "$MC_SID" -o ! -t 1 ; then
        echo $"$1: command not found"
        return 127
    fi

    # do not run when within a subshell
    read pid cmd state ppid pgrp session tty_nr tpgid rest  < /proc/self/stat
    if test $$ -eq $tpgid ; then
        echo "$1: command not found"
        return 127
    fi

    # test for /usr/sbin and /sbin
    if test -x "/usr/sbin/$1" -o -x "/sbin/$1" ; then
        if test -x "/usr/sbin/$1" ; then prefix='/usr' ; else prefix='' ; fi
        echo $"Absolute path to '$1' is '$prefix/sbin/$1', so running it may require superuser privileges (eg. root)."
        return 127
    fi

    if test -n "$COMMAND_NOT_FOUND_AUTO" ; then
        # call command-not-found directly
        test -x /usr/bin/python && test -x /usr/bin/command-not-found && /usr/bin/python /usr/bin/command-not-found "$1" zypp
    else
        # print only info about command-not-found
        echo -e $"If '$1' is not a typo you can use command-not-found to lookup the package that contains it, like this:\n    cnf $1"
    fi

    return 127
}
Load Disqus comments