Chapter 18. Debugging Scripts

A description of debugging techniques could easily fill an entire book or more—and rightfully so. In any software development, debugging often takes more time than the programming (not to mention design[59]).

The need for debugging is even more exaggerated in Tcl where many people code while thinking. The interpretive nature of the language makes this seductively easy.

Good design principles can help avoid getting stuck in a quagmire. Careful thought can prevent many common traps. In the same way, careful (and imaginative) thought is helpful in solving problems. Some people claim never to use debuggers. I am not one of them, but sometimes it helps to walk away from the keyboard and simply think through what could possibly be happening—or what cannot possibly be happening but is anyway.

Tracing

In this section, I will briefly recap some useful techniques and mention a few more that are very useful for tracking down problems. All of these have to do with tracing control. I will go into other debugging techniques later.

Simple output commands (using puts and send) can go a long way towards finding problems. However, putting commands in and taking them out is a hassle. You can conditionalize their execution with a variable.

if (verbose) {
    puts ". . ."
}

Rather than having raw if/puts commands throughout your code, it is cleaner to isolate them in one place, such as a procedure called vprint (for “verbose print”).

proc vprint {msg} {
    global verbose

    if {$verbose} {puts "$msg"}
}

Later if you decide to redirect output to a file or window, you only have to change the one output command in vprint.

This idea can be augmented in many different ways. For example, the following definition prints the procedure name before the message.

proc vprint {msg} {
    global verbose

    if {$verbose} {
        puts "[lindex [info level −1] 0]: $msg"
    }
}

Logging

It is often useful to write information to files so that you can study it later. You can write log files yourself or you can use Expect’s logging functions.

The commands log_user, log_file, and exp_internal can all be helpful while debugging. These commands can also be controlled indirectly through procedures similar to the puts example above.

I will summarize what these commands do. The log_user command controls whether the output of spawned processes is seen. In most scripts, you want to leave this set one way or the other, but it is nice to have the flexibility to turn it off and on during development. The log_user command is described further in Chapter 7 (p. 171).

The log_file command is related to the log_user command. However, log_file has almost no uses other than for debugging. The log_file command records everything from a spawned process. Even output suppressed via "log_user 0" can be recorded. The log_file command is further described in Chapter 7 (p. 176).

The exp_internal command is another command that is useful only for debugging. The exp_internal command enables the printing of internal information, mostly concerning pattern matching. This command was discussed in Chapter7 (p. 161).

In Chapter 17 (p. 379), I described how to use mail to save information. This technique can be very useful because you can get an immediate indication as soon as the mail has been generated. With the log commands, you have to remember to look at the files they create. For example, in Chapter 1 (p. 13) I described a script that regularly checks a set of modems. If the script encounters a problem that it cannot resolve, it sends mail rather than simply logging the error to a file. This way the problem is brought to an administrator’s attention immediately.

Command Tracing

The strace command is yet another command that is useful only for debugging. The command enables the printing of commands before they are executed. Here is what it looks like when commands are traced.

expect1.2> set foo [split "a b [expr 10+5]"]
 3      expr 10+5
 2    split "a b [expr 10+5]"
 1  set foo [split "a b [expr 10+5]"]
a b 15

Each command is prefixed by an integer describing the depth of the evaluation stack. Each bracketed command increases the evaluation stack. In this example, you can see the expr command being evaluated at the third stack level, the split command being evaluated at the second stack level, and the set command being evaluated at the first level.

Each command is indented by two spaces for each additional level in the stack. The precise level numbers are not that important except as a guide to bounding the amount of information that is produced.

The strace command takes an integer argument that bounds the depth to which commands are traced. For example, the following command traces only commands executed at the first three levels.

strace 3

This could be useful when there are a lot of subroutines being executed and you want to ignore, for example, many iterations of a loop at some deep level. Unfortunately, this control is not very fine. In practice, I often just use a very high value no matter what kind of script I am using. It is simply not worth the effort of figuring out a precise cut-off. This command is likely to be revised in the future.[60]

Given the flag -info, the strace command returns the current depth to which it is tracing.

Variable Tracing

Tcl supports the ability to invoke procedures when variables are read or written. This can be useful in regular programming (for example, Tk makes extensive use of tracing) but it is especially useful during debugging.

For example, the following command traces any assignments to the array expect_out.

trace variable expect_out w traceproc

The w argument in the command stands for “write”. The last argument is a procedure which is called whenever the variable is written (i.e., assigned to). Instead of w, you can also put r (read) or u (unset). A variable is unset when the unset command is used or the variable goes out of scope.

