Multiple Bash traps for the same signal

Viewed 42580

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?

20 Answers

I have been wrote a set of functions for myself to a bit resolve this task in a convenient way.

Update: The implementation here is obsoleted and left here as a demonstration. The new implementation is more complex, having dependencies, supports a wider range of cases and quite big to be placed here.

New implementation: https://sf.net/p/tacklelib/tacklelib/HEAD/tree/trunk/bash/tacklelib/traplib.sh

Here is the list of features of the new implementation:

Pros:

  1. Automatically restores the previous trap handler in nested functions. Originally the RETURN trap restores ONLY if ALL functions in the stack did set it.
  2. The RETURN signal trap can support other signal traps to achieve the RAII pattern as in other languages. For example, to temporary disable interruption handling and auto restore it at the end of a function while an initialization code is executing.
  3. Protection from call not from a function context in case of the RETURN signal trap.
  4. The not RETURN signal handlers in the whole stack invokes together in a bash process from the bottom to the top and executes them in order reversed to the tkl_push_trap function calls
  5. The RETURN signal trap handlers invokes only for a single function from the bottom to the top in reverse order to the tkl_push_trap function calls.
  6. Because the EXIT signal does not trigger the RETURN signal trap handler, then the EXIT signal trap handler does setup automatically at least once per bash process when the RETURN signal trap handler makes setup at first time in a bash process. That includes all bash processes, for example, represented as (...) or $(...) operators. So the EXIT signal trap handlers automatically handles all the RETURN trap handlers before to run itself.
  7. The RETURN signal trap handler still can call to tkl_push_trap and tkl_pop_trap functions to process the not RETURN signal traps
  8. The RETURN signal trap handler can call to tkl_set_trap_postponed_exit function from both the EXIT and RETURN signal trap handlers. If is called from the RETURN signal trap handler, then the EXIT trap handler will be called after all the RETURN signal trap handlers in the bash process. If is called from the EXIT signal trap handler, then the EXIT trap handler will change the exit code after the last EXIT signal trap handler is invoked.
  9. Faster access to trap stack as a global variable instead of usage the (...) or $(...) operators which invokes an external bash process.
  10. The source command ignores by the RETURN signal trap handler, so all calls to the source command will not invoke the RETURN signal trap user code (marked in the Pros, because RETURN signal trap handler has to be called only after return from a function in the first place and not from a script inclusion).

Cons:

  1. You must not use builtin trap command in the handler passed to the tkl_push_trap function as tkl_*_trap functions does use it internally.
  2. You must not use builtin exit command in the EXIT signal handlers while the EXIT signal trap handler is running. Otherwise that will leave the rest of the RETURN and EXIT signal trap handlers not executed. To change the exit code from the EXIT handler you can use tkl_set_trap_postponed_exit function for that.
  3. You must not use builtin return command in the RETURN signal trap handler while the RETURN signal trap handler is running. Otherwise that will leave the rest of the RETURN and EXIT signal trap handlers not executed.
  4. All calls to the tkl_push_trap and tkl_pop_trap functions has no effect if has been called from a trap handler for a signal the trap handler is handling (recursive call through the signal).
  5. You have to replace all builtin trap commands in nested or 3dparty scripts by tkl_*_trap functions if already using the library.
  6. The source command ignores by the RETURN signal trap handler, so all calls to the source command will not invoke the RETURN signal trap user code (marked in the Cons, because of losing the back compatability here).

Old implementation:

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

In this answer I implemented a simple solution. Here I implement another solution that is based on extracting of previous trap commands from trap -p output. But I don't know how much it is portable because I'm not sure that trap -p output is regulated. Maybe its format can be changed in future (but I doubt that).

trap_add()
{
    local new="$1"
    local sig="$2"

    # Get raw trap output.
    local old="$(trap -p "$sig")"

    # Extract previous commands from raw trap output.
    old="${old#*\'}"          # Remove first ' and everything before it.
    old="${old%\'*}"          # Remove last  ' and everything after  it.
    old="${old//\'\\\'\'/\'}" # Replace every '\'' (escaped ') to just '.

    # Combine new and old commands. Separate them by newline.
    trap -- "$new
$old" "$sig"
}

trap_add 'echo AAA'      EXIT
trap_add '{ echo BBB; }' EXIT

But this solution doesn't work well with subshells. Unfortunately trap -p prints commands of outer shell. And we execute them in subshell after extracting.

