Tcl 编程/调试

Tcl 编程/调试

Tcl 本身就是一个很好的老师。不要害怕犯错 - 它通常会给出有用的错误信息。当 tclsh 不带任何参数调用时,它会以交互模式启动并显示 "%" 提示符。用户输入内容并查看结果:结果或错误信息。

尝试交互式地进行独立的测试用例,并在满意后将命令粘贴到编辑器中,可以大大减少调试时间(无需在每次小改动后重启应用程序 - 只需确保它是正确的,然后重启)。

快速浏览[编辑 | 编辑源代码]

这里有一个带注释的会话记录

% hello

invalid command name "hello"

好的,我们应该输入一个命令。虽然看起来不像,但这里有一个

% hi

1 hello

2 hi

交互式 tclsh 试图猜测我们的意思,而 "hi" 是 "history" 命令的明确前缀,我们在这里看到了它的结果。另一个值得记住的命令是 "info"

% info

wrong # args: should be "info option ?arg arg ...?"

错误信息告诉我们应该至少有一个选项,并可选地更多参数。

% info option

bad option "option": must be args, body, cmdcount, commands, complete, default,

exists, functions, globals, hostname, level, library, loaded, locals, nameofexecutable,

patchlevel, procs, script, sharedlibextension, tclversion, or vars

另一个有用的错误:"option" 不是选项,而是列出了有效的选项。要获取有关命令的信息,最好键入以下内容

% info commands

tell socket subst lremove open eof tkcon_tcl_gets pwd glob list exec pid echo

dir auto_load_index time unknown eval lrange tcl_unknown fblocked lsearch gets

auto_import case lappend proc break dump variable llength tkcon auto_execok return

pkg_mkIndex linsert error bgerror catch clock info split thread_load loadvfs array

if idebug fconfigure concat join lreplace source fcopy global switch which auto_qualify

update tclPkgUnknown close clear cd for auto_load file append format tkcon_puts alias

what read package set unalias pkg_compareExtension binary namespace scan edit trace seek

while flush after more vwait uplevel continue foreach lset rename tkcon_gets fileevent

regexp tkcon_tcl_puts observe_var tclPkgSetup upvar unset encoding expr load regsub history

exit interp puts incr lindex lsort tclLog observe ls less string

哦,我的天,真多... 有多少个?

% llength [info commands]

115

现在来完成一个更实际的任务 - 让 Tcl 计算圆周率的值。

% expr acos(-1)

3.14159265359

嗯... 我们能以更高的精度得到它吗?

% set tcl_precision 17

17

% expr acos(-1)

3.1415926535897931

回到第一次尝试,其中 "hello" 是一个无效命令。让我们创建一个有效的命令

% proc hello {} {puts Hi!}

静默承认。现在测试

% hello

Hi!

错误是异常[编辑 | 编辑源代码]

在 Tcl 中称为 error 的东西实际上更像是其他语言中的 exception - 你可以故意引发错误,也可以 catch 错误。示例

if {$username eq ""} {error "please specify a user name"}

if [catch {open $filename w} fp] {

error "$filename is not writable"

}

错误的一个原因可能是未定义的命令名称。可以使用它来玩弄,与 catch 一起使用,如下面的多循环 break 示例,当矩阵元素为空时,它会终止两个嵌套循环

if [catch {

foreach row $matrix {

foreach col $row {

if {$col eq ""} throw

}

}

}] {puts "empty matrix element found"}

throw 命令在正常的 Tcl 中不存在,因此它会抛出一个错误,该错误被外部循环周围的 catch 捕获。

errorInfo 变量[编辑 | 编辑源代码]

Tcl 提供的这个全局变量包含最后一条错误信息和最后一次错误的回溯。一个愚蠢的例子

% proc foo {} {bar x}

% proc bar {input} {grill$input}

% foo

invalid command name "grillx"

% set errorInfo

invalid command name "grillx"

while executing

"grill$input"

(procedure "bar" line 1)

invoked from within

"bar x"

(procedure "foo" line 1)

invoked from within

"foo"

如果还没有发生错误,errorInfo 将包含空字符串。

errorCode 变量[编辑 | 编辑源代码]

此外,还有一个 errorCode 变量,它返回一个最多包含三个元素的列表

类别(POSIX、ARITH 等)

最后一次错误的缩写代码

人类可读的错误文本

例子

% open not_existing

couldn't open "not_existing": no such file or directory

% set errorCode

POSIX ENOENT {no such file or directory}

% expr 1/0

divide by zero

% set errorCode

ARITH DIVZERO {divide by zero}

% foo

invalid command name "foo"

% set errorCode

NONE

跟踪过程调用[编辑 | 编辑源代码]

要快速了解一些过程是如何调用的,以及何时调用它们,以及它们返回什么,以及何时返回,trace execution 是一个有价值的工具。让我们以以下阶乘函数为例

proc fac x {expr {$x<2? 1 : $x * [fac [incr x -1]]}}

我们需要提供一个处理程序,该处理程序将在不同的参数数量下被调用(进入时两个参数,离开时四个参数)。这里有一个非常简单的处理程序

proc tracer args {puts $args}

