9

I'm trapping both INT and ERR with the following code

set -ex -o pipefail

dest=$(mktemp -d)
cd "$dest"

trap "echo; echo Clean up; rm -rf $dest" INT ERR
sleep 9999

When I press ^C the clean up callback is executed multiple times

++ echo Clean up
Clean up
++ rm -rf /tmp/tmp.KYXL110516
++ echo

++ echo Clean up
Clean up
++ rm -rf /tmp/tmp.KYXL110516

Is that expected behavior? Is it possible to execute it only once?

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
daisy
  • 54,555

2 Answers2

8

You're getting both the INT and ERR signals; SIGINT is handed to sleep, who exits with a non-zero return code. The non-zero return code then triggers the trap for SIGERR.

If a sigspec is ERR, the command arg is executed whenever a pipeline (which may consist of a single simple command), a list, or a compound command returns a non-zero exit status...

An example, to see the separate traps:

set -ex -o pipefail
trap "echo Clean up for INT" INT
trap "echo Clean up for ERR" ERR
sleep 9999

Executing, then Control-C:

+ trap 'echo Clean up for INT' INT
+ trap 'echo Clean up for ERR' ERR
+ sleep 9999
^C++ echo Clean up for INT
Clean up for INT
++ echo Clean up for ERR
Clean up for ERR

As for calling the trap only once, one option would be to reset the ERR trap while inside the INT trap:

...
trap "echo Clean up for INT; trap ERR" INT ERR
...

... which results in:

+ trap 'echo Clean up for INT; trap ERR' INT ERR
+ sleep 9999
^C++ echo Clean up for INT
Clean up for INT
++ trap ERR
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
3

In bash, you can simply replace this with trap "my-cleanup-command" EXIT. This trap will be run when the script exits, including if it is sent SIGINT. In my opinion this is usually the more elegant approach...

Except for the fact that it is bash-specific. EXIT traps are mentioned in POSIX, but the behaviour is not specified. In many other shells, the EXIT trap is not run if the shell exits because of a signal.

https://unix.stackexchange.com/a/57960/29483

Sometimes I really hate shell scripting :).


Looking at the linked answer, I think a less bash-specific way to ensure no code gets run after the first trap would be to exit immediately. It seems there's different ways you might want to do this, but here's the simplest one I would try.

cleanup() {
    echo "Clean up"
    rm -rf "$dest"
}
trap cleanup EXIT
trap "exit 1" INT ERR
sourcejedi
  • 50,249