Every unattended script needs the same five things before it does any work: strict mode, an error trap that says which line failed, cleanup that runs however the script exits, a lock so cron cannot start two copies, and a timeout so a hung command cannot hold that lock forever. Most scripts get two of the five, copied from the last script and slightly different each time. The copy that is missing the -E, or registers its cleanup inside a subshell, fails quietly in production and nowhere else.
This file is the five, as ten functions, in one place you source. It is the same file the email form on this site sends you to, and it lives in the scripts repo beside a test that exercises every function on its failure path.
The Library
How do I use it in a script?
Put the file beside your script, source it with a path relative to the script, and call enable_strict_traps before any real work:
I ran exactly this on my machine: it logged [INFO] fetched 559 bytes and exited 0, and afterwards neither the temp file nor the lock directory existed.
What does each function protect you from?
| Function | What goes wrong without it |
|---|---|
enable_strict_traps | A failure one level down exits with no message, or the script keeps running past it |
register_temp | A path you created survives every crash and piles up in /tmp |
make_temp_file, make_temp_dir | Predictable temp names collide between runs and invite symlink tricks |
acquire_lock | Cron starts a second copy while the first is still working |
run_with_timeout | A hung network call blocks the script and its lock forever |
retry | One transient failure kills a job you then re-run by hand |
log | Output with no timestamp, mixed into the data on stdout |
die | Error paths that exit without saying why |
require_cmd | A missing tool discovered halfway through, after the script has changed things |
Why does the ERR trap need -E?
Without -E (errtrace), bash does not pass the ERR trap into functions. set -e still kills the script, so the failure is not ignored, but the trap that was supposed to name the failing command never runs and the log holds an exit code and nothing else. Most copies of the strict-mode line online are set -euo pipefail, which is exactly that. The first check in the test runs ls on a missing path inside a function; with -E the log says failed (exit 2) at line 5: ls /nonexistent-bashlib-test.
Why does make_temp_file still clean up inside $( )?
tmp="$(make_temp_file)" runs the function in a subshell. Anything the subshell adds to an array disappears when the subshell exits, so a library that only keeps its cleanup list in an array loses every path created this way, and the file survives the script. enable_strict_traps therefore also creates a private registry file, and every registration is appended to it. A line written to a file survives the subshell; the EXIT trap reads it back and removes both the paths and the registry. If you prefer to avoid the subshell entirely, make_temp_file tmp sets the variable directly.
How do I know it works?
Run the test beside the library. It generates small scripts that source the library and then break them on purpose: errors inside functions, set -e aborts, exit 3, SIGTERM in the middle of a run, a second copy fighting for the lock, a lock left by a dead process, a command that hangs, a command that fails twice before succeeding. On my machine, Kali 2026.3 with bash 5.3.9, all 31 checks pass:
It has not been run anywhere else yet, and run_with_timeout needs GNU timeout, which macOS does not ship. Run the test on your own system before you trust it there.
Can I use it in cron jobs?
That is what it is for. Cron starts scripts with a minimal environment and an unpredictable working directory, which is why the library is sourced relative to the script rather than the current directory. Send the log somewhere with LOG_FILE:
A run that fails leaves a timestamped line naming the command and line number; a run that is still going when the next one starts is refused by acquire_lock with the PID that holds it.
Frequently Asked Questions
How do I use a bash function library in my own script?
Source it with a path relative to your script, then turn on strict mode before doing any work: source "$(dirname "${BASH_SOURCE[0]}")/bashlib-starter.sh" followed by enable_strict_traps. Sourcing alone changes nothing about your shell, so an existing script keeps behaving the same until you call enable_strict_traps. Running the file directly is refused with a message, because a library executed as a script would do nothing useful.
Why does my ERR trap stay silent when a command fails inside a function?
Because by default bash does not pass the ERR trap into functions, command substitutions or subshells. set -e still stops the script, but the trap that was supposed to print the failing command never runs, so the log shows an exit code and nothing else. The fix is the E in set -Eeuo pipefail (errtrace). enable_strict_traps sets it, and the test checks that a failing ls inside a function is reported with its line number.
Does the cleanup still run if the script is killed?
For SIGTERM and Ctrl-C, yes: bash runs the EXIT trap on the way down, and the test kills a running copy with SIGTERM and confirms the exit code is 143 and every temp path is gone. SIGKILL (kill -9) cannot be caught by any process, so nothing runs; that is why the temp paths live under /tmp, where a reboot or tmpfiles clears them.
Will it work on macOS?
It has only been tested on Kali Linux with bash 5.3. run_with_timeout calls the GNU timeout command, which macOS does not ship; install coreutils or skip that one function. The rest uses bash built-ins, mktemp, mkdir and ps, which macOS has, but untested is untested: run bash lib/bashlib-starter.test.sh on your Mac before you rely on it.
How is this different from the Production Bash Toolkit?
The toolkit's bashlib.sh has 31 functions, including coloured leveled logging, safe_rm, notifications and portability helpers, plus backup, healthcheck, cleanup and cron-wrapper scripts built on it. Nine of the ten functions here share their names and arguments with it, so a script written against the starter keeps working when you switch. run_with_timeout is starter-only; the toolkit handles timeouts in cron-wrapper.sh.
Part of the bash snippets collection
Related Scripts
- Bash Error Handling with set -euo pipefail — what each strict-mode flag does, one at a time
- Bash trap: Clean Up Temp Files on Exit — the EXIT-trap and atomic-mv pattern on its own
- Prevent Overlapping Cron Jobs with flock — the kernel-lock alternative to acquire_lock
- Bash timeout Command — exit 124,
--kill-after, and why functions cannot be timed out - Retry a Command with Exponential Backoff — the retry loop with jitter and logging
- The Safe Bash Script Template — the guide that explains every line of the strict-mode header