侧边栏壁纸
博主头像
Zengyq's Blog博主等级

但行好事,莫问前程!

  • 累计撰写 4 篇文章
  • 累计创建 10 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

TCL

zengyq
2024-09-01 / 0 评论 / 2 点赞 / 109 阅读 / 22761 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2024-09-26,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

由于 TCL 的解释器是用一个 C\C++ 语言的过程库实现的,可以把 TCL 看作一个 C 库,可以很容易就在 C\C++ 应用程序中嵌入 TCL。

TCL 解释器对一个命令的求值过程分为两部分:分析和执行。在分析阶段,TCL 解释器运用规则把命令分成一个个独立的单词,同时进行必要的置换(substitution);在执行阶段,TCL 解释器会把第一个单词当作命令名, 并查看这个命令是否有定义,如果有定义就激活这个命令对应的 C/C++ 过程,并把所有的单词作为参数传递给该命令过程,让命令过程进行处理。

TCL 脚本执行依赖于解释器逐行执行)。

一个 TCL 脚本可以包含一个或多个命令,命令之间必须用换行符或分号隔开。

TCL 每一命令包含一个或几个单词第一个单词代表命令名另外的单词则是这个命令的参数,单词之间必须用空格或TAB键隔开

置换

变量置换 $

所有的命令参数都当作字符串看待:

set x 10;			# 定义变量 x,并把x的值赋为 10
set y x+100;		# y 的值是“x+100”,而不是 110

变量置换由一个 $ 符号标记:

set y $x+100;		# y 的值是 10+100,x 被替换为了 10

命令置换 []

命令置换是由 [] 括起来的 TCL 命令及其参数,命令置换会导致某一个命令的所有或部分单词被另一个命令的结果所代替:

set y [expr $x+100];  # y 的值是110

[] 中脚本的值为最后一个命令的返回值,例如:

set y [expr $x+100; set b 300];  # y 的值为300,因为 set b 300 的返回值是300

有了命令置换,实际上就表示命令之间是可以嵌套的,即一个命令的结果可以作为别的命令的参数。

反斜杠置换 \

