A basic debugger

Arjen Markus (23 august 2004) Execution traces are a wonderful thing if you want to interactively debug your Tcl program or create a trace of "every" command it is running.

I used the technique described in Steppin' out to create a basic debugger (it is not meant to be the ultimate in interactive debuggers for Tcl, by no means - very nice ones exist already!), but I intend it to be a basic out-of-the-box facility.

Here is the (not completely completed) script

(A trace tool can be implemented in a similar way ...)

3-14-2-14 - Bezoar - minor bug fix


# debug.tcl --
#    Script providing basic debugging facilities
#

namespace eval ::Tracedebug:: {
    variable go
    variable breakpoints {}
    variable dcmd
    variable dname
    variable silent 0
}

# dbg_enter --
#    Callback for enterstep event
#
# Arguments:
#    cmd        Expanded command line
#    op         Operation
# Result:
#    None
# Side effect:
#    Sets dname
#
#
proc ::Tracedebug::dbg_enter {cmd op} {
    variable dname
    variable silent
    variable breakpoints
    if { [lsearch $breakpoints [lindex $cmd 0]] >= 0 } {
       set silent 0
    }
    if { ! $silent } {
       puts "Next: $cmd"
       set dname [lindex $cmd 0]
       dbg
    }
}

# dbg_leave --
#    Callback for leavestep event
#
# Arguments:
#    cmd        Expanded command line
#    code       Return code
#    result     Result of the command
#    op         Operation
# Result:
#    None
#
#
proc ::Tracedebug::dbg_leave {cmd code result op} {
    variable dname
    variable silent
    if { ! $silent } {
       set dname [lindex $cmd 0]
       if { $code == 1 } {
          puts "ERROR: $result"
       } else {
          puts "Result: $result"
       }
    }
}

# printHelp --
#    Print short help information
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Prints help
#
proc ::Tracedebug::printHelp {} {
    puts "Tcl debugger:
    ?/h     - print help information
    b name  - set a breakpoint in procedure name
    c       - continue
    db ?name? - remove the (current) breakpoint
    e       - print error information
    l       - list the body of the current procedure
    lb      - list current breakpoints
    n       - next step (or return)
    p name  - print a variable
    t       - print a stack trace
    v name ?cond? - set a trace on the variable (possibly with a
              particular condition)"
}

# printStack --
#    Print the stack
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Prints stack
#
proc ::Tracedebug::printStack {} {
    set nolevels [info level]
    set level    [expr {$nolevels-3}]

    set number 1
    while { $level > 0 } {
       puts "$number: [info level $level]"
       incr number
       incr level  -1
    }
    puts "(global level)"
}

# printBody --
#    Print the body of the current routine
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Prints body
#
proc ::Tracedebug::printBody {} {
    set nolevels [info level]
    set level    [expr {$nolevels-3}]
    set procname [lindex [info level $level] 0]

    set number 1
    foreach line [split [info body $procname] "\n"] {
       puts "[format "%3d" $number]: $line"
       incr number
    }
}

# setBreak --
#    Set a breakpoint in the given routine
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Prints help
#
proc ::Tracedebug::setBreak {} {
    variable breakpoints
    variable dcmd

    if { [llength $dcmd] == 2 } {
       lappend breakpoints [lindex $dcmd 1]
    } else {
       lappend breakpoints [lindex [info level 3] 1]
    }
}

# readCmd --
#    Read the user's command
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Sets dcmd
#
proc ::Tracedebug::readCmd {} {
    variable dcmd

    puts -nonewline ">> "
    flush stdout
    set dcmd [gets stdin]
}

# guiReadCmd --
#    Read the user's command via a simple GUI
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Sets go and dcmd
#
proc ::Tracedebug::guiReadCmd {} {
    variable go
    variable dcmd

    vwait ::Tracedebug::go
    puts ">> $dcmd"
    update
}

# createCmdWindow --
#    Create a toplevel window to enter commands
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Toplevel window created
#
proc ::Tracedebug::createCmdWindow {} {
    variable go
    variable dcmd

    toplevel .debug
    wm title .debug "Debug"

    label  .debug.label -text         "Debug:"
    entry  .debug.entry -textvariable ::Tracedebug::dcmd -width 15
    button .debug.go    -text "Go" -width 10 \
       -command {set ::Tracedebug::go   1}
    button .debug.next  -text "Next" -width 10 \
       -command {set ::Tracedebug::dcmd "n"
                 set ::Tracedebug::go   1}
    button .debug.cont  -text "Continue" -width 10 \
       -command {set ::Tracedebug::dcmd "c"
                 set ::Tracedebug::go   1}
    button .debug.quit  -text "Quit" -width 10 \
       -command {set ::Tracedebug::dcmd "q"
                 set ::Tracedebug::go   1}

    grid .debug.label .debug.entry - .debug.go   -sticky news
    grid .debug.next  .debug.cont  .debug.quit
}

# dbg --
#    Handle the user input in debug mode
#
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Whatever the user does
#
proc ::Tracedebug::dbg {} {
    variable go
    variable dcmd
    variable dname
    variable silent

    while {1} {
       #
       # Wait for the user to enter a command
       #
       readCmd

       #
       # Handle the command:
       # ?/h     - print help information
       # b name  - set breakpoint
       # c       - continue (remove this breakpoint)
       # e       - print error information
       # l       - list the body of the current procedure
       # n       - next step
       # p name  - print a variable
       # t       - print a stack trace
       # v name ?cond? - set a trace on the variable (possibly with a
       #           particular condition)
       #
       switch -- [lindex $dcmd 0] {
       "?" -
       "h" {printHelp}
       "b" {setBreak }
       "c" {

           #trace remove execution $dname enterstep ::Tracedebug::dbg_enter
           #trace remove execution $dname leavestep ::Tracedebug::dbg_leave
           set silent 1 ;# Much TODO
           break
       }
       "e" {
            puts "Errorinfo: $::errorInfo"
            puts "Errorcode: $::errorCode"
       }
       "l" {printBody}
       ""  -
       "n" {
           break
       }
       "p" {
           catch {
              uplevel 2 "puts \$[lindex $dcmd 1]"
           }
       }
       "q" -
       "quit" {exit}
       "t" {printStack}
       "v" {traceVar}
       default {
           # Ignore for the moment
           puts "Unknown debug command: $dcmd"
       }
       }
    }
}

# main --
#    Get the thing going
#
trace add execution source enterstep ::Tracedebug::dbg_enter
trace add execution source leavestep ::Tracedebug::dbg_leave

#
# Create a console - if necessary
#
catch {
    console show
    rename ::Tracedebug::readCmd    {}
    rename ::Tracedebug::guiReadCmd ::Tracedebug::readCmd
    ::Tracedebug::createCmdWindow
} msg
puts $msg

puts [trace info execution source]

puts $::argv
set argv0 [lindex $::argv 0]
set argv  [lrange $::argv 1 end]
source $argv0