2022-05-29 - bash and RAII

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

1)
ok – it won't , if creating one of the temp files would fail itself. but let's leave that 1 out for a sec.
blog/2022/05/29/2022-05-29_-_bash_and_raii.txt · Last modified: 2022/05/29 18:36 by basz
Back to top
Valid CSS Driven by DokuWiki Recent changes RSS feed Valid XHTML 1.0