《TCLWISE》 官方推荐教程

2017-07-01 00:00:00 UTC

知识共享许可协议

英文原文

介绍

亲爱的读者,这本书是对 Tcl 编程语言的主要思想的介绍:如果你想学习一种简单而强大的编程语言,这本书是为你而设。 要阅读这本书,所需的唯一先前的知识是对任何语言的编程的一些基本的了解:从 C 到 Python,Perl,Lisp,如果你明白功能和变量的含义以及其他基本概念,你应该不会在接下来的阅读中遇到问题。

为什么选择 Tcl? 因为它是一种简单而通用的编程语言,可以在短时间内成功开发应用程序。 您接下来将发现 Tcl 是一种可编程的编程语言。 TCL 包含了几个可以用来组合创建程序的理念,可以扩展语言本身以便以非常直接的方式来解决编程问题。

Tcl 由 John Ousterhout 在 1988 年左右创建为可嵌入的命令语言。 今天,Ousterhout 不再积极开发语言,Tcl 的演变掌握在 Tcl 核心团队以及 Tcl 的社区手中,该团队在 2000 年夏天指导被选为 Tcl Core 的发展。 Tcl 核心团队正在致力于 Tcl 8.5 版本,该版本应该在 2005 年秋季发布。

基础

命令解剖

Tcl 程序由命令组成,因此命名为 Tcl(发音为 Tickle):Tool Command Language(工具命令语言)。 一个命令是一系列由空格分隔的单词,如下例所示:

puts Hello

第一个单词是 Tcl 过程的名称(或多或少与 C 中的函数相同),其后面的单词是参数。 在这个例子中只有“Hello”参数。

上面的命令告诉 Tcl 用参数“Hello”来调用puts过程。 由于使用puts过程将参数输出到终端,所以该命令的效果是写“Hello”。

如果你想尝试这个命令(我强烈建议你用这本书中的例子),运行tclsh ,即 Tcl shell,一个程序,你可以在其中键入 Tcl 命令,并在屏幕上交互地看到结果,然后输入:“puts Hello”,后跟键。 Tcl 将执行该命令并输出“Hello”到终端:

% puts Hello
Hello
%

Tcl 中包含一套标准的程序,如puts ,但用户可以定义自己的程序。 与许多其他语言不同,与 Tcl 默认的过程和用户定义的过程之间的程序员观点没有真正的区别,我们稍后会看到更好。 我们将参考 Tcl 中包含的默认程序作为核心程序 。 实际上,Tcl 用户经常调用过程命令 ,即使命令实际上是过程名称和参数的总和。 但是由于这是用过的术语,我们还将把核心程序称为核心命令 ,一般来说可以用可交换的方式使用命令和过程。

设置一个重要的核心命令,它有两个参数:变量的名称和一个字符串。 Set将字符串分配给变量,并返回分配的值。 每个 Tcl 命令返回一个值,但返回值根本不重要的一些命令返回一个空字符串,如puts ,因为我们对调用puts的效果感兴趣,而不是返回的值。

这是set命令的使用示例:

% set a apple
apple
%

如果我们指定一个不存在的变量,结果就是一个错误:

% set b
can't read "b": no such variable
while evaluating {set b}

在 Tcl 变量中没有默认值。 如果您尝试使用从未定义的变量,则结果是一个错误。

分组

我们可以编写一个打印“Hello World!”的程序吗? 肯定的,但还有更多的东西要学习,叫做分组。 为了使put命令工作,程序员需要传递一个字符串作为一个参数。 如果我们写:

puts Hello World!

实际上用两个参数被命令调用。 第一个是“Hello”和第二个“World!”,所以结果是一个错误。 要在单个参数中分组包含空格的字符串,请使用引号:

% puts "Hello World!"
Hello World!

这样程序的工作完美。 还有另一种引用方式,正如我们将在下一节中讨论的替换 一样。

方案结构

还有一些关于 Tcl 程序基本结构的信息。 首先,程序中的不同命令被划分为换行符,所以你可以编辑一个文本文件,并编写这个程序:

puts "Hello World"
puts "Ciao Mondo"

保存为 hello.tcl,并执行它。 如果你是一个 unix 用户,你可以从 unix shell 中调用它:

tclsh hello.tcl

并看到输出。 另外要知道的是,命令也可以用“;”分隔,以便在单行中写入更多的命令。 所以以下程序完全等同于之前的程序:

puts "Hello World"; puts "Ciao Mondo"

在接下来的章节中,我们将编写更复杂的程序,这些程序在tclsh中直接输入是不舒服的,因此您可能希望将代码键入文件,并运行将文件名作为参数传递给tclsh 的程序

命令替换

您可能需要使用puts命令才能在屏幕上打印变量的值。 为了做到这一点,我们需要一种方法来使用变量的值作为puts命令的参数。 这是怎么做到的:

% puts [set a]
apple
%

这叫做命令替代 。 简而言之,Tcl 命令可以包含嵌套在[和]括号内的其他命令:Tcl 将首先计算此命令的结果,替换此结果代替[和](括号括起来)之间的所有内容,最后调用puts命令。 这就是 Tcl 解释器将如何一步一步处理命令。

Tcl 尝试处理命令:

puts [set a]

它将用[set a]替换它返回的值来获取:

puts apple

最后会调用puts命令,就像直接用“apple”作为参数一样调用。 替代的命令可能又包含更多的命令来替代。 当然,许多参数或命令中的一部分参数可能是命令来替代。 看到这个例子,通常直接输入到tclsh中:

% set a apple
apple
% set b orange
orange
% puts "I want an [set a] and an [set b]"
I want an apple and an orange

这两个命令都是从左到右替换的。 正如您可以看到命令替换工作在引号内,实际上在示例中有一个参数中有两个命令替换,它们与其他字符串混合。 这称为插值

变量替换

您可能会认为,如果您只需要像以前的示例中那样替换一个变量,则命令替换有点冗长,很难输入。 这就是为什么 Tcl 也支持将变量替换为特殊情况。 所以为了写[set a],你可以在下面的例子中写一个$ a(假设变量ab已经被设置为一个值):

% puts "I want an $a and an $b"
I want an apple and an orange

正如你可以看到,Tcl 插补程序不限于变量,而是限于完整的命令。 这和我们稍后会探讨的其他功能使得 Tcl 在使用字符串时非常方便,而今天许多东西都是字符串:从 xml,html 到许多网络协议,配置文件等。

现在,命令和变量替代概念应该是清楚的,我们可能想知道如何能替代。 你还记得引号用于分组,但是如预期的那样,还有另一种形式的分组,使用{和}代替引号。 它工作相同,但不允许命令和变量替换:

% puts {I want $a and $b}
I want $a and $b

“$ a”和“$ b”现在逐字地打印,不用任何方式处理。

关于插值的更多内容

重要的是要意识到插补是隔离参数之后完成的,例如写:

set a "puts Hello"
$a

将不会工作,Tcl 将尝试调用一个名为“puts Hello”的命令。 相反,以下将工作:

set a "pu"
set b "ts"
$a$b Hello

上面的脚本将在屏幕上打印 Hello,因为 Tcl 将扩展$ a和$ b,在同一个参数中连接,获得“puts”,但是您可以看到$ a $ b 在插值阶段之前已经是一个唯一的参数。

注释

像许多不同的语言一样,您可以在 Tcl 代码中写出由解释器跳过的注释:它们对于人类来说很有用,以便更好地了解程序的某些部分,这些程序可能不太简单,只需要查看代码。 在 Tcl 中,注释以字符开头,并在遇到换行符时结束:

## 这是一个注释,下一行会打印你好
puts "Hello"

注释可以从 Tcl 命令的预期位置开始,因此可以在同一行中显示一行代码:

puts "Hello" ; # 这是一个注释,下一行会打印你好

要谨慎使用注释,尽量避免注释琐事,如:

set a 5; # 将5设置为a的值

从代码上看就不言而喻的含义是不需要注释的。

结语

不管你信不信,你几乎会用 Tcl 了。 当然,要成为一个有经验的 Tcl 程序员还有许多其他的东西要学,但 Tcl 的主要思想已经揭示了:命令,分组和替换命令和变量的概念。

下一步是学习一些更多的命令,以便能够编写更多有趣的程序。 命令在 Tcl 中是中心的,即使是命令也是条件,这个命令的每个参数都是字符串,这些字符串是由命令来解释的。

一切都是一个字符串

在 Tcl 中,每种数据由字符串表示:数字,列表,代码,字典。 用户不应该提供一个具有正确数据类型的命令:“2”与 2 或 2 的{2}相同,如果给定的过程以这种方式解释,则为 2,或者可能只是一个用于另一个过程的字符串,所以命令的参数总是只是字符串,命令将以某种方式解释。

例如,要计算 Tcl 中的数学表达式,有一个名为expr的命令,它将一个数学表达式作为参数,并返回该表达式的值。

% expr 20+30
50
%

但我也可以写这段代码:

% set a "2"; set b "0+30"; expr $a$b
50
%

Expr 也可以计算布尔表达式:

% set a 5; set b 2
2
% expr $a > $b
1

它返回 1,因为 5> 2.像在 C 中,expr 的逻辑表达式将返回 1 为 true,0 为 false。

你可能想知道为什么 Tcl 中的数学没有使用每个数学运算符的命令来完成,例如:+的命令,等等,等等,像这样:

% + 1 2
3

和这个:

% + 1 [* 2 40]
81

只是为了方便用户。 就个人而言,我喜欢这种方式,但 Tcl 设计师介绍了expr ,因为大多数人在学校用中缀符号写数学表达式。 实际上对于复杂的数学表达式来说,这是非常舒服的。 仍然有一个建议将此命令添加到 Tcl。 但是,我们当然可以在 Tcl 中定义自己的过程,所以我们来看看如何创建一个+命令。

用户定义的过程

要创建一个过程,使用proc命令:

% proc + {a b} {expr $a+$b}
% + 3 4
7

现在我们可以像任何其他 Tcl 过程一样使用+过程,但最好分析proc命令创建过程所需的参数。 第一个参数是我们要创建的程序的名称,“+”。 第二个是表示过程在输入中输入的参数的字符串,即 a 和 b。 这些参数作为唯一字符串传递给proc ,其中每个元素都用空格分隔。 最后一个参数是表示每次调用+过程时执行的 Tcl 代码的字符串。

所以当我调用命令+时,代码“expr $ a + $ b”在一个上下文中执行,其中 varialbes ab具有传递给过程的参数的值。 返回值是表达式的结果,因为默认情况下,Tcl 过程返回执行的最后一个命令返回的值。

有一件非常重要的事情要注意。 proc过程创建其他过程,但没有什么特别之处,它只是一个 Tcl 过程本身,它将字符串作为参数。 现在通过{和}分组的重要性应该是明确的。 如果我们写:

