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