TCL 语言简介

2022-04-17
9 min read

Tcl 是比较古老的语言,硬件领域用的比较多,其他领域已不多见,下面是相关资料:

  1. B 站 tcl&tk 简介视频 /
  2. Learn TCL in Y Minutes / 《Tcl/Tk入门经典》 / 《TTPL(2017)》 / 官方资料 / 官方教程 /
  3. 不同语言实现的的解析器,例如简化版 tcl 解释器 partcl,对应的 blog 有简单介绍;还有 Tcl 8.0 简化版 picol;类似于 Tcl 但仅有一个 .h 文件和一个 .c 文件实现的语言 LIL
  4. 类似 Python,因为是脚本语言所以 Tcl 有脚本解释程序 tclsh 和 wish,后者提供了更多的命令,比如 tcl 的 GUI 库 tk 相关命令。没有 GUI 需求的脚本使用 tclsh 作为默认的解析器,移植性会更好一些。tk 使用可以参考TkDocs

Tcl 没有确定的语法来解释整个语言,而是由12 条基本规则定义。常见的语言比如 C,包含词法分析(Flex)和语法分析(Bison)等过程,Tcl 有简单的词法分析过程,并将“语法”分析部分留给了各个命令。分支控制(if/else)、循环(while)以及表达式等功能都是通过命令实现的,它们的具体含义并不能由 Tcl 解释器直接理解,而由命令实现来确定。下面代码中 while 命令后面的两组大括号并无不同,这两个大括号都是让 tcl 解释器把括号内的字符原封不动地传给 while 命令,具体循环细节由 while 命令控制

while {$val>0} {
    set result [expr $result*$val]
    incr val -1
}

与 C 语言做比较,C 中如下代码,y 的值为 14

x = 4;
y = x + 10;

对应的 Tcl 脚本,y 的值是字符串 "x+10" 而非 14

set x 4
set y x+10

在 Tcl 中对表达式求值需要明确指明

set x 4
set y [expr $x+10]

基本概念与定义

括号

Tcl 有多种“括号”:方括号(brackets,[])、花括号(braces,{})和双引号(quotes,""

  1. 方括号一般用来表示内嵌脚本,解析器会执行方括号内的脚本并用执行结果替换原来的内嵌脚本(Script substitution)
  2. 位于花括号内的空格、分号、$\ 没有任何特殊的意义,即空格不用于分隔单词,\ 也不用于字符转义,但 \newline 还有效,因为很多时候一行写不下所有代码;
  3. 引号包含的空格和分号将不做特殊处理,但其他特殊符号,例如 \ $ 还是会做处理

单词

单词Wrod)是 Tcl 的基本组成单元,命令中的单词由空格分隔,单词由一个或多个除换行符外的任意字符组成,命令Command)由单词列表组成

% set "nice to" "meet you"
meet you
% put $"nice to" # 单词首字母不是引号 ",故解析器会将 $"nice 当作一个单词
can not find channel named "$"nice"
% put ${nice to}
meet you

替换格式的字符串也是一个单词,例如参数展开内嵌脚本求值过程(Evaluation)将替换每一个替换格式的单词,例如由{*}开头的单词(即参数展开(Argument expansion))将在原来位置展开

12 条规则