All trace procedures must be declared with three parameters. The first parameter is the variable name, the second is the element name (if the variable is an array), and the third is the letter r, w, or u, depending on if the variable is being read, written, or unset.

The usual thing to do with a trace procedure when debugging is to print out the variable. Here is a trace procedure to do that. The procedure prints the variable name followed by the value. The type argument is ignored since it will always be w in this example.

proc traceproc {array element type} {
    upvar [set array]($element) var
    puts "new value of [set array]($element) is $var"
}

The trace procedure executes in the context of the scope in which the variable was accessed, so the upvar command is necessary. It gets the variable from the caller’s scope, where it is meaningful.

The array name is stored in array and the element name is stored in element. The expression "[set array]($element)" produces the compound name. The more obvious $array($element) would return the value and not the name.

Here is what it looks like when I trace the array expect_out and type the string "hello world" to an expect command that is just looking for "world“:

expect1.2> trace variable expect_out w traceproc
expect1.3> expect world hello world
new value of expect_out(0,string) is world
new value of expect_out(spawn_id) is 0
new value of expect_out(buffer) is hello world
expect1.4>

Tracing can also be limited to particular array elements. The following command creates a trace only on the element named buffer in the expect_out array.

expect1.5> trace variable expect_out(buffer) w traceproc

Tracing variables that are not arrays is a little simpler. The second argument to the trace procedure is just an empty string and can be ignored. Here is a trace procedure for scalar variables.

proc traceproc_simple {v null type} {
    upvar $v var
    puts "new value of $v is $var"
}

Using this procedure, you can trace non-array variables. Here are some examples:

trace variable spawn_id w traceproc_simple
trace variable timeout w traceproc_simple

You can associate the same trace procedure with multiple variables, and you can also associate multiple trace procedures with the same variable. They will all be triggered (sequentially of course). You can even associate the same trace procedure multiple times with the same variable.

Example—Logging By Tracing

The log_file command does not provide any special support for writing the output of different processes to different log files. This can be simulated by tracing the expect_out array. Remember that the spawn_id element identifies the particular spawn id that produced the output.

The following trace procedure writes the value the expect_out(buffer) to a file specific to the spawn id.

proc log_by_tracing {array element op} {
    uplevel {
        global logfile
        set file $logfile($expect_out(spawn_id))
        puts -nonewline $file $expect_out(buffer)
    }
}

The association between the spawn id and each log file is made in the array logfile which contains a pointer to the log file based on the spawn id. Such an association could be made with the following code when each process is spawned.

spawn ...
set logfile($spawn_id) [open ... w]

The trace is armed in the usual way:

trace variable expect_out(buffer) w log_by_tracing

Internally, the expect command saves the spawn_id element of expect_out after the X,string elements but before the buffer element. For this reason, the trace must be triggered by the buffer element rather than the spawn_id element. A trace triggered on expect_out(spawn_id) will see an old value of expect_out(buffer).

In Chapter 6 (p. 147), I described how process output could be discarded if more arrived than was permitted by match_max. In fact, a copy of the output is saved in expect_out(buffer) before it is removed from the internal buffer. So even if the expect command does not pass control to a user-supplied action, the buffer to be discarded can be saved with the trace command just shown.

Saving output from the interact command in the same fashion is a little trickier. The interact command is optimized for speed and there is no automatic saving of the spawn id or buffer. This can be simulated but requires the use of explicit actions and flags.

UNIX System Call Tracing

Before I finish with tracing, I will mention one more tracing aid—for UNIX system calls. Tracing UNIX system calls is totally outside Tcl and yet that is exactly the reason why it is so valuable. Systems calls are precisely the points at which Tcl interacts with the outside world.

By tracing system calls, you can see files being opened and accessed. You can see when your process is sleeping or creating new processes. You can see signals being sent or received. This kind of information is very useful because, after all, programs are run for the side-effects that they have on the outside world. By tracing system calls, you get an idea of all the side-effects that your script is causing.

I will not go into a lot of detail here because so much of this is system specific. However, there are three programs that are particularly useful. truss is a very flexible system call tracer that is found on System V derivatives. trace is a similar program that is found on BSD-based systems. strace is a public-domain version of trace that is a lot better than the original. I highly recommend getting familiar with at least one of these tools.

Tk And tkinspect

The Tk extension to Tcl provides commands that are primarily used for the purpose of building graphic user interfaces. However, one Tk command that has nothing to do with graphics is the send command.[61]Tk’s send command sends a command from one Tk application to another. The command is evaluated by the second application and returned to the first.

