L_finally: Robust Cleanup & Signal Handling¶
The L_finally library provides a robust "try-finally" mechanism for Bash scripting, addressing the limitations and complexities of raw trap commands. It ensures that cleanup actions are reliably executed, whether a script exits normally, a function returns, or a signal is received.
For advanced usage involving resource management contexts, see the with section.
Why not just use trap?¶
Using the native trap builtin correctly in complex scripts is notoriously difficult:
- Stacking is hard: Appending multiple actions to a trap without overwriting existing ones is clumsy.
- Signal Complexity: Deciding which signals to trap (EXIT, INT, TERM, HUP) and handling their differences is error-prone.
- EXIT vs. Signals: The
EXITtrap behavior varies (e.g., inside command substitutions) and doesn't always catch termination signals. - Double Execution: Registering for both
EXITand specific signals can lead to actions running twice (or not at all). - Exit Status: Preserving the correct exit code (especially
128 + SIGNUMfor signals) requires boilerplate code (trap - SIG; kill -SIG $$). - Function Scope: There is only one global
RETURNtrap. Inner functions overwriting it break the outer function's cleanup logic.
L_finally abstracts these problems away, giving you a clean, stack-based API.
Key Features¶
- Guaranteed Execution: Actions registered with
L_finallyrun when the script exits, no matter the cause (success, error, or signal). - Function-Scoped Cleanup: Use
L_finally -rto bind cleanup to the current function's return. - Stack-Based Order: Actions execute in reverse order of registration (LIFO), ensuring dependencies are cleaned up correctly (e.g., delete file before removing directory).
- Manual Control:
L_finally_pop: Remove and run the most recent action immediately.L_finally_pop -n: Remove the most recent action without running it (useful for "commit" logic).
- Correct Exit Codes: Preserves the exit status as
WIFSIGNALEDreports, ensuring parent processes know why the script died. - Safety: Detects double-signal loops (e.g., spamming Ctrl+C) and terminates gracefully.
Usage Guide¶
1. Basic Script Cleanup¶
Register commands to run when the script exits.
# Create a temporary file
tmpf=$(mktemp)
# Register cleanup immediately
L_finally rm -f "$tmpf"
# Use the file
echo "data" > "$tmpf"
# ... script continues ...
# When the script ends (or is killed), 'rm -f' runs automatically.
2. Function-Scoped Cleanup (-r)¶
Use the -r flag to execute the action when the function returns.
process_data() {
local work_dir=$(mktemp -d)
# Cleanup work_dir when this function returns
L_finally -r rm -rf "$work_dir"
# Do work...
if [[ -f "$work_dir/error" ]]; then
return 1 # Cleanup runs here
fi
# ...
# Cleanup runs here automatically at end of function
}
3. Conditional Cleanup (Pop)¶
Sometimes you only want to cleanup on failure, or you want to "commit" a result.
prepare_file() {
local tmpf=$(mktemp)
L_finally rm -f "$tmpf" # Remove if we fail early
generate_data > "$tmpf" || return 1 # 'rm' runs if this returns
# Success! Move the file to its final location.
mv "$tmpf" "./final_output.txt"
# We don't need the cleanup anymore, and we don't want to run it.
L_finally_pop -n
}
4. Custom Signal Handlers¶
You can still use custom traps while L_finally manages the rest. Call L_finally (no args) first to initialize the unified handler.
# Initialize L_finally logic for this process
L_finally
increase_volume() { echo "Volume up!"; }
# Register custom handler for specific signal
trap 'increase_volume' USR1
# Usage:
# kill -USR1 $$ -> Prints "Volume up!", script continues.
# kill -TERM $$ -> L_finally cleanup runs, script exits.
Internal Variables¶
Inside an L_finally handler (the command you registered), you can access:
L_SIGNAL: Name of the received signal (e.g.,SIGINT),EXIT, orRETURN.L_SIGNUM: Number of the received signal.L_SIGRET: The exit code ($?) at the moment the trap was triggered.
Note: Do not use
returnin a top-levelL_finallyaction; it only returns from the handler, not the script.
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:
-
$1The value of $?. -
$2The value of $BASH_COMMAND.
L_finally_handle_exit
¶
L_finally EXIT handler.
L_finally_handle_signal
¶
L_finally signal handler.
Arguments:
-
$1The trap signal name to handle. -
$2The 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:
-
-rSet 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
-
-lAdd action to be executed last, not first of the stack.
Calling L_finally_pop after registering such action is undefined.
-
-RForce 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 -ito remove the action. -
-hPrint 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:
-
-nDo not execute the action, only remove. -
-i <index>Remove action of index. -
-hPrint 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.