Skip to content

When discovering Bash one of the missing features is not try catch block, but try finally block.

See also with section of documentation. It has some function build on top of L_finally.

The issues with raw trap:

  • Not easy to append actions to trap.
    • (This is something I crudely wanted to solve with L_trap_push and L_trap_pop with unmanageable results)
  • On which signals you want to execute? EXIT? INT? TERM? HUP?
  • EXIT trap signal is not executed always and is not executed in command substitutions. Or it is? It depends.
  • If you register action on EXIT and INT trap, it might execute twice, or not.
    • Trapped SIGINT does not exit. Calling exit inside SIGINT trap changes exit status.
  • The signal exit status is hard to preserve.
    • The proper way is to trap - SIGINT; kill -SIGINT $BASHPID to have the Bash process properly exit with the signal cause.
    • The trap and kill is different between signals.
  • There is one RETURN trap shared across all functions. Inner functions overwrite outer functions RETURN trap.

Features:

  • Execute something when Bash exits. Always.
    • L_finally something
  • Execute something when a function returns or Bash exits, whichever comes first.
    • func() { L_finally -r something; }
  • Execute something on signal and continue execution after it.
    • L_finally; trap 'something' USR1
  • If 2 signals are received during trap handler execution, terminate execution with a friendly message.
  • Registered actions execute in reverse order.
  • Remove the last registered action without execution.
    • L_finally_pop -n
  • Remove and execute the last registered action.
    • L_finally_pop
  • Critical section delays the execution of signal handlers.
    • L_finally_critical_section func
  • The signal exit status as reported by WIFSIGNALED should be preserved.

How L_finally works?

  • Registers trap on all signals that result in process termination.
  • Keeps a list of actions to execute on exit.
  • When receiving a signal, all exit actions are executed.
  • When receiving a RETURN trap:
    • Execute the RETURN traps for the function that is returning.
    • The traps are removed the exit traps.

Usage notes:

  • Inside the L_finally handler:
    • Variable L_SIGNAL is set to the received signal name.
    • Variable L_SIGNUM is set to the received signal number.
    • Variable L_SIGRET is set to the value of $? when trap is expanded.
    • It is not allowed to call return on top level. This would just return from the L_finally handler.
  • If you want to provide your own signal handlers, it is important to first call L_finally without or with arguments. The first call to L_finally will register the signal handlers for this BASHPID! They will not be re-registered later, so they allow to overwrite the handler.
  • Consider using L_eval to properly escape arguments when evaluating them.

Examples

tmpf=$(mktemp)
L_finally rm -rf "$tmpf"
: do something to tmpf >"$tmpf"

if [[ -n "$option" ]]; then
  tmpf2=$(mktemp)
  L_finally rm -rf "$tmpf2"
  : we need another tmpf2 >"$tmpf2"
  : it is ok, we can remove it now
  L_finally_pop
if

# tmp will be removed on the end of the script.

Function return example:

option_func() {
  local tmpf=$(mktemp)
  L_finally -r rm -rf "$tmpf"
  echo Do something with temporary file >"$tmpf"

  # exit 1         # this will remove the tempfile
  # return 1       # this will also remove the tempfile
  # kill $BASHPID  # this will also remove the tempfile
  # the tempfile is automatically removed on the end of function
}

main() {
  tmpf=$(mktemp)
  L_finally -r rm -rf "$tmpf"
  if [[ -n "$option" ]]; then
    option_func
  fi
}

Custom action on signal:

increase_volume() { : your function to increase volume; }

L_finally                    # with no arguments, just registers the action on all traps for this BASHPID.
# kill -USR1 $BASHPID        # would terminate the process executing L_finally_run
trap 'increase_volume' USR1
kill -USR1 $BASHPID          # will increase volume and continue execution

Implementation notes

There have been multiple iterations of the design with multiple arrays. Bottom line, the idea was that "choosing" which traps to execute should be as fast as possible.

The simplest design with just two array of commands to execute turned out to be most effective and efficient and easy. Each element has an index, can be easily removed and navigate.