An obvious command to send to an application is "set foo" to find out, for example, what the value of foo is. To do this, all you have to do is send the command from another Tk application. Below, I typed the shell command to start expectk—a program that combines Expect and Tk. Using expectk, I then sent to the application named frogger a request to get the value of the variable named frogcount:

% expectk
expect1.1> send frogger set frogcount
17

Because X applications typically spend most of their life waiting in an event loop, applications in Tk are almost always ready to respond to send requests. The applications do not have to do anything special to handle them. Most importantly, applications do not have to be stopped to handle them. Tk applications process send requests as they are received. This makes the send command quite useful for debugging. Without any preparation, you can pry into the internal data structures of scripts while they are running!

In fact, you are not restricted to looking at variables. You can change variables. You can execute procedures. You can even redefine procedures as the script is running. Although you may not want to do this last type of operation frequently, all of the commands mentioned previously in this chapter are good candidates for sending to an application that needs to be debugged.

To facilitate the use of send for debugging purposes, I recommend tkinspect. Written by Sam Shen, tkinspect provides a graphic browser to all the global variables and procedures in all the Tk-based applications currently running. You can scroll through variables and values, and change them just by pointing at and editing them using emacs-style commands. At any time, you can send back new values or commands by pressing the send button. tkinspect is so easy to use that you may find yourself using it on bug-free programs simply because tkinspect provides a direct view into Tk programs. The Tcl FAQ describes how to obtain tkinspect. For more information on the FAQ, see Chapter 1 (p. 20).

Despite my enthusiasm for send and tkinspect, there are several drawbacks. You can see only global variables. You cannot stop the application to look at variables that are frequently changing. And you may have to switch from using Expect (or some derivative) to Expectk. Chapter 19 (p. 425) explains the changes that are required to run Expect scripts in Expectk.

Tk’s send command is really worth exploring. I have brought it up here in the context of debugging, but the ability to have applications send arbitrary commands to each other is very powerful. Although the interprocess communication that send uses is built in to X, Tk brings this capability out in such a way that it is very easy to use. The send command is much more than a debugging tool.

Traditional Debugging

Expect offers a debugging mechanism that is similar to traditional source-level debuggers such as dbx and gdb. From now on, I will call it the debugger. This particular debugger is just one of several that are available as separate extensions to Tcl. Read the Tcl FAQ to learn about other Tcl debuggers.

The debugger can be started with the command "debug 1" or by starting Expect itself with the command-line flag "-D“. The -D flag is just shorthand for a form of the debug command, so I will talk about debug first.

The debug command can be used to start or stop the debugger. With an argument of 1, the debugger is started. It is stopped with an argument of 0.

debug 1

When the debugger starts running, it prints the next command to be executed and then prompts you for what to do next. At this point you can do things like single-step the script, set breakpoints, or continue the script. You can also enter any Expect or Tcl commands as well.

The following Tcl commands illustrate that the debugger evaluates Tcl commands as usual.

dbg2.1> set m {a b c}
a b c
dbg2.2> llength $m
3

The debugger interaction looks a lot like that of the Expect interpreter command. As with the interpreter command, the second number of the prompt is the Tcl history identifier. The first number is the depth of the evaluation stack. In the context of a script, the initial depth of the evaluation stack is 1 but the debugger always introduces a new level to the stack. So the debugger prompts are always 2.X or higher.

The debug command can appear anywhere in a script. If you have a good idea of where your script is misbehaving, add the debug command at that point. If the misbehavior is dependent on some data, put the debug command in the appropriate side of an if command.

if {$a<0} {                ;# code only misbehaves when a is negative?
    debug 1            ;# turn on debugger
}

The command-line flag "-D 1" makes Expect execute a "debug 1" command before executing any commands in the script. The effect is as if you had made this command the first line in your script. This is probably not where you would actually want to start debugging; however, the flag avoids having to edit the script to manually add the command.

The -D flag also initializes a trap so that if you generate SIGINT (e.g., press ^C), the debugger takes control before the next command.

The command-line flag "-D 0" arms the trap without executing the "debug 1" command. This is convenient if you want to run your script for a while and then press ^C when you want to take control. If you want to arm a different trap or take some other action upon -D, you can replace the default trap initialization by defining the environment variable EXPECT_DEBUG_INIT with an appropriate Tcl command.

If "debug 1" is executed from an interrupt handler, control is not passed to the debugger until the next command is about to execute. By using the -now flag, the debugger takes control at the first point at which the interrupt can be fully processed. (See Chapter 14 (p. 314) for more information.)

