Tom Duff
[email protected]
翻译:寒蝉退士
译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。
原文:http://plan9.bell-labs.com/sys/doc/rc.html
摘要
rc 是一个命令解释器,它为 Plan 9 提供类似于 UNIX 的 Bourne shell 的设施,并带有一些小的补充和更少特异性的语法。本文使用很多例子来描述 rc 的特征,并把 rc 与 Bourne shell 做对比,这个模型很多读者已经很熟悉了。
rc 在精神上类似于 UNIX 的 Bourne shell 但在细节上有很多不同。本文使用很多小例子和一些大点的例子来描述 rc 的首要特征。假定读者熟悉 Bourne shell。
对于最简单的使用,rc 有着 Bourne-shell 用户很熟悉的语法。下列命令都会如愿而行:
date
cat /lib/news/build
who >user.names
who >>user.names
wc <file
echo [a-f]*.c
who | wc
who; date
vc *.c &
mk && v.out /*/bin/fb/*
rm -r junk || echo rm failed!
包含一个空格或 rc 的其他语法字符之一的参数必须包围在单引号(')之中:
rm 'odd file name'
在已被引号括起来的参数中的单引号必须是双重的:
echo 'How''s your father?'
包含字符 * ? [ 中任何一个的未被引号括起来的参数都是同文件名字匹配的模式。字符 * 匹配字符的任意序列,字符 ? 匹配任何单一字符,而 [class] 匹配在 class 中的任意字符,除非 class 的第一个字符是 ~,在这种情况下取这个类的补集。class 也可以包含用 - 分隔的成对的字符,它表示词法上在两者之间的所有字符。字符 / 在模式中必须显式的出现,路径名分量 . 和 .. 也是如此。模式被替代为一个参数的列表,每个匹配的路径名字都是一个参数,作为例外,没有匹配的名字的模式不被替代为空列表;而是保持为自身。
UNIX 的 Bourne shell 提供字符串值的变量。rc 提供的变量值是参数的列表 - 就是说,字符串的数组。这是在 rc 和传统的 UNIX 命令解释器之间的原则性区别。变量可以通过键入来给出值,例如:
path=(. /bin)
user=td
font=/lib/font/bit/pelm/ascii.9.font
圆括号指示赋予 path 的值是两个字符串的一个列表。变量 user 和 font 被赋予包含一个单一字符串的列表。
通过在变量的名字前面前导一个 $ 可以把变量的值替换到一个命令中,比如:
echo $path
如果 path 已经做如上设置,它将等价于
echo . /bin
变量可以有数字或数字的列表作为下标,比如:
echo $path(2)
echo $path(2 1 2)
它们等价于
echo /bin
echo /bin . /bin
在左圆括号和变量名字之间必须没有空格分隔;否则,这些下标将被当作一个独立的圆括号中的列表。
在变量中的字符串的数目可以用 $# 操作符确定。例如,
echo $#path
对于上面的例子将输出 2。
下面的两个赋值有着微妙的不同:
empty=()
null=''
第一个设置 empty 为不包含字符串的一个列表。第二个设置 null 为包含一个单一的字符串的一个列表,但是这个字符串不包含字符。
尽管它们或多或少是同样的东西(在 Bourne 的 shell 中,它们是不可区分的),它们在几乎所有情况下都表现的很不同。在其他东西当中
echo $#empty
打印 0,而
echo $#null
打印 1。
所有未设置的变量都有值 ()。
偶尔的,把变量的值作为一个单一的字符串是方便的。通过 $" 操作符把字符串的所有元素联接到一个单一字符串中,在元素之间带有空格。这样,如果我们设置
list=(How now brown cow)
string=$"list
则
echo $list
和
echo $string
二者导致相同的输出,就是:
How now brown cow
而
echo $#list $#string
将输出
4 1
因为 $list 有四个成员,而 $string 有一个单一的成员,带有三个空格分隔它的单字。
当 rc 从一个文件读取它的输入的时候,这个文件有权访问在 rc 的命令行上提供的参数。变量 $* 最初被赋值为参数的列表。名字 $1、$2 等是 $*(1)、$*(2) 等的同义字。此外,$0 是一个文件的名字,rc 的输入从其中读入。
rc 有一个字符串联接操作符,插入符 ^,用来建造由多个片断组成的参数。
echo hully^gully
完全等价于
echo hullygully
假定变量 i 包含命令的名字。则
vc $i^.c
vl -o $1 $i^.v
可以编译这个命令的源代码,把结果留在适当的文件中。
联接可以应用于列表之上。下面的
echo (a b c)^(1 2 3)
src=(main subr io)
cc $src^.c
等价于
echo a1 b2 c3
cc main.c subr.c io.c
在细节上,规则是: 如果 ^ 的两个操作数(operand)是有相同的非零个字符串的列表,则成对的连接它们。否则,如果操作数之一是一个单一的字符串,它依次与另一个操作数的每个参数联接。操作数的任何其他组合都是错误的。
用户需求决定了 rc 在特定位置插入插入符,来使语法看起来更像 Bourne shell。例如,下面的:
cc -$flags $stems.c
等价于
cc -^$flags $stems^.c
一般的,rc 会在不由空白分隔的两个参数之间插入 ^。特别是,只要 $ ' ` 跟随着一个被引号括起来的或未被引号括起来的字,或者一个未被引号括起来的字跟随着一个被引号括起来的字并且无空白或 Tab 介于其间,就在二者之间插入一个隐含的 ^。如果立即跟随着一个 $ 的一个未被引号括起来的字包含不是一个字母下划线或 * 的字符,在第一个这样的字符之前插入 ^。
通常用来从一个命令的输出建造一个参数列表。rc 允许一个命令,包围在花括号中并前导着一个反引号 `{...},出现在需要参数的任何地方。执行这个命令并捕获它的标准输出。使用存储在变量 ifs 中的字符来把输入分解到参数中。例如,
cat `{ls -tr|sed 10q}
将按时间次序连接当前目录中十个最旧的文件,假定缺省 ifs 设置为空格、Tab 和换行符。
一般的管道表示法对于几乎所有情况都是足够用了。有时后拥有非线性的管道线是有用的。比树状结构更一般化的管道线拓扑结构可能要求任意大的管道缓冲区,甚至更坏、导致死锁。rc 拥有某种非线性的树状管道线的语法。例如,
cmp <{old} <{new}
将回归测试一个命令的新版本。跟随着在花括号中的命令的 < 或 >,导致运行这个命令并把它的标准输出或输入连结到一个管道上。父命令(本例中的 cmp)开始于连结到某个文件描述符或其他东西上的管道线的另一端,并带有在打开的时候会连接到管道的一个参数(例如,/dev/fd/6)(译注:一个命名管道的名字)。一些命令不准备处理关闭就不可搜寻的输入文件。例如 diff 需要读取它的输入两次。
在命令退出的时候,它向执行它的进程返回一个状态。在 Plan 9 中,状态是描述错误状况的一个字符串。在正常终止的使用它是空的。
rc 把命令退出状态捕获到变量 $status 中。对于一个简单命令 $status 的值就如同上面描述的一样。对于一个管道线 $status 被设置为管道线构件的状态的联接、并带有 | 字符作为分隔符。
rc 有多种控制流,其中很多控制流以前面执行的命令返回的状态作为条件。所有只包含 0 和 | 的 $status 拥有逻辑值 true。任何其他状态都是 false。
包围在 {} 中的命令序列可以用在需要命令的任何地方。例如:
{sleep 3600;echo 'Time''s up!'}&
将在后台等待一小时,接着打印一个消息。不带花括号,
sleep 3600;echo 'Time''s up!'&
将锁住终端一个小时,接着在后台打印这个消息。
为键入的列表中的每个成员执行一次命令,例如:
for(i in printf scanf putchar) look $i /usr/td/lib/dw.dat
它在给定文件中查找单字 printf、scanf 和 putchar 中的每一个。一般形式是
for(name in list) command
或
for(name) command
在第一种情况下为 list 的每个成员执行一次 command,并把这个成员赋予变量 name。如果缺少子句“in list”,假定为“in $*”。
rc 还提供一个通用 if 语句。例如:
for(i in *.c) if(cpp $i >/tmp/$i) vc /tmp/$i
在 cpp 处理而没有错误的每个 C 源程序上运行 C 编译器。‘if not’语句提供双尾(two-tailed)条件。例如:
for(i){
if(test -f /tmp/$i) echo $i already in /tmp
if not cp $i /tmp
}
它是在 $* 中的每个文件之上的循环,把不存在于 /tmp 中的文件复制到这里,并对那些已经存在于这里的文件打印一个消息。
rc 的 while 语句看起来如下:
while(newer subr.v subr.c) sleep 5
它等待直到 subr.v 比 subr.c 新,原因大概是 C 编译处理完了它。
如果控制命令是空的,循环将不会终止。这样,
while() echo y
将模拟 yes 命令。
rc 提供一个 switch 语句来在任意字符串上做模式匹配。它的一般形式是
switch(word){
case pattern ...
commands
case pattern ...
commands
...
}
Rc 依次尝试对在每个 case 语句中的模式去匹配这个 word。除了 / 和 . 和 .. 不需要被显式的匹配之外,模式与用于文件名匹配的一样。
如果有任何模式匹配,执行紧随这个 case 之后直到下一个 case 之前(或 switch 的结束处)的命令,并且这个 switch 的执行完成。例如,
switch($#*){
case 1
cat >>$1
case 2
cat >>$2 <$1
case *
echo 'Usage: append [from] to'
}
是一个 append 命令。调用时带有一个文件参数,它把它的标准输入添加到指名的文件中。带有两个参数时,把第一个文件添加到第二个文件中。任何其他参数数目引发一个错误消息。
内置 ~ 命令也匹配模式,并且经常比 switch 更加简洁。它的参数是一个字符串和一个模式的列表。只有在所有模式都匹配这个字符串的时候,设置 $status 为真。下列例子为 man(1) 命令处理选项参数:
opt=()
while(~ $1 -* [1-9] 10){
switch($1){
case [1-9] 10
sec=$1 secn=$1
case -f
c=f s=f
case -[qwnt]
cmd=$1
case -T*
T=$1
case -*
opt=($opt $1)
}
shift
}
可以通过如下键入来定义函数
fn name { commands }
此后,在遇到叫做 name 的命令的时候,命令的参数列表的余下部分将赋给 $* ,接着 rc 将执行 commands。在完成的时候恢复 $* 的值。例如:
fn g {
grep $1 *.[hcyl]
}
定义 g pattern (用来查找在当前目录中的所有程序源文件中查找 pattern 的出现)。
通过写
fn name
而没有函数体来删除函数定义。
rc 做许多事情来执行一个简单命令。如果这个命令名字是使用 fn 定义的一个函数的名字,则执行这个函数。否则,如果它是一个内置命令的名字,rc 直接执行这个内置命令。否则,查找在变量 $path 提及的目录,直到找到一个可执行文件。在 Plan 9 中不鼓励 $path 变量的广泛使用。转而,使用缺省(. /bin) 和把你需要的绑定入 /bin。
许多命令由 rc 内部执行,原因是不这样就难于实现。
. [-i] file ...
执行来自 file 的命令。在此期间 $* 设置为紧随 file 之后的参数列表的余下部分。使用 $path 来查找 file。选项 -i 指示交互式输入 ­ 在读取每个命令之前打印一个提示(在 $prompt 找到)。
builtin command ...
除了忽略叫做 command 的任何函数之外,正常的执行 command。例如,定义 cd 内置命令(见后)的一个替代者,它宣布新目录的全名。
fn cd{
builtin cd $* && pwd
}
cd [dir]
改变当前目录到 dir。缺省参数是 $home。 $cdpath 是在其中查找 dir 的位置的一个列表。
eval [arg ...]
参数被联接(由空格分隔)到一个字符串中,读为给 rc 的输入并执行它。例如,将回显
x='$y'
y=Doody
eval echo Howdy, $x因为在替换了 $x 之后,eval 的参数是
Howdy, Doody
echo Howdy, $y
exec [command ...]
Rc 用给定命令替代自身。有点像一个 goto - rc 不等待命令退出,并且不回来读更多的命令。
exit [status]
Rc 立即退出并返回给定状态。如果未给出,则使用 $status 的当前的值。
flag f [+-]
这个命令操纵并测试命令行标志(后面描述).设置标志 f。
flag f +
flag f -
清除标志 f。
测试标志 f,恰当的设置 $status。所以
flag f
如果已经设置了 -x 标志则设置 -v 标志。
if(flag x) flag v +
rfork [nNeEsfF]
它使用 Plan 9 rfork 系统入口来把 rc 放置到带有下列属性的新进程组之中:
标志 名字 功能 n RFNAMEG 制做父名字空间的一个复本 N RFCNAMEG 开始于一个新的空名字空间 e RFENVG 制做父环境的一个复本 E RFCENVG 开始于一个新的空环境 s RFNOTEG 制做一个新的通知组 f RFFDG 制做父文件描述符空间的一个复本 F RFCFDG 开始于一个新的空文件描述符空间
程序员手册的 fork(2) 章节详细描述这些属性。
shift [n]
删除 $* 的前 n(缺省为 1)个元素。
wait [pid]
等待有给定 pid 的进程退出。如果未给出 pid, 等待所有未完结的进程。
whatis name ...
以适合输入到 rc 的形式打印每个名字的值。输出的是到变量的赋值,函数的定义,到对一个内置命令的 builtin 的调用,或一个二进制程序的路径名字。例如,可能打印
whatis path g cd who
path=(. /bin)
fn g {gre -e $1 *.[hycl]}
builtin cd
/bin/who
~ subject pattern ...
依次针对每个 pattern 匹配 subject。在匹配时, $status 设置为真。否则,设置为‘不匹配’。模式同于文件名匹配的。在执行 ~ 命令之前模式不受文件名替代的支配,所以它们不需要被包围在引号之中,当然,除非想要的是对 * [ or ? 的一个文字上的匹配。例如匹配任何单一字符,而
~ $1 ?
~ $1 '?'
只匹配一个文字问号。
rc 允许在 < 或 > 之后的方括号 [ ] 中指定文件描述符,来做不是 0 和 1(标准输入和输出)的文件描述符的重定向。例如,
vc junk.c >[2]junk.diag
把编译器的诊断从标准错误保存到 junk.diag 中。
通过如下键入,这些文件描述符可以被替代为一个已经打开了的文件的 dup(2) 意义上的复制:
vc junk.c >[2=1]
它用文件描述符 1 的一个复件替换文件描述符 2。在与其它重定向联合时非常有用,比如
vc junk.c >junk.out >[2=1]
从左至右求值重定向,所以重定向文件描述符 1 到 junk.out,接着把文件描述符 2 指向相同的文件。与之相对,
vc junk.c >[2=1] >junk.out
重定向文件描述符 2 到文件描述符 1(大概是终端), 并接着定向文件描述符 1 到一个文件。在第一种情况下,标准和诊断输出将被混合到 junk.out 中。在第二种情况下,诊断输出将出现在终端上,而标准输出将发送到文件。
可以使用带有右手边为空的复制记号(notation)关闭文件描述符。例如,
vc junk.c >[2=]
将丢弃来自编译的诊断。
通过如下键入,可以派遣(send)任意的文件描述符经过管道,
vc junk.c |[2] grep -v '^$'
它从 C 编译器的错误输出中删除空行。注意 grep 的输出仍出现在文件描述符 1 上。
有时你可能希望把一个管道的输入端连接到不是 0 的某个文件描述符。记号
cmd1 |[5=19] cmd2
建立一个管道线,并且 cmd1'的文件描述符 5 通过管道连接到 cmd2'的文件描述符 19。
rc 过程可以包含叫做“立即文档”数据,它被提供为给命令的输入,比如在下面这版本的 tel 命令中
for(i) grep $i <<!
...
tor 2T-402 2912
kevin 2C-514 2842
bill 2C-562 7214
...
!
通过跟随着一个任意的 EOF 标记符(这个例子中是 !)的重定向符号 << 介入立即文档。紧随这个命令之后,直到只包含唯一的 EOF 标记符的一行之前的那些行都保存到一个临时文件中,它在命令运行的时候连接到命令的标准输入上。
Rc 在立即文档中作变量替换。下列命令:
ed $3 <<EOF
g/$1/s//$2/g
w
EOF
改变在文件 $3 中的 $1 的所有出现为 $2。要在一个立即文档中包含一个文字 $ 需要键入 $$。如果变量的名字立即跟随着 ^,则删除这个插入符。
通过把紧随在 << 之后的 EOF 标记符用引号包围起来,比如 <<'EOF',可以完全的抑制变量替换。
通过如下键入,可以在不是 0 的文件描述符上提供立即文档,
cmd <<[4]End
...
End
如果立即文档出现在一个复合块中,文档的内容必须在整个块之后:
for(i in $*){
mail $i <<EOF
}
words to live by
EOF
在接收到来自终端的中断的时候,rc 脚本通常终止。以通常的方式定义带有小写的一个 UNIX 信号名字的函数,但在 rc 收到对应的通知的时候调用。程序员手册的 notify(2) 章节详细的讨论了通知。有意思的通知有:
sighup
通知是‘hangup’。Plan 9 在终端从 rc 断开的时候发送这个通知。
sigint
通知是‘interrupt’,通常在终端上键入中断字符(ASCII DEL)的时候发出。
sigterm
通知是‘kill’,通常由 kill(1) 发送。
sigexit
在 rc 将要退出的时候发送的人为通知。
作为一个例子,
fn sigint{
rm /tmp/junk
exit
}
为键盘中断设置一个陷入来在退出之前删除临时文件。
如果通知例程被设置为 {} 则忽略通知。在删除它的处理器(handler)定义的时候信号恢复它们的缺省行为。
环境是对于执行的二进制程序可以获得的名字-值对的一个列表。在 Plan 9 上,环境存储在叫做 #e 的文件系统中,它通常挂装在 /env 上。每个变量的值都保存在一个单独的文件中,都带有终止于 0 字节的分量(component)。(文件系统完全维持在内存中,所以不涉及磁盘或网络。) /env 的内容在一个每进程(per-process)组基础上共享 - 在建立一个新进程组的时候,把 /env 有效的连结到一个新文件系统上,它被初始化为旧文件系统的复件。这种组织的一个结果是命令可以改变环境条目并看到改变反射到 rc 中。
函数也出现在环境中,通过对它们的名字前导 fn# 来命名,如 /env/fn#roff。
在一个单一命令期间设置一个变量经常是有用的。一个赋值紧随一个命令有这种效果。例如
a=global
a=local echo $a
echo $a
将打印
local
global
这对复合命令也有效,如
f=/fairly/long/file/name {
{ wc $f; spell $f; diff $f.old $f } |
pr -h 'Facts about '$f | lp -dfn
}
下面的这对函数提供标准 cd 和 pwd 命令的增强版本。(为此感谢 Rob Pike。)
ps1='% ' # default prompt
tab=' ' # a tab character
fn cd{
builtin cd $1 &&
switch($#*){
case 0
dir=$home
prompt=($ps1 $tab)
case *
switch($1)
case /*
dir=$1
prompt=(`{basename `{pwd}}^$ps1 $tab)
case */* ..*
dir=()
prompt=(`{basename `{pwd}}^$ps1 $tab)
case *
dir=()
prompt=($1^$ps1 $tab)
}
}
}
fn pwd{
if(~ $#dir 0)
dir=`{/bin/pwd}
echo $dir
}
函数 pwd 是标准 pwd 的一个增强版本,它在变量 $dir 中缓冲它的值,因为真正的 pwd 可能执行起来非常慢。(最新版本的 Plan 9 已经有了非常快的 pwd 实现,这减低了 pwd 函数的优势。)
函数 cd 调用 cd 内置命令,并检查它是否成功。如果成功,它设置 $dir 和 $prompt。提示将包含当前目录的最后的成员(除非在 home 目录中,在这里它将是空),并且 $dir 将被重置为正确的值或者是 (),这样 pwd 函数将正确的工作。
man 命令打印程序员手册的页面。例如,下面的调用
man 2 sinh
man rc
man -t cat
在第一种情况,打印章节 2 中的 sinh 页面。在第二种情况,打印 rc 的手册页。因为未指定章节,在所有章节中查找这个页面,在章节 1 中找到了它。在第三种情况,排版 cat 的页面(使用了 -t 选项)。
cd /sys/man || {
echo $0: No manual! >[1=2]
exit 1
}
NT=n # default nroff
s='*' # section, default try all
for(i) switch($i){
case -t
NT=t
case -n
NT=n
case -*
echo Usage: $0 '[-nt] [section] page ...' >[1=2]
exit 1
case [1-9] 10
s=$i
case *
eval 'pages='$s/$i
for(page in $pages){
if(test -f $page)
$NT^roff -man $page
if not
echo $0: $i not found >[1=2]
}
}
注意使用了 eval 来制作候选手册页的一个列表。不使用 eval,存储在 $s 中的 * 将不触发文件名匹配 - 它被包围在引号中,并且即使不这样,它将在赋值给 $s 的时候被展开。 eval 导致它的参数被 rc 的分析器和解释器重新处理,有效的延迟 * 的求值直到赋值给 $pages.
下列 rc 脚本玩一个假装的简单游戏 holmdel,在其中游戏者交替的指名 Bell Labs 的位置,第一个提及 Holmdel 的获胜。
t=/tmp/holmdel$pid
fn read{
$1=`{awk '{print;exit}'}
}
ifs='
' # just a newline
fn sigexit sigint sigquit sighup{
rm -f $t
exit
}
cat <<'!' >$t
Allentown
Atlanta
Cedar Crest
Chester
Columbus
Elmhurst
Fullerton
Holmdel
Indian Hill
Merrimack Valley
Morristown
Neptune
Piscataway
Reading
Short Hills
South Plainfield
Summit
Whippany
West Long Branch
!
while(){
lab=`{fortune $t}
echo $lab
if(~ $lab Holmdel){
echo You lose.
exit
}
while(read lab; ! grep -i -s $lab $t) echo No such location.
if(~ $lab [hH]olmdel){
echo You win.
exit
}
}
这个脚本值得详细描述(至少它不蠢。)
变量 $t 是对临时文件名字的一个简写。在这个脚本的多个实例同时运行的情况下,在临时文件名字包括 $pid 能确保它们的名字不会冲突,rc 把 $pid 初始化为它的进程-id。
函数 read 的参数是一个变量的名字,把从的标准输入读取来的行汇集到其中。$ifs 被设置未只有一个换行符。这样 read 的输入不会在空格处被分离,但是终止的换行被删除了。
设置了一个处理器来捕获 sigint、sigquit 和 sighup,还有一个人工的 sigexit 信号。它们只是删除临时文件并退出。
从包含 Bell Labs 位置的一个列表的立即文档来初始化临时文件,接着主循环开始。
首先,程序使用 fortune 程序从位置列表中挑出随机的一行来猜测一个位置(在 $lab 中),如果它猜到 Holmdel,打印一个消息并退出。
接着它使用 read 函数来从标准输入得到一些行,并对它们做有效性检查直到得到一个合法的名字。注意 while 的条件部分可以是一个复合命令。只检查在序列中最后一个命令的退出状态。
最后,如果结果是 Holmdel,打印一个消息并退出。否则它回到循环的顶部。
Rc 在很大程度上吸取了 Steve Bourne 的 /bin/sh。任何 Bourne shell 的后继者都纳入此类比较。我尝试着去修补它最周知的缺点,并通过省略无关紧要的特征近可能的去简化事情。只在遇到不可抗拒的诱惑的时候我才介入新颖的想法。明显的,我广泛的修补了 Bourne 的语法。
在 rc 的设计中的最重要的原理是它不是一个宏处理器。词法和语法分析代码从不扫描输入多于一次(当然 eval 命令是个例外,它的存在就是为了打破这个规则)。
通过把包含空格的参数传递给 Bourne shell 脚本,它们经常被粗暴的运行。经常在不恰当的时候使用 $IFS 把它们分割到多个参数中。在 rc 中,变量的值,包括命令行参数,在替换到命令中的时候是不重新扫描的。参数大概已经在父进程中扫描过了,并且不应当被重新扫描。
为什么 Bourne 在变量替换之后重新扫描命令? 它需要能够在值是字符串的变量中存储参数列表。如果我们除去了重新扫描,我们必须改变变量的类型,这样它们可以显式的承载字符串的列表。
这介入了一些概念上的复杂性。我们需要对字的列表的表示法。有两种不同的联接,对字符串的 - $a^$b, 和对列表的 - ($a $b)。 在 () 和 '' 之间的区别对初学者是容易混淆的,尽管差别是确切明显的 - 一个空参数和没有参数是不一样的。
Bourne 在做命令替换的时候也需要重新扫描输入。这是因为包围在反引号之中的文本不是字符串而是一个命令。严格的说,在包围着命令是嵌套的命令替换时候必须分析它,但是这使处理它很困难,比如:
size=`wc -l \`ls -t|sed 1q\``
内部的反引号必须被转义来避免终止外部的命令。比上面的例子更糟的是,需要的转义数目是嵌套深度的指数。rc 通过把后引用改为参数是命令的一个一元操作符来修正这个问题,例如:
size=`{wc -l `{ls -t|sed 1q}}
不需要转义,并且在一遍之中分析所有事情。
出于类似的原因,rc 定义信号处理器如同它们是函数,而不是如同 Bourne 那样为每个信号关联上一个字符串,伴有在键入中断字符时得到语法错误作为响应的可能性。因为 rc 在键入的时候分析输入,它在你制作它们的时候就报告错误。
解决了所有这些麻烦,我们获得了实质的语义上的简化。不需要区分 $* 和 $@。不需要四种类型的引用,没有支配它们的非常复杂的规则。在 rc 中你在想让一个语法字符出现在参数中、或者参数是空串的时候,而不在其他时候使用引号。IFS 不再使用,除了在绝对必须的一种情况下: 在命令替换期间把命令输出转换到参数列表中。
这也避免了一个重要的 UNIX 安全漏洞。在 UNIX 中,system() 和 popen() 函数调用 /bin/sh 来执行一个命令。不可能使用任何一个这种例程而且保证指定的命令会执行,即使 system() 或 popen() 的调用者为这个命令指定了全路径名字。如果这发生在 set-userid 程序中将是破坏性的。问题在于使用 IFS 把命令分解到一组字中,所以攻击者在运行特权程序之前,只需要在他的环境中设置 IFS=/ 并在当前目录下留下一个叫做 usr 或 bin 的木马程序。rc 通过永不为任何原因而重新扫描输入来解决修正问题。
在 rc 和 Bourne shell 之间的多数其他区别都是不严重的。我去除了 Bourne 变量替换的怪异形式,如
echo ${a=b} ${c-d} ${e?error}
因为它们很少使用,多余和易于用不深奥的术语表达。我删除了内置的 export、readonly、break、continue、read、return、set、times 和 unset,因为他们好象是多余的或只有少量的使用。
在 Bourne 出自 Algol 68 语法的地方,rc 转而基于 C 或 Awk。这是很难辩护的。我相信,比如
if(test -f junk) rm junk
优于
if test -f junk; then rm junk; fi
因为它更少受关键字的困扰。它避免了 Bourne 在奇怪的地方需要的分号,并且语法字符更好的突出了命令的起作用的部分。
Bourne 无可争议的比 rc 好的一个大范围的语法是带有 else 子句的 if 语句。rc 的 if 没有终止的 fi 式的括号(bracket)。结果是,分析器不预先查看它的输入就不能断定是否预期有一个 else 子句。问题出在读取之后,例如
if(test -f junk) echo junk found
在交互模式下,rc 不能确定是立即执行它并打印 $prompt(1),还是打印 $prompt(2) 并等待键入 else。在 Bourne shell 中,这不是个问题,因为 if 语句必须以 fi 结束,不管它是否包含 else。
Rc 公认的薄弱的解决方案是在一个单独的语句中声明 else 子句,带有它必须立即跟随在 if 之后的语义附加条件,并把它叫做 if not 而不是 else,作为快要接近怪物的警示。它唯一值得注意的结果是在下面的构造中需要花括号。
for(i){
if(test -f $i) echo $i found
if not echo $i not found
}
rc 解决“摇摆的 else”二义性的方法与多数人的预期相反。
值得注意的是在 UNIX 系统程序员手册的四个最新的版本中,手册页中描述的 Bourne shell 文法不认可命令 who|wc。这的确是一个疏漏,但它暗示了更黑暗的事情: 没有人真正知道 Bourne shell 的语法是什么。即使是查看源代码也少有帮助。分析器是以递归下降方式实现的,但是对应于文法类属(category)的例程都有一个标志参数,依据上下文巧妙的改变它们的操作。rc 的分析器是使用 yacc 实现的,所以我们可以精确的说出它的语法。
Rob Pike、Howard Trickey 和其他 Plan 9 用户是好想法和批评的持久的、无尽的来源。本文档中的一些例子提取自 [Bourne],这些是 rc 最好的特征。
S. R. Bourne, UNIX Time-Sharing System: The UNIX Shell, Bell System Technical Journal, Volume 57 number 6, July-August 1978
Copyright © 2000 Lucent Technologies Inc. All rights reserved.