OnCallReady

Lesson 6.15 · Bash Scripting · 10 min read

Functions, traps and mktemp

In plain words

Imagine borrowing a friend's kitchen to bake. A good guest promises: "whatever happens, whether I finish, give up or get called away, I will wash up before I leave". You say it before you start, not halfway through, because by then the emergency may already have happened.

trap cleanup EXIT INT TERM is that promise, and mktemp -d is taking a clean bowl with your name on it that nobody else can guess. Functions are little recipes inside the big one; local keeps their ingredients from spilling into the rest of the kitchen, and return ends the little recipe while exit ends the whole baking session. The one exception: if the house is demolished (kill -9), nobody washes up.

Why this lesson

A script makes a scratch directory, works in it, and deletes it at the end. Then one day it fails halfway, or you press Ctrl+C - and the "delete it at the end" line never runs. Do that nightly and /tmp fills up with junk. You need cleanup that runs however the script ends. That is trap, and it only makes sense once you can write functions.

What you need to know already: signals INT (Ctrl+C), TERM and KILL, and that KILL cannot be caught (3.6); /tmp (4.1); local x=$(cmd) (6.3).

Functions

A function is a named group of commands you can run like a command:

deploy() {
  local env=$1              # local: this variable exists only in the function
  local file=$2
  [[ -n $env ]] || return 1 # return ends the FUNCTION, with status 1
  echo "deploying $file to $env"
}

deploy prod app.tar.gz      # call it, with two arguments

And remember local x=$(cmd) swallows the status (6.3) - declare, then assign.

trap: the cleanup that always runs

trap COMMAND EVENT... tells bash: "when EVENT happens, run COMMAND first".

tmp=$(mktemp -d)            # make a private temp directory (below)
cleanup() {
  rm -rf "$tmp"
}
trap cleanup EXIT INT TERM

Set the trap immediately after creating the thing it cleans up. One line later is one line where a failure leaks a temp directory.

Two other events you may see: ERR runs whenever a command fails (handy for printing where a script died), DEBUG runs before every command.

What a trap cannot do: SIGKILL cannot be caught (3.6), so kill -9 leaves the temp directory behind. That is not a bug in your script; it is what -9 means.

mktemp, and why not /tmp/myapp.$$

mktemp creates a new, empty temp file (or with -d, a directory) with a random name, and prints that name:

tmp=$(mktemp -d)                    # /tmp/tmp.8Ld0Xq3vBn
f=$(mktemp)                         # a single file
tmp=$(mktemp -d -t deploy.XXXXXX)   # -t NAME: use this name pattern; X = random

It creates it safely: only you can read it (mode 600 for a file, 700 for a directory), and nobody can guess the name in advance. A hand-made /tmp/myapp.$$ ($$ is the script's PID) is guessable. On a box shared with other users, someone could create a symlink (4.13) with that name first, and your script would write through it into their chosen file.

Always quote "$tmp" afterwards - this path has no spaces, but the habit is what saves you when a value comes from somewhere else.

What you can now do

Why it helps

Scripts that leave temp directories, lock files, half-written downloads or services stopped for maintenance behind after a failure cause the next run to fail, fill disks, or leave systems in a strange state. A trap on EXIT is how you guarantee cleanup and restore steps, such as removing a lock, deleting temporary credentials, or starting a stopped service again after maintenance. It is also a classic interview question.

Predictable temp names like /tmp/app.$$ are a real security finding on shared hosts, fixed by mktemp. And local plus return versus exit decide whether your functions can be reused in other scripts. These patterns show up in every non-trivial deploy or maintenance script you review.

Commands in this lesson

bash

FAQ

Does an EXIT trap run when the script is killed?

It runs for normal completion, exit, and failures under set -e. For signals, bash runs the EXIT trap when it exits because of a trapped signal, and in practice also for untrapped SIGINT and SIGTERM in current bash versions, but trapping INT and TERM explicitly (and exiting from the handler) makes the behaviour clear. SIGKILL can never be trapped, so nothing runs after kill -9 or an OOM kill.

Why use mktemp instead of /tmp/myscript.$$?

$$ is the PID, which is predictable. On a shared machine, another user can create a symlink at /tmp/myscript.1234 pointing at a file you own, and your script then overwrites it. PIDs also wrap, so names collide. mktemp picks a random name and creates the file or directory atomically with private permissions (600 for files, 700 for directories), so nobody can pre-create or read it.

What is the difference between return and exit in a function?

return N ends the function and sets its status to N, and the caller continues. exit N ends the entire script, or the subshell it runs in, from inside the function. Library functions should use return so callers decide what to do. A function called inside $(...) runs in a subshell, where even exit only ends that subshell.

How does a function return a string?

Functions only return an exit status (0-255). To return data, print it to stdout and capture it: result=$(get_version). Keep diagnostics on stderr (>&2) so they are not captured with the data. Alternatives are setting a global variable or using a nameref (local -n out=$1, bash 4.3+) to write into a variable the caller names.

Can I have more than one EXIT trap?

No, setting trap ... EXIT again replaces the previous handler. If different parts of a script need cleanup, write one cleanup function that handles all of them, for example by keeping a list of paths to remove in an array and deleting them all in the handler. trap -p EXIT prints the current handler, which helps when combining scripts.

In an interview Junior

Write a script that always cleans up its temporary directory, even if it fails or is interrupted.

#!/usr/bin/env bash
set -euo pipefail
tmp=$(mktemp -d)
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT INT TERM
# ... work in "$tmp" ...

The one thing it cannot handle: kill -9. SIGKILL cannot be caught, so the directory stays - that is what -9 means.

Also asked: Why should variables inside a function be declared local? · What is the difference between return and exit in a function? · How does a function hand data back to its caller?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.