debug -now 1

Being able to interrupt a command is particularly useful if the command is of a type that can run for a long time, such as an expect command. However, in this situation the debugger may be unable to show the current command, so -now should not be used if it is not necessary. It may be convenient for you to associate the "debug -now 1" command with a different interrupt than ^C.

The remainder of this tutorial will assume that the debugger has been started by using the command-line flag "-D 1“.

Debugger Command Overview And Philosophy

The debugger commands are:

Name

Description

s

step into procedure

n, N

step over procedure

r

return from procedure

b

set, clear, or show breakpoint

c

continue

w

show stack

u

move scope up

d

move scope down

h

help

repeat last action

The debugger commands are all one letter.[62] This is partly for convenience. Since the debugger is purely an interactive application, commands should be easy to enter. Also, scripts rarely use one-letter commands, so the chances of name conflicts between the debugger and scripted applications is very low.

The command names are very similar and, in some cases, identical to other popular debuggers (e.g., gdb, dbx). Existing Tcl procedures are directly usable, so there are no new commands, for example, to print variables since Tcl already provides such commands (e.g., set, puts, parray).

For the purposes of describing the debugger commands, I will use a script called debug-test.exp. It is shown below. The script does not do anything particularly useful. It merely serves to illustrate how the debugger is used.

set b 1

proc p4 {x} {
    return [
        expr 5+[expr 1+$x]]
}

set z [
   expr 1+[expr 2+[p4 $b]]
]

proc p3 {} {
    set m 0
}

proc p2 {} {
    set c 4
    p3
    set d 5
}

proc p1 {} {
    set a 2
    p2
    set a 3
    set a 5
}

p1
set k 7
p1

If the debugger is started at the beginning of the script, no commands have been executed. Tcl and Expect commands have global scope.

% expect -D 1 debug-test.exp
1: set b 1
dbg2.1>

When a new command is about to be executed, the debugger prints the evaluation stack level followed by the command. "set b 1" is the first line in the script. It has not yet been executed. "info exists" confirms this.

dbg2.1> info exists b
0

Stepping Over Procedure Calls

The n command executes the pending command—in this case "set b 1“—and displays the next command to be executed.

dbg2.2> n
1: proc p4 {x} {
    return [
        expr 5+[expr 1+$x]]
}
dbg2.3> info exists b
1

The command "info exists b" confirms that b has been set. The procedure p4 is about to be defined.

dbg2.4> n
4: p4 $b
dbg5.5>

The procedure p4 has now been defined. The next command to be executed is p4 itself. It appears in the command:

set z [
    expr 1+[expr 2+[p4 $b]]
]

The three sets of braces introduce three new levels on the evaluation stack; hence the stack level in which p4 is about to be executed is shown as 4.[63]

Notice that the evaluation stack level does not affect the scope. I am still in the top-level scope and b is still visible.

dbg5.5> info exists b
1

The argument to p4 is $b. The value of this variable can be evaluated by using set or puts.

dbg5.6> set b
1
dbg5.7> puts $b
1

Another n command executes p4, popping the stack one level. Additional n commands continue evaluation of the "set z" command, each time popping the stack one level. It is not necessary to enter "n" each time. Pressing return is sufficient. Expect remembers that the last action command was an n and executes that.

dbg5.8> n
3: expr 2+[p4 $b]
dbg4.9>
2: expr 1+[expr 2+[p4 $b]]
dbg3.10>
1: set z [
    expr 1+[expr 2+[p4 $b]]
]
dbg2.11>

It is often useful to skip over multiple commands embedded in a single complex command and just stop the debugger before it executes the last command. The N command does this. Here is what the interaction looks like if I restart the debugging just before the definition of p4:

1: proc p4 {x} {
    return [
        expr 5+[expr 1+$x]]
}
dbg2.2> N
1: set z [
   expr 1+[expr 2+[p4 $b]]
]
dbg2.3>

Having typed N, the debugger executes the proc command but does not stop before execution of p4. Instead, the debugger executes all the commands (p4, and expr twice) up to the set before stopping. The N command is a convenient way of stepping over complex commands such as if, for, and source commands that invoke many other commands at the current scope.

Stepping Into Procedure Calls

The n command executes a procedure atomically. In contrast, it is possible to step into a procedure with the s command. (If the command that is about to be executed is not a procedure, then s and n behave identically.)

Imagine that p4 is just about to be executed. After the s command, the debugger stops before the first command in the procedure and waits for more interactive commands.

4: p4 $b
dbg5.5> s
7: expr 1+$x
dbg8.6>

