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;

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; }

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; }

I often define die function so the code becomes:

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

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"

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.


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

echo ${target:=${PWD}}

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"}

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}

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

or, when name is set and not null:

find . -name _config.yml -o -name $name

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

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


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() {
  test $rv -ne 0 && rm /tmp/mycache/* -rf

trap cleanup EXIT

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}}

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.

Comments on

  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.