Heiner's SHELLdorado
Good Shell Coding Practices
SHELLdorado - your UNIX shell scripting resource
HomeGood Shell Coding PracticesExample Shell ScriptsShell Scripting LinksShell Scripting Tips+TricksShell Scripting Articles


Good Coding - Home

Previous Page | Top | Next Page

2. Temporary files and signal handling

Temporary files are frequently used in shell scripts. In a typical shell script often some data is processed, and the results are written to a scratch file, the new data is processed in another way, and eventually the scratch file is removed.

So why write an article about this topic?

Often shell script programmers use temporary files in their scripts, and remove them at the end of the program. This simple and straight forward approach works well as long as a user does not interrupt the script using a signal (i.e. by pressing ^C or DEL). In this case the script doesn't have a chance to remove its temporary files before closing.

This article shows how to intercept interrupts from shell scripts.

One example:

:
# viman - start "vi" on a manual page

Tmp=/tmp/viman

man "$@" | col -b | uniq > $Tmp
vi $Tmp
rm -f $Tmp

This script passes its command line arguments on to the man command, and writes the result to a temporary file /tmp/viman. Before starting vi on the file, all control characters are removed ("col -b"), and duplicate or empty lines are removed ("uniq"). After vi terminates, the file is removed.

This simple script has two drawbacks.

Consider what happens if two people call this script, one after the other. The first one has his manual page written to /tmp/viman. Shortly after that the second one has his manual page written to the same file, overwriting the contents of the first manual page. Now the first user gets the wrong manual page in the vi editor, and terminates. His instance of the script removes the file /tmp/viman, and with a little bad luck the first user at the same time now has an empty file within the vi.

The solution to this problem is clear: each user needs to have a unique temporary file, but how to do it? We could try to create the temporary file in the directory $HOME. Each user is (normally) guaranteed to have a unique HOME directory. But even then the user may overwrite the file if he has a windowing system (like OpenWindows or the Common Desktop Environment (CDE)) and is logged in more than once with the same HOME directory.

Steve Bourne (the creator of the Bourne Shell) suggests in The UNIX system to use the unique process identifier (PID) of the shell script as part of the file name. Since the process id of the script is always available via the environment variable $$, we could rewrite the script as follows:

:
# viman - start "vi" with a manual page

Tmp=/tmp/vm$$

man "$@" | col -b | uniq > $Tmp
vi $Tmp
rm -f $Tmp

This small change solves the problem.

But one problem remains: what happens to the temporary file, if the script is terminated with a signal? In this case, the temporary file may is not removed, because the last line of the script is never reached!

You may think: "Who cares about files clogging up the /tmp directory? The directory gets cleaned up automatically anyway!" On the other hand you are reading this text to become a better shell programmer, and could be excited to come to know there is an easy way to "trap" signals from a shell script.

The general syntax for the trap command is:

trap [ command ] signal [ signal ... ]

Signals may be specified using numbers (0 to 31), "0" being a pseudo-signal meaning "program termination". The Korn shell also understands names for the signal, i.e. HUP for HANGUP signal, TERM for the SIGTERM signal etc. Newer kill commands display a list of signal names if called with the flag -l. The following table lists the most common signals along with their KSH names:

Number KSH name Comments
0 EXIT This number does not correspond to a real signal, but the corresponding trap is executed before script termination.
1 HUP hangup
2 INT The interrupt signal typically is generated using the DEL or the ^C key
3 QUIT The quit signal is typically generated using the ^[ key. It is used like the INT signal but explicitly requests a core dump.
9 KILL cannot be caught or ignored
10 BUS bus error
11 SEGV segmentation violation
13 PIPE generated if there is a pipeline without reader to terminate the writing process(es)
15 TERM generated to terminate the process gracefully
16 USR1 user defined signal 1
17 USR2 user defined signal 2
- DEBUG KSH only: This is no signal, but the corresponding trap code is executed before each statement of the script.

A simple example would be:

trap "rm -f $Tmp" 0 1 2 3 15

This means: execute the command "rm -f $Tmp" if the script terminates ("signal" 0), or after receiving any of the signals 1 (HANGUP), 2 (QUIT), 3 (INTR), or 15 (TERM). Actually, a good shell script should handle all these signals.

Only one refinement has to be made before we can present The Canonical Way To Handle Temporary Files ©. Suppose we use the following line in our script:

trap "rm -f $Tmp" 0 1 2 3 15

If somebody sends the SIGTERM signal to our script (i.e. by entering "kill -15 scriptpid"), the following would happen:

  1. The script would trap the signal 15, and execute the command "rm -f $Tmp", thus removing the temporary file.
  2. Then it would continue with the next script command. This could cause strange results, because the (probably needed) temporary file $Tmp is gone. Another point is that somebody explicitly tried to terminate the script, a fact it deliberately ignores.
  3. Just before the script exits the trap for signal "0" is always performed, resulting in a second attempt to remove $Tmp. This will result in unwanted error messages (although in this case it will do no harm).

A better (and the recommended) way to handle the signals is as follows:

trap 'rm -f "$Tmp" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 15

The first trap ensures that the temporary file $Tmp is removed at the end of the script execution. Possible error messages are simply discarded.

The second trap causes our script to terminate after receiving one of the specified signals. Before the script terminates, the trap for "signal" 0 is executed, effectively removing the temporary file.

Our original script, now rewritten to handle signals and use unique temporary files looks as follows:

 
:
# viman - start "vi" with a manual page

Tmp="${TMPDIR:=/tmp}/vm$$"

# Assure the file is removed at program termination
# or after we received a signal:
trap 'rm -f "$Tmp" >/dev/null 2>&1' 0
trap "exit 2" 1 2 3 13 15

EXINIT="set ignorecase nowrapscan readonly"
export EXINIT

man "$@" | col -b | uniq > "$Tmp" || exit

[ -s "$Tmp" ] || exit 0		# file is empty
head -1 < "$Tmp" |
    grep 'No.*entry' && exit 0 # no manual page

${EDITOR:-vi} "$Tmp"

Handling signals requires a bit more overhead; perhaps overkill for simple scripts like this one but definitely worthwhile for complex scripts.

Previous Page | Top | Next Page
   
Copyright © 1998-2022 Heiner Steven (heiner.steven@shelldorado.com)