92

Many examples for trap use trap ... INT TERM EXIT for cleanup tasks. But is it really necessary to list all the three sigspecs?

The manual says:

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?

Roland
  • 163
musiphil
  • 1,611
  • 2
  • 14
  • 16
  • 7
    Also keep in mind that with a spec like INT TERM EXIT the cleanup code is executed twice when SIGTERM or SIGINT is received. – maxschlepzig Oct 29 '16 at 16:49

7 Answers7

42

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
Zaz
  • 2,589
  • 1
    The solution with cleanup does the right thing - very elegant! It has become an idiom for my bash scripts with mktemp calls. – Bjoern Dahlgren Jan 06 '16 at 13:20
  • 6
    This doesn't work if you have shellscript errors in your code that cause it to exit prematurely. – ijw Nov 10 '16 at 03:30
  • 3
    @ijw: In Bash and Ksh, you can trap ERR to handle that, but it is not portable. – Zaz Feb 06 '17 at 14:40
  • 8
    This 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 call cleanup at the end of the script.

    – Nicholas Pipitone Jul 20 '18 at 20:47
  • 1
    Doing 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 with EXIT 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 Jul 20 '18 at 20:51
  • @Zaz: Could you incorporate the comments and the two-trap variation from the other answer into your answer. As it currently stands, your answer makes a great point but isn't really as robust as it claims. – Fritz Aug 22 '19 at 19:57
  • @NicholasPipitone Mere trap cleanup EXIT will not do, if you want to interupt the "ping" loop at https://mywiki.wooledge.org/SignalTrap#Special_Note_On_SIGINT_and_SIGQUIT – jarno Mar 22 '20 at 13:00
28

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
# 
Shawn J. Goff
  • 46,081
15

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.

ijw
  • 247
  • 3
    The 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 Nov 14 '16 at 05:12
  • 2
    Can 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 the EXIT redundant? – Six Feb 03 '17 at 08:33
  • 1
    With set -o errexit, calling false in sig_cleanup() impedes cleanup() from being called. In this case, trap cleanup EXIT seems sufficient. – Enno Jan 19 '20 at 11:28
  • @musiphil - err=$? in sig_cleanup only gives 0 in dash, so apparently the original signal code is lost when trapping signals there, and that's why something like calling false is required. But as @Enno says, this would need a set +e in sig_cleanup to avoid exiting before cleanup is called. – mstorsjo Jan 29 '20 at 08:52
  • @mstorsjo err=$? seems to work in dash 0.5.10.2-6; by which version did you try? – jarno Mar 24 '20 at 13:42
  • @musiphil why not use sig_cleanup() { err=$?; trap - EXIT; cleanup; exit $err; }? – jarno Mar 24 '20 at 13:50
  • @jarno - I tried dash 0.5.8-2.10 (ubuntu 18.04), and I can confirm that I see that it behaves the way you describe in dash 0.5.10.2. I bisected it down to this change, https://git.kernel.org/pub/scm/utils/dash/dash.git/commit/?id=d28c13e7119a605ef152a4310e9415dc7ae9b8f3, which was included in dash 0.5.9. – mstorsjo Mar 24 '20 at 20:17
  • @mstorsjo if the changed code is already in 0.5.8, how could the change be in 0.5.9? BTW In my experience mere exit works in dash, too, but it does not work in bash. – jarno Mar 24 '20 at 22:58
  • @jarno - No, the change is in 0.5.9. With 0.5.8, $? is zero in sig_cleanup(), in 0.5.9 and onwards it has the actual signal return value. – mstorsjo Mar 25 '20 at 10:34
  • @mstorsjo I tested it in Ubuntu 18.04 and it worked. $? is 130 after Ctrl-C. It has dash 0.5.8-2.10. – jarno Mar 25 '20 at 11:19
  • @jarno With https://pastebin.com/d74jkFVU on Ubuntu 18.04 I'm getting $? in sig_cleanup, if I press ctrl+c during the sleep. With a newer dash I'm getting 130 there. – mstorsjo Mar 25 '20 at 12:40
  • @mstorsjo oh, but that is only, if you have errexit flag set. I do not see why it should affect, though. And in never version it is fixed. – jarno Mar 25 '20 at 18:49
  • @mstorsjo see also my related answer. – jarno Mar 25 '20 at 23:33
8

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
  • 3
    Redirecting INT and TERM to the EXIT trap is an elegant solution, which prevents dual calls to the trap command. Thanks! – Noam Manos Jun 02 '21 at 11:46
1

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)
x-yuri
  • 3,373
0

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 &quot;&quot;
echo &quot;Job done, but we found some errors !&quot;
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"

-1

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