Elegant bash conditionals

5 minute read    Published: 2021-03-02

Table of contents

The if-statement is a very basic thing, not just in bash, but in all of programming. I see them used quite a lot in shell scripts, even though in many cases they can be replaced with something much more elegant.

In this rather short article, I'll show how control operators can be used instead. Many probably know about this, but don't realize how to use them nicely. This will help you write cleaner shell scripts in the future.

Here is what a simple if-statements looks like in bash:

if [ expression ]

# or
if [ expression ]; then command; fi

Ughh. Let's improve!

Control operators

Bash provides control operators to build sequences of commands. Some of these are conditional and allow logical branching based on the success state of the last run command.

We will just focus on these two logical operators:

  • &&: the AND operator, run the following command only if previous succeeded
  • ||: the OR operator, run the following command only if previous failed

Exit codes

You might wonder how bash considers whether a command succeeded. This is where exit codes come in. When a program exits a numeric status code is returned. A value of 0 means the program succeeded, any other value means it failed.

The exit code is normally hidden. The status of the last run command is stored in the ? variable. You may inspect it by invoking:

echo $?

E.g., listing with ls normally returns 0, but this value differs if the directory doesn't exist or if an error occurred.

ls ~/            # exit code: 0
ls ~/nonexistent # exit code: 2

This will function similarly to almost any program.

Chaining commands

Let's go over some examples to show how these control operators can be used.

Imagine you want to source the ~/.profile file, but only if it is readable:

if [ -r ~/.profile ]; then
    source ~/.profile

We can simplify this using control operators:

[ -r ~/.profile ] && . ~/.profile

Only if the readability check expression is truthful/succeeds, we want to source the file, so we use the && operator.

To require invoking user to be root, we can do the following:

[ $EUID -ne 0 ] && echo You must be root && exit 1

The echo command exists with 0, so this propagates to exit if the first expression is truthful.

If we'd like to print our profile file contents with a success message, or an error message on reading failure, we can do the following:

cat ~/.profile && echo This is your profile || echo Failed to read profile

You can make these command sequences as long as you want. Useful for building install scripts that go through a series of steps. You could define bash functions for each step and orchestrate the installation like this:

init && configure && install && cleanup || echo Install failed

Or format it differently to make long sequences better readable:

init &&
  configure &&
  install &&
  cleanup ||
  echo Install failed

Nice to know

There are many more control operators, including list terminators, pipe operators, and others.

The [ (commonly used in if-statements) isn't just a shell feature. It is a binary on Unix-like systems, usually located at /usr/bin/[, so it can be used anywhere. It returns the exit code 0 if the expression was truthful.

You can chain multiple [ expr ] && [ expr ] expressions together, they are commands after all.

Bash also has [[, which is different from [.

You can wrap multiple commands with { expr; } to run it as single expression in your command chain. For example:

[ $EUID -ne 0 ] && { echo You must be root; exit 1; }

The true and false commands do nothing more than returning 0 or 1.

Bash features parameter expression. You can use ${EDITOR:=nvim} to set the EDITOR variable to nvim only if it is empty, so you won't even need conditionals.

Most other shells support similar operators. fish uses ; and and ; or, but now supports && and || as well in modern versions.

$_ (or Alt+.) is your last used argument (noted by @Diti). For example:

test -f "FILE" && source "$_" || echo "$_ does not exist" >&2

A function or script returns the exit code of the last expression. If your last expression is a command chain as described in this article, you might have an unexpected exit code. See this comments.

Closing thoughts

These command sequences with control operators are an elegant alternative for simple if-statements. I think they look much better and are more expressive looking at conditional logic.

But, don't overuse them. For bigger statements or advanced branching, you should fall back to if-statements.

I hope this article motivates you to make your shell scripts a little more elegant in the future by using these operators.


Comments on Hacker News, Reddit, Lobsters & Mastodon.