Many examples for trap use trap ... INT TERM EXIT for cleanup tasks. But is it really necessary to list all the three sigspecs?
If a SIGNAL_SPEC is EXIT (0) ARG is executed on exit from the shell.
which I believe applies whether the script finished normally or it finished because it received SIGINT or SIGTERM. An experiment also confirms my belief:
$ cat ./trap-exit #!/bin/bash trap 'echo TRAP' EXIT sleep 3 $ ./trap-exit & sleep 1; kill -INT %1 [1] 759 TRAP [1]+ Interrupt ./trap-exit $ ./trap-exit & sleep 1; kill -TERM %1 [1] 773 TRAP [1]+ Terminated ./trap-exit
Then why do so many examples list all of INT TERM EXIT? Or did I miss something and is there any case where a sole EXIT would miss?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
Yes, there is a difference.
This script will exit when you press Enter, or send it SIGINT or SIGTERM:
trap '' EXIT echo ' --- press ENTER to close --- ' read response
This script will exit when you press Enter:
trap '' EXIT INT TERM echo ' --- press ENTER to close --- ' read response
* Tested in sh, Bash, and Zsh. (no longer works in sh when you add a command for trap to run)
There’s also what @Shawn said: Ash and Dash don’t trap signals with EXIT.
So, to handle signals robustly, it’s best to avoid trapping EXIT altogether, and use something like this:
cleanup() {
echo "Cleaning stuff up..."
exit
}
trap cleanup INT TERM
echo ' --- press ENTER to close --- '
read var
cleanup
Method 2
The POSIX spec doesn’t say much about the conditions resulting in executing the EXIT trap, only about what its environment must look like when it is executed.
In Busybox’s ash shell, your trap-exit test does not echo ‘TRAP’ before exiting due to either SIGINT or SIGTERM. I would suspect there are other shells in existance that may not work that way as well.
# /tmp/test.sh & sleep 1; kill -INT %1 # [1]+ Interrupt /tmp/test.sh # # # /tmp/test.sh & sleep 1; kill -TERM %1 # [1]+ Terminated /tmp/test.sh #
Method 3
Refining the last answer, because it has issues:
# Our general exit handler
cleanup() {
err=$?
echo "Cleaning stuff up..."
trap '' EXIT INT TERM
exit $err
}
sig_cleanup() {
trap '' EXIT # some shells will call EXIT after the INT handler
false # sets $?
cleanup
}
trap cleanup EXIT
trap sig_cleanup INT QUIT TERM
Points above:
INT and TERM handlers don’t quit for me when I test – they handle the error then the shell returns to exiting (and this is not too surprising). So I ensure that the cleanup exits afterwards, and in the case of the signals always uses an error code (and in the other case of a normal exit, preserves the error code).
With bash, it seems that exiting in the INT handler also calls the EXIT handler, hence I untrap the exit handler and call it myself (which will work in any shell regardless of behaviour).
I trap exit because shell scripts can exit before they reach the bottom – syntax errors, set -e and a nonzero return, simply calling exit. You can’t rely on a shellscript getting to the bottom.
SIGQUIT is Ctrl- if you’ve never tried it. Gets you a bonus coredump. So I think it’s also worth trapping, even if it’s a little obscure.
Past experience says if you (like me) always press Ctrl-C several times, you’ll sometimes catch it half way through the cleanup part of your shell script, so this works but not always as perfectly as you’d like.
Method 4
This is how you can make the Bash script report its return code $?, while being able to catch the SIGINT and SIGTERM signals. I find this very useful for scripts running in a CI/CD pipeline:
notify() {
[[ $1 = 0 ]] || echo ❌ EXIT $1
# you can notify some external services here,
# ie. Slack webhook, Github commit/PR etc.
}
trap '(exit 130)' INT
trap '(exit 143)' TERM
trap 'rc=$?; notify $rc; exit $rc' EXIT
Method 5
It depends on what you’re trying to achieve, and which shells you’re targeting. For bash it’s probably okay to just use EXIT. But not all shells invoke the EXIT handler on SIGINT/SIGTERM.
For them you can try to set one handler for several signals (trap '...' INT EXIT), but then it may be invoked several times:
$ bash -c 'trap "echo trap" INT EXIT; sleep 3' & pid=$!; sleep 1; kill -INT $pid; wait [1] 276923 trap trap [1]+ Done bash -c 'trap "echo trap" INT EXIT; sleep 3'
So either you write it with that in mind, or you can try to forward everything to the EXIT handler:
$ bash -c 'trap "exit 123" INT; trap "echo EXIT $?" EXIT; sleep 3' & pid=$!; sleep 1; kill -INT $pid; wait [1] 286229 EXIT 123 [1]+ Exit 123 bash -c 'trap "exit 123" INT; trap "echo EXIT $?" EXIT; sleep 3'
But if you set up a handler for SIGINT, you generally want it to kill the script with SIGINT:
a.sh:
trap 'exit 123' INT trap 'echo EXIT $?; trap - INT; kill -INT $$' EXIT sleep 3
$ bash h.sh & pid=$!; sleep 1; kill -INT $pid; wait $pid [1] 236263 EXIT 123 [1]+ Interrupt bash h.sh
And under Debian < 10 (dash < 0.5.10) the signal that killed the script (if any) is not passed.
The solution I came up with:
set -eu
cleanup() {
echo "cleanup ($1)"
trap - INT TERM EXIT # avoid reexecuting handlers
if [ "$1" = 130 ]; then
kill -INT $$
elif [ "$1" = 143 ]; then
kill -TERM $$
else
exit "$1"
fi
}
trap 'cleanup 130' INT
trap 'cleanup 143' TERM
trap 'cleanup $?' EXIT
if [ "${1-}" = fail ]; then
no-such-command
fi
sleep 3
$ bash f.sh; echo $? cleanup (0) 0 $ bash f.sh fail; echo $? f.sh: line 20: no-such-command: command not found cleanup (127) 127 $ bash f.sh & pid=$!; sleep 1; kill -INT $pid; wait $pid [1] 282422 cleanup (130) [1]+ Interrupt bash f.sh $ bash f.sh & pid=$!; sleep 1; kill -TERM $pid; wait $pid [1] 282458 cleanup (143) [1]+ Terminated bash f.sh
Tested in:
bash:5.1.8- dash:
0.5.10,0.5.8,0.5.7 - Alpine Linux 3.14 (
busybox)
Method 6
I found no 100% perfect answer, but @ijw answer and @musiphil comment were very close:
“trap … INT TERM EXIT” really necessary?
The problem with their answers:
- if you press many times Ctrl^C, the cleanup function is repeated which is not what we would usually expect
Here’s a proper code I tested under bash and POSIX sh
#!/bin/sh
# Script to test traps
# If interrupted:
# - SIGINT: Ctrl^C (kill -2), exit code 128+2=130
# - SIGTERM: kill -15, allows a graceful termination
# unlike kill -9 that cannot be trapped
# exit code 128+15=143
# - SIGQUIT: Ctrl^ or kill -3, like SIGINT but do a core dump
# exit code 128+3=131
# - EXIT: on any exit reason. On some shells, it is always called
# even after SIGINT, SIGTERM...
# this causes the clean function to be called twice,
# unless we cancel the trap like in sig_cleanup()
# we need to save the exit code when calling cleanup function
# If left to properly finish, simulate error exit 2 to test script exit code (Signal EXIT)
# Our test function to handle cleanup:
some_job() {
echo "Working hard on some stuff..."
for i in $(seq 1 5); do
#printf "."
printf '%s' "$i."
sleep 1
done
echo ""
echo "Job done, but we found some errors !"
return 2 # to simulate script exit code 2
}
# Our clean temp files function
# - should not be interrupted
# - should not be called twice if interrupted
clean_tempfiles() {
echo ""
echo "Cleaning temp files, do not interrupt..."
for i in $(seq 1 5); do
printf "> "
sleep 1
done
echo ""
}
# Called on signal EXIT, or indirectly on INT QUIT TERM
clean_exit() {
# save the return code of the script
err=$?
# reset trap for all signals to not interrupt clean_tempfiles() on any next signal
trap '' EXIT INT QUIT TERM
clean_tempfiles
exit $err # exit the script with saved $?
}
# Called on signals INT QUIT TERM
sig_cleanup() {
# save error code (130 for SIGINT, 143 for SIGTERM, 131 for SIGQUIT)
err=$?
# some shells will call EXIT after the INT signal
# causing EXIT trap to be executed, so we trap EXIT after INT
trap '' EXIT
(exit $err) # execute in a subshell just to pass $? to clean_exit()
clean_exit
}
trap clean_exit EXIT
trap sig_cleanup INT QUIT TERM
some_job
exit # remove to test curl interruption
curl -T test.sh "tftp://10.0.10.55"
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0