9.1 Basic Debugging Aids
What sort of functionality do you need to debug a program? At the most empirical level, you need a way of determining what is causing your program to behave badly, and where the problem is in the code. You usually start with an obvious what (such as an error message, inappropriate output, infinite loop, etc.), try to work backwards until you find a what that is closer to the actual problem (e.g., a variable with a bad value, a bad option to a command), and eventually arrive at the exact where in your program. Then you can worry about how to fix it.Notice that these steps represent a process of starting with obvious information and ending up with often obscure facts gleaned through deduction and intuition. Debugging aids make it easier to deduce and intuit by providing relevant information easily or even automatically, preferably without modifying your code.The simplest debugging aid (for any language) is the output statement, echo, in the shell's case. Indeed, old-time programmers debugged their FORTRAN code by inserting WRITE cards into their decks. You can debug by putting lots of echo statements in your code (and removing them later), but you will have to spend lots of time narrowing down not only what exact information you want but also where you need to see it. You will also probably have to wade through lots and lots of output to find the information you really want. 9.1.1 Set Options
Luckily, the shell has a few basic features that give you debugging functionality beyond that of echo. The most basic of these are options to the set -o command (as covered in Chapter 3). These options can also be used on the command line when running a script, as Table 9.1 shows. Table 9.1. Debugging Options |
set -o Option | Command-Line Option | Action |
noexec | -n | Don't run commands; check for syntax errors only |
verbose | -v | Echo commands before running them | |
xtrace | -x | Echo commands after command-line processing | |
The verbose option simply echoes (to standard error) whatever input the shell gets. It is useful for finding the exact point at which a script is bombing. For example, assume your script looks like this:alice
hatter
march
teatime
treacle
well
None of these commands is a standard UNIX program, and each does its work silently. Say the script crashes with a cryptic message like "segmentation violation." This tells you nothing about which command caused the error. If you type bash -v scriptname, you might see this:alice
hatter
march
segmentation violation
teatime
treacle
well
Now you know that march is the probable culpritthough it is also possible that march bombed because of something it expected alice or hatter to do (e.g., create an input file) that they did incorrectly.The xtrace option is more powerful: it echoes command lines after they have been through parameter substitution, command substitution, and the other steps of command-line processing (as listed in Chapter 7). For example:.ps 8
$ set -o xtrace$ alice=girl+ alice=girl
$ echo "$alice"+ echo girl
girl
$ ls -l $(type -path vi)++ type -path vi
+ ls -F -l /usr/bin/vi
lrwxrwxrwx 1 root root 5 Jul 26 20:59 /usr/bin/vi -> elvis*
$
As you can see, xtrace starts each line it prints with + (each + representing a level of expansion). This is actually customizable: it's the value of the built-in shell variable PS4. So if you set PS4 to "xtrace>" (e.g., in your .bash_profile or .bashrc), then you'll get xtrace listings that look like this:.ps 8
$ ls -l $(type -path vi)xxtrace--> type -path vi
xtrace> ls -l /usr/bin/vi
lrwxrwxrwx 1 root root 5 Jul 26 20:59 /usr/bin/vi -> elvis*
$
Notice that for multiple levels of expansion, only the first character of PS4 is printed. This makes the output more readable.An even better way of customizing PS4 is to use a built-in variable we haven't seen yet: LINENO, which holds the number of the currently running line in a shell script. [2]Put this line in your .bash_profile or environment file:[2] In versions of bash prior to 2.0, LINENO won't give you the current line in a function. LINENO, instead, gives an approximation of the number of simple commands executed so far in the current function.PS4='line $LINENO: '
We use the same technique as we did with PS1 in Chapter 3: using single quotes to postpone the evaluation of the string until each time the shell prints the prompt. This will print messages of the form line N: in your trace output. You could even include the name of the shell script you're debugging in this prompt by using the positional parameter $0:PS4='$0 line $LINENO: '
As another example, say you are trying to track down a bug in a script called alice that contains this code:dbfmq=$1.fmq
...
fndrs=$(cut -f3 -d' ' $dfbmq)
You type alice teatime to run it in the normal way, and it hangs. Then you type bash -x alice teatime, and you see this:+ dbfmq=teatime.fmq
...
+ + cut -f3 -d
It hangs again at this point. You notice that cut doesn't have a filename argument, which means that there must be something wrong with the variable dbfmq. But it has executed the assignment statement dbfmq=teatime.fmq properly...ah-hah! You made a typo in the variable name inside the command substitution construct. [3]You fix it, and the script works properly.[3] We should admit that if you had turned on the nounset option at the top of this script, the shell would have flagged this error.The last option is noexec, which reads in the shell script, checks for syntax errors, but doesn't execute anything. It's worth using if your script is syntactically complex (lots of loops, command blocks, string operators, etc.) and the bug has side effects (like creating a large file or hanging up the system).You can turn on these options with set -o option in your shell scripts, and, as explained in Chapter 3, turn them off with set +o option. For example, if you're debugging a chunk of code, you can precede it with set -o xtrace to print out the executed commands, and end the chunk with set +o xtrace.Note, however, that once you have turned noexec on, you won't be able to turn it off; a set +o noexec will never be executed. 9.1.2 Fake Signals
A more sophisticated set of debugging aids is the shell's "fake signals," which can be used in trap statements to get the shell to act under certain conditions. Recall from the previous chapter that trap allows you to install some code that runs when a particular signal is sent to your script.Fake signals work in the same way, but they are generated by the shell itself, as opposed to the other signals which are generated externally. They represent runtime events that are likely to be of interest to debuggersboth human ones and software toolsand can be treated just like real signals within shell scripts. Table 9.2 lists the two fake signals available in bash. Table 9.2. Fake Signals |
Fake Signal | Sent When |
EXIT | The shell exits from script |
DEBUG | The shell has executed a statementa |
[4]