How to Exit When Errors Occur in Bash Scripts

By Evan Sangaline | September 11, 2017
Follow @sangaline

It’s a common issue that scripts written and tested on GNU/Linux don’t run correctly on macOS–or vice versa–because of differences between the GNU and BSD versions of the core utils. Error messages can get drowned in the script output, making it far from obvious that something isn’t executing correctly. There are a couple of easy fixes to avoid problems like this, but they rely on some bash features that you may not be familiar with if you don’t do a ton of scripting. I’ll summarize my two approaches here and hopefully they’re of some use to you if you’re looking for a how-to guide for this specific problem.

Exit When Any Command Fails

This can actually be done with a single line using the set builtin command with the -e option.

# exit when any command fails
set -e

Putting this at the top of a bash script will cause the script to exit if any commands return a non-zero exit code. We can get a little fancier if we use DEBUG and EXIT traps to execute custom commands before each line of the script is run and before the script exits, respectively. Adding the following lines will allow us to print out a nice error message including the previously run command and its exit code.

# exit when any command fails
set -e

# keep track of the last executed command
trap 'last_command=$current_command; current_command=$BASH_COMMAND' DEBUG
# echo an error message before exiting
trap 'echo "\"${last_command}\" command filed with exit code $?."' EXIT

For example, if we run ls --fake-option then we’ll see an informative error message as follows.

ls: unrecognized option '--fake-option'
Try 'ls --help' for more information.
"ls --fake-option" command filed with exit code 2.

Exit Only When Specific Commands Fail

The global solution is often fine, but sometimes you only care if certain commands fail. We can handle this situation by defining a function that needs to be explicitly invoked to check the status code and exit if necessary.

exit_on_error() {
    exit_code=$1
    last_command=${@:2}
    if [ $exit_code -ne 0 ]; then
        >&2 echo "\"${last_command}\" command failed with exit code ${exit_code}."
        exit $exit_code
    fi
}

# enable !! command completion
set -o history -o histexpand

That bottom line isn’t strictly necessary, but it allows us to use !! and have it expand to the last command executed. For example, we can check explicitly for an error like this

ls --fake-option
exit_on_error $? !!

will pass the exit code of the previous command as the first argument to exit_on_error() and then !! will expand to ls --fake-option as the second and third arguments. The second and third arguments–plus any further arguments if they were there–are then recombined by slicing ${@:2}.

This approach will print the same error message as the one in the previous section, but will only check the commands that are explicitly followed by exit_on_error calls.

Conclusion

Hopefully you found one of these two methods helpful! If you’re ever struggling with any devops or infrastructure issues then please reach out about our consulting services in these areas.

Suggested Articles

If you enjoyed this article, then you might also enjoy these related ones.

Installing Google Chrome On CentOS, Amazon Linux, or RHEL

By Evan Sangaline
on September 6, 2017

A guide to getting Google Chrome running on RHEL variants.

Read more

Making Chrome Headless Undetectable

By Evan Sangaline
on August 9, 2017

Using MitmProxy and injected JavaScript feature mocks to bypass Headless Chrome detection tests.

Read more

Patching a Linux Kernel Module

By Evan Sangaline
on July 27, 2017

A case-study in debugging and patching kernel-level issues on Linux.

Read more

Comments