跳转到内容

Tcl 编程/简介

来自维基教科书,开放世界中的开放书籍

什么是 Tcl?

[编辑 | 编辑源代码]

Tcl 的名字来源于“工具命令语言”(Tool Command Language),发音为“tickle”。Tcl 是一种非常简单的开源解释型编程语言,它提供诸如变量、过程和控制结构等常见功能,以及其他主要语言中没有的许多有用功能。Tcl 几乎可以在所有现代操作系统上运行,例如 Unix、Macintosh 和 Windows(包括 Windows Mobile)。

虽然 Tcl 足够灵活,可以用于几乎所有可以想象到的应用程序,但它在几个关键领域确实很出色,包括:自动与外部程序交互、将库嵌入应用程序程序、语言设计和通用脚本。

Tcl 由 John Ousterhout 于 1988 年创建,并根据 BSD 风格的 许可证 分发(它允许你做 GPL 所允许的一切,以及关闭你的源代码)。截止 2008 年 2 月,当前的稳定版本是 8.5.1(较旧的 8.4 分支中的 8.4.18)。

第一个与 Tcl 协同工作的重大 GUI 扩展是 Tk,它是一个旨在快速 GUI 开发的工具包。这就是为什么 Tcl 现在通常被称为 Tcl/Tk 的原因。

该语言具有深远的自省能力,它的语法虽然 简单,但却与 Fortran/Algol/C++/Java 世界截然不同。虽然 Tcl 是一种基于字符串的语言,但它有相当多的面向对象的扩展,例如 Snitincr TclXOTcl,仅举几例。

Tcl 最初是作为实验性计算机辅助设计 (CAD) 工具的可重用命令语言而开发的。解释器实现为一个 C 库,可以链接到任何应用程序中。为 Tcl 解释器添加新函数非常容易,因此它是一个理想的可重用“宏语言”,可以集成到许多应用程序中。

但是,Tcl 本身就是一种编程语言,可以粗略地描述为

  • LISP/Scheme(主要是因为它的尾递归功能),
  • C(控制结构关键字,表达式语法)和
  • Unix shell(但具有更强大的结构化功能)。

一种语言,多种风格

[编辑 | 编辑源代码]

虽然“一切皆命令”的语言似乎一定是“命令式”和“过程式”的,但 Tcl 的灵活性使得人们可以非常轻松地使用函数式或面向对象的编程风格。有关示例,请参见下面的“Tcl 示例”。

传统的“过程式”方法是

proc mean list {
   set sum 0.
   foreach element $list {set sum [expr {$sum + $element}]}
   return [expr {$sum / [llength $list]}]
}


以下还有另一种风格(在较长的列表上速度不快,但仅依赖于 Tcl)。它通过构建一个表达式来工作,其中列表的元素用加号连接,然后评估该表达式

proc mean list {expr double([join $list +])/[llength $list]}

从 Tcl 8.5 开始,将数学运算符公开为命令,以及使用扩展运算符,这种风格更好

proc mean list {expr {[tcl::mathop::+ {*}$list]/double([llength $list])}}

或者,如果你已经导入了 tcl::mathop 运算符,只需

proc mean list {expr {[+ {*}$list]/double([llength $list])}}

请注意,以上所有内容都是有效的独立 Tcl 脚本。

在 Tcl 中实现其他编程语言(无论是(逆波兰)表示法,还是其他任何语言)也非常容易,以便进行实验。人们可能会称 Tcl 为“计算机科学实验室”。例如,以下是如何在 Tcl 中计算数字列表的平均值(首先编写更多 Tcl 来实现 J 风格的函数式语言——请参见 Tacit programming in examples)

Def mean = fork /. sum llength

或者,人们可以实现类似于 FORTH 或 Postscript 的 RPN 语言,并编写

 : mean  dup sum swap size double / ;


一个更实际的方面是 Tcl 对“面向语言的编程”非常开放——在解决问题时,指定一个(小)语言,该语言最简单地描述和解决该问题——然后去实现该语言…

我为什么要使用 Tcl?

[编辑 | 编辑源代码]

好问题。一般建议是:“为工作使用最佳工具”。好的工匠有一套好工具,并且知道如何最好地使用它们。

Tcl 是其他脚本语言(如 awk、Perl、Python、PHP、Visual Basic、Lua、Ruby,以及任何其他即将出现的语言)的竞争者。这些语言各有优劣,当某些语言在适用性上相似时,最终就变成了一种个人喜好问题。

Tcl 的优势在于

  • 最简单的语法(可以轻松扩展)
  • 跨平台可用性:Mac、Unix、Windows 等
  • 强大的国际化支持:一切都是 Unicode 字符串
  • 健壮、经过良好测试的代码库
  • Tk GUI 工具包以 Tcl 为原生语言
  • BSD 许可证,允许开源使用,例如 GPL,以及闭源使用
  • 一个非常有帮助的社区,可以通过新闻组、Wiki 或聊天联系 :)

Tcl 并不是解决所有问题的最佳方案。但是,了解 Tcl 的功能却是一次宝贵的体验。

示例:一个微型 Web 服务器

[编辑 | 编辑源代码]

在逐一介绍 Tcl 的各个部分之前,提供一个稍微长一点的示例可能更合适,这样你就能体会到它是什么样的。以下是用 41 行代码编写的完整微型 Web 服务器,它提供静态内容(HTML 页面、图像),但也提供了一部分 CGI 功能:如果一个 URL 以 .tcl 结尾,就会调用一个 Tcl 解释器来执行它,并将结果(一个动态生成的 HTML 页面)发送出去。

