Skip to end of metadata
Go to start of metadata

Traps (signal handlers) are useful for cleaning up resources, but have some unexpected quirks in bash.

Multiple EXIT Traps

You would hope that the following would call f1 then f2 on exit:

function f1() { echo "one" >&2; }
function f2() { echo "two" >&2; }

trap f1 EXIT  # nope!
trap f2 EXIT

... but only f2 is called, since a new EXIT trap replaces an existing one.

This is a particular problem when two EXIT traps are widely separated; for example, if one trap is inside a script sourced by another script.

EXIT Trap in a function

An EXIT trap defined in a function is called on exit from the script using that function. For instance:

#!/bin/bash
 
function f()
{
	trap 'echo trap defined in f >&2' EXIT
	echo "end of f" >&2
}
 
f
echo "after f" >&2 

will print

end of f
after f
trap defined in f

This trap will also be called if the function exits explicitly via exit statement or after receiving a signal such as INT or TERM. If the function is called as bash -c f, however, the order of messages is reversed:

#!/bin/bash

function f()
{
    trap 'echo trap defined in f >&2' EXIT
	echo "end of f" >&2
}
export -f f

bash -c f
echo "after f" >&2

produces

end of f
trap defined in f
after f

In particular, this happens when a function is invoked under comma_execute_and_wait wrapper.

Signal Traps

Traps can catch signals such as SIGTERM (from a kill command) or SIGINT (sent by Ctrl+C).

But be aware that EXIT traps are still called when a signal is received, so bye will be called twice in the following script when Ctrl+C is pressed:

function bye() { echo "Bye!" >&2; exit 1; }

trap bye EXIT SIGINT
sleep 1; sleep 1; sleep 1; sleep 1   # pressing Ctrl+C here calls bye() twice
echo "Reached the end" >&2

The EXIT trap was called a second time because of the exit command inside bye(). The solution is not to just remove the exit, since then bye() won't terminate the script on SIGINT.

It is safer just to just trap EXIT (and also ignore signals inside the trap function):

function bye()
{
    trap '' SIGINT SIGHUP SIGTERM SIGQUIT   # ignore signals
    # ... clean up resources ...
    echo "Bye!" >&2
    # no need for "exit 1"
}

trap bye EXIT
sleep 1; sleep 1; sleep 1; sleep 1   # pressing Ctrl+C here just calls bye() once now
echo "Reached the end" >&2

In this case bye() doesn't need to contain an exit command:

  • If a script runs to completion, the exit status is 0.
  • If it is terminated by exit n, it exits with status n after calling the trap function.
  • If it is terminated by a signal, the exit status is 128 plus the signal code, even if there is an exit command inside the trap function. (Use trap -l to see a list of signal codes).

RETURN Traps

RETURN traps are not called if the script terminates (via a signal or exit command), so they are best avoided in general.

An alternative method of cleaning up on function return is to call the function inside a subshell with an EXIT trap. This has the normal limitations of subshells, however (e.g. a variable set in the subshell won't be set in the parent).

  • There is an extra gotcha here: using the normal subshell bracket syntax ( ... ) doesn't seem to call the EXIT trap. Instead, create the subshell using bash -c.
  • See comma_process_kill() in comma/bash/process/comma-process-util for an example.

If you do end up using a RETURN trap, there is one last gotcha: the trap needs to be unset (using trap - RETURN), otherwise it can remain active. (This behaviour is inconsistent in bash: sometimes it happens and sometimes it doesn't).

DEBUG Traps

DEBUG traps are called for every command in a script:

function debug_trap() { echo "Line $1: $2" >&2 ; }
trap 'debug_trap $LINENO "$BASH_COMMAND"' DEBUG

The main gotcha is that DEBUG traps are not inherited by functions unless they have the "trace" attribute (declare -t).

 

  • No labels

2 Comments

  1. Any gotchas concerning USR1? It is used in ibis in a few places.

    1. Unknown User (david) AUTHOR

      Not that I know of ...