Learning the Korn Shell

Learning the Korn ShellSearch this book
Previous: 9.1 Basic Debugging AidsChapter 9
Debugging Shell Programs
Next: 10. Korn Shell Administration
 

9.2 A Korn Shell Debugger

Commercially available debuggers give you much more functionality than the shell's set options and fake signals. The most advanced have fabulous graphical user interfaces, incremental compilers, symbolic evaluators, and other such amenities. But just about all modern debuggers-even the more modest ones-have features that enable you to "peek" into a program while it's running, to examine it in detail and in terms of its source language. Specifically, most debuggers let you do these things:

Our debugger, called kshdb, has these features and a few more. Although it's a basic tool, without too many "bells and whistles", it is real. [3] The code is available from an anonymous FTP archive, as described in Appendix C, Obtaining Sample Programs; if you don't have access to the Internet, you can type or scan the code in. Either way, you can use kshdb to debug your own shell scripts, and you should feel free to enhance it. We'll suggest some enhancements at the end of this chapter.

[3] Unfortunately, kshdb won't work completely on SunOS versions 4.1.x and older.

9.2.1 Structure of the Debugger

The code for kshdb has several features worth explaining in some detail. The most important is the basic principle on which it works: it turns a shell script into a debugger for itself, by prepending debugger functionality to it; then it runs the new script.

9.2.1.1 The driver script

Therefore the code has two parts: the part that implements the debugger's functionality, and the part that installs that functionality into the script being debugged. The second part, which we'll see first, is the script called kshdb. It's very simple:

# kshdb -- Korn Shell debugger
# Main driver: constructs full script (with preamble) and runs it

print 'Korn Shell Debugger version 1.0\n'
_guineapig=$1
if [[ ! -r $1 ]]; then		# file not found or readable
    print "Cannot read $_guineapig." >&2
    exit 1
fi
shift

_tmpdir=/tmp
_libdir=.
_dbgfile=$_tmpdir/kshdb$$	# temp file for script being debugged (copy)
cat $_libdir/kshdb.pre $_guineapig > $_dbgfile
exec ksh $_dbgfile $_guineapig $_tmpdir $_libdir "$@"

kshdb takes as argument the name of the script being debugged, which for the sake of brevity we'll call the guinea pig. Any additional arguments will be passed to the guinea pig as its positional parameters.

If the argument is invalid (the file isn't readable), kshdb exits with error status. Otherwise, after an introductory message, it constructs a temporary filename 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. [4] Also, make sure that _libdir is set to the directory where the kshdb.pre and kshdb.fns files (which we'll see soon) reside. /usr/lib is a good choice if you have access to it.

[4] All function names and variables (except those local to functions) in kshdb have names beginning with an underscore (_), to minimize the possibility of clashes with names in the guinea pig.

The cat statement builds the temp file: it consists of a file we'll see soon called kshdb.pre, which contains the actual debugger code, followed immediately by a copy of the guinea pig. Therefore the temp file contains a shell script that has been turned into a debugger for itself.

9.2.1.2 exec

The last line runs this script with exec, a statement we haven't seen 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, the shell running the above script will terminate immediately and be replaced by exec's arguments. The situations in which you would want to use exec are few, far between, and quite arcane-though this is one of them. [5]

[5] exec can also be used with an I/O redirector only; this causes the redirector to 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 entire script.

In this case, exec just runs the newly-constructed shell script, i.e., the guinea pig with its debugger, in another Korn shell. It passes the new script three arguments-the names of the original guinea pig ($_guineapig), the temp directory ($_tmpdir), and the directory where kshdb.pre and kshdb.fns are kept-followed by the user's positional parameters, if any.

9.2.2 The Preamble

Now we'll see the code that gets prepended to the script being debugged; we call this the preamble. It's kept in the following file kshdb.pre, which is also fairly simple.

