Tcl一个可嵌入的命令语言.docx
《Tcl一个可嵌入的命令语言.docx》由会员分享,可在线阅读,更多相关《Tcl一个可嵌入的命令语言.docx(17页珍藏版)》请在冰豆网上搜索。
Tcl一个可嵌入的命令语言
Tcl:
一个可嵌入的命令语言
Tcl是用于工具命令语言的一个解释器。
它由作为基本命令解释器嵌入到工具(比如编辑器、调试器等)中的一个库包组成。
Tcl提供了(a)用于简单的文本命令语言的分析器,(b)一组内置的实用命令,和(c)一个C接口,工具通过它用特定于工具的命令增加内置命令。
Tcl在与窗口组件库整合的时候特别有吸引力:
它通过了提供变量、过程、表达式等机制增进了组件的可编程性;它允许用户编程组件的外观和动作二者;并且它在交互式程序之间提供一个简单但强力的通信机制。
本文出现于 1990WinterUSENIXConferenceProceedings
1.介绍
Tcl代表了“toolcommandlanguage-工具命令语言”。
它由一个库包组成,程序可以把它用作自己的命令语言的基础。
Tcl的开发由两项观察所推动。
第一项观察是,通用可编程命令语言通过允许用户用命令语言写程序来扩展工具的内置设施,从而扩大了工具的能力。
在强力的命令语言之中最众所周知的例子是UNIXshell[5]和Emacs编辑器[8]。
在各自情况下,出现的有着不同寻常能力的计算环境,在很大程度上是因为能获得可编程的命令语言。
第二个促成它的观察是交互式应用正在增长。
在1970年代晚期和1980年代早期的分时环境中,几乎所有的程序都是面向批处理的。
典型的使用交互式的命令shell来调用它们。
除了shell之外,只有少数其他的程序是交互式的,比如编辑器和邮件器。
正好相反,今天使用的个人工作站,带有它们自己的光栅显示器和鼠标,鼓励了一种不同的系统结构,在这里大量的程序是交互式的,并且最常见的交互方式是直接用鼠标操纵单独的应用。
此外,今天能获得的大显示器使很多交互式的应用立即活跃起来成为可能,而对于在十年前很小的屏幕这是不实际的。
不幸的是,很少的今天的交互式程序拥有shell或Emacs命令语言的能力。
在这里好的命令语言是存在着的,它们趋向与特定的程序捆绑在一起。
每个新的交互式程序都要求开发一个新的命令语言。
在多数情况下,应用程序员没有时间或爱好去实现一个通用设施(特别是在应用自身很简单的时候),所以结果的命令语言趋向于带有不充分的功能和笨拙的语法。
Tcl是一个独立于应用的命令语言。
它作为一个C库包存在,可以用于很多不同的程序中。
Tcl库提供了用于简单但完全可编程的命令语言的一个分析器。
这个库还实现了提供了通用的编程构造的一组内置命令,比如变量、列表、表达式、条件、循环和过程。
单个的应用程序可以用特定于应用的命令来扩展基本的Tcl语言。
Tcl库还提供一组实用工具例程来简化特定于工具的命令的实现。
我相信Tcl在窗口环境中是特别有用的,它提供了两项优势。
首先,它可以用做编制应用的界面的一个通用机制。
如果一个工具基于Tcl,则应当相对容易的去修改应用的用户界面,并使用新命令来扩展这个界面。
其次和更重要的是,Tcl为工具之间通信提供一种统一的框架。
如果在所有的工具中统一使用了它,Tcl将使工具在一起工作得比今天的状况更加优雅。
本文余下部分组织如下:
第2节描述用户见到的Tcl语言。
第3节讨论在应用程序中如何使用Tcl,包括在应用程序和Tcl库之间的C语言接口。
第4节描述在窗口环境中如何使用Tcl来定制界面动作和外观。
第5节展示如何使用Tcl作为进程间通信的媒介,和为什么这很重要。
第6表述Tcl实现的状态和一些初步的性能测量。
第7节把Tcl与Lisp、Emacs和NeWS做对比,第8节总结本文。
2. Tcl语言
在某种意义上,Tcl语言的语法是不重要的:
任何编程语言,不管它是C[6]、Forth[4]、Lisp[1]还是Postscript[2]都可以提供同Tcl大体相同的可编程性和通信上的优势。
这提示了最佳实现途径是借用现存的语言,并集中于为使用这门语言提供一个方便的框架。
但是,可嵌入的命令语言的所处环境对语言提出了一组不同寻常的约束,后面将描述它们。
我最终决定了从头设计一个新语言更有可能满足这些约束,并比现存的语言带有更少的实现努力。
Tcl是不寻常的因为它提供两种不同的接口:
给用户发起Tcl命令的一个文本接口,和给它所嵌入的应用的一个过程接口。
这些接口的每个都必须是简单的、强力的和高效的。
在语言设计中有四个主要的因素:
[1] 语言用于命令。
几乎所有Tcl“程序”都是短小的,很多只有一行长。
多数程序将是键入的,执行一次或者几次,接着就丢弃了。
这提示了这门语言应当有一个简单的语法,以便于键入命令。
多数现存的编程语言都有复杂的语法;在写长程序的时候有益,但如果用做命令语言就笨拙了。
[2] 语言必须是可编程的。
它应当包含通用编程构造,比如变量、过程、条件和循环,这样用户可以通过写Tcl过程来扩展内置的命令。
可扩展性也要求简单的语法:
这使Tcl程序生成其他Tcl程序变得容易了。
[3] 语言必须允许一个简单而高效的解释器。
由于Tcl库要包含到许多小程序中,特别是在没有共享库的机器上,解释器必须不占用太多的内存。
用来解释Tcl命令的机制必须足够快,可用于每秒发生上百次的事件,比如鼠标移动。
[4] 语言必须允许对C应用的一个简单接口。
它必须易于让C应用调用这个解释器,并易于让它们用特定于应用的命令来扩展内置的命令。
这个因素是我决定不使用Lisp作为命令语言的原因之一:
Lisp的基本数据类型和存储管理机制与C实在是不同,很难在它们之间建立清晰而简单的接口。
对Tcl我使用了对于C最自然的数据类型(字符串)。
2.1.Tcl语言语法
Tcl的基本语法类似于UNIXshell:
命令由用空格或TAB分隔的一个或多个字段组成。
第一个字段是命令的名字,它可以是内置命令、特定于应用的命令、或者是由一系列的Tcl命令组成的过程。
在第一个后面的字段都作为参数传递给命令。
如同在UNIXshell中那样,换行字符用做命令分隔符,分号也可用来分隔在同一行上的命令。
不同于UNIXshell,每个Tcl命令返回一个字符串结果,或者是空串,如果不适宜返回值的话。
在Tcl中有四个补充的语法构造,它们给予语言一种类似Lisp的风格。
使用花括号来组合复杂的参数;它们充当可嵌套的引用字符。
如果参数的第一个字符是左花括号,则这个参数不以空白终结。
转而,它终结于相匹配的右花括号。
传递给这个命令的参数由在花括号中间的所有东西组成,并剥除围绕的花括号。
例如,命令
seta{dogcat{horsecowmule}bear}
将收到两个参数:
“a”和“dogcat{horsecowmule}bear”。
这个特定命令将把变量a设置为等于第二个参数的一个字符串。
如果参数包围在花括号中,则不对这个参数做下面描述的其他替换。
花括号最常见的用途是把一个Tcl子程序指定为到Tcl命令的参数。
在Tcl中第二个语法构造是是方括号,它用于引发命令替换。
如果在参数中出现了左方括号,则从这个左方括号一直到相匹配的右方括号的所有东西都作为一个命令来对待,并由Tcl解释器递归的执行。
命令的结果接着替换到这个方括号包围的字符串所在的位置上。
例如,考虑命令
seta[format{SantaClausis%syearsold}99]
format命令做类似printf的格式化并返回字符串“SantaClausis99yearsold”,接着把它传递给set并赋值到变量a。
第三个语法构造是美元号,它用于变量替换。
如果它出现在参数中,则随后的字符作为变量的名字对待;变量的内容被替换到参数中这个美元符号和名字所在的位置上。
例如,命令
setb99
seta[format{SantaClausis%syearsold}$b]
导致a有同前面段落中的简单命令相同的最终值。
变量替换不是严格必须的,因为有其他方式来达到相同的效果,但是它减少了键入。
最后一个语法构造是反斜杠字符,可以用它把特殊字符插入到参数中,比如花括号或非打印字符。
2.2.数据类型
在Tcl中只有一种数据类型:
字符串。
所有命令、到命令的参数、命令返回的结果和变量的值都是ASCII字符串。
Tcl始终使用字符串便于在Tcl库过程和包围它的应用的C代码之间来回传递信息。
这使它易于在不同类型的机器之间来回传递有关Tcl的信息。
尽管在Tcl中所有的东西都是字符串,很多命令都希望它们的字符串参数有特定的格式。
这里的字符串有三种特定的通用格式:
列表、表达式和命令。
列表只是包含用空白分隔的一个或多个字段的字符串,类似于命令。
可以使用花括来包围复杂的列表元素;这些复杂的列表元素自身经常也是列表,类似于Lisp。
例如,字符串
dogcat{horsecowmule}bear
是有四个元素的一个列表,其中第三个元素是有三个元素的列表。
Tcl提供一组列表操纵的命令,比如建立列表、提取元素、和计算列表长度。
字符串的第二种常见形式是数值表达式。
Tcl表达式同C中的表达式有着同样的操作符合优先级。
Tcl命令 expr把字符串作为表达式来求值并返回结果(当然是作为字符串)。
例如,命令
expr{($a<$b)||($c!
=0)}
在变量a小于变量b或者变量c是零的时候返回“1”,否则返回“0”。
一些其他的命令,比如if和for,期望它们的一个或多个参数是表达式。
字符串的第三种常见解释是命令(或命令的序列)。
这种形式的参数用在实现控制结构的Tcl命令中。
例如,考虑下列命令:
if{$a<$b}{
settmp$a
seta$b
setb$tmp
}
这里的if命令接受两个参数,每个都是用花括号界定的。
If是内置命令,它把它的第一个参数作为表达式来求值;如果结果非零,则if把它的第二个参数作为Tcl命令执行。
这个特定命令在变量a小于b的时候交换a和b的值。
Tcl还允许用户定义用Tcl语言写的命令过程。
我称谓这些过程为 tclproc,为的是区别于用 C写成的其他过程。
使用proc内置命令来建立tclproc。
例如,下面定义了一个递归的阶乘过程的Tcl命令:
procfacx{
if{$x==1}{return1}
return[expr{$x*[fac[expr$x-1]]}]
}
proc命令接受三个参数:
新tclproc的名字、一个变量名字的列表(在这个实例中试只有一个元素x的列表),和一个构成tclproc的过程体的Tcl命令。
一旦执行了这个proc命令,fac就可以同其他Tcl命令一样调用了。
例如
fac4
将返回字符串“24”。
控制
catch,error,info,time
杂项
exec
调用子进程
file,glob,print,source
文件操纵
format,scan,string
字符串操纵
expr
表达式
concat,index,length,list,range
列表操纵
global,proc,return,set
变量和过程
break,case,continue,eval,for,foreach,if
图表1。
内置的Tcl命令。
这是对使用Tcl的任何应用都能获得的一组命令。
应用可以定义额外的命令。
图表1分组列出了所有内置Tcl命令。
除了已经提及的命令,Tcl还提供了操纵字符串的命令(比较、匹配和类似printf/scanf的操作),操纵文件和文件名字的命令,和fork子进程并返回子进程的标准输出作为结果的命令。
内置Tcl命令提供了简单但完整的编程环境。
可以按三种方式扩展内置设施:
写tclproc;把其他程序作为子进程调用;或按下一节描述的那样用C过程定义新命令。
3.在应用中嵌入Tcl
尽管内置Tcl命令可以令人信服的用作独立的编程系统,Tcl实际上意图被嵌入到应用程序中。
我已经建造了使用Tcl的几个应用程序,其中之一是针对X的叫做 mx 的一个基于鼠标的编辑器。
在本文的余下部分,我将使用来自 mx 的例子来展示Tcl如何与包围它的应用进行交互。
使用Tcl的应用程序用同特定应用有关的一些额外的命令来扩展内置命令。
例如,时钟程序可以提供额外的命令来控制时钟如何显示和设置闹钟;mx 编辑器提供额外的命令来从磁盘读取文件,在窗口中显示它,选择和修改一定范围内的字节,和把修改后的文件写回磁盘。
应用程序员只需要写特定于应用的命令;内置命令“免费的”提供编程能力和扩展能力。
对于用户,特定于应用的命令表现的如同内置命令一样。
图表2.Tcl 库为 Tcl 语言提供一个分析器、一组内置命令、和一些实用工具过程。
应用提供特定于应用的命令加上收集要执行的命令的过程。
命令由 Tcl 分析,并接着传递给相关的(在 Tcl 中或者在应用中)命令过程去执行。
图表2展示了在Tcl和应用的余下部分之间的联系。
Tcl是连接到应用上的一个C库包。
Tcl库包含针对Tcl 语言的一个分析器、执行内置命令的过程、和做表达式求值和列表管理这种事情的一组实用工具过程。
分析器包括可以用来扩展语言的命令集的一个扩展接口。
要使用Tcl,应用首先要使用下面的库过程建立叫 interpreter 的一个对象:
Tcl_Interp*Tcl_CreateInterp()
解释器由一组命令、一组变量绑定和一个命令执行状态组成。
它是多数Tcl库过程操纵的基本单位。
简单应用将只使用一个单一的解释器,更复杂的应用可以为不同用途而使用多个解释器。
例如,mx 为在屏幕上的每个窗口使用一个解释器。
一旦应用建立了一个解释器,它就可以调用Tcl_CreateCommand过程来用特定于命令的过程来扩展这个解释器:
typedefint(*Tcl_CmdProc)(ClientDataclientData,
Tcl_Interp*interp,intargc,char*argv[]);
Tcl_CreateCommand(Tcl_Interp*interp,char*name,
Tcl_CmdProcproc,ClientDataclientData)
每次对Tcl_CreateCommand的调用都把一个特定的命令名字(name)、同一个实现这个命令的过程(proc)、和一个任意的传递到这个过程的一个单字值(clientData)关联起来。
在建立了特定于应用的命令之后,应用进入一个主循环,收集命令并把它们传递给Tcl_Eval过程去执行:
intTcl_Eval(Tcl_Interp*interp,char*cmd)
在最简单的形式中,应用可以简单的从终端或屏幕读取命令。
在 mx 编辑器中为事件如击键、鼠标按钮或菜单激活都关联上Tcl命令;每次事件发生的时候,就把对应的Tcl命令传递给Tcl_Eval。
Tcl_Eval过程把它的cmd参数分析成字段,在与这个解释器相关联的表格中查找命令的名字,并调用与这个命令相关联的命令过程。
所有的命令过程,不管内置的还是特定于应用的,都按照上面Tcl_CmdProc中typedef描述的方式,以同样的方式来调用。
向命令过程传递描述命令参数的一个字符串的数组(argc和argv),加上在建立的时候关联到命令上的clientData值。
ClientData典型的是到包含执行这个命令所需信息的特定于应用的结构一个指针。
例如,在 mx 中clientData参数指向一个每窗口数据结构,描述了编辑的文件和在其中显示的窗口。
控制机制如if和for是使用对Tcl_Eval的递归调用实现的。
例如,if命令的命令过程把它的第一个参数作为表达式求值;如果结果是非零,则它递归的调用Tcl_Eval来把它的第二个参数作为Tcl命令来执行。
在这个命令执行期间,可以再次递归调用Tcl_Eval,以此类推。
Tcl_Eval还递归的调用自身来执行出现在参数中的方括号包围的命令。
甚至tclproc如fac都使用了同样的基本机制。
在调用了proc命令建立fac的时候,proc命令过程通过调用Tcl_CreateCommand建立一个新命令,这在图表3中展示。
新命令拥有名字fac。
它的命令过程(对Tcl_CreateCommand调用中的proc)是叫做InterpProc的一个特殊的Tcl库过程,而它的clientData是到描述这个tclproc的结构的一个指针。
这个结构包含了,同其他东西在一起的,这个tclproc的过程体的一个复件(给proc命令的第三个参数)。
在调用fac命令的时候,Tcl_Eval调用InterpProc,它依次调用Tcl_Eval来执行tclproc的过程体。
需要有一些额外的代码来把fac命令的参数(它们在传递给InterpProc的argv数组中)关联到在fac过程体内使用的x变量上,和支持带有局部作用域的变量,但是用于tclprocs的机制多数与用于任何其他Tcl命令的机制相同。
图表3.tclproc(用Tcl写的过程)的建立和执行:
(a)调用了proc命令,就是说,要建立fac过程;(b)Tcl分析器调用与proc相关联的命令过程;(c)proc命令过程建立一个数据结构来持有是fac过程体的Tcl命令;(d)fac注册为一个新的Tcl命令,带有InterpProc作为它的命令过程;(e)fac作为Tcl命令而被调用;(f)Tcl分析器把InterpProc作为fac的命令过程来调用;(g)InterpProc从数据结构中取回fac的过程体;(h)把在fac过程体中的Tcl命令传递回到Tcl分析器去执行。
Tcl命令过程向Tcl_Eval返回两个结果:
一个整数返回代码和一个字符串。
返回代码是作为过程的结果返回的,而这个字符串存储在解释器中,以后可以从中取回它。
Tcl_Eval向它的调用者返回相同的代码和字符串。
表格I总结了返回代码和字符串。
正常的返回代码是TCL_OK和包含命令结果的字符串。
如果在执行命令中发生一个错误,则返回代码将是TCL_ERROR而字符串将描述错误状况。
在返回的TCL_ERROR的时候(或者任何不是TCL_OK的值),对于嵌套命令的通常的动作是向它们的调用者返回相同的代码和字符串,回退(unwind)所有未决的命令执行,直到返回代码和字符串最终从对Tcl_Eval的顶层调用返回。
在这个时刻应用通常向用户显示错误消息,通常在终端上打印它或在一个通知窗口中显示它。
返回代码
意义
字符串
TCL_OK
命令正常完成
结果
TCL_ERROR
在命令中出现了错误
错误消息
TCL_BREAK
应当从最内层循环中退出
没有
TCL_CONTINUE
应当跳过最内层迭代
没有
TCL_RETURN
应当从最内层过程中返回
过程结果
表格I. 每个Tcl命令都返回描述发生了什么的一个代码和提供补充信息的一个字符串。
如果返回代码不是TCL_OK,则回退(unwind)嵌套的命令并返回相同的代码,直到达到顶层或预备来处理异常返回代码的某个命令。
不是TCL_OK或TCL_ERROR的返回代码导致部分回退。
例如,break命令返回一个TCL_BREAK代码。
它导致回退嵌套的命令执行,直到达到一个嵌套的for或foreach命令。
在for或foreach命令递归的调用Tcl_Eval的时候,它特别的检查TCL_BREAK结果。
当这个代码出现的时候,for或foreach命令终止这个循环,但是它不向它的调用者返回TCL_BREAK。
它转而返回TCL_OK。
这样就不会有更高层次的执行被中止(abort)。
TCL_CONTINUE返回代码也由for和foreach命令处理(它们继续做下一个循环迭代),而TCL_RETURN由InterpProc过程处理。
只有一些命令过程,如break和for,知道关于特殊返回代码如TCL_BREAK的事情;其他命令过程在见到不是TCL_OK的返回代码的时候就是简单的中止。
可以使用catch命令来防止对TCL_ERROR返回的完全回退。
catch接受是要执行的Tcl命令的一个参数。
它把这个命令传递到Tcl_Eval去执行,但总是返回TCL_OK。
如果在这个命令中发生一个错误,catch的命令过程检测来自Tcl_Eval的TCL_ERROR返回值,在Tcl变量中保存有关错误的信息,并接着向它的调用者返回TCL_OK。
在几乎所有的情况下,我认为对一个错误最好的响应就是退出所有的命令调用并通知用户;catch是为那些少见的场合提供的,这里期望着一个错误并做出处理而不用中止。
4.Tcl和窗口应用
可嵌入的命令语言如Tcl在窗口环境中提供了特别的好处。
部分原因是在窗口环境中有很多交互式程序(所以有很多地方要使用命令语言),部分的原因是在今天的窗口环境中可配置性是重要的,并且语言如Tcl提供了做重新配置的灵活性。
Tcl在窗口应用中可以用于两个目的:
配置应用的界面动作,配置应用的界面外观。
在下面的段落中讨论这两个用途。
Tcl的第一个用法是用于界面动作。
理想的,对应用重要的每个事件都应当绑定上Tcl命令。
每次击键、每次鼠标移动或鼠标按钮按下(或释放)、和每个菜单条目都应当关联上Tcl命令。
当事件发生时,首先把它映射到它的Tcl命令上,接着通过把这个命令传递到Tcl_Eval来执行它。
应用不应当直接接收任何动作;所有动作都应当首先通过Tcl来传递。
进一步,应用应当提供Tcl命令允许用户改变与任何事件相关联的Tcl命令。
在交互式的窗口应用中,Tcl的使用可能对于初级用户是不可见的:
他们将使用按钮、菜单和其他界面构件来操纵应用。
但是,如果使用Tcl作为所有界面动作的中间媒介,则会产生两个好处。
首先,使得写Tcl程序来重新配置界面成为可能。
例如,用户将能够重新绑定击键、改变鼠标按钮、或把一个现存的操作替代为指定为一组Tcl命令或tclproc的更加复杂的操作。
第二个好处是这种方式强制所有的应用的功能都可通过Tcl来访问:
任何可以使用鼠标或键盘调用的东西都可以使用Tcl程序调用。
这使得有可能写模拟程序动作的tclproc,或把程序的基本动作组合到更加强力的动作中。
这还允许交互式会话作为一序列Tcl命令而被记录和重演(参见章节5)。