2019-06-11

Defining Shell Aliases for Subcommands

If you spend any time using the command line, you're probably familiar with aliases. The idea is to substitute a short and easy name for frequently-typed or long commands. For example, the following common alias lets you shorten ls -la to just ll:

alias ll='ls -la'

Some commands, like git, provide hierarchical subcommands. To view the status of a git repository, you run git status, where git is the command your shell executes and status is the subcommand interpreted by Git. Git also provides an alias feature very much like shell aliases. The command git config --global alias.st status defines an alias st for the status subcommand, so that you only need to type git st. What if you could define new subcommands like that anywhere you wanted?

Wrapping Commands with Functions

Another popular command line tool that works using subcommands is apt. Unlike git, though, it does not provide its own facility for subcommand aliases. For example, if you wanted to add a log subcommand to view recent package installations, removals, and upgrades, you're out of luck. The closest you might get is defining an alias apt-log:

alias apt-log='less /var/log/apt/history.log'

This works fine, but it's not really the same thing. Is there any way to actually add it as a subcommand alias, or "subalias," without editing the source code? One possibility is writing a wrapper function:

apt() {
    if test "$1" = "log"; then
        less /var/log/apt/history.log
    else
        command apt "$@"
    fi
}

Success! This shell function opens the apt log file if its first argument is log; otherwise it passes its arguments to the real apt command. However, this technique quickly gets tedious if you want to define many such subaliases. It would be much nicer to generate wrapper functions like this automatically.

The Subalias Function

Using some shell magic, we can write a function to do this sort of wrapping for us.

subalias() {
    local cmd body name

    name="${1%%=*}"
    cmd="${name%_*}"
    body="${1#*=}"

    eval "$cmd"'() {
        local cmd='"$cmd"' subcmd="$1"
        if type "${cmd}_${subcmd}" >/dev/null 2>&1; then
            shift
            "${cmd}_${subcmd}" "$@"
        elif type "${cmd}_${subcmd}_subalias" >/dev/null 2>&1; then
            shift
            "${cmd}_${subcmd}_subalias" "$@"
        elif type "${cmd}_subalias" >/dev/null 2>&1; then
            "${cmd}_subalias" "$@"
        else
            command "$cmd" "$@"
        fi
    }'

    if test "$body" != "$1"; then
        eval "${name}_subalias"'() { '"$body"'; }'
    fi
}

Now you can just write subalias apt_log='less /var/log/apt/history.log'. Two functions will be generated for you: apt_log_subalias to open the log file, and the apt wrapper function to dynamically dispatch based on the arguments passed to it.

What else can we do with this? Try implicitly invoking sudo only when it's required:

for action in install reinstall remove purge autoremove update upgrade \
    full-upgrade edit-sources; do
    subalias apt_${action}='sudo apt '"${action}"' "$@"'
done

Now running apt install hello will automatically invoke sudo, but apt show hello will not. There are a couple interesting things to note. One is that, unlike normal aliases, we had to explicitly append arguments to the command by ending it with "$@". Two is that we were able to run subalias multiple times to define multiple subaliases for apt. (The wrapper functions didn't clobber each other and erase the previous subalias.)

OK, what about sub-subaliases? We can do that too!

subalias apt_show='command apt show "$@"'
subalias apt_show_vim='apt show emacs'

Since everybody knows that Emacs is better than vim, now whenever you type apt show vim you will be directed to Emacs instead. Typing any other package name will dispatch to the real apt show like normal.

Conclusion

This kind of code is a lot of fun to play with, but it's also very brittle! You should definitely avoid using these techniques in production, or to prank unsuspecting coworkers ;-). Defining a normal alias is always the simpler solution, but subaliases can still be handy for personal interactive usage. The Bourne shell is a truly malleable language that can be tailored to your preferences, if you can put up with its idiosyncrasies.