Skip to main content
RunBook Academy

LinuxXXXV · Shell Scripting for SysadminsTraps tmpfiles

Traps and temporary files - clean exit on signal or error

Intermediate⏱ ~10 minbashmktemp

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

Not yet marked complete on this device.

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.tmp is a race condition. Two instances may write to the same file, and a hostile local user can pre-create it as a symlink. Use mktemp. Lock files are the exception - see the next section: a lock needs a fixed, shared path, so the safety comes from flock, 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 later mktemp into the directory your trap deletes.
  • Staging in /tmp and moving to /etc: that crosses a filesystem boundary, so the mv is 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

  1. Q1. Why use mktemp instead of a fixed path such as /tmp/my-script.tmp for a scratch file?

  2. Q2. Atomic file writes are essential for config files.

  3. Q3. Which of the following are valid trap signals? Select all that apply.

  4. 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?

  5. 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.