文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>es:带有高阶函数的shell(P.Haahr,B.Rakitzis著,中..

es:带有高阶函数的shell(P.Haahr,B.Rakitzis著,中..

时间:2006-06-01  来源:mhss

 

es: 带有高阶函数的 shell

Paul Haahr
Adobe Systems Incorporated [勘误1]
Byron Rakitzis
Network Appliance Corporation

翻译: 寒蝉退士

译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。
原文:http://www.webcom.com/~haahr/es/es-usenix-winter93.html

摘要

在 1990 年秋天,我们中的一个(Rakitzis)重新实现了 Plan 9 命令解释器 rc,并用做了 UNIX shell。对它的实验导致我们设想,是否有设计 shell 的更一般性的方式,本文将描述这次实验的结果。我们把来自现代函数式编程语言如 Scheme 和 ML 的概念应用于 shell,shell 典型的更加关注 UNIX 特征而不是语言设计。我们的 shell 同时是简单的和高度可编程的。通过暴露很多内情和接受来自函数式编程语言的构造,我们建立了支持新编程典范的新 shell。

注意: 本网页是在 1993 年冬天 San Diego 举办的 Usenix Conference 上的论文的 HTML版本。本文对应于一个过期了的 es 发行;对本文有影响的变更请参见勘误章节。从匿名 FTP 上能获得当前版本的 es 源代码和本文的 PostScript 版本。

目录

  • 介绍
  • 使用 es 命令
  • 函数
  • 变量
  • 绑定
  • Settor 变量
  • 返回值
  • 例外
  • 欺伪
  • 实现
  • 初始化
  • 环境
  • 同 UNIX 交互
  • 垃圾收集
  • 将来的工作
  • 结论
  • 致谢
  • 脚注
  • 勘误
  • 引用
  • 作者信息

尽管多数用户认为 shell 是交互式命令解释器,它实际上是每个语句都作为命令运行的编程语言。因为它必须同时满足命令执行的交互式和编程的外貌,它成型于历史上的和设计上的因素。

-- Brian Kernighan & Rob Pike[1]

介绍

shell 同时是一门编程语言和交互环境的核心。大多数当前 shell 的祖先是第 7 版的 Bourne shell[2],它被特征化为简单的语义,最小化的交互式特征集合,和援引 Algol 的语法。一个新近的 shell 是 rc[3],它替换上了更清晰的语法,而保持了大多数 Bourne shell 的性质。但是,shell 的大多数新近开发(比如 csh、ksh、zsh)都集中于增进交互环境,而不是改变底层语言的结构 -- 现已证实了 shell 抗拒在编程语言上的创新。

rc 是向 Bourne shell 语义增加现代语法的实验,而 es 是组合受 rc 影响的语法的对新语义的探索: es 有词法作用域变量,一级(first-class)函数,和例外(也译为异常)机制,这借鉴了现代编程语言如 Scheme 和 ML[4, 5]的概念。

在 es 中,大多数标准 shell 构造(比如管道和重定向)被转换成统一的表示法: 函数调用。实现这些构造的基本函数可以用同所有其他函数一样的方式来操纵: 调用、替代、作为参数传递给另一个函数。在 es 中,替代基本函数的能力是它的可扩展性的关键;例如,一个用户可以把管道的定义取代(override)为导致远程执行,或取代路径查找机制来实现路径查找缓存。

在表面层次上,es 看起来像大多数 UNIX shell 一样。管道、重定向、后台作业等的语法同 Bourne shell 是完全一样的。而 es 的编程构造是新的,借鉴了 rc 和 Tcl[6]。

es 是可自由重新发行的,并可以通过 ftp.white.toronto.edu 的匿名 ftp 服务器获得。

使用 es 命令

对于简单的命令,es 雷同于其他 shell。例如,换行通常充当命令终止符。这些类似的命令都可以在 es 中工作:

 cd /tmp rm Ex* ps aux | grep '^byron' | awk '{print $2}' | xargs kill -9 
对于简单使用,es 紧密的追随于 rc。所以读者可以参照关于 rc 的论文来获得关于引用规则、重定向等等的讨论。(这里展示的例子将作为 shell 语法的最小公分母,所以理解 rc 不是理解本文的先决条件)。

函数

es 可以通过使用 shell 函数来编程。下面是以 yy-mm-dd 格式打印日期的简单的函数:
 fn d { date +%y-%m-%d } 
函数调用还可以带有参数(argument)。es 允许对函数指定形参(parameter),通过把它们放置于函数名字和左花括号之间。下面的函数接受命令 cmd 和参数 args 并把命令依次应用于每个参数:
 fn apply cmd args { for (i = $args) $cmd $i } 
例如: [脚注1]
 es> apply echo testing 1.. 2.. 3.. testing 1.. 2.. 3.. 
注意 apply 调用带有多于两个参数;es 一对一的赋值参数到形参,而任何剩余的参数都被赋值给最后的形参。例如:
 es> fn rev3 a b c { echo $c $b $a } es> rev3 1 2 3 4 5 3 4 5 2 1 
如果参数少于形参,es 使剩余的形参为空:
 es> rev3 1 1 
迄今为止我们只见到了简单的字符串作为参数传递。但是,es 函数可以接受程序片段(包围在花括号中)作为参数。例如,上面定义的 apply 函数可以使用在命令行上直接键入的程序片段:
 es> apply @ i {cd $i; rm -f *} /tmp /usr/tmp 
这个命令包含了很多需要理解的东西,所以我们慢一点来解说。

在任何其他 shell 中,这个命令通常可以被分解为两个独立的命令:

 es> fn cd-rm i { cd $i rm -f * } es> apply cd-rm /tmp /usr/tmp 
所以,构造
 @ i {cd $i; rm -f *} 
只是在命令行上内嵌一个函数的一种方式。这叫做一个 lambda。[脚注2] 它采用如下形式

@ 形参 { 命令 }

在效果上,lambda 是"等待发生"的过程。例如,可能直接在 shell 键入:

 es> @ i {cd $i; rm -f *} /tmp 
它直接在参数 /tmp 上运行这个内嵌函数。

有些事情要注意: 提供给 apply 的内嵌函数有一个形参叫做 i,而 apply 函数自身使用了到叫做 i 的一个变量的引用。注意这两个使用不会冲突: 因为 es 函数的形参是词法作用域的,类似于 C 和 Scheme 中的变量。

变量

在 shell 函数和 lambda 之间的类似不是偶然的。事实上,函数定义就被写为把 lambda 赋值到 shell 变量。所以下面两个 es 命令是完全等价的:

 fn echon args {echo -n $args} fn-echon = @ args {echo -n $args} 
为不与常规变量相冲突,函数变量在名字前面有前缀 fn-。这个机制还在执行的时候使用;当 es 见到像 apply 这样的名字的时候,它首先在它的符号表中查找名字是 fn-apply 的变量。当然,总是可能通过用 $ (美元号)明确的去引用来执行任何变量的内容:
 es> silly-command = {echo hi} es> $silly-command hi 
前面的例子还展示了变量可以被设置为包含程序片段,同简单字符串是一样的。实际上,这两者可以混合:
 es> mixed = {ls} hello, {wc} world es> echo $mixed(2) $mixed(4) hello, world es> $mixed(1) | $mixed(3) 61 61 478 
变量可以持有命令的列表,甚至是 lambda 的列表。这使得变量成为万能的工具。例如,变量可以被用做函数分派(dispatch)表。

绑定

在关于函数的章节中,我们提及了函数形参是词法作用域的。还可以直接使用词法作用域的变量。例如,为了避免妨碍 i 的全局实例,可以使用下列的范围语法:
 let (var = value) { 使用 $var 的命令 } 
词法绑定在 shell 函数中是有用的,它对于不想相互破坏其他函数的变量的 shell 函数是很重要的。

es 代码片段,不管是用做给命令的参数、还是存储在变量中,捕获包围在词法作用域内的变量的值。例如,

 es> let (h=hello; w=world) { hi = { echo $h, $w } } es> $hi hello, world 
在重定义函数时会使用词法绑定。新函数可以在词法作用域内的变量中存储以前的定义,所以它是新函数唯一的变量。这个特征可以用来定义跟踪对另一个函数的调用的函数:
 fn trace functions { for (func = $functions) let (old = $(fn-$func)) fn $func args { echo calling $func $args $old $args } } 
这个 trace 函数重定义了在它的命令行上指名的所有函数,它们被套入打印这个函数名字和参数并接着调用以前定义的函数内,它们以前的定义被捕获在词法绑定变量 old 中。考虑一个递归函数 echo-nl,它一行一个的打印它的参数:
 es> fn echo-nl head tail { if {!~ $#head 0} { echo $head echo-nl $tail } } es> echo-nl a b c a b c 
应用 trace 到这个函数生成:
 es> trace echo-nl es> echo-nl a b c calling echo-nl a b c a calling echo-nl b c b calling echo-nl c c calling echo-nl 
读者需要注意

! 命令

是 es 的“非”命令,它反转 cmd 的返回值的意义,而

~ 主体 模式

针对模式匹配主体,并返回真,如果主体合于模式的话。(事实上,对于包含着通配符的模式,匹配是有些复杂性的)。

shell 比如 Bourne shell 和 rc 支持某种形式的局部赋值,叫做动态绑定。它典型 shell 语法是:

变量=值 命令

这种表示法同 es 的赋值语法冲突(在这里零个或多个字被赋值到一个变量),所以动态绑定有如下语法:

 local (var = value) { 使用 $var 的命令 } 
下面的例子可以看出两种绑定形式的区别:
 es> x = foo es> let (x = bar) { echo $x fn lexical { echo $x } } bar es> lexical bar es> local (x = baz) { echo $x fn dynamic { echo $x } } baz es> dynamic foo 

Settor 变量

除了前面描述的用于函数执行的前缀(fn-),es 还使用另一种前缀来查找 settor 变量。settor 变量 set-foo 是在每次变量 foo 变更值的时候被求值的变量。使用 settor 变量的好例子是 watch 函数:
 fn watch vars { for (var = $vars) { set-$var = @ { echo old $var '=' $$var echo new $var '=' $* return $* } } } 
Watch 为它的每个形参建立一个 settor 函数;这个 settor 打印这个变量的旧值和要设置的新值,比如:
 es> watch x es> x=foo bar old x = new x = foo bar es> x=fubar old x = foo bar new x = fubar 

返回值

UNIX 程序退出时带有单一的在 0 和 255 之间的一个数,它报告程序的状态。es 把退出状态的概念替代为“丰富”返回值。es 函数返回的不只是一个数,而是任何一个对象: 字符串,程序片段,lambda,或混合这些种类的值的列表。

通过对一个命令前导 <> 来返回它的返回值: [勘误2]

 es> fn hello-world { return 'hello, world' } es> echo <>{hello-world} hello, world 
这个例子展示了被用来实现层次列表的丰富返回值:
 fn cons a d { return @ f { $f $a $d } } fn car p { $p @ a d { return $a } } fn cdr p { $p @ a d { return $d } } 
第一个函数 cons 返回一个函数,它接受另一个函数作为它的参数,并在形参 a 和 d 上运行它。car 和 cdr 都调用 cons 返回的某种函数,作为参数提供的函数分别返回第一个或第二个形参。例如:
 es> echo <>{car <>{cdr <>{ cons 1 <>{cons 2 <>{cons 3 nil}} }}} 2 

例外

除了传统的控制流构造 -- 循环、条件和子例程 -- es 还有用来实现非结构化的控制流的例外机制。内置函数 throw 发起一个例外,它典型的由命名这个例外的字符串、和特定于这个指名例外类型的其他参数构成。例如,例外 error 被缺省解释器循环所捕获,它把余下参数作为错误消息来处理。[勘误3] 所以:
 es> fn in dir cmd { if {~ $#dir 0} { throw error 'usage: in dir cmd' } fork # 在 subshell 中运行 [勘误4] cd $dir $cmd } es> in usage: in dir cmd es> in /tmp ls webster.socket yacc.312 
通过提供捕获 error 例外的一个例程,程序员可以在这个消息被打印之前解释内部 shell 错误。

例外也被用于实现 break 和 return 控制流构造,并提供了用户代码和 UNIX 信号交互的一种方式。尽管有六个错误类型对于解释器是已知的并有特殊意义,参数的任何任何集合都可以被传递给 throw。

例外由内置的 catch 来捕获,它典型的采用如下形式

 catch @ e args { handler } { body } 
Catch 首先执行 body;如果没有引发例外,catch 简单的返回,直接传递 body 的返回值。在另一方面,如果 body 的调用的任何东西发出一个例外,则运行 handler,带有 e 被绑定到导致的这个问题的例外之上。例如,上面的 in 的最后两行可以被替代为:
 catch @ e msg { if {~ $e error} { echo >[1=2] in $dir: $msg } { throw $e $msg } } { cd $dir $cmd } 
来更好的告之用户错误来自在什么地方:
 es> in /temp ls in /temp: chdir /temp: No such file or directory 

欺伪

es 的万能函数和变量只是故事的一半;另一半是 es 的 shell 语法只是对内置函数调用的一个前端。例如:
 ls > /tmp/foo 
执行前在内部被写为
 %create 1 /tmp/foo {ls} 
%create 是在文件描述符 1 上打开 /tmp/foo 并运行 ls 的函数。

这种重写的价值在于 %create 函数(和任何其他 shell 服务)是可以被欺伪的,就是说,被用户定义函数所取代: 当定义了一个新的 %create 函数的时候,缺省的重定向动作就被取代了。

进一步,%create 不是内置的文件重定向服务。它是到自身不能被取代的基本 $&create 的一个挂钩。这意味着访问底层 shell 服务总是可能的,即使是在它的挂钩被重新指派的时候。

在心里记住,下面代码是我们对已经讨论了的重定向运算符的欺伪。这个欺伪是简单的: 如果要建立的文件已经存在(通过运行 test -f 确定),则这个命令不运行,类似于 C-shell 的 "noclobber" 选项:

 fn %create fd file cmd { if {test -f $file} { throw error $file exists } { $&create $fd $file $cmd } } 
事实上,多数重定向不明确的提及 $&-形式,而是通过词法作用域捕获到它们的引用。所以,上面的重定向通常会表示为
 let (create = $fn-%create) fn %create fd file cmd { if {test -f $file} { throw error $file exists } { $create $fd $file $cmd } } 
后者形式更好些,因为它允许一个函数的多个重定义;前者版本总是丢弃以前的定义。

取代传统 shell 内置命令是欺伪的另一个例子。例如,(通过假想的命令 title)在窗口标题栏中放置当前目录的 cd 操作可以写为:

 let (cd = $fn-%cd) fn cd { $cd $* title `{pwd} } 
欺伪也可以用于其他 shell 不能完成的任务;一个例子是通过欺伪 %pipe 来计时管道的每个成员,图表 1 [勘误5] 展示了 Jon Bentley[7] 建议的管道剖析器。

 

 es> let (pipe = $fn-%pipe) { fn %pipe first out in rest { if {~ $#out 0} { time $first } { $pipe {time $first} $out $in {%pipe $rest} } } } es> cat paper9 | tr -cs a-zA-Z0-9 '\012' | sort | uniq -c | sort -nr | sed 6q 213 the 150 a 120 to 115 of 109 is 96 and 2r 0.3u 0.2s cat paper9 2r 0.3u 0.2s tr -cs a-zA-Z0-9 \012 2r 0.5u 0.2s sort 2r 0.4u 0.2s uniq -c 3r 0.2u 0.1s sed 6q 3r 0.6u 0.2s sort -nr 

图表 1: 计时管道成员

 

很多 shell 提供了缓存在用户的 $PATH 中找到的可执行文件的全路径名的机制。es 不在 shell 中提供这种功能,它可以让需要的用户轻易的增加这个功能。调用函数 %pathsearch (参见图表 2)来查找被用做命令的非绝对文件名。

 

 let (search = $fn-%pathsearch) { fn %pathsearch prog { let (file = <>{$search $prog}) { if {~ $#file 1 && ~ $file /*} { path-cache = $path-cache $prog fn-$prog = $file } return $file } } } fn recache { for (i = $path-cache) fn-$i = path-cache = } 

图表 2: 路径缓存

 

es 的另一个可以被替代的部分是解释器循环。事实上,缺省解释器自身是用 es 写的;参见图表 3。

 

 fn %interactive-loop { let (result = 0) { catch @ e msg { if {~ $e eof} { return $result } {~ $e error} { echo >[1=2] $msg } { echo >[1=2] uncaught exception: $e $msg } throw retry } { while {} { %prompt let (cmd = <>{%parse $prompt}) { result = <>{$cmd} } } } } } 

图表 3: 缺省交互循环

 

这个例子的一些细节需要进一步解释。例外 retry 由 catch 在一个例外处理器运行的时候解释,并导致 catch 例程的主体被再次运行。%parse 向标准错误文件打印它的参数,从命令输入的当前来源读一个命令(潜在的多于一行长),在输入来源被耗尽的时候发起 eof 例外。提供挂钩 %prompt 给用户去重定义,缺省不做什么事情。

要么被提议了要么已经被使用了的其他欺伪包括: 在目录不存在时问用户是否建立它的 cd 的版本;尝试正确拼写未找到文件的重定向和程序执行的版本;在(不同的)远程机器上运行管道成员的 %pipe,用来获得并行执行;shell 函数的自动装载;和替代用户波浪符展开的函数,用来支持主目录的替代定义。此外,为了调试的目的,你可以在挂钩函数上使用 trace。

实现

es 是用大约 8000 行 C 代码实现的。尽管我们估计有大约 1000 行用于在各种版本 UNIX 之间可移植性的问题,还有一些 es 必须做的与 UNIX 相结合的外围工作。path 变量是个好例子。

es 约定的路径查找、涉及在叫做 path 的变量的列表元素之上的查找。好处是可以同其他变量一样把所有常规的列表运算应用于 path。但是,UNIX 程序希望路径是存在 PATH 中的分号分割的列表。所以 es 必须维护每个变量的一个复本,并在其中一个中有变更的时候反映为另一个中的变更。

初始化

多数 es 的初始化实际上是通过叫做 initial.es 的 es 脚本完成的,在编译时间它被一个 shell 脚本转换成 C 字符串并被内部存储。这个脚本展示了 es 分析器的初始动作,还有上面提及的 path/PATH 别名特征是如何设置的。[勘误6]

脚本的很多部分由下列式样的行构成:

 fn-%and = $&and fn-%append = $&append fn-%background = $&background 
它们把 shell 服务如短路与、后台等等绑定到 %-前缀变量。

还有一组赋值把内置 shell 函数绑定到它们的挂钩变量:

 fn-. = $&dot fn-break = $&break fn-catch = $&catch 
不同之处在于给它们的是用户直接调用的名字;"." 是 Bourne-兼容的包含一个文件的命令。

最后,定义了一些 settor 函数来与 UNIX 路径查找(和其他)约定一起工作。例如,

 set-path = @ { local (set-PATH = ) PATH = <>{%flatten : $*} return $* } set-PATH = @ { local (set-path = ) path = <>{%fsplit : $*} return $* } 
关于实现的注解: 这些函数在赋值它们的对当(opposite-case) settor 变量之前,临时的赋值它们的对当 settor 函数为空。这避免了在两个 settor 函数之间无限递归。

环境

UNIX shell 典型的维护一个变量定义表,在子进程被建立的时候要传递给它。这个表被宽松的称为环境或环境变量。尽管在传统上环境已经只被用来传递变量的值,在 es 中函数和变量的二元性使得传递函数定义到 subshell 成为可能。(尽管 rc 也提供这种功能,没有"环境函数"的独立空间的限制导致了它是很杂牌的。)

有函数在环境中,把它们带入了同变量一样的概念框架中 -- 在环境中创建、删除、存在等等都服从同一的规则。此外,在环境中的函数对文件 I/O 和分析时间的优化。因为几乎所有 shell 声明(state)现在都可以在环境中编码,es 的新实例比如开始于 xterm (1)的,去运行一个配置文件变得多余了。所以 shell 启动变得非常快速。

作为对这种环境的支持的结果,es 必须专注于"未分析的"函数定义,因为它们可以作为环境字符串传递。这有些复杂,因为函数定义的词法环境必须保持为未分析的。最好用例子来展示:

 es> let (a=b) fn foo {echo $a} 
它在这个函数定义的词法范围内把 b 绑定到变量 a。所以,这个函数的外部表示必须暴露这个信息。它被编码为:
 es> whatis foo %closure(a=b)@ * {echo $a} 
(为了同其他 shell 在文化上兼容,没有指名参数的函数使用 "*" 来绑定参数)。

同 UNIX 交互

不像多数传统 shell,它们有着 UNIX 系统调用接口所指示的特征集合,es 包含着不能同 UNIX 自身完好交互的特征。例如,丰富返回值对于 shell 函数有意义(它在 shell 自身内部运行),却不能从 shell 脚本或其他外部程序返回,因为 exit/wait 接口只支持传递小整数。这强迫我们把一些东西建造在 shell 内部,而其他 shell 可能把它们建造于外部。

例外机制也有类似的问题。当 shell 函数引发一个例外的时候,它按预期传播;如果引发自 subshell,它就不能按人们预期的那样传播了,在从 subshell 退出的时候打印一个消息,并返回一个失败退出状态。我们认为这是不幸的,但是好象没有合理的方式把例外传播结合到现存的 UNIX 机制上。特别是,信号机制就不适合这个任务。事实上,信号使 shell 内的控制流足够复杂了,并在整个 shell 中导致了足够特殊的情况,所以它是一个麻烦而不是优点。

我们把 es 塞入 UNIX 的另一个不幸的结论是在词法作用域变量、环境和 subshell 之间的交互。例如,两个函数可以定义在同一个词法作用域中。如果其中一个修改了词法作用域变量,这个改变将影响另一个函数所看到的这个变量。在另一个方面,如果这个函数运行在 subshell 中,在它们之间的词法作用域连接就丢失了,这是它们被导出到分离的环境字符串中的结果。这不是个严重的问题,但对于有函数式编程语言背景的程序员而这是不符合直觉的。

关于 es 的另一个限制是它必须在列表是没有层次的传统 UNIX 环境中工作;就是说,列表不能包含列表作为元素。为了能够传递给外部程序的列表、同传递给 shell 函数的列表有相同的语义,我们必须限制列表同 exec-风格的参数向量有相同的结构。所以所有列表同在 rc 和 csh 一样是平坦的。

垃圾收集

因为 es 结合了真正的 lambda 演算,它包含了要么直接的要么间接的建立递归结构,就是说包含指向它们自身的指针的对象的能力。尽管这个特征对于程序员是有用的,它有一个不幸的结果是在 es 中的内存管理比其他 shell 要复杂的多。简单的内存回收策略比如舞台方式分配[8]或引用计数都是不够的;需要完整的垃圾收集系统来填补所有的内存泄露。

基于我们对 rc 的内存使用的经验,我们决定复制垃圾收集器对于 es 是适合的。导致这个结论的观察是: (1) 在两个独立的命令之间保留小内存(粗略的对应于环境变量的存储);(2)命令执行可以在短时间内消耗大量的内存,特别是在涉及循环的时候;(3) 尽管占用了很多内存,shell 的工作集典型的比可用的物理内存要小。所以,我们选取侧重相对快速的收集时间,而在内存使用上有些浪费的策略。尽管对我们选取的复制收集器所提出的理由而言,通用的垃圾收集器也是合理的,我们决定避免增加切换到通用模型所蕴涵的复杂性。

在 shell 的正常执行期间,通过增加指向一个预先分配块的指针来获得内存。当这个块被耗尽的时候,检查在垃圾收集器内存 rootset 中的所有活跃指针。把它们指向的任何结构都复制到一个新块中。在 rootset 已经被扫描完的时候,所有新鲜的复制数据也做类似的扫描,这个过程一直重复,直到所有可到达的数据都被复制到这个新块中。在这一点上,触发收集器的内存需要应当能够获得成功,如果不能,分配一个更大的块并重做收集。

在 shell 执行的部分期间, -- 特别是在 yacc 分析器运行期间 -- 不能标识所有的 rootset,所以垃圾收集器被停用。如果在此期间有一个分配请求,在舞台中没有足够的内存可用,夺取一大块内存使分配可以继续下去。

垃圾收集器已经发展出了难于调试的声名。收集例程自身典型的不是困难的来源。甚至比 es 的算法更复杂的算法也只是几百行代码。而最常见形式的 GC bug 是不能够标识所有 rootset 的元素,因为这是一个非常无限度的问题,它蕴涵了几乎所有例程。为了找到这种形式的 bug,我们使用了有两个关键特征的修改版本的垃圾收集器: (1)在收集器没有被停用的时候,于每次分配时发起收集,(2)在收集完成之后,停用对旧区域的所有内存的访问。[脚注3] 所以,到对垃圾收集器空间内指针的任何引用,都可能被收集使其无效,从而导致内存保护故障。我们向实现复制垃圾收集器的人强烈建议这种技术。

垃圾收集器有两个性能牵连;首先是在 shell 正在运行的时候,当收集器被调用的时候所有动作都必须停止。这粗略的占了 4% 的 shell 运行时间。更加严重的是在有任何潜在分配的时候,要么收集器必须停用,要么到在垃圾收集器内的结构的所有指针必须被标识,有效的要求它们在内存中已知地址中,这挫败了来自现代体系的良好性能所需要的注册优化。量化这种限制的性能结果是困难的。

垃圾收集器由大约 250 行收集器自身的代码(加上其他 300 行调试代码)组成,还有标识变量为是 rootset 的一部分的一些声明,和分配、复制和扫描从收集器空间分配的结构类型的小(典型 5 行)过程。

将来的工作

在 es 中你可能希望在很多地方能够重定义内置行为却不存在这种挂钩。其中最显著的是通配符展开,它表现的如同传统 shell。我们希望在将来的版本中暴露 es 的某些剩余部分。

es 最不太令人满意的部分是它的分析器。我们已经谈论在核心语言和完整语言之间的差别;实际上,到核心语言特征的语法糖衣变换(就是说,向用户提供便利的 UNIX shell 语法)在同识别核心语言的同一个 yacc 生成的分析器中完成。不幸的是,这把完整语言同核心捆绑的太紧密了,为用户扩展 shell 的语法提供了很小的空间。

我们可以想象一种系统,分析器只识别核心语言,和一组暴露的变换规则,它们把使 es 感觉起来象是一个 shell 的扩展语法映射到核心语言。Scheme 的扩展语法系统[9]提供如何设计这种机制的好例子,象多数其他为 Lisp-式样语言设计宏系统一样,它不吻合 UNIX shell 所发展出来的自由形式的语法。

es 的当前实现有一个不好的性质,就是所有函数调用都导致 C 栈嵌套。特别是,尾部调用消耗了栈空间,有时这是可以被优化掉的。所以,正确的尾部递归函数,比如上面的 echo-nl,Scheme 或 ML 程序员可能希望它等价于循环,有隐藏的代价。这个实现上的难题我们希望以后解决。

es 除了对 shell 编程是好语言之外,也是用做可嵌入的"脚本"语言的好的候选者,同 Tcl 位于同一个行列中。事实上,es 从 Tcl 借鉴了很多 -- 最显著的是把成块的代码作为未分析的字符串来传递 -- 因为对两种语言的要求是类似的,语法上的类似就没有什么可奇怪的了。es 有两个超出大多数嵌入式语言的优点: (1) 同样的代码可以用于 shell 或其他程序,并且很多函数可以是同一的;(2) 它支持各种编程构造,比如闭包和例外。我们目前正在制作可以单独作为 shell 或连接入其他程序内的 es 的库版本,可以带有或不带有 shell 特征比如通配符展开或管道。

结论

在 es.背后有两个中心想法。首先,通过暴露内部给用户操纵可以使一个系统有更好的可编程性。通过允许欺伪那些迄今为止不可更改的 shell 特征,es 给予它的用户在裁剪他们的编程环境上的巨大灵活性,而早先的 shell 要只能通过更改 shell源代码来支持了。

其次,es 被设计支持一种编程模型,这里的代码片段被作为只是某种形式的数据来处理。而其他 shell 经常通过把命令作为字符串来传递来近似这个特征,但是这种方式要求借助于繁复的引用规则,特别是嵌套的命令有很多层深的时候。在 es 中,一个构造一旦为花括号所包围,它就可以被存储或传递给一个程序而不用担心有所破坏。

es 不是全新的。它是对我们所认可的一些性质的综合 -- 来自两个 shell、古老的 Bourne shell 和 Tom Duff 的 rc -- 和一些编程语言,特别是 Scheme 和 Tcl。我们尽可能的尝试保持 es 预处理器的简单性,并在很多情况下,比如流控制构造,我们相信我们已经简化和一般化了在早先 shell 中所找到的东西。

我们不相信 es 是终极 shell。它有着麻烦和不可扩展的语法,对传统 shell 表示法的支持强制了某些不幸的设计决定,某些 es 特征如例外和丰富返回值,不能同 UNIX 按我们期望的那样交互。尽管如此,我们认为 es 作为 shell 和编程语言二者是成功的,如果我们被强迫回复到其他 shell 就会发现缺少了它的这些特征和扩展性。

致谢

我们要感谢众多帮助了 es 的开发和本文写作的人。Dave Hitz 提出了我们应当关注什么的根本性建议。Chris Siebenmann 维护了 es 邮件列表和源代码的 ftp 发布。Donn Cave, Peter Ho, Noel Hunt, John Mackin, Bruce Perens, Steven Rezsutek, Rich Salz, Scott Schwartz, Alan Watson, 和列表上的所有其他的贡献者提出了很多建议,和对没有开发好的 shell 作了积极的实验,对 es 的开发是紧要的。最后,Susan Karp 和 Beth Mitcham 读了本文的草稿并在 es 开发期间鼓励了我们。

脚注

1. 在我们的例子中,我们使用了 "es>" 作为 es 的提示符。可以被取代的缺省提示符是 "; ",它被 es 解释为跟随着命令分割符的空命令。所以包括提示符一整行,可以被剪切并粘贴回 shell 去重新执行。在我们的例子中,斜体固定宽度字体指示用户输入。

2. 关键字 @ 介入 lambda。因为 @ 在 es 中不是特殊字符,它周围必须是空白。@ 是对希腊字母 lambda 的替代,它是键盘上剩下的少数还没有特殊意义的字符之一。

3. 这种停用依赖于操作系统支持。

勘误

本节覆盖自从本文发表之后对 es 的变更。如果你担心有未加文档说明的差异,请联系作者。

1. Haahr's present affiliation is Jive Technology, and he can be reached by email at [email protected].

2. 获取一个命令的返回值的 <> 操作符已经被改为 <=,为了避免和 <> 的 POSIX-兼容定义 "打开用来读写" 相冲突。

3. error 例外增加了补充的信息。第二个字(在 error 后面第一个字)现在是导致这个错误的例程的名字。所以,在下面的新版本的 in 中,throw 命令有一个补充的 in 在其中。

4. 这个例子使用了废弃版本的 fork 内置命令。in 函数现在要写为

 fn in dir cmd { if {~ $#dir 0} { throw error in 'usage: in dir cmd' } fork { # run in a subshell cd $dir $cmd } } 

5. 管道计时例子可能不能在所有操作系统上工作。这依赖于理解 es 的 time 版本,要么把它内置到 es 中要么使用 SHELL 环境变量搞一个外部 time。es 将包含(最小化) time 函数,如果使用编译选项 BUITIN_TIME 建造的话。

6. 出于两个原因,最初描述的初始化过程会导致性能问题。首先需要时间去分析和运行初始化代码;其次是运行代码建立的数据(比如,变量名字和函数定义)必须被垃圾收集。es 通过把这项工作转移到编译时间来解决这两个问题。

在建造 es 的时候,创建了叫做 esdump 的可执行文件,它是 shell 的裁减版本。这个程序运行时带有初始化文件 initial.es 作为它的输入。在初始化文件中做的最后一件事情是调用基本的 $&dump,它只包含在 esdump 中,它按照 C 源代码中的声明写出 shell 的全部内存状态。生成的 C 代码被编译和与产生真实的 shell 可执行文件的源代码的余下部分连接起来。来自 dump 的数据是不被垃圾收集的并且是声明了 const 的,所以 C 编译器会把它放入只读内存中,如果可能的话。

引用

1. Brian W. Kernighan and Rob Pike, The UNIX Programming Environment, Prentice-Hall, 1984.

2. S. R. Bourne, "The UNIX Shell," Bell Sys. Tech. J., vol. 57, no. 6, pp. 1971-1990, 1978.

3. Tom Duff, "Rc -- A Shell for Plan 9 and UNIX Systems," in UKUUG Conference Proceedings, pp. 21-33, Summer 1990.

4. William Clinger and Jonathan Rees (editors), The Revised^4 Report on the Algorithmic Language Scheme, 1991.

5. Robin Milner, Mads Tofte, and Robert Harper, The Definition of Standard ML, MIT Press, 1990.

6. John Ousterhout, "Tcl: An Embeddable Command Language," in Usenix Conference Proceedings, pp. 133-146, Winter 1990.

7. Jon L. Bentley, More Programming Pearls, Addison-Welsey, 1988.

8. David R. Hanson, "Fast allocation and deallocation of memory based on object lifetimes," Software -- Practice and Experience, vol. 20, no. 1, pp. 5-12, January, 1990.

9. R. Kent Dybvig, The Scheme Programming Language, Prentice-Hall, 1987.

作者信息

Paul Haahr is a computer scientist at Adobe Systems Incorporated where he works on font rendering technology. His interests include programming languages, window systems, and computer architecture. Paul received an A.B. in computer science from Princeton University in 1990. He can be reached by electronic mail at [email protected] or by surface mail at Adobe Systems Incorporated, 1585 Charleston Road, Mountain View, CA 94039. [勘误1]

Byron Rakitzis is a system programmer at Network Appliance Corporation, where he works on the design and implementation of their network file server. In his spare time he works on shells and window systems. His free-software contributions include a UNIX version of rc, the Plan 9 shell, and pico, a version of Gerard Holzmann's picture editor popi with code generators for SPARC and MIPS. He received an A.B. in Physics from Princeton University in 1990. He has two cats, Pooh-Bah and Goldilocks, who try to rule his home life. Byron can be reached at http://www.rakitzis.com/resume.html or at Network Appliance Corporation, 2901 Tasman Drive, Suite 208, Santa Clara, CA 95054.

相关阅读 更多 +
排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载