In Bash scripting (and shell scripting in general), we often want to check the
exit value of a command to decide an action to take after it completes, likely
for the purpose of error handling. For example, to determine whether a
particular regular expression regex
was present somewhere in a file
options
, we might apply grep(1)
with its POSIX -q
option to suppress
output and just use the exit value:
grep -q regex options
An approach sometimes taken is then to test the exit value with the $?
parameter, using if
to check if it’s non-zero, which is not very elegant and
a bit hard to read:
# Bad practice
grep -q regex options
if (($? > 0)); then
printf '%s\n' 'myscript: Pattern not found!' >&2
exit 1
fi
Because the if
construct by design tests the exit value of commands,
it’s better to test the command directly, making the expansion of $?
unnecessary:
# Better
if grep -q regex options; then
# Do nothing
:
else
printf '%s\n' 'myscript: Pattern not found!\n' >&2
exit 1
fi
We can precede the command to be tested with !
to negate the test as well,
to prevent us having to use else
as well:
# Best
if ! grep -q regex options; then
printf '%s\n' 'myscript: Pattern not found!' >&2
exit 1
fi
An alternative syntax is to use &&
and ||
to perform if
and else
tests
with grouped commands between braces, but these tend to be harder to read:
# Alternative
grep -q regex options || {
printf '%s\n' 'myscript: Pattern not found!' >&2
exit 1
}
With this syntax, the two commands in the block are only executed if the
grep(1)
call exits with a non-zero status. We can apply &&
instead to
execute commands if it does exit with zero.
That syntax can be convenient for quickly short-circuiting failures in
scripts, for example due to nonexistent commands, particularly if the command
being tested already outputs its own error message. This therefore cuts the
script off if the given command fails, likely due to ffmpeg(1)
being
unavailable on the system:
hash ffmpeg || exit 1
Note that the braces for a grouped command are not needed here, as there’s
only one command to be run in case of failure, the exit
call.
Calls to cd
are another good use case here, as running a script in the wrong
directory if a call to cd
fails could have really nasty effects:
cd wherever || exit 1
In general, you’ll probably only want to test $?
when you have specific
non-zero error conditions to catch. For example, if we were using the
--max-delete
option for rsync(1)
, we could check a call’s return value
to see whether rsync(1)
hit the threshold for deleted file count and write
a message to a logfile appropriately:
rsync --archive --delete --max-delete=5 source destination
if (($? == 25)); then
printf '%s\n' 'Deletion limit was reached' >"$logfile"
fi
It may be tempting to use the errexit
feature in the hopes of stopping a
script as soon as it encounters any error, but there are some problems with
its usage that make it a bit error-prone. It’s generally more
straightforward to simply write your own error handling using the methods
above.
For a really thorough breakdown of dealing with conditionals in Bash, take a look at the relevant chapter of the Bash Guide.