8

I am looking for an idiomatic pattern to use traps as a meaning "ensure" or "finally", so they should execute no matter how a bash function exits.

I've found the RETURN trap, but nesting that is not trivial. I guess the trap inside bar shadows the trap in foo. How can I set up traps for both foo and bar

So given the following code, I'd like to have cleanup when exiting foo and exiting bar.

My first attempt was to have a global string storing the cleanup tasks, but that one breaks whenever you have subshells. And it seems from the output of the following code, that RETURN signal is also "shell-global"

fooclear() { echo fooclear; }
barclear() { echo barclear; }
bar() {
  echo bar
  trap barclear RETURN INT ERR TERM EXIT
  sleep 1
}

foo() {
  echo foo
  sleep 1
  trap fooclear RETURN INT ERR TERM EXIT
  bar
}

for i in 1 2 3; do
  foo &
done
wait

And a warning: to avoid duplicate handling, one must somehow administer that a clearing function has already completed.

3 Answers 3

5

I can't find this documented anywhere, but it seems like adding:

trap - RETURN

as the last command in the trap handler, causes the trap to revert to the previous one (bash is keeping a stack of RETURN handlers somewhere. The documentation says that in general trap - {SIGSPEC} causes the trap to revert to the default; I guess in this case, the default is the 'shadowed' trap command.

2
  • 1
    It's more complex than this. Calling trap - [SIG_SPEC] doesn't revert to previous trap, it reverts to "the value it had upon entrance to the shell". This could be blank, or inherited from the parent shell. It wil not revert to one you previously set in the current shell. There is no trap stack other than that created each time you spawn a subshell. In other words if you set a trap for a given SIG_SPEC and then set it again the first one is replaced by the second. Commented Sep 1, 2021 at 20:43
  • But could you make function run in subshell (like, func() ( … )) then? Commented Jul 13, 2024 at 8:31
3

Correct, RETURN signal is shell-global, as are all of the signals. Trapping any signal replaces any trap already set on that signal which is probably what you mean by "shadows".

Traps afford some simple exception handling but for something more robust I'd check the answers to this stackoverflow question about TRY/CATCH

0

I really missed the "finally" functionality in Bash, so I decided to implement.

Bottom line, there is one single trap shared by the process. If you set trap something RETURN in one function, another function will also execute something. The traps are process wide, global, for all functions.

Thus, we have to implement it ourselves to call proper callback depending on which function has just returned. We know which function returned by inspecting BASH_SOURCE, BASH_LINENO or FUNCNAME. Thus in my L_lib library I implemented L_finally function.

Below you might find a stripped down implementation. The elements to execute are just stored in a global array.

finally_pid=""
finally_on_return=()
finally_on_exit=()
finally_handle_return() {
   local i IFS=' '
   for i in ${finally_on_return[${#BASH_SOURCE[*]}]}; do
      eval "${finally_on_exit[i]}"
      unset -v 'finally_on_exit[$i]'
   done
   unset -v 'finally_on_return[${#BASH_SOURCE[*]}]'
}
finally_handle_exit() {
   # execute on_exit traps in reverse order
   for ((i=${#finally_on_exit[*]}-1; i>=0; --i)); do
      eval "${finally_on_exit[i]}"
   done
   # Even if EXIT trap is called after INT trap, it will not re-execute.
   finally_on_exit=()
}
finally() {
   local cmd
   # Reset arrays in subshells, so that they are not inherited
   if ((finally_pid != BASHPID)); then
      finally_pid=$BASHPID
      finally_on_return=()
      finally_on_exit=()
      # register the traps
      trap finally_handle_exit EXIT INT TERM
      trap finally_handle_return RETURN
   fi      
   # Properly quote command for eval.
   printf -v cmd "%q " "$@"
   # Add the return trap. Store the index of on_exit trap to execute.
   # The RETURN trap executes at specific "depth" of RETURN,
   # simply indexed by the number of elements in BASH_SOURCE.
   finally_on_return[${#BASH_SOURCE[*]}-1]+=" ${#finally_on_exit[@]}"
   # Add to the exit trap.
   finally_on_exit+=("$cmd;")
   # Attach trace attribute to function so it executes RETURN handler when functrace is not enabled.
   declare -f -t "${FUNCNAME[1]}"
}

############

fooclear() { echo fooclear; }
barclear() { echo barclear; }
bar() {
  finally barclear
  echo bar
}

foo() {
  finally fooclear
  echo foo
  bar
}

foo

The code outputs:

foo
bar
barclear
fooclear

The "barclear" and "fooclear" are executes on RETURN from foo and bar functions. You might want to insert exit at various points in the code and see how EXIT trap will execute.

The code above might be improved in several ways - introducing critical sections with delayed signals, attaching RETURN trap to function higher in the stack, properly preserving exit status when receiving a signal, handling set -e, popping last registered action or popping action with specific index.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.