Bash EXIT traps are indispensable when writing complex scripts that need to clean up after themselves. An EXIT trap works a lot like a try/finally pair in a language like C++ or Python: it allows you to specify code that should be run when a script exits.
Here's a concrete example showing its use. This script creates a backup of a Bitcoin data directory, encrypts it with GPG, and then copies the encrypted backup to a Google Cloud Storage bucket. The script uses an EXIT trap to ensure that the local backup and the local encrypted backup are always deleted when the script exits:
# exit when any command returns an error
set -e
# make sure our cleanup code runs on script exit
trap 'rm -f ~/backup.dat{,.gpg}' EXIT
# create the raw wallet backup
bitcoin-cli backupwallet ~/backup.dat
# encrypt the backup and copy to google cloud storage
gpg --batch --yes -r myself -e ~/backup.dat
gsutil cp ~/backup.dat.gpg gs://my-backups/
Unfortunately, there is no mechanism to create traps that are scoped to
functions. For instance, suppose we had wrapped our backup code in a function
defined in our ~/.bashrc
file. If the function sets an EXIT trap, there are
two problems:
- The trap installed by the function will replace any existing EXIT traps.
- The EXIT will run when the shell process itself exits, not when the function exits.
Normally when you're writing shell scripts these aren't major problems. You
should know what traps have been defined, and deferring cleanup until the script
actually exits isn't generally a problem. However, this behavior does become
problematic when defining functions intended for interactive use, e.g. functions
defined in one's ~/.bashrc
file. The problem is that even if you did want to
use an EXIT trap, it might be hours or days (or really any arbitrarily long
amount of time) before the user actually exits the
I would love it if some day a future Bash version implements a new type of trap scoped to functions (e.g. FUNCTIONEXIT), but that doesn't exist. Here are two possible workarounds.
Workaround #1
The most straightforward way to work around this problem is to write two functions. The first has the core logic, and by convention will be prefixed with an underscore. The second function calls the core function, saves the exit status, does cleanup, and then return the original exit status.
# user should not call this directly
_backup() {
# create the raw wallet backup
bitcoin-cli backupwallet ~/backup.dat
# encrypt the backup and copy to google cloud storage
gpg --batch --yes -r myself -e ~/backup.dat
gsutil cp ~/backup.dat.gpg gs://my-backups/
}
# version the user is expected to call
backup() {
# invoke _backup and save the exit status
_backup
local s=$?
# do the cleanup logic
rm -f ~/backup.dat{,.gpg}
# return the exit status of _backup
return $s
}
Workaround #2
Here's another way to solve the problem, which I like more. The idea is to use a regular EXIT trap, but run the core logic in a subshell:
backup() {
( # everything within these parens run in a subshell
trap 'rm -f ~/backup.dat{,.gpg}' EXIT
# create the raw wallet backup
bitcoin-cli backupwallet ~/backup.dat
# encrypt the backup and copy to google cloud storage
gpg --batch --yes -r myself -e ~/backup.dat
gsutil cp ~/backup.dat.gpg gs://my-backups/
) # end of the subshell expression
}
In case you're not familiar with the subshell syntax, enclosing one or more bash statements in parentheses causes those statements to run in a subshell. This isolates the code to its own process, and makes this pattern much simpler.