Bash – Error cleanup for shell scripts

bashdasherror handlingshell-script

What's the best way to include error cleanup logic for shell scripts?

Specifically, I have a script that does something like this:

mount a x
mount b y
setup_thing
mount c z
do_something
umount z
cleanup_thing
umount y
umount x

Any of the mounts, plus do_something itself, might fail. If, say, the mount c z fails, I would like the script to unmount the mounts that did succeed before exiting.

I don't want to have to repeat the cleanup code multiple times, and I don't want to wrap everything into an if-nest (because then you have to re-indent everything if you add an extra mount).

Is there a way to create a finally-stack or something, so the above could be written as:

set -e
mount a x && finally umount x
mount b y && finally umount y
setup_thing && finally cleanup_thing
mount c z && finally umount z
do_something

The idea being that once the "finally" command is registered it will execute (in reverse order) that command on exit, pass or fail — but the things that didn't get set up successfully don't get cleaned up (because the cleanup might not be safe to execute if the setup failed).

Or to put it another way, if everything succeeds then it will execute umount z, cleanup_thing, umount y, umount x in that order — same if only do_something fails. But if the mount b y fails then it will only execute umount x.

(And regardless it needs to exit with 0 or not-0 appropriately, although I don't need to preserve the exact exit code on failure.)

I know there's a trap builtin that lets you run a command on exit, but it only supports one command and replaces it each time. Is there a way to extend this into a stack like above? Or some other clean way to do it?

In error-handling patterns from Ye Olde C code you'd probably achieve something like this with x || goto cleanup_before_x but of course there's no goto.

Ideally something that works in (da)sh, although I'm ok with requiring bash if that simplifies things. (Perhaps using arrays?)

Best Answer

A common technique is to use a trap to undo everything when you're done. It needs to be idempotent, i.e. it should fail gracefully if some of the steps cannot be completed.

#!/bin/bash

clean_up=false
errorhandler () {
    umount z || true
    $clean_up && cleanup_thing
    umount y || true
    umount x
}
trap errorhandler ERR EXIT

mount a x
mount b y
setup_thing
clean_up=true
mount c z
do_something

Notice that the trap is also triggered on EXIT so it will execute before the script terminates normally, too; so you don't need to clean up explicitly at all.

The ERR pseudo-signal is a Bash extension, I believe. So this won't work in Ash/Dash/legacy Bourne shell etc.

Related Question