请注意,不需要任何扩展包——Tcl 已经可以用 socket 命令很好地完成这些任务。套接字是一个可以与 puts 命令写入的通道。fcopy 命令异步地(在后台)从一个通道复制到另一个通道,源通道可以是进程管道(“exec tclsh” 部分)或一个打开的文件。

这个服务器经过测试,即使在 200MHz 的 Windows 95 上通过 56k 调制解调器并同时为多个客户端提供服务时也能很好地工作。此外,由于代码的简洁性,这是一个关于 HTTP 工作原理(部分)的教学示例。

# DustMotePlus - with a subset of CGI support
set root      c:/html
set default   index.htm
set port      80
set encoding  iso8859-1
proc bgerror msg {puts stdout "bgerror: $msg\n$::errorInfo"}
proc answer {sock host2 port2} {
    fileevent $sock readable [list serve $sock]
}
proc serve sock {
    fconfigure $sock -blocking 0
    gets $sock line
    if {[fblocked $sock]} {
        return
    }
    fileevent $sock readable ""
    set tail /
    regexp {(/[^ ?]*)(\?[^ ]*)?} $line -> tail args
    if {[string match */ $tail]} {
        append tail $::default
    }
    set name [string map {%20 " " .. NOTALLOWED} $::root$tail]
    if {[file readable $name]} {
        puts $sock "HTTP/1.0 200 OK"
        if {[file extension $name] eq ".tcl"} {
            set ::env(QUERY_STRING) [string range $args 1 end]
            set name [list |tclsh $name]
        } else {
            puts $sock "Content-Type: text/html;charset=$::encoding\n"
        }
        set inchan [open $name]
        fconfigure $inchan -translation binary
        fconfigure $sock   -translation binary
        fcopy $inchan $sock -command [list done $inchan $sock]
    } else {
        puts $sock "HTTP/1.0 404 Not found\n"
        close $sock
    }
}
proc done {file sock bytes {msg {}}} {
    close $file
    close $sock
}
socket -server answer $port
puts "Server ready..."
vwait forever

以下是我用它测试过的一个小“CGI”脚本(保存为 time.tcl)

# time.tcl - tiny CGI script.
if {![info exists env(QUERY_STRING)]} {
    set env(QUERY_STRING) ""
}
puts "Content-type: text/html\n"
puts "<html><head><title>Tiny CGI time server</title></head>
<body><h1>Time server</h1>
Time now is: [clock format [clock seconds]]
<br>
Query was: $env(QUERY_STRING)
<hr>
<a href=index.htm>Index</a>
</body></html>"

哪里可以获取 Tcl/Tk

[编辑 | 编辑源代码]

在大多数 Linux 系统上,Tcl/Tk 已经安装好了。你可以在控制台提示符(xterm 或类似的)中输入 tclsh 来确认。如果出现 "%" 提示符,就说明你已经准备好了。为了确保,在 % 提示符下输入 info pa 查看补丁级别(例如 8.4.9)以及 info na 查看可执行文件在文件系统中的位置。

Tcl 是一个开源项目。如果你想自己构建它,可以在 http://tcl.sourceforge.net/ 获取源代码。

对于所有主要平台,你可以从 ActiveState 下载二进制 ActiveTcl 发行版。除了 Tcl 和 Tk,它还包含许多流行的扩展——它被称为规范的“包含电池”发行版。

或者,你可以获得 Tclkit:一个封装在一个单个文件中的 Tcl/Tk 安装包,你不需要解压缩。运行时,该文件会将自己挂载为虚拟文件系统,允许访问其所有部分。

2006 年 1 月,发布了一个新的、有前景的 Tcl 单文件 vfs 发行版;eTcl。可在 http://www.evolane.com/software/etcl/index.html 下载 Linux、Windows 和 Windows Mobile 2003 的免费二进制文件。特别是在 PocketPC 上,它提供了一些其他端口中一直缺少的功能:套接字、窗口“缩回”,并且可以通过提供启动脚本以及安装纯 Tcl 库来扩展。

第一步

[edit | edit source]

要查看你的安装是否有效,你可以将以下文本保存到一个名为 hello.tcl 的文件中并运行它(在 Linux 上的控制台中输入 tclsh hello.tcl,在 Windows 上双击)

package require Tk
pack [label .l -text "Hello world!"]

它应该会弹出一个带有问候语的小灰色窗口。