trap_add 'echo AAA' EXIT
( trap_add 'echo BBB' EXIT; )

In the above example echo AAA is executed twice: first time in subshell and second time in outer shell.

We have to check whether we are in new subshell and if we are then we must not take commands from trap -p.

trap_add()
{
    local new="$1"

    # Avoid inheriting trap commands from outer shell.
    if [[ "${trap_subshell:-}" != "$BASH_SUBSHELL" ]]; then
        # We are in new subshell, don't take commands from outer shell.
        trap_subshell="$BASH_SUBSHELL"
        local old=
    else
        # Get raw trap output.
        local old="$(trap -p EXIT)"

        # Extract previous commands from trap output.
        old="${old#*\'}"          # Remove first ' and everything before it.
        old="${old%\'*}"          # Remove last  ' and everything after  it.
        old="${old//\'\\\'\'/\'}" # Replace every '\'' (escaped ') to just '.
    fi

    # Combine new and old commands. Separate them by newline.
    trap -- "$new
$old" EXIT
}

Note that to avoid security issue you have to reset the trap_subshell variable at script startup.

trap_subshell=

Unfortunately the solution above works only with EXIT signal now. A generic solution that works with any signal is below.

# Check if argument is number.
is_num()
{
    [ -n "$1" ] && [ "$1" -eq "$1" ] 2>/dev/null
}

# Convert signal name to signal number.
to_sig_num()
{
    if is_num "$1"; then
      # Signal is already number.
      kill -l "$1" >/dev/null # Check that signal number is valid.
      echo    "$1"            # Return result.
    else
      # Convert to signal number.
      kill -l "$1"
    fi
}

trap_add()
{
    local new="$1"
    local sig="$2"

    local sig_num
    sig_num=$(to_sig_num "$sig")

    # Avoid inheriting trap commands from outer shell.
    if [[ "${trap_subshell[$sig_num]:-}" != "$BASH_SUBSHELL" ]]; then
        # We are in new subshell, don't take commands from outer shell.
        trap_subshell[$sig_num]="$BASH_SUBSHELL"
        local old=
    else
        # Get raw trap output.
        local old="$(trap -p "$sig")"

        # Extract previous commands from trap output.
        old="${old#*\'}"          # Remove first ' and everything before it.
        old="${old%\'*}"          # Remove last  ' and everything after  it.
        old="${old//\'\\\'\'/\'}" # Replace every '\'' (escaped ') to just '.
    fi

    # Combine new and old commands. Separate them by newline.
    trap -- "$new
$old" "$sig"
}

trap_subshell=

trap_add 'echo AAA'      EXIT
trap_add '{ echo BBB; }' 0 # The same as EXIT.

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.

Arrays

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

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).