The only downside is that during L_finally_pop the code needs to find the index of RETURN array element connected to the EXIT array element. This is a simple loop. It is still faster than any ${//} ${##} parsing I have come up with before this design.

Users do not need an array on custom signals. I decided there is little need for "expanding" the features of this library into a ultra-signal-manager. 99.9% of the time I require a simple "try finally" block, nothing more, nothing less, and this covers most usages.

Generated documentation from source:

finally

Properly preserve exit status for parent processes. https://www.cons.org/cracauer/sigint.html Re-signaling does not work properly on specific signals. Re-signaling does not work properly in subshells and subshell exits with 0.

Array of commands to execute on EXIT.

_L_finally_arr=()

Array of commands to execute on function returns.

When a function returns at stack depth given by ${#BASH_LINENO[@]} the _L_finally_return[${#BASH_LINENO[@]}] should be executed. _L_finally_return=()

BASHPID that registered traps.

_L_finally_pid=""

Holds signal received inside a critical section.

[0] - signal name [1] - signal number _L_finally_pending=()

Currently handled signal name.

Special values: RETURN EXIT POP NONE POP - when calling from L_finally_pop NONE - used inside critical section. L_SIGNAL=""

Currently handled signal number.

Unset when handling RETURN or POP. 0 for EXIT trap. L_SIGNUM=""

The value of $? as expanded by trap.

L_SIGRET=""

L_finally_handle_return

L_finally RETURN handler.

Arguments:

  • $1 The value of $?.
  • $2 The value of $BASH_COMMAND.

L_finally_handle_exit

L_finally EXIT handler.

L_finally_handle_signal

L_finally signal handler.

Arguments:

  • $1 The trap signal name to handle.
  • $2 The trap signal number to handle.

L_finally_list

List elements registered by L_finally.

L_finally

Register an action to be executed upon termination.

The action will be executed only exactly once, even if the signal is received multiple times. The signal exit code of the program or subshell is preserved. The variable $L_SIGNAL is set to the currently handled signal name and available in action.

Warning

The function assumes full ownership of all trap values.

This is needed to properly set traps accross PIDs and subshells and functions and on Bash below 5.2. Bash below 5.2 does not execute EXIT trap in subshells after receiving a signal, so L_finally trap handler is registered on all possible signals. The signal exit status is preserved.

Example

tmpf=$(mktemp)
L_finally rm "$tmpf"

calculate_something() {
   local tmpf
   tmpf=$(mktemp)
   L_finally -r rm "$tmpf"
   echo use tmpf >"$tmpf"
   # tmpf automatically cleaned up once on RETURN or EXIT or signal, whichever comes first.
}

Options:

  • -r

    Set trace attribute on the function and add RETURN trap to register the function on.

    This does not work correctly with source and instead source RETURN trap will execute parent scope actions. To mitigate this, wrap source in a function, for example use L_source.

  • -s <int>

    Increment the stack offset for the RETURN trap by this number.

    The RETURN trap handler will execute the action only if called from the nth position in the stack relative to the current position. Default: 0

  • -l

    Add action to be executed last, not first of the stack.

    Calling L_finally_pop after registering such action is undefined.

  • -R Force reregister all the traps. Unless this option, traps are only registered on the first call of a BASHPID.
  • -v <var>

    Store the action index in the variable.

    This index can be used with L_finally_pop -i to remove the action.

  • -h Print this help and return 0.

Argument: $@

Action to execute. When action is missing, then only traps are registered.

The command may not call eval 'return'. It would just return from the handler function.

Shellcheck disable= SC2089 SC2090

See: L_finally_pop

L_finally_pop

Execute and unregister the last action registered with L_finally.

Options:

  • -n Do not execute the action, only remove.
  • -i <index> Remove action of index .
  • -h Print this help and return 0.

Return:

1 if nothing was popped, 2 on invalid usage,

otherwise return the exit status of the executed action.

See: L_finally

L_finally_critical_section

Execute a command inside a critical section.

The signals will be raised after the command is finished. Prerequisite: L_finally has registered signal handlers.

Argument: $@ Command to execute.