Replacing standard UNIX commands

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:

  1. We check if bat is installed.
  2. If it is, we create an alias cat_ that points to the original cat.
  3. We alias cat to bat.

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.