Memra

Ignoring, catching, and cleaning up

◈ 8 cards

Set the disposition of a signal from a shell script with trap: run a cleanup handler, ignore the signal outright, or restore the kernel default — and know the two signals that refuse all three.

A signal is a named interruption

A running process can be interrupted by a signal: a short, named message delivered by the kernel. You send them constantly without noticing. <Ctrl+C> at the keyboard sends SIGINT to the foreground job. <Ctrl+\> sends SIGQUIT. <Ctrl+Z> sends SIGTSTP. Logging out sends SIGHUP to what you left running. Plain kill 4021 sends SIGTERM, and kill -KILL 4021 sends SIGKILL.

In every process, every signal has a disposition — what happens when it arrives. There are exactly three:

  • default — the kernel’s own action, which for most signals is "terminate this process";
  • ignored — the signal is discarded on arrival and the process never learns of it;
  • caught — a handler you nominated runs, and then the process carries on.

trap is how a shell script sets the disposition of a signal. Everything else in this lesson is a consequence of those three cases.

The four forms of trap

trap 'rm -f "$tmp"; exit 1' INT TERM
trap '' INT TERM
trap - INT TERM
trap

A command list catches. trap 'cmds' SIGS runs cmds when any of those signals arrives, then resumes with the next command — unless the handler ends in exit, which is usually what you want. The quotes around the command list are not decoration: without them the shell splits at the first space and reads the remainder as signal names.

Empty quotes ignore. trap '' SIGS supplies a command list that is present but empty, which the shell reads as "discard these signals". The two characters '' are the entire difference between ignoring a signal and resetting it, and mixing them up is the classic error in this section. An ignored disposition is also inherited by child processes, so a script that ignores SIGINT makes the commands it runs deaf to <Ctrl+C> as well.

A hyphen restores the default. trap - SIGS puts the kernel’s action back. The textbook writes this as trap 2 — signal operands with no command list — which older shells accept, but trap - is the POSIX spelling and it is unambiguous.

trap alone prints. With no operands at all, POSIX trap writes out the traps currently set, in a form you could paste back into a script. It resets nothing. That is worth knowing because it is the fast way to check a trap actually took.

Two signals you can never trap

SIGKILL and SIGSTOP cannot be caught and cannot be ignored. The kernel refuses the request. This is a deliberate guarantee, and it is the reason kill -KILL always works no matter what the target has installed: a process cannot make itself immortal. The textbook does not say this, and it is exactly the kind of omission an exam question is built on.

Names, not numbers

Write trap '' INT and not trap '' 2. The numbers are not portable: SIGTSTP is signal 18 on the BSD and Solaris systems the textbook runs on and 20 on Linux, while SIGCHLD is 20 there and 17 here. A script that traps by number and is copied across systems traps the wrong signal, silently. Names are stable everywhere. POSIX writes them without the SIG prefix — INT, TERM, HUP, QUIT, TSTP — and every shell you will use also accepts the prefixed spelling.

Worked example one — the terminal locker

A script that makes a terminal unusable until a code word is retyped:

#!/bin/sh
trap '' HUP INT QUIT TERM TSTP
stty -echo
printf 'Lock code: '
read code
stty echo

The first line makes the script deaf to hangup, <Ctrl+C>, <Ctrl+\>, plain kill and <Ctrl+Z>. stty -echo stops the terminal from printing what is typed, so the code word is not left on screen; stty echo puts it back. In between, a real locker loops on read until the typed word matches.

The teaching point is what it takes to get out of it. If the script had ignored only SIGINT, you could suspend it with <Ctrl+Z>, find it with ps, and end it with kill. This one ignores TSTP as well, so <Ctrl+Z> does nothing either — you need a different terminal, ps to find the process id, and kill -KILL, which works because SIGKILL cannot be trapped. Ignoring signals is a real capability, and it has a real cost.

Worked example two — from ignoring to cleaning up

Ignoring is blunt. Most scripts want the other case: catch the signal, tidy up, and exit.

#!/bin/sh
tmp=/tmp/report.$$
trap 'rm -f "$tmp"; echo interrupted; exit 1' INT TERM
sort big.txt > "$tmp"
wc -l < "$tmp"
rm -f "$tmp"

Without the trap, a <Ctrl+C> during the sort kills the script where it stands and leaves the temporary file behind for ever. With it, the handler removes the file, says so, and exits with a failure status, so a caller can tell the run was interrupted.

The exit 1 is load-bearing. A handler that does not exit hands control back to the command after the interrupted one, and the script blunders on with a temporary file it has just deleted. Decide deliberately: cleanup handlers exit, progress-reporting handlers do not.

To protect a section that must not be interrupted — writing a file that would be corrupted half-written — bracket it: trap '' INT before, trap - INT after. The window is then as small as you can make it, which is the honest version of "this cannot be interrupted".

no trap settrap '' INTtrap 'cleanup' INTSIGINT arrivesCtrl+C at the keyboarddefault actionscript terminatesdiscardedscript never noticeshandler runsthen the next commandSIGKILL and SIGSTOP take the left branch always.
Three outcomes, three commands. The middle branch is the one the empty quotes select.
NORMAL ~/memra/learn/comp-325/trap-and-signals-in-a-script utf-8 LF