预计阅读本页时间:-
Debugger Functions
The function _steptrap is the entry point into the debugger; it is defined in the file bashdb.fns. Here is _steptrap:
# After each line of the test script is executed the shell traps to
# this function.
function _steptrap
{
_curline=$1 # the number of the line that just ran
(( $_trace )) && _msg "$PS4 line $_curline: ${_lines[$_curline]}"
if (( $_steps >= 0 )); then
let _steps="$_steps - 1"
fi
# First check to see if a line number breakpoint was reached.
# If it was, then enter the debugger.
if _at_linenumbp ; then
_msg "Reached breakpoint at line $_curline"
_cmdloop
# It wasn't, so check whether a break condition exists and is true.
# If it is, then enter the debugger.
elif [ -n "$_brcond" ] && eval $_brcond; then
_msg "Break condition $_brcond true at line $_curline"
_cmdloop
# It wasn't, so check if we are in step mode and the number of steps
# is up. If it is then enter the debugger.
elif (( $_steps == 0 )); then
_msg "Stopped at line $_curline"
_cmdloop
fi
}
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
_steptrap starts by setting _curline to the number of the guinea pig line that just ran. If execution tracing is on, it prints the PS4 execution trace prompt (like the shell's xtrace mode), line number, and line of code itself. It then decrements the number of steps if the number of steps still left is greater than or equal to zero.
Then it does one of two things: it enters the debugger via _cmdloop, or it returns so the shell can execute the next statement. It chooses the former if a breakpoint or break condition has been reached, or if the user stepped into this statement.
Commands
We will explain shortly how _steptrap determines these things; now we will look at _cmdloop. It's a simple combination of the case statements we saw in Chapter 5, and the calculator loop we saw in the previous chapter.
# The Debugger Command Loop
function _cmdloop {
local cmd args
while read -e -p "bashdb> " cmd args; do
case $cmd in
\? | h ) _menu ;; # print command menu
bc ) _setbc $args ;; # set a break condition
bp ) _setbp $args ;; # set a breakpoint at the given
# line
cb ) _clearbp $args ;; # clear one or all breakpoints
ds ) _displayscript ;; # list the script and show the
# breakpoints
g ) return ;; # "go": start/resume execution of
# the script
q ) exit ;; # quit
s ) let _steps=${args:-1} # single step N times
# (default = 1)
return ;;
x ) _xtrace ;; # toggle execution trace
!* ) eval ${cmd#!} $args ;; # pass to the shell
* ) _msg "Invalid command: '$cmd'" ;;
esac
done
}
At each iteration, _cmdloop prints a prompt, reads a command, and processes it. We use read -e so that the user can take advantage of the readline command-line editing. The commands are all one- or two-letter abbreviations; quick for typing, but terse in the UNIX style.[14]
Table 9-3 summarizes the debugger commands.
Table 9-3. bashdb commands
Command
Action
bp N
Set breakpoint at line N
bp
List breakpoints and break condition
bc string
Set break condition to string
bc
Clear break condition
cb N
Clear breakpoint at line N
cb
Clear all breakpoints
ds
Display the test script and breakpoints
g
Start/resume execution
s [N]
Execute N statements (default 1)
x
Toggle execution trace on/off
h, ?
Print the help menu
! string
Pass string to a shell
q
Quit
Before looking at the individual commands, it is important that you understand how control passes through _steptrap, the command loop, and the guinea pig.
_steptrap runs after every statement in the guinea pig as a result of the trap on DEBUG in the preamble. If a breakpoint has been reached or the user previously typed in a step command(s), _steptrap calls the command loop. In doing so, it effectively "interrupts" the shell that is running the guinea pig to hand control over to the user.
The user can invoke debugger commands as well as shell commands that run in the same shell as the guinea pig. This means that you can use shell commands to check values of variables, signal traps, and any other information local to the script being debugged. The command loop continues to run, and the user stays in control, until he types g, q, or s. We'll now look in detail at what happens in each of these cases.
Typing g has the effect of running the guinea pig uninterrupted until it finishes or hits a breakpoint. It simply exits the command loop and returns to _steptrap, which exits as well. The shell then regains control and runs the next statement in the guinea pig script. Another DEBUG signal occurs and the shell traps to _steptrap again. If there are no breakpoints then _steptrap will just exit. This process will repeat until a breakpoint is reached or the guinea pig finishes.
The q command calls the function _cleanup, which erases the temporary file and exits the program.
Stepping
When the user types s, the command loop code sets the variable _steps to the number of steps the user wants to execute, i.e., to the argument given. Assume at first that the user omits the argument, meaning that _steps is set to 1. Then the command loop exits and returns control to _steptrap, which (as above) exits and hands control back to the shell. The shell runs the next statement and returns to _steptrap, which then decrements _steps to 0. Then the second elif conditional becomes true because _steps is 0 and prints a "stopped" message and then calls the command loop.
Now assume that the user supplies an argument to s, say 3. _steps is set to 3. Then the following happens:
- After the next statement runs, _steptrap is called again. It enters the first if clause, since _steps is greater than 0. _steptrap decrements _steps to 2 and exits, returning control to the shell.
- This process repeats, another step in the guinea pig is run, and _steps becomes 1.
- A third statement is run and we're back in _steptrap. _steps is decremented to 0, the second elif clause is run, and _steptrap breaks out to the command loop again.
The overall effect is that the three steps run and then the debugger takes over again.
All of the other debugger commands cause the shell to stay in the command loop, meaning that the user prolongs the "interruption" of the shell.
Breakpoints
Now we'll examine the breakpoint-related commands and the breakpoint mechanism in general. The bp command calls the function _setbp, which can do two things, depending on whether an argument is supplied or not. Here is the code for _setbp:
# Set a breakpoint at the given line number or list breakpoints
function _setbp
{
local i
if [ -z "$1" ]; then
_listbp
elif [ $(echo $1 | grep '^[0-9]*') ]; then
if [ -n "${_lines[$1]}" ]; then
_linebp=($(echo $( (for i in ${_linebp[*]} $1; do
echo $i; done) | sort -n) ))
_msg "Breakpoint set at line $1"
else
_msg "Breakpoints can only be set on non-blank lines"
fi
else
_msg "Please specify a numeric line number"
fi
}
If no argument is supplied, _setbp calls _listbp, which prints the line numbers that have breakpoints set. If anything other than a number is supplied as an argument, an error message is printed and control returns to the command loop. Providing a number as the argument allows us to set a breakpoint; however, we have to do another test before doing so.
What happens if the user decides to set a breakpoint at a nonsensical point: a blank line, or at line 1,000 of a 10-line program? If the breakpoint is set well beyond the end of the program, it will never be reached and will cause no problem. If, however, a breakpoint is set at a blank line, it will cause problems. The reason is that the DEBUG trap only occurs after each executed simple command in a script, not each line. Blank lines never generate the DEBUG signal. The user could set a breakpoint on a blank line, in which case continuing execution with the g command would never break back out to the debugger.
We can fix both of these problems by making sure that breakpoints are set only on lines with text.[15] After making the tests, we can add the breakpoint to the breakpoint array, _linebp. This is a little more complex than it sounds. In order to make the code in other sections of the debugger simpler, we should maintain a sorted array of breakpoints. To do this, we echo all of the line numbers currently in the array, along with the new number, in a subshell and pipe them into the UNIX sort command. sort -n sorts a list into numerically ascending order. The result of this is a list of ordered numbers which we then assign back to the _linebp array with a compound assignment.
To complement the user's ability to add breakpoints, we also allow the user to delete them. The cb command allows the user to clear single breakpoints or all breakpoints, depending on whether a line number argument is supplied or not. For example, cb 12 clears a breakpoint at line 12 (if a breakpoint was set at that line). cb on its own would clear all of the breakpoints that have been set. It is useful to look briefly at how this works; here is the code for the function that is called with the cb command, _clearbp:
function _clearbp
{
local i
if [ -z "$1" ]; then
unset _linebp[*]
_msg "All breakpoints have been cleared"
elif [ $(echo $1 | grep '^[0-9]*') ]; then
_linebp=($(echo $(for i in ${_linebp[*]}; do
if (( $1 != $i )); then echo $i; fi; done) ))
_msg "Breakpoint cleared at line $1"
else
_msg "Please specify a numeric line number"
fi
}
The structure of the code is similar to that used for setting the breakpoints. If no argument was supplied to the command, the breakpoint array is unset, effectively deleting all the breakpoints. If an argument was supplied and is not a number, we print out an error message and exit.
A numeric argument to the cb command means the code has to search the list of breakpoints and delete the specified one. We can easily make the deletion by following a procedure similar to the one we used when we added a breakpoint in _setbp. We execute a loop in a subshell, printing out the line numbers in the breakpoints list and ignoring any that match the provided argument. The echoed values once again form a compound statement, which can then be assigned to an array variable.[16]
The function _at_linenumbp is called by _steptrap after every statement; it checks whether the shell has arrived at a line number breakpoint. The code for the function is:
# See if this line number has a breakpoint
function _at_linenumbp
{
local i=0
if [ "$_linebp" ]; then
while (( $i < ${#_linebp[@]} )); do
if (( ${_linebp[$i]} == $_curline )); then
return 0
fi
let i=$i+1
done
fi
return 1
}
The function simply loops through the breakpoint array and checks the current line number against each one. If a match is found, it returns true (i.e., returns 0). Otherwise, it continues looping, looking for a match until the end of the array is reached. It then returns false.
It is possible to find out exactly what line the debugger is up to and where the breakpoints have been set in the guinea pig by using the ds command. We'll see an example of the output later, when we run a sample bashdb debugging session. The code for this function is fairly straightforward:
# Print out the shell script and mark the location of breakpoints
# and the current line
function _displayscript
{
local i=1 j=0 bp cl
( while (( $i < ${#_lines[@]} )); do
if [ ${_linebp[$j]} ] && (( ${_linebp[$j]} == $i )); then
bp='*'
let j=$j+1
else
bp=' '
fi
if (( $_curline == $i )); then
cl=">"
else
cl=" "
fi
echo "$i:$bp $cl ${_lines[$i]}"
let i=$i+1
done
) | more
}
This function contains a subshell, the output of which is piped to the UNIX more command. We have done this for user-friendly reasons; a long script would scroll up the screen quickly and the users may not have displays that allow them to scroll back to previous pages of screen output. more displays one screenful of output at a time.
The core of the subshell code loops through the lines of the guinea pig script. It first tests to see if the line it is about to display is in the array of breakpoints. If it is, a breakpoint character (*) is set and the local variable j is incremented. j was initialized to 0 at the beginning of the function; it contains the current breakpoint that we are up to. It should now be apparent why we went to the trouble of sorting the breakpoints in _setbp: both the line numbers and the breakpoint numbers increment sequentially, and once we pass a line number that has a breakpoint and find it in the breakpoint array, we know that future breakpoints in the script must be further on in the array. If the breakpoint array contained line numbers in a random order, we'd have to search the entire array to find out if a line number was in the array or not.
The core of the subshell code then checks to see if the current line and the line it is about to display are the same. If they are, a "current line" character (>) is set. The current displayed line number (stored in i), breakpoint character, current line character, and script line are then printed out.
We think you'll agree that the added complexity in the handling of breakpoints is well worth it. Being able to display the script and the location of breakpoints is an important feature in any debugger.
Break conditions
bashdb provides another method of breaking out of the guinea pig script: the break condition. This is a string that the user can specify that is evaluated as a command; if it is true (i.e., returns exit status 0), the debugger enters the command loop.
Since the break condition can be any line of shell code, there's a lot of flexibility in what can be tested. For example, you can break when a variable reaches a certain value—e.g., (( $x < 0 ))—or when a particular piece of text has been written to a file (grep string file). You will probably think of all kinds of uses for this feature.[17]To set a break condition, type bc string. To remove it, type bc without arguments—this installs the null string, which is ignored.
_steptrap evaluates the break condition $_brcond only if it's not null. If the break condition evaluates to 0, then the if clause is true and, once again, _steptrap calls the command loop.
Execution tracing
The final feature of the debugger is execution tracing, available with the x command.
The function _xtrace "toggles" execution tracing simply by assigning to the variable _trace the logical "not" of its current value, so that it alternates between 0 (off) and 1 (on). The preamble initializes it to 0.
Debugger limitations
We have kept bashdb reasonably simple so that you can see the fundamentals of building a shell script debugger. Although it contains some useful features and is designed to be a real tool, not just a scripting example, it has some important limitations. Some are described in the list that follows.
- Debuggers tend to run programs slower than if they were executed on their own. bashdb is no exception. Depending upon the script you use it on, you'll find the debugger runs everything anywhere from 8 to 30 times more slowly. This isn't so much of a problem if you are stepping through a script in small increments, but bear it in mind if you have, say, initialization code with large looping constructs.
- The debugger will not "step down" into shell scripts that are called from the guinea pig. To do this, you'd have to edit your guinea pig script and change a call to scriptname to bashdb scriptname.
- Similarly, nested subshells are treated as one gigantic statement; you cannot step down into them at all.
- The guinea pig itself should not trap on the fake signals DEBUG and EXIT; otherwise the debugger won't work.
- Command error handling could be significantly improved.
Many of these are not insurmountable and you can experiment with solving them yourself; see the exercises at the end of this chapter.
The debugger from an earlier version of this book helped inspire a more comprehensive bash debugger maintained by Rocky Bernstein, which you can find at the Bash Debugger Project, http://bashdb.sourceforge.net/ .