proc + "a b" "expr $a+$b"

我们得到一个错误:

% proc + "a b" "expr $a+$b"
can't read "a": no such variable
while evaluating {proc + "a b" "expr $a+$b"}

Tcl 解释器尝试在$ a和$ b 之前扩展以调用 proc 命令。 请注意,可以使用反斜杠来引用$字符,所以相同的代码实际上可以写成:

% proc + "a b" "expr \$a+\$b"
% + 1 1
2

正如你可以看到proc只是一个过程,并且{和}不会向在 C 中一样识别为一个代码块,而只是字符串分组。 全部是一个字符串。 你甚至可以重新定义 proc 命令,如果你愿意的话。

% proc proc {a b} {puts "$a likes $b"}
% proc Bill Windows
Bill likes Windows

现在不要这样做;)即使这样,你会发现有趣的用法。 如果您用最后一个例子重新定义了proc命令,只需重新启动tclsh ,否则将无法创建新的过程。

if 命令

让我们来看一个新的命令: if 它就像是用任何其他语言,但是又是一个需要字符串的命令。 以下是使用 if 的代码示例。 这个时候不要将代码输入到tclsh 中 ,而是在使用编辑器的文件中输入代码,并执行它将文件名传递给tclsh

proc abs x {
    if {$x > 0} {
	  return $x
    } else {
	  expr -$x
    }
}


puts [abs 10]
puts [abs -13]

上面的代码创建一个abs proc,它返回一个数字的绝对值(也就是说,x 如果 x> 0,如果 x <0,则为 0-x)。 在这个例子中, if命令需要四个参数。 第一个是要测试的表达式如果测试这个表达式在内部调用expr过程,如果结果为 true,则执行作为第二个参数传递的代码,否则执行作为最后一个参数传递的代码。 在abs程序中,我们首次使用return命令。 目的是放弃执行当前的过程,继续执行调用过程,并重试指定的值($ x在上面的return $ x命令中)。

有一个有趣的细节要注意。 您已经知道,不同的命令是用新的行分隔的。 所以有效的是写:

if {$a > 4} {
    puts Hello!
}

或者只是:

if {$a > 4} {puts Hello!}

但我不能写这个:

if {$a > 4}
{
    puts Hello!
}

因为有两个命令。 第一个是如果有一个参数,第二个是{ ... puts Hello! ... }命令! 但是,我们当然希望很好地缩进代码,为了做到这一点,只需使用以下格式即可:

if {$a > 4} {
	puts Hello!
}

