Unix 下的 Basic TCL & TK
 

    一提到直译语言,令人印象最深刻的,想必是 BASIC 了。没错,BASIC 陪伴很多人进入PC 的奇妙世界,甚至到今天,Visual Basic 在 Windows 下依然魅力十足,把Interpretor 的优点发挥的淋淋尽致 ─ 程序发展快、方便易学。在 UNIX 的世界里,C几乎是程序语言的代名词,BASIC 早已被人们所淡忘。但是,UNIX 下是否有像 BASIC 一般简单易学,功能强大的解译语言呢?答案是肯定的,而且功能皆有过之而无不及,举凡lisp、prolog 等人工智能语言,或者像 Perl、tcl/tk 皆是,而且都有提供方便的图型界面,使得在 X window 下设计程序方便不少。以下文章就是在下寒假研究 tcl/tk 的心得,提供大家参考;并且强力推销 tcl。



一、什么是 tcl?什么是 tk?

   tcl 是 Tool Command Language 的缩写,而 tk 是一个 X window 的 Tool Kit,是 tcl在 X Window System 的应用。tcl 是一种解译语言,也是一套 C 的函式库。为什么这样说呢?因为 tcl 的解译器被设计成一个 C 的函式库,提供基本的命令与控制结构,并且使用 tcl 的任何程序皆可以根据 tcl 的规格撰写 C 程序与之链接增加新的命令,以提
高关键程序的效率、或增加新的特色。如 tk 就是这样子的示范。废话少说,以下先来个示范:

   tk 的解译器叫 wish,是 WIndowing SHell 的简称。只要在提示号下 (xterm 下)输入 wish 就可以了。接下来你可以看到一个空白的窗口出现,xterm 下的提示号也变成了 wish 的提示号。此时,在提示号输入以下两行指令,就可以见最简单,最让人惊奇的t k程序了: 

  button .b1 -text "Hello,World!" -command exit 
  pack .b1

下完第二个指令后,原本空白的窗口就变成一个印有 Hello,World 的立体
按钮,而且鼠标移近时会变成高亮度。但是别急,且慢按键再输入两个命令看看: 

button .b2 -text "Hello,TCL/TK" -command "destroy .b2" 
pack .b2 

   此时,屏幕上会出现第二个按钮。以下两个命令可以更改颜色: 

 .b1 configure -background red 

 .b2 configure -foreground green 

 

   按下第二个按钮会使第二个按钮消失,而第一个按钮会结束程序。 


   每次这样写很麻烦,但是你也可以照 UNIX 的规矩把程序写成一个档案 hello,再│执行之。 (当然要先 chmod +x hello) hello 的内容: (第一行的内容可由 `which wish` 指令得知) 

 #!/usr/local/bin/wish -f 
 button .b -text "Hello,World!" -command exit 
 pack .b 

注1:请查询系统管理者详细的路径。
注2:tcl/tk 原始码可以在 NCTUCCCA:/X/contrib/ 下找到。
注3:cc.ntu.edu.tw 的使用者其 wish 的路径为 /remote/bin/wish
注4:交谈式的 wish 的命令行编辑非常原始,使用者可以用 fep 或 ile 两个front end 程序达到类似 tcsh/bash/ksh 的行编辑。
注5:Linux/386BSD 的使用者 (应该说 XFree86 的使用者) 应该安装时就有tcl/tk了。

二、关于 tcl

   因为 tk 是基于 tcl 语言而来的,因此我们有必要先了解 tcl。

   与 tk 相同,tcl 也附了一个交谈式解译器 tclsh,可供线上学习 tcl 之用。

   tcl 的语法非常简单,基本上就是与 Shell 的语法类似。学过一点点 shell programing的人非常容易进入状况:

A.一个 tcl 程序是由好几个 tcl 叙述组成的。

B.一个 tcl 叙述与平常在 shell 下面的命令一模一样,如前面 hello,world 的例子一样,第一个字是命令,剩下的全部是该命令的参数。

C.tcl 除了命令外就只有变量。变量与 shell 变量一样,只有一种型别:字符串。钱号可以取出变量的值。

D.tcl 对每一个叙述最多只做一次变量代换。而被大括号括住的部份不做任何处理。

E.tcl 会优先执行被方括号括住的叙述,并将其结果当成原来命令的一部份。这与shell 的重音符号相同 (mkdir `echo Hello`)

以下就是一些范例:

unix% tclsh
tcl% set x 100 输出 100
tcl% set y 200 200
tcl% expr $x + $y 300
tcl% set z [expr x+y] 300, z = 300
tcl% set a [set b 100] a = b = 100
tcl% expr (3>4)||(6<=7) 1
tcl% expr 14.1*sin($x)
tcl% set organization "Taiwan University" (两句同意,只是 )
tcl% set organization {Taiwan University} (引号会做变量代换)

