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
local for every variable inside. Without it, every variable is global (visible in the whole script), and two functions that both use i will overwrite each other.
return N ends the function; exit N ends the whole script. A function that calls exit cannot be reused anywhere that wanted to carry on.
Arguments work exactly as in a script (6.6): $1 $2 ... "$@" $#. $0 stays the script's name.
A function "returns" only an exit status, 0-255. To hand back data, echo it and capture it: result=$(myfunc).
And remember local x=$(cmd) swallows the status (6.3) - declare, then assign.
tmp=$(mktemp -d) # make a private temp directory (below)
cleanup() {
rm -rf "$tmp"
}
trap cleanup EXIT INT TERM
EXIT is not a real signal; it is bash's own event for "the script is ending, for any reason": reaching the last line, an explicit exit, or a failure under set -e.
INT is the signal Ctrl+C sends; TERM is a polite kill (3.6). Listing them too makes sure cleanup runs on those.
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
Write functions with local variables, return and arguments.
Create a temp directory with mktemp -d and guarantee it is removed with trap ... EXIT INT TERM.
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.
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" ...
mktemp -d makes a private directory (mode 700) with a random name. A hand-made /tmp/myapp.$$ is guessable, and another user could plant a symlink there first.
trap cleanup EXIT INT TERM runs cleanup however the script ends: EXIT is bash's own event for "ending for any reason" (last line, exit, a failure under set -e); INT is Ctrl+C, TERM a polite kill.
Set the trap immediately after creating the directory - one line later is a line where a failure leaks it.
Quote "$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?