Here is a late night attempt at writing a small wish based debugger. It isn't elegant but it helped me out a bit. The idea of inserting breakpoints as "bp" was stolen/influenced by A minimal debugger.
At the bottom you will find some test code that exercises most of the features. Read it before you run it (for context).
# wdbg - A simple debugger for wish based applications.
#
# Author: Todd Coram
#
# Commands:
# bp id - set a break point and output 'id' to identify it. You are
# then allowed to interactively enter Tcl commands in the debugger
# window.
#
# try id block - try and execute 'block'. If it raises and error, enter
# the debugger window under the break point guise of 'id'.
#
# log_trace var ?ops? ?state? - If 'state' is "on" (default), then trace
# the variable named 'var' based on 'ops', where 'ops' can be:
# r - read, w - write or u - unset (default "w"). If 'state' is
# "off", then turn that particular trace off. Tracing output is sent
# to the debugger window.
#
# log - send arbitrary output to the debugger window.
#
# Plus, the following debugger commands (in addition to plain-old tcl):
# i - show the contents of the current local variables.
# l - show lines surrounding the breakpoint (only in procs)
# c - continue execution
namespace eval ::wdbg {
variable got_cmd 0
namespace export bp log_trace log show_locals try
}
proc ::wdbg::bp {id} {
variable got_cmd
::wdbg::_raise_window
::wdbg::log "\n*Breakpoint: $id" cmdTag
.wdbg.e configure -state normal
while {1} {
set res [catch {info level -1} level]
if {$res != 0} {
set level 0
}
::wdbg::log "$level> " cmdTag -nonewline
.wdbg.f.t yview moveto 1.0
.wdbg.e selection range 0 end
focus .wdbg.e
tkwait variable ::wdbg::got_cmd
set line [.wdbg.e get]
set cmd [lindex $line 0]
set args [lindex $line 1]
switch -- $cmd {
"c" { .wdbg.e configure -state disabled; return }
"i" { set cmd ::wdbg::show_locals}
"l" { set cmd "::wdbg::_show_lines {$id} $args"}
"help" {
::wdbg::log "Commands:"
::wdbg::log "\tc - continue"
::wdbg::log "\ti - info on local variables"
::wdbg::log "\tl ?n? - list proc source around the breakpoint"
::wdbg::log "\tuse \"set var\" to retrieve a variable's content."
::wdbg::log "\talmost any Tcl command is valid here..."
continue
}
default {
set cmd [.wdbg.e get]
}
}
::wdbg::log "$cmd" cmdTag
catch {uplevel 1 $cmd} result
::wdbg::log "$result" resultTag
}
}
proc ::wdbg::try {id block} {
set cres [catch {uplevel 1 $block} result]
if {$cres != 0} {
::wdbg::log "Failed try: $result" errorTag
uplevel 1 [list ::wdbg::bp $id]
}
}
proc ::wdbg::log_trace {var {ops w} {state on}} {
set cmd "::wdbg::_log_trace $var"
if {$state == "off"} {
uplevel 1 ::trace vdelete $var $ops [list $cmd]
return
}
uplevel 1 ::trace variable $var $ops [list $cmd]
}
proc ::wdbg::log {str {tag logTag} {opt ""}} {
::wdbg::_raise_window
.wdbg.f.t insert end "$str" $tag
if {$opt == "-nonewline"} {
} else {
.wdbg.f.t insert end "\n"
}
.wdbg.f.t yview moveto 1.0
}
proc ::wdbg::show_locals {} {
set vars [uplevel 1 {info local}]
if {[llength $vars] == 0} {
::wdbg::log "none" resultTag
return
}
::wdbg::log "proc [uplevel 1 {info level 0}]'s local variables:" resultTag
foreach var $vars {
upvar $var value
set _value {}
if {![uplevel 1 info exists $var]} {
set _value "\#unset\#"
} else {
set _value $value
}
::wdbg::log "\t$var = $_value" resultTag
}
}
proc ::wdbg::_show_lines {id {lines 6}} {
if {[catch [uplevel 1 {info body [lindex [info level 0] 0]}] body] != 0} {
::wdbg::log "Sorry, I can only show source for procs."
return
}
set blist [split $body "\n"]
set lidx [lsearch -glob $blist "*bp*$id*"]
if {$lidx == -1} {
set lidx [lsearch -glob $blist "*try*$id*"]
if {$lidx == -1} {
::wdbg::log "Can't find breakpoint in source" errorTag
return
}
}
set range [expr $lines / 2]
set blist [lreplace $blist $lidx $lidx "--> [lindex $blist $lidx]"]
set lstr [lrange $blist [expr $lidx - $range] [expr $lidx + $range]]
set str [join $lstr "\n"]
::wdbg::log "$str" \
resultTag
}
proc ::wdbg::_raise_window {} {
if {![winfo exists .wdbg]} {
# Create the breakpoint window
#
toplevel .wdbg
entry .wdbg.e -width 80
bind .wdbg.e <Key-Return> [list set ::wdbg::got_cmd 1]
.wdbg.e configure -state disabled
frame .wdbg.f
text .wdbg.f.t -width 80 -height 20 -yscrollcommand {.wdbg.f.sb set} \
-wrap word -takefocus 0
bind .wdbg.f.t <KeyPress> {break}
# Color scheme for the text window
#
.wdbg.f.t tag configure traceTag -foreground orange
.wdbg.f.t tag configure errorTag -foreground red
.wdbg.f.t tag configure logTag -foreground darkgreen
.wdbg.f.t tag configure resultTag -foreground blue
.wdbg.f.t tag configure cmdTag -foreground [.wdbg.f.t cget -foreground]
scrollbar .wdbg.f.sb -orient vertical -command {.wdbg.f.t yview}
pack .wdbg.f.t -anchor nw -expand yes -fill both -side left
pack .wdbg.f.sb -anchor e -expand no -fill y -side left
pack .wdbg.e -side top -fill x -expand no -anchor nw
pack .wdbg.f -fill both -expand yes -anchor nw
wm title .wdbg "Wdbg - A Simple Wish-based Debugger"
# "continue" if someone tries to close the debugger window...
#
wm protocol .wdbg WM_DELETE_WINDOW { .wdbg.e delete 0 end; \
.wdbg.e insert end "c"; \
set ::wdbg::got_cmd 1; \
wm withdraw .wdbg }
# first command defaults to "help"...
#
.wdbg.e insert end "help"
}
wm deiconify .wdbg
raise .wdbg
focus .wdbg.e
}
proc ::wdbg::_log_trace {vname name1 name2 op} {
upvar $vname vn
::wdbg::log "trace -> $name1 = ($vn)" traceTag
}
#############
# Test
if { [info exists argv0] && [string compare [info script] $argv0] == 0} {
namespace import ::wdbg::*
log "starting"
bp "top level"
proc foo {} {
set x what
set y huh?
set n1 22
set n2 43
try "this works" {show_locals}
try "this fails" {show_local}
log_trace n1 w
incr n1
incr n1
incr n1
incr n1
bp "try changing the local variables"
show_locals
}
proc bar {} {
bp "in bar"
foo
}
bar
}If it looks useful, have fun -- Todd Coram