Bash Subcommand Aliases

A number of UNIX CLI tools use the “subcommand” pattern, meaning the command takes a subcommand argument separated by whitespace. A prominent example of this is git, which uses subcommands like fetch, push, pull, etc.:

# Git invoking the "fetch" subcommand with flags "--all"
$ git fetch --all

These types of commands have a tricky interaction with bash aliases. For instance, suppose we always want pip install to be actually be invoked as pip install --user. At first we might try to alias pip install:

# XXX: Does not work, alias name cannot contain whitespace.
alias 'pip install=pip install --user'

Whitespace is forbidden in bash alias names, so unfortunately this approach doesn’t work. A common workaround is to come up with some shorter alias name, like pi:

# This works, but might be difficult to remember.
alias 'pi=pip install --user'

This is a matter of taste, but I dislike this approach because I find these short aliases hard to remember. They’re also annoying when following instructions for things like installing a library, as you need to recognize aliased commands in the instructions and remember to invoke your alias instead.

Fortunately there’s a way to solve this issue using Bash functions and the command shell builtin. Here’s what it looks like:

function pip() {
  if [[ "$1" == "install" ]]; then
    shift 1
    command pip install --user "$@"
    command pip "$@"

The code inspects the first argument, and it it’s install it:

In the else case the code just delegates to command pip with the unaltered function arguments. Note that prefixing pip with command is necessary here: without command, this code would invoke itself recursively.

After doing this, we can see that Bash knows about two versions of pip: the function we just defined, and the /usr/bin/pip executable:

# Look up all of the known types of "pip".
$ type -a pip
pip is a function
...function definition here...
pip is /usr/bin/pip

From time to time you might want to not use the --user flag. You have two options. The first option is to use command pip install to ensure you get the pip command, not the pip function. Another alternative is to use the absolute command path, e.g. /usr/bin/pip install, although this is a bit more verbose and requires that you actually know the command’s absolute path.

This is a simple example, but the same general technique applies to more complex commands, such as those that intermix flags and subcommands, or commands like gcloud that use subcommands of subcommands.