现在我们指示解释器跟踪 fac 的 enter 和 leave

trace add execution fac {enter leave} tracer

让我们用 7 的阶乘来测试它

fac 7

这将给出以下输出

{fac 7} enter

{fac 6} enter

{fac 5} enter

{fac 4} enter

{fac 3} enter

{fac 2} enter

{fac 1} enter

{fac 1} 0 1 leave

{fac 2} 0 2 leave

{fac 3} 0 6 leave

{fac 4} 0 24 leave

{fac 5} 0 120 leave

{fac 6} 0 720 leave

{fac 7} 0 5040 leave

因此我们可以看到递归如何下降到 1,然后以相反的顺序返回,逐步建立最终的结果。 "leave" 行中出现的第二个词 0 是返回状态,0 代表 TCL_OK。

逐步执行过程[编辑 | 编辑源代码]

要找出 proc 的确切工作方式(以及哪里出了问题),你还可以注册命令,这些命令在过程内部的命令被调用之前和之后调用(递归地传递到所有调用的 proc)。你可以为此使用以下 step 和 interact 过程

proc step {name {yesno 1}} {

set mode [expr {$yesno? "add" : "remove"}]

trace $mode execution $name {enterstep leavestep} interact

}

proc interact args {

if {[lindex $args end] eq "leavestep"} {

puts ==>[lindex $args 2]

return

}

puts -nonewline "$args --"

while 1 {

puts -nonewline "> "

flush stdout

gets stdin cmd

if {$cmd eq "c" || $cmd eq ""} break

catch {uplevel 1 $cmd} res

if {[string length $res]} {puts $res}

}

}

#----------------------------Test case, a simple string reverter:

proc sreverse str {

set res ""

for {set i [string length $str]} {$i > 0} {} {

append res [string index $str [incr i -1]]

}

set res

}

#-- Turn on stepping for sreverse:

step sreverse

sreverse hello

#-- Turn off stepping (you can also type this command from inside interact):

step sreverse 0

puts [sreverse Goodbye]

上面的代码在源代码中加载到 tclsh 时,会给出以下记录

{set res {}} enterstep -->

==>

{for {set i [string length $str]} {$i > 0} {} {

append res [string index $str [incr i -1]]

}} enterstep -->

{string length hello} enterstep -->

==>5

{set i 5} enterstep -->

==>5

{incr i -1} enterstep -->

==>4

{string index hello 4} enterstep -->

==>o

{append res o} enterstep -->

==>o

{incr i -1} enterstep -->

==>3

{string index hello 3} enterstep -->

==>l

{append res l} enterstep -->

==>ol

{incr i -1} enterstep -->

==>2

{string index hello 2} enterstep -->

==>l

{append res l} enterstep -->

==>oll

{incr i -1} enterstep -->

==>1

{string index hello 1} enterstep -->

==>e

{append res e} enterstep -->

==>olle

{incr i -1} enterstep -->

==>0

{string index hello 0} enterstep -->

==>h

{append res h} enterstep -->

==>olleh

==>

{set res} enterstep -->

==>olleh

eybdooG

调试[编辑 | 编辑源代码]

检查出错原因的最简单方法是在出错位置之前插入一个 puts 命令。假设你想查看变量 x 和 y 的值,只需插入

puts x:$x,y:$y

(如果字符串参数不包含空格,则不需要加引号)。输出将发送到 stdout - 你启动脚本的控制台。在 Windows 或 Mac 上,你可能需要添加命令

console show

以获取 Tcl 为你创建的替代控制台,当没有真正的控制台存在时。

如果你想在某些时候查看程序的详细信息,而在其他时候不想查看,你可以定义和重新定义一个 dputs 命令,它要么调用 puts,要么什么也不做

proc d+ {} {proc dputs args {puts $args}}

proc d- {} {proc dputs args {}}

d+ ;# initially, tracing on... turn off with d-

为了获得更舒适的调试体验,请将上面的 proc interact 添加到你的代码中,并在出错位置之前添加一个调用 interact 的命令。在这样的调试提示符下,一些有用的操作是

info level 0 ;# shows how the current proc was called

info level ;# shows how deep you are in the call stack

uplevel 1 ... ;# execute the ... command one level up, i.e. in the caller of the current proc

set ::errorInfo ;# display the last error message in detail

断言[编辑 | 编辑源代码]

检查数据是否满足某些条件是编码中的常见操作。绝对不能容忍的条件可以直接抛出一个错误

if {$temperature > 100} {error "ouch... too hot!"}

出错位置在 ::errorInfo 中很明显,如果你编码如下,它看起来会更清楚一些(没有提及错误命令)

if {$temperature > 100} {return -code error "ouch... too hot!"}

如果你不需要手工制作的错误信息,你可以将这些检查分解为一个 assert 命令

proc assert condition {

set s "{$condition}"

if {![uplevel 1 expr $s]} {

return -code error "assertion failed: $condition"

}

}

用例如下所示

assert {$temperature <= 100}

请注意,条件被反转了 - 因为 "assert" 大致意味着 "认为成立",所以指定了肯定情况,如果它不满足,就会引发错误。

