When I use the "trap" command in bash, the previous trap for the given signal is replaced.
Is there a way of making more than one trap fire for the same signal?
When I use the "trap" command in bash, the previous trap for the given signal is replaced.
Is there a way of making more than one trap fire for the same signal?
Edit:
It appears that I misread the question. The answer is simple:
handler1 () { do_something; }
handler2 () { do_something_else; }
handler3 () { handler1; handler2; }
trap handler3 SIGNAL1 SIGNAL2 ...
Original:
Just list multiple signals at the end of the command:
trap function-name SIGNAL1 SIGNAL2 SIGNAL3 ...
You can find the function associated with a particular signal using trap -p
:
trap -p SIGINT
Note that it lists each signal separately even if they're handled by the same function.
You can add an additional signal given a known one by doing this:
eval "$(trap -p SIGUSR1) SIGUSR2"
This works even if there are other additional signals being processed by the same function. In other words, let's say a function was already handling three signals - you could add two more just by referring to one existing one and appending two more (where only one is shown above just inside the closing quotes).
If you're using Bash >= 3.2, you can do something like this to extract the function given a signal. Note that it's not completely robust because other single quotes could appear.
[[ $(trap -p SIGUSR1) =~ trap\ --\ \'([^\047]*)\'.* ]]
function_name=${BASH_REMATCH[1]}
Then you could rebuild your trap command from scratch if you needed to using the function name, etc.
Technically you can't set multiple traps for the same signal, but you can add to an existing trap:
trap -p
Here is a bash function that does the above:
# note: printf is used instead of echo to avoid backslash
# processing and to properly handle values that begin with a '-'.
log() { printf '%s\n' "$*"; }
error() { log "ERROR: $*" >&2; }
fatal() { error "$@"; exit 1; }
# appends a command to a trap
#
# - 1st arg: code to add
# - remaining args: names of traps to modify
#
trap_add() {
trap_add_cmd=$1; shift || fatal "${FUNCNAME} usage error"
for trap_add_name in "$@"; do
trap -- "$(
# helper fn to get existing trap command from output
# of trap -p
extract_trap_cmd() { printf '%s\n' "$3"; }
# print existing trap command with newline
eval "extract_trap_cmd $(trap -p "${trap_add_name}")"
# print the new trap command
printf '%s\n' "${trap_add_cmd}"
)" "${trap_add_name}" \
|| fatal "unable to add to trap ${trap_add_name}"
done
}
# set the trace attribute for the above function. this is
# required to modify DEBUG or RETURN traps because functions don't
# inherit them unless the trace attribute is set
declare -f -t trap_add
Example usage:
trap_add 'echo "in trap DEBUG"' DEBUG
About the best you could do is run multiple commands from a single trap
for a given signal, but you cannot have multiple concurrent traps for a single signal. For example:
$ trap "rm -f /tmp/xyz; exit 1" 2
$ trap
trap -- 'rm -f /tmp/xyz; exit 1' INT
$ trap 2
$ trap
$
The first line sets a trap on signal 2 (SIGINT). The second line prints the current traps — you would have to capture the standard output from this and parse it for the signal you want. Then, you can add your code to what was already there — noting that the prior code will most probably include an 'exit' operation. The third invocation of trap clears the trap on 2/INT. The last one shows that there are no traps outstanding.
You can also use trap -p INT
or trap -p 2
to print the trap for a specific signal.
I liked Richard Hansen's answer, but I don't care for embedded functions so an alternate is:
#===================================================================
# FUNCTION trap_add ()
#
# Purpose: appends a command to a trap
#
# - 1st arg: code to add
# - remaining args: names of traps to modify
#
# Example: trap_add 'echo "in trap DEBUG"' DEBUG
#
# See: http://stackoverflow.com/questions/3338030/multiple-bash-traps-for-the-same-signal
#===================================================================
trap_add() {
trap_add_cmd=$1; shift || fatal "${FUNCNAME} usage error"
new_cmd=
for trap_add_name in "$@"; do
# Grab the currently defined trap commands for this trap
existing_cmd=`trap -p "${trap_add_name}" | awk -F"'" '{print $2}'`
# Define default command
[ -z "${existing_cmd}" ] && existing_cmd="echo exiting @ `date`"
# Generate the new command
new_cmd="${existing_cmd};${trap_add_cmd}"
# Assign the test
trap "${new_cmd}" "${trap_add_name}" || \
fatal "unable to add to trap ${trap_add_name}"
done
}
I didn't like having to play with these string manipulations which are confusing at the best of times, so I came up with something like this:
(obviously you can modify it for other signals)
exit_trap_command=""
function cleanup {
eval "$exit_trap_command"
}
trap cleanup EXIT
function add_exit_trap {
local to_add=$1
if [[ -z "$exit_trap_command" ]]
then
exit_trap_command="$to_add"
else
exit_trap_command="$exit_trap_command; $to_add"
fi
}
Here's another option:
on_exit_acc () {
local next="$1"
eval "on_exit () {
local oldcmd='$(echo "$next" | sed -e s/\'/\'\\\\\'\'/g)'
local newcmd=\"\$oldcmd; \$1\"
trap -- \"\$newcmd\" 0
on_exit_acc \"\$newcmd\"
}"
}
on_exit_acc true
Usage:
$ on_exit date
$ on_exit 'echo "Goodbye from '\''`uname`'\''!"'
$ exit
exit
Sat Jan 18 18:31:49 PST 2014
Goodbye from 'FreeBSD'!
tap#
I have been wrote a set of functions for myself to a bit resolve this task in a convenient way.
traplib.sh
#!/bin/bash
# Script can be ONLY included by "source" command.
if [[ -n "$BASH" && (-z "$BASH_LINENO" || ${BASH_LINENO[0]} -gt 0) ]] && (( ! ${#SOURCE_TRAPLIB_SH} )); then
SOURCE_TRAPLIB_SH=1 # including guard
function GetTrapCmdLine()
{
local IFS=$' \t\r\n'
GetTrapCmdLineImpl RETURN_VALUES "$@"
}
function GetTrapCmdLineImpl()
{
local out_var="$1"
shift
# drop return values
eval "$out_var=()"
local IFS
local trap_sig
local stack_var
local stack_arr
local trap_cmdline
local trap_prev_cmdline
local i
i=0
IFS=$' \t\r\n'; for trap_sig in "$@"; do
stack_var="_traplib_stack_${trap_sig}_cmdline"
declare -a "stack_arr=(\"\${$stack_var[@]}\")"
if (( ${#stack_arr[@]} )); then
for trap_cmdline in "${stack_arr[@]}"; do
declare -a "trap_prev_cmdline=(\"\${$out_var[i]}\")"
if [[ -n "$trap_prev_cmdline" ]]; then
eval "$out_var[i]=\"\$trap_cmdline; \$trap_prev_cmdline\"" # the last srored is the first executed
else
eval "$out_var[i]=\"\$trap_cmdline\""
fi
done
else
# use the signal current trap command line
declare -a "trap_cmdline=(`trap -p "$trap_sig"`)"
eval "$out_var[i]=\"\${trap_cmdline[2]}\""
fi
(( i++ ))
done
}
function PushTrap()
{
# drop return values
EXIT_CODES=()
RETURN_VALUES=()
local cmdline="$1"
[[ -z "$cmdline" ]] && return 0 # nothing to push
shift
local IFS
local trap_sig
local stack_var
local stack_arr
local trap_cmdline_size
local prev_cmdline
IFS=$' \t\r\n'; for trap_sig in "$@"; do
stack_var="_traplib_stack_${trap_sig}_cmdline"
declare -a "stack_arr=(\"\${$stack_var[@]}\")"
trap_cmdline_size=${#stack_arr[@]}
if (( trap_cmdline_size )); then
# append to the end is equal to push trap onto stack
eval "$stack_var[trap_cmdline_size]=\"\$cmdline\""
else
# first stack element is always the trap current command line if not empty
declare -a "prev_cmdline=(`trap -p $trap_sig`)"
if (( ${#prev_cmdline[2]} )); then
eval "$stack_var=(\"\${prev_cmdline[2]}\" \"\$cmdline\")"
else
eval "$stack_var=(\"\$cmdline\")"
fi
fi
# update the signal trap command line
GetTrapCmdLine "$trap_sig"
trap "${RETURN_VALUES[0]}" "$trap_sig"
EXIT_CODES[i++]=$?
done
}
function PopTrap()
{
# drop return values
EXIT_CODES=()
RETURN_VALUES=()
local IFS
local trap_sig
local stack_var
local stack_arr
local trap_cmdline_size
local trap_cmd_line
local i
i=0
IFS=$' \t\r\n'; for trap_sig in "$@"; do
stack_var="_traplib_stack_${trap_sig}_cmdline"
declare -a "stack_arr=(\"\${$stack_var[@]}\")"
trap_cmdline_size=${#stack_arr[@]}
if (( trap_cmdline_size )); then
(( trap_cmdline_size-- ))
RETURN_VALUES[i]="${stack_arr[trap_cmdline_size]}"
# unset the end
unset $stack_var[trap_cmdline_size]
(( !trap_cmdline_size )) && unset $stack_var
# update the signal trap command line
if (( trap_cmdline_size )); then
GetTrapCmdLineImpl trap_cmd_line "$trap_sig"
trap "${trap_cmd_line[0]}" "$trap_sig"
else
trap "" "$trap_sig" # just clear the trap
fi
EXIT_CODES[i]=$?
else
# nothing to pop
RETURN_VALUES[i]=""
fi
(( i++ ))
done
}
function PopExecTrap()
{
# drop exit codes
EXIT_CODES=()
local IFS=$' \t\r\n'
PopTrap "$@"
local cmdline
local i
i=0
IFS=$' \t\r\n'; for cmdline in "${RETURN_VALUES[@]}"; do
# execute as function and store exit code
eval "function _traplib_immediate_handler() { $cmdline; }"
_traplib_immediate_handler
EXIT_CODES[i++]=$?
unset _traplib_immediate_handler
done
}
fi
test.sh
#/bin/bash
source ./traplib.sh
function Exit()
{
echo exitting...
exit $@
}
pushd ".." && {
PushTrap "echo popd; popd" EXIT
echo 111 || Exit
PopExecTrap EXIT
}
GetTrapCmdLine EXIT
echo -${RETURN_VALUES[@]}-
pushd ".." && {
PushTrap "echo popd; popd" EXIT
echo 222 && Exit
PopExecTrap EXIT
}
Usage
cd ~/test
./test.sh
Output
~ ~/test
111
popd
~/test
--
~ ~/test
222
exitting...
popd
~/test
There's no way to have multiple handlers for the same trap, but the same handler can do multiple things.
The one thing I don't like in the various other answers doing the same thing is the use of string manipulation to get at the current trap function. There are two easy ways of doing this: arrays and arguments. Arguments is the most reliable one, but I'll show arrays first.
When using arrays, you rely on the fact that trap -p SIGNAL
returns trap -- ??? SIGNAL
, so whatever is the value of ???
, there are three more words in the array.
Therefore you can do this:
declare -a trapDecl
trapDecl=($(trap -p SIGNAL))
currentHandler="${trapDecl[@]:2:${#trapDecl[@]} - 3}"
eval "trap -- 'your handler;'${currentHandler} SIGNAL"
So let's explain this. First, variable trapDecl
is declared as an array. If you do this inside a function, it will also be local, which is convenient.
Next we assign the output of trap -p SIGNAL
to the array. To give an example, let's say you are running this after having sourced osht (unit testing for shell), and that the signal is EXIT
. The output of trap -p EXIT
will be trap -- '_osht_cleanup' EXIT
, so the trapDecl
assignment will be substituted like this:
trapDecl=(trap -- '_osht_cleanup' EXIT)
The parenthesis there are normal array assignment, so trapDecl
becomes an array with four elements: trap
, --
, '_osht_cleanup'
and EXIT
.
Next we extract the current handler -- that could be inlined in the next line, but for explanation's sake I assigned it to a variable first. Simplifying that line, I'm doing this: currentHandler="${array[@]:offset:length}"
, which is the syntax used by Bash to say pick length
elements starting at element offset
. Since it starts counting from 0
, number 2
will be '_osht_cleanup'
. Next, ${#trapDecl[@]}
is the number of elements inside trapDecl
, which will be 4 in the example. You subtract 3 because there are three elements you don't want: trap
, --
and EXIT
. I don't need to use $(...)
around that expression because arithmetic expansion is already performed on the offset
and length
arguments.
The final line performs an eval
, which is used so that the shell will interpret the quoting from the output of trap
. If we do parameter substitution on that line, it expands to the following in the example:
eval "trap -- 'your handler;''_osht_cleanup' EXIT"
Do not be confused by the double quote in the middle (''
). Bash simply concatenates two quotes strings if they are next to each other. For example, '1'"2"'3''4'
is expanded to 1234
by Bash. Or, to give a more interesting example, 1" "2
is the same thing as "1 2"
. So eval takes that string and evaluates it, which is equivalent to executing this:
trap -- 'your handler;''_osht_cleanup' EXIT
And that will handle the quoting correctly, turning everything between --
and EXIT
into a single parameter.
To give a more complex example, I'm prepending a directory clean up to the osht handler, so my EXIT
signal now has this:
trap -- 'rm -fr '\''/var/folders/7d/qthcbjz950775d6vn927lxwh0000gn/T/tmp.CmOubiwq'\'';_osht_cleanup' EXIT
If you assign that to trapDecl
, it will have size 6 because of the spaces on the handler. That is, 'rm
is one element, and so is -fr
, instead of 'rm -fr ...'
being a single element.
But currentHandler
will get all three elements (6 - 3 = 3), and the quoting will work out when eval
is run.
Arguments just skips all the array handling part and uses eval
up front to get the quoting right. The downside is that you replace the positional arguments on bash, so this is best done from a function. This is the code, though:
eval "set -- $(trap -p SIGNAL)"
trap -- "your handler${3:+;}${3}" SIGNAL
The first line will set the positional arguments to the output of trap -p SIGNAL
. Using the example from the Arrays section, $1
will be trap
, $2
will be --
, $3
will be _osht_cleanup
(no quotes!), and $4
will be EXIT
.
The next line is pretty straightforward, except for ${3:+;}
. The ${X:+Y}
syntax means "output Y
if the variable X
is unset or null". So it expands to ;
if $3
is set, or nothing otherwise (if there was no previous handler for SIGNAL
).
trap 'handler1;handler2;handler3' EXIT
trap "$( trap -p EXIT | cut -f2 -d \' );newHandler" EXIT
handlers="$( trap -p EXIT | cut -f2 -d \' )"
trap "${handlers}${handlers:+;}newHandler" EXIT
trap-add() {
local sig="${2:?Signal required}"
hdls="$( trap -p ${sig} | cut -f2 -d \' )";
trap "${hdls}${hdls:+;}${1:?Handler required}" "${sig}"
}
export -f trap-add
Usage:
trap-add 'echo "Bye bye"' EXIT
trap-add 'echo "See you next time"' EXIT
Remark : This works only has long as the handlers are function names, or simple instructions that did not contains any simple cote (simple cotes conflicts with cut -f2 -d \'
).