"expr 1+$x" is the first command to be executed inside of p4. It is nested inside of two brackets, plus the procedure call of p4, so the stack level is increased by three.

s, n, and N take an optional argument in the form of a number describing how many commands to execute. For example:

s 2
s 100
s $b
s [expr 2+[p4 $b]]

The arguments are evaluated according to the usual Tcl rules because s, n, and N are commands known to Tcl.

The debugger will not interrupt procedures invoked from the command line. This is usually the desired behavior although it is possible to change this.

s, n, and N are action commands. This means that the debugger remembers the last one and uses it if you press return without a command. The arguments are remembered as well. So if you want to execute 10 steps at a time, you need only enter "s 10" once and then press returns thereafter.

Where Am I

In the current scenario, I am about to execute "expr 1+$x" in the procedure p4. I can remind myself of this by displaying the stack of procedure scopes using the w command.

7: expr 1+$x
dbg8.6> w
 0: expect -D 1 debug-test.exp
*1: p4 1
 7: expr 1+1

The first line of the response describes scope 0. This is the top-level scope of the file itself, and the command used to invoke the program is shown. The second line describes scope 1 which is the invocation of procedure p4. The last line is not a scope but just repeats the evaluation stack level and the command about to be executed.

Notice that when w prints commands, they are displayed using the actual values of each parameter. In contrast, when the debugger automatically prints out the next command to be executed, the command is printed as it was originally entered in the script. For example, the debugger initially stopped and printed "expr 1+$w“, but the same instruction shows as "expr 1+1" in the output from the w command. Being able to see the values of the parameters this way is exceedingly useful.

The Current Scope

By typing "s 14“, the debugger executes fourteen steps. This brings me to the first command in procedure p3.

dbg8.8> s 14
4: set m 0
dbg5.9> w
 0: expect -D 1 debug-test.exp
 1: p1
 2: p2
*3: p3
 4: set m 0

The asterisk denotes that p3 is the current scope. I can now execute Tcl commands appropriate to the scope of p3. This includes commands that can look or operate directly in other scopes such as global, uplevel, and upvar, but it is simpler yet to move the current scope up and down the stack.

dbg5.10> uplevel {set c}
4

Moving Up And Down The Stack

The current scope can be changed by the u and d commands. u moves the current scope up, while d moves it down. Interactive variable accesses always refer to the current scope.

dbg5.11> u
dbg5.12> w
 0: expect -D 1 debug-test.exp
 1: p1
*2: p2
 3: p3
 4: set m 0
dbg5.13> set c
4

Both u and d accept an argument representing the number of scopes by which to move. For example, "u 2" moves from scope 2 to scope 0.

dbg5.14> u 2
dbg5.15> w
*0: expect -D 1 debug-test.exp
 1: p1
 2: p2
 3: p3
 4: set m 0

An absolute scope is also accepted in the form of "#" followed by a scope number, such as "#3“.

dbg5.16> u #3
dbg5.17> w
 0: expect -D 1 debug-test.exp
 1: p1
 2: p2
*3: p3
 4: set m 0

When an absolute scope is named, either u or d may be used, irrespective of which direction the new scope lies.

Moving the scope does not affect the next script command that is about to be executed. If a command such as s or n is given, the current scope is automatically reset to wherever is appropriate for execution of the new command.

Returning From A Procedure

The r command completes execution of the current procedure. In other words, it stops after the current procedure returns.

dbg5.18> r
3: set d 5
dbg4.19> w
 0: expect -D 1 debug-test.exp
 1: p1
*2: p2
 3: set d 5
dbg4.20> r
2: set a 3
dbg3.21> w
 0: expect -D 1 debug-test.exp
*1: p1
 2: set a 3
dbg3.22> r
1: set k 7
dbg2.23> w
*0: expect -D 1 debug-test.exp
 1: set k 7
dbg2.24> r
nowhere to return to

Continuing Execution

The c command lets execution continue without having to single-step. In the scenario so far, given a command anywhere, the program would continue until the script ends and the shell prompt appears.

dbg2.25> c
%

The c command is also useful in other ways. After setting breakpoints, the program can be continued until it hits a breakpoint. The program can also be continued until a signal occurs, such as by pressing ^C.

r and c are action commands just like s, n, and N. Thus, they can be executed by pressing return if they were the previous action command executed. All other commands are not action commands. For example, w is not an action command. If you enter a c command, hit a breakpoint, and then enter a w command, pressing return after that continues the script.

Defining Breakpoints

