DEV Community

Cover image for My Script Crashed and Left a Lock File Behind. Every Run After That Refused to Start.
Anguishe
Anguishe

Posted on • Originally published at bashsnippets.xyz

My Script Crashed and Left a Lock File Behind. Every Run After That Refused to Start.

A backup script of mine created a lock file on startup so two copies couldn't run at once — sensible. Then one night it hit an error partway through, set -e killed it on the spot, and it died without ever reaching the line that removes the lock. The lock file sat there. Every scheduled run for the next three days started up, saw the lock, printed "already running," and exited immediately. No backups ran. The cron job was firing perfectly on time and doing nothing, and the only symptom was an absence — backups that simply weren't there — until I went looking and found a stale lock from Tuesday.

The fix is a trap. A trap registers a cleanup handler that runs when the script exits for any reason — clean finish, set -e failure, Ctrl+C, kill. Put the lock removal in an EXIT trap and it runs no matter how the script dies. The lock would have been gone the instant that backup crashed, and the next run would have started fine.

So why did I write the script without one? Because I could never remember the syntax cold. Single quotes or double? Which signals? Does EXIT fire on exit 1 or only on a clean finish? How do I get the exit code inside the handler? Every time I needed a trap I ended up with three browser tabs open, reading the same Stack Overflow answers, second-guessing the quoting. Under pressure, in the middle of fixing something else, that friction is exactly when people skip the trap entirely — which is how I ended up with the stale lock in the first place.

So I built the thing I kept wishing existed: a Bash trap & Signal Handler Builder. You pick the signals you want to handle, check off the cleanup actions you need, and it writes a correct, ShellCheck-clean trap block you paste into your script. No tabs, no second-guessing the quoting.

It covers the signals that actually come up. EXIT — fires on every exit, the one that should carry your cleanup. ERR — fires when a command fails under set -e, the one that logs the exact failing line with $LINENO. INT — Ctrl+C. TERM — what kill, systemctl stop, and docker stop send. HUP — terminal closed or SSH dropped. PIPE — writing to a closed pipe. Each one has a one-line reminder of when it fires and what it's good for, because half the battle is just remembering that TERM is the one Docker sends.

The cleanup actions are the things people forget until a crash makes them care: remove temp files (with the TMPFILE=$(mktemp) declaration wired in up top), remove a lock file — the exact failure that bit me — stop background jobs the script started, log the exit reason with the code, and restore the terminal cursor. Tick the ones you need and they land in the handler.

The generated code isn't a toy snippet. It single-quotes the trap so the handler resolves when the signal fires instead of at definition time — the SC2064 gotcha most hand-written traps get wrong. It includes an idempotency guard so that when ERR and EXIT both fire on the same failure, your cleanup runs exactly once instead of twice. The ERR handler captures $LINENO so you find out which line actually blew up. I ran the generator's output through ShellCheck across a dozen different configurations while building it, and every one comes back clean — the whole point was that you can paste it and trust it, not paste it and then go debug the thing that was supposed to save you debugging.

There are two copy buttons, because there are two situations. "Copy trap block only" when you've already got a script and just want to drop the traps in. "Copy complete script header" when you're starting fresh and want the shebang, set -euo pipefail, the CHECK/CROSS vars, the resource declarations, and the handler all assembled in the right order. It only declares the variables the generated code actually uses, so you never paste in a CROSS you never reference and get a ShellCheck warning for your trouble.

The lock-file incident cost me three days of silently missing backups and ten minutes of feeling foolish when I found the cause. The trap that would have prevented it is four lines. The reason I didn't have those four lines was pure friction — I couldn't recall the syntax fast enough to be bothered in the moment. This tool removes the friction, which is the only thing that was ever standing between me and a correct script.

Build your trap block here — pick signals, pick cleanup actions, copy clean code: https://bashsnippets.xyz/tools/bash-trap-builder

If you want the full picture of where traps fit — strict mode, cleanup, the failure modes that make them necessary — the Bash Error Handling snippet is the companion read, and the rest of the tools are at https://bashsnippets.xyz/tools

Top comments (0)