我们在日常的TCL代码编写中,经常会使用如下命令的写法:
set a [dict create a1 1 a2 3 a3 5]
上述命令使用了命令的嵌套,也就是说主命令中包含有子命令。嵌套命令的执行是有严格的执行顺序的,这种执行顺序实际上是由命令解释器去定义的。通过测试我们可以发现,TCL的命令解释器对命令的解释执行顺序规则是:从左到右、从内到外。
从左到右:是指在TCL的命令解析时,属于同一层级的子命令的执行顺序是按照从左到右的顺序依次执行的(从严格的角度来说,除了多线程以外,没有可以同时执行的命令)。
从内到外:如果命令的嵌套有很多层,那么命令的执行顺序是从最内层子命令开始执行,然后到它的外一层命令执行,这样一直到最外层命令的执行。当前在每一层命令中如果包含多个同一层级的命令依然会按照“从左到右”的顺序执行。实际上这种从内到外的解释执行顺序我们可以使用递归算法就可以方便的实现。下面我们给出一个用于测试的命令:
set a [list [set i [expr 2+1]] [set i [expr $i+1]]]
上述命令几包含了多层子命令的嵌套,也包含了同层子命令有多个命令的情况。TCL解释器运行的结果是:“3 4”。
那么我们如何编写一个模仿TCL解释器解释执行该命令的函数呢?请看下面这段代码:
# 将命令分解成单词列表,并且不去掉元素的外层大括号或者双引号
proc words2list cmd {
if {![info complete $cmd]} {
puts "error [list {not a complete command} $cmd]"
return ""
}
# 去除命令中的续行符
set cmd [string map {"\\\n" ""} $cmd]
set words {}
set logical {}
set cmd [string trimleft $cmd[set cmd {}] "\f\n\r\t\v "]
while {[regexp {([^\f\n\r\t\v ]*)([\f\n\r\t\v ]+)(.*)} $cmd full first delim last]} {
append logical $first
if {[info complete $logical\n]} {
lappend words $logical
set logical {}
} else {
append logical $delim
}
set cmd $last[set last {}]
}
if {$cmd ne {}} {
append logical $cmd
}
if {$logical ne {}} {
lappend words $logical
}
return $words
}
# 按照从左往右的顺序解释执行命令中的子命令
proc substEval {substcmd} {
set cmdtemp {}
set res ""
# 将命令解析为字符串列表
set cmdwords [words2list $substcmd]
# 接着遍历命令列表,对每个元素执行子命令查找操作,找到了需要执行的子命令直接交给eval_subcommands去执行,实现逐层查找和由内而外的执行逻辑
foreach word $cmdwords {
set depth 0
set start 0
set end -1
for {set i 0} {$i < [string length $word]} {incr i} {
set char [string index $word $i]
# 判断当前的"\["是不是有效的子命令起始符,主要是辨别该字符前是否有0个或者偶数个"\\",如果是则是有效的子命令起始符,否则不是
if {$char eq "\["} {
if {[regexp {(\\*)\[$} [string range $word 0 $i] partfull part1]} {
if {[expr [string length $part1] % 2] eq 0} {
# 偶数个"\\",说明是有效的子命令起始符
if {$depth == 0} {
set start $i
}
incr depth
}
}
} elseif {$char eq "\]"} {
if {[regexp {(\\*)\]$} [string range $word 0 $i] partfull part1]} {
if {[expr [string length $part1] % 2] eq 0} {
# 偶数个"\\",说明是有效的子命令终止符
incr depth -1
if {$depth == 0} {
set end $i
set rescmd [string range $word [expr $start + 1] $end-1]
# 如果该命令是一个完整的命令则执行子命令逻辑
if {[info complete $rescmd]} {
# 找到该层的子命令后直接交给eval_subcommands去执行
set res [eval_subcommands $rescmd]
# 然后将执行结果替换掉字符串中原来的子命令
set word [string replace $word $start $i $res]
}
}
}
}
}
}
lappend cmdtemp $word
}
return $cmdtemp
}
# 按照命令替换规则解释执行命令中的子命令,并最后执行输入的主命令
proc eval_subcommands {command} {
# 首先解释执行该命令的下一层子命令,在该逻辑中会使用递归逻辑,逐层往内查找子命令
set cmdtemp [substEval $command]
set res ""
# 最后执行被转译后的命令,并返回命令执行结果
if {[catch {set res [uplevel 1 "eval \{$cmdtemp\}"]} msg] eq 1} {
puts "error $msg"
return ""
}
return $res
}
注意:上述代码只是为了实现本课中命令中子命令的解释执行而设计的,并未完整实现对命令的解释执行,因此在很多情况直接使用上述代码解释执行TCL命令会出错。如需完整实现命令的解析,还需加入更多命令的替换逻辑和解析逻辑,如字符串中变量的替换、转译字符的替换、特殊语法的处理等。
上述代码实现了3个函数,其中words2list函数,完整实现了将命令转化成字符串列表的逻辑;substEval实现了对命令中子命令解释执行的逻辑;eval_subcommands是本课示例代码的主入口函数,它调用substEval实现对命令(包含其中的子命令)解释执行并返回执行结果。
上述代码实现了对命令中的子命令按照从内到外,从左到右的解释执行逻辑,并实现了命令中子命令的替换操作。当然这里只是简单的逻辑实现,并未完整实现。完整的命令解析还需考虑更多情况,逻辑也会比现在的复杂的多。我们解决复杂问题需要按照化繁为简、化整为零的思维。希望大家持续关注新爸,新爸将接受你们的鼓舞,持续创作。