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?
6 Answers 6
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
-
1The solution with cleanup does the right thing - very elegant! It has become an idiom for my bash scripts with
mktemp
calls.Bjoern Dahlgren– Bjoern Dahlgren2016年01月06日 13:20:18 +00:00Commented Jan 6, 2016 at 13:20 -
6This doesn't work if you have shellscript errors in your code that cause it to exit prematurely.ijw– ijw2016年11月10日 03:30:14 +00:00Commented Nov 10, 2016 at 3:30
-
3@ijw: In Bash and Ksh, you can trap
ERR
to handle that, but it is not portable.Zaz– Zaz2017年02月06日 14:40:43 +00:00Commented Feb 6, 2017 at 14:40 -
8This solution isn't robust when another shell calls it. It doesn't handle wait on cooperative exit; you will want to
trap - INT TERM; kill -2 $$
as the last line of cleanup, to tell the parent shell that it exited prematurely. If a parent shell foobar.sh calls your script (foo.sh), and then calls bar.sh, you don't want bar.sh to execute if INT/TERM is sent to your foo.sh.trap cleanup EXIT
will handle this propagation automatically, so it is IMO the most robust. It also means you wouldn't have to callcleanup
at the end of the script.Nicholas Pipitone– Nicholas Pipitone2018年07月20日 20:47:09 +00:00Commented Jul 20, 2018 at 20:47 -
1Doing
kill -2 $$
will propagate up the chain of non-interactive shells, killing each parent shell, and then stop at the first interactive shell, giving stdin back to the keyboard (Which is as excepted, ctrl+c should return to the interactive shell, not hang). Quite sad ash doesn't work withEXIT
as bash makes it real easy (so nvm on EXIT being robust if you're talking compatibility), but the kill is important there.Nicholas Pipitone– Nicholas Pipitone2018年07月20日 20:51:01 +00:00Commented Jul 20, 2018 at 20:51
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
#
-
5
dash
also doesn't trap on justEXIT
when it receivesSIGINT/SIGTERM
.maxschlepzig– maxschlepzig2016年10月29日 16:50:53 +00:00Commented Oct 29, 2016 at 16:50 -
5
zsh
as well - thus, perhapsbash
is the only shell whereEXIT
also does match signals.maxschlepzig– maxschlepzig2016年10月29日 16:59:22 +00:00Commented Oct 29, 2016 at 16:59 -
4@maxschlepzig
zsh
doesn't trap onEXIT
when it receivesINT
, but it does when it receivesTERM
. EDIT: I just noticed how old this was...JoL– JoL2018年05月15日 15:57:28 +00:00Commented May 15, 2018 at 15:57
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.
-
3The caller would just get 1 as the exit code, no matter what signal caused the exit, while withour
trap
the caller would get 130 for SIGINT, 143 for SIGTERM, etc. So I would capture and pass the correct exit code as:sig_cleanup() { err=$?; trap '' EXIT; (exit $err); cleanup; }
.musiphil– musiphil2016年11月14日 05:12:57 +00:00Commented Nov 14, 2016 at 5:12 -
2Can you clarify the purpose of
trap '' EXIT INT TERM
in the cleanup function? Is this to prevent accidental user interruption of cleanup that you mentioned in the last paragraph? Isn't theEXIT
redundant?Six– Six2017年02月03日 08:33:56 +00:00Commented Feb 3, 2017 at 8:33 -
1With
set -o errexit
, callingfalse
insig_cleanup()
impedescleanup()
from being called. In this case,trap cleanup EXIT
seems sufficient.Enno– Enno2020年01月19日 11:28:45 +00:00Commented Jan 19, 2020 at 11:28 -
@musiphil -
err=$?
insig_cleanup
only gives 0 in dash, so apparently the original signal code is lost when trapping signals there, and that's why something like callingfalse
is required. But as @Enno says, this would need aset +e
insig_cleanup
to avoid exiting beforecleanup
is called.mstorsjo– mstorsjo2020年01月29日 08:52:29 +00:00Commented Jan 29, 2020 at 8:52 -
@mstorsjo
err=$?
seems to work in dash 0.5.10.2-6; by which version did you try?jarno– jarno2020年03月24日 13:42:55 +00:00Commented Mar 24, 2020 at 13:42
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
-
3Redirecting INT and TERM to the EXIT trap is an elegant solution, which prevents dual calls to the trap command. Thanks!Noam Manos– Noam Manos2021年06月02日 11:46:59 +00:00Commented Jun 2, 2021 at 11:46
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
)
This code will be executed on HUP INT QUIT ABRT TERM and EXIT. Assuring that it will not run twice like on INT and following EXIT.
#!/bin/sh
stty -echoctl # hide ^C when pressing ctrl + c
tmpFile=$(mktemp tmp-XXXXXX --suffix=.tmp -p "$TMPDIR")
declare -a signals=("HUP" "INT" "QUIT" "ABRT" "TERM" "EXIT") #array for all custom signal traps to cleanup
cleanup() { # function called by trap
rc=$? #returnCode+128 of last command eg INT=2 +128 -> 130
trap '' "${signals[@]}" # mute trap for all signals to not interrupt cleanup() on any next signal
rm "${tmpFile}" #clearTmpFiles
exit "${rc}" #exit with returnCode+128 of last command
}
trap "cleanup" "${signals[@]}"
Tested with:
#!/bin/sh
exit 0
exit 1
exit 2
tr #EXIT TRAP RC 1
kill -INT $$ #interrupt signal #INT 2 RC 0
kill -QUIT $$ #interrupt signal #QUIT 3 RC 0
kill -ABRT $$ #ABRT signal #ABRT 6 RC 0
kill -KILL $$ #KILL yourself THIS IS NOT CAUGHT in cleanup #KILL 9 The SIGKILL signal cannot be trapped. It always immediately interrupts the script.
kill -TERM $$ #terminate yourself gently #TERM 15 RC 0
#AND THIS FOR GENERATING ERRORCODES
ping #EXIT TRAP RC 64
$((hg)) #EXIT TRAP RC 126 permission denied
/tmp/nosuchmethod #EXIT TRAP RC 127 no such file or dir
-
This does not seem to answer the question, "why do so many examples list all of INT TERM EXIT?"2023年03月16日 00:07:11 +00:00Commented Mar 16, 2023 at 0:07
INT TERM EXIT
the cleanup code is executed twice whenSIGTERM
orSIGINT
is received.