So far, I have shown how to execute a fixed number of commands or procedure calls with debugger commands such as s and n. In contrast, breakpoints provide a way to stop execution upon a condition. The conditions include:

  • line number and filename matching

  • expression testing

  • command and argument name matching

Now I will demonstrate these conditions and also show how Tcl’s trace facility can be used to cause breakpoints.

Breakpoint By Line Number And Filename

Line numbers and filenames are the most common way to specify a breakpoint.[64]This form is correspondingly the most compact. For example, the following command causes execution to break before executing line 7.

dbg2.26> b 7
0

After creation of a breakpoint, an integer identifying the breakpoint is printed. Later, I will show how this is helpful when you have to keep track of multiple breakpoints.

By default, the line number refers to the file associated with the current scope. A filename may be used to refer to a different file. A colon is used to separate the filename and line number.

dbg2.27> b foo.exp:7
1

Breakpoint By Expression

It is possible to break only when an expression is true. For example, the following command causes execution to break only when foo is greater than 3.

dbg2.28> b if {$foo > 3}
2

Expressions follow the usual Tcl syntax and may be arbitrarily complex.

No breakpointing occurs inside of the evaluation of breakpoint expressions (unless another breakpoint dictates this).

Line numbers and expressions may be combined. Here is the same command as before but augmented with a line number so that execution breaks only when foo is greater than 3 on line 7.

dbg2.28> b 7 if {$foo > 3}
2

I will show the general form for breakpoints on page 420.

Breakpoint By Pattern Match

It is also possible to define breakpoints by pattern matching on commands and arguments. Regular expressions are introduced by the flag "-re“.[65]The following command stops if the string p4 appears within the command about to be executed:

dbg2.29> b -re "p4"
3

Here are the results of this based on the sample script:

% expect -D 1 debug-test.exp
1: set b 1
dbg2.1> b -re "p4"
0
dbg2.2> c
breakpoint 0: -re "p4"
1: proc p4 {x} {
        return [
        expr 5+[expr 1+$x]]
}
dbg2.3> c
breakpoint 0: -re "p4"
4: p4 $b
dbg5.4> c
breakpoint 0: -re "p4"
3: expr 2+[p4 $b]
dbg4.5> c
breakpoint 0: -re "p4"
2: expr 1+[expr 2+[p4 $b]]

The first breakpoint occurred upon the definition of p4. The second occurred when p4 was called. Two more breakpoints occurred only because p4 was mentioned in the command.

With appropriate regular expressions, any one of these can be selected by itself. For example, to stop only on a definition of p4:

dbg2.1> b -re "proc p4 "

To stop only on a call to p4 itself:

dbg2.2> b -re "^p4 "

To stop only on commands which call p4:

dbg2.3> b -re "\[p4 "

The complexity of this last example is somewhat ameliorated by the unlikelihood of it ever being used. I have shown it simply for completeness. The point is, the ability to match on regular expressions is extremely powerful.

Multi-line patterns may be matched in the usual way—using characters such as and . Using braces instead of double quotes permits the previous pattern to be simplified to "{[p4 }“. However, the braces prevent the possibility of explicitly matching escaped characters such as .

Glob-style matching is available by using the flag -gl instead of -re. Because glob patterns match an entire string by default, the equivalents to the previous example look slightly different—anchors are not used and asterisks are required in several places.

To stop only on definitions:

dbg2.4> b -gl "proc p4 *"

On calls to p4:

dbg2.5> b -gl "p4 *"

On commands which call p4:

dbg2.6> b -gl "*\[p4 *"

Expressions can be combined with patterns just as they are with line numbers. For example, the following command defines a breakpoint which occurs on a call to p4 but only when foo is greater than 3

dbg2.7> b -gl "p4 *" if {$foo>3}

Strings which match regular expressions are saved in the array dbg. The part of the command matched by the entire pattern is saved in $dbg(0). Up to nine parenthesized subpattern matches are stored in $dbg(1) through $dbg(9).

For example, the name of a variable being set can be accessed as $dbg(1) after the following breakpoint:

dbg2.8> b -re {^set ([^ ])+ }

This can be used to construct more sophisticated breakpoints. For example, the following breakpoint occurs only when the variable being set was already set.

dbg2.9> b -re {^set ([^ ])+ } if {[info exists $dbg(1)]}

Breakpoint Actions

Breakpoints may trigger actions. The default action prints the breakpoint id and definition. It is possible to replace this action with any Tcl command. As an example, the following command defines a breakpoint which prints a descriptive message whenever the variable a is being defined:

dbg2.1> b -re "^set a " then {
+>    puts "a is being set"
+>    puts "old value of a = $a"
+> }
2

