PracTcl 编程技巧
有时,我编写的 Tcl 程序第一次运行时并不正确,因此需要“调试”。调试 Tcl 程序最简单的方法是使用 puts 命令。
puts stderr "Some useful information to print"
一些精心放置的 puts 语句可以用来找出大多数错误。不幸的是,通常似乎错误在 puts 语句删除后很快就会返回。
解决重复出现错误问题的方法是将 puts 封装在一个名为 dputs 的过程中,这样我们就可以在不更改代码的情况下打开或关闭调试打印
proc dputs {args} { global Debug if {[info exists Debug]} { puts stderr $args } }
第一个版本的 dputs 在打印传递给 dputs 的参数之前,会检查全局变量 Debug 是否已设置(设置为任何值)。作为额外的好处,dputs 允许我们将要打印的内容指定为多个参数。args 参数在 Tcl 中很特殊,它会自动将 dputs 的所有参数收集到一个字符串中。
虽然 dputs 比 puts 有所改进,但很快就会发现此版本的局限性。您可以选择输出太少或太多。我们想要做的是在程序的不同部分有选择地打开或关闭调试打印。
我们可以使用 Tcl 的内省功能来确定每个 dputs 从哪个过程调用,并为每个过程选择性地打开或关闭调试打印。我们将使用 info level 命令来查看当前的过程堆栈,并找出调用 dputs 的过程的名称。我们可以将 Debug 设置为 glob 样式模式,这将仅导致打印与该模式匹配的过程中的 dputs 语句。作为奖励,我们将打印调用过程名称作为输出的一部分,因此不必将其作为参数包含在 dputs 中。
proc dputs {args} { global Debug if {![info exists Debug]} return set current [expr [info level] - 1] set caller toplevel catch { set caller [lindex [info level $current] 0] } if {[string match $Debug $caller]} { puts stderr "$caller: $args" } }
在此版本的 dputs 中,与之前一样,如果未设置 Debug,则不会生成任何调试输出。info level 命令返回过程调用堆栈(dputs 过程)的当前嵌套级别。从 $current 中减去 1 是 dputs 调用者的堆栈级别。info level $current 命令返回有关级别 $current 处的过程堆栈的信息列表,其第一个元素是过程的名称。如果在全局范围内调用 dputs,则对 info level 的调用将失败(current 将为 -1),因此在 info level 周围使用 catch,这将使 $caller 保持预初始化的值 toplevel。
现在我们有了调用 dputs 的过程的名称,string match 可以简单地将 $caller 中的过程名称与 Debug 中的模式进行比较,并且仅为所需的过程发出调试输出。Debug 中的模式可以在命令提示符下交互式更改,也可以在程序控制下自动更改。
虽然此版本的 dputs 更好,但它要求程序员 预先 知道要将哪些信息作为参数传递给 dputs,以便调试输出有助于定位错误。通常,调试的一半战斗是确定需要打印哪些信息才能找到错误,而 dputs 打印的内容可能不正确。
我们可以很容易地克服这个限制,因为 Tcl 是一种解释型语言。与其简单地打印作为参数传递给 dputs 的预定义值,我们可以在任何 dputs 调用处停止程序,并允许程序员输入任意 Tcl 命令来获取有关程序当前执行状态的信息。
下一个过程 breakpoint 可以插入到 Tcl 程序的任何位置,使其停止并允许交互式执行命令。例如,C assert 命令的 Tcl 等价物是通过在检测到任何无效条件时调用 breakpoint 来实现的。或者,可以将 breakpoint 插入到 dputs 中,以便可以使用 Debug 变量选择性地打开或关闭断点。
breakpoint 过程实现了四个内置命令:+、-、? 和 C。+ 和 - 命令允许用户在调用堆栈中上下移动。? 命令打印有关当前堆栈帧的有用信息,C 从 breakpoint 返回,恢复程序的执行。任何其他命令都将传递给 uplevel 以在适当的堆栈级别进行评估。
proc breakpoint {} { set max [expr [info level] - 2] set current $max show $current while {1} { puts -nonewline stderr "#$current: " gets stdin line while {![info complete $line]} { puts -nonewline stderr "? " append line \n[gets stdin] } switch -- $line { + {if {$current < $max} {show [incr current]}} - {if {$current > 0} {show [incr current -1]}} C {puts stderr "Resuming execution";return} ? {show $current} default { catch { uplevel #$current $line } result puts stderr $result } } } }
breakpoint 过程演示了如何使用 Tcl 命令 info level 和 uplevel 来检查正在运行的 Tcl 程序的状态,以及如何使用 info complete 命令来读取和评估交互式输入的 Tcl 命令。
首先,info level 计算过程调用堆栈的深度(在 $max 中)。我们需要从 info level 中减去 2,一个用于 breakpoint 过程,另一个用于 dputs。然后我们循环(while {1})获取 Tcl 命令并运行它们。变量 $current 包含当前堆栈级别,我们将打印它作为给用户的提示的一部分。
从控制台获取 Tcl 命令有点棘手,因为单个命令可能跨越多行输入。我们将在内部 while 循环中使用 info complete 和 append 命令来收集足够的输入行以形成完整的 Tcl 命令。一旦我们有了完整的命令,switch 语句就会选择内置命令之一,或者调用 uplevel 在当前堆栈级别运行该命令,该级别可能先前已被 + 或 - 命令修改。uplevel 周围的 catch 确保用户键入的错误命令不会因错误而终止程序。然后我们打印命令的结果(或错误消息,如果失败),然后循环返回以从用户那里获取下一个命令。
内置命令 + 和 - 用于更改我们输入的命令将在其中评估的堆栈级别。它们只是更改 $current 的值。? 命令调用 show,C 返回,恢复程序的执行。过程 show(我们接下来编写)显示有关当前堆栈级别的有用信息。
proc show {current} { if {$current > 0} { set info [info level $current] set proc [lindex $info 0] puts stderr "$current: Procedure $proc \ {[info args $proc]}" set index 0 foreach arg [info args $proc] { puts stderr \ "\t$arg = [lindex $info [incr index]]" } } else { puts stderr "Top level" } }
过程 show 是在调试时打印特定于应用程序的信息的快捷方式,因为用户可以键入 Tcl 命令来获得相同的结果。此版本的 show 将堆栈级别 $current 作为参数传递,它打印过程名称、其参数以及调用过程时的值。在 dputs 中,我们使用 info level $current 的第一个元素作为堆栈帧 $current 中过程的名称。其余元素包含传递给过程的参数的值。对 info args 的调用返回参数的名称,我们将其与 info level $current 中的值配对,使用变量 index 逐步遍历参数值列表。以下是 show 的一些示例输出,取自 HMtag_img 的调试会话,它是 Tcl HTML 库包的一部分。
4: Procedure HMtag_img {win param text} win = .clone1.text param = src=green_ball.gif text = text #4: info vars var text param win #4: set var(font) font:courier:14:medium:r #4: - 3: Procedure HMrender {win tag not param text} win = .clone1.text tag = img not = param = src=green_ball.gif text = This is a good point #3: C Resuming Execution
总之,我们从简单的 puts 开始进行程序调试,并在不到 50 行的 Tcl 代码中,创建了一个强大的调试环境,可以轻松地进行定制以满足大多数 Tcl 应用程序的调试需求。
Stephen Uhler 是 Sun Microsystems 实验室的研究员,他在那里与 John Ousterhout 一起改进 Tcl 和 Tk。Stephen 是 MGR 窗口系统和基于 Tcl 的个人电话环境 PhoneStation 的作者。您可以通过电子邮件 Stephen.Uhler@Eng.Sun.COM 与他联系。