Version 15 of A minimal debugger

Updated 2019-08-19 21:19:51 by ET

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___ You can either set that global, or if you don't mind having a few 1 and 2 letter commands, use the do proc. So, when you get the prompt in the console you can say,

do <some command runs in context of the proc you've stopped at ...>
do parray myarray
do i ;# this is the original i command of this debugger
d ;# if you're lazy, otherwise same as do d
c

I do 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 foo {args} {
    set x 1
    set arr(foo) bar
    bp 1
    puts "in foo with [info locals]"
    set x 2
    set envs [array names ::env]
    set arr(bar) foobar
    bp 2
    string toupper $args
}
proc d {} { ;# dump locals
        do d
}
proc c {} { ;# continue
        do c
}
        
proc do {args} {
        set ::___zzz___ $args        
}



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)
        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 "\n$who/$s> "; flush stdout
                vwait ::___zzz___
                set line $::___zzz___
                #gets stdin line < now do this via a    set ___zzz___ {command}
                if {$line=="c"} {puts "continuing.."; break}
                if {$line=="i"} {set line "info locals"}
                if {$line=="d"} {set line {
                                foreach ___var___ [lsort [info locals]] {
                                        if { [array exist $___var___ ]} {
                                                puts "[format %13s $___var___]() = ([string range [array names  $___var___] 0 80])" 
                                        } else {
                                                puts "[format %15s $___var___] = |[string range [set $___var___] 0 80]|"
                                                
                                        }
                                }
                                unset  ___var___
                        }
                }
                catch {uplevel 1 $line} res
                puts stderr $res
        }
}


 foo bar AND grill

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