When run, it looks like this:

dbg2.2> c
a is being set
2: set a 2
dbg3.3> c
a is being set
old value of a = 2
2: set a 3
dbg3.4> c
a is being set
old value of a = 3
2: set a 5

Each time the breakpoint occurs, the old and new values of a are displayed. Notice that the first time the breakpoint occurred, a was not defined. In this case, $a was meaningless and the puts command was not executed. If there had been further commands in the breakpoint, they would also have been skipped.

Implicit error messages generated by actions are discarded. Error messages generated in breakpoint expressions are also discarded. The debugger assumes that such errors are just variables temporarily out of scope.

By default, breakpoints stop execution of the program. It is possible to tell the debugger not to stop by using the commands c, s, n, N, or r from within an action. In this way, breakpoints can be used to trace variables, although Tcl’s built-in variable tracing commands perform this particular task much more efficiently.

Here is the trace procedure for simple variables (from page 403) amended with an extra command to interrupt the script if the variable’s value ever exceeds 100.

proc traceproc_simple {v null type} {
    upvar $v var
    puts "new value of $v is $var"
    if {$var > 100} s
}

The s command here still means “step”. However, because the script is not being single-stepped to begin with, the s command forces the script back into single-step mode and returns control back to the debugger’s interactive prompt at the next command. The n, N, and r commands work similarly.

The following breakpoint prints out the name of each procedure as it is being defined.

dbg2.1> b -re "proc ([^ ]*)" then {
+>     puts "proc $dbg(1) defined"
+>     c
+> }
0

The c command in the last line allows execution to continue after each breakpoint.

dbg2.2> c
proc p4 defined
proc p3 defined
proc p2 defined
proc p1 defined

The following breakpoint causes the debugger to break after execution of any procedure which has called p4.

dbg2.1> b -gl "p4 *" then "r"

The following command prints out the string "entering p4" when p4 is invoked. Execution continues for four more steps after that.

dbg2.2> b -re "^p4 " then {
+>    puts "entering p4"
+>    s 4
+> }

Multiple breakpoints can occur on the same line. All corresponding actions are executed. At most one debugger command will be executed, however. For example, if breakpoints trigger commands containing both "s 1" and "s 2“, only the second (or last in general) will have any effect.

Limitations Of Breakpoint Actions And Interactive Commands

Debugger commands specified in a breakpoint action occur only after all the breakpoint pattern matching and other tests have completed. For example, the following breakpoint appears to print out the old and new values of every variable about to be set.

dbg2.1> b -re {^set ([^ ]+) } then {
+>     puts "old $dbg(1) = [set $dbg(1)]"
+>     n
+>     puts "new $dbg(1) = [set $dbg(1)]"
+> }

However, the debugger does not execute the next command (i.e., from the n) until the breakpoint action completes. This breakpoint therefore prints the old value twice, incorrectly claiming that the latter is the new value.

dbg4.7> c
old a = 2
new a = 2

In this case, it is possible to get the new value by just omitting the last puts. The debugger will then automatically print the new value as part of echoing the next command to be executed.

dbg4.7> c
old a = 2
2: set a 3

This example illustrates a limitation of the debugger. The debugger does not use a separate thread of control and therefore does not allow arbitrary automation of its own commands.

General Form Of Breakpoints

Expressions and actions may be combined. This follows the syntax of Tcl’s if-then (but no else). For example, the following command prints the value of foo whenever it is nonzero.

dbg2.1> b if {$foo} then {
+>    puts "foo = $foo"
+>}

The general form of the breakpoint command permits up to one location (specified by pattern, or line number and filename), one expression, and one action. They must appear in this order, but all are optional.

If a location is provided or the if-expression does not look like a line number and/or filename, the if token may be omitted. If an if-expression has already appeared, the then token is also optional. For example, the following two commands have the same effect:

dbg2.1> b if {$foo} then {
+>    puts "foo = $foo"
+>}
0
dbg2.2> b {$foo} {
+>    puts "foo = $foo"
+>}
1

When the first argument resembles both a line number and expression, it is assumed to be a line number. The following command breaks on line 17:

dbg2.3> b 17
2

Listing Breakpoints

If no arguments are supplied, the b command lists all breakpoints. The following example assumes the previous three breakpoints have been set and creates two more. Notice that breakpoints 0 and 1 are identical.