因为第二个参数以{字符开头,所有其余参数将作为单个参数,直到遇到下一个参数为止。 分组不会被换行符停止。 所以实际上,使用expr和分组 Tcl 程序或多或少地出现在从语法语言角度编写的程序 C 中,而在某种程度上,它们应该与使用中缀表达式的其他语言(如 Lisp)更相似。

但是,我们需要在if命令上多说一点,例如简单的形式:if <expression> truebranch> else <falsebranch>可以省略“else”。

% if 1 {puts Hello!} {puts Ciao!}

它的工作方式与上一个参数之间有“else”一样。 要注意的第二件事是, if是一个命令,它返回一个值,准确的说它返回由最后一个命令返回的值,所以例如你可以写:

% set max [if {$a > $b} {expr $a} {expr $b}]

C 程序员可能会注意到这与三元运算符类似。 在上面的例子中,expr 仅用于确保两个分支的最后一个命令返回我们感兴趣的数值。

最后要知道(至少现在)关于 if / elseif / elseif / else 表单的语法如下:

if {$a > 0} {
  set x "It's positive"
} elseif {$a < 0} {
  set x "It's negative"
} else {
  set x "It's zero"
}

if过程不是每个 Tcl 解释器默认包含的唯一条件。 还有switch和其他流程控制命令,如forwhile 。 此外,用户可以创建新的条件或循环命令(但这是更进一步章节的参数)。

列表

Tcl 列表

在 Tcl 中,所有内容都表示为一个字符串。 列表不能逃避语言的这个基本规则,而是列表的字符串表示是非常符合直接的人类思考的。 Tcl 列表的最简单形式是具有零个或多个空格分隔字的字符串。 例如,字符串a b c是一个三元素列表,您可以使用llength命令测试它,它将作为参数作为 Tcl 列表,并返回此列表中存在的元素数量:

% llength "a b c"
3

空列表由空字符串表示,即“”或{}。

% llength {}
0

还有一个从名为lindex的列表中提取给定元素的命令:

% lindex "this is a list" 1
is

您可以看到,列表索引是基于零的 。 第一个元素是索引 0(在我们的示例中为“a”),第二个元素为索引 1,依此类推。 Lindex ,通常每个与列表索引作为参数相关的列表相关命令也接受“end”作为索引,用于指定列表的最后一个元素。 还有end- <number>形式,表示列表的最后一个<number>元素。 一些例子:

% set mylist "foo bar biz! Tcl"
foo bar biz! Tcl
% lindex $mylist end
Tcl
% lindex $mylist end-1
biz!

您可能会想知道如何创建一个元素包含空格的列表。 该解决方案已经实际显示,称为分组:

% llength "a b {c d} e"
4

但是当您需要编写复杂的列表时,要使用的命令是使用仅为此创建的命令,即list命令:

% list a b "c d" e
a b {c d} e

列表使用每个输入参数作为列表的元素创建一个列表。 我们可以用它创建各种列表,当然也可以嵌套:

% set mylist [list a b [list 1 2 3 4] c d]
a b {1 2 3 4} c d
% lindex $mylist 2 2
3

可以看到lindex命令可以直接访问嵌套列表。 “lindex $ mylist 2 2”表示访问$ mylist 列表中第二个元素的第二个元素。

foreach 命令

Foreach是一个非常强大的命令,用于为一个或多个列表的每个元素执行 Tcl 脚本。 命令签名在其最简单的用法是:

foreach varname list script

以下是打印屏幕上前五个自然数的平方的示例。

set mylist {1 2 3 4 5}
foreach e $mylist {
    puts [expr $e*$e]
}

对于列表的每个元素,foreach 将其值分配给指定的变量并执行脚本。 实际上而不是单个变量,用户可以提供变量列表,如下例所示:

set mylist {1 2 3 4 5 6}
foreach {x y} $mylist {
	puts "$x+$y = [expr $x+$y]"
}

该程序将输出:

1+2 = 3
3+4 = 7
5+6 = 11

基本上当用户通过 N 个变量的列表时,对于每次迭代,N 个新元素被消耗分配给指定的变量,脚本被执行。 Foreach可以做更多的事情,接受比以下示例中的变量/列表对更多:

set firstlist {1 2 3 4 5 6}
set secondlist {a b c d e f g}
foreach {a b} $firstlist {c d} $secondlist {
	puts "$a$c,$b$d"
}

这次输出将是:

1a,2b
3c,4d
5e,6f

这是简单的使用,但是多个列表并行走。 正如你可以看到,foreach 是一个非常灵活的命令,用于 Tcl 中心的数据结构,因此您将在几乎每个非常简单的 Tcl 程序中看到此命令。

lrange 命令

Lrange从列表中提取一个子列表。 要提取的子列表由开始和结束索引指定,索引通常为零,并且可以使用“end- ”符号。 例:

% lrange {Tcl is a programmable programming language} 2 end-1
a programmable programming
%

lappend 命令

在这一点上,我们知道一些操作在列表上的命令: lengthlindexforeachlrange 。 他们有一些共同之处,因为所有的都将 Tcl 列表作为输入。 lappend命令替代地使用包含列表的变量的名称作为其第一个参数,以及可变数目的其他参数。 Lappend获取存储在指定变量中的列表,将列表中的每个参数附加(作为新列表元素的每个参数),并将该变量设置为新获取列表的值。 所获得的列表的值也作为lappend的返回值返回:

% set a {foo bar}
foo bar
% lappend a biz buz
foo bar biz buz
%

如果指定的变量不存在,它将被创建(并且 lappend 将假定以前的内容是空列表)。

使用 lappend,我们可以创建给定输入列表的用户定义的过程,返回由相同元素组成的列表,以相反的顺序。 这个程序的一个好名字是lreverse ,写得很简单:

proc lreverse l {
    set result {}
    set i [llength $l]
    incr i -1
    while {$i >= 0} {
	  lappend result [lindex $l $i]
	  incr i -1
    }
    return $result
}

用法也很简单:

% lreverse {a b c}
c b a

lreverse实现中,有两个我们从未使用过核心命令。 第一个是自增incr,第二个是whileIncr非常简单,它需要两个参数:一个变量的名称和一个整数。 它获取必须是整数的变量的值,将第二个参数添加到其中,并将结果设置为变量的新值。

如果变量 a 设置为 6,则命令“incr a 2”将其设置为 8,而“incr a -1”将设置为 5,依此类推。

第二个参数虽然有两个参数,第一个是一个表达式,第二个是脚本。 该表达式继续执行时执行该脚本。 表达式,就像对于if命令一样,在内部使用expr命令进行评估。

lset 命令

lset命令在某种程度上是对lindex的补充。 当lindex返回指定索引的 list 元素时,lset 将使用新值替换指定位置处的 list 元素。 像lappend 一样lset将输入一个包含列表的变量名称,并直接修改变量的值,而不是返回修改的列表。 使用lset 的一个例子:

% set a {a b c d}
a b c d
% lset a 2 X
a b X d
% puts $a
a b X d

Lset可以以类似于lindex的方式直接访问嵌套列表,指定多个索引,例如命令lset foobar 2 3 $ newval$newvalue设置为列表$foobar中第二个元素(它正好也是列表)中的第三个元素。

lsort 命令

如名称所示, lsort在输入中输入一个列表,并将其作为返回值返回。 命令结构是:

lsort ?options? list

请注意,在 Tcl 中,广泛使用在两个问号包裹显示的命令结构,这些参数在实际的命令使用中可以被省略(实际上这意味着lsort命令能只使用一个参数:list )。

选项只是对 lsort 命令具有特殊意义的字符串,并且以 T 字符命令为前缀“ - ”字符(但请注意,lsort 知道除最后一个之外的所有选项都是参数)。 选项的目标是修改 lsort 排序的方式,并且可以组合以获得特定的效果。 不是所有的选项将在这里重新出现,请查看手册页,如果你想要的细节。

一些例子:


% set mylist [list this Tcl book have more than 2 chapters and 10 pages]
this Tcl book have more than 2 chapters and 10 pages
% lsort $mylist
10 2 Tcl and book chapters have more pages than this
% lsort -decreasing $mylist
this than pages more have chapters book and Tcl 2 10
% lsort -dictionary $mylist
2 10 and book chapters have more pages Tcl than this
%

第一个例子调用lsort没有选项,但只要列表排序:如果以这种方式调用lsort排序列表比较使用 ASCII 值的字母(实际上使用 unicode,但现在你可以忽略这个特别是如果你不使用 unicode 字符串)。

第二个示例添加了-decreasing选项来反转排序顺序。 第三个例子改变了指定-dictionary 选项的比较算法 :这种模式对于许多用途来说更为智能,因为它忽略了字母的情况,并且能够更好地处理数字(如果一个字符串包含一个数字,数字的值是比较价值)。 这个效果类似于字典中引语的顺序,因此是名字。

lsort 的一个有趣的lsort是由于-unique选项,只要重复一次,返回每个元素一次:

% lsort -unique {foo bar bar bar x y y y z foo}
bar foo x y z

将这个功能直接包含在一个库中的 Tcl 过程叫做“ unique就可以很舒服。 实现简单:

proc unique l {
lsort -unique $l
}

列出与变量名称相对应的值

也许现在提问有关 Tcl 的复杂问题为时尚早,但有些读者可能会想知道为什么有一个 Tcl 列表命令可以正常工作,直接在列表的值上运行,以返回另一个列表作为结果。存储列表的变量的名称。

现在知道有效率的原因是足够的,因为一种方法比另一种方法更方便。 第二个原因是,实际上对于已经存储在变量中的列表使用lsetlappend更为常见。

当然,由于它是 Tcl 的标准,如果您需要使用与值相似的程序,则可以实现它们。 实际上已经有可以用于范围的程序,即可以lreplaceconcat。 如果您有兴趣,请查看此功能的手册页。 作为示例,以下是与lset类似的函数,它将参数作为参数,列表,索引和新值,并返回一个列表,其中指定的索引将替换为新值:

proc mylset {mylist index newval} {
    lset mylist $index $newval
}

它是用 lset 编写的,因为 lset 已经返回修改的列表的值。 用法很简单:

% mylset {a b c d} 2 foo
a b foo d
%

请注意, mylset不会以列表作为参数传递的任何方式进行更改。 在 Tcl 中,procedure 的参数是局部变量,参数按值传递。 注意, mylset不会将存储的列表更改变量:

% set a {a b c d}
a b c d
% mylset $a 2 foo
a b foo d
% set a
a b c d

一个变量继续包含列表abcd ,Tcl 是一个安全的语言,不可能改变作为参数传递的变量的值, 除非过程以特殊的方式写入,但在这种情况下,用户必须通过变量的名称,而不是使用变量名之前的$字符替换值。 我们会在以后更好地看到这些事情。

字符串

本章展示了有趣的 Tcl 命令,用于对字符串进行基本字符串操作,字符串匹配,正则表达式,字符串转换为列表,反之亦然。 Tcl 上的字符串相关命令集很大,你可以猜到,这是对语言本身的语义特别重要的字符串,而不仅仅是数据类型。 幸运的是,这是更好的组织语言的一部分,所以很多命令不难记住。

append 命令

append命令非常类似于lappend ,而是将元素附加到列表中,它将字符串附加到字符串。 命令的结构是:

append varName ?value value ...?

varName之后的每个参数都追加到varName变量的当前内容中,并返回该变量的新内容。 例:

% set s "foo"
% foo
% append s bar
% foobar
% append s x y z [string length $s]
% foobarxyz6

append命令非常有效,写append a $b比写set a $a$b更快,但这两种解决方案都可以正常工作。 在使用非常高级编程语言(如 Tcl)进行编程时,考虑速度问题仍然有点习惯,因为它们不像 C 等低级语言那么快。

string 命令

而不是使用不同的命令来执行不同的字符串操作 Tcl 使用一个名为string的单个字符串操作命令,它将操作作为第一个参数。 其余参数与执行操作有不同的含义。 在 Tcl 俚语中,不同的操作称为子命令

例如要获取字符串的长度,提供给字符串命令的第一个参数是length ,即要执行的操作的名称,可以称它为子命令 。 另一个参数是字符串本身。

% string length "Tcl is a string processor"
25
%

数字 25 当然是字符串“Tcl 是字符串处理器”内的字符数。 重要的是要知道 Tcl 字符串是二进制安全的 ,所以每种字符都可以在一个字符串中,包括值为零的字节:

% string length "ab\000xy"
5

最好理解这个概念,因为在 Tcl 编程中,不只有当您需要读取文本文件时,才会使用字符串,当涉及二进制数据时也会使用字符串。

string命令有许多其他子命令,本章中我们将只显示它的有趣部分子集。

string range

range子命令用于提取字符串的部分。 它的工作方式与lrange命令非常相似。 索引也可以是end-<index>的形式。 正式的命令结构是:

string range string start-index end-index

例:

% string range "Dante Alighieri is a Tcl user" 7 end-10
Alighieri is
%

string index

index子命令只从整个字符串中提取单个字符。

string index string index

例:

% string index "foobar" 3
b
% string index "foobar" end
r

作为string index命令的更有趣的真实应用程序是以下过程,它反转字符串中字符的顺序,转换为“lcT”中的“Tcl”。 因为最后一个字符串被反转,所以这个过程被称为stringReverse

proc stringReverse s {
    set res {}
    for {set i 0} {$i < [string length $s]} {incr i} {
        append res [string index $s end-$i]
    }
    return $res
}

即使您将程序输入到文件中,例如 rev.tcl,仍然可以使用tclsh进行一些使用source命令进行交互式实验的测试。

% source stringReverse.tcl
% stringReverse "string to reverse"
esrever ot gnirts
%

source命令告诉 Tcl 执行指定文件的内容,代替它被输入。 所以在“source stringReverse.tcl”调用之后,过程 stringReverse 被定义并且可以被调用。

% source stringReverse.tcl
% stringReverse "string to reverse"
esrever ot gnirts
%

string equal

发生频繁的操作是比较两个字符串。 String equal ,它搜索完全匹配,也就是说,字符串必须匹配逐字符,以被认为是相同的命令。 如果作为值传递的两个字符串相同,则返回值为 1,否则返回 0:

% string equal foo bar
0
% string equal tcl tcl
1
% string equal tcl TCL
0

“tcl”和“TCL”对于字符串相等是不一样的。 如果要以不区分大小写的方式进行比较,则会有一个-nocase选项来更改行为并考虑不同情况下的字符相同:

% string equal -nocase tcl TCL
1

另一个有趣的选项是-length num ,它限制与第一个num字符的比较:

% string equal Petroleum Peter
0
% string equal -length 3 Petroleum Peter
1

两个选项-nocase-length可以组合。

string compare

这个子命令非常类似于equal ,但是如果字符串相同,则返回 true 或 false,则该命令将返回:

-1 if the first string is < than the second
0  if the first string is the same as the second
1  if the first string is > than the second

string equal ,这可能会对排序或其他任务有用。

string match

当需要更强大的字符串匹配能力时,可以使用字符串匹配代替字符串相等 ,因为为了比较两个字符串,该命令会将字符串与模式进行比较。

字符串匹配支持由正常字符组成的模式,以及以下特殊序列:

*匹配任何字符序列, 即使它是空字符串。 ? 匹配任何单个字符。 [chars]匹配指定的角色集合。 可以在 xy 形式中指定一个点,如[az],它将匹配从az的每个字符。 \x完全匹配x,而不用特殊的方式解释它。 这是为了匹配*?[]\这些字符本身。

这是模式的一个例子,它可以匹配什么,以便使它更容易理解它的工作原理:

模式 说明
xyz 可以匹配 xyz,fooxyz,fooxyzbar 等。
x?z 可以匹配 xaz,xxz,x8z,但是不能匹配 xz。
[ab]c 可以匹配 ac,bc。
[a-z]*[0-9] 可以匹配 alf4,biz5,但不能匹配 123,2foo1

string match的命令结构是:

string match ?-nocase? pattern string

如果string匹配patern ,返回值分别为 1 或 0。 -nocase 选项可以用于在匹配时不关心大小写的情况。 例:

% string match {[0-9]} 5
1
% string match foo* foo
1
% string match foo* foobar
1
% string match foo* barfoo
0
% string match ?*@*.* antirez@invece.org
1
% string match ?*@?*.?* antirez@invece.org
1
% string match ?*@?*.?* antirez@org
0
%

请注意,包含[xy]形式的模式必须使用大括号分组,或使用\引用,以防止 Tcl 尝试将其替换为命令。

该示例中的最后一个模式显示了如何匹配任何东西至少 N 个字符的长度使用 N 个问号后跟一个星号。 “*”将至少匹配 3 个字符,依此类推。 Tcl 支持使用正则表达式更高级的模式匹配,但string match仍然是非常有趣的,因为在大多数情况下,它足以以更简单的方式表达模式,并且比正则表达式命令工作得更快。

string map

String map是一个强大的工具,可以用其他字符串替换字符串的出现。 替换由键值对列表驱动。 例如,列表{foo bar x {} y yy}将用“bar”替换每次出现的“foo”,将删除每次出现的“x”,并将每次出现的“y”复制。 命令结构如下:

string match ?-nocase? pattern string

替换以有序的方式完成:从原始字符串的第一个字符开始,搜索键值对列表中的每个键。 如果没有匹配,则该字符将附加到将被返回的结果,并且该过程从下一个字符继续。 如果有匹配,则相对于匹配键的值将追加到结果中,并且该过程从匹配键之后的字符继续。

上述描述可能会出现迂回和复杂,实际上了解string map如何工作并不困难。 它将键值对中的每个键的发生转换为相应值的出现。 一旦程序员可以使用string map ,他可能会想知道替换过程的细节,所以上面的文本将在以后更加有用,当你将是一个更有经验的 Tcl 程序员。

例子:

% string map {x {}} exchange
echange
% string map {1 Tcl 2 great} "1 is 2"
Tcl is great

请注意, string map在原始字符串上仅迭代一次,因此模式不能作为早期替换的效果匹配:

% string map { { } xx x yyy} "Hello World"
HelloxxWorld

当键值对列表不是常数时,最好使用list命令创建它:

% set a foo
foo
% set b bar
bar
% string map [list $a $b $b $a] foobar
barfoo
%

string is

String is测试字符串是否是给定类的成员,如整数,字母数字字符,空格等。 命令的结构是:

string is class ?-strict? ?-failindex varname? string

默认情况下,该命令为空字符串返回 1,因此-strict选项用于反转行为并返回空字符串 0(即不考虑空字符串给定类的成员)。

class可以是以下之一:

class(类别) 说明
alnum 字母或数字字符
alpha 字母字符
ascii 7 位 ASCII 范围内的每个字符
boolean 任何形式允许 Tcl 布尔(0,1,是,否,…)
control 一个控制符
digit 一个数字字符
double 一个有效的 Tcl 双精度数字
false Tcl 布尔允许的任何形式假值
graph 打印字符,空格除外
integer 任何有效形式的 32 位整数
lower 一个小写字符
print 打印字符包括空格
punct 标点符号
space 任何空格字符
true Tcl 布尔允许的任何形式真值
upper 一个大写字符
wordchar 任何字词。字母数字,符号,下划线
xdigit 一个十六进制数字

正如你可以看到一些类面向单个字符(如 alnum),有些类对字符串(如整数)有用。 如果由多个单个字符组成的字符串针对面向字符的类进行测试,则字符串的每个元素都必须属于要返回的命令的类 1.一些示例:

% string is integer 33902123
1
% string is integer foobar
0
% string is upper K
1
% string is lower K
0
% string is upper "KKK"
0
% string is upper "KKz"
0

如果使用-failidnex选项后跟变量的名称,则该命令将将测试失败的第一个字符的索引存储在变量中。

更多字符串子命令

我们没有覆盖大量的string命令。 读者可能希望查看**string **手册页以查看可用内容:了解使用内置 Tcl 功能可以做些什么来避免重新实现已有的功能非常重要。

高级字符串匹配

Tcl 字符串匹配功能包括两个强大的命令[regexp]和[regsub],以利用类似于 egrep 的正则表达式功能。 本命令将在本书“FIXME”一章中进行探讨。

列表和行

因为在 Tcl 中,列表是一个这样的中央数据结构,所以在列表操作方面编写程序往往很有意思,即使目标是使用不同类型的数据。 在字符串上执行复杂操作时,这是非常真实的:而是直接处理字符串,可以将字符串转换为列表,对结果列表执行操作,然后将其转换为字符串。 将列表中的字符串转换为自然的方法是将其转换为列表,其中原始字符串的每个字符都是列表的元素。 在 Lisp 方言中常常使用相同的概念,如 Scheme 编程语言。 在继续探索这个强大的编程概念之前,您需要学习两个新的 Tcl 命令,以便能够将一个字符串转换成一个列表,反之亦然。 这两个命令是splitjoin

将字符串转换为列表

split命令将一个字符串转换为一个列表,其中列表中的每个元素都将被分割成字符串。 分割字符串的位置是根据一组字符指定的:该字符串在每个出现指定字符的位置分割。 split命令的结构是:

split string ?splitChars?

如果省略splitChars ,则它默认为单个空格字符,因此字符串将在列表中转换,其中每个元素都以原始字符串中的空格分隔。

例如,“abracadabra”可以分割成六个元素列表,使用“a”作为splitChars参数:

% split abracadabra a
{} br c d br {}

注意列表的第一个和最后一个元素是空字符串,因为我们拆分的字符串以一个开头和结尾。 拆分代替b ,结果如下:

% split abracadabra b
a racada ra

我们获得了三个元素列表。 如果我们将bc指定为分割字符呢?

% split abracadabra bc
a ra ada ra

字符串被分割在有b c字符的地方,而不是字符串“bc”出现在字符串中。 下图显示了上一个split示例中的拆分点:

abracadabra
 |  |   |
 0  1   2

splitChar在示例中为“bc”,因此字符串在有bc字符的位置被拆分。 因为字符串中有 4 个字符是bc ,所以有 4 个分割点,如图所示,所以列表的元素是“a”,“ra”,“ada”,“ra”。

作为拆分的使用示例,我们可以尝试解析 Unix 系统中存在的/ etc / passwd 文件的一行。 系统中的第一行如下所示:

root:x:0:0:root:/root:/bin/zsh

假设我们需要得到第五个字段,该怎么做呢? 最简单的解决方案之一是将字符串转换为元素列表,使用“:”作为元素分隔符,然后使用 lindex 获取第五个元素。

root:x:0:0:root:/root:/bin/zsh
% lindex [split $line :] 5
/root

split输出产生用作lindex输入的列表。 这个代码在 Tcl 中是惯用的,是解析包含由单个字符分隔的文件的许多字符串的最简单的方法。

请注意,有一类字符串,元素之间的分隔符只是一个空格,如“这是一个字符串”。 您可能会想知道为什么在这种情况下使用拆分是有用的,因为这个字符串实际上是一个有效的 Tcl 列表,因此可以直接使用lindex或其他列表操作命令:

% lindex "this is a string" 1
is

但是这是一个简单的例子,因为有一些字符串包含空格分隔的字段, 这些字段无效的 Tcl 列表:

% set mystring "this is { a string"
this is { a string
% lindex $mystring 1
unmatched open brace in list
while evaluating {lindex $mystring 1}

因为$ mystring 没有包含一个有效的 Tcl 列表(有一个开放的分支,而不是相应的关闭的),调用lindex对它的结果是一个错误。 另外请注意,即使关闭括号仍然意义可以不同:如果字符串是一个空格分隔的字段列表, {字符应该只是一个像任何其他元素。

要解决所有这些问题,只需像以前一样对/ etc / passwd 行调用与空格分隔的字符串进行split

% set mystring "this is { a string"
this is { a string
% lindex [split $mystring] 2
{

从字符串到字符列表

当使用空字符串调用split命令作为splitChars参数时,该命令的行为是转换列表中的字符串,其中原始字符串的每个字符现在都是列表的元素:

% split "foobar" {}
f o o b a r

这允许程序员操作字符串作为我们将在本章后面看到的列表,但是在介绍这个重要的参数之前,最好理解反向转换是如何完成的:如何将列表转换成字符串。

将列表转换为字符串

join命令是split的补充。 它将列表的每个元素连接到一个字符串中,使用另一个字符串作为元素之间的分隔符。 命令结构是:

join list ?joinString?

joinString参数可以省略,默认为 null。 命令使用非常简单:

% join [list 1 2 3] .
1.2.3
% join [list 1 2 3] "000"
100020003
% join [list 1 2 3] {}
123

最后一个例子很有趣,因为与使用空分隔符分割字符串相反。 如果列表的每个元素都是使用与空字符串连接的字符,则joinString参数将将列表转换为字符串。

连接的另一个有趣的用法与expr有关:通常,您可能有一个包含数字的 Tcl 列表,您可能想要计算列表中所有数字的总和。 你可能认为的第一个解决方案是这样的:

set list {1 2 3 4 5}
set sum 0
for {set i 0} {$i < [llength $list]} {incr i} {
	incr sum [lindex $list $i]
}

这或多或少的类似你应该如何在 C 中使用的方法,但在 Tcl 还有其他更舒适的解决方案。 一个涉及使用join ,使用“+”字符串作为元素分隔符:

% join {1 2 3 4 5} +
1+2+3+4+5

您可以看到,生成的字符串是一个有效的expr表达式 ,因此我们可以以更为巧妙的方式重写上述代码:

set list {1 2 3 4 5}
set sum [expr [join $list +]]

将字符串当作列表操作

现在我们有足够的知识将一个字符串转换成一个列表,然后再回到一个字符串。 这个简单的事实是非常强大的,因为 Tcl 的列表相关命令在某些方面比字符串命令要强大得多 :例如, foreachlsort命令对于字符串没有等效。

让我们从一个真实的例子开始:目标是编写一个给定一个字符串的 Tcl 脚本,返回一个由字符串生成的字符串,字符串中的每个字母只出现一次,得到拼写这个单词所需的字母。 例如,对于字符串apple,字母表是aelp,而对于字符串Tcl is cool,字母表是cilost。 这是我们需要写的简单代码来执行操作:

% join [lsort -unique [split "supercalifragilistichespiralitoso" {}]] {}
acefghiloprstu

所以单词supercalifragilistichespiralitoso(意大利语中的世界),只由 14 个不同的字符组成。脚本的效果已经清楚, 让我们来分析脚本中使用的技巧:此命令的效果是删除重复的元素并对列表进行排序, 令将字符串转换为由-lsort 使用-unique选项处理的字符列表, 然后使用join命令将此新列表转换回字符串。

我们可以使用一种简单的技术来编写一个过程来测试两个字符串是否是另一个字符串的变位词(anagram):

proc isanagram {word1 word2} {
    set sorted1 [join [lsort [split $word1 {}]] {}]
    set sorted2 [join [lsort [split $word2 {}]] {}]
    string equal $sorted1 $sorted2
}

这一次我们不使用 lsort 的-unique 选项,所以ordered1ordered2将只包含一个字符串,其中word1word2的字符被排序:如果两个字符串由两个字符串的排序版本完全相同的字母组成将是一样的

例如,more的变位词是rome,我们可以直接使用 tclsh 来检查它是否正确:

% join [lsort [split rome {}]] {}
emor
% join [lsort [split more {}]] {}
emor

两个词排序后的版本是一样的。 如果word1word2的变位词,则程序isanagram返回 1,否则返回 0,因为它的最后一行,其中string equal两个排序的字比较(记住,如果有 nnn,Tcl 过程返回执行默认的最后一个命令的返回值执行路径中的return命令)。

lsortsplit 相似 , foreach命令可以用于字符列表。 为了迭代一个字符串的每个字符,使用这种简单的例子:

% foreach x [split "mystring" {}] {puts $x}
m
y
s
t
r
i
n
g

但是,如果需要,没有理由不利用 foreach 的全部力量。 以下程序反转字符串中每两个字符的位置:

% set var {}
% foreach {a b} [split "mystring" {}] {append var $b$a}
% set var
ymtsirgn
%

Tcl 字符串是二进制安全的,所以例如你可以使用上面的代码翻译一个由 16 位数字组成的文件,这个 16 位数字存储在一个小字节,以 16 位数字存储在大字节。

另一个例子涉及使用我们之前写过的一些部分的lreverse命令(它只是返回一个输入列表的一个反转版本,其顺序为元素的顺序):

% set string "hello world"
hello world
% join [lreverse [split $string {}]] {}
dlrow olleh

这是一个舒适的方式来反转字符串。

最后一个例子有点复杂,但也可能更有趣。 假设你有两个字符串,并想知道两个字符串是否至少有一个共同的字符。 使用 Tcl 的列表命令和将字符串转换为列表的能力,可以以紧凑的形式编写这样的代码:

proc commonChars {a b} {
    set a [split $a {}]
    set b [split $b {}]
    set union [concat [lsort -unique $a] [lsort -unique $b]]
    expr {[llength $union] != [llength [lsort -unique $union]]}
}

concat命令使用空格作为分隔符将字符串连接在一起:如果字符串是有效列表,则生成的字符串将是一个有效的列表,因此可以使用concat创建一个列表,该列表是更多列表的并置。 现在你知道这个新的命令,不应该很难理解commonChars命令的工作原理:前两行将’a’和’b’转换成字符列表。 第二行将列出在变量“union”中连接“a”和“b”的列表。 这只是一个字符列表,其中包含“a”和“b”的两个字符。 如果’a’和’b’具有共同的字符,则命令[lsort-unique $ union]将使列表更短(因为有重复的元素),所以过程的最后一行将原始列表的长度“union”,删除重复元素后的列表长度。 如果两个长度相同,则两个字符串不具有共同的字符,否则它们具有。

这就是用法:

% commonChars foo bar
0
% commonChars tcl char
1

就这样。 我希望本章显示字符串和列表在 Tcl 中是非常相关的,这可能是一个好主意,转换字符列表中的字符串,以利用与 Tcl 相关的强大的列表相关的命令。

更多程序

Tcl 程序对于不是新手读者来说,我们已经有机会使用proc命令编写一些简单的过程 。 仍然有必要调查的细节,以便能够编写非常简单的程序。 本章将介绍什么是 Tcl 过程的局部变量 ,如何使用可变数量的参数编写过程,使用默认参数,如何编写递归过程,以及最后是什么以及如何从 Tcl 过程访问全局变量。

局部变量

Tcl 程序可以使用setappendlappend等命令创建新的变量。 如果在一个过程中创建一个变量,那么这个变量就被称为局部变量 ,他的可见度和生命与过程调用密切相关。

以下foobar过程创建两个名为ab 的变量,并返回一个二元素列表,其值为a作为第一个元素, b值作为第二个元素:

proc foobar {} {
    set a foo
    set b bar
    list $a $b
}

每次调用该过程(没有任何参数)时,它将返回两个元素列表“foo bar”。 为了理解一个局部变量是什么,我们将尝试每次调用foobar时执行Tcl 中发生的事情:第一个事件是执行命令set foo ,该命令创建一个局部变量字符串“foo”作为内容。 当执行foobar过程的第二行,创建局部变量b时也会发生相同的情况。 最后,两个变量的值用作list命令的参数。 因为list $ a $ bfoobar过程的最后一个命令,所以 Tcl 解释器现在可以将其值返回给调用者:该过程可以退出。 局部变量ab会发生什么? 它们只是被破坏,值被丢失,并且每次调用foobar过程时都会发生这种情况,对于每个调用,这两个变量被创建然后被销毁。

所以我们可以说:在 Tcl 中,局部变量是在一个过程中创建的变量(即一个过程运行时),一旦创建过程准备好返回到调用者,就会被破坏。

到目前为止,我们知道一个局部变量的生命周期,但是它的访问能力呢? 一个局部变量只能通过创建它的过程中的代码进行访问, 换句话说 ,局部变量的可见性仅限于创建过程(实际上这个规则有一个重要的例外,但是您需要等待下一章才能知道更多)。

顶级

前面的部分解释了在过程中创建的变量的行为,但实际上在 Tcl 中,可以在一个称为顶级的上下文中的程序外部运行代码。 例如,当您启动tclsh并写入一些 Tcl 命令时,该命令在顶层执行。 一般来说,没有出现在程序中的每个 Tcl 代码都处于顶级。 以下面的程序为例:

puts "Here the code is running at top level"
proc foo {} {
	puts "But not here!"
}
foo

程序在顶层调用puts命令,然后调用proc命令,仍然在顶层,最后调用foo命令。 在这一点上,我们在foo函数内(不再在顶层)。 因为 toplevel 是一个 Tcl 自动调用来启动程序的过程,并且在程序运行时永远不会返回,在顶层创建的变量永远不会被销毁。 这个变量称为全局变量

全局变量

实际上,全局变量有些特别,不但不会被破坏(除非程序通过unset命令显式地破坏一个全局变量),还可以从 Tcl 程序创建和访问(即可以创建或访问全局变量,不在顶级)。 为了使其成为可能,使用全局命令。 在第一个例子中,我们将使用set在顶层创建一个全局变量,然后我们将使用全局命令从一个过程中访问它:

set PI 3.1415926536

proc area radius {
    global PI
    expr $radius*$radius*$PI
}

顶级设置命令创建包含PI 的近似值的全局变量 PI。 区域过程使用全局变量来计算具有给定半径的圆的面积(作为区域过程的唯一参数传递)。 在命令全局 PI被调用后, 区域过程可以自由使用PI变量。

过程也可以以类似的方式创建一个全局变量:

proc createPI {} {
    global PI
    set PI 3.1415926536
}

createPI
puts $PI

你可以猜到,这个程序的输出是“3.1415926536”。 createPI命令创建 PI 全局变量,可以通过puts $ PI命令从顶层直接访问 PI 全局变量。

重要的是要明白,虽然全局变量是有用的,以便采取程序的一些重要的状态,明智的用法是 raccomended:使用全局变量的程序往往在其他情况下不太可重复使用,具有微妙的副作用 ,一般来说在编写干净可读的代码的过程中,不但有很大的帮助。

程序参数和按值传递

程序的参数是一种特殊的局部变量。 这个变量唯一特别的是每次调用过程时都会自动创建它们的值,并将其值设置为传递给该命令的相应参数的值。 看下面的代码:

proc myproc x {
    set x ""
}
set list "1 2 3 4 5"
myproc $list
puts $list

程序myproc采用一个参数x ,并将其设置为空字符串。 这只是一个虚拟函数,仅适用于此示例。 代码创建一个包含五个数字列表的变量列表 ,然后调用myproc $ list 。 在这一点上会发生什么? $ list被扩展为它的值,然后调用myproc过程,所以就像直接调用:

myproc "1 2 3 4 5"

这意味着我们总是传递字符串作为过程参数。 在这一点上, myproc 的执行开始:它首先做的是创建一个局部变量x ,这是函数的参数,将“1 2 3 4 5”设置为该变量的值。 然后myproc函数的第一行将空字符串设置为x变量的值。 最后, $ list命令在屏幕上打印列表变量的值,因此程序将输出“1 2 3 4 5”。

这里有什么意义 在 Tcl 中,程序的参数总是通过值传递 。 这意味着myproc不能更改列表变量的值:该值刚刚展开,传递给 myproc,然后设置为x参数。 除非一个过程需要一个变量的名称作为参数,而不是一个值,否则总是安全的,该过程不应该改变调用者变量的值。

这使得 Tcl 编程非常安全,您可能不会在内部重新计算 mystrangeprocedure,您知道以下代码片段不能更改存储在l变量中的列表的值:

set l [list foo bar]
mystrangeprocedure $l

mystrangeprocedure可以混淆其参数尽可能多的喜欢,但仍然变量l的值将继续是一个两个元素列表“foo bar”。

在这一点上,您应该想知道如何在 Tcl 中编写一个类似于incr命令的过程,能够增加一个居住在调用者程序上下文中的变量。 在下一章中,我们将看到,由于这种语言提供的极大的灵活性和内省性,Tcl 规则如何被简单粗暴。

具有可变数量参数的过程

记住我们尝试写一个+程序? 这是我们在上一章中写的代码:

% proc + {a b} {expr $a+$b}
% + 3 4
7

这个代码是可以的,但只用两个参数。 如果我要求三个数字,而不是写+ 1 2 3我必须写+ 1 [+ 2 3] 。 我相信你不喜欢这个限制,我同意。 在 Tcl 中编写具有可变数量参数的过程非常简单,所以我们可以编写一个新版本的+程序,可以从 1 到无限参数接受。 这是代码:

proc + {x args} {
    foreach e $args {
        set x [expr $x+$e]
    }
    return $x
}

我们可以通过使用tclsh进行交互式测试,以确保它按预期工作:

% + 10 20
30
% + 1 2 3 4
10
% + 50
50
%

现在看看它是如何工作的 正如你可以看到proc命令被调用的{x args}作为第二个参数(这是我们正在创建的过程的参数列表)。 第二个参数是args ,它是一个特殊的参数:如果参数列表的最后一个参数完全是字符串args ,则该函数可以接受无数个参数,这些参数是在过程中作为列表存储到args参数中叫做。

+过程的例子中,如果我们用一个参数调用它,它将被分配给x ,并且args将是一个空列表。 如果我们用两个参数来调用它,那么第一个被赋值给x ,而第二个被赋值给列表的唯一元素args ,依此类推。 以下是传递给+的不同数量参数的 xarg将包含的表:

+                  ;# error, wrong number of arguments for procedure
+ 10               ;# x <- "10", args <- ""
+ 10 11            ;# x <- "10", args <- [list 10 11]
+ 10 11 20         ;# x <- "10", args <- [list 10 11 20]

这就是为什么+过程的实现使用foreach来遍历参数列表,将所有参数添加到第一个参数(包含在x 中 )。

请注意,只要args显示为最后一个参数,可以提供任何数量的正常参数(包括零),例如{xyz args}是一个有效的参数列表,用于需要 3 到无数个参数的过程。 前三个参数将被分配给xyz ,所有剩余的参数将被放在列表中并分配给args

具有默认参数的过程

proc命令的另一个有用的工具是使用默认参数编写过程的能力。 调用该过程时可以省略默认参数,默认值在缺省值为创建过程时指定的值。 有很多情况下,这是可取的,我们已经显示了使用此功能的核心命令(例如在用于将列表的元素连接到字符串中的join命令中, joinString参数可以省略,默认为单个空间)。

作为一个例子,我们可以编写一个将列表的每个元素增加一个的过程:

proc lincr l {
	set result {}
	foreach e $l {
	    lappend result [expr $e+1]
	}
	return $result
}

这是一些输出:

% lincr {10 20 30}
11 21 31
% lincr {5 6}
6 7

请注意,我们需要将结果变量初始化为空列表,以确保函数在输入为空列表时工作良好,否则结果变量可能无法创建,因为lappend将永远不会被调用, return $ result将生成一个错误。

如果我想增加元素 10 而不只是 1,怎么办? 我们可以重写这个函数,这样它会增加一个称为 increment 的参数,并在foreach循环中使用它的值创建新的字符串。

proc lincr {l increment} {
	set result {}
	foreach e $l {
	    lappend result [expr $e+$increment]
	}
	return $result
}

再次,这样做很好,将代码剪切并粘贴到tclsh 中并尝试一下:

% lincr {10 20 30} 1
11 21 31
% lincr {10 20 30} 5
15 25 35

但是,不可思议的是,在一段时间后,您在程序中使用此过程,您发现 80%的时间需要增加一个,为什么不能只写lincr $ mylist而不是lincr $ mylist 1 ,并指定增量只有当它不同于普通的情况? 这是默认参数在场景中输入的地方:

proc lincr {l {increment 1}} {
	set result {}
	foreach e $l {
	    lappend result [expr $e+$increment]
	}
	return $result
}

新版本与前一版本完全相同,但是现在是{l {increment 1}}的过程的参数列表有所不同。 简而言之,如果参数列表中的一个参数本身是两个元素列表,则第一个参数将被解释为参数的名称,第二个作为默认值,如果未指定,则赋予该参数。 现在函数可以接受一个或两个参数,只有一个参数会将列表元素增加一个,更多的参数将使用指定的值增加:

% lincr {1 2 3}
2 3 4
% lincr {1 2 3} 10
11 12 13

一个过程可能有多个默认参数,但它们都必须位于参数列表的末尾,您不能像{a {b 10} c}在中间添加默认参数,但可以使用多个默认参数例如{a {b 10} {c 20}}:在该示例中,如果您只指定一个参数,则 b 将默认为 10,c 至 20,如果您指定一个参数将用于设置值的b ,最后如果你添加另一个它将被用于c

此规则的一个例外是,可以使用args特殊参数来编写具有可变数量的参数的过程,其中一些最后一个参数具有默认值:

% proc foo {a {b 10} {c 20} args} {puts "$a - $b - $c - $args"}
% foo 5
5 - 10 - 20 -
% foo 5 1 2
5 - 1 - 2 -
% foo 5 1 2 a b c d
5 - 1 - 2 - a b c d
%

只是最后一个注释,如果默认值在变量内,或者想要在运行时用命令替换来计算呢? 如果参数列表使用{}分组,则不会发生变量和命令替换,因此您需要使用 list 命令以不同的方式编写该过程,如下所示:

set value 100
proc myproc [list a b [list c $value]] {
	puts "$a $b $c"
}

proc的第二个参数是一个列表,因此您可以在运行时创建它。 具有可变数量参数的默认参数和过程是非常重要的,因为编写需要较少键入基本情况的过程很好,并且在有意义的情况下可以接受无数个参数(如在+程序的情况下)。

递归

Tcl 过程可以自己调用,这使得写入递归过程成为可能。 递归是如此重要,因为许多问题在一个简单的例子中表达是微不足道的。 递归过程的第一个例子用于计算整数列表的最大元素。 我们知道如何解决一个长度为 1 的列表(最大只是唯一的元素)的情况,并使用递归,我们可以解决列表中任意长度的问题:

proc lmax l {
    if {[llength $l] == 1} {
        lindex $l 0
    } else {
        if {[lindex $l 0] > [lmax [lrange $l 1 end]]} {
	      lindex $l 0
	  } else {
	      lmax [lrange $l 1 end]
	  }
    }
}

可以读取该过程:如果列表长度为 1,则 max 是唯一包含的元素,否则将列表分为两部分,第一个元素和列表的其余部分。 如果第一个元素大于其余元素的最大值,则它是列表的最大值,否则列表的最大值是列表其余部分的最大值。 该过程将适用于每个非空的整数列表:

% lmax {1 50 34 25 61 7 8 9}
61
% lmax {1 2 3}
3

递归过程的另一个例子是经典斐波纳契函数,定义如下:

FIB(1) = FIB(2) = 1
FIB(N) = FIB(N-2)+FIB(N-1) (for N > 2)

斐波纳契函数的 Tcl 实现如下:

proc fib n {
    if {$n < 3} {
        return 1
    } else {
        expr {[fib [expr {$n-2}]]+[fib [expr {$n-1}]]}
    }
}

在看程序细节之前,请注意,我们把expr的表达式放在大括号中。 你可能会想知道如果组合会阻止它,命令替换将会生效:诀窍是, expr命令执行自己的变量转换和命令替换到我们传递的参数,如果我们提供一个表达式,它会快得多与大括号分组。 所以要写expr $a+$b,你也可以编写**expr {$a+$b}** ,让编译器优化代码运行得更快。

关于fib程序,都应该是清楚的,它是斐波纳契函数的数学定义的一个简单的 Tcl 翻译。 有趣的是,这个功能将自动结束计算多次。 例如为了计算 FIB(5),FIB(2)的值被计算 3 次,这不是最优的,因为 FIB(2)将始终具有相同的值,所以有用的是计算一次。 我们将在接下来的章节中看到如何编写一个可以自动缓存递归过程的计算值的 Tcl 过程。 缓存已经计算的过程值的技术称为记忆 ,由于 Tcl 的内省功能,我们将能够编写一个记忆过程,用作过程的第一个命令将把该过程转换成一个记忆的过程

递归限制

为了在太晚之前陷入无限递归错误,如果达到给定的递归深度(嵌套调用数),Tcl 解释器将生成错误。 例如,以下过程将在调用时退出并显示错误。

proc infinite {} {
    infinite
}
infinite

infinite将永远称为infinite

这可能有时会造成问题,您可能需要编写执行递归的代码,深度不允许以默认限制。 为了更改此使用以下 Tcl 命令:

interp recursionlimit {} $newlimit

其中*$newlimit$是新递归限制的值。 如果你想检查当前的递归限制是什么,调用这个命令没有最后一个参数:

% interp recursionlimit {}
1000

请注意,放大此限制太多可能并不总是导致使用递归递归深度编写递归过程的能力:Tcl 在 C 中实现,解释器调用自身,因此 C 堆栈本身可能会溢出。 如果发生这种情况,您将看到类似于堆栈溢出分段故障的操作系统错误

有时可以在程序返回之前编写递归出现的过程,这称为尾递归 。 正如我们将在本书的高级章节中看到的,可以编写一个执行所谓的尾递归优化proc版本,这样可以编写在恒定空间中运行的递归过程。

控制结构

在最后一章中,我们将重点放在了学习 Tcl 如何工作的目标,一些重要的核心命令避免了我们的注意。 现在读者应该熟悉 Tcl 程序的结构,无需太多的努力来学习一些新的有趣的命令。 本章涵盖的所有命令都是关于控制程序的流程。 我们已经看到, ifwhileforeach ,但还有其他有趣的控制结构发现。

switch 命令

当我们要针对多个模式测试字符串的值,并根据匹配模式执行不同的代码时, switch命令非常有用。 您可以看到switch 作为一个特殊情况, if,例如代码:

proc number2word n {
    if {$n == 0} {
        return "zero"
    } elseif {$n == 1} {
        return "one"
    } elseif {$n == 2} {
        return "two"
    } else {
        return "hello!"
    }
}

可以翻译成这个使用switch

proc number2word n {
    switch $n {
        0 {return "zero"}
	  1 {return "one"}
	  2 {return "two"}
	  default {return "hello!"}
    }
}

正如你可以看到第二个版本更可读。 在简单的形式中, switch有两个参数:

switch string {pattern body pattern body ?pattern body? ...}

第一个参数是我们要针对模式进行测试的字符串,第二个参数是具有匹配对象的模式对象的列表,以及脚本的正文 ,如果该模式匹配则执行。 如果最后一个模式是字符串“default”,则如果没有一个先前的模式匹配,则相对体将被执行。

这是开关的主要思想,但实际上命令有点复杂,因为它支持另一种形式,还有一些选项可以改变字符串与模式的匹配方式。 这是命令的完整签名:

switch string {pattern body pattern body ?pattern body? ...}

第一个参数是我们要针对模式进行测试的字符串,第二个参数是具有匹配对象的模式对象的列表,以及脚本的正文 ,如果该模式匹配则执行。 如果最后一个模式是字符串“default”,则如果没有一个先前的模式匹配,则相对体将被执行。

这是开关的主要思想,但实际上命令有点复杂,因为它支持另一种形式,还有一些选项可以改变字符串与模式的匹配方式。 这是命令的完整签名:

switch ?options? string {pattern body ?pattern body ...?}

或者,替代:

switch ?options? string pattern body ?pattern body ...?

第二个形式只是意味着将图案体对传递到列表中,也可以直接作为开关参数传递。 写这个很简短的代码的时候,你可能会喜欢这个表单:

switch $a 1 {return one} 2 {return two} 3 {return three}

但一般来说,从缩进的角度来说,第一种形式更好。 另外需要注意的是, 交换机可以选择修改他的行为。 支持的选项有:

-exact        Use exact matching when comparing the string with patterns.
-glob         Use glob style matching, like the [string match] command.
-regexp       Use regular expressions for matching.
--            End of options, all the rest is interpreted as an argument
              even if it starts with a - character.

默认匹配类型是-glob ,这不是很舒服,因为大多数时候你可能想要匹配匹配的字符串(即使用-exact)。 switch选项的另一个问题是,如果字符串参数以-字符开头,则将其解释为一个选项,除非在命令名后面有一个参数。 所以,大多数时候你想使用这样的东西:

switch -exact -- $string {
    1 {return one}
    2 {return two}
    3 {return three}
    default {return Hello}
}

这是使用switch的安全方式。 当然,如果您喜欢使用 glob 风格或正则表达式匹配,您可以用-exact 替换所需的内容。

从 Tcl 8.5 开始, switch支持两个新选项,它们是-matchvar-indexvar 。 这两个选项都是关于正则表达式匹配样式( -regexp选项),并且在您的 Tcl 体验的这个阶段并不重要,但如果需要使用与switch匹配的 regexp,并且要存储在变量中,请务必检查手册页匹配模式的匹配或子匹配。

值得注意的是,像if命令一样, switch返回在匹配的分支中执行的最后一个命令的值,所以例如可以写下如下:

proc identity x {return $x} ; # The identity function, returns its argument.
set word [switch $number 1 {identity one} 2 {identity two}]

如果$x与其中一个模式匹配,则身份函数仅用于返回我们感兴趣的值以分配给单词变量。

for 命令

Tcl for命令以 C 语言为模型,就像许多其他语言一样。 for点复杂。 更常见的用法是在算术进程上运行变量。

以下是 for 循环的例子,用于从 0 到 9:

for {set i 0} {$i < 10} {incr i} {
    puts $i
}

如果您尝试执行此代码,则输出为

0
1
2
...

如果你看代码,很容易注意到,将四个 Tcl 脚本作为参数,这是for命令的签名

for start test next body

for 循环开始评估起始脚本,在我们的例子中是 Tcl 代码“set i 0”。 这将把变量i设置为零。 起始脚本只评估一次,作为for循环的第一步。 现在循环可以启动,遵循以下规则:

使用expr评估测试参数,如果结果为 true,则执行主体脚本,则执行下一个脚本。 现在完成了一个交互,如果测试是真的,则循环重启测试。 第一次测试为 false 时, for循环终止。

该命令始终返回一个空字符串。 请注意,可以将各种 Tcl 脚本放在startnextbody参数以及test参数中的各种有效的expr 表达式中。 例如,这是一个无限循环:

for {} 1 {} {
    puts "I'll print this forever"
}

类似地,可以使用多个变量:

for {set x 0; set y 0} {$x+$y != 50} {incr x 2; incr y 3} {puts .}
puts "$x+$y = 50"

上述代码将 x 增加 2 和 y 加 3,当两者的和将为 50(10 次迭代后)时,将停止。 在该示例中, body参数为空,因为继续计算所需的工作在下一个脚本中。

break 和 continue

Tcl 核心命令实现循环结构,如foreachwhilefor ,支持一种从循环过早退出的方法,或者在循环的所有命令执行之前过早重申。 为了控制这个特性,有两个叫做breakcontinue Tcl 命令。 它们在 C 编程语言断开之后建模,并继续关键字。

break命令是用于过早退出循环的命令。 它可能出现在循环的任何位置。 如果有嵌套循环并且执行了break命令,则只有最内部的循环才会终止。 在 Tcl 中断没有任何参数,没有办法指定要退出的级别。 这是使用break的代码示例:


set a 0
while 1 {
    puts $a
    incr a
    if {$a == 10} break
}

程序的输出是从 0 到 9 的数字序列。请注意,即使while循环 apperas 成为无限循环(因为条件为真,只有1 ,永远不会改变), break可以退出如果遇到给定的条件(变量a包含 10 的值),则循环。

在循环之外调用break命令会产生一个错误,因为没有循环可以转义。

您可能会想知道如果 break 命令只能终止最内层的循环,那么可以退出多个 nexring 循环。 此示例包含两个嵌套循环:

set a 0
while 1 {
    puts $a
    incr a
    if {$a == 10} break
}

程序的输出是从 0 到 9 的数字序列。请注意,即使while循环 apperas 成为无限循环(因为条件为真,只有1 ,永远不会改变), break可以退出如果遇到给定的条件(变量a包含 10 的值),则循环。

在循环之外调用break命令会产生一个错误,因为没有循环可以转义。

您可能会想知道如果 break 命令只能终止最内层的循环,那么可以退出多个 nexring 循环。 此示例包含两个嵌套循环:

set a 0
while 1 {
    puts $a
    incr a
    if {$a == 10} break
}

为了退出两个级别,我们可以使用以下技巧:

set dobreak 0
while 1 {
    while 1 {
        set dobreak 1; break
    }
    if {$dobreak} break
}

这次为了退出这两个循环,我们将一个dobreak变量初始化为零,并在每次退出内部循环时测试它,如果设置为 1,则在外部循环中再次调用break 。在内部循环中,我们可以使用只要我们想逃脱一个循环,或者set dobreak 1; break* 当我们想要逃脱的时候打破

您几乎不需要使用这种技巧,因为需要打破多个循环是不常见的,但如果真的需要,知道如何执行此操作可能是有用的。

请注意, break 功能与forforeach和其他循环命令的方式完全相同。

如果你是一个 C 程序员,你可能会记得在 C 中可能会有一段时间或者一个嵌套的内置的循环。 因为在 C 中, 交换机需要中断才能终止一个情况,你不能使用break来从交换机中逃脱循环。 相反,需要像上面的 dobreak 一样的标志变量,或者使用goto 。 在 Tcl 中,这个问题并不存在,因为 Tcl 的开关不会以任何方式使用断点 ,所以在开关内部的断开将产生终止最内圈的设计效果。

外部循环将继续运行,因为 nexted while循环的中断只会影响。

continue命令类似于break ,而是终止执行中的最内层循环,它只会重申它。 在这个上下文中指定重要的意义很重要:当遇到继续时,如果在循环中不再执行命令,会发生什么情况。 在while循环的情况下,将再次评估测试条件,如果仍然为 true,则循环体将被执行。 在foreach的情况下,列表的下一个元素将被分配给 foreach 变量,并且主体将被再次执行,依此类推。

以下是continue的示例:

foreach x {0 1 2 3 4 5 6 7 8 9} {
	if {$x < 3} continue
	puts $x
}

上述 Tcl 程序将从 3 到 9 输出数字,因为在每次迭代时,如果$ x <3为真,则循环将重复执行,而不执行puts $ x命令。

缺乏 goto

在 Tcl 中没有goto命令,有很多理由避免设计高级编程语言,实际上本书的作者在 C 中很擅长使用goto,为了处理异常或者逃避嵌套循环,但是当在 Tcl 中进行编程时,很难想念goto命令。 Tcl 处于更高级别:有异常处理,内部循环可以使用break来终止循环,一般在较高级别工作时,缺少goto命令不是问题。 大多数时候,您需要在 C 中进行goto ,您可能希望在 Tcl 中实现状态机(state machine)。

尽管如此,Tcl 的目标是灵活性,在下一章中,您将学习如何在 Tcl 本身中编写实现新的控制结构的 Tcl 命令,包括类似于goto的内容 。

你可能想知道,有可能使用foreachbreak来实现可能类似于无条件跳转的东西,如下面的代码:

foreach _ _ {
  ... some code ...
  if {$condition} break ; # Will exit this "block"
  ... some other code ...
}

foreach将在由一个元素(“_”字符串)组成的列表中使用变量__来迭代一次。 当foreach身体会break身体会终止。

扩展 TCL

Tcl 是一种可编程的编程语言。 语言的几乎每个功能都通过命令导出到用户:条件,创建过程,控制结构,数学表达式,都被实现为以字符串作为参数的命令。 因为用户可以创建新的命令,甚至可以用用户定义的命令替换核心命令,所以语言可以以一种激进的方式进行扩展和修改。 您可以编写实现自己的控制结构的命令,一种处理数学表达式的新方法,与正在编写的程序密切相关的功能或面向对象的编程扩展。 所以专家 Tcl 程序员将以一种新的方式写一个程序:在第一阶段,他将专门使 Tcl 使语言更好地完成任务,然后他将使用这种新的专门语言编写程序。

我们已经看到了如何创建 Tcl 程序,但是为了创建更强大的程序,需要更强大的命令,这个命令是evaluplevelupvar ,还有其他命令:这些命令将在本章中进行说明,关于如何在 Tcl 中编程 Tcl 的例子,以增加语言的力量。

执行程序的程序:eval 命令

在本书的第一章中,我们已经表明,Tcl 具有很强的动态性。 例如,您可以在变量中放置一个命令名称,并将其命名为:

% set a llength
llength
% $a {1 2 3}
3

您甚至可以使用插值在运行时创建命令名称(或其任何参数):

% [string range "Xllen" 1 end]gth {1 2 3}
3

仍然有一些非常可取的,我们不能使用一个名为eval的命令,即将一个字符串作为 Tcl 程序来评估。 这就是eval命令的作用:它运行您作为参数传递的 Tcl 脚本,并使用该脚本中执行的最后一个命令的返回值作为返回值。 这是一个例子,只是为了说明它的工作原理:

% eval "set foo 10"
10
% set myscript {puts $foo}
puts $foo
% eval $myscript
10

在第一行,我们使用字符串“set foo 10”作为参数调用eval 。 Eval 只会评估它,并返回脚本返回的值。 然后我们将变量myscript设置为一个表示有效 Tcl 命令的字符串,并使用eval执行它。 这是非常重要的操作,例如,您想要编写一个名为repeat的新 Tcl 控件结构。 目标是避免一个forwhile循环,当你想要重复一个给定的 Tcl 脚本多次,而是使用“repeat 10 {lappend mylist foo}”来创建一个包含 10 个值为“foo”的元素的列表“。 repeat命令的第二个参数是脚本,根本不可能在循环中执行此脚本,而不使用eval 。 这是repeat 命令的可能实现:

proc repeat {n script} {
    while {[incr n -1] >= 0} {
        eval $script
    }
}

tclsh中剪切并粘贴,以检查它是否正常工作:

% repeat 4 {puts Hello}
Hello
Hello
Hello
Hello

太好了,我们只是在 Tcl 中扩展 Tcl。 其实我们稍后会看到,这种重复的实现不是很正确,但是为了了解我们的目标是非常了解eval 的工作原理。 关于eval 的一个重要信息是它可以使用多个参数,它们将使用concat命令进行连接,并且级联的结果将被评估。 所以其实写的是:

eval $foo $bar

是一样的叫

eval [concat $foo $bar]

concat命令执行一个简单的操作:它删除传递的每个参数的右侧和左边的每个空格,然后使用单个空格连接所有生成的字符串作为每个字符串之间的分隔符,将获得的结果返回给调用者。 使用一些示例来显示行为可能更简单:

% concat {a  } b
a b
% concat a b c
a b c
% concat {1 2 3} {a b c}
1 2 3 a b c
%

虽然concat只对字符串执行一个操作,但您应该注意到,如果其所有参数都是有效列表,则结果是单个列表,即所有原始列表的并置。 所以concat可以用来连接列表。 因为eval连接了concat 这样的参数 ,所以当参数是脚本时,我们必须非常仔细地研究concat行为,以便我们可以应用我们对concat的理解,以便很好地使用eval

让我们从一个例子开始,但首先我们需要回顾一下我们写过的+程序的最后一个版本,讲的是可变数量的参数的程序。

proc + {x args} {
   foreach e $args {
       set x [expr $x+$e]
   }
   return $x
}

现在假设你有一个整数列表,并且你想使用+过程来总和所有这个整数。 +被设计为使每个整数作为一个分离的参数求和,所以我们不能写“+ $ mylist”,但是可以使用eval完成工作:

% set mylist [list 1 2 3 4 5]
1 2 3 4 5
% eval + $mylist
15

它的工作原理,并且行为被解释回忆起, eval连接它的参数,如concat 。 我们可以使用完全相同的参数来调用concat而不是eval ,以检查在连接步骤之后eval将最终评估的结果脚本:

% set mylist [list 1 2 3 4 5]
1 2 3 4 5
% concat + $mylist
+ 1 2 3 4 5

结果脚本是“+ 1 2 3 4 5”,它是完美的,因为+过程将被调用与每个列表元素作为一个不同的参数。 这个行为是有用的,但是我们也想避免这种情况,那就是如果我想使用一个参数,即使它包含空格也应该被视为一个单独的参数呢? 将参数传递给 eval 将不起作用:

% set a "puts"
puts
% set b "Hello World"
Hello World
% eval $a $b
can not find channel named "Hello"
while evaluating {eval $a $b}

返回一个错误,因为[concat $ a $ b]将产生“puts Hello World”,所以put将使用三个参数调用,而不是像“puts {Hello World}”中的两个。 为了解决问题,我们必须使用list命令引用参数。 首先我们可以直接尝试看到生成的字符串调用concat的区别:

% set a "puts"
puts
% set b "Hello World"
Hello World
% concat $a $b
puts Hello World
% concat [list $a $b]
puts {Hello World}

正如你所看到的,在最后一行,我们使用列表来引用命令:list 将关心使用大括号围绕参数,包含 Tcl 以特殊方式解释的空格或其他字符。 使用或不使用list命令来引用参数,使得我们能够选择应该如何扩展 eval 的参数,就像使用+过程来计算数字列表一样,而不应该是这样。 例如在代码中:

eval $a [list $b $c $d] $e

如果$a$e包含空格,则内容将被解释为多于一个参数,而$b $c$d将始终被解释为单个参数,因为list命令引用。

不要担心,如果所有这一切现在不是很清楚,这是一个复杂的话题,你会掌握在一些 pratice 之后。 现在我们来介绍一个新的命令,这将使我们能够编写一个新的更好的repeat

打破上级规则

在上一节中,我们写了repeat命令的这个实现。

proc repeat {n script} {
    while {[incr n -1] >= 0} {
        eval $script
    }
}

它在某些情况下起作用,就像我们使用它在屏幕上打印四次“Hello World”一样:

repeat 4 {puts "Hello World"}

但是如果你看repeat ,有一些奇怪的事情。 包含循环体的脚本参数在repeat过程的上下文中执行 ,而不是在调用者的上下文中执行。 在{puts“Hello World”}这样的脚本的情况下,没有问题,因为这个脚本可以在任何上下文中执行,没有麻烦,它不引用局部变量,它只是一个具有常量参数的命令。 相反,这将产生麻烦:

% set a 10
10
% repeat 2 {puts $a}
can't read "a": no such variable
while evaluating {repeat 2 {puts $a}}

发生的是脚本puts $ a重复过程的上下文中执行 ,其中$a变量不存在。 我们需要修复此过程,是一个非常类似于eval的命令,但能够在调用者的上下文中运行脚本。 这个命令叫做uplevel 。 uplevel 命令与eval完全相同,但它将脚本执行的级别作为第一个参数。 这是命令签名

uplevel ?level? arg ?arg ...?

例如,如果 level 为 1,则在调用者过程的上下文中,一级执行代码,如果 level 为 2,则在调用方的调用方的上下文中执行代码,依此类推。 仔细查看以下示例:

proc a {} {
    set myvar "Tcl is a programmable programming language"
    b
}


proc b {} {
    c
}


proc c {} {
   uplevel 2 {puts $myvar}
}


a

该程序的输出是“Tcl 是一种可编程的编程语言”,但重要的是要理解为什么。 程序创建三个过程abc ,并开始调用过程a一个过程将使用“Tcl is a …”字符串作为内容创建一个局部变量myvar ,并将调用b ,它将调用c ,它将在调用者的上下文中执行脚本“puts $ myvar”调用者(即一个过程)。 因为在myvar变量的上下文中存在并设置为给定值,程序将打印它并退出。

现在你需要改变repeat过程来使其工作,就是用eval来替换上层 1 ,这样就可以了。

proc repeat {n script} {
    while {[incr n -1] >= 0} {
        uplevel 1 $script
    }
}

该版本将按预期工作:您无法将其与核心命令区分开来,您只需要一个新的控制结构即可在需要时使用。 当最后一次执行失败时,这个新程序当然会起作用:

% set a 10
10
% repeat 2 {puts $a}
10
10

repeat **和其他 Tcl 命令执行循环的唯一区别在于它不适用于breakcontinue** 。 当然也可以解决这个问题,我们将在关于 Tcl 的错误处理功能的一章中看到如何做到这一点。

将变量名传递给程序

现在我们有一个像上级的工具,我们可以看出如何实现可以将变量名称作为参数的过程,以及修改或使用调用者上下文中的这个变量的内容。 这是像incrappendlappendforeach这样的命令的情况。 在某些方面,这是 Tcl 通过引用传递的方法 ,但是不是引用传递的是调用者变量的名称。

我们尝试实现一个给定变量名的过程,将存储在该变量中的字符串转换为大写,将该字符串的大写版本设置为变量的新内容。

proc toupper varname {
    set oldval [uplevel 1 [list set $varname]]
    set newval [string toupper $oldval]
    uplevel 1 [list set $varname $newval]
    return {}
}

用法很简单:

% set myvar "tcl"
tcl
% toupper myvar
% puts $myvar
TCL

请注意, toupper 的实现返回一个空字符串,以便强调函数作为副作用的工作,而不是返回值。 你应该能够理解如何工作,但我们将一步一步地分析它。

从第一行程序开始:

set oldval [uplevel 1 [list set $varname]]

该命令将oldval变量设置为在调用者的上下文中执行的脚本set $ varname的值。 你还记得只有一个参数的set命令返回指定变量的值吗? 但是这个变量存在于调用者的上下文中,所以我们需要使用uplevel ,并且使用列表引用脚本,因为$varname的值可能包含空格。

程序的下一行只是基本的 Tcl:

set newval [string toupper $oldval]

我们使用string命令将$ oldval的大写版本设置为newval变量的值。 在这一点上,我们准备在调用者的上下文中修改名为$ varname的变量来存储字符串的新的upperCased值:

uplevel 1 [list set $varname $newval]

这与程序的第一行非常相似,但是这里set是通过三个参数调用(再次,在调用者的上下文中,由于uplevel ),最后一个是要分配的新值。 为了确保即使$ varname$ newval包含空格也是必须的,它们将被作为一个参数处理。

最后,该过程的最后一行返回空字符串。

这一切都很好…但是有一个问题:它有点太复杂了要舒适,所以 Tcl 有一个名为upvar的命令,使这更简单。 以下是使用upvar而不是uplevel的等效程序:

proc toupper varname {
    upvar 1 $varname var
    set var [string toupper $var]
    return {}
}

这看起来更简单,但它是如何工作的? upvar能够绑定生活在当前过程中的变量名,一个生活在不同的级别。 这是程序签名

upvar ?level? otherVar myVar ?otherVar myVar ...?

level参数的作用与上级的 level参数完全一样,所以 level 值为 1 表示绑定在调用者的上下文中的一个变量。 在上面的例子中,代码:

upvar 1 $varname var

意思是:绑定局部变量名var ,调用者的变量名为$ varname 。 每次toupper过程将使用变量var ,效果将访问调用者的变量名为$ varname (当然,varname 的实际值取决于传递给toupper过程的参数的值)。

可以为upvar指定多个变量名称,如:

upvar 1 $listname list $somename foo

以便使用单个upvar命令绑定更多变量。

将脚本映射到列表

现在我们可以扩展语言,我们可以编写一个程序,在许多情况下证明是非常有用的。 这个称为map 的过程用于将脚本映射到 Tcl 列表。 列表的每个元素都用作 Tcl 脚本的参数,结果用于生成新的列表(由生成的元素组成)。 例如,将脚本返回到“1 2 3 4”列表中,我们将获得列表“1 4 9 25”,依此类推。 以下是map的第一个实现,但是我们将在关于Tcl 的功能编程的章节中写一个更好的例子。

proc map {varname mylist body} {
    upvar 1 $varname var
    set res {}
    foreach var $mylist {
        lappend res [uplevel 1 $body]
    }
    return $res
}

map过程与foreach命令非常相似,但是在每次迭代时,脚本的返回值都将附加到循环结束时由map返回的列表。 例如,为了计算您可以写的前 5 个自然数的平方:

map x {1 2 3 4 5} {expr $x*$x}

该命令的返回值为列表{1 4 9 16 25}。 另一个例子是使用 map 来转换其长度列表中的字符串列表。 这次tclsh用于交互式会话:

% set l {I will be translated into a list of length}
I will be translated into a list of length
% map x $l {string length $x}
1 4 2 10 4 1 4 2 6

它使很多任务更简单! 实际上, map做的是在一个程序中封装一段代码,否则可以输入多次,这就是这样的:

set result {}
foreach e $list {
    lappend result [string length $e]
}

这解释了为什么有经验的程序员认为具有极大扩展能力的编程语言更好:程序员可以在代码中查找模式(程序员正在做的重复操作的症状,语言可能为他做),并编写一个程序使特定任务发生频繁更简单。

rename 命令

在第一章中,我们提到使用proc可以创建具有已有命令(包括核心命令或已定义的命令)的名称的过程。 不仅可以覆盖现有的命令名称,还可以使用rename命令重命名现有命令。 用法非常简单:

rename oldName newName

重命名将命名oldName的名称更改为新名称newName ,但有一个例外:如果newName参数为空字符串,则oldName命令将从 Tcl 中删除。 可以使用此功能,以使语言在关键上下文中更安全(删除所有有潜在安全问题的命令),但我们将在下一章中看到更好的安全解释器

rename命令进入本章跟踪的图片,因为重命名命令的能力导致包装现有命令的能力。 例如,您可能希望包装puts命令,以便在使用多个参数调用时充当eval命令,使用空格分隔所有参数并将结果打印在屏幕上,但同时我们需要把原来的put实现放在别的名下,因为我们的 new put需要调用它才能执行输出。 我们可以使用重命名做所有这些:

rename puts _puts
proc puts args {
	_puts [join $args]
}

执行上述代码之后,put 将使用多个参数:

% puts multiple arguments passed to puts
multiple arguments passed to puts

包装过程可能很有趣,例如,您可能希望扩展proc功能,以便将proc描述定义为附加参数,然后编写命令以从命令名称获取文档,或者按顺序包装set在日志文件中打印关于修改变量的调试信息,以便跟踪一些错误,等等。

Tcl 8.5 中的列表扩展为参数

谈到eval,我们展示了如何使用这个命令来执行列表的元素作为单个命令参数的扩展。 在这个例子中,我们使用这个特征和+过程来计算整数列表的总和:

% set mylist [list 1 2 3 4 5]
1 2 3 4 5
% eval + $mylist
15

从 Tcl 8.5 开始,为了执行扩展,有一种更好的方式(从性能上和从用户的角度来看)。 这不是一个命令,而是新的语法。 基本上使用字符串{expand}添加一个参数的结果是,Tcl 解释器将把原来参数的每个元素都解释为一个 Tcl 列表,将其扩展为一个参数。 例如脚本:

{expand}{puts Hello}

set mylist [list puts Hello]
{expand}$mylist

都相当于

puts Hello

我们可以使用{expand}重写获取列表的整数元素的总和,而不使用eval

+ {expand}$mylist

$ mylist 的每个元素都将被扩展为+命令的参数。

在 Tcl 语法中引入{expand}是有争议的,许多人不喜欢语法,或者使 Tcl 语法更加复杂的想法(幸运的是,简单的想法在 Tcl 的团队中非常重要)。 严格来说,可以编写各种程序,而不必使用{expand},但实际上有很多情况需要扩展参数, eval不是参数扩展的最佳工具( eval的主要目标是不同的,它是评估 Tcl 脚本)。 我相信,在Tcl 核心团队决定将{expand}语言引入到语言中是一件很好的事情:现在,Tcl 8.5 接近发布,Tcl 的朋友们开始欣赏如何舒适和快速的论证扩展可以。