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