DEV Community

loading...

Bash Error Handling with Trap

Michael Kohl
I dev @ DEV. Your friendly neighborhood anarcho-cynicalist. ¯\_(ツ)_/¯ and (╯°□°)╯︵ ┻━┻) are my two natural states.Tag mod for #ruby, #fsharp, #ocaml
Originally published at citizen428.net on ・3 min read

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
Enter fullscreen mode Exit fullscreen mode

There are some other options one should consider adding at this point:

  • -E (-o errtrace): Ensures that ERR 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Discussion (7)

Collapse
rhymes profile image
rhymes • Edited

Woah, what a ride! I tried to change the rm test into self deleting the script at the end and it worked. Now I know how hackers do it :D

Thanks Michael!

Collapse
darksmile92 profile image
Robin Kretzschmar

Thanks a lot for this detailed post!

I often write scripts to be able to automate as much as I can but they are not very robust because I didn't know about this in the past and honestly need to take more care about error handling and exception behavior 😏

Collapse
citizen428 profile image
Michael Kohl Author

Hm, this makes me wonder if I should do a follow-up post on other techniques for more robust shell scripting 🤔

Collapse
darksmile92 profile image
Robin Kretzschmar

I'd love to read it!

Collapse
nickytonline profile image
Nick Taylor (he/him)

I’m no bash expert, so this was super informative. I also learnt about some more about our monitoring at DEV. Thanks for sharing Michael!

Collapse
fennecdjay profile image
Jérémie Astor

Very good 😄

Collapse
andruszd profile image
PinguAttacks

Nice :)