< Day Day Up > |
8.4. trapWe've been discussing how signals affect the casual user; now let's talk a bit about how shell programmers can use them. We won't go into too much depth about this, because it's really the domain of systems programmers. We mentioned above that programs in general can be set up to Section 8.4 specific signals and process them in their own way. The trap built-in command lets you do this from within a shell script. trap is most important for "bullet-proofing" large shell programs so that they react appropriately to abnormal events—just as programs in any language should guard against invalid input. It's also important for certain systems programming tasks, as we'll see in the next chapter. The syntax of trap is: trap cmd sig1 sig2 ... That is, when any of sig1, sig2, etc., are received, run cmd, then resume execution. After cmd finishes, the script resumes execution just after the command that was interrupted.[12]
Of course, cmd can be a script or function. The sigs can be specified by name or by number. You can also invoke trap without arguments, in which case the shell will print a list of any traps that have been set, using symbolic names for the signals. Here's a simple example that shows how trap works. Suppose we have a shell script called loop with this code: while true; do sleep 60 done This will just pause for 60 seconds (the sleep command) and repeat indefinitely. true is a "do-nothing" command whose exit status is always 0.[13] Try typing in this script. Invoke it, let it run for a little while, then type CTRL-C (assuming that is your interrupt key). It should stop, and you should get your shell prompt back.
Now insert this line at the beginning of the script: trap "echo 'You hit control-C!'" INT Invoke the script again. Now hit CTRL-C. The odds are overwhelming that you are interrupting the sleep command (as opposed to true). You should see the message "You hit control-C!", and the script will not stop running; instead, the sleep command will abort, and it will loop around and start another sleep. Hit CTRL-Z to get it to stop and then type kill %1. Next, run the script in the background by typing loop &. Type kill %loop (i.e., send it the TERM signal); the script will terminate. Add TERM to the trap command, so that it looks like this: trap "echo 'You hit control-C!'" INT TERM Now repeat the process: run it in the background and type kill %loop. As before, you will see the message and the process will keep on running. Type kill -KILL %loop to stop it. Notice that the message isn't really appropriate when you use kill. We'll change the script so it prints a better message in the kill case: trap "echo 'You hit control-C!'" INT trap "echo 'You tried to kill me!'" TERM while true; do sleep 60 done Now try it both ways: in the foreground with CTRL-C and in the background with kill. You'll see different messages. 8.4.1. Traps and FunctionsThe relationship between traps and shell functions is straightforward, but it has certain nuances that are worth discussing. The most important thing to understand is that functions are considered part of the shell that invokes them. This means that traps defined in the invoking shell will be recognized inside the function, and more importantly, any traps defined in the function will be recognized by the invoking shell once the function has been called. Consider this code: settrap ( ) { trap "echo 'You hit control-C!'" INT } settrap while true; do sleep 60 done If you invoke this script and hit your interrupt key, it will print "You hit control-C!" In this case the trap defined in settrap still exists when the function exits. Now consider: loop ( ) { trap "echo 'How dare you!'" INT while true; do sleep 60 done } trap "echo 'You hit control-C!'" INT loop When you run this script and hit your interrupt key, it will print "How dare you!" In this case the trap is defined in the calling script, but when the function is called the trap is redefined. The first definition is lost. A similar thing happens with: loop ( ) { trap "echo 'How dare you!'" INT } trap "echo 'You hit control-C!'" INT loop while true; do sleep 60 done Once again, the trap is redefined in the function; this is the definition used once the loop is entered. We'll now show a more practical example of traps.
The basic idea is to use cat to create the message in a temporary file and then hand the file's name off to a program that actually sends the message to its destination. The code to create the file is very simple: msgfile=/tmp/msg$$ cat > $msgfile Since cat without an argument reads from the standard input, this will just wait for the user to type a message and end it with the end-of-text character CTRL-D. 8.4.2. Process ID Variables and Temporary FilesThe only thing new about this script is $$ in the filename expression. This is a special shell variable whose value is the process ID of the current shell. To see how $$ works, type ps and note the process ID of your shell process (bash). Then type echo "$$"; the shell will respond with that same number. Now type bash to start a subshell, and when you get a prompt, repeat the process. You should see a different number, probably slightly higher than the last one. A related built-in shell variable is ! (i.e., its value is $!), which contains the process ID of the most recently invoked background job. To see how this works, invoke any job in the background and note the process ID printed by the shell next to [1]. Then type echo "$!"; you should see the same number. To return to our mail example: since all processes on the system must have unique process IDs, $$ is excellent for constructing names of temporary files. The directory /tmp is conventionally used for temporary files. Many systems also have another directory, /var/tmp, for the same purpose. Nevertheless, a program should clean up such files before it exits, to avoid taking up unnecessary disk space. We could do this in our code very easily by adding the line rm $msgfile after the code that actually sends the message. But what if the program receives a signal during execution? For example, what if a user changes her mind about sending the message and hits CTRL-C to stop the process? We would need to clean up before exiting. We'll emulate the actual UNIX mail system by saving the message being written in a file called dead.letter in the current directory. We can do this by using trap with a command string that includes an exit command: trap 'mv $msgfile dead.letter; exit' INT TERM msgfile=/tmp/msg$$ cat > $msgfile # send the contents of $msgfile to the specified mail address... rm $msgfile When the script receives an INT or TERM signal, it will remove the temp file and then exit. Note that the command string isn't evaluated until it needs to be run, so $msgfile will contain the correct value; that's why we surround the string in single quotes. But what if the script receives a signal before msgfile is created—unlikely though that may be? Then mv will try to rename a file that doesn't exist. To fix this, we need to test for the existence of the file $msgfile before trying to delete it. The code for this is a bit unwieldy to put in a single command string, so we'll use a function instead: function cleanup { if [ -e $msgfile ]; then mv $msgfile dead.letter fi exit } trap cleanup INT TERM msgfile=/tmp/msg$$ cat > $msgfile # send the contents of $msgfile to the specified mail address... rm $msgfile 8.4.3. Ignoring SignalsSometimes a signal comes in that you don't want to do anything about. If you give the null string ("" or `') as the command argument to trap, then the shell will effectively ignore that signal. The classic example of a signal you may want to ignore is HUP (hangup). This can occur on some UNIX systems when a hangup (disconnection while using a modem—literally "hanging up") or some other network outage takes place. HUP has the usual default behavior: it will kill the process that receives it. But there are bound to be times when you don't want a background job to terminate when it receives a hangup signal. To do this, you could write a simple function that looks like this: function ignorehup { trap "" HUP eval "$@" } We write this as a function instead of a script for reasons that will become clearer when we look in detail at subshells at the end of this chapter. Actually, there is a UNIX command called nohup that does precisely this. The start script from the last chapter could include nohup: eval nohup "$@" > logfile 2>&1 & This prevents HUP from terminating your command and saves its standard and error output in a file. Actually, the following is just as good: nohup "$@" > logfile 2>&1 & If you understand why eval is essentially redundant when you use nohup in this case, then you have a firm grasp on the material in the previous chapter. Note that if you don't specify a redirection for any output from the command, nohup places it in a file called nohup.out. 8.4.4. disownAnother way to ignore the HUP signal is with the disown built-in.[14] disown takes as an argument a job specification, such as the process ID or job ID, and removes the process from the list of jobs. The process is effectively "disowned" by the shell from that point on, i.e., you can only refer to it by its process ID since it is no longer in the job table.
disown's -h option performs the same function as nohup; it specifies that the shell should stop the hangup signal from reaching the process under certain circumstances. Unlike nohup, it is up to you to specify where the output from the process is to go. disown also provides two options which can be of use. -a with no other arguments applies the operation to all jobs owned by the shell. The -r option with does the same but only for currently running jobs. 8.4.5. Resetting TrapsAnother "special case" of the trap command occurs when you give a dash (-) as the command argument. This resets the action taken when the signal is received to the default, which usually is termination of the process. As an example of this, let's return to Task 8-2, our mail program. After the user has finished sending the message, the temporary file is erased. At that point, since there is no longer any need to clean up, we can reset the signal trap to its default state. The code for this, apart from function definitions, is: trap abortmsg INT trap cleanup TERM msgfile=/tmp/msg$$ cat > $msgfile # send the contents of $msgfile to the specified mail address... rm $msgfile trap - INT TERM The last line of this code resets the handlers for the INT and TERM signals. At this point you may be thinking that you could get seriously carried away with signal handling in a shell script. It is true that "industrial strength" programs devote considerable amounts of code to dealing with signals. But these programs are almost always large enough so that the signal-handling code is a tiny fraction of the whole thing. For example, you can bet that the real UNIX mail system is pretty darn bullet-proof. However, you will probably never write a shell script that is complex enough, and that needs to be robust enough, to merit lots of signal handling. You may write a prototype for a program as large as mail in shell code, but prototypes by definition do not need to be bullet-proofed. Therefore, you shouldn't worry about putting signal-handling code in every 20-line shell script you write. Our advice is to determine if there are any situations in which a signal could cause your program to do something seriously bad and add code to deal with those contingencies. What is "seriously bad"? Well, with respect to the above examples, we'd say that the case where HUP causes your job to terminate is seriously bad, while the temporary file situation in our mail program is not. |
< Day Day Up > |