20

What is a good way to override bash trap handlers that don't permanently trample existing ones that may or may not already be set? What about dynamically managing arbitrary chains of trap routines?

Is there a way to save the current state of the trap handlers so they can be restored later?

codeforester
  • 39,467
  • 16
  • 112
  • 140
Iron Savior
  • 4,238
  • 3
  • 25
  • 30
  • 2
    Not directly related to your question, but the [bash-preexec](https://github.com/rcaloras/bash-preexec) library lets you register multiple callbacks to the `DEBUG` trap via the `preexec_functions` array. – dimo414 May 16 '17 at 21:24
  • My implementation: https://stackoverflow.com/questions/3338030/multiple-bash-traps-for-the-same-signal/50205727#50205727 – Andry May 07 '18 at 00:45

1 Answers1

20

Save and Restore Your Trap Handler State in Bash

I would submit the following stack implementation to track and restore trap state. Using this method, I am able to push trap changes and then pop them away when I'm done with them. This could also be used to chain many trap routines together.

See the following source file (.trap_stack.sh)

#!/bin/bash
trap_stack_name() {
  local sig=${1//[^a-zA-Z0-9]/_}
  echo "__trap_stack_$sig"
}

extract_trap() {
  echo ${@:3:$(($#-3))}
}

get_trap() {
  eval echo $(extract_trap `trap -p $1`)
}

trap_push() {
  local new_trap=$1
  shift
  local sigs=$*
  for sig in $sigs; do
    local stack_name=`trap_stack_name "$sig"`
    local old_trap=$(get_trap $sig)
    eval "${stack_name}"'[${#'"${stack_name}"'[@]}]=$old_trap'
    trap "${new_trap}" "$sig"
  done
}

trap_pop() {
  local sigs=$*
  for sig in $sigs; do
    local stack_name=`trap_stack_name "$sig"`
    local count; eval 'count=${#'"${stack_name}"'[@]}'
    [[ $count -lt 1 ]] && return 127
    local new_trap
    local ref="${stack_name}"'[${#'"${stack_name}"'[@]}-1]'
    local cmd='new_trap=${'"$ref}"; eval $cmd
    trap "${new_trap}" "$sig"
    eval "unset $ref"
  done
}

trap_prepend() {
  local new_trap=$1
  shift
  local sigs=$*
  for sig in $sigs; do
    if [[ -z $(get_trap $sig) ]]; then
      trap_push "$new_trap" "$sig"
    else
      trap_push "$new_trap ; $(get_trap $sig)" "$sig"
    fi
  done
}

trap_append() {
  local new_trap=$1
  shift
  local sigs=$*
  for sig in $sigs; do
    if [[ -z $(get_trap $sig) ]]; then
      trap_push "$new_trap" "$sig"
    else
      trap_push "$(get_trap $sig) ; $new_trap" "$sig"
    fi
  done
}

This can manage handlers that are defined as named functions and also ad-hoc routines defined like this example trap "kill $!" SIGTERM SIGINT.

This is the test script I used to help me write it:

#!/bin/bash
source .trap_stack.sh

initial_trap='echo "messy" ;'" echo 'handler'"
non_f_trap='echo "non-function trap"'
f_trap() {
  echo "function trap"
}

print_status() {
  echo "    SIGINT  trap: `get_trap SIGINT`"  
  echo "    SIGTERM trap: `get_trap SIGTERM`"
  echo "-------------"
  echo
}

echo "--- TEST START ---"
echo "Initial trap state (should be empty):"
print_status

echo 'Setting messy non-function handler for SIGINT ("original state")'
trap "$initial_trap" SIGINT
print_status

echo 'Pop empty stacks (still in original state)'
trap_pop SIGINT SIGTERM
print_status

echo 'Push non-function handler for SIGINT'
trap_push "$non_f_trap" SIGINT
print_status

echo 'Append function handler for SIGINT and SIGTERM'
trap_append f_trap SIGINT SIGTERM
print_status

echo 'Prepend function handler for SIGINT and SIGTERM'
trap_prepend f_trap SIGINT SIGTERM
print_status

echo 'Push non-function handler for SIGINT and SIGTERM'
trap_push "$non_f_trap" SIGINT SIGTERM
print_status

echo 'Pop both stacks'
trap_pop SIGINT SIGTERM
print_status

echo 'Prepend function handler for SIGINT and SIGTERM'
trap_prepend f_trap SIGINT SIGTERM
print_status

echo 'Pop both stacks thrice'
trap_pop SIGINT SIGTERM
trap_pop SIGINT SIGTERM
trap_pop SIGINT SIGTERM
print_status

echo 'Push non-function handler for SIGTERM'
trap_push "$non_f_trap" SIGTERM
print_status

echo 'Pop handler state for SIGINT (SIGINT is now back to original state)'
trap_pop SIGINT
print_status

echo 'Pop handler state for SIGTERM (SIGTERM is now back to original state)'
trap_pop SIGTERM
print_status
Iron Savior
  • 4,238
  • 3
  • 25
  • 30
  • 2
    +1 for spunk in answering your own question AND for rather exhaustive solution. Good luck to all. – shellter Apr 20 '13 at 02:09
  • 1
    Thanks. I spent a good deal of my Friday trying to figure out how to do this without resorting to `eval`. In the end, I never found an eval-free (that was also satisfactory) way to parse the output of `trap -p`, so I wound up caving on trying to find a way to assign to dynamically-named array variables, too. I hope someone has an equivalent 3-line replacement and we'll hear from that person. – Iron Savior Apr 20 '13 at 02:51
  • 1
    Sorry, won't be able to spend my Friday looking for a 3-line replacement for this ;-), but don't you think the hype about "never use eval" is a bit overblown? You're not processing user input in those variables are you? I think eval, like goto has it's limited place in the programmers toolbox. Or do you dislike because it might be (is?) spawning an extra process? Good luck to all. – shellter Apr 20 '13 at 03:00
  • I agree that "never use eval" is too often held up as a hard-and-fast rule for all situations. The hard fact of the matter is that bash, as a language, has some shortcomings that simply require eval as a workaround. A person should always _scrutinize_ what they feed to `eval`, but you can't always outright avoid it. – Iron Savior Apr 20 '13 at 03:08
  • In the source file here, eval is only given input by a script author that uses it to manage traps. It uses eval to parse the output of `trap -p` and it uses a given signal name to form part of some variable names (which is then filtered to the `[a-zA-Z0-9_]` set because some signals have names that aren't legal). I thought about using an associative array (bash has those, right?), but the prospect of having arrays of arrays in bash was horrifying. It doesn't bother me that it is spawning an extra process for this purpose. I wanted to avoid eval because of the stigma and the challenge. – Iron Savior Apr 20 '13 at 03:33
  • 1
    Interesting. My traps are normally along the lines of `trap 'rm -f $tmp.?; exit 1'` 0 1 2 3 13 15` where `$tmp` is the basic name used for temporary files in a script. Because of the `exit` in that trap, you can't meaningfully append to it (not that I can think of many places where I'd want to), though clearly, your 'prepend' option works in this case. So, maybe this is just overkill for the problems I use `trap` to handle, but it is a tour de force in shell programming. – Jonathan Leffler Mar 26 '15 at 00:06
  • This is a fantastic answer and helped me immensely. Thanks! – e40 Oct 29 '15 at 23:35
  • @IronSavior, thanks! To make the script work even when `set -o nounset` consider adding `test -v "${stack_name}" || eval "${stack_name}=()"` before the `eval` in `trap_push()`. – Piotr Findeisen Apr 18 '16 at 12:16