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.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.