细节请参考官网

  1. 命令(Commands)。Tcl 脚本(Script)由使用换行符或者分号分隔的命令组成
  2. 求值过程(Evaluation)。命令求值有两步①命令解析,解析器将命令拆解为单词并执行必要的替换,替换也是完整的求值过程②执行命令,解析后的命令,第一个单词是子程序(Routine)名,其他单词作为参数传递给子程序
  3. 单词(Wrods)。命令中的单词由空格分隔,单词可以由除换行符之外的任意字符组成(命令行由换行符分隔)
  4. 双引号(Double quotes)。如果一个单词的第一个字符是 ",那么单词的结尾将由下一个 " 标识,位于引号之间的字符串将作为一个完整的单词。双引号中命令替换(即 Script substitution)、变量替换和字符转义等操作会被执行
  5. 参数展开Argument expansion)。cmd a {*}{b [c]} d {*}{$e f {g h}}cmd a b {[c]} d {$e} f {g h}是等价的命令
  6. 花括号(Braces)。如果单词的第一个字符是 { 且规则 5 没有执行,那么单词的结尾由 } 标识
  7. 命令替换(Command substitution (script substitution))。如果一个单词包含一个 [ ,那么 Tcl 解析器将执行命令替换。如果 [ 之后的字符串还包含 [,解析器将被再次调用
  8. 变量替换(Variable substitution)。如果一个单词包含如下形式的字串,变量替换将被执行:$name$name(idx)${name}
  9. 字符转义(Backslash substitution)。Tcl 的字符转义方式类似于 C/C++
  10. 注释Comment)。如果一条命令的第一个非空白字符是#,那么这一行将被视为注释而忽略
  11. 替换顺序(Order of substitution)。替换从左向右执行,每个字符只会被解析器处理一次。当然,递归调用中内部的替换后生成的字符也只会被解析器处理一次。set y [set x 0][incr x][incr x] 将输出 012
  12. 替换与单词边界(Substitution and word boundaries)。除规则 5 之外,所有替换规则不影响单词边界,即不改变命令原有的单词个数。即使命令替换中脚本返回的字符包含空格,整个字符串也只会被当作一个单词

下面是源自官网教程的示例代码片段:

puts "Hello, World - In quotes"    ;# This is a comment after the command.
# This is a comment at beginning of a line
puts {Hello, World - In Braces}
puts {Bad comment syntax example}   # *Error* - there is no semicolon!

puts "This is line 1"; puts "this is line 2"

puts "Hello, World; - With  a semicolon inside the quotes"

# Words don't need to be quoted unless they contain white space:
puts HelloWorld

set a 456
set b 789
puts 123[set a]$b;# => 123456789

其他细节

更多 Tcl 语言细节可以参考Dodekalogue,Dodekalogue 以问答的形式回答了一些问题,例如 Tcl 是否支持双重参数展开、不同平台下哪些符号会被看作空格、什么是单词(Word)等

注意注释符必须出现在 tcl 以预期将获得命令的第一个字符的位置上。如果注释符出现在其他地方,会被看成 token 的一部分。尽量不要在大括号内写注释,容易造成错误;尽量以单行命令形式写注释;多行注释可以使用 if 实现,如下:

proc countdown {x} {
	puts "Running countdown"
	if 0 {
    	while {$x >= 0} {
			puts "x = $x"
			incr x -1
		}
	}
}

变量

预定义变量

tcl 定义了一些内置变量 tclvars,例如:argc, argv, argv0, auto_path, env, errorCode...

当调用 tclsh 或 wish 脚本文件时,脚本文件的文件名就存放在全局变量 argv0 中,命令行参数以列表形式存放在变量 argv 中, 命令行参数的个数存放在变量 argc 中,示例如下:

#!/usr/bin/env tclsh
puts "The command name is \"$argv0\""
puts "There were $argc arguments: $argv"

全局变量 env 是由 tcl 预定义的。它是一个数组变量,其元素是环境变量。例如 puts "Your home direcotry is $env(HOME)" 输出用户的主文件夹, 由 HOME 环境变量设定

全局变量 tcl_platform 是一个数组变量,其元素是对应用程序正在运行的平台的描述

puts $tcl_platform(platform);# unix
puts $tcl_platform(os);# Darwin
puts $tcl_platform(machine);# i386

命令与函数

set/expr/eval

set 命令(set varName ?value?)总会返回变量的值。set 可以接受一个或者两个参数,一个参数的时候返回变量的值,两个参数时使用第二个参数设置第一个变量的值。细节可以参考这里

Tcl 中 expr 命令用于求解表达式,例如 expr 1 + 2。expr 将其后面的多个参数以字符串拼接的形式生成一个表达式,因为这个特点,最好总是把表达式用大括号括起来。expr 支持的操作符和数学函数可参考这里,例如 +/-/*/in/ni/eq/ne

set x "123"
set result [expr $x eq "New York"];   # => expr {123 eq New York},无效命令
set result [expr {$x eq "New York"}]; # 有效命令

只要可能,Tcl 就把表达式作为数字处理。只有在使用字符串比较操作符时,或在使用关系操作符且至少一个操作数无法理解为数字时才进行字符串操作

Tcl 中的 eval 命令类似于 Python 中对应的命令,eval 可以执行一个满足要求的 Tcl 命令字符串。结合 string 相关命令,Tcl 在执行过程中可以创建新的命令并执行

数学函数示例

在 Tcl 8.5 中,当表达式解析器遇到像 sin($x) 这样的数学函数时,它会把函数置换为对 tcl::mathfunc 命名空间中的一个普通 Tcl 命令的调用。如果数学函数的参数中包含逗号,则由 expr 处理参数,将各个分开的参数传给函数的实现过程

expr {sin($x+$y)}; # 内部转换为下式
expr {[tcl::mathfunc::sin [expr {$x+$y}]]}

expr {atan2($y-0.3, $x/2)}; # 内部转换为下式
expr {[tcl::mathfunc::atan2 [expr {$y-0.3}] [expr {$x/2}]]}

字符串

Tcl 内部使用 Unicode 保存字符串,不过 Tcl 也支持其他类型的编码方式且提供了转换函数。字符串相关常用子命令有:index / range / length / trim / trimleft / repeat 等。其他字符串相关函数有 formatsubstr 等,细节可参考官方教程

string index "Sample string" 3; # => p

# 8.5后支持,索引参数中不能有空白,索引越界返回空字符串。end-1 对应着倒数第二个字符
string index "Sample string" end-$i
string range "Sample string" 3 end; # [3, end],包括 end 指向的字符

string is digit "A man, a plan ,a canal. Panama."; # => 0
format "The square root of 10 is %.3f" [expr sqrt(10)]; # => The square root of 10 is 3.162

Tcl 对正则的支持请参考其他文档,例如:http://regexlib.com/,Tcl 支持正则形式的搜索和替换。

使用 binary 命令可以操作二进制字符串,细节请参考官网

Message catalog

Tcl 使用 msgcat lib 实现国际化。msgcat 工具包的基本工作原理是:您创建一系列消息文件,每一个文件对应一种语言,包含您的应用程序或发布包所要显示的全部字符串的本地化版本。然后在您的应用程序或发布包中, 不要直接地使用字符串,而是调用 ::msgcat::mc 命令返回您想要的字符串的本地化版本

文件与网络

Tcl 默认提供了文件访问相关的命令,例如:open、close、read、puts 等,细节可参考官方教程

Tcl 也提供了网络相关命令,例如:socket、fileevent 等,细节请参考官网

自定义命令

Tcl 支持自定义函数,细节可参考这里,Tcl 使用 proc 命令定义新的命令

proc sum {arg1 arg2} {
    set x [expr {$arg1 + $arg2}];
    return $x
}
puts " The sum of 2 + 3 is: [sum 2 3]\n"

proc for {a b c} {
    puts "The for command has been replaced by a puts";
    puts "The arguments were: $a\n$b\n$c\n"
}
for {set i 1} {$i < 10} {incr i}

上面脚本执行结果如下:

 The sum of 2 + 3 is: 5

The for command has been replaced by a puts
The arguments were: set i 1
$i < 10
incr i

数据结构

关联数组

使用示例可参考官方教程

set earnings(January) 87966
set earnings(February) 95400
set earnings(January); # => 87966

set matrix(1,2) 140; # 多维数组,逗号间最好不要使用空格(1,2) 与(1, 2) 代表了不同的元素

不要把元素名用双引号括起来。如果这样做,双引号也会被识别为元素名的一部分,而不是元素名的定界符。可以使用诸如 0、1、2 之类的元素名,但是这些名称仍然会被解释为字符串而不是整型值。因此,名称为 1 的元素和名称为 1.0 的元素是不同的

列表

在 Tcl 命令中输入文字列表时,列表通常用大括号括起来,就像前面几个示例那样。这对大括号不是列表的一部分;命令行需要它们是要使整个列表作为一个单词传递。Tcl 中的列表支持嵌套,常用命令可参考这里,使用示例可参考官方教程

lindex {John Anne Mary Jim} 1; # => Anne
llength {}; # => 0

set test3 {a \} b \{ c}
llength $test3; # => 5

lindex {a b {c d e} f} 2; # => c d e

字典

字典的使用示例可参考官方教程。关联数组和字典很像,但关联数组在使用上有些不便,例如关联数组不能嵌入到其他数据结构中(例如 list),故 Tcl8.5 之后系统引入了字典(dict)命令。dict 可以嵌套 dict

Tcl 扩展

Tcl 扩展有两种方式,一种是使用其他语言(例如 C)调用 Tcl 脚本,并使用其他语言实现新的 Tcl 命令;另一种是使用 tclsh 执行 Tcl 脚本,新的命令可以使用 proc 命令进行扩展,或者 Tcl 解析器载入外部 lib 文件

使用 C 语言执行 Tcl 脚本的方式可以参考 Writing Tcl-Based Applications in C,更多细节可以参考《Tcl/Tk入门经典》第三部分。其他扩展方式可以参考 Extending Tcl 或者其他资料