In this section we’ll develop a very basic debugger for bash. Most debuggers have numerous sophisticated features that help a programmer in dissecting a program, but just about all of them include the ability to step through a running program, stop it at selected places, and examine the values of variables. These simple features are what we will concentrate on providing in our debugger. Specifically, we’ll provide the ability to:
 Unfortunately, the debugger will not work with versions of bash prior to 2.0, because they do not implement the DEBUG signal.
- Specify places in the program at which to stop execution. These are called breakpoints.
- Execute a specified number of statements in the program. This is called stepping.
- Examine and change the state of the program during its execution. This includes being able to print out the values of variables and change them when the program is stopped at a breakpoint or after stepping.
- Print out the source code we are debugging along with indications of where breakpoints are and what line in the program we are currently executing.
- Provide the debugging capability without having to change the original source code of the program we wish to debug in any way.
As you will see, the capability to do all of these things (and more) is easily provided by the constructs and methods we have seen in previous chapters.
9.2.1. Structure of the Debugger
The bashdb debugger works by taking a shell script and turning it into a debugger for itself. It does this by concatenating debugger functionality and the target script, which we’ll call the guinea pig script, and storing it in another file that then gets executed. The process is transparent to users—they will be unaware that the code that is executing is actually a modified copy of their script.
The bash debugger has three main sections: the driver, the preamble, and the debugger functions.
188.8.131.52 The driver script
The driver script is responsible for setting everything up. It is a script called bashdb and looks like this:
# bashdb - a bash debugger # Driver Script: concatenates the preamble and the target script # and then executes the new script.
echo 'bash Debugger version 1.0'
if (( $# < 1 )) ; then echo "$_dbname: Usage: $_dbname filename" >&2 exit 1 fi
if [ ! -r $1 ]; then echo "$_dbname: Cannot read file '$_guineapig'." >&2 exit 1 fi
_tmpdir=/tmp _libdir=. _debugfile=$_tmpdir/bashdb.$$ # temporary file for script that is being debugged cat $_libdir/bashdb.pre $_guineapig > $_debugfile exec bash $_debugfile $_guineapig $_tmpdir $_libdir "$@"
bashdb takes as the first argument the name of guinea pig file. Any subsequent arguments are passed on to the guinea pig as its positional parameters.
If no arguments are given, bashdb prints out a usage line and exits with an error status. Otherwise, it checks to see if the file exists. If it doesn’t, exist then bashdb prints a message and exits with an error status. If all is in order, bashdb constructs a temporary file in the way we saw in the last chapter. If you don’t have (or don’t have access to) /tmp on your system, then you can substitute a different directory for _tmpdir. The variable _libdir is the name of the directory that contains files needed by bashdb (bashdb.pre and bashdb.fns). If you are installing bashdb on your system for everyone to use, you might want to place them in /usr/lib.
 All function names and variables (except those local to functions) in bashdb have names beginning with an underscore (_), to minimize the possibility of clashes with names in the guinea pig script.
The cat statement builds the modified copy of the guinea pig file: it contains the script found in bashdb.pre (which we’ll look at shortly) followed by a copy of the guinea pig.
The last line runs the newly created script with exec, a statement we haven’t discussed yet. We’ve chosen to wait until now to introduce it because—as we think you’ll agree—it can be dangerous. exec takes its arguments as a command line and runs the command in place of the current program, in the same process. In other words, a shell that runs exec will terminate immediately and be replaced by exec’s arguments.
 exec can also be used with an I/O redirector only; this makes the redirector take effect for the remainder of the script or login session. For example, the line exec 2>errlog at the top of a script directs standard error to the file errlog for the rest of the script.
In our script, exec just runs the newly constructed shell script, i.e., the guinea pig with its debugger, in another shell. It passes the new script three arguments—the name of the original guinea pig file ($_guineapig), the name of the temporary directory ($_tmpdir), and the name of the library directory ($_libdir)—followed by the user’s positional parameters, if any.
9.2.2. The Preamble
Now we’ll look at the code that gets prepended to the guinea pig script; we call this the preamble. It’s kept in the file bashdb.pre and looks like this:
# bashdb preamble # This file gets prepended to the shell script being debugged. # Arguments: # $1 = the name of the original guinea pig script # $2 = the directory where temporary files are stored # $3 = the directory where bashdb.pre and bashdb.fns are stored
source $_libdir/bashdb.fns _linebp= let _trace=0 let _i=1
while read; do _lines[$_i]=$REPLY let _i=$_i+1 done < $_guineapig
trap _cleanup EXIT let _steps=1 trap '_steptrap $(( $LINENO -29 ))' DEBUG
Head over to :