[以下省略tcl的提示号]

tcl 的 procedure:

proc power {base p} {
set result 1
while {$p > 0} {
set result [expr $result*$base]
set p [expr $p-1]
}
return $result
}
power 2 6 可得 64
power 1.15 5 2.01136

   仔细观察 procedure 的 demo,其实 tcl 并没有 procedure 结构的语法。

proc 只是一个命令,接受4个自变量:

. proc
. 新命令名字
. 参数
. 一段 tcl 程序代码

其中,参数与 tcl 程序代码用大括号括起来的原因是我们不希望 tcl 现在就执行这些程序码,而是当 procedure 被呼叫时才执行。while 结构也是如此:

while 判断 程序代码

   因为我们希望每次 while 执行时 $result,$p 的值都会变。如果不用大括号括起来,则所有的值在 tcl 解译的时候就固定了,while 循环永远也不会结束。

   与大括号相反,eval 命令可以把一个字符串当成 tcl 命令执行:

eval {set x 123} 等于 set x 123
eval "set x 123" 同上

eval 可以造成 tcl 对同一叙述 parse 两次,解决一些难缠的问题:

exec rm [glob *.o]

会告诉你:

"a.o b.o c.o" not found

正确的解法是叫 tcl 再 pars e一遍命令行:

eval "exec rm [glob *.o]"

tcl 的 array:不须宣告,直接用即可,但是只有一维数组而已。

set days_of_a_month(Jan) 31
set days_of_a_month(Fab) 28

多维数组可用单维数组仿真:

set matrix(1,1) 100
set matrix(3,9) 50
set matrix($x,$y) 66
set z $matrix(6,6) 77

数组的 index 其实为 "1,1" 、 "3,9" 与 "$x,$y"

相关的命令:

set var value
append var value [value2 vaule3 ...]
incr var [increament] /* default = 1 */
unset var [var2 var3 ...]

tcl 还有一种之料结构叫 list:

set x {Sun Mon Tue Wed Thu Fri Sat}
lindex $x 1 输出 Mon
lindex {a b {c d e} f g} 2 输出 "c d e"
concat {a b} {c d} e 输出 "a b c d e"
list {a b} {c d} e "{a b} {c d} e"
llength { {a b} e f} 3
llength {} 0
llength a 1
linsert $x 2 a b c Sun Mon a b c Tue ...
linsert $x 0 a a Sun Mon ...
lreplace $x 0 a a Mon Tue ...
lrange $x 0 1 Sun Mon
lappend $x a b c
lsearch $x Sat
lsearch -glob $x S* /* Wild Cards */
lsearch -regexp /* regular expression */
lsort [-decreasing|-integer] $x

Strings & Lists

set x a/b/c
set y /usr/local/bin/wish
split $x / 输出 a b c
split y {} usr local bin wish
反函数为join

Lists & Commands:

其实 tcl 语言本身就是一个 list,瞧,最后一个是 command 或是 list:

button .b -text "Reset" -command {set x 0}

list 可以解决一些难以构成的命令:

假设有一个情况,我们写了下列命令:

button .b -text "Reset" -command "set x $InitValue"

此命令的情况是我们希望 Reset button 按下后把 x 设回 InitValue,可是天不从人愿,如果 $InitValue 是 "tcl tk",则 Command 变成 set x tcl tk,引述个数不对了。

如果改成:

button .b -text "Reset" -command {set x $InitValue}

则 x 值取决于按钮时的 InitValue,而非真正的 InitValue 所以可用下列方法解决:

button .b -text "Reset" -command [list set x $initValue]

控制结构:

if 判断 [then] 叙述 elseif 叙述 elseif 叙述 [else] 叙述then 与 else 皆可省略。

while : (make b the reverse of a)

set b ""
set i [expr [llength $a] -1]
while {$i >= 0} {
lappend b [lindex $a $i]
incr i -1
}

for:

set b ""
for {set i [expr [llength $a] -1]} {$i >=0} {incr i -1} {
lappend b [lindex $a $i]
}

foreach:

set b ""
foreach i $a {
set b [linset $b 0 $i]
}

注意,受限于 tcl 语法,大括号不能独立一行:

while {}
{
}

break 与 continue 也都有效。

switch 命令:

switch $x {
Mon {incr days(Mon)}
Tue {incr days(Tue)}
default {...}
}

亦可写成:

switch $x Mon {...} Tue {...} default {...}


switch $x \
Mon {...} \
Tue {...} \
default {...}

如果动作相同可用 - 代表。

switch $x {
1 -
3 -
5 -
7 -
9 {incr odd}
default {incr even}
}

