[Richard Suchenwirth] 2001-01-16 -- Of course, Tcl's most minimal debugger is ''puts''. But here is a cute little piece of code that offers some more debugging functionality (if you have stdin and stdout available - so not for ''wish'' on Windows):
======
proc bp {{s {}}} {
if ![info exists ::bp_skip] {
set ::bp_skip [list]
} elseif {[lsearch -exact $::bp_skip $s]>=0} return
if [catch {info level -1} who] {set who ::}
while 1 {
puts -nonewline "$who/$s> "; flush stdout
gets stdin line
if {$line=="c"} {puts "continuing.."; break}
if {$line=="i"} {set line "info locals"}
catch {uplevel 1 $line} res
puts $res
}
}
======
The idea is that you insert breakpoints, calls to ''bp'', in critical code with an optional string argument (that may be used for distinguishing), like this:
======
proc foo {args} {
set x 1
bp 1
string toupper $args
}
foo bar and grill
======
When execution reaches ''bp'', you get a prompt on stdout, giving the calling context and the bp string, like this:
foo bar and grill/1> pwd
/home/suchenwi/src/roi/test/mr
foo bar and grill/1> i
args x
foo bar and grill/1> set args
bar and grill
foo bar and grill/1> set args lowercase
lowercase
foo bar and grill/1> c
on which you can issue any Tcl command (especially getting or setting variable values in the scope of ''foo''), shorthand commands ("i" for "info locals"; add which others you need), and exit this micro-debugger with "c"(ontinue). Because you modified a local variable, foo's result will be
LOWERCASE
To turn off all breakpoints for an application, just say (maybe from inside ''bp''):
proc bp args {}
You can disable single breakpoints labeled e.g. ''x'' with the command
lappend ::bp_skip x
Stepping is made easy with the new execution traces from 8.4 - see [Steppin' out], but in some situations a micro-debugger like this is good enough.
See also [debugging]
----
----
[ET]
Here's my take at this cool little debugger, to work with the windows console.
It replaces the read from stdin to vwaiting on the global ___zzz___
See the do proc below for the commands added. The g command is a combined continue and dump locals on next breakpoint. It takes a counter N that will continue and skip N breakpoints.
There is now code following the bp proc that is optional which lets an <enter> alone repeat the last command from the history without saving the command repeatedly in the history. It is especially useful with the g command to watch the local variables change for a proc that sets a breakpoint, or in a loop. It should be fast enough to be able to hold down the enter key and watch the changes go by rapidly.
It does create one temporary variable ___var___ inside the proc that is unset at the end. This is if you use the d command (to dump the local variables).
======
proc do {args} {
set ::___zzz___ $args
if { [llength $::___zzz___ ] == 1 } {
set ::___zzz___ [lindex $args 0 ]
}
#puts "args= |$args| [llength $::___zzz___ ]"
#
# do {command to run inside proc where the breakpoint sits} inside braces
# d dump local variables
# c continue from breakpoint
# g continue from breakpoint and at next break dump locals
# g nnn continue and go nnn times before next breakpoint, then dump locals
# s show stack
#
}
proc d {} { ;# dump locals
do d
}
proc g {args} { ;# dump locals and continue, can take a numerical skip counter e.g. g 20
do g {*}$args
}
proc c {} { ;# continue
do c
}
proc s {} { ;# stack dump
do {puts {} ; for {set _n_ 10} {$_n_ >= 0} {incr _n_ -1} {catch {puts "$_n_ ) [string range [uplevel $_n_ info level 0] 0 80]" } } ; unset _n_}
}
proc bp {{s {}}} {
console show
console eval {focus -force .console} ;# this is the consoles text widget, focus in case it got lost (c would do that)
incr ::___times___
if { ! [info exists ::bp_counter] } {
set ::bp_counter -1
}
set dowait 1
if {[incr ::bp_counter -1] > 0} {
return
} else {
if { $::bp_counter == 0 } {
set ::___zzz___ d
set dowait 0
}
set ::bp_counter -1
}
if {![info exists ::bp_skip]} {
set ::bp_skip [list]
} elseif {[lsearch -exact $::bp_skip $s]>=0} {
return
}
if [catch {info level -1} who] {
set who ::
}
set who [lindex $who 0 ]
while 1 {
if { $dowait } {
puts -nonewline " $::___times___ \[$who\]/ "
puts -nonewline stderr "$s> "; flush stdout
update idletasks
vwait ::___zzz___
} else {
set dowait 1 ;# for next time around
}
set line $::___zzz___
set ::___zzz___ {}
#puts "line= |$line| "
set linedo $line
#gets stdin line < now do this via a set ___zzz___ {command}
if {$line=="c"} { break}
if {$line=="i"} {set linedo "info locals"}
if {[lindex $line 0 ]=="d" || [lindex $line 0 ] == "g"} {
set linedo {
puts stderr " proc = \[[lindex [info level 0] 0]\] { [info args [lindex [info level 0] 0]] }"
foreach ___var___ [lsort [info locals]] {
if { [array exist $___var___ ]} {
puts "[format %10s $___var___]() = ([string range [array names $___var___] 0 80])"
} else {
puts "[format %12s $___var___] = |[string range [set $___var___] 0 80]|"
}
}
unset ___var___
}
}
if { ![info exist linedo] } {
set linedo $line
}
if {[lindex $line 0 ]=="g"} {
set cnt [lindex $line 1 ]
if { $cnt ne "" } {
set ::bp_counter [expr {( $cnt )}]
} else {
set ::bp_counter 1
}
break
}
#puts "linedo= |$linedo| "
catch {uplevel 1 $linedo} res
puts stderr $res
}
}
#
#optional, to patch the console code to allow <return> alone to repeat last command
#
console eval {
namespace eval tk { ; # replace this so we can capture a null command and repeat the last one
proc ::tk::ConsoleInvoke {args} {
set ranges [.console tag ranges input]
set cmd ""
if {[llength $ranges]} {
set pos 0
while {[lindex $ranges $pos] ne ""} {
set start [lindex $ranges $pos]
set end [lindex $ranges [incr pos]]
append cmd [.console get $start $end]
incr pos
}
}
if {$cmd eq ""} {
ConsolePrompt
} elseif {[info complete $cmd]} {
if { $cmd == "\n" } { #patch
set cmd_next [consoleinterp eval {history nextid}]
set cmd_event [consoleinterp eval "history event [expr {( $cmd_next - 1 )}]"]
if { $cmd_event != "" } {
set cmd $cmd_event
consoleinterp eval {namespace eval ::tcl {incr history(nextid) -1;incr history(oldest) -1}} ;# don't store this one again into history
}
}
#end patch
.console mark set output end
.console tag delete input
set result [consoleinterp record $cmd]
if {$result ne ""} {
puts $result
}
ConsoleHistory reset
ConsolePrompt
} else {
ConsolePrompt partial
}
.console yview -pickplace insert
}
}
}
# here's some test code with a 5 level deep set of calls to test the stack dump
button .path1 -text "label1" -command {DoThis 500}
button .path2 -text "label2" -command {DoThis 1000}
button .path3 -text "label3" -command {DoThis 1000000}
button .cons -text "console" -command {console show}
pack .path1 .path2 .path3 .cons -fill both -expand true
proc DoThis {args} {
set last $args
bp before-loop
for {set n 0} {$n < $last} {incr n} {
puts "args= |$args| n= |$n| "
#bp inloop
DoThat $n
}
bp afterloop
}
proc DoThat {{myArg 100}} {
DoAgain $myArg
#bp indothat
}
proc DoAgain {myArg} {
#bp DoAgainStart
DoAgain1 $myArg
}
proc DoAgain1 {myArg} {
#bp DoAgainStart1
DoAgain2 $myArg
}
proc DoAgain2 {myArg} {
#bp DoAgainStart2
DoAgain3 $myArg
}
proc DoAgain3 {myArg} {
bp DoAgain3
}
======
----
Bits and pieces.. Here's a minimal variable watcher, that logs every change to the specified variables to stdout:
======
proc watch {args} {
foreach arg $args { uplevel 1 "trace add variable $arg write {puts $arg:\[set $arg\] ;#}"
}
}
======
----
[[Incidental remark: [Expect] builds in lots of interesting
debugging capabilities.]]
----
[Tkcon] has a couple of debugging features like those described above. '''idebug''' is like
'''bp''' above but with a few more bells and whistles, and
'''observe''' is like
'''watch''' above. [[[kpv]]]
----
[AM] It is very easy to extend this minimal debugger to become a mini debugger. To keep the intrusion as small as possible, just handle all debugging commands that you want to implement in a switch statement (rather than the cascading "if") for readability. This way you introduce but a single new command, bp, instead of a handful (but it may be useful to define a few, like watch, as a procedure, so that you can log the variables automatically.
One problem remains (one that I would like to solve or see solved!): how to get this to work under Windows - perhaps a solution via a pipe? The reason I am concerned is that I use (want to use) Tcl in an "embedded" mode. I can not use Tk in there, so a solution via Tkcon or [console show] becomes difficult... - [RS]: What is the problem? The little one above works only with stdin and stdout, no Tk required; and I've made good use of it debugging our app where a Tcl interpreter is embedded.
----
See also [An error experiment] with an even minimaller debugger...
<<categories>> Debugging | Arts and crafts of Tcl-Tk programming