DEV Community

panmanio
panmanio

Posted on • Edited on • Originally published at burnicki.pl

Bash anti-if

Bash if-statements often look bloated to me, especially when the script aims to accomplish quite simple goal but needs to perform plenty of sanity checks before proceeding with the actual task. Let's take advantage of shell builtin capabilities to write simpler code.

The test command and logical operators

Instead of writing:

if [ $# -eq 0 ]; then
  echo Params missing;
  exit 1;
fi
Enter fullscreen mode Exit fullscreen mode

we can use the test (or [) builtin command 1:

test $# -eq 0 && { echo >&2 Params missing; exit 1; }
# or:
[ $# -eq 0 ] && { echo >&2 Params missing; exit 1; }
Enter fullscreen mode Exit fullscreen mode

In the latter, ] is simply the last argument to [. Putting ] is required. I prefer test over [ as it looks less confusing for someone who sees it for first time. Also, with test we can take advantage of _ variable, which is the last argument of previous command:

test -f ~/.bash_aliases && source "$_" || { echo >&2 "$_ is missing"; exit 1; }
Enter fullscreen mode Exit fullscreen mode

I often define die function so the code becomes:

die() {
  echo >&2 "$@"
  exit 1
}
test -f ~/.bash_aliases && source "$_" || die "$_ is missing"
Enter fullscreen mode Exit fullscreen mode

Remember to group the commands in curly brackets when you expect they can fail or when not sure; or go with a separate function.

The commands can be chained like this:

test -x ./configure &&
  ./configure &&
  make &&
  make install ||
  die "Failed to build & install"
Enter fullscreen mode Exit fullscreen mode

Many people find such concise constructs undesired as these statements may make one stop and think on the purpose of what is happening there, so they prefer to go with standard if-statements as being more expressive. This argument is perfectly fine and should be definitely considered when working in a team. But it's good to know what are the possibilities.

After all, I am not trying to fight the if-statements (pardon the click-bait 😅). Personally I use these "tricks" only for simple cases. Let's have a look at other shell capabilities we can use no matter if we go with if-statement or test.

Use default values

Let's take a look at parameter expansion and default variable values. That can also help to reduce the number of if-statements.

target=${1:-${PWD}}
Enter fullscreen mode Exit fullscreen mode

If there is an argument provided, use that as the target. Otherwise choose current directory.

echo ${target:=${PWD}}
Enter fullscreen mode Exit fullscreen mode

Prints target if it is set, otherwise prints ${PWD} and assigns that value to target.

echo ${target:?"Okay, Houston, we've had a problem here"}
Enter fullscreen mode Exit fullscreen mode

When target is not set (or null), prints an error with the message provided and quits (when used non-interactively, like in a script).

find . -name _config.yml ${name:+-o -name $name}
Enter fullscreen mode Exit fullscreen mode

Use alternate value. When name is not set or null, leave it as it is. Otherwise, replace with the alternate value. Results with:

find . -name _config.yml
Enter fullscreen mode Exit fullscreen mode

or, when name is set and not null:

find . -name _config.yml -o -name $name
Enter fullscreen mode Exit fullscreen mode

In case the colon is omitted in the examples above the null check is ignored (only unset is verified).

Reject unset variables by default

You can go one step further and reject any unset variable expansion by default. Just do this:

set -u
Enter fullscreen mode Exit fullscreen mode

Now when you try to expand an unset variable, the shell will print an error and quit.

Traps

Traps let you register some action to be executed when the script receives a signal. That can be a standard signal (see kill -l or man 7 signal), but it can also be EXIT 2:

cleanup() {
  rv=$?
  test $rv -ne 0 && rm /tmp/mycache/* -rf
}
trap cleanup EXIT
Enter fullscreen mode Exit fullscreen mode

The cleanup function will be executed when the script exits (even when the script ends it's life naturally, without explicit exit call). Once the trap is registered, you don't have to worry about the cleanup. Traps not only let you write less ifs, they are much more powerful tool (consider trap cleanup SIGABRT SIGKILL).

The null command

There is a special builtin : command that is always successful (just like true) and does nothing but expands its parameters (and performs redirections if specified). Example:

: ${target=${PWD}}
Enter fullscreen mode Exit fullscreen mode

Here I've reused one of previous examples. The command above sets target to current directory when target is not set (but leaves null untouched as I have used = instead of := this time).

Final words

Bash (and other shells) is really powerful and complex language. I keep learning it and there is always much more to learn, which I find fascinating. If you have enjoyed this post I advice you to read more on the topic. I am not the first one to explore these areas, please check out Elegant bash conditionals by Tim Visée and Anybody can write good bash (with a little effort) by William Woodruff, among others.


  1. Most often there is also a usual test/[ command on the system, compare the output of which test with type test. ↩

  2. The bash manual specifies more signals supported by trap, like RETURN or DEBUG. ↩

Top comments (0)