Simple ways to do it

  1. If all the signal handling functions are known at the same time, then the following is sufficient (has said by Jonathan):

    trap 'handler1;handler2;handler3' EXIT
    
  2. Else, if there is an existing handler(s) that should stay, then new handlers can easily be added like this:

    trap "$( trap -p EXIT | cut -f2 -d \' );newHandler" EXIT
    
  3. If you don't know if there are existing handlers but want to keep them in this case, do the following:

 handlers="$( trap -p EXIT | cut -f2 -d \' )"
 trap "${handlers}${handlers:+;}newHandler" EXIT
  1. It can be factorized in a function like that:
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 as long as the handlers are function names, or simple instructions that did not contain any simple code (simple code conflicts with cut -f2 -d \').

I add a slightly more robust version of Laurent Simon's trap-add script:

  • Allows using arbitrary commands as trap, including such with ' characters
  • Works only in bash; It could be rewritten with sed instead of bash pattern substitution, but that would make it significantly slower.
  • Still suffers from causing unwanted inheritance of the traps in subshells.

trap-add () {
    local handler=$(trap -p "$2")
    handler=${handler/trap -- \'/}    # /- Strip `trap '...' SIGNAL` -> ...
    handler=${handler%\'*}            # \-
    handler=${handler//\'\\\'\'/\'}   # <- Unquote quoted quotes ('\'')
    trap "${handler} $1;" "$2"
}

I would like to propose my solution of multiple trap functions for simple scripts

# Executes cleanup functions on exit
function on_exit {
    for FUNCTION in $(declare -F); do
        if [[ ${FUNCTION} == *"_on_exit" ]]; then
            >&2 echo ${FUNCTION}
            eval ${FUNCTION}
        fi
    done
}
trap on_exit EXIT

function remove_fifo_on_exit {
    >&2 echo Removing FIFO...
}

function stop_daemon_on_exit {
    >&2 echo Stopping daemon...
}

Just like to add my simple version as an example.

trap -- 'echo "Version 1";' EXIT;

function add_to_trap {
    local -a TRAP;
    # this will put parts of trap command into TRAP array
    eval "TRAP=($(trap -p EXIT))";
    # 3rd field is trap command. Concat strings.
    trap -- 'echo "Version 2"; '"${TRAP[2]}" EXIT;
}

add_to_trap;

If this code is run, will print:

Version 2
Version 1

Here's how I usually do it. It's not much different from what other people have suggested here but my version seems dramatically simpler and so far it always worked as desired for me.

Somewhere in the code, set a trap:

trap "echo Hello" EXIT

and later on update it:

oldTrap=$(trap -p EXIT)
oldTrap=${oldTrap#"trap -- '"}
oldTrap=${oldTrap%"' EXIT"}; 
trap "$oldTrap; echo World" EXIT

finally, on exit

Hello
World

A special case of Richard Hansen's answer (great idea). I usually need it for EXIT traps. In such case:

extract_trap_cmd() { printf '%s\n' "${3-}"; }
get_exit_trap_cmd() {
    eval "extract_trap_cmd $(trap -p EXIT)"
}

...
trap "echo '1  2'; $(get_exit_trap_cmd)" EXIT
...
trap "echo '3  4'; $(get_exit_trap_cmd)" EXIT

Or this way, if you will:

add_exit_trap_handler() {
    trap "$1; $(get_exit_trap_cmd)" EXIT
}
...
add_exit_trap_handler "echo '5  6'"
...
add_exit_trap_handler "echo '7  8'"

Always assuming I remember to pass multiple code snippets in semi-colon delimited fashion (as per bash(1)s' requirement for multiple commands on a single line, it's rare that the following (or something similar to it) fails to fulfil my meagre requirements...

extend-trap() {
    local sig=${1:?'Der, you forgot the sig!!!!'}
    shift
    
    local code s ; while IFS=\' read code s ; do
        code="$code ; $*"
    done < <(trap -p $sig)

    trap "$code" $sig
}

I'd like something simpler... :)

My humble contrib:

#!/bin/bash

# global vars
TRAPS=()

# functions
function add_exit_trap() {     TRAPS+=("$@")     }

function execute_exit_traps() {
   local I
   local POSITIONS=${!TRAPS[@]}  # retorna os índices válidos do array
   for I in $POSITIONS
   do
        echo "executing TRAPS[$I]=${TRAPS[I]}"
        eval ${TRAPS[I]}
   done
}

#    M A I N

LOCKFILE="/tmp/lock.me.1234567890"
touch $LOCKFILE

trap execute_exit_traps EXIT

add_exit_trap "rm -f $LOCKFILE && echo lock removed."
add_exit_trap "ls -l $LOCKFILE"
add_exit_trap "echo END"

echo "showing lockfile..."
ls -l $LOCKFILE

add_exit_trap() keeps adding strings (commands) to a bash global array while execute_exit_traps() just loops thru that array and eval the commands

Executed script...

showing lockfile...
-rw-r--r--. 1 root root 0 Mar 24 10:08 /tmp/lock.me.1234567890
executing TRAPS[0]=rm -f /tmp/lock.me.1234567890 && echo lock removed.
lock removed.
executing TRAPS[1]=ls -l /tmp/lock.me.1234567890
ls: cannot access /tmp/lock.me.1234567890: No such file or directory
executing TRAPS[2]=echo END
END

This is a simple and compact solution to run multiple trap's by executing all functions that start with the name trap_:

trap 'eval $(declare -F | grep -oP "trap_[^ ]+" | tr "\n" ";")' EXIT

Now simply add as many trap functions as you like::

# write stdout and stderr to a log file
exec &> >(tee -a "/var/log/scripts/${0//\//_}.log")
trap_shrink_logs() { echo "$(tail -n 1000 "/var/log/scripts/${0//\//_}.log")" > "/var/log/scripts/${0//\//_}.log" }

# make script race condition safe
[[ -d "/tmp/${0//\//_}" ]] || ! mkdir "/tmp/${0//\//_}" && echo "Already running!" && exit 1
trap_remove_lock() { rmdir "/tmp/${0//\//_}"; }

A simple solution is to save commands for trap to variable and, when adding new trap, to restore them from that variable.

trap_add()
{
    # Combine new and old commands. Separate them by newline.
    trap_cmds="$1
$trap_cmds"
    trap -- "$trap_cmds" EXIT
}

trap_add 'echo AAA'
trap_add '{ echo BBB; }'

Unfortunately this solution does not work well with subshells, because subshell inherits outer shell variables and thus outer shell trap commands are executed in subshell.

trap_add 'echo AAA'
( trap_add 'echo BBB'; )

In the above example echo AAA is executed twice: first time in subshell and second time in outer shell.

We have to check whether we are in new subshell and if we are then we must not take commands from the trap_cmds variable.

trap_add()
{
    # Avoid inheriting trap commands from outer shell.
    if [[ "${trap_subshell:-}" != "$BASH_SUBSHELL" ]]; then
        # We are in new subshell, don't take commands from outer shell.
        trap_subshell="$BASH_SUBSHELL"
        trap_cmds=
    fi

    # Combine new and old commands. Separate them by newline.
    trap_cmds="$1
$trap_cmds"
    trap -- "$trap_cmds" EXIT
}

Note that to avoid security issue you have to reset the used variables at script startup.

trap_subshell=
trap_cmds=

Otherwise someone who runs your script can inject their malicious commands via environment variables.

export trap_subshell=0
export trap_cmds='echo "I hacked you"'
./your_script

Generic version that works with arbitrary signals is below.

# Check if argument is number.
is_num()
{
    [ -n "$1" ] && [ "$1" -eq "$1" ] 2>/dev/null
}

# Convert signal name to signal number.
to_sig_num()
{
    if is_num "$1"; then
      # Signal is already number.
      kill -l "$1" >/dev/null # Check that signal number is valid.
      echo    "$1"            # Return result.
    else
      # Convert to signal number.
      kill -l "$1"
    fi
}

trap_add()
{
    local cmd sig sig_num

    cmd="$1"
    sig="$2"
    sig_num=$(to_sig_num "$sig")

    # Avoid inheriting trap commands from outer shell.
    if [[ "${trap_subshell[$sig_num]:-}" != "$BASH_SUBSHELL" ]]; then
        # We are in new subshell, don't take commands from outer shell.
        trap_subshell[$sig_num]="$BASH_SUBSHELL"
        trap_cmds[$sig_num]=
    fi

    # Combine new and old commands. Separate them by newline.
    trap_cmds[$sig_num]="$cmd
${trap_cmds[$sig_num]}"

    trap -- "${trap_cmds[$sig_num]}" $sig
}

trap_subshell=
trap_cmds=

trap_add 'echo AAA'      EXIT
trap_add '{ echo BBB; }' 0 # The same as EXIT.

PS In this answer I implemented another solution that gets previous commands from trap -p output.

There's two actually correct answers.

Answer #1: Create a subshell.

## some code that may contain other traps

(
  trap 'whatever' EXIT HUP INT ABRT TERM
  somecommand
) # 'whatever' executed when leaving the subshell.
# Previous trap(s) restored.

Do note the documentation about traps inheritance:

Signals that were ignored on entry to a non-interactive shell cannot be trapped or reset, although no error need be reported when attempting to do so.

Answer #2: Save and restore traps.

When the trap command is invoked without arguments, it will print entire list of traps formatted for shell re-evaluation. Thus, if subshell is not an option, something like this could be done:

save_traps=$(trap)
trap 'something' HUP INT ABRT TERM
somecommand
eval "$save_traps"

But the root cause of the OP is most likely an XY problem. Suppose you have various commands called in different modules of your script and producing residue temp files across filesystem. Being a good citizen, you want to cleanup after yourself. And you employ trap to call cleanup tack. Instead of trying to stack traps, stack your cleanup list:

_cleanup_list=
_cleanup() {
    case "$1" in
        add) 
            shift
            for file; do _cleanup_list="${_cleanup_list:+$_cleanup_list }'$file'"; done
            ;;
        clear)
            for file in $_cleanup_list; do eval "[ -f $file ] && rm $file"; done
            ;;
    esac
}

trap '_cleanup clear' EXIT
Related