今后,Tcl将会继续开发开放源代码的和进行商业赢利的两种产品,在这种理念下,我们会不断的提升开放源代码的核心部件的性能,改进产品的不足,为免费的和商品化的两种开发工具提供资金支持,我们要做就要做到最好。Tcl社区将一如既往的在Tcl的发展中扮演重要的角色。作者: 611测试员 时间: 2007-7-9 23:36
TCL脚本数据文件格式 -by Koen Van Damme
简介
一个典型的tcl脚本把它的内部数据保存在列表和数组(tcl中两种主要的数据结构)中.比如,假定你想写一个能将数据先保存在磁盘上,然后再读取的tcl应用程序, 这将使你的用户可以先把一个项目保存下来,以后再重新装入.你需要一个办法,把数据从其内部存储处(列表与数组)写入到一个文件中,同样,也要有一个办法把数据从文件中读出装入到正在运行的脚本中去.
你可以选择把数据保存为二进制格式或文本格式.本文讨论的仅限文本格式,我们将考虑几种可能的数据格式及如何用tcl来进行分析.我们会特别介绍一些简单的技巧,使文本文件分析更容易.
本文假定你对tcl语言很熟悉,至少已经用tcl语言写过几个脚本.
▲一个简单的例子
假定你有一个简单的绘图工具,能把文本和长方形放到画布上.为了保存画好的图,你需要一个必须容易读取的文本格式的文件,最先想到而且最容易的文件是这样的:
example1/datafile.dat
rectangle 10 10 150 50 2 blue
rectangle 7 7 153 53 2 blue
text 80 30 "Simple Drawing Tool" c red
The first two lines of this file represent the data for two blue, horizontally stretched rectangles with a line thickness of 3. The final line places a piece of red text, anchored at the center (hence the "c"), in the middle of the two rectangles.
文件的前两行代表两个蓝色的水平展开的长方形,线条宽度是2(原文此处为3,可能是笔误,译者注).最后一行放了一段红色的文字,定位在中心(由"c"来指定)----在两个长方形的中间.
用文本文件保存你的数据使程序的调试更容易,因为你可以检查程序输出来保证一切都正常。同时也允许用户手工修改保存的数据(这样做可能好,也可能不好,取决于你的意图).
内建的TCL命令source读取文件,分析并执行文件中的命令.因为我们已经完成了d_rect和d_text过程,source命令将自动以正确的参数调用这两个过程.我们将d_rect和d_text称为分析过程.
我们无需再做任何分析,不用正则表达式,不用一行一行地循环,不用打开/关闭文件.只需调用source命令就完成了所有的工作。
数据文件已经成了可以执行的TCL脚本.因为它包含的是可执行命令,而不仅仅是被动的数据,所以称之为主动文件.主动文件在大多数脚本语言环境中均可正常运行,在Nat Pryce的主页上对其有详细的描述.
▲使用主动文件样本的优点:
无需再写一个分析程序,source调用TCL分析程序即可完成.
容易读取数据文件格式.
使用主动文件样本的缺点:
如果数据文件包含有危险命令,象l -a exec rm *,它们执行后会带来严重的后果.解决这个问题的办法是在安全模式下执行主动文件,防止危险命令。具体信息可参看TCL手册中"安全解释器"部分.
▲主动文件样本的局限
此样本不是对所有可能的数据格式都有效.数据格式必须是以行为基础的,每一行必须以一个关键字开头.用关键字开头写TCL过程,就把被动的关键字变成了主动的命令。这也意味着你不能使用象if或while之类的关键字,因为TCL不允许你用这样的名字来写过程.事实上,上面的例子中我把关键字改为d_text,就是因为开发工具包已经有了保留字text,该命令用来创建文本工具.
▲英语言过程
至此我们已经可以写一个简单的文件格式了:
d_rect 10 10 150 50 2 blue
d_rect 7 7 153 53 2 blue
d_text 80 30 "Simple Drawing Tool" c red
我们还有一个很简单的分析程序,就是两个分析过程和source命令.现在,我们看一下如何来进一步改进.
当你观察大量此类数据时,极易被数据搞糊涂.第一行包含10 10 110 50 3,你得有些这方面的经验才能很快明白前两个代表一个坐标,后两个是另一个坐标,最后一个是线宽.我们能用在数据中引入附加文本的方法来使一个程序员在阅读时较为容易.
example3/datafile.dat
d_rect from 10 10 to 150 50 thick 2 clr blue
d_rect from 7 7 to 153 53 thick 2 clr blue
d_text at 80 30 "Simple Drawing Tool" anchor c clr red
我们再一次看到,实现的过程并未改变.尽管这个解决方案只对最简单的数据格式有效,但它很清晰明了.它的优点有两个:选项在参数列表中的位置是固定的.比如,你不能把color(颜色属性)放在thickness(线宽属性)前面.对一个纯数据文件格式来说这个方法还不错(因为数值往往按相同的顺序存储),但当你想将其用于脚本中的手工输入数据时,这个方法则成了一个障碍.
选项没有默认值:你必须提供所有选项的值,而不能遗漏其中任何一个.
下面是一个可解决所有问题的实现过程.
example4/parser.tcl
proc d_rect {args} {
# First, specify some defaults
set a(-thickness) 1
set a(-color) blue
# Then, 'parse' the user-supplied options and values
array set a $args
与使用一个长长的参数表不同,分析过程现在仅有一个名为args的参数,由它来收集调用过程时所有的实际参数.参数x1,y1等消失了.他们现在由一个局部的数组来处理,稍后我们将圆心解释.
代码的第一部分为选项设定默认值,第二部分分析args中的选项/数值对.TCL内建的数组处理模块对此做得非常得心映手.它先在数组a中创建新的入口,使用选项名(包括前导横线"-")作为索引,选项值作为数组值.
如果用户在调用中不指定-color选项,a(-color)的入口默认值保持不变. 除用数组入口代替过程参数外,过程体中的最后一行与前面的实现一样.
如果用户调用时忘记指定选项-x1,则-x1的数组入口不会被设置(没有其默认值),创建矩形的调用就会引发一个错误.此例说明你可以给其中一些选项指定默认值,使其可随意选择,而另一些则不指定默认值,强制其必须由用户指定.
▲最好的格式通常是各种方法的结合
现在我们已经明白了TCL数据文件的常见方法(主动文件,英语言过程,选项/数值对),我们可以将其各自的优点组合进一个单独的数据格式中去.对强制性选项,我们使用固定位置参数时,多半与哑介词相结合增强可读性(见英语言过程).而所有的可随意选择的选项,宜用选项/数值对机制来进行处理,好让用户可以空着选项或在调用时改变其位置.最后,数据文件可能会是这样的:
d_rect from 10 10 to 150 50 -thickness 2
d_rect from 7 7 to 153 53 -thickness 2
d_text at 60 30 "Simple Drawing Tool" -anchor c -color red
假定所有项目的color属性的默认值都是"blue".
作为一个个人习惯,我通常会写这样的命令:
d_rect \
from 10 10 \
to 150 50 \
-thickness 2
d_rect \
from 7 7 \
to 153 53 \
-thickness 2
d_text \
at 80 30 "Simple Drawing Tool" \
-anchor c \
-color red
I find it slightly more readable, but that's all a matter of personal taste (or in my case lack of taste :-).
我觉得可读性要好一些,但这仅是一个个人偏好的问题.(or in my case lack of taste)(这句话是作者在调侃自己,但我不知如何把它译出来,请哪位大侠帮忙指点一下,译者注)
--------------------------------------------------------------------------------作者: 611测试员 时间: 2007-7-9 23:38
(go onsdlkfj5 )
Source SYST {
A system of patterns
Pattern-oriented software architecture
Buschmann, Meunier, Rohnert, Sommerlad, Stal
Wiley, 1996
0 471 95869 7
}
#下一步,我介绍一些类别,为了更容易找到样本,我想把样本进行分组.每个类别都
#有一个名称(如"存取控制")和一个简短的说明.
Category "Access control" {
How to let one object control the access to one or more
other objects.
}
Category "Distributed systems" {
Distributing computation over multiple processes, managing
communication between them.
}
Pattern "roxy" {
# This pattern fits in two categories:
Categories {"Access control" "Structural decomposition:bject"}
Level design
# Both these books talk about the Proxy pattern:
Sources {SYST:263 GOF:207}
Info {
Communicate with a representative rather than with the
actual object.
}
}
Pattern "Facade" {
Categories {"Access control" "Structural decomposition:bject"}
Sources {GOF:185}
Level design
Info {
Group sub-interfaces into a single interface.
}
}
# 下面是关键字"Source"的分析过程.
# 正如你所看到的,关键字后面跟有一个id号(是source的唯一标志符),
#还有source的说明文本.
proc Source {id info} {
# Remember that we saw this source.
global l_sources
lappend l_sources $curSource
# Remember the info of this source in a global array.
global a_sources
set a_sources($curSource,info) $info
}
# The parsing proc for the 'Category' keyword is similar.
proc Category {id info} {
global l_categories
lappend l_categories $curCategory
global a_categories
set a_categories($curCategory,info) $info
}
# This is the parsing proc for the 'Pattern' keyword.
# Since a 'Pattern' structure can contain sub-structures,
# we use 'uplevel' to recursively handle those.
proc Pattern {name args} {
global curPattern
set curPattern $name ; # This will be used in the sub-structures
# which are parsed next
global l_patterns
lappend l_patterns $curPattern
# We treat the final argument as a piece of TCL code.
# We execute that code in the caller's scope, to parse the elements
# of the structure.
# 'uplevel' will call 'Categories', 'Level' and other commands that
# handle the sub-structures.
# This is similar to how we use the 'source' command to parse the entire
# data file.
uplevel 1 [lindex $args end]
set curPattern ""
}
# The parsing proc for one of the sub-structures. It is called
# by 'uplevel' when the 'Pattern' keyword is handled.
proc Categories {categoryList} {
global curPattern ; # We access the global variable 'curPattern'
# to find out inside which structure we are.
global a_patterns
set a_patterns($curPattern,categories) $categoryList
}
# The following parsing procs are for the other sub-structures
# of the Pattern structure.
proc Level {level} {
global curPattern
global a_patterns
set a_patterns($curPattern,level) $level
}
proc Sources {sourceList} {
global curPattern
global a_patterns
# We store the codes such as 'SYST:99' in a global array.
# My implementation uses regular expressions to extract the source tag
# and the page number from such a code (not shown here).
set a_patterns($curPattern,sources) $sourceList
}
proc Info {info} {
global curPattern
global a_patterns
set a_patterns($curPattern,info) $info
}
猛一看,这个程序比我们在相对简单的绘图例子所做的要多很多.但考虑到这个方法的功能,只用几个分析过程并灵活运用命令"uplevel",我们同样可以分析包含有复杂结构,注释,嵌套子结构和自由格式文本数据的数据文件.设想一下如果我们从头写这样一个分析器会有多难.
数据由Source,Pattern或Info等过程进行解析.解析后的数据在内部存储在三个列表和三个数组中.数据的嵌套由调用uplevel来进行处理,用变量curPattern来记住我们当前所在的位置.
要注意的是这种方法需要你的数据能够理解TCL语法.这意味着大括号应该放在一行的最后,而不是下一行的开头.
▲递归结构
在仓库的样例中,Pattern类型的结构包含有其他类型的子结构如Info和Sources.那么当一个结构包含有相同类型的子结构时会如何呢?换句话说,我们如何处理递归结构?
例如,你要描述一个面向对象系统的设计,该设计由递归子系统实现.
example6/datafile.dat
# Description of an object-oriented video game
System VideoGame {
System Maze {
System Walls {
Object WallGenerator
Object TextureMapper
}
System Monsters {
Object FightingEngine
Object MonsterManipulator
}
}
System Scores {
Object ScoreKeeper
}
}作者: 611测试员 时间: 2007-7-9 23:41
为跟踪我们当前处于哪一个System系统结构中,看上去我们需要不只一个全局变量currPattern.在分析的任何时刻,我们都可能处在很多嵌套的System结构中,因此我们需要两个以上的变量.我们可能需要某种堆栈,在遇到System过程时压入一个值,在过程的结束时再弹出来.我们用一个TCL列表可以构造这样一个栈.
但若你不想维护一个栈的话,也可以不用它.这种方法也是基于一个非常简单的建议:当你需要使用一个栈时,看一下能否使用函数调用栈.处理递归数据时,我通常就用这个方法来实现我的分析过程的.
example6/parser.tcl
set currSystem ""
proc System {name args} {
# Instead of pushing the new system on the 'stack' of current systems,
# we remember it in a local variable, which ends up on TCL's
# function call stack.
global currSystem
set tmpSystem $currSystem
set currSystem $name ; # Thanks to this, all sub-structures called by
# 'uplevel' will know what the name of their
# immediate parent System is
# Store the system in an internal data structure
# (details not shown here)
puts "Storing system $currSystem"
# Execute the parsing procedures for the sub-systems
uplevel 1 [lindex $args end]
# Pop the system off the 'stack' again.
set currSystem $tmpSystem
}
proc Object {name} {
global currSystem
# Store the object in the internal data structure of the current
# system (details not shown here)
puts "System $currSystem contains object $name"
}
# Output preformatted text. This text must be surrounded by '<pre>' tags.
# Since it can recursively contain other tags such as '<em>' or hyperlinks,
# the procedure uses 'uplevel' on its final argument.
proc cgi_preformatted {args} {
cgi_put "<pre"
cgi_close_proc_push "cgi_puts </pre>"
# Output a single list bullet.
proc cgi_li {args} {
cgi_put <li
if {[llength $args] > 1} {
cgi_put "[cgi_lrange $args 0 [expr [llength $args]-2]]"
}
cgi_puts ">[lindex $args end]"
}
# Output a bullet list. It contains list bullets, represented
# by calls to 'cgi_li' above. Those calls are executed thanks
# to 'uplevel'.
proc cgi_bullet_list {args} {
cgi_put "<ul"
cgi_close_proc_push "cgi_puts </ul>"
我不想对这个庞大的库的细节进行详细的解释,你可以自己从Don的主页上下载后看一下.
--------------------------------------------------------------------------------
作为另一个例子,我的TODL工具使用类和方法等解析过程对面向对象的设计加以分析.下面是我的工具中一个输入文件的例子:
# Todl schema for module 'shapes'. It describes classes for some
# geometrical shapes such as rectangles and squares.
odl_module shapes {
#######
# Classes
# Base class for all shapes.
class shape {} {
attr id 0 ; # Attribute 'id' is inherited by all shapes
# and has default value 0.
}
# Rectangle with a width and height.
# Inherits from 'shape'.
class rect {shape} {
attr w 10
attr h 10
# Some methods to calculate properties for the shape,
# and to draw it on the screen.
method "" perimeter {}
method "" area {}
method "" draw { x {y 0} }
}
class square {shape} {
... (details similar to 'rect')
}
#######
# Module parameters
# All classes automatically get a 'print' method.
param all { print }
# Name of the 'delete' proc.
param delete_name delete
class myListElt: public CListElt, private FString {
This is a documentation string for the class 'myListElt'.
You can see multiple inheritance here.
} {
public:
method int GetLength(void) {
This is a documentation string
Returns the total length of the FString.
} {
// This is the final argument of the 'method' parsing proc.
// It contains freeform text, so this is where I can write
// pure C++ code, including the comment you are now reading.
return myLength;
}
private:
method virtual void privateMethod(short int p1, short int p2) {
Note that just saying "short" is not enough:
you have to say "short int".
} {
printf("Boo! p1=%d, p2=%d\n", p1, p2);
}
}
data short int b {This is just a counter}
data void* somePointer {to store the end-of-list or something like that}
method void error(short int errNo, char* message) {
This is a global library procedure, which
reports an error message.
} {
cout << "Hey, there was an error (" << errNo << ") " << message << endl;
}
cpp_report
这个例子可能有些牵强,但它显示了主动文件样本的强大功能.你看到的是TCL代码,但它看上去象是C++代码,它能自动产生文档,类图,编程参考,当然还有可编译的C++代码.
解析过程如方法和类把C++实现存储在内部的TCL数据结构中,最后,调用cpp_report产生最终的C++代码.
下面的来自分析器的程序片段说明你可以使TCL分析器去读取与C++语法类似的文件.
# Parsing proc for 'class' keyword.
# Arguments:
# - class name
# - list of inheritance specifications, optional
# - comment block
# - body block
proc class {args} {
global _cpp
# split names from signs like : , *
set cargs [expand [lrange $args 0 [expr [llength $args] - 3]]]
# -3 to avoid the comment block and the class body.
# First process the name
set className [lindex $cargs 0]
if { $_cpp(CL) == "" } {
set _cpp(CL) $className ; # This is like 'currPattern' in the
# pattern repository example
} else {
error "Class definition for $className: we are already inside class $_cpp(CL)"
}
# Then process the inheritance arguments.
# Obvisouly, this is already a lot more complicated than in the
# previous examples.
set inhr
set mode beforeColon
set restArgs [lrange $cargs 1 end]
foreach arg $restArgs {
if { $arg == ":" } {
if { $mode != "beforeColon" } {
error "Misplaced \":\" in declaration \"class $className $restArgs\""
}
set mode afterColon
} elseif { $arg == "public" || $arg == "private" } {
if { $mode != "afterColon" } {
error "Misplaced \"$arg\" in declaration \"class $className $restArgs\""
}
set mode $arg
} elseif { $arg == "," } {
if { $mode != "afterInherit" } {
error "Misplaced \",\" in declaration \"class $className $restArgs\""
}
set mode afterColon
} else {
if { $mode != "public" && $mode != "private" } {
error "Misplaced \"$arg\" in declaration \"class $className $restArgs\""
}
if { ![IsID $arg] } {
warning "$arg is not a valid C++ identifier..."
}
lappend inhr [list $mode $arg]
set mode afterInherit
}
}
if { $mode != "afterInherit" && $mode != "beforeColon" } {
error "Missing something at end of declaration \"class $className $restArgs\""
}
set _cpp(CLih) $inhr
set _cpp(CLac) "private"
# First execute the comment block
uplevel 1 [list syn_cpp_docClass [lindex $args [expr [llength $args] - 2]]]
# Then execute the body
uplevel 1 [list syn_cpp_bodyClass [lindex $args end]]
set _cpp(CL) ""
set _cpp(CLac) ""
set _cpp(CLih) ""
}
--------------------------------------------------------------------------------
关于懒惰
按Larry Wall的话,一个好的程序员的最重要的潜质就是懒惰.也就是说,有创造性的懒惰.本文提到了两个建议,他们能够归于一件事:懒惰.
当你需要一个解析器时,使用一个现成的解析器,修改你的文件格式去造就分析器的要求(当然,需要你已经达到了能够自由选择文件格式的境界)
当你需要使用堆栈时,你可以使用现成的函数调用堆栈,忘掉压入,弹出和其他的操作.
"重用"并不仅表示封装和信息的隐藏.有些时候它只不过表示懒惰罢了.
(完)