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.