[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. It takes a counter N that will cause the next N breakpoints to be skipped. There is a counter that is output on the prompt line, and there is now code following the bp proc that is optional which if used will let a null command (just the enter key) to repeat the last command from the history and also not save that command in the history. It is especially useful with the g command to watch the local variables change for a proc that sets a breakpoint. 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 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 var $arg w {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... <> Debugging | Arts and crafts of Tcl-Tk programming