Replacing standard UNIX commands

I've recently been replacing more and more of the standard UNIX commands that ship with most linux distros and macOS with more modern alternatives.
Some of these comands, like du
and df
, have been around for decades and are showing their age. They can be very useful, but often are not very user-friendly.
In this post, I'll explain what I mean by "replacing" these commands and show you how I do it.
Replacing a command with another one
One typical example is replacing cat
with bat
.
If you're like me, you've developed a habit of using cat
to display the contents of a file. At this point I have muscle memory for cat
and I type it without thinking, so I don't really want to retrain myself to use a different command.
So, it's obvious, right? Just alias cat
to bat
in your shell configuration file and you're done:
# `alias` is a shell built-in that creates an alias for another command.
alias cat=bat
Done!
Okay, wait a minute. What if bat
isn't installed on your system? If you run the new cat
, you'll see an error:
zsh: command not found: bat
Oops. Let's handle this.
Handling cases where the replacement is not installed (yet)
To cover scenarios where the replacement command isn't installed yet, we can use the type
command to check for its existence.
Here's how this looks:
if type bat >/dev/null 2>&1; then
alias cat=bat
fi
Assuming there is no bat
installed on the system, let's check where cat
points to:
$ which cat # `which` locates a command and returns its path.
/bin/cat
So cat
still refers to the original command. Let's install bat
and try again:
$ which cat
cat: aliased to bat
Great! Now cat
is correctly aliased to bat
.
To get a better understanding of type
, try the following commands:
$ type cat
cat is /bin/cat
$ echo $? # $? stores the exit code of the last executed command.
0
$ type i_dont_exist
i_dont_exist not found
$ echo $?
1
So if the command is not installed, type
will return an exit code of 1
, which is what we use in our conditional.
Keeping the original command
Okay, let's say that for some reason you have the corner case where you want to use the original command instead of the replacement. For instance, gping
is nice, but sometimes good ol' ping
is simpler and just enough.
You could always call the original command by using its full path (like /bin/cat
), but that's tedious. Let's create a backup alias:
if type bat >/dev/null 2>&1; then
alias cat_="$(which cat)"
alias cat="bat"
fi
With this setup:
- We check if
bat
is installed. - If it is, we create an alias
cat_
that points to the originalcat
. - We alias
cat
tobat
.
Great!
However, there is still one (albeit small) issue. What if we source this file twice? Let's say you change some other part of the configuration and source it again. The second time which cat
will return the aliased cat
(i.e., bat
) and not the original one. You'll end up overwriting cat_
to point to the replacement instead of the original command.
To prevent this, we add an extra check:
# Check that `bat` exists.
if type bat >/dev/null 2>&1; then
# If the backup alias `cat_` isn't already set,
# create it pointing to the original command.
if ! type cat_ &>/dev/null; then
alias cat_="$(which cat)"
fi
# Alias `cat` to `bat`.
alias cat="bat"
fi
Now, cat_
won't be redefined every time you source the configuration file. It's defined only once, guaranteeing that it always points to the original cat
command.
Aaand that's it! You can reuse this setup for any command you want to replace.
Extra Credit: Aliasing multiple commands
So what if you want to alias multiple commands? You can just repeat the same code for each command, but if you're like me, copy-pasting these lines will bug the hell out of you.
Let's go further and create a function that will do this for us:
set_alias_with_backup() {
# `$1` and `$2` are the first and second parameters passed to the function.
local alias_name="$1"
local new_cmd="$2"
# Check that both parameters are provided.
if [ -z "$alias_name" ] || [ -z "$new_cmd" ]; then
echo "Usage: set_alias_with_backup <alias_name> <new_command> [extra_params]"
return 1
fi
# In the next 2 lines, we add suport for extra parameters
# to be passed to the new command.
# `shift` removes the first N parameters. In this case removing `$1` and `$2`.
shift 2
# `$*` contains all positional parameters passed to the function.
# It's equivalent to `$1 $2 $3 ... $N`.
# After we shifted the first two parameters away, we're left with
# extra parameters to be passed to the new command.
local extra_params="$*"
# Check that the new command exists.
if type "$new_cmd" >/dev/null 2>&1; then
# Define a backup alias by appending an underscore
# (e.g., "cat_" for "cat")
local backup_alias="${alias_name}_"
# If the backup alias isn't already set, create it pointing
# to the original command.
if ! type "$backup_alias" >/dev/null 2>&1; then
alias "$backup_alias"="$(which "$alias_name")"
fi
# Create the alias: e.g., alias "cat" to "bat" and pass any extra params.
alias "$alias_name"="$new_cmd $extra_params"
fi
}
And now, finally, we can happily do:
set_alias_with_backup cat bat
set_alias_with_backup df duf
set_alias_with_backup ping gping
You can also pass extra parameters to the new command. For example, if you always want to pass --style plain
to bat
, you can do:
set_alias_with_backup cat bat --style plain
Done! This time for real 😁. As usual, thanks for reading! If you found this post useful, please click the heart below to let me know.