# kshdb preamble
# prepended to shell script being debugged
# arguments: 
# $1 = name of original guinea-pig script
# $2 = directory where temp files are stored
# $3 = directory where kshdb.pre and kshdb.fns are stored

_dbgfile=$0
_guineapig=$1
_tmpdir=$2
_libdir=$3
shift 3				# move user's args into place

. $_libdir/kshdb.fns		# read in the debugging functions
_linebp=
_stringbp=
let _trace=0			# initialize execution trace to off
let _i=1			      # read guinea-pig file into lines array
while read -r _lines[$_i]; do
    let _i=$_i+1
done < $_guineapig

trap _cleanup EXIT		# erase files before exiting
let _steps=1			# no. of stmts to run after trap is set
LINENO=-1
trap '_steptrap $LINENO' DEBUG
:

The first few lines save the three fixed arguments in variables and shift them out of the way, so that the positional parameters (if any) are those that the user supplied on the command line as arguments to the guinea pig. Then, the preamble reads in another file, kshdb.fns, that contains the "meat" of the debugger as function definitions. We put this code in a separate file to minimize the size of the temp file. We'll examine kshdb.fns shortly.

Next, kshdb.pre initializes the two breakpoint lists to empty and execution tracing to off (see below), then reads the guinea pig into an array of lines. We do the latter so that the debugger can access lines in the script when performing certain checks, and so that the execution trace feature can print lines of code as they execute.

The real fun begins in the last group of code lines, where we set up the debugger to start working. We use two trap commands with fake signals. The first sets up a cleanup routine (which just erases the temporary file) to be called on EXIT, i.e., when the script terminates for any reason. The second, and more important, sets up the function _steptrap to be called after every statement.

_steptrap gets an argument that evaluates to the number of the line in the guinea pig that just ran. We use the same technique with the built-in variable LINENO that we saw earlier in the chapter, but with an added twist: if you assign a value to LINENO, it uses that as the next line number and increments from there. The statement LINENO=-1 re-starts line numbering so that the first line in the guinea pig is line 1.

After the DEBUG trap is set, the preamble ends with a "do-nothing" statement (:). The shell executes this statement and enters _steptrap for the first time. The variable _steps is set up so that _steptrap executes its last elif clause, as you'll see shortly, and enters the debugger. As a result, execution halts just before the first statement of the guinea pig is run, and the user sees a kshdb> prompt; the debugger is now in full operation.

9.2.3 Debugger Functions

The function _steptrap is the entry point into the debugger; it is defined in the file kshdb.fns, which is given in its entirety at the end of this chapter. Here is _steptrap:

# Here after each statement in script being debugged.
# Handle single-step and breakpoints.
function _steptrap {
    _curline=$1                       # arg is no. of line that just ran

    (( $_trace )) && _msg "$PS4 line $_curline: ${_lines[$_curline]}"

    if (( $_steps >= 0 )); then       # if in step mode
        let _steps="$_steps - 1"      # decrement counter 
    fi

    # first check if line num or string breakpoint reached
    if _at_linenumbp || _at_stringbp; then
        _msg "Reached breakpoint at line $_curline"
        _cmdloop                       # breakpoint, enter debugger
                                        
    # if not, check whether break condition exists and is true
    elif [[ -n $_brcond ]] && eval $_brcond; then
        _msg "Break condition $_brcond true at line $_curline"
        _cmdloop

    # next, check if step mode and number of steps is up
    elif (( $_steps == 0 )); then      # if step mode and time to stop
        _msg "Stopped at line $_curline"
        _cmdloop                       # enter debugger

    fi
}

_steptrap starts by setting _curline to the number of the guinea pig line that just ran. If execution tracing is turned on, it prints the PS4 execution trace prompt (a la xtrace mode), the line number, and the line of code itself.

Then it does one of two things: enter the debugger, the heart of which is the function _cmdloop, or just return so that the shell can execute the next statement. It chooses the former if a breakpoint or break condition (see below) has been reached, or if the user stepped into this statement.

