Bash Error Handling with Trap
- shell ops
Yesterday I ended up writing an impromptu guide to Bash error handling on a PR, so I decided to polish it a bit and turn it into an actual post.
The goal: whenever our release script encounters an error, send a notification to a Slack channel. We won’t look into the latter part in this post, as it was handled by some Ruby code using the slack-notifier gem. Instead we’ll look into what was necessary to make this work in Bash.
Exiting On Errors
The first step is to add the -e
(or -o errexit
) option to the script, which will exit at the first error. This is contrary to Bash’s default behavior of continuing with the next command:
set -e
There are some other options one should consider adding at this point:
-E
(-o errtrace
): Ensures thatERR
traps (see below) get inherited by functions, command substitutions, and subshell environments.-u
(-o nounset
): Treats unset variables as errors.-o pipefail
: normally Bash pipelines only return the exit code of the last command. This option will propagate intermediate errors.
set -Eeuo pipefail
While debugging it may also be useful to add x
(-o xtrace
) to the options, which will print all expanded commands to stdout
before executing them:
bash -x script.sh
Trapping Errors
Traps in Bash are used for executing a command or series of commands upon catching a signal. For example if we want to print a message when the user hits Ctrl-C
, we can do it in the following way:
#/bin/bash
trap "{ echo 'Bye!' ; exit 0; }" SIGINT
while true ; do sleep 1 ; done
Let’s see this in action:
$ bash -x infinite.sh
+ trap '{ echo '\''Bye!'\'' ; exit 0; }' SIGINT
+ true
+ sleep 1
+ true
+ sleep 1
^C++ echo 'Bye!'
Bye!
++ exit 0
$ echo $?
0
This script is running an infinite loop, but when the user sends a SIGINT
signal it will print a message and exit with a successful error code.
This same mechanism can also be used to perform cleanup tasks when a script terminates:
#/bin/bash
trap "rm test" EXIT
echo "Hello, reader!\n" > test
cat test
This will create a file named test
, output its content and then remove it on exit. Let’s see this in action and verify that the file has indeed been removed:
$ bash -x cleanup.sh
+ trap 'rm test' EXIT
+ echo 'Hello, reader!\n'
+ cat test
Hello, reader!\n
+ rm test
$ file test
test: cannot open `test' (No such file or directory)
There’s also a special signal named ERR
, which will be triggered every time a command exits with a non-zero status. This is exactly what we need to make our Slack notifier work:
#!/bin/bash
function notify {
echo "Something went wrong!"
}
trap notify ERR
nonexisting_command
As you can see trap
also supports calling functions, which we use here to invoke notify
whenever an error occurs:
$ bash -x err.sh
+ trap notify ERR
+ nonexisting_command
err.sh: line 8: nonexisting_command: command not found
++ notify
++ echo 'Something went wrong!'
Something went wrong!
Generating the Message
For the purpose of our Slack notifier, we didn’t just want to know that something went wrong, bug also what exactly the error was. Once again Bash had us covered by providing the caller
builtin, which will output information about execution frames.
Let’s update the last script to make use of this functionality:
#!/bin/bash
function notify {
echo "Something went wrong!"
echo "$(caller): ${BASH_COMMAND}"
}
trap notify ERR
nonexisting_command
Running this will generate the following error message:
$ bash err.sh
err.sh: line 9: nonexisting_command: command not found
Something went wrong!
9 err.sh: nonexisting_command
Here “9” is the line number where the error occurred, “err.sh” is the script that triggered it and “nonexisting_command” is the command that caused the error (provided by the $BASH_COMMAND
variable). Alternatively we could also have used the $LINENO
variable:
- echo "$(caller): ${BASH_COMMAND}"
+ echo "Error on line ${LINENO}: ${BASH_COMMAND}"
This generates the following output: “Error on line 4: nonexisting_command”.
Using all of the described features, we end up with the following script:
set -Eeuo pipefail
notify () {
FAILED_COMMAND="$(caller): ${BASH_COMMAND}" \
# perform notification here
}
trap notify ERR
# actual release commands