dbg2.4> b -re "^p4"
3
dbg2.5> b zz.exp:17 if {$foo}
4
dbg2.6> b
breakpoint 4: zz.exp:23 if {$foo}
breakpoint 3: -re "^p4" if {^p4}
breakpoint 2: b 17
breakpoint 1: if {$foo} then {
    puts "foo = $foo"
}
breakpoint 0: if {$foo} then {
    puts "foo = $foo"
}

Each breakpoint is identified by an integer. For example, breakpoint 4 occurs if foo is true just before line 23 is executed in file zz.exp.

When multiple breakpoints occur on the same line, the actions are executed in the order that they are listed by the b command.

Deleting Breakpoints

A breakpoint can be deleted with the command "b -#" where # is the breakpoint number. The following command deletes breakpoint 4.

dbg2.7> b -4

All of the breakpoints may be deleted by omitting the number. For example:

dbg2.8> b -

Help

The h command prints a short listing of debugger commands, arguments and other useful information.

Changing Program Behavior

When the debugger is active, the variable dbg is defined in the global scope. When the debugger is not active, dbg is not defined nor are the debugger commands such as s and n. This allows scripts to behave differently when the debugger is running.

Changing Debugger Behavior

By default, long (multi-line) commands are truncated so that the debugger can fit them on a line. This occurs when the debugger prints out a command to be executed and also in the listing from the w command.

The w command has a -width flag which can change the current printing width. It takes a new width as an argument. For example to display long commands (such as procedure definitions):

dbg2.2> w -w 300

Because of the parameter substitutions, the w command may try to display extremely lengthy output. Imagine the following script:

puts [exec cat /etc/passwd]

When the debugger is run, w command output will be truncated unless the printing width is quite large.

2: exec cat /etc/passwd
dbg3.1> s
1: puts [exec cat /etc/passwd]
dbg2.2> w
*0: expect -D 1 debug-test3.exp
 1: puts {root:Xu.VjBHD/xM7E:0:1:Operator:/:/bin/csh
nobody:*:65534:65534::/...
dbg2.3> w -w 200
dbg2.4> w
*0: expect -D 1 debug-test3.exp
 1: puts {root:Xu.VjBHD/xM7E:0:1:Operator:/:/bin/csh
nobody:*:65534:65534::/:
daemon:*:1:1::/:
sys:*:2:2::/:/bin/csh
bin:*:3:3::/bin:
uucp:*:4:8::/var/spool/uucppublic:
news:*:6:6::/var/spool/news:/bin...
dbg2.5>

When output is truncated, an ellipsis is appended to the end. The default width is 75 which allows some space at the beginning of the line for the procedure call depth information.

By default, no other output formatting is performed. But even short commands can cause lots of scrolling. The following declaration of p4 is less then 75 characters but still takes several lines.

% expect -D 1 debug-test.exp
set b 1
dbg2.1> s
1: proc p4 {} {
    return [
        expr 5+[expr 1+$x]]
}

The -compress flag with argument 1 tells the debugger to display control characters using escape sequences. For example:

dbg2.2> w -c 1
dbg2.3> w
*0: expect -D 1 debug-test.exp
 1: proc p4 {x} {
	return [
	     expr 5+[expr 1+$x]]
}

The compressed output is useful for preventing excessive scrolling and also for displaying the precise characters that should be used in order to match patterns in breakpoints.

To revert to uncompressed output, use the same flag with value 0.

dbg2.4> w -c 0

With no value specified, flags to the w command print out their current values.

dbg2.5> w -c
0
dbg2.6> w -w
75

Exercises

  1. The debugger assumes you use the commands set and parray for printing scalars and arrays. Write a command named “p” that prints out a variable no matter what kind it is. Add additional features, such as checking the next higher scope or the global scope if the named variable is not found in the current scope.

  2. The next chapter describes Tk, a Tcl-based extension for the X Window System. Try using the debugger with Tk. Does it work as you expected?



[59] Assuming you did any.

[60] The name is particularly likely to change. The s stands for “statement”. Once a synonym for “command”, “statement” has been relegated to the historical wastebasket.

[61] When using Tk, Expect’s send command can be accessed as exp_send. For more information, see Chapter 19 (p. 429).

[62] The “repeat last action” command is entered by just pressing the return key.

[63] Whether the word “stack” refers to procedure call stack or evaluation stack is either explicit or clearly implied by the context.

[64] Breakpoints by line number and filename have yet to be implemented as this book goes to press. They will likely be supported in the near future with the syntax shown here.

[65] The debugger permits all flags to be abbreviated to the smallest unique prefix. For example, "-re" can be abbreviated "-r“. The usual quoting conventions around patterns should be observed. In this example, the quotes around p4 can be omitted.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.174.239