What you will learn
Trap SIGTERM and SIGINT in a long-running script so it finishes its current unit of work, cleans up temp files, and exits with an accurate status — instead of dying mid-write when systemd or an operator sends a stop signal.
Before you begin
Signal handling in Bash runs between statements, not inside a running builtin or external command — a handler set while sleep 300 is executing only runs once sleep returns or is itself interrupted. Design the main loop with that in mind.
1. Set up a shutdown flag instead of exiting from the trap directly
#!/usr/bin/env bash
set -Eeuo pipefail
SHUTTING_DOWN=0
WORK_DIR="$(mktemp -d)"
on_signal() {
echo "signal received, finishing current unit of work..." >&2
SHUTTING_DOWN=1
}
cleanup() {
rm -rf -- "$WORK_DIR"
echo "cleaned up ${WORK_DIR}"
}
trap on_signal SIGTERM SIGINT
trap cleanup EXIT
Setting a flag rather than calling exit directly from the trap avoids exiting mid-write to a file or mid-transaction against a database — the main loop decides when it's safe to stop.
2. Check the flag at safe boundaries in the main loop
process_queue() {
local queue=(job1 job2 job3 job4 job5)
for job in "${queue[@]}"; do
if (( SHUTTING_DOWN )); then
echo "shutdown requested, stopping before ${job}" >&2
return 1
fi
echo "processing ${job}"
: > "${WORK_DIR}/${job}.done" # unit of work is atomic; safe to stop right after
sleep 2
done
return 0
}
if process_queue; then
echo "queue drained"
else
echo "queue interrupted, will resume unfinished jobs next run"
fi
3. Give a hard deadline for genuinely stuck work
timeout --signal=TERM --kill-after=10s 300s ./run-batch-job.sh
timeout sends TERM at 300s, giving the job a chance to catch it and shut down cleanly via the same trap pattern, then escalates to KILL after another 10s if it ignores the request — a safety net for the case where the trap itself hangs.
Example output
processing job1
processing job2
signal received, finishing current unit of work...
processing job3
shutdown requested, stopping before job4
queue interrupted, will resume unfinished jobs next run
cleaned up /tmp/tmp.XkQ2p9
Verify the result
Start the script, send kill -TERM <pid> mid-loop, and confirm it finishes the in-flight job, skips the rest, and still runs cleanup (check the temp dir is gone).
Troubleshooting
If cleanup never runs, confirm the trap is EXIT (fires on any exit path) and not just SIGTERM — a script that hits set -e and exits on an unrelated error still needs cleanup.
Next steps
Combine with pidfile-heartbeat-supervisor so an external monitor can tell the difference between "shutting down cleanly" and "hung."