半年前的上回书说到你如何去玩shell游戏,估计现在你已经玩腻了,不过不要着急,下面,该轮到shell玩你了。
欢迎来到元语法(metasyntacitic)动物园
C
Shell的元语法操作符带来了大量和引用有关的问题和混乱。元操作符在一个命令被执行前对其进行转换。我们把这些操作符叫做元操作符是因为它们不属于命令的语法成分,它们作用于命令本身。大多数程序员对元操作符(有时也叫做escape
operators)并不陌生。比如,C字符串中的反斜杠(\)就是一个元语法操作符;它不代表自己,而是对其后的字符的说明。如果你不希望如此,你必须使用引用机制来告诉系统把元操作符当做一般字符来处理。回到刚才C字符串的例子,如果想得到反斜杠字符,你必须写成\\。
简单的引用机制在C Shell中很难工作,这是因为Shell和它执行的程序之间各执一词,无法统一。例如,下面是个简单的命令:
grep string filename;
参数string包含了grep定义的字符,比如?, [,
和]等等,但这些字符是shell的元操作符。这意味着必须对它们进行引用。然而,有些时候可能不需要如此,这和你使用什么样的shell以及环境变量有关。
如果想在字符串中寻找包含点(.)或其他以横杠(-)开头的模式,这就更为复杂了。
一定要记住对元字符进行正确引用。不幸的是,和模式识别一样,操作系统的各个部分都充斥着互不兼容的引用标准。
C
Shell的元语法动物园里饲养着七种不同的元操作符家族。斗转星移,转眼间动物园里已经人满为患了,笼子使用的不再是钢铁,而是用锡了。动物间的小磨擦不断。这七种针对shell命令行的转换方式是:
别名 alias, unalias
命令输出替代 `
文件名替代 *, ?, []
历史替代 !, ^
变量替代 $, set, unset
进程替代 %
引用 ',"
这一“设计”的结果是,问号(?)永远被shell当成单字符匹配符,它永远无法被作为命令行参数传给用户程序,所以别想使用问号来作为帮助选项。
如果这七种元字符有着清晰的逻辑顺序和定义,那么情况也不会太糟糕。可事实并非如此:
日期: Mon, 7 May 90 18:00:27 - 0700
发信人: Andy Beals <bandy@lll-crg.llnl.gov>
主题: Re: today's gripe: fg %3 (今天之不爽事:fg %3)
收信人: UNIX-HATERS
你可以使用%emacs或者%e来恢复一个任务(如果唯一的话),也可以使用%?foo,如果"foo"出现在命令行中。
当然,!ema和!?foo也可以用于历史命令替换上。
但是,UCB的猪头(pinheads)们没有想到!?foo后面可能伴随的编辑命令:
!?foo:s/foo/bar&/:p
多向前扫描一个编辑字符真的这么困难么?
哪怕是Unix“专家”,也要晕菜了。下面再来看看Milt
Epstein的这个例子,他想写个shell脚本获得实际被敲入的命令行,而不是那个经shell处理后的结果。他最后发现这并不容易,因为shell为命令做了太多的“好事”。要做到这一点,需要各种稀奇古怪的变形技术,连Unix专家也会望而却步。这就是Unix的典型做法:把简单的东西搞得异常复杂,这只是因为这些东西在Unix诞生之时从没被仔细考虑过:
日期: 19 Aug 91 15:26:00 GMT
发信人: Dan_Jacobson@att.com
主题: ${1+"$@"} in /bin/sh family of shells shell scripts
收信人: comp.emacs.gnu.emacs.help, comp.unix.shell
>>>>> On Sun, 19 Arg 91 18:21:58 - 0500
>>>>> Milt Epstein <esptein@suna0.cs.uiuc.edu>
写到:
Milt> "${1+"$@"}"究竟是什么意思?我估计这是用来
Milt> 读取其余的命令行参数,但我不敢肯定。
这是/bin/sh里用来完整复制命令行参数的一种方法。
它的意思是:如果有至少一个参数(${1+),那么用所有参数("$@")来替代以保留所有的空白字符。
如果我们只使用"$@",那么在没有参数的情形下会得到""而不是我们想要的空参数。
那么,为什么不使用"$*"呢?sh(1)的man手册是这样说的:
双引号之间的参数和命令会被替换,shell会对结果加上引用,以避免解析空格或生成文件名。如果$*出现在双引号之中,各个参数之间的空格会被加上引用("$1
$2 ..."),而如果$@出现在双引号之中,各个参数之间的空格不会被加上引用("$1""$2" ...)。
我认为${1+"$@"}一直可以兼容到“版本7”的shell。
老天!一直兼容到“版本7”。
听"chdir"的还是听"cd"的?
Unix在漫长的演化过程中几经易手,这些Unix系统开发者把Unix引向了不同方向,他们之中没有一个人停下来考虑一下自己的做法会不会对和其他人发生冲突。
日期: Mon, 7 May 90 22:58:58 EDT
发信人: Alan Bawden <alan@ai.mit.edu>
主题: cd ..: I am not making this up (cd ..: 这不是我编造出来的)
收信人: UNIX-HATERS
有什么命令能比"cd"更直接了当的呢?让我们看这个简单的例子:"cd ftp"。如果我的当前目录/home/ar/alan中有个子目录叫做"ftp",那么它就变成了我新的当前目录,现在我在/home/ar/alan/ftp下了。简单吧?
现在,你们知不知道"."和".."?每个目录都会有两个记录:"."是指该目录自己,".."是指父目录。在上面的例子中,如果我想回到/home/ar/alan,只要敲"cd
.."就可以了。
现在假设"ftp"是一个符号链接。假设它指向的是目录/com/ftp/pub/alan。如果执行"cd
ftp",我的当前目录将是/com/ftp/pub/alan。
和其他所有目录一样,/com/ftp/pub/alan也有一个叫".."的记录,它指的是父目录:/com/ftp/pub。如果我想进入那个目录,我敲入命令:
% cd ..
猜一下我现在在哪儿呢?我又回到了/home/ar/alan!shell(准确的说是人工智能实验室里用的tcsh)认为我其实是想回到那个装有符号链接的目录。现在我必须使用"cd
./.."才能进入/com/ftp/pub。
Shell编程
Shell程序员和《侏罗纪公园》里的恐龙制造者有些类似。他们手上没有所需的完整材料,所以不得不用一些乱七八糟的材料填充。尽管有着无穷的自信和能力,他们似乎并不是总能控制住造出来的那些玩意。
理论上说,使用Shell编程比用C语言要有很多好处:Shell程序移植容易。这指的是使用shell“编程语言”写的程序能够在不同的体系结构和不同的Unix变种上运行,因为shell会解析这些程序,而不是把这些程序编译成机器码运行。而且,标准Unix
Shell sh 自从1977年以来就成为Unix中不可或缺的一部分,所以你可以在许多机器上找到它。
让我们来验证一下这个理论,写个脚本列举目录下的所有文件,并使用file命令来显示文件的类型:
日期: Fri, 24 Apr 92 14:45:48 EDT
发信人: Stephen Gildea <gildea@expo.lcs.mit.edu>
主题: Simple Shell Programming (简单Shell编程)
收信人: UNIX-HATERS
同学们好。今天我们将学习"sh"编程。"sh" 是个简单,用途广泛的程序,让我们先看个基本的例子:
打印一个目录下所有文件的类型
(我听到你们在后面说什么了!那些已经会了的同学可以写个脚本在远程启动一个X11客户端,不要吵吵!)
我们学习sh编程的同时,当然也希望自己的程序是健壮,可移植和优雅的。我假设你们都读过了相应的man手册,所以下面这个实现应该很简单:
file *
很不错,是不是?简单的答案给简单的问题;符号 *
用来匹配目录下的所有文件。嗯,不一定。以点(.)开头的文件会被忽略,*不会匹配它们。也许这种情况很少发生,但既然要写健壮的程序,我们将使用"ls"的一个特殊选项:
for file in `ls -A`
do
flie $file
done
多么优雅,多么健壮!不过,唉,一些系统上的"ls"不接受"-A"选项。没问题,我们使用"-a"选项,然后再去掉"."和"..":
for file in `ls -a`
do
if [ $file != . -a $file != ..] then
file $file
fi
done
不是那么优雅,但至少是健壮的和可移植的。你说什么?"ls -a"也不是哪里都能用的?没问题,我们用"ls
-f"好啦。它还快一点呢。我希望你们能从man手册中看到所有这些东西。
唔,可能不是那么健壮。Unix文件名中除了斜杠(/)以外可以使用任何字符。如果文件名中有个空格的话,这个脚本就完蛋了,这是因为shell会把它当成两个文件名传给"file"命令。不过这也不是太难对付。我们只要把它放到引用中就可以了:
for file in `ls -f`
do
if [ "$file" != . -a "$file" != ..]
then
file "$file"
fi
done
你们中可能已经有人看出来了,我们只是减少了问题,但还是没有完全解决。因为换行符也能用在文件名中。
我们的脚本不是那么简单了,看来得重新评估一下我们用的方法了。如果我们不使用"ls"就不用费劲去处理它的输出了。这个怎么样:
for file in * .*
do
if [ "$file" != . -a "$file" != ..]
then
file "$file"
fi
done
看起来不错。能够处理点(.)文件和有非打印字符的文件名。我们不断把一些稀奇古怪的文件名加入到测试目录下,这个脚本始终工作得很好。然而有个家伙用它测一个空目录,这时候
* 产生了"No such file"(没有这个文件)的输出。不过,我们当然可以继续处理这种情况...
....到了这一步uucp可能会嫌我的这封邮件可能,看来我只能到此为止了,请读者去自己解决剩下的bug吧。
Stephen
还有一个更大的问题Stephen没有想到,我们从一开始就有意隐藏着:Unix file 命令不工作。
日期: Sat, 25 Apr 92 17:33:12 EDT
发信人: Alan Bawden <Alan@lcs.mit.edu>
主题: Simple Shell Programming (简单Shell编程)
收信人: UNIX-HATERS
喔!别忙。再仔细看看。你真的想用'file'命令?如果谁想开心大笑一场可以马上去找一台Unix机器,在有各种各样文件的目录下敲命令"file *"。
例如,我在有各种C源代码文件的目录下运行"file"——这是一些结果:
arith.c: c program text
binshow.c: c program text
bintxt.c: c program text
看起来还不错。不过这个就不太对了:
crc.c: ascii text
看到了么?'file'并不是根据后缀".c"去判断的,它对文件的内容采用了一些启发式(heuristics)算法。很明显crc.c看起来不那么象C代码——尽管对我来说它就是。
gencrc.c.~4~: ascii text
gencrc.c: c program text
估计我在第4版本以后做了一些修改,使得gencrc.c更象是C代码了...
tcfs.h.~1~: c program text
tcfs.h: ascii text
很明显第1版本以后的tcfs.h不太象是C代码了。
time.h: English text
没错,time.h看起来更象英语,而不是一般的ascii码。我不知道'file'是不是还能判断出西班牙语或法语。(顺便说一下,你的TeX文档会被当成"ascii
text"而不是"English text",不过这有点儿跑题了)
words.h.~1~: ascii text
words.h: English text
这可能是因为我在第1版本以后在words.h中加入了一些注释。
我把最精采的留在最后:
arc.h: shell commands
Makefile: [nt]roff, tbl, or eqn input text
都错得一塌糊涂。我不知道如果根据'file'的判断结果去使用这些文件会造成什么结果。
—Alan
Shell变量
当然Alan还不算最倒霉的,至少他没试过shell变量。
我们前面说过,sh和csh对shell变量的实现不太一样。这本来没有什么,可是一些shell变量的语义(比如定义的时刻,改变的原子性等)没有被好好说明或定义。总会遇到一些奇怪反常规的shell变量,只有反复试验以后才能明白。
日期: Thu, 14 Nov 1991 11:46:21 PST
发信人: Stanley's Tool Works <lanning@parc.xerox.com)
主题: You learn something new every day (每天都有新发现)
收信人: UNIX-HATERS
运行一下这个脚本:
#!/bin/csh
unset foo
if ( ! $?foo ) hen
echo foo was unset
else if ("$foo" = "You lose") then
echo $foo
endif
会产生如下错误:
foo: Undefined variable.
如果要让这个脚本"正确工作",你必须求助于以下这个脚本:
#!/bin/csh
unset foo
if ( ! $?foo ) hen
echo foo was unset
set foo
else if ("$foo" = "You lose") then
echo $foo
endif
[注意,我们必须在发现foo没有被定义的时候'set foo'.] 清楚了么?
错误码和错误检查
我们上面的例子没有指出file命令如何将错误返回给脚本。事实上,它根本就没有返回错误。错误被忽略了。这不是因为我们粗心大意:许多Unix
shell脚本(以及其他程序)忽略所调用程序所返回的错误码。这个做法是可取的,这是因为没有标准的错误码。
也许之所以错误码被广泛地忽略,是因为当用户敲命令的时候这些错误码很少被显示出来。错误码和错误检查在Unix阵营中是如此少见,以至于有些程序甚至根本就不费劲去报告错误。
日期: The, 6 Oct 92 08:44:17 PDT
发信人: Bjorn Freeman-Benson <bnfb@ursamajor.uvic.ca>
主题: It's always good news in Unix land (Unix世界里都是好消息)
收信人: UNIX-HATERS
看看这个tar程序。和所有的Unix"工具"(似乎不太准确)一样,tar的工作方式非常奇怪和特别。例如,tar是一个极为乐观向上的程序,它认为从不会有什么坏事,所以太从来不返回错误状态。事实上,哪怕是在屏幕上打出了错误信息,它返回的仍然是"好消息"
(状态0)。运行一下这个脚本:
tar cf temp.tar no.such.file
if ( $status == 0 ) echo "good news! No error."
你将得到如下结果:
tar: no.such.file: No such file or directory
Good news! No error.
我明白了——我从一开始就不应该奢望什么一致,有用,帮助良好,快速,甚至是正确的结果...
Bjorn
OK, 被shell折腾得很爽吧?还没过足瘾?不要紧,下回书我们换个地方,钻进Unix下水道(pipe)里体验无穷的痛苦和快乐
管道
Unix受虐狂们,欢迎来到Unix下水道。
“在我们这个世纪,巴黎下水道仍是一个神秘的场所。如果知道自己的下面是个可怕的大窖,巴黎会感到不安。” —— 雨果 《悲惨世界》
下面只是我自己对Unix的看法。大约六年前(当我有了第一台工作站的时候),我用了很多时间学习Unix。应该学得算是不错的。幸运的是,脑子里的这些垃圾正随着时间的推移慢慢降解。可是,自从这个讨论开始以来,不少Unix支持者发给我例子来“证明”Unix的强大。这些例子当然唤起了我许多美好的回忆:他们都是用一种最怪异的方式去实现一些简单而无用的功能。
有个家伙发了篇贴子讲述一个shell脚本是如何让他获得“圆满”的(这个脚本使用了四个噪声一样的命令把他所有的'.pas'后缀的文件改名为'.p'文件)。可我还是想把自己的宗教热情留给比改几个文件名更重要的事情上。是的,这就是Unix工具留给我的记忆:你用大量的时间去学那些复杂奇特的花架子,可到头来却是一场空。我还是去学些有用的真功夫吧。
——Jim Giles
Los Alamos国家实验室
Unix迷们拜倒在管道(pipe)的真善美之下。他们他们歌唱管道:没有管道就没有Unix。他们异口同声地颂扬管道:“管道能够让你用简单的程序去构造更复杂的程序。管道能够以意想不到的方式去使用命令,管道使得实现更为简单。”不幸的是,颂歌对Unix的作用并不比对伟大旗手的要好多少。
管道并不是一无是处。模块化和抽象化是建立复杂系统中所必需的,这是计算机科学的基本原则。基本工具越是优秀,用其建立的复杂系统就会更为成功,可维护性也越高。管道作为构造工具还是有价值的。
以下是个管道的例子:
egrep '^To:|^Cc:' /var/spool/mail/$USER | \
cut -c5- | \
awk '{ for (i = 1; i <= NF; i++) print $i}' | \
sed 's/,//g' | grep -v $USER | sort | uniq
看明白了么?这个程序通过读取用户的邮箱,得到用户所在的邮件列表(差不多是这个意思)。和你家里的水管一样,这个Unix管道也会在特定情况下神秘地破裂。
管道有时的确很有用,但它通过连接标准输入输出的方式进行进程间通讯,这个机制限制了它的应用。首先,信息只能单向流动。进程无法通过管道进行双向通讯。其次,管道不支持任何形式的抽象。发送方和接收方只能使用字符流传输信息。比字符稍微复杂一点的对象是不能通过管道直接传输的,必须串行化为字符流以后才成,当然接收方必须对得到的字符流进行重新组装。这意味着你无法传输一个对象以及用于建立这个对象的定义代码。你无法传输指针到另一个进程的地址空间。你无法传输文件句柄或socket句柄或文件权限属性。
冒着被骂做自以为是的风险,我们认为正确的模型应该是过程调用(本地的或是远程的),用以传递第一类结构(first-class
structures)(这是C语言从一开始就支持的)和函数组合(functional composition)。
管道对简单任务是不错的,比如文本流处理,但用它来建立健壮软件就显得有些捉襟见肘了。例如,早期关于管道的一篇论文中说明了如何使用管道把一些小程序组合在一起来构成一个拼写检查程序。这是体现简单性的经典之作,但如果真的用来检查拼写错误就再糟糕没有了。
管道在shell脚本中有经常能露一小手。程序员用它实现一些简单而脆弱的解决方案。这是因为管道使得两个程序之间产生了倚赖关系,如果你修改了一个程序的输出格式,就必须同时修改另一个程序的输入处理。
大多数程序是一步步建立起来的:首先制定程序的需求规范,然后程序的内部逐渐成型,最后写一个输出处理函数。管道则不把这一套放在眼里:只要有人把一个半生不熟的程序放到了管道中,其输出格式就定死了,不管是多么不一致,不标准和低效,你都只能认命了。
管道不是程序间通讯的唯一选择。Macintosh就没有管道,我们最喜欢的Unix宣传手册是这样写的:
但是,Macintosh采用的则是截然相反的一种模型。系统不和字符流打交道。数据文件具有更高的层次,总是和特定的程序相关的。你什么时候把一个Mac程序的输出传给另一个过?(如果能找到管道符都算你运气)程序自成一体,你必须彻底明白自己在干嘛呢。你无法把MacFoo和MacBar搞到一起。-—
摘自 《Unix生活》 Libes和ressler著
是呀,这些可怜的Mac用户。如果无法把字符流通过管道四处乱传,他们怎么能在文档中插入绘画程序制作的图片?怎么能插入一个表格文档?怎么能把这个东拼西凑成的用电子邮件发出去?接到以后又怎么能无缝地对它进行浏览和编辑,再回复回去?没有管道,我们不能想像这一切在过去的十年中是如何被Macintosh做到的。
上次你的Unix工作站和Macintosh一样有用是什么时候?上次你能在它上面跑不同公司(甚至是同一公司的不同部门)的软件是什么时候?更不用说这些软件能互相通信。如果Unix真做到了这一点,那是因为Mac软件开发商拼了老命把他们的软件移植到了Unix上,寄希望于让Unix看起来象Mac一些。
Unix和Macintosh操作系统的根本区别是,Unix是为取悦程序员而设计的,而Mac是为了取悦用户。(Windows一门心思想取悦的则是会计,不过这有些跑题了)。
研究表明管道和重定向是难于使用的,不是因为想法本身,而是由于其随意和不直观的限制。Unix自己的文档早就指明了只有Unix死党才能体会管道的妙处。
日期: thu, 31 Jan 91 14:29:42 EST
发信人: Jim Davis <jrd@media-lib.media.mit.edu>
收信人: UNIX-HATERS
主题: Expertise (专业知识)
今天早上我读到《人机接口杂志》上的一篇文章《计算机操作系统专业知识》,是Stephanie M. Doane和其他两位作者写的。猜猜他们研究的是什么操作系统?Doane对Unix新手、中手和专家的知识和表现进行了研究,下面是一些摘要:
“只有专家能够使用Unix特有的一些功能(例如管道和重定向)来构造命令组合”
换句话说,Unix的每个新功能(除了那些从其他系统上生搬硬套过来的)都是如此怪异,以至于必须经过多年同样怪异的学习和实践才能掌握。
“这个发现有些出乎意料,因为这些正是Unix的基础功能,而且这些功能是所有初级课程都会涉及的”
她还引用了S. W. Draper的一些文章,Draper相信:
“世上根本没有什么Unix专家,如果专家指的是这样一些人,他们穷尽了某专业的所有知识,无需再学习什么了。”
这一点我不能苟同。在学习Unix各种荒谬技术的征途上,已经有无数人被“穷尽”了。
有些程序甚至吃饱了撑的,把管道和文件重定向区别对待了:
发信人: Leigh L. Klotz <klotz@adoc.xerox.com>
收信人: UNIX-HATERS
主题: | vs. < (|对<)
日期: Thu, 8 Oct 1992 11:37:14 PDT
collard% xtpanel -file xtpanel.out < .login
unmatched braces
unmatched braces
unmatched braces
3 unmatched right braces present
collard% cat .login | xtpanel -file xtpanel.out
collard%
你自己琢磨琢磨吧。
Find
Unix最为恐怖的是,不管你被它开过多少次瓢,你总是没法失去知觉。它就这么开来开去,没完没了。
——Patrick Sobalvarro
在一个庞大的文件系统中遗失个把文件是常有的事(想像一下大海捞针)。现在由于更大更便宜的磁盘的出现,PC和Apple用户也遇到了这样的问题。为了解决这个问题,系统往往提供一个搜索程序,根据各种条件(比如文件名称,类型,创建时间等等)进行文件搜索。Apple
Macintosh和微软Windows都提供强大、方便、稳定的文件搜索程序。这些搜索程序的设计中考虑到了用户习惯和现代网络。Unix的搜索程序find考虑的则不是用户,而是cpio,一个Unix备份工具。Find没能预见到网络的存在和文件系统的新功能(如符号链接),即使是经历了反复修改,它还是无法很好工作。于是,尽管它对于遗失文件的用户意义重大,find还是不能稳定、正常的工作。
Unix的作者们努力是find跟上系统其他部分的发展,但这并不容易。今天的find有各种特殊的选项用于处理NFS文件系统,符号链接,执行程序,交互式地执行程序,甚至直接使用cpio或cpio-c格式对找到的文件进行归档。Sun公司修改了find,添加了一个后台程序建立系统上每个文件的索引数据库,由于一些奇怪的理由,当你不加任何参数执行"find
filename"时,这个数据库被用于进行搜索,(够安全的,是吧?) 即使有个这么多修修补补,find还是不能正常工作。
例如,csh见到符号链接会顺着走下去,但find不会:csh是伯克利(符号链接的发源地)的家伙们写的,可是find是从AT&T的原始时代开始就有了。就这样,东西方的文化差异激烈地碰撞了,造成了巨大的混乱:
日期: Thu, 28 Jun 1990 18:14 EDT
发信人: pgs@crl.dec.com
主题: more things to hate about Unix (更多恨的理由,就在Unix)
收信人: UNIX-HATERS
这个是我的最爱。我在一个目录下工作,想用find去找另一个目录里的文件,我是这么做的:
po> pwd
/ath/u1/pgs
po> find ~halstead -name "*.trace" -print
po>
看来没有找到。不过别忙,看看这个:
po> cd ~halsead
po> find . -name "*.trace" -print
../learnX/fib-3.trace
../learnX/p20xp20.trace
../learnX/fib-3i.trace
../learnX/fib-5.trace
../learnX/p10xp10.trace
po>
嘿!文件就在那里呀!下次如果你想找一个文件,记住随机到各个目录下转转,说不定你要的文件就藏在那里呢。Unix这个废物。
可怜的Halstead同志的/etc/passwd记录一定是使用了符号链接去指向了真正的目录,所以有的命令工作,有的不工作。
为什么不改改find,也让它顺着符号链接呢?这是因为任何一个指向高一级目录的符号链接都会把find引入死循环。要处理这种情况需要精心的设计和小心的实现,以保证系统不会重复搜索同一个目录。Unix采用了最简单的做法:索性不处理符号链接,让用户自己去看着办吧。
联网系统变得越来越复杂,问题也越来越难以解决了:
日期: Wed, 2 Jan 1991 16:14:27 PST
发信人: Ken Harrenstien <klh@nisc.sri.com>
主题: Why find doesn't find anything (为什么find什么也找不到?)
收信人: UNIX-HATERS
我刚刚发现为什么"find"不再工作了。
尽管"find"的语法非常恶心怪异,我还在勉强用它,以免几小时泡在在迷宫似的文件目录中去寻找文件。
在这个有NFS和符号链接存在的勇敢新世界里,"find"没用了。我们这里的所谓文件系统是由众多文件服务器和符号链接组成的一团乱麻,"find"哪个也不想去处理,甚至连选项也不提供...
结果是大量的搜索路径被无声无息地忽略了。我注意到了这个,是在一个很大的目录下搜索时结果一无所获,最后发现是因为那个目录是个符号链接。
我不想自己去检查每一个交给find的搜索目录——这他妈应该是find的工作。我不想去每次这类情况发生时都要去调查一下系统软件。我不想浪费时间来和SUN或者整个Unix党徒们做斗争。我不想用Unix。恨,恨,恨,恨,恨,恨,恨,恨。
——Ken (感觉好些了,可还是有点恼)
如果想写个复杂一点的shell脚本对找到的文件进行处理,结果往往会很奇怪。这是shell传递参数方式所产生的悲惨后果。
日期: Sat, 12 Dec 92 01:15:52 PST
发信人: Jamie Zawinski <jwz@lucid.com>
主题: Q: what's the opposite of 'find?' A: 'lose'
(问题:'find'的反义词是什么? 答案:丢失)
收信人: UNIX-HATERS
我想找出一个目录下的所有的没有对应.elc文件存在的.el文件。这应该不太难,我用的是find.
不过我错了。
我先是这么干的:
% find . -name '*.el' -exec 'test -f {}c'
find: incomplete statement
噢,我记起来了,它需要个分号。
% find . -name '*.el' -exec 'test -f {}c'\;
find: Can't execute test -f {}c:
No such file or directory
真有你的,竟然没去解析这个命令。
% find . -name '*.el' -exec test -f {}c \;
咦,似乎什么也没做...
% find . -name '*.el' -exec echo test -f {}c \;
test -f c
test -f c
test -f c
test -f c
....
明白了。是shell把大括号给展开了。
% find . -name '*.el' -exec test -f '{}'c \;
test -f {}c
test -f {}c
test -f {}c
test -f {}c
嗯?也许我记错了,{}并不是find使用的那个“替换成这个文件名”的符号。真的么?...
% find . -name '*.el' \
-exec test -f '{}' c \;
test -f ./bytecomp/bytecomp-runtime.el c
test -f ./bytecomp/disass.el c
test -f ./bytecomp/bytecomp.el c
test -f ./bytecomp/byte-optimize.el c
....
喔,原来如此。下面该怎么办呢?我想,我似乎可以试试"sed..."
可我忘记了一个深刻的哲理:“当遇到一个Unix问题的时候,有的人会想‘我懂,我可以试试sed.’这下他们有两个问题去对付了。”
试验了五次,阅读了sed手册两遍,我得到了这个:
% echo foo.el | sed 's/$/c/'
于是:
% find . -name '*.el' \
-exec echo test -f `echo '{}' \
| sed 's/$/c'` \;
test -f c
test -f c
test -f c
....
OK, 看来只能去试试所有shell引用的排列组合了,总会有一款和我意吧?
% find . -name '*.el' \
-exec echo test -f "`echo '{}' \
| sed 's/$/c'`" \;
Variable syntax.
% find . -name '*.el' \
-exec echo test -f '`echo "{}" \
| sed "s/$/c"`' \;
test -f `echo "{}" | sed "s/$/c"`
test -f `echo "{}" | sed "s/$/c"`
test -f `echo "{}" | sed "s/$/c"`
....
嗨,最后一个似乎有戏。我只需要这么干一下:
% find . -name '*.el' \
-exec echo test -f '`echo {} \
| sed "s/$/c"`' \;
test -f `echo {} | sed "s/$/c"`
test -f `echo {} | sed "s/$/c"`
test -f `echo {} | sed "s/$/c"`
....
别急,这是我想要的,可是你为什么不把{}替换成文件名呢?你再仔细瞅瞅,{}两边不是有空格么?你究竟想要什么?
哦,等等。那个反单引号间的引用被当成了一个元素。
或许我能用sed把这个反单引号过滤掉。嗯,没戏。
于是我用了半分钟去想如何能运行"-exec sh -c..."之类的东西,终于出现了曙光,写了一段emcas-lisp代码去做这件事。这不困难,挺快的,而且工作了。
我真高兴。我以为一切都过去了。
今天早上我洗澡的时候突然想到了另一种做法。我试了一次又一次,深深坠入了她的情网,意乱情迷,无法自拔。醉了。只有罕诺塔的Scribe实现曾给过我这样的快感。我仅试了12次就找到了解法。对于每个遍历到的文件它只产生两个进程。这才是Unix之道!
% find . -name '*.el' -print \
| sed 's/^/FOO-/'|\
sed 's/$/; if [ ! -f ${FOO}c]; then \
echo \ $FOO; fi/' | sh
BWAAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH HAAAAHH!!!!
—Jamie
OK, 在下水道里玩捉迷藏挺有意思的吧?第8章就在欢声笑语中这么结束了。下回书我们就要开始编程了,还记得小时候那个可爱迷人的护士阿姨是怎么对你说的么?
“牛牛别怕,不疼的。”
|