对内部条件(不依赖于外部数据的条件)的测试可以在开发期间使用,当编码人员确信它们是防弹的,总是会成功时,他/她可以在一个地方集中关闭它们,方法是定义

proc assert args {}

这样,断言根本不会被编译成字节码,并且可以保留在源代码中作为一种文档。

如果断言被测试,它只会在代码中的断言位置发生。使用跟踪,还可以只指定一个条件,并在变量的值发生变化时测试它

proc assertt {varName condition} {

uplevel 1 [list trace var $varName w "assert $condition ;#"]

}

跟踪末尾的 ";#" 会导致在跟踪触发时附加到命令前缀的附加参数 name element op 被忽略为注释。

测试

% assertt list {[llength $list]<10}

% set list {1 2 3 4 5 6 7 8}

1 2 3 4 5 6 7 8

% lappend list 9 10

can't set "list": assertion failed: 10<10

错误信息没有那么清晰,因为 [llength $list] 已经在其中被替换了。但我在这个早餐娱乐项目中找不到一个简单的解决方案 - 在 assertt 代码中对 $condition 进行反斜杠处理当然没有帮助。欢迎提出更好的想法。

为了使断言条件更易读,我们可以再对条件加引号一次,即

% assertt list {{[llength $list]<10}}

% set list {1 2 3 4 5 6 7 8}

1 2 3 4 5 6 7 8

% lappend list 9 10

can't set "list": assertion failed: [llength $list]<10

%

在这种情况下,当跟踪触发器触发时,断言的参数为 {[llength $list]<10}。

无论如何,这五行代码给了我们一种边界检查 - 原则上,Tcl 的数据结构大小只受可用虚拟内存的限制,但与少数对可疑变量的 assertt 调用相比,失控循环可能更难调试

assertt aString {[string length $aString]<1024}

assertt anArray {[array size anArray] < 1024*1024}

Tcllib 有一个 control::assert,它具有更多功能。

一个微型测试框架[编辑 | 编辑源代码]

错误总是会发生。越早发现,对于程序员来说就越容易,因此“尽早测试,经常测试”的黄金法则应该真正得到应用。

一个简单的方法是在 Tcl 代码文件中添加自测试。当该文件作为库的一部分被加载时,只有 proc 定义会被执行。但是,如果你直接将该文件提供给 tclsh,则会检测到该事实,并且“e.g.”调用会被执行。如果结果不是预期的,则会在 stdout 上报告;最后,你还会得到一些统计信息。

以下是一个实现和演示“e.g.”的文件。

# PROLOG -- self-test: if this file is sourced at top level:

if {[info exists argv0]&&[file tail [info script]] eq [file tail $argv0]} {

set Ntest 0; set Nfail 0

proc e.g. {cmd -> expected} {

incr ::Ntest

catch {uplevel 1 $cmd} res

if {$res ne $expected} {

puts "$cmd -> $res, expected $expected"

incr ::Nfail

}

}

} else {proc e.g. args {}} ;# does nothing, compiles to nothing

##------------- Your code goes here, with e.g. tests following

proc sum {a b} {expr {$a+$b}}

e.g. {sum 3 4} -> 7

proc mul {a b} {expr {$a*$b}}

e.g. {mul 7 6} -> 42

# testing a deliberate error (this way, it passes):

e.g. {expr 1/0} -> "divide by zero"

## EPILOG -- show statistics:

e.g. {puts "[info script] : tested $::Ntest, failed $::Nfail"} -> ""

受保护的 proc[编辑 | 编辑源代码]

在更复杂的 Tcl 软件中,可能会发生一个过程被定义两次,但具有不同的主体和/或参数,从而导致难以追踪的错误。Tcl 命令 proc 本身不会在以现有名称调用时报错。以下是一种添加此功能的方法。在你的代码早期,你可以像这样重载 proc 命令

rename proc _proc

_proc proc {name args body} {

set ns [uplevel namespace current]

if {[info commands $name]!="" || [info commands ${ns}::$name]!=""} {

puts stderr "warning: [info script] redefines $name in $ns"

}

uplevel [list _proc $name $args $body]

}

从该文件被加载开始,任何尝试覆盖 proc 名称的行为都会被报告到 stderr(在 Win-wish 上,它会在控制台中以红色显示)。你可以通过在“puts stderr ...”之后添加“exit”来使其非常严格,或者抛出一个错误。

已知功能:带有通配符的 proc 名称会陷入此陷阱,例如

proc * args {expr [join $args *]*1}

将始终导致投诉,因为“*”匹配任何 proc 名称。修复(在 'name' 上进行一些 regsub 魔法)留作练习。

Windows wish 控制台[编辑 | 编辑源代码]

虽然在类 Unix 系统上,标准通道 stdin、stdout 和 stderr 与你从其启动 wish 的终端相同,但 Windows wish 通常没有这些标准通道(并且通常使用双击启动)。为了帮助解决这个问题,添加了一个控制台,它接管了标准通道(stderr 甚至以红色显示,stdin 以蓝色显示)。控制台通常是隐藏的,但可以使用以下命令显示

console show

你也可以使用部分文档化的“console”命令。“console eval