子程序:

同 csh,tcl 也有 source 命令:

source tclInit.tcl
procedure:
proc name ArgList Body

定义一个叫做 name 的 procedure,如果 ArgList 的最后一个为args,则此 procedure 为不定自变量函数,而 args 为一 list。

global name1 name2 ...

使用 global 中的 name1 name2 变量,而非自定 local 变量

return value
uplevel [level] script1 script2...

类似 inline 函式,把 stript1 script2 ... 串起来然后在上一层中执行,而非在 procedure 自己的 stack 内执行 (可以更改上一层的变量)。

upvar [level] name localname [name1 localname1] ...

引用上一层的变量 name,但是在本 procedure 内用 localname 存取之。(call by reference)

uplevel 例:

proc do {varName first last body} {
upvar $varName v
for {set v $first} {$v <= $last} {incr v}
uplevel $body
}
}
set a {}
do i 1 5 {
lappend a [expr $i*$i]
}
set a 显示 1 4 9 16 25

如果不用 uplevel,则 $body 就不可能存取到 a 变量了。

Errors & exceptions:

catch {
tcl 程序代码
} messages

如果程序代码有错,catch return 1,否则为 0,messages 为实际的错误讯息。

以下没力气打中文,写不下去了,抄两个玩具给大家欣赏 :-b

#!/usr/local/bin/wish -f
proc power {base p} {
set result 1
while {$p > 0} {
set result [expr $result*$base]
incr p -1
}
return $result
}
entry .base -width 6 -relief suken -textvariable base
label .label1 -text "to the power"
entry .power -width 6 -relief sunken -textvariable power
label .label2 -text "is"
label .result -textvariable result
pack .base .label1 .power .label2 .result -side left -padx 1m -pady 2m
bind .base <Return> {set result [power $base $power]}
bind .power <Return> {set result [power $base $power]}
# End of File

注: -relief sunken的意思是凹陷的轮廓。

本程序产生一个窗口:
┌──────────────────────────┐
│〔 A 〕to the power 〔 B 〕 is 〔 〕              │
└──────────────────────────┘
只要输入A,B就可以得到A的B次方。

#!/usr/local/bin/wish -f
set id 0
entry .entry -width 30 -relief sunken -textvariable cmd
pack .entry -padx 1m -pady 1m <- 显示输入行
bind .entry <Return> { <- 当.entry 收到<Return>这个event时
set id [incr id]
if {$id > 5} {
destroy .b[expr $id -5] <- tcl的变量名也可以用凑的
└> 删除第5次前的命令
}
button .b$id -command "exec <@stdin >@stdout $cmd" -text $cmd
pack .b$id -fill x <- 显示按钮,且水平(x)方向填满。
.b$id invoke <- 仿真按钮被按下
.entry delete 0 end <- 清除输入行
}

#end of file

本程序产生一个输入行,可以下命令,并且把过去的5个命令记录下来,用按钮就可以执行。

看到这里,您是否同意 tcl/tk 是 UNIX 世界的 BASIC 呢?

本篇文章是我阅读一本书:tcl and tk toolkit 的部份心得。这本书已于 1994 年由 Addison-Wesley Publishing Company, Inc. 出版 (ISBN 0-201-63337-X)。此书网络上有 postscript 档案,但是我不认为大家有闲情逸致印个 500 页左右的书吧 ─ 用 10ppm 的激光打印机也要 50 几分钟、实际上用 Postscript 输出更慢,还会卡纸哩:)最好是找找看有没有进口。

tcl/tk 在 USENET 上有自己的讨论群:comp.lang.tcl 各位可以参考其 FAQ。FAQ 可在NCTUCCCA/USENET/FAQ/comp/lang/tcl 拿到。

UNIX下 的 Interpretor 种类繁多,功能复杂,但愿这篇文章能收到拋砖引玉之效使有人愿意写些中文文件来介绍与 X Window 整合的其它 Interpretor,如 tkperl、Prolog 等。甚至是一些 Windows 的程序设计工具,如 SUIT、xvwindow等 library 使得 X Window Programing 对入门者不再是梦靥。

以上所提及的软件更是网络上可以免费取得的合法软件,特别是 Linux 下全部都有。

ps:

上学期末在天龙书局看到 Larry Wall 的 Programing Perl,对 perl 有兴趣者可以去买,保证不会后悔!

pps:

tk 目前的版本无法处理 16bits 的字集,即 Botton.. 无法看到中文,可能要等到tk 4.0。






作者:萧永庆  syc@cc.ntu.edu.tw
Phone: 7358499 - 717
Department of Electrical Engineering, Taiwan University

©Tcl/Tk中文网 2003-2008
ALL RIGHTS RESERVED