9.2.3.1 Commands

We'll explain shortly how _steptrap determines these things; now we'll look at _cmdloop. It's a typical command loop, resembling a combination of the case statements we saw in Chapter 5 and the calculator loop we saw in the previous chapter.

# Debugger command loop.
# Here at start of debugger session, when breakpoint reached,
# or after single-step.
function _cmdloop {
    typeset cmd args

    while read -s cmd"?kshdb> " args; do
        case $cmd in
            \*bp ) _setbp $args ;;  # set breakpoint at line num or string.

            \*bc ) _setbc $args ;;  # set break condition.

            \*cb ) _clearbp ;;      # clear all breakpoints.

            \*g  ) return ;;        # start/resume execution

            \*s  ) let _steps=${args:-1} # single-step N times (default 1) return ;;

            \*x  ) _xtrace ;;        # toggle execution trace

            \*\? | /*h ) _menu ;;    # print command menu        

            \*q  ) exit ;;           # quit

            \**  ) _msg "Invalid command: $cmd" ;; 

            *  ) eval $cmd $args ;;  # otherwise, run shell command

        esac
    done
}

At each iteration, cmdloop prints a prompt, reads a command, and processes it. We use read -s so that the user can take advantage of command-line editing within kshdb. All kshdb commands start with * to prevent confusion with shell commands. Anything that isn't a kshdb command (and doesn't start with *) is passed off to the shell for execution. Table 9.3 summarizes the debugger commmands.

Table 9.3: kshdb Commands
CommandAction
*bp NSet breakpoint at line N
*bp strSet breakpoint at next line containing str
*bpList breakpoints and break condition
*bc strSet break condition to str
*bcClear break condition
*cbClear all breakpoints
*gStart or resume execution
*s [N]Step through N statements (default 1)
*xToggle execution tracing
*h, *?Print a help menu
*qQuit

Before we look 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 ... DEBUG statement 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. [6]

[6] In fact, low-level systems programmers can think of the entire trap mechanism as quite similar to an interrupt-handling scheme.

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 runs, and the user stays in control, until the user types *g, *s, or *q. Let's look in detail at what happens in each of these cases.

*g has the effect of running the guinea pig uninterrupted until it finishes or hits a breakpoint. But actually, it simply exits the command loop and returns to _steptrap, which exits as well. The shell takes control back; it runs the next statement in the guinea pig script and calls _steptrap again. Assuming there is no breakpoint, this time _steptrap will just exit again, and the process will repeat until there is a breakpoint or the guinea pig is done.

9.2.3.2 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 sees that _steps is 1 and decrements it to 0. Then the second elif conditional sees that _steps is 0, so it prints a "stopped" message and 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:

  1. 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.

  2. This process repeats, another step in the guinea pig is run, and _steps becomes 1.

  3. 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 three steps run and then the debugger takes over again.

Finally, the *q command calls the function _cleanup, which just erases the temp file and exits the entire program.

All other debugger commands (*bp, *bc, *cb, *x and shell commands) cause the shell to stay in the command loop, meaning that the user prolongs the "interruption" of the shell.

9.2.3.3 Breakpoints

Now we'll examine the breakpoint-related commands and the breakpoint mechanism in general. The *bp command calls the function _setbp, which can set two kinds of breakpoints, depending on the type of argument given. If it is a number, it's treated as a line number; otherwise it's interpreted as a string that the breakpoint line should contain.

For example, the command *bp 15 sets a breakpoint at line 15, and *bp grep sets a breakpoint at the next line that contains the string grep-whatever number that turns out to be. Although you can always look at a numbered listing of a file, [7] string arguments to *bp can make that unnecessary.

[7] pr -n filename prints a numbered listing to standard output on System V-derived versions of UNIX. Some older BSD-derived systems don't support it. If this doesn't work on your system, try cat -n filename, or if that doesn't work, create a shell script with this single line:

awk '{ print NR, "\t", $0 }' $1

Here is the code for _setbp:

# Set breakpoint(s) at given line numbers and/or strings
# by appending lines to breakpoint file
function _setbp {
    if [[ -z $1 ]]; then
        _listbp
    elif [[ $1 = +([0-9]) ]]; then  # number, set bp at that line
        _linebp="${_linebp}$1|"
        _msg "Breakpoint at line " $1
    else                            # string, set bp at next line w/string
        _stringbp="${_stringbp}$@|"
        _msg "Breakpoint at next line containing $@."
    fi
}

_setbp sets the breakpoints by storing them in the variables _linebp (line number breakpoints) and _stringbp (string breakpoints). Both have breakpoints separated by pipe character delimiters, for reasons that will become clear shortly. This implies that breakpoints are cumulative; setting new breakpoints does not erase the old ones.

The only way to remove breakpoints is with the command *cb, which (in function _clearbp) clears all of them at once by simply resetting the two variables to null. If you don't remember what breakpoints you have set, the command *bp without arguments lists them.

The functions _at_linenumbp and _at_stringbp are called by _steptrap after every statement; they check whether the shell has arrived at a line number or string breakpoint, respectively.

Here is _at_linenumbp:

# See if next line no. is a breakpoint.
function _at_linenumbp {
    [[ $_curline = @(${_linebp%\|}) ]]
}

_at_linenumbp takes advantage of the pipe character as the separator between line numbers: it constructs a regular expression of the form @(N1|N2|...) by taking the list of line numbers _linebp, removing the trailing |, and surrounding it with @( and ). For example, if $_linebp is 3|15|19|, then the resulting expression is @(3|15|19).

If the current line is any of these numbers, then the conditional becomes true, and _at_linenumbp also returns a "true" (0) exit status.

The check for a string breakpoint works on the same principle, but it's slightly more complicated; here is _at_stringbp:

# Search string breakpoints to see if next line in script matches.
function _at_stringbp {
    [[ -n $_stringbp && ${_lines[$_curline]} = *@(${_stringbp%\|})* ]]
}

The conditional first checks if $_stringbp is non-null (meaning that string breakpoints have been defined). If not, the conditional evaluates to false, but if so, its value depends on the pattern match after the &&-which tests the current line to see if it contains any of the breakpoint strings.

The expression on the right side of the equal sign is similar to the one in _at_linenumbp above, except that it has * before and after it. This gives expressions of the form *@(S1|S2|...)*, where the Ss are the string breakpoints. This expression matches any line that contains any one of the possibilities in the parenthesis.

The left side of the equal sign is the text of the current line in the guinea pig. So, if this text matches the regular expression, then we've reached a string breakpoint; accordingly, the conditional expression and _at_stringbp return exit status 0.

_steptrap uses the || ("or") construct in its if statement, which evaluates to true if either type of breakpoint occurred. If so, it calls the main command loop.

9.2.3.4 Break conditions

kshdb has another feature related to breakpoints: 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 lots 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. [8] 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 non-null. If the break condition evaluates to 0, then the if clause is true and, once again, _steptrap calls the command loop.

[8] Bear in mind that if your break condition produces any standard output (or standard error), you will see it after every statement. Also, make sure your break condition doesn't take a long time to run; otherwise your script will run very, very slowly.

9.2.3.5 Execution tracing

The final feature is execution tracing, available through the *x command. This feature is meant to overcome the fact that a kshdb user can't use set -o xtrace while debugging (by entering it as a shell command), because its scope is limited to the _cmdloop function.

The function _xtrace "toggles" execution tracing by simply 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.

9.2.3.6 Limitations

kshdb was not designed to push the state of the debugger art forward or to have an overabundance of features. It has the most useful basic features, its implementation is compact and (we hope) comprehensible, and it does have some important limitations. The ones we know of are described in the list that follows.

  1. The shell should really have the ability to trap before each statement, not after. This is the way most commercial source-code debuggers work. [9] At the very least, the shell should provide a variable that contains the number of the line about to run instead of (or in addition to) the number of the line that just ran.

    [9] This kind of functionality is expected to be added in the next Korn shell release.

  2. String breakpoints cannot begin with digits or contain pipe characters (|) unless they are properly escaped.

  3. You can only set breakpoints-whether line number or string-on lines in the guinea pig that contain what the shell's documentation calls simple commands, i.e., actual UNIX commands, shell built-ins, function calls, or aliases. If you set a breakpoint on a line that contains only whitespace or a comment, the shell will always skip over that breakpoint. More importantly, control keywords like while, if, for, do, done, and even conditionals ([[...]] and ((...))) won't work either, unless a simple command is on the same line.

  4. kshdb will not "step down" into shell scripts that are called from the guinea pig. To do this, you have to edit your guinea pig and change a call to scriptname to kshdb scriptname.

  5. Similarly, nested subshells are treated as one gigantic statement; you cannot step down into them at all.

  6. The guinea pig should not trap on the fake signals DEBUG or EXIT; otherwise the debugger won't work.

  7. Variables that are typeset (see Chapter 4, Basic Shell Programming) are not accessible in break conditions. However, you can use the shell command print to check their values.

  8. Command error handling is weak. For example, a non-numeric argument to *s will cause it to bomb.

Many of these are not insurmountable; see the exercises.

9.2.4 Sample kshdb Session

Now we'll show a transcript of an actual session with kshdb, in which the guinea pig is the solution to Task 6-2. For convenience, here is a numbered listing of the script, which we'll call lscol.

1 	 set -A filenames $(ls $1)
2 	 typeset -L14 fname
3 	 let count=0
4 	 let numcols=5
5 	 
6 	 while [[ $count -lt ${#filenames[*]} ]]; do
7 	     fname=${filenames[$count]}
8 	     print -n "$fname  " 
9 	     let count="count + 1"
10 	     if [[ $((count % numcols)) = 0 ]]; then
11 	         print            # NEWLINE
12 	     fi
13 	 done
14 	 
15 	 if [[ $((count % numcols)) != 0 ]]; then
16 	     print
17 	 fi

Here is the kshdb session transcript:

$ kshdb lscol /usr/spool
Korn shell Debugger version 1.0

Stopped at line 0
kshdb> *bp 4
Breakpoint at line 4
kshdb> *g
Reached breakpoint at line 4
kshdb> print $count $numcols
0 5
kshdb> *bc [[ $count -eq 10 ]]
Break when true: [[ $count -eq 10 ]]
kshdb> *g
bwnfs           cron            locks           lpd             lpd.lock 
mail            mqueue          rwho            secretmail      uucp
Break condition [[ $count -eq 10 ]] true at line 9
kshdb> *bc
Break condition cleared.
kshdb> *bp NEWLINE
Breakpoint at next line containing "NEWLINE".
kshdb> *g

Reached breakpoint at line 11
kshdb> print $count
10
kshdb> let count=9
kshdb> *g
uucp            
Reached breakpoint at line 11
kshdb> *bp 
Breakpoints at lines:
 4 
Breakpoints at strings:
NEWLINE 
No break condition.
kshdb> *g 
uucppublic      
$

First, notice that we gave the guinea pig script the argument /usr/spool, meaning that we want to list the files in that directory. We begin by setting a simple breakpoint at line 4 and starting the script. It stops after executing line 4 (let numcols=5). Then we issue a shell print command to show that the variables count and numcols are indeed set correctly.

Next, we set a break condition, telling the debugger to kick in when $count is 10, and we resume execution. Sure enough, the guinea pig prints 10 filenames and stops at line 9, on which $count is incremented. We clear the break condition by typing *bc without an argument, since otherwise the shell would stop after every statement until the condition becomes false.

The next command shows how the string breakpoint mechanism works. We tell the debugger to break when it hits a line that contains the string NEWLINE. This string is in a comment on line 11. Notice that it doesn't matter that the string is in a comment-just that the line it's on contain an actual command. We resume execution, and the debugger hits the breakpoint at line 11.

After that, we show how we can use the debugger to change the guinea pig's state while running. We see that $count is still 10; we change it to 9. In the next iteration of the while loop, the script accesses the same filename that it just did (uucp), increments count back to 10, and hits the breakpoint again. Finally, we list breakpoints and let the script execute to its end; it prints out one last filename and exits.

9.2.5 Exercises

We'll conclude this chapter with a few exercises, which are suggested enhancements to kshdb.

  1. Improve command error handling in these ways:

    1. For numeric arguments to *bp, check that they are valid line numbers for the particular guinea pig.

    2. Check that arguments to *s are valid numbers.

    3. Any other error handling you can think of.

  2. Enhance the *cb command so that the user can delete specific breakpoints (by string or line number).

  3. Remove the major limitation in the breakpoint mechanism:

    1. Improve it so that if the line number selected does not contain an actual UNIX command, the next closest line above it is used as the breakpoint instead.

    2. Do the same thing for string breakpoints. (Hint: first translate each string breakpoint command into one or more line-number breakpoint commands.)

  4. Implement an option that causes a break into the debugger whenever a command exits with non-0 status:

    1. Implement it as the command-line option -e.

    2. Implement it as the debugger commands *be (to turn the option on) and *ne (to turn it off). (Hint: you won't be able to use the ERR trap, but bear in mind that when you enter _steptrap, $? is still the exit status of the last command that ran.)

  5. Add the ability to "step down" into scripts that the guinea pig calls (i.e., non-nested subshells) as the command-line option -s. One way of implementing this is to change the kshdb script so that it "plants" recursive calls to kshdb in the guinea pig. You can do this by filtering the guinea pig through a loop that reads each line and determines, with the whence -v and file(1) (see the man page) commands, if the line is a call to another shell script.[10] If it is, prepend kshdb -s to the line and write it to the new file; if not, just pass it through as is.

    [10] Notice that this method should catch most nested shell scripts but not all of them. For example, it won't catch shell scripts that follow semicolons (e.g., cmd1; cmd2).

  6. Add support for multiple break conditions, so that kshdb stops execution when any one of them becomes true and prints a message that says which one is true. Do this by storing the break conditions in a colon-separated list or an array. Try to make this as efficient as possible, since the checking will take place after every statement.

  7. Add any other features you can think of.

If you add significant functionality to kshdb, we invite you to send your version to the author, care of O'Reilly and Associates, at on the Internet or, via US Mail, at:

O'Reilly & Associates, Inc.
103 Morris St., Suite A
Sebastopol, CA 95472

We'll select the best one and publish it in the next revision of our UNIX Power Tools CD-ROM. Remember: there is no "official" Korn shell debugger, and as more and more programmers realize how powerful the Korn shell is as a programming environment, a debugger will become more and more necessary. We've made the initial effort, and we leave it up to you to finish the job!

Finally, here is the complete source code for the debugger function file kshdb.fns:

# Here after each statement in script being debugged.
# Handle single-step and breakpoints.
function _steptrap {
    _curline=$1                       # arg is no. of line that just ran
    (( $_trace )) && _msg "$PS4 line $_curline: ${_lines[$_curline]}"
    if (( $_steps >= 0 )); then       # if in step mode
        let _steps="$_steps - 1"      # decrement counter 
    fi
    # first check if line num or string breakpoint reached
    if _at_linenumbp || _at_stringbp; then
        _msg "Reached breakpoint at line $_curline"
        _cmdloop                      # breakpoint, enter debugger
                                        
    # if not, check whether break condition exists and is true
    elif [[ -n $_brcond ]] && eval $_brcond; then
        _msg "Break condition $_brcond true at line $_curline"
        _cmdloop
    # next, check if step mode and number of steps is up
    elif (( $_steps == 0 )); then     # if step mode and time to stop
        _msg "Stopped at line $_curline"
        _cmdloop                      # enter debugger
    fi
}

# Debugger command loop.
# Here at start of debugger session, when breakpoint reached,
# or after single-step.
function _cmdloop {
    typeset cmd args

    while read -s cmd"?kshdb> " args; do
        case $cmd in
            \*bp ) _setbp $args ;;    # set breakpoint at line num or string.
            \*bc ) _setbc $args ;;    # set break condition.
            \*cb ) _clearbp ;;        # clear all breakpoints.
            \*g  ) return ;;            # start/resume execution
            \*s  ) let _steps=${args:-1} # single-step N times (default 1)
                   return ;;
        
            \*x  ) _xtrace ;;           # toggle execution trace
            \*\? | \*h ) _menu ;;       # print command menu        
            \*q  ) exit ;;              # quit
            \**  ) _msg "Invalid command: $cmd" ;; 
            *  ) eval $cmd $args ;;     # otherwise, run shell command
        esac
    done
}
 
# See if next line no. is a breakpoint.
function _at_linenumbp {
    [[ $_curline = @(${_linebp%\|}) ]]
}

# Search string breakpoints to see if next line in script matches.
function _at_stringbp {
    [[ -n $_stringbp && ${_lines[$_curline]} = *@(${_stringbp%\|})* ]]
}

# Print the given message to standard error.
function _msg {
    print "$@" >&2
}

# Set breakpoint(s) at given line numbers and/or strings
# by appending lines to breakpoint file
function _setbp {
    if [[ -z $1 ]]; then
        _listbp
    elif [[ $1 = +([0-9]) ]]; then  # number, set bp at that line
        _linebp="${_linebp}$1|"
        _msg "Breakpoint at line " $1
    else                            # string, set bp at next line w/string
        _stringbp="${_stringbp}$@|"
        _msg "Breakpoint at next line containing $@."
    fi
}

# List breakpoints and break condition.
function _listbp {
    _msg "Breakpoints at lines:"
    _msg "$(print $_linebp | tr '|' ' ')" 
    _msg "Breakpoints at strings:"
    _msg "$(print $_stringbp | tr '|' ' ')"
    _msg "Break on condition:"
    _msg "$_brcond"
}

# Set or clear break condition
function _setbc {
    if [[ -n "$@" ]]; then
        _brcond=$args   
        _msg "Break when true: $_brcond"
    else
        _brcond=
        _msg "Break condition cleared."
    fi
}

# Clear all breakpoints.
function _clearbp {
    _linebp=
    _stringbp=
    _msg "All breakpoints cleared."
}

# Toggle execution trace feature on/off
function _xtrace {
    let _trace="! $_trace"
    _msg "Execution trace \c"
    if (( $_trace )); then
        _msg "on."
    else
        _msg "off."
    fi
}

# Print command menu
function _menu {
    _msg 'kshdb commands: 
         *bp N               set breakpoint at line N
         *bp str             set breakpoint at next line containing str
         *bp                 list breakpoints and break condition 
         *bc str             set break condition to str 
         *bc                 clear break condition 
         *cb                 clear all breakpoints 
         *g                  start/resume execution 
         *s [N]              execute N statements (default 1)
         *x                  toggle execution trace on/off 
         *h, *?              print this menu 
         *q                  quit'
}

# Erase temp files before exiting.
function _cleanup {
    rm $_dbgfile 2>/dev/null
}


Previous: 9.1 Basic Debugging AidsLearning the Korn ShellNext: 10. Korn Shell Administration
9.1 Basic Debugging AidsBook Index10. Korn Shell Administration

The UNIX CD Bookshelf NavigationThe UNIX CD BookshelfUNIX Power ToolsUNIX in a NutshellLearning the vi Editorsed & awkLearning the Korn ShellLearning the UNIX Operating System