LinuxXXXV · Shell Scripting for SysadminsTraps tmpfiles
Traps and temporary files - clean exit on signal or error
What you'll learn
- Use trap for cleanup
- Use mktemp for safe temporary files
- Write atomic file operations
- Avoid common pitfalls with temporary files
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
Scripts that crash mid-execution leave temporary files behind. Trap handlers clean up. Atomic operations prevent partial writes. This lesson covers both.
mktemp
Safe temporary files:
# Create a temporary file
TMPFILE=$(mktemp)
echo "data" > "$TMPFILE"
# Create a temporary directory. Name it anything EXCEPT TMPDIR -
# see the warning below.
workdir=$(mktemp -d)
cp /etc/passwd "$workdir/passwd"
# Create with a template
TMPFILE=$(mktemp -t my-script.XXXXXX)
mktemp creates files with mode 600 and unique names.
Predictable names like /tmp/foo are race-condition risks.
trap for cleanup
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT INT TERM
# Use $workdir...
# Cleanup runs automatically on exit
trap '...' EXIT runs on any exit - success, error, signal.
INT is Ctrl-C. TERM is kill.
Multiple commands in one trap:
cleanup() {
rm -rf "$workdir"
log "Cleaned up"
}
trap cleanup EXIT INT TERM
Atomic file operations
When updating a config file, use atomic writes:
mv is atomic only within one filesystem. Across
filesystems it degrades to copy-then-unlink, and a reader that
opens the file mid-copy sees a truncated config. Bare mktemp
puts the file under $TMPDIR, which is /tmp — a separate tmpfs
on a hardened build — so mv /tmp/x /etc/myapp.conf is exactly
the non-atomic case. Create the staging file in the destination
directory instead:
dst=/etc/myapp.conf
tmp=$(mktemp "${dst}.XXXXXX") # same directory, therefore same filesystem
trap 'rm -f "$tmp"' EXIT
printf '%s\n' "new content" > "$tmp"
# Inherit the real file's identity, not root:root 0600 from mktemp
chown --reference="$dst" "$tmp" 2>/dev/null || true
chmod --reference="$dst" "$tmp" 2>/dev/null || chmod 0644 "$tmp"
command -v restorecon >/dev/null && restorecon "$tmp"
mv -f -- "$tmp" "$dst" # atomic
The three lines before the mv are not decoration. mktemp
creates mode 0600 owned by the caller, so the naive version
silently changes the config’s owner and permissions. On an
SELinux host a file created under /tmp carries the tmp_t
label; move it to /etc and it keeps tmp_t, and the daemon
that reads it is denied — a failure that looks like file
corruption and is actually a label.
After the mv, the destination is either the old file or the new
file, never a partial one.
Some applications can reload config without restart:
mv -f -- "$tmp" "$dst"
systemctl reload myapp
Common pitfalls
- Predictable temp files:
/tmp/my-script.tmpis a race condition. Two instances may write to the same file, and a hostile local user can pre-create it as a symlink. Usemktemp. Lock files are the exception - see the next section: a lock needs a fixed, shared path, so the safety comes fromflock, not from a unique name. - No cleanup: a script that crashes mid-way leaves
files. Use
trap. - Non-atomic writes: writing to the destination directly means a crash mid-write leaves a partial file. Write to temp, then rename.
- Trap misconfiguration:
trap 'rm -rf "$workdir"'(no EXIT) only runs on the signal, not on normal exit. Always include EXIT. - Naming your scratch directory
TMPDIR: it redirects every latermktempinto the directory your trap deletes. - Staging in
/tmpand moving to/etc: that crosses a filesystem boundary, so themvis not atomic and the file arrives with the wrong owner and SELinux label.
Safe lock file
A lock file prevents two instances running simultaneously.
Use flock, and nothing else:
# The path is fixed and shared - that is the point of a lock
exec 9>/run/lock/my-script.lock
flock -n 9 || { echo "Already running"; exit 0; }
# Work here. No trap, no cleanup, no rm.
# The kernel releases the lock when the process dies -
# including on SIGKILL, on a crash, and on power loss.
The fd stays open for the lifetime of the process and the
lock lives on the open file description, not on the file’s
existence. That is why no cleanup is needed and why flock
survives the failure modes a hand-rolled lock does not.
Note the exit 0. A second instance finding the lock held is
usually normal, not an error - exiting non-zero from cron
turns every overlap into an email or a failed-unit alert.
If you would rather not manage the fd yourself, flock can
wrap the command directly - the usual form in a crontab:
flock -n /run/lock/my-script.lock /usr/local/bin/my-script
Knowledge check
Knowledge check · 5 questions
Q1. Why use mktemp instead of a fixed path such as /tmp/my-script.tmp for a scratch file?
Q2. Atomic file writes are essential for config files.
Q3. Which of the following are valid trap signals? Select all that apply.
Q4. A nightly backup guarded by `LOCKFILE=$(mktemp -t backup.lock.XXXXXX); [[ -e "$LOCKFILE" ]] && err "Already running"` has logged "Already running" every night for three weeks and no backup exists. Why?
Q5. A lock file guarded by flock needs no cleanup at all, and removing it in an EXIT trap lets two runs overlap.
Passing score: 75%. Answers are checked in this browser.