during both $DAY_JOB
and $PET_PROJECT
i often use bash. like A LOT. ;) sometimes to automate some repetitive actions, sometimes to do a quick 1-off task on loads of data… anyway – there's a lot of bash
involved, typically. to make scripts react properly on errors, i typically start off with:
#!/bin/bash set -eu -o pipefail # actual code goes here
it's almost like a c&p sequence in my head. it's great as it allows you to catch bugs and errors early and stop the action, before any more damage gets done. the common problem there however is to cleanup stuff afterwards.
let's imagine we have some complex logic, that needs 3 temp files to operate, created at different stages of the script. sth along these lines:
#!/bin/bash set -eu -o pipefail tmp1=$(mktemp) # do sth with tmp1 tmp2=$(mktemp) # do sth with tmp1 and tmp2 tmp3=$(mktemp) # do sth with tmp1, tmp2 and tmp3 rm -f "$tmp1" "$tmp2" "$tmp3"
all good, when script goes well. the problem begins, when sth fails in the middle (eg. after creating 2nd temp file). then we end up with not calling rm -f …
part, and thus leave out garbage.
in bash
there is a trap statement, that allows to call some actions on given signals… or EXIT
event. so you can do this:
#!/bin/bash set -eu -o pipefail tmp1=$(mktemp) tmp2=$(mktemp) tmp3=$(mktemp) trap "rm -f '$tmp1' '$tmp2' '$tmp3'" EXIT # do sth with tmp1 # do sth with tmp1 and tmp2 # do sth with tmp1, tmp2 and tmp3
now all the temp files will auto-cleanup nicely1). there is just one problem with that – you can have only 1 trap
per signal/event in the script. declaring new one will overwrite the previous. it is therefor all good when you can say upfront what elements do you need to “release”.
but what if you don't? what if these are runtime-defined? or names are known only any at a certain point in time? there is an excellent paradigm / pattern in C++ called RAII. while there are no “object destructors” in bash
, there are “scopes”. in particular you can spawn new shell, in a given “scope”, nesting things, effectively giving you as many trap
s as you want. example:
#!/bin/bash set -eu -o pipefail ( tmp1=$(mktemp) trap "rm '$tmp1'" EXIT # do sth with tmp1 ( tmp2=$(mktemp) trap "rm '$tmp2'" EXIT # do sth with tmp1 and tmp2 ( tmp3=$(mktemp) trap "rm '$tmp3'" EXIT # do sth with tmp1, tmp2 and tmp3 ) ) )
…and voila! now you can have as many “cleanup” procedures as you want, each being called once resource is “descoped”. as a free bonus, it also solves the issue of failure of initialization Nth element, before trap gets registered, like in our 2nd example.
i ten to call this pattern bash
-RAII – it tend to work exceptionally well in practice.