要使脚本直接可执行(在 Unix/Linux 和 Windows 上的 Cygwin 上),请使用以下第一行(# 位于最左边)

#!/usr/bin/env tclsh

或(使用较旧的、已弃用的复杂风格)

#! /bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" ${1+"$@"}

这样,shell 就可以确定要使用哪个可执行文件来运行脚本。

一种更简单的方法,并且强烈推荐初学者和经验丰富的用户使用,就是以交互方式启动 tclsh 或 wish。你将在控制台中看到一个 % 提示符,可以在其中输入命令并查看其响应。即使错误信息在这里也很有帮助,也不会导致程序中止——不要害怕尝试任何你喜欢的操作!例如

$ tclsh
info patchlevel
8.4.12
expr 6*7
42
expr 42/0
divide by zero

你甚至可以以交互方式编写程序,最好是一行一行地写

proc ! x {expr {$x<=2? $x: $x*[! [incr x -1]]}}
! 5
120

有关更多示例,请参阅“快速浏览”一章。


语法

[edit | edit source]

语法仅仅是语言结构的规则。英语的简单语法可以这样说(暂时忽略标点符号)

  • 文本由一个或多个句子组成
  • 句子由一个或多个单词组成

虽然简单,但它也很好地描述了 Tcl 的语法——如果你将“脚本”替换为“文本”,将“命令”替换为“句子”。还存在另一个差异,即 Tcl 单词可以再次包含脚本或命令。所以

if {$x < 0} {set x 0}

是一个由三个单词组成的命令:if、一个用大括号括起来的条件、一个用大括号括起来的命令(也由三个单词组成)。

Take this for example

是一个格式良好的 Tcl 命令:它调用 Take(必须在之前定义)并传递三个参数“this”、“for”和“example”。命令可以随意解释其参数,例如

puts acos(-1)

会将字符串“acos(-1)”写入 stdout 通道,并返回空字符串“”,而

expr acos(-1)

会计算 -1 的反余弦并将结果返回 3.14159265359(Pi 的近似值),或者

string length acos(-1)

会调用 string 命令,该命令会再次分派到其 length 子命令,该子命令会确定第二个参数的长度并返回 8。

快速总结

[edit | edit source]

Tcl 脚本是一个字符串,它是一个命令序列,由换行符或分号分隔。

命令是一个字符串,它是一个单词列表,由空格分隔。第一个单词是命令的名称,其他单词作为参数传递给它。在 Tcl 中,“一切都是命令”——即使在其他语言中被称为声明、定义或控制结构的东西也是如此。命令可以随意解释其参数——特别是,它可以实现另一种语言,例如 expr

单词是一个字符串,它是一个简单的单词,或者以 { 开头并以匹配的 }(大括号)结尾,或者以 " 开头并以匹配的 " 结尾。用大括号括起来的单词不会被解析器评估。在引号中的单词中,在调用命令之前可能会发生替换

  • $[A-Za-z0-9_]+ 替换给定变量的值。或者,如果变量名包含该正则表达式以外的字符,则可以添加另一层大括号来帮助解析器正确识别
puts "Guten Morgen, ${Schüler}!"

如果代码写成 $Schüler,这将被解析为变量 $Sch 的值,紧跟着常量字符串 üler

  • 单词的一部分可以是嵌入式脚本:一个用 [] 方括号括起来的字符串,其内容在调用当前命令之前作为脚本进行评估(见上文)。

简而言之:脚本和命令包含单词。单词可以再次包含脚本和命令。(这会导致单词超过一页长……)

算术和逻辑表达式不是 Tcl 语言本身的一部分,而是 expr 命令的语言(也用于 ifforwhile 命令的一些参数),基本上等效于 C 语言的表达式,具有中缀运算符和函数。请参阅下面关于 expr 的单独章节。


手册页:11 条规则

[edit | edit source]

以下是 Tcl(8.4)的完整手册页,其中包含“十诫”,即 11 条规则。(从 8.5 开始,第十二条规则涉及 {*} 特性)。

以下规则定义了 Tcl 语言的语法和语义

(1) 命令 Tcl 脚本是一个字符串,包含一个或多个命令。分号和换行符是命令分隔符,除非用引号括起来,如以下所述。在命令替换期间,右括号是命令终止符(见下文),除非用引号括起来。

(2) 评估 命令评估分为两个步骤。首先,Tcl 解释器将命令分解成单词并执行替换,如以下所述。这些替换对所有命令执行的方式相同。第一个单词用于定位一个命令过程来执行命令,然后所有命令单词都传递给命令过程。命令过程可以随意解释其每个单词,例如整数、变量名、列表或 Tcl 脚本。不同的命令会不同地解释其单词。

(3) 单词 命令的单词由空格分隔(换行符除外,换行符是命令分隔符)。

(4) 双引号 如果单词的第一个字符是双引号 ("),则单词由下一个双引号字符终止。如果引号之间出现分号、右括号或空格字符(包括换行符),则它们将被视为普通字符并包含在单词中。命令替换、变量替换和反斜杠替换将对引号之间的字符执行,如以下所述。双引号不会作为单词的一部分保留。

(5) 大括号 如果单词的第一个字符是左大括号 ({),则单词由匹配的右大括号 (}) 终止。大括号在单词中嵌套:每个额外的左大括号都必须有一个额外的右大括号(但是,如果单词中左大括号或右大括号用反斜杠引起来,则它不会被计入定位匹配的右大括号)。除了以下描述的反斜杠-换行符替换之外,对大括号之间的字符不执行任何替换,分号、换行符、右括号或空格也不会被赋予任何特殊解释。单词将正好包含外层大括号之间的字符,不包括大括号本身。

(6) 命令替换 如果一个单词包含左括号 ([),则 Tcl 将执行命令替换。为此,它会递归地调用 Tcl 解释器来处理左括号后面的字符作为 Tcl 脚本。该脚本可以包含任意数量的命令,并且必须以右括号 (]) 终止。脚本的结果(即其最后一个命令的结果)将替换到单词中,代替括号及其之间的所有字符。在一个单词中可以进行任意数量的命令替换。对用大括号括起来的单词不执行命令替换。

(7) 变量替换 如果一个单词包含美元符号 ($),则 Tcl 将执行变量替换:美元符号和后面的字符将被单词中变量的值替换。变量替换可以采用以下任何形式

$name

Name 是标量变量的名称;名称是字母、数字、下划线或命名空间分隔符(两个或多个冒号)的序列。

$name(index)

Name 给出数组变量的名称,index 给出该数组中元素的名称。Name 只能包含字母、数字、下划线和命名空间分隔符,并且可以是空字符串。对 index 的字符执行命令替换、变量替换和反斜杠替换。

${name}

Name 是标量变量的名称。它可以包含除右大括号以外的任何字符。在一个单词中可以进行任意数量的变量替换。对用大括号括起来的单词不执行变量替换。

(8) 反斜杠替换 如果一个单词中出现反斜杠 (\),则会发生反斜杠替换。在除以下描述的情况之外的所有情况下,反斜杠都会被丢弃,后面的字符将被视为普通字符并包含在单词中。这允许将双引号、右括号和美元符号等字符包含在单词中,而不会触发特殊处理。下表列出了特殊处理的反斜杠序列,以及替换每个序列的值。

\a
可听警报(铃声)(0x7)。
\b
退格键(0x8)。
\f
换页符(0xc)。
\n
换行符(0xa)。
\r
回车符(0xd)。
\t
制表符(0x9)。
\v
垂直制表符 (0xb)。
\<newline>whiteSpace
一个空格字符将替换反斜杠、换行符以及换行符后的所有空格和制表符。此反斜杠序列是唯一的,因为它在命令实际解析之前,在单独的预处理阶段被替换。这意味着它即使出现在大括号之间也会被替换,并且如果结果空格不在大括号或引号中,则它将被视为单词分隔符。
\\
字面反斜杠 (\),没有特殊效果。
\ooo
数字 ooo(一个、两个或三个)给出将要插入的 Unicode 字符的八位八进制值。Unicode 字符的较高位将为 0。
\xhh
十六进制数字 hh 给出将要插入的 Unicode 字符的八位十六进制值。可以存在任意数量的十六进制数字;但是,除了最后两个之外,其他所有数字都会被忽略(结果始终是一个字节量)。Unicode 字符的较高位将为 0。
\uhhhh
十六进制数字 hhhh(一个、两个、三个或四个)给出将要插入的 Unicode 字符的十六位十六进制值。

除非如上所述的反斜杠-换行符,否则不会对括在大括号中的单词执行反斜杠替换。

(9) 注释 如果在 Tcl 预期命令的第一个单词的第一个字符出现的地方出现了井号 (#),则井号和其后的字符,一直到下一个换行符,都被视为注释并被忽略。注释字符仅在出现在命令开头时才有意义。

(10) 替换顺序 每个字符在 Tcl 解释器中被处理一次,作为创建命令单词的一部分。例如,如果发生了变量替换,则不会对变量的值执行任何进一步的替换;该值将原样插入到单词中。如果发生了命令替换,则嵌套命令将完全由对 Tcl 解释器的递归调用处理;在进行递归调用之前不会执行任何替换,并且不会对嵌套脚本的结果执行任何额外的替换。替换从左到右进行,每个替换在尝试执行下一个替换之前完全评估。因此,像

set y [set x 0][incr x][incr x]

这样的序列始终将变量 y 设置为值 012。

(11) 替换和词边界 替换不会影响命令的词边界。例如,在变量替换期间,变量的整个值将成为单个单词的一部分,即使变量的值包含空格。

注释

[edit | edit source]

注释的第一个规则很简单:注释以 # 开头,其中预期命令的第一个单词,并一直持续到行尾(可以通过尾部的反斜杠扩展到下一行)

# This is a comment \
going over three lines \
with backslash continuation

Tcl 新用户迟早会遇到的问题之一是注释的行为方式出乎意料。例如,如果您像这样注释掉代码的一部分

# if {$condition} {
    puts "condition met!"
# }

这碰巧可以工作,但注释中任何不平衡的大括号都可能导致意外的语法错误。原因是 Tcl 的分组(确定词边界)发生在考虑 # 字符之前。

要在同一行上在命令后面添加注释,只需添加分号即可

puts "this is the command" ;# that is the comment

注释仅在预期命令的地方被视为注释。在数据(例如 switch 中的比较值)中,# 只是一个字面字符

if $condition {# good place
   switch -- $x {
       #bad_place {because switch tests against it}
       some_value {do something; # good place again}
   }
}

要注释掉多行代码,最简单的方法是使用 “if 0”

if 0 {
    puts "This code will not be executed"
    This block is never parsed, so can contain almost any code
    - except unbalanced braces :)
}

数据类型

[edit | edit source]

在 Tcl 中,所有值都是字符串,并且短语“一切都是字符串”通常用来说明这一点。但是正如2可以在英语中解释为“数字 2”或“表示数字 2 的字符”,Tcl 中的两个不同函数可以以两种不同的方式解释相同的值。例如,expr 命令将 “2” 解释为一个数字,但 string length 命令将 “2” 解释为单个字符。Tcl 中的所有值都可以解释为字符或字符所代表的其他内容。需要记住的重要一点是,Tcl 中的每个值都是一个字符字符串,每个字符字符串可能被解释为其他内容,具体取决于上下文。这将在下面的示例中变得更加清晰。出于性能原因,从 8.0 版本开始的 Tcl 会跟踪字符串值和该字符串值的最后解释方式。本节涵盖了 Tcl 值(字符串)被解释为的各种“类型”。

字符串

[edit | edit source]

字符串是一个包含零个或多个字符的序列(其中几乎所有情况下都接受所有 16 位 Unicode,在下面更详细地介绍)。字符串的大小是自动管理的,因此您只需在字符串长度超过虚拟内存大小时才需要担心它。

与许多其他语言不同,Tcl 中的字符串不需要引号进行标记。以下是完全有效的

set greeting Hello!

引号(或大括号)更用于分组

set example "this is one word"
set another {this is another}

区别在于,在引号内,会执行替换(如变量、嵌入命令或反斜杠),而在大括号内,不会执行替换(类似于 shell 中的单引号,但可以嵌套)

set amount 42
puts "You owe me $amount" ;#--> You owe me 42
puts {You owe me $amount} ;#--> You owe me $amount

在源代码中,引号或大括号字符串可以跨越多行,物理换行符也是字符串的一部分

set test "hello
world
in three lines"

反转字符串,我们首先让一个索引 i 指向字符串的末尾,并递减 i 直到它为零,将索引的字符追加到结果 res 的末尾

proc sreverse str {
set res ""
for {set i [string length $str]} {$i > 0} {} {
    append res [string index $str [incr i -1]]
} 
set res
}

sreverse "A man, a plan, a canal - Panama"
amanaP - lanac a ,nalp a ,nam A


十六进制转储字符串

proc hexdump string {
    binary scan $string H* hex
    regexp -all -inline .. $hex
}

hexdump hello
68 65 6c 6c 6f

在字符串中查找子字符串可以通过多种方式完成

string first  $substr  $str ;# returns the position from 0, or -1 if not found
string match *$substr* $str ;# returns 1 if found, 0 if not
regexp $substr  $str ;# the same

匹配是在 string first 中使用精确匹配,在 string match 中使用 glob 样式匹配,在 regexp 中使用正则表达式匹配。如果 substr 中有对 glob 或正则表达式有特殊意义的字符,则建议使用 string first

列表

[edit | edit source]

许多字符串也是格式良好的列表。每个简单单词都是长度为 1 的列表,更长列表的元素由空格隔开。例如,一个对应于三个元素列表的字符串

set example {foo bar grill}

包含不平衡引号或大括号的字符串,或紧跟在大括号后面的非空格字符,无法直接解析为列表。您可以显式地分割它们以创建一个列表。

列表的“构造函数”当然叫做 list。建议在元素来自变量或命令替换时使用(大括号不会执行此操作)。由于 Tcl 命令本身就是列表,因此以下内容可以完全替代 list 命令

proc list args {set args}

列表可以再次包含列表,可以到任何深度,这使得对矩阵和树的建模变得容易。以下字符串表示一个 4 x 4 单位矩阵,它是一个包含列表的列表。外部大括号将整个内容分组为一个字符串,其中包括字面内部大括号和空格,包括字面换行符。然后,列表解析器将内部大括号解释为分隔嵌套列表。

{{1 0 0 0}
 {0 1 0 0}
 {0 0 1 0}
 {0 0 0 1}}

换行符也是有效的列表元素分隔符。

Tcl 的列表操作在一些示例中演示

set      x {foo bar}
llength  $x        ;#--> 2
lappend  x  grill  ;#--> foo bar grill
lindex   $x 1      ;#--> bar (indexing starts at 0)
lsearch  $x grill  ;#--> 2 (the position, counting from 0)
lsort    $x        ;#--> bar foo grill
linsert  $x 2 and  ;#--> foo bar and grill
lreplace $x 1 1 bar, ;#--> foo bar, grill

请注意,只有上面的lappend 是可变的。要就地更改列表(列表……)的元素,lset 命令很有用 - 只需提供所需的索引即可

set test {{a b} {c d}}
{a b} {c d}
lset test 1 1 x
{a b} {c x}

lindex 命令也接受多个索引

lindex $test 1 1
x

示例:要确定元素是否包含在列表中(从 Tcl 8.5 开始,可以使用in 运算符来实现这一点)

proc in {list el} {expr {[lsearch -exact $list $el] >= 0}}
in {a b c} b
1
in {a b c} d
#ignore this line, which is only here because there is currently a bug in wikibooks rendering which makes the 0 on the following line disappear when it is alone 
0

示例:按值从列表变量中删除元素(与 lappend 相反),如果存在

proc lremove {_list el} {
  upvar 1 $_list list
  set pos [lsearch -exact $list $el]
  set list [lreplace $list $pos $pos]
}

set t {foo bar grill}
foo bar grill
lremove t bar
foo grill
set t
foo grill

一个更简单的替代方案,它还删除了 el 的所有出现

proc lremove {_list el} {
  upvar 1 $_list list
  set list [lsearch -all -inline -not -exact $list $el]
}

示例:要从列表 L 中绘制一个随机元素,我们首先确定它的长度(使用 llength),将该长度乘以一个大于 0.0 小于 1.0 的随机数,截断该数为整数(因此它位于 0 到长度-1 之间),并将其用于索引(lindex)到列表中

proc ldraw L {
   lindex $L [expr {int(rand()*[llength $L])}]
}

示例:转置矩阵(交换行和列),使用整数作为生成的变量名

proc transpose matrix {
   foreach row $matrix {
       set i 0
       foreach el $row {lappend [incr i] $el}
   }
   set res {}
   set i 0
   foreach e [lindex $matrix 0] {lappend res [set [incr i]]}
   set res
}

transpose {{1 2} {3 4} {5 6}}
{1 3 5} {2 4 6}

示例:漂亮地打印一个表示表格的包含列表的列表

proc fmtable table {
   set maxs {}
   foreach item [lindex $table 0] {
       lappend maxs [string length $item]
   }
   foreach row [lrange $table 1 end] {
       set i 0
       foreach item $row max $maxs {
           if {[string length $item]>$max} {
               lset maxs $i [string length $item]
           }
           incr i
       }
   }
   set head +
   foreach max $maxs {append head -[string repeat - $max]-+}
   set res $head\n
   foreach row $table {
       append res |
       foreach item $row max $maxs {append res [format " %-${max}s |" $item]}
       append res \n
   }
   append res $head
}

测试

fmtable {
   {1 short "long field content"}
   {2 "another long one" short}
   {3 "" hello}
}
+---+------------------+--------------------+
| 1 | short            | long field content |
| 2 | another long one | short              |
| 3 |                  | hello              |
+---+------------------+--------------------+

枚举:列表也可以用来实现枚举(从符号到非负整数的映射)。lsearch/lindex 的一个很好的包装示例

proc makeEnum {name values} {
   interp alias {} $name: {} lsearch $values
   interp alias {} $name@ {} lindex $values
}

makeEnum fruit {apple blueberry cherry date elderberry}

这将“apple”分配给 0,“blueberry”分配给 1,等等。

fruit: date
3
fruit@ 2
cherry

数字

[edit | edit source]

数字是可以解析为数字的字符串。Tcl 支持整数(32 位或 64 位宽)和“双精度”浮点数。从 Tcl 8.5 开始,支持大数(任意精度整数)。算术运算通过expr 命令完成,该命令基本上采用与 C 相同的运算符语法(包括三元运算符 x?y:z)、括号和数学函数。有关 expr 的详细讨论,请参见下文。

使用format 命令控制数字的显示格式,该命令执行适当的舍入

expr 2/3.
0.666666666667
format %.2f [expr 2/3.]
0.67

在 8.4 版本(当前版本为 8.5)之前,Tcl 遵循 C 语言的约定,即以 0 开头的整数被解析为八进制,因此

0377 == 0xFF == 255

这在 8.5 中发生了变化,尽管 - 太多人偶然发现了作为小时或月份的“08”,并引发了语法错误,因为 8 不是有效的八进制数。在将来,如果您确实想要八进制,则必须编写 0o377。您可以使用 format 命令执行数字进制转换,其中格式为 %x 表示十六进制、%d 表示十进制、%o 表示八进制,输入数字应具有 C 语言的标记来指示其进制

format %x 255
ff
format %d 0xff
255
format %o 255
377
format %d 0377
255

具有整数值的变量可以使用 incr 命令最有效地修改

incr i    ;# default increment is 1
incr j 2
incr i -1 ;# decrement with negative value
incr j $j ;# double the value

最大正整数可以从十六进制形式确定,前面是 7,后面跟着几个“F”字符。Tcl 8.4 可以使用 64 位的“宽整数”,并且最大整数是

expr 0x7fffffffffffffff
9223372036854775807

演示:再加一个,它就变成了最小整数

expr 0x8000000000000000
-9223372036854775808

大数:从 Tcl 8.5 开始,整数可以是任意大小的,因此不再存在最大整数。例如,您想要一个很大的阶乘

proc tcl::mathfunc::fac x {expr {$x < 2? 1: $x * fac($x-1)}}

expr fac(100)


93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

IEEE 特殊浮点数: 从 8.5 版本开始,Tcl 支持几种特殊的浮点数,即 Inf(无穷大)和 NaN(非数字)。

set i [expr 1/0.]
Inf
expr {$i+$i}
Inf
expr {$i+1 == $i}
1
set j NaN ;# special because it isn't equal to itself
NaN
expr {$j == $j}
#ignore this line, which is only here because there is currently a bug in wikibooks rendering which makes the 0 on the following line disappear when it is alone 
0

布尔值

[edit | edit source]

Tcl 与 C 类似,以数字形式支持布尔值,其中 0 代表假,任何其他数字代表真。它还支持字符串 "true"、"false"、"yes" 和 "no" 以及其他一些字符串(见下文)。规范的 "true" 值(由布尔表达式返回)为 1。

foreach b {0 1 2 13 true false on off no yes n y a} {puts "$b -> [expr {$b?1:0}]"}
0 -> 0
1 -> 1
2 -> 1
13 -> 1
true -> 1
false -> 0
on -> 1
off -> 0
no -> 0
yes -> 1
n -> 0
y -> 1
expected boolean value but got "a"

字符

[edit | edit source]

字符是书写元素(例如字母、数字、标点符号、汉字、连字等)的抽象。从 8.1 版本开始,Tcl 内部使用 Unicode 表示字符,Unicode 可以被视为 0 到 65535 之间的无符号整数(最近的 Unicode 版本甚至超出了这个边界,但 Tcl 实现目前最多使用 16 位)。任何 Unicode U+XXXX 可以使用 \uXXXX 转义符指定为字符常量。建议在 Tcl 脚本中直接使用 ASCII 字符 (\u0000-\u007f),并对其他所有字符进行转义。

使用以下方法在数字 Unicode 和字符之间进行转换:

set char [format %c $int]
set int  [scan $char %c]

注意,超过 65535 的 int 值会再次产生“递减”的字符,而负 int 值甚至会产生两个伪字符。format 不会发出警告,因此最好在调用之前进行测试。

字符序列称为字符串(见上文)。单个字符没有特殊的数据类型,因此单个字符只是一个长度为 1 的字符串(一切都字符串)。在 UTF-8 中,Tcl 内部使用的编码,单个字符的编码可能占用 1 到 3 个字节的空间。要确定单个字符的字节长度:

string bytelength $c ;# assuming [string length $c]==1

字符串例程也可以应用于单个字符,例如 [string toupper] 等。使用以下方法找出字符是否在给定集合(字符字符串)中:

expr {[string first $char $set]>=0}

由于字符的 Unicode 处于不同的范围内,检查字符代码是否在一个范围内可以或多或少地粗略地对其类别进行分类:

proc inRange {from to char} {
    # generic range checker
    set int [scan $char %c]
    expr {$int>=$from && $int <= $to}
}
interp alias {} isGreek {}    inRange 0x0386 0x03D6
interp alias {} isCyrillic {} inRange 0x0400 0x04F9
interp alias {} isHangul {}   inRange 0xAC00 0xD7A3

这是一个有用的帮助程序,用于将所有超出 ASCII 集的字符转换为它们的 \u.... 转义符(因此结果字符串是严格的 ASCII):

proc u2x s {
   set res ""
   foreach c [split $s ""] {
     scan $c %c int
     append res [expr {$int<128? $c :"\\u[format %04.4X $int]"}]
   }
   set res
}

内部表示

[edit | edit source]

在用 C 编写的 Tcl 主实现中,每个值都具有字符串表示(UTF-8 编码)和结构化表示。这是一种实现细节,可以提高性能,但对语言没有语义影响。Tcl 跟踪这两种表示,确保如果其中一个发生更改,另一个表示将在下次使用时更新以反映该更改。例如,如果一个值的字符串表示为 "8",并且该值最后一次作为数字在 [expr] 命令中使用,那么它的结构化表示将是数值类型,例如有符号整数或双精度浮点数。如果值 "one two three" 最后一次在列表命令中使用,那么它的结构化表示将是列表结构。C 侧有各种其他“类型”,它们可以被用作结构化表示。从 Tcl 8.5 版本开始,只存储值的最新结构化表示,并在需要时用其他表示替换它。这种值的“双端口”有助于避免重复解析或“字符串化”,否则这些操作会经常发生,因为每次在源代码中遇到一个值时,它都会被解释为字符串,然后才能在当前上下文中被解释。但是对于程序员来说,“一切都是字符串”的观点仍然得到维护。

这些值存储在称为对象的引用计数结构中(对象一词有很多含义)。从所有使用值的代码(与实现特定表示的代码相反)的角度来看,它们是不可变的。在实践中,这是通过写时复制策略实现的。

变量

[edit | edit source]

变量可以是局部变量或全局变量,也可以是标量变量或数组变量。它们的名称可以是任何不包含冒号(冒号保留用于命名空间分隔符)的字符串,但为了 $-解引用方便,通常使用 [A-Za-z0-9_]+ 模式,即一个或多个字母、数字或下划线。

变量不需要事先声明。如果变量之前不存在,则在第一次赋值时创建,并在不再需要时取消设置。

set foo    42     ;# creates the scalar variable foo
set bar(1) grill  ;# creates the array bar and its element 1
set baz    $foo   ;# assigns to baz the value of foo
set baz [set foo] ;# the same effect
info exists foo   ;# returns 1 if the variable foo exists, else 0
unset foo         ;# deletes the variable foo

使用 $foo 符号检索变量的值仅仅是 [set foo] 的语法糖。后者更强大,因为它可以嵌套,以进行更深层的解引用。

set foo   42
set bar   foo
set grill bar
puts [set [set [set grill]]] ;# gives 42

有些人可能期望 $$$grill 返回相同的结果,但事实并非如此,因为 Tcl 解析器。当它遇到第一个和第二个 $ 符号时,它尝试找到一个变量名称(包含一个或多个字母、数字或下划线),但没有成功,因此这些 $ 符号按字面意思保留下来。第三个 $ 允许对变量 grill 进行替换,但不会对之前的 $ 符号进行回溯。因此,$$$grill 的计算结果为 $$bar。嵌套的 [set] 命令使用户可以更好地控制。

局部变量与全局变量

[edit | edit source]

局部变量只存在于定义它的过程中,并在过程完成后被释放。默认情况下,过程中的所有变量都是局部变量。

全局变量存在于过程之外,只要它们没有被明确取消设置。它们可能需要用于长期存在的数据,或者不同过程之间的隐式通信,但总的来说,尽可能少使用全局变量更安全,也更有效。下面是一个只有一个账户的简单银行示例:

 set balance 0 ;# this creates and initializes a global variable

 proc deposit {amount} {
    global balance
    set balance [expr {$balance + $amount}]
 }

 proc withdraw {amount} {
    set ::balance [expr {$::balance - $amount}]
 }

这说明了两种引用全局变量的方法 - 或者使用 global 命令,或者使用 :: 前缀限定变量名称。变量 amount 在两个过程中的都是局部变量,它的值是相应过程的第一个参数的值。

自省

info vars ;#-- lists all visible variables
info locals
info globals

在过程(不建议)中使所有全局变量可见:

eval global [info globals]

标量变量与数组变量

[edit | edit source]

上面在 数据类型 中讨论的所有值类型都可以放入标量变量中,这是普通类型。

数组是变量的集合,由一个可以是任何字符串的键索引,实际上是哈希表。其他语言中被称为 "array"(由整数索引的值的向量)的东西,在 Tcl 中更像列表。一些示例:

#-- The key is specified in parens after the array name
set         capital(France) Paris

#-- The key can also be substituted from a variable:
set                  country France
puts       $capital($country)

#-- Setting several elements at once:
array set   capital         {Italy Rome  Germany Berlin}

#-- Retrieve all keys:
array names capital    ;#-- Germany Italy France -- quasi-random order

#-- Retrieve keys matching a glob pattern:
array names capital F* ;#-- France

一个奇特的数组名称是 ""(空字符串,因此我们可以称之为“匿名数组”:),这使得阅读起来很愉快。

set (example) 1
puts $(example)

请注意,数组本身不是值。它们可以作为引用传递到过程,而不是作为 $capital(这会尝试检索值)。dict 类型(从 Tcl 8.5 开始可用)可能更适合这些目的,同时还提供哈希表功能。

系统变量

[edit | edit source]

在启动时,tclsh 提供以下全局变量:

argc
命令行上的参数数量
argv
命令行上的参数列表
argv0
可执行文件或脚本的名称(命令行上的第一个词)
auto_index
包含从何处加载更多命令的指令的数组
auto_oldpath
(与 auto_path 相同?)
auto_path
用于搜索包的路径列表
env
数组,反映环境变量
errorCode
最后一个错误的类型,或 {},例如 ARITH DIVZERO {divide by zero}
errorInfo
最后一个错误消息,或 {}
tcl_interactive
如果解释器是交互式的,则为 1,否则为 0
tcl_libPath
库路径列表
tcl_library
Tcl 系统库目录的路径
tcl_patchLevel
详细的版本号,例如 8.4.11
tcl_platform
包含操作系统信息的数组
tcl_rcFileName
初始资源文件的名称
tcl_version
简短的版本号,例如 8.4

可以使用临时环境变量从命令行控制 Tcl 脚本,至少在包括 Cygwin 的类 Unix 系统中。示例脚本片段:

set foo 42
if [info exists env(DO)] {eval $env(DO)}
puts foo=$foo

此脚本通常会报告:

 foo=42

要对其进行远程控制而无需编辑,请在调用之前设置 DO 变量:

DO='set foo 4711' tclsh myscript.tcl

这将明显报告:

foo=4711

解引用变量

[edit | edit source]

引用是指一个指向另一个事物的东西(如果你不介意我使用这个科学术语)。在 C 语言中,引用通过*指针*(内存地址)实现;在 Tcl 中,引用是字符串(一切皆为字符串),具体来说是变量名,通过哈希表可以解析(解引用)到它们指向的“另一个事物”。

puts foo       ;# just the string foo
puts $foo      ;# dereference variable with name of foo
puts [set foo] ;# the same

可以使用嵌套的 set 命令多次执行此操作。比较以下 C 和 Tcl 程序,它们执行相同的(微不足道的)工作,并表现出显著的相似性。

#include <stdio.h>
int main(void) {
  int    i =      42;
  int *  ip =     &i;
  int ** ipp =   &ip;
  int ***ippp = &ipp;
  printf("hello, %d\n", ***ippp);
  return 0;
}

...以及 Tcl

set i    42
set ip   i
set ipp  ip
set ippp ipp
puts "hello, [set [set [set [set ippp]]]]"

C 中的星号对应于 Tcl 中对 set 的调用,进行解引用。C 中的 & 运算符没有对应的运算符,因为在 Tcl 中,声明引用不需要特殊的标记。这种对应关系并不完美;有四个 set 调用,但只有三个星号。这是因为在 C 中提及一个变量是隐式的解引用。在本例中,解引用用于将它的值传递给 printf。Tcl 将所有四个解引用都显式化(因此,如果你只有 3 个 set 调用,你会看到 hello,i)。单次解引用非常常用,因此通常缩写为 $varname,例如

puts "hello, [set [set [set $ippp]]]"

使用 set 的地方 C 使用星号,而最后一个(默认)解引用使用 $。

变量名哈希表要么是全局的,用于在该作用域中评估的代码,要么是局部于过程的。你仍然可以使用 upvar 和 global 命令“导入”作用域“更高”的变量的引用。(如果名称是唯一的,C 中后者是自动的。如果 C 中有相同的名称,则最内层作用域获胜)。

变量跟踪

[编辑 | 编辑源代码]

Tcl 的一个特殊功能是,你可以将跟踪与变量(标量、数组或数组元素)相关联,这些变量在可选情况下会在读取、写入或取消设置时进行评估。

调试是其中一个明显的用途。但还有更多可能性。例如,你可以引入常量,任何尝试更改其值的尝试都会引发错误。

proc const {name value} {
  uplevel 1 [list set $name $value]
  uplevel 1 [list trace var $name w {error constant ;#} ]
}

const x 11
incr x
can't set "x": constant

跟踪回调会附加三个词:变量名;数组键(如果变量是数组,否则为空),以及模式。

  • r - 读取
  • w - 写入
  • u - 取消设置

如果跟踪只是一个像上面一样的单条命令,并且你不想处理这三个,请使用注释“;#”将它们屏蔽掉。

另一种可能性是将本地对象(或过程)与变量绑定 - 如果变量被取消设置,则对象/过程会被销毁/重命名。

华夏公益教科书