反斜杠 \ 置换似于 C 语言中反斜杠的用法,主要用于在单词符号中插入诸如换行符、空格[、$ 等被 TCL 解释器当作特殊符号对待:

set msg multiplex\ space;   # msg 的值为 multiple space

如果没有 '\' 的话,TCL 会报错,因为解释器会把这里最后两个单词之间的空格认为是分隔符,于是发现 set 命令有多于两个参数,从而报错。加入了 '\' 后,空格不被当作分隔符。

"" 和 {}

除了使用反斜杠外,TCL 使用双引号 "" 花括号 {} 来使得解释器把分隔符和置换符等特殊字符当作普通字符,而不作特殊处理

TCL 解释器双引号中的各种分隔符将不作处理,但是对换行符及 $ 和 [] 两种置换符会照常处理:

set y "$x ddd";    # y 的值是 10 ddd

而在花括号中,所有特殊字符都将成为普通字符,失去其特殊意义,TCL 解释器不会对其作特殊处理:

set y {/n$x [expr 10+100]};  # y 的值是 /n$x [expr 10+100]

注释

#必须出现在 TCL 解释器期望命令的第一个字符出现的地方,才被当作注释:

set a 100   # 不是注释,会报错
set b 101;  # 是注释

第一行中#就不被当作注释符,因为它出现在命令的中间,TCL 解释器把它和后面的字符当作命令的参数处理,从而导致错误。

第二行中,前一个命令已经用一个分号结束,#出现在了下一个命令的第一个字符,因此被视为注释。

变量

set、unset

TCL 中的 set 命令能生成一个变量,也能读取或改变一个变量的值:

set a {apple banana}

如果变量 a 还没有定义,这个命令将生成变量 a,并将其值置为 "apple banana",若 a 已定义,就简单的把 a 的值修改为"apple banana"。

unset 可以从解释器中删除变量:

unset a

append、incr

append 把文本加到一个变量的后面:

set txt hello;          # txt 的值是 hello
append txt "! apple";   # txt 的值是 hello! apple

incr 一个变量值加上一个整数。变量原来的值和新加的值都必须是整数:

set b 2;     # b 的值是 2
incr b -3;   # b 的值是 -1

数据结构

list

TCL 中 list 是由一堆元素组成的有序集合,list 可以嵌套定义,list 每个元素可以是任意字符串, 也可以是 list。

# 语法:list value value...
# 这个命令生成 list,元素是所有的 value
list 1 2 {3 4} ;            # list 的元素是1 2 {3 4}

# 整合两个 list
# 语法:concat list list...            
concat {1 2 3} {4 5 6};     # 整合后为 1 2 3 4 5 6
 
# 返回 list 的第 index 个元素
# 语法:lindex list index             
lindex {1 2 {3 4}} 2;      # 返回 list 的第 2 个元素 3 4

# 返回 list 的元素个数
# 语法:llength list                     
llength {1 2 {3 4}};       # 3 个元素

# 返回指定范围(从 index1 到 index2)的元素
#语法:lrange list index1 index2
lrange {1 2 3 4 5} 1 3;    # 返回 2 3 4

# 在 list 指定下标处插入元素
# 语法:linsert list index value1 value2…  
linsert {1 2 5 6} 1 7 8    # 1 7 8 2 5 6
 
# 在 list 末尾追加元素
# 语法:lappend varname value1 value2...
lappend a 1 2 3            # 在 a 的后面添加 3 个元素 

# 替换指定范围(从 index1 到 index2)的元素
# 语法:lreplace list index1 index2 value1 value2
lreplace {1 2 3 4 5} 1 3 6 6 6    # 返回 1 6 6 6 5

# 查找 list 中元素 value 的下标,找不到返回 -1
# 语法:lsearch list value
lsearch {1 2 3 5 6} 4      # 返回 -1

# 对列表排序(其中 switches 指定升降序等)
# 语法:lsort [switches] list
lsort {5 6 1 2 4 3}    # 返回 1 2 3 4 5 6

# 以指定字符为分隔符将列表中元素合并在一起
# 语法:join list joinChars
join {apple banana china} -    # 返回 apple-banana-china

# 以 splitChars 为分隔符,将字符串分解为一个列表
# 语法:split string splitChars
split apple-banana-china -      # 返回列表 apple banana china

# 遍历列表
# 语法:foreach var list { proc body }
foreach num $nums_list { puts $num }

# 判断元素是否在 list 内
# 语法:in 操作符, 如果在返回1, 否则返回 0; ni(not in) 与 in 相反
expr {$test1 in $test}

数组

Tcl 中的数组和其他高级语言的数组有些不同:Tcl 数组元素的索引,或称键值,可以是任意的字符串,而且其本身没有所谓多维数组的概念。

Tcl 中数组是一系列元素的集合,每个元素都是由元素名称和值构成的变量,元素名称由数组名和数组中的元素名构成。创建数组的过程就是对变量进行赋值的过程。在 Tcl 中任何合法的字符串都可以作为数组名或元素名,也可以作为变量值。元素名与值是成对创建的,因此数组也被称为关联数组。

# 创建数组
# 语法1:set arrName(index) value
# 语法2:array set arrName { index1 val1 index2 val2 ...}
set fruit(apple) 1
array set fruit {banana 2 china 3}

# 获取数组元素个数
array size fruit    # 返回 3

# 获取所有元素的索引和值
array get fruit    # 返回 apple 1 banana 2 china 3

# 获取所有元素索引名
array names fruit    # 返回 apple banana china

# 判断是否为数组变量,是返回 1,否则返回 0
array exists fruit    # 返回 1
array exists fruits   # 返回 0

# 打印出数组的所有元素名和元素值
parray fruit



# 遍历数组(重要)
# 方式一:foreach + array names
foreach name [array names fruit] {
  puts [format "name: %s --> value: %s" $name $fruit($name)]
}

# 方式二:foreach + array get
foreach {name value} [array get fruit] {
  puts [format "name: %s --> value: %s" $name $value]
}

# 方式三:for + 借助 list
set names [array names fruit]
for {set i 0} {$i < [llength $names]} {incr i 1} {
  set name [lindex $names $i]
  set val $fruit($name)
  puts [format "name: %s --> value: %s" $name $val]
}

# 方式四:while + array startsearch 等命令
set searchId [array startsearch fruit]
while {[array anymore fruit $searchId] == 1} {
  set name [array nextelement fruit $searchId]
  puts [format "name: %s --> value: %s" $name $fruit($name)]
}
array donesearch arr $searchId

# 其中,
array startsearch fruit           # 返回永远 fruit 进行搜索的搜索标记
array nextelement fruit searchId  # 返回下一个元素值,如果已经在尾部的话,返回空串
array donesearch fruit searchId   # 结束有 searchId 标识的搜索

控制流命令

if

if { $x>0 } {      # 注意'{'一定要写在上一行,否则,TCL解释器会认为if命令在换行符就结束了
  ...              # 同时,if和'{'之间应该有空格,否则解释器会把 if{ 作为一个整体命令导致出错
} elseif { $x==1 } {
  ...
} elseif { $x==2 } {
  ...
} else {
  ...
}

for

# 变量 a 是一个列表,下面代码把 a 的值复制到 b
set b ""
for { set i [expr [llength $a] -1] } { $i>=0 } { incr i -1 } {
  lappend b [lindex $a $i]
}

foreach

# 变量 a 是一个列表,下面代码把 a 的值复制到 b
set b ""
foreach i $a {
  set b [linsert $b 0 $i]
}

# 共有 3 次循环,x 的值为 b a d c f e
set x {}
foreach {i j} {a b c d e f} {
  lappend x $j $i
}

# 共有 4 次循环,y 的值为 a d b e c f {} g
set y {}
foreach i {a b c} j {d e f g} {
  lappend y $i $j
}

# 共有 3 次循环,z 的值为 a d e b f g c {} {}
set z {}
foreach i {a b c} {j k} {d e f g} {
  lappend z $i $j $k
}

while

# 变量 a 是一个列表,下面代码把 a 的值复制到 b
set b ""
set i [expr [llength $a] -1]
while { $i>=0 } {
  lappend b [lindex $a $i]
  incr i -1
}

break、continue

和 C/C++ 一样。break 命令结束整个循环过程并从循环中跳出,continue 只是结束本次循环。

switch

switch $x {
  a -
  b { incr 1 }
  c { incr 2 }
  d { incr 3 }
  default { incr -1 }
}

其中,a 后面跟一个- 表示使用和下一个模式相同的脚本。

source 命令

source 命令读一个文件,并把这个文件的内容作为一个脚本进行求值,例如:

source ./test.tcl

过程 proc

过程 proc 和 C/C++ 里面的函数类似。

proc add {x y} {
  expr $x+$y
}

proc abs {x} {
  if {$x>=0} { 
    return $x 
  }
  return [expr -$x]
}

puts [add 1 2];  # 输出 3
puts [abs -5];   # 输出 5

proc 命令的第一个参数是你要定义的过程的名字,第二个参数是过程的参数列表,参数之间用空格隔开,第三个参数是一个 TCL 脚本, 代表过程体。proc 生成一个新的命令,可以像固有命令一样调用。

局部变量和全局变量

对于在过程中定义的变量,因为它们只能在过程中被访问,并且当过程退出时会被自动删除,所以称为局部变量;在所有过程之外定义的变量我们称之为全局变量。

TCL 中,局部变量和全局变量可以同名,两者的作用域的交集为空:局部变量的作用域是它所在的过程的内部;全局变量的作用域则不包括所有过程的内部。这一点和 C/C++ 语言有很大的不同。

如果我们想在过程内部引用一个全局变量的值,可以使用global命令。

set a 4
proc sample {x} {
  global a
  incr a
  return [expr $a + $x]
}
puts [sample 3];  # 输出 8
puts $a;          # 输出 5

缺省参数和可变参数

可以定义具有缺省参数值的过程,我们可以为过程的部分或全部参数提供缺省值,如果调用过程时未提供那些参数的值,那么过程会自动使用缺省值赋给相应的参数。

和 C\C++ 中具有缺省参数值的函数一样,有缺省值的参数只能位于参数列表的后部,即在第一个具有缺省值的参数后面的所有参数,都只能是具有缺省值的参数。

proc add {val1 {val2 2} {val3 3}} {
  puts [expr $val1 + $val2 + $val3]
}
puts [add 1]       # 输出 6
puts [add 2 20]    # 输出 25
puts [add 4 5 6]   # 输出 15

TCL 的过程定义还支持可变个数的参数,如果过程的最后一个参数是args,那么就表示这个过程支持可变个数的参数调用。

调用时,位于args以前的参数像普通参数一样处理,args中的元素为一个列表。

proc add {val1 args} {
  set sum $val1
  foreach i $args {
    incr sum $i
  }
  return $sum
}
puts [add 2]          # 输出 2
puts [add 2 3 4 5 6]  # 输出 20

字符串操作

因为 TCL 把所有的输入都当作字符串看待,所以 TCL 提供了较强的字符串操作功能,TCL 中与字符串操作有关的命令有stringformatregexpregsubscan等。

format

format 按照指定的格式,把各个 value 的值组合到一个新的字符串中,并返回。

set name tom
set age 20
set msg [format "%s is %d years old" $name $age]
puts msg  # 输出 tom is 20 years old

scan

scan 命令可以认为是 format 命令的逆,它按 format 提供的格式分析 string 字符串,然后把结果存到指定的变量中,注意除了空格和 TAB 键之外,string 和 format 中的字符和%必须匹配。

scan "some 26 34" "some %d %d" a b
puts $a;  # 输出 26
puts $b;  # 输出 34

scan "12.34.56.78" "%d.%d.%d.%d" c d e f
puts [format "the value of c is %d, d is %d, e is %d, f is %d" $c $d $e $f]
# 输出 the value of c is 12, d is 34, e is 56, f is 78

regexp

regexp [switchs] [--] exp string [matchVar] [subMatchVar ...]

用于判断正则表达式 exp 是否全部或部分匹配字符串 string,匹配返回 1,否则 0。

如果 regexp 命令后面有参数 matchVar 和 subMatchVar,则所有的参数被当作变量名。regexp 把匹配整个正则表达式的子字符串赋值给第一个变量,匹配正则表达式的最左边的子表达式的子字符串复制给第二个变量,以此类推。

regexp {([0-9]+) *([a-z]+)} "there is 100 apples" total num word
puts "$total, $num, $word";  # 输出:100 apples, 100, apples

regexp 可以设置一些开关(switchs),来控制匹配结果:

  • -nocase:匹配时不考虑大小写

  • -indices:改变各个变量的值,这使各个变量的值变成了对应的匹配子串在整个字符串中所处位置的索引

    regexp -indices {([0-9]+) *([a-z]+)} "there is 100 apples" total num word
    puts "$total, $num, $word";  # 输出:9 18, 9 11, 13 18
    # 因为子串 100 apples 首尾在字符串中的下标分别为 9 和 18
  • -all:尽最大可能匹配

  • --:表示这后面再没有开关(switchs)了,即使后面有以-开头的参数也被当作正规表达式的一部分

  • ...

regsub

regsub [switchs] [--] exp string subSpec varname

regsub的第一个参数是一个整个表达式,第二个参数是一个输入字符串,这一点和regexp命令完全一样,也是当匹配时返回 1,否则返回 0。

不过regsub用第三个参数的值来替换字符串 string 中和正则表达式匹配的部分,第四个参数被认为是一个变量,替换后的字符串存入这个变量中。

regsub there "They live there lives" their x
puts $x;  # 输出:They live their lives

regsub 命令也有几个开关(switchs):

  • -nocase:匹配时不考虑大小写

  • -all :没有这个开关时,regsub 只替换第一个匹配,有了这个开关,regsub 将把所有匹配的地方全部替换

  • --:意义同 regexp 命令中

正则表达式常用字符

  • ·:匹配任意单个字符

  • ^:表示从头进行匹配

  • $:表示从末尾进行匹配

  • \:对紧跟的字符进行转义,匹配特殊字符

  • [chars]:匹配字符集合"chars"中的任意字符,如果是"^chars",表示匹配任意不在"chars"中的字符,chars 的表示方法支持 a-z 之类的表示,如:[a-z]、[0-9]

  • (regexp):把 regexp 作为一个单项进行匹配,匹配的结果放在一个组里面

  • *:对*前面的项进行 0 次或多次匹配

  • +:对+前面的项进行 1 次或多次匹配

  • ?:对?前面的项进行 0 次或 1 次匹配

  • regexp1 | regexp2:匹配 regexp1 或 regexp2 中的一项

string

string option arg [arg ...]

string 命令具有强大的操作字符串的功能,其中的 option 选项多达 20 个。下面只介绍其中常用的几个。

  1. 比较字符串大小

    # 把字符串 string1 和 string2 进行比较,返回值为 -1、0 或 1
    # 分别对应 string1 小于、等于或大于 string2
    string compare [-nocase] [-length int] string1 string2
    • -nocase:比较时不区分大小写

    • -length int:只比较前 int 个字符,如果 int 为负数,那么这个参数被忽略

  2. 判断字符串相等

    # 把字符串 string1 和 string2 进行比较, 如果两者相同返回值为 1, 否则返回 0
    # -nocase 和 -length 同上
    string equal [-nocase] [-length int] string1 string2
  3. 返回字符串长度

    # 输出 5
    string length "apple"
  4. 返回指定范围子串

    # 输出 apple
    string range "apple banana pear" 0 4
  5. 字符串大小写转换

    # 将 stringSpec 转换成大/小写
    # 如果给出了 first 和 last,就只转换 first 和 last 之间的字符
    string toupper stringSpec [first] [last]
    string tolower stringSpec [first] [last]
  1. 字符串匹配

    # 如果 pattern 匹配字符串 stringSpec,那么返回 1,否则返回 0
    # 如果有 -nocase,则不区分大小写
    string match [-nocase] pattern stringSpec

文件读写

proc test { pattern filename } {
  set f [open $filename r]
  while { [gets $f line] } {
    if { [regexp $pattern $line] } {
      puts stdout $line
    }
  }
  close $f
}
  1. 打开文件(open)

    # 以 access 方式打开文件 name
    # 返回供其他命令(gets、close等)使用的文件标识符
    open name [access]

    文件打开方式(access 的取值):

    r:只读,文件必须已经存在(默认方式)

    r+:读写,文件必须已经存在

    w:只写,如果文件存在则先清空,文件不存在则创建文件

    w+:读写,如果文件存在则先清空,文件不存在则创建文件

    a:只写(追加写),文件必须已经存在,并把指针指向文件尾巴

    a+:只读,并把指针指向文件尾巴,文件不存在则创建新的空文件

    open 命令返回一个字符串用于表示打开的文件。当调用别的命令对打开的文件进行操作时,就可以使用这个文件标识符。

  2. 读取文件内容(gets)

    gets fileId [varName]

    读 fileId 标识的文件的下一行,忽略换行符。如果命令中有 varName 就把该行赋给它,并返回该行的字符数(文件尾返回 -1)。

    如果没有 varName 参数,返回文件的下一行作为命令结果(如果到了文件尾,就返回空字符串)。

  3. 写文件(puts)

    puts [-nonewline] [fileId] string
    
    # 例:
    set fileId [open "examplt.txt" w]
    puts $fileId "Hello, world!"
    close $fileId

    -nonewline:如果指定,puts 不会在写入的字符串末尾添加换行符\n

    fileId:这是一个文件句柄,表示要写入的文件。如果省略,则向标准输出写入

    string:这是要写入文件的字符串

  4. 刷新缓冲器(flush)

    puts命令使用 C 的标准 I/O 库的缓冲区方案,这就意味着使用puts产生的信息不会立即出现在目标文件中。如果想使数据立即出现在文件中,则需要调用flush命令。

    flush fileId
  5. 关闭文件(close)

    close fileId

参考资料:

TCL培训教程|Jerkwin

2

评论区