# csh、管道和 find：强力工具，大力出奇迹

![](/files/4RVxiHe2Jk6GwrY142kx)

***

![](/files/UKQtt3fK163vKFGXvZot)

> 我对所有操作系统都天然的感到反感，尤其是那些缺乏规划，以至于不得不将所有命令命名为消化系统发出的声音（如 awk、grep、fsck、nroff）。
>
> ——佚名

Unix 的“动力工具”比喻是个谬论。那不过是个口号，背后隐藏着 Unix 那些晦涩难懂、拼凑而成的命令和临时工具。真正的动力工具可以在很少额外努力和指导的情况下，放大使用者的力量。会用螺丝刀和电钻的人都能使用电动螺丝刀和电钻。使用者不需要理解电力、电机、扭矩、磁力、散热或维护等原理，只需插上电源，戴上护目镜，然后扣动扳机即可。大多数人甚至连护目镜都省了。在五金店，很难找到致命设计缺陷的动力工具：大多数设计糟糕的动力工具要么根本没能上市，要么因诉讼而被迫下架，制造商也受到惩罚。

Unix 的动力工具则不符合这一模式。与其设计者最初期望的简单、单一用途工具不同，如今的 Unix 工具功能过剩、设计过度、过于复杂。例如，`ls` 这个程序曾经只是列出文件，现在却有超过 18 种不同选项，控制从排序顺序到打印列数等各种功能——这些功能本应由其他工具处理（曾经确实是如此）。find 命令除了找文件，还能输出 cpio 格式的文件，这本来可以通过 Unix 著名的管道连接两个命令轻松完成。现在，Unix 的“动力电钻”可能会有 20 个旋钮和开关，使用非标准插头，需要手动绕线电机线圈，而且还不支持 3/8 寸或 7/8 寸钻头（尽管这会写在说明书的 BUGS 部分）。

与五金店里的工具不同，大多数 Unix 动力工具都有缺陷（有时对文件来说致命）：例如 tar，有其任意的路径名长度限制（100 个字符）；又比如 Unix 调试器，在崩溃时会用自己的“core”文件覆盖掉你的“core”文件。

Unix 的“动力工具”更像是快速且高效切断操作者手指的开关刀。

## shell 游戏

Unix 的发明者有个伟大的想法：让命令处理器只是另一个用户级程序。如果用户不喜欢默认的命令处理器，他们可以自己编写。更重要的是，shell 可以进化，理应变得更强大、更灵活、更易用。

这是个绝佳的想法，但它却适得其反。功能的缓慢积累导致了混乱。因为这些 shell 不是设计出来的，而是进化而来的，所有编程语言的通病——既有程序基础——对它们冲击更大。只要一个功能被加入到 shell 中，就会有人写依赖该功能的 shell 脚本，从而保证该功能的延续。糟糕的想法和功能不会消亡。

结果就是今天大量不完整、不兼容的 shell（以下是各 shell 描述，摘自它们各自的 man 页面）：

|      |                           |
| ---- | ------------------------- |
| sh   | 一种命令编程语言，可以执行从终端或文件读取的命令。 |
| jsh  | 与 sh 相同，但启用了类似 csh 的作业控制。 |
| csh  | 具有类似 C 语言语法的 shell。       |
| tcsh | 带有 emacs 风格编辑功能的 csh。     |
| ksh  | KornShell，另一种命令和编程语言。     |
| zsh  | Z Shell。                  |
| bash | GNU 又一款 Bourne SHell。     |

五金店里有三四家公司生产的螺丝刀或锯子，它们的使用方式都差不多。而典型的 Unix `/bin` 或 `/usr/bin` 目录里却有上百种不同的程序，由数十个自负的程序员编写，每个程序都有自己独特的语法、操作范式、使用规则（这个是过滤器，这个作用于临时文件等等）、不同的选项指定策略以及一套不同的限制条件。以 grep 程序及其“堂兄”fgrep 和 egrep 为例。哪个最快？ 为什么这三个程序的选项不同，并且对“正则表达式”这一短语实现了略有差异的语义？为什么不干脆有一个程序结合这三者的所有功能？到底谁在掌控这一切？

当你掌握了这些不同命令之间的差异，并将这些晦涩内容记忆于心后，你仍然会经常感到震惊和惊讶。

以下几个例子或许能说明问题。

### Shell 崩溃

以下消息发布在哥伦比亚大学一块编译器课程的电子公告板上。

> 主题：相关的 Unix 漏洞
>
> 1991 年 10 月 11 日
>
> W4115x 的同学们——
>
> 既然我们谈到了激活记录、参数传递和调用约定，你们知道在 C shell 中输入：
>
> ```sh
> !xxx%s%s%s%s%s%s%s%s
> ```
>
> 会立即导致它崩溃吗？你知道为什么吗？
>
> 思考的问题：
>
> * 当你输入 `!xxx` 时，shell 会做什么？
> * 当你输入 `!xxx%s%s%s%s%s%s%s%s` 时，它对你的输入一定做了什么？
> * 为什么这会导致 shell 崩溃？
> * 你如何（相当容易地）重写 shell 的相关部分，以避免这个问题？
>
> 最重要的是：
>
> * 你觉得合理吗？你（没错，就是你！）竟然能用 21 个按键让可能成为未来操作系统的 Unix 崩溃？

试试看。按照 Unix 的设计，shell 崩溃会导致你所有的进程被终止，并且你被登出。其他操作系统会捕捉无效的内存引用，然后跳转到调试器里。但 Unix 不会。

或许这就是为什么 Unix shell 不允许你通过加载新的目标代码到它们的内存映像中，或者调用其他程序中的目标代码来扩展它们。那样做太危险了。一不小心——砰——你就被登出了。对程序员错误零容忍。

### 元语法动物园

C Shell 的元语法操作符动物园导致了许多引用问题和普遍的混乱。元语法操作符会在命令执行之前对命令进行转换。我们称这些操作符为元语法操作符，是因为它们不是命令语法的一部分，而是作用于命令本身的操作符。元语法操作符（有时也称为转义操作符）对大多数程序员来说都很熟悉。例如，C 语言字符串中的反斜杠字符（`\`）就是一种元语法操作符；它不表示自身，而是对后续字符进行某种操作。当你想让元语法操作符表示它自己时，必须使用一种引用机制，告诉系统将该操作符解释为普通文本。比如，回到 C 语言字符串的例子，要表示字符串中的反斜杠字符，就必须写成 `\`。

在 C Shell 中，简单的引用机制几乎不起作用，因为 shell 与它代表用户调用的程序之间没有任何约定。例如，想想下面这个简单命令：

```sh
grep 字符串 文件名:
```

字符串参数包含由 grep 定义的字符，比如 `?`、`[` 和 `]`，这些字符对 shell 来说是元语法的。这意味着你可能需要对它们进行引用，但这也取决于你使用的 shell 以及环境变量的设置。

搜索包含句点或以连字符开头的模式会使问题更加复杂。务必正确引用你的元字符。遗憾的是，就像模式匹配一样，操作系统中存在多种不兼容的引用规范。

C Shell 的元语法动物园里有七个不同家族的元语法操作符。因为这个“动物园”是在一段时间内逐渐“填充”起来的，而且“笼子”是锡制而不是钢制的，这些“居民”往往会互相冲突。对 shell 命令行的七种不同转换是：

|           |                     |
| --------- | ------------------- |
| 别名定义与取消别名 | `alias` 和 `unalias` |
| 命令输出替换    | \`                  |
| 文件名替换     | `*`、`?`、`[]`        |
| 历史命令替换    | `!`、`^`             |
| 变量替换      | `$`、`set`、`unset`   |
| 进程替换      | `%`                 |
| 引号        | `'`、`"`             |

由于这种“设计”，问号字符注定只能用于单字符匹配：它永远不能在命令行上用作帮助符号，因为它从不传递给用户的程序，因为 Unix 要求这个元语法操作符必须由 shell 解释。

如果这七类不同的元语法字符遵循一个逻辑的操作顺序，并且它们的替换规则被统一应用，那也不算太糟糕。但事实并非如此，它们既没有统一的顺序，也没有统一的规则。

> 日期：1990 年 5 月 7 日 星期一 18:00:27 -0700
>
> 发件人：Andy Beals <bandy@lll-crg.llnl.gov>
>
> 主题：回复：今天的抱怨：`fg %3`
>
> 收件人：Unix 痛恨者
>
> 不仅可以说 `%emacs`，甚至 `%e` 来重启一个作业（如果它是唯一匹配的），你也可以用 `%?foo` 来匹配命令行中包含子串“foo”的作业。当然，`!ema` 和 `!?foo` 也可以用于历史替换。但是，加州大学伯克利分校的笨蛋们没有让 `!?foo` 识别后续的编辑命令，所以那个脑残的 c shell 无法识别像
>
> ```sh
> !?foo:s/foo/bar&/:p
> ```
>
> 这样的命令，这让输入变得痛苦。
>
> 真有那么难往前扫描找那个编辑字符吗？

这些内容即使对 Unix“专家”来说也有些令人困惑。以 Milt Epstein 为例，他想写一个 shell 脚本，能够准确获取正在输入的命令行内容，而不经过 shell 的任何预处理。他发现这并不容易，因为 shell 为程序“代劳”了太多工作。要避免 shell 的处理，需要使用一种极为复杂晦涩的魔法般语法，连大多数专家也难以理解。这就是 Unix 的典型特征——使看似简单的事情变得异常困难，仅仅因为当初设计 Unix 时并未考虑到这些需求。

> 日期：1991 年 8 月 19 日 15:26:00 GMT
>
> 发件人：<Dan_Jacobson@att.com>
>
> 主题：/bin/sh 系列 shell 脚本中的 `${1+“$@”}`
>
> 新 sgroups：comp.emacs, gnu.emacs.help, comp.unix.shell
>
> > 1991 年 8 月 18 日 星期日 18:21:58 -0500，\
> > Milt Epstein <epstein@suna0.cs.uiuc.edu> 说：
>
> Milt> `${1+“$@”}` 是什么意思？我确定它是
>
> Milt> 用来读取剩下的命令行参数的，但
>
> Milt> 我不太确定具体含义。
>
> 这是在 `/bin/sh` 系列 shell 脚本中精确复现命令行参数的方法。
>
> 它说：“如果至少有一个参数（`${1+}`），那么就替换所有参数（`"$@"`），保持每个参数内的所有空格等。
>
> 如果我们只用 `"$@"`，当没有调用参数时，它会被替换成 （一个空参数），但我们想要的是在这种情况下不产生任何参数，而不是一个空字符串。
>
> 为什么不用 `"$*"` 等呢？摘自 sh(1) 手册页：”
>
> > 在一对双引号（“”）内，会发生参数和命令替换，shell 会对结果加引号以避免空白被解释和文件名生成。如果 $\* 在双引号内，位置参数会被替换并加引号，用加引号的空格分隔（“$1 $2 …”）；但是，如果 $@ 在双引号内，位置参数会被替换并加引号，用不加引号的空格分隔（“$1” “$2” …）。\
> > 我认为 `${1+“$@”}` 的可移植性可以一直追溯到“第七版 Unix”。

哇！竟然可以追溯到第 7 版。

### Shell 命令“chdir”不能用

Bug 和明显的古怪行为，是 Unix 由众多作者长期演进的结果，这些作者各自尝试将操作系统引向不同的方向，却无人停下来考虑彼此之间的影响。

> 日期：1990 年 5 月 7 日 星期一 22:58:58 EDT
>
> 发件人：Alan Bawden <alan@ai.mit.edu>
>
> 主题：cd . . ：我没有编造这个
>
> 收件人：Unix 痛恨者
>
> 有什么命令比“cd”更直接明了的吗？我们来看一个简单的例子：“cd ftp。”\
> 如果我当前目录是 `/home/ar/alan`，并且它有一个名为“ftp”的子目录，那么这个子目录就成为了我的新当前目录。\
> 所以现在我就在 `/home/ar/alan/ftp`。很简单。
>
> 你们都知道“.”和“..”吧？每个目录总有两个条目：一个叫“.”，指向目录本身；一个叫“..”，指向该目录的父目录。所以在我们的例子里，我可以通过输入 `cd ..` 回到 `/home/ar/alan`。
>
> 现在假设“ftp”是一个符号链接（请耐心听我说完）。假设它指向目录 `/com/ftp/pub/alan`。那么执行“cd ftp”后，我就进入了 `/com/ftp/pub/alan`。
>
> 像所有目录一样，/com/ftp/pub/alan 也有一个条目叫“..”，它指向它的上级目录：`/com/ftp/pub`。假设我想接下来去那里，我输入：
>
> ```sh
> % cd ..
> ```
>
> 猜猜看？我回到了 `/home/ar/alan`！shell 中的某个地方（显然我们 AI 实验室用的是叫“tcsh”的 shell）记住了为了进入 `/com/ftp/pub/alan` 而追踪了符号链接，cd 命令猜测我宁愿回到包含该链接的目录。如果我真的想访问 `/com/ftp/pub`，我应该输入 `cd ./..`。

## Shell 编程

Shell 程序员和《侏罗纪公园》里的恐龙克隆者有许多相似之处。他们没有所有需要的“基因片段”，于是用随机的基因材料来填补缺失的部分。尽管他们充满自信且能力出众，但并不总能完全控制自己的“创造物”。

理论上，shell 程序相比用 C 语言写的程序有一个巨大优势：shell 程序具有可移植性。也就是说，用 shell“编程语言”写的程序可以运行在许多不同版本的 Unix 系统上，运行于各种不同的计算机架构，因为 shell 是解释执行程序，而不是将其编译成机器码。而且，sh，作为标准的 Unix shell，自 1977 年起就成为 Unix 的核心组成部分，因此几乎可以在任何机器上找到它。

现在，我们用一个示例来检验这一理论：写一个 shell 脚本，使用 file 命令打印当前目录中每个文件的名称和类型。

> 日期：1992 年 4 月 24 日 星期五 14:45:48 EDT
>
> 发件人：Stephen Gildea <gildea@expo.lcs.mit.edu>
>
> 主题：简单的 Shell 编程
>
> 收件人：Unix 痛恨者
>
> 大家好，今天我们将学习用“sh”编程。\
> “sh”shell 是一个简单且多功能的程序，但我们先从一个基本例子开始：\
> 打印目录中所有文件的类型。\
> （后面有人发出了声音！那些对 shell 有点了解并觉得无聊的，可以写“在远程机器上启动一个 X11 客户端”来加分。暂时先别吭声！）
>
> 在学习 sh 的同时，我们也希望写出的程序健壮、可移植且优雅。\
> 我假设大家都看过相关的手册页，所以下面的内容应该很明显：
>
> ```sh
> file *
> ```
>
> 很好，不是吗？简单问题的简单解决方案；星号匹配目录中所有文件。\
> 不过，也不完全是这样。以点开头的文件被认为是不重要的，而 \* 不会匹配它们。\
> 可能没有这样的文件，但为了健壮，我们用“ls”并加一个特殊选项：
>
> ```sh
> for file in `ls -A`
> do
> file $file
> done
> ```
>
> 这样：优雅且健壮……哎呀，有些系统的“ls”不支持“-A”选项。没关系，我们用 -a 代替，然后过滤掉 . 和 .. 文件：
>
> ```sh
> for file in `ls -a`
> do
> if [ $file != . -a $file != .. ]
> then
> file $file
> fi
> done
> ```
>
> 没那么优雅，但至少健壮且可移植。什么？“ls -a”也不是处处可用？没问题，我们用“ls -f”替代，速度也快。\
> 我希望这些都能从手册页里看出来。
>
> 嗯，似乎也不算太健壮。Unix 文件名可以包含任何字符（除了斜杠），文件名中有空格会打破这个脚本，因为 shell 会把它当成两个文件名解析。\
> 这不难解决。我们只需改变 IFS，不包含空格（或者同时去掉制表符），并且小心地给变量加引号（不过也不能多或少），比如这样：
>
> ```sh
> IFS='
> '
> for file in `ls -f`
> do
> if [ "$file" != . -a "$file" != .. ]
> then
> file "$file"
> fi
> done
> ```
>
> 你们中有些机灵的人可能已经注意到，问题虽变小了，但没消除，因为换行符也是文件名中合法字符，它仍然在 IFS 里。\
> 脚本失去了一些简洁性，是时候重新评估我们的做法了。\
> 如果去掉“ls”，就不用担心解析它的输出了。那下面这样如何？
>
> ```sh
> for file in .* *
> do
> if [ "$file" != . -a "$file" != .. ]
> then
> file "$file"
> fi
> done
> ```
>
> 看起来不错。处理了点文件和带有不可打印字符的文件。\
> 我们不断往测试目录里添加各种奇怪命名的文件，这个脚本依旧有效。\
> 但有人在空目录试用时，\* 模式会产生“No such file”的错误。我们可以加个检测……
>
> ……到这里，我的邮件可能对你们中一些用 uucp 邮件系统的人来说太长了，所以我得先写到这里，剩下的 Bug 修正留给读者作为练习。
>
> Stephen

还有一个重大问题，从一开始我们一直在回避不提。那就是 Unix 的 file 程序根本就不靠谱。

> 日期：1992 年 4 月 25 日 星期六 17:33:12 EDT
>
> 发件人：Alan Bawden <Alan@lcs.mit.edu>
>
> 主题：简单 Shell 编程
>
> 收件人：Unix 痛恨者
>
> 哇！等等，回头。你真的打算用 `file` 程序？想找乐子的人应该立刻停下来，找一台 Unix 机器，在一个杂七杂八的目录里试试输入 `file *`。
>
> 例如，我刚刚对一个充满 C 源代码的目录运行了 `file` —— 以下是部分结果：
>
> ```sh
> arith.c: c program text  
> binshow.c: c program text  
> bintxt.c: c program text
> ```
>
> 目前还不错。但然后：
>
> ```sh
> crc.c: ascii text
> ```
>
> 你看，`file` 并没有看文件名里的 `.c`，而是根据文件内容用一些启发式方法来判断。显然 `crc.c` 看起来不像 C 代码——尽管对我来说它肯定是。
>
> ```sh
> gencrc.c.~4~: ascii text  
> gencrc.c: c program text
> ```
>
> 我猜我在版本 4 之后改了什么，让 `gencrc.c` 看起来更像 C 代码了……
>
> ```sh
> tcfs.h.~1~: c program text  
> tcfs.h: ascii text
> ```
>
> 而 `tcfs.h` 在版本 1 之后看起来就不像 C 代码了。
>
> ```sh
> time.h: English text
> ```
>
> 没错，time.h 看起来像英文，而不仅仅是 ascii。我想知道 `file` 是否有识别西班牙语或法语的规则？（顺便说一句，你典型的 TeX 源文件会被分类为“ascii text”而非“English text”，不过我扯远了……）
>
> ```sh
> words.h.~1~: ascii text  
> words.h: English text
> ```
>
> 也许我在版本 1 之后给 words.h 加了一些注释？
>
> 但我留的最好的是：
>
> ```sh
> arc.h: shell commands  
> Makefile: [nt]roff, tbl, or eqn input text
> ```
>
> 都完全错了。我真想知道如果我试图按 `file` 程序给出的类型去使用它们，会发生什么？
>
> ——Alan

### Shell 变量不起作用

Alan 的情况还能更糟，比如他可能正在尝试使用 shell 变量。

正如我们之前提到的，sh 和 csh 对 shell 变量的实现略有不同。这本来不算太糟糕，但 shell 变量的语义——何时定义、变更操作的原子性以及其他行为——在很大程度上都是未文档化且定义模糊的。shell 变量经常表现出奇怪且违反直觉的行为，只有经过大量实验才能理解。

> 日期：1991 年 11 月 14 日 星期四 11:46:21 PST
>
> 发件人：Stanley’s Tool Works <lanning@parc.xerox.com>
>
> 主题：你每天都会学到新东西
>
> 收件人：Unix 痛恨者
>
> 运行这个脚本：
>
> ```sh
> #!/bin/csh
> unset foo
> if ( ! $?foo ) then
> echo foo was unset
> else if ( "$foo" = "You lose" ) then
> echo $foo
> endif
> ```
>
> 会产生这个错误：
>
> ```sh
> foo: Undefined variable.  
> ```
>
> 要让脚本“做正确的事情”，你必须写成这样：
>
> ```sh
> #!/bin/csh
> unset foo
> if ( ! $?foo ) then
> echo foo was unset
> set foo
> else if ( "$foo" = "You lose" ) then
> echo $foo
> endif
> ```
>
> \[注意在发现变量未设置后需要执行 `set foo`。]
>
> 清楚了吗？

### 错误代码与错误检查

我们的编程示例忽略了 file 命令如何向 shell 脚本报告错误。其实，它根本不报告错误。错误被忽略了。这种行为不是疏忽：大多数 Unix shell 脚本（以及其他程序）都会忽略它们调用的程序可能产生的错误代码。这种行为可以理解，因为没有统一的标准规范来指定程序应该返回哪些代码来表示错误。

或许错误代码被普遍忽略的原因，是因为在用户在 shell 提示符下输入命令时，错误代码不会被显示。错误代码和错误检查在 Unix 规范中几乎不存在，导致许多程序甚至根本不报告错误。

> 日期：1992 年 10 月 6 日 星期二 08:44:17 PDT
>
> 发件人：Bjorn Freeman-Benson <bnfb@ursamajor.uvic.ca>
>
> 主题：在 Unix 世界里总是好消息
>
> 收件人：Unix 痛恨者
>
> 试想这个 tar 程序。像所有 Unix“工具”（我用这个词很宽泛）一样，它以奇怪且独特的方式工作。比如，tar 是一个充满正能量的程序，因此它坚信不会发生任何坏事，所以它从不返回错误状态。实际上，即使它向屏幕打印错误信息，它仍然报告“好消息”，即状态码为 0。
>
> 在 shell 脚本中试试这个：
>
> ```sh
> tar cf temp.tar no.such.file
> if( $status == 0 ) echo "Good news! No error."
> ```
>
> 你会得到：
>
> ```sh
> tar: no.such.file: No such file or directory  
> Good news! No error.  
> ```
>
> 我知道——我本不该期待任何一致、有用、文档完善、快速，甚至能用的东西……
>
> Bjorn

## 管道

> 我的 Unix 判断是我自己的。大约六年前（当我第一次拿到工作站时），我花了大量时间学习 Unix，学得相当不错。幸运的是，那些垃圾大部分现在已经从记忆中消失了。然而，自从加入这个讨论后，许多 Unix 支持者给我发来了各种示例，想“证明”Unix 的强大。这些例子确实足以让我回想起来：它们都做了一些琐碎或无用的事情，而且都是以非常晦涩难懂的方式完成的。
>
> 有人在网上发帖说他从一个 shell 脚本中“顿悟”了（该脚本用了四个命令，整个脚本看起来像是乱码），它把所有 '.pas' 文件的后缀改成了“.p”。我把我的宗教狂喜留给比重命名文件更重要的事情。事实上，这就是我对 Unix 工具的记忆——你花了所有时间学着做复杂且奇怪的事，但最终并没有多么令人印象深刻。我决定还是学点真正能干活的东西。
>
> ——Jim Giles
>
> 洛斯阿拉莫斯国家实验室

Unix 爱好者坚信管道的纯粹、美德与美感。他们颂扬管道机制，认为它比任何其他特性都更能代表 Unix 的本质。Unix 爱好者一遍又一遍地吟唱道：“管道使得可以用简单程序构建复杂程序。管道让程序可以被以非预期和未计划的方式使用。管道允许简单的实现。”不幸的是，念咒语对 Unix 并没有比对哈里·克里希纳教徒更大的帮助。

管道确实有一些优点。构建复杂系统需要模块化和抽象。这一真理是计算机科学的信条。拥有更好的工具将较小的系统组合成较大的系统，就越可能得到成功且可维护的结果。管道是一种结构化工具，因此具有一定价值。

以下是个管道示例 ：

```sh
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
```

很清楚，对吧？这个流水线查看用户的邮箱并判断他们订阅了哪些邮件列表，（嗯，差不多）。像大多数流水线一样，这个在某些情况下也会以神秘的方式失败。

确实，虽然管道在某些时候很有用，但它们在程序之间传递信息的方式——通过标准输入和标准输出传递文本——限制了它们的实用性 。

很清楚，是吧？这个管道会查看用户的邮箱，并判断他们订阅了哪些邮件列表（嗯，差不多）。像大多数管道一样，这个在某些情况下也会以神秘的方式失败。

确实，虽然管道有时很有用，但它们在程序之间的通信方式——文本通过标准输入和标准输出传递——限制了它们的实用性。首先，信息流只能是单向的。进程无法通过 shell 管道实现双向通信。其次，管道不支持任何形式的抽象。发送和接收的进程必须使用字节流。任何比字节更复杂的对象在发送之前都必须被转换成字符串形式的字节流，而且接收端还必须知道如何重组它。这意味着你不能发送一个对象及其所需的类定义代码。你不能发送指针到另一个进程的地址空间。你不能发送文件句柄、TCP 连接，或访问特定文件或资源的权限。

冒着听起来像是银河间绝望梦想守护者的风险，我们提出一个更正确的模型——过程调用（无论是本地还是远程），并在支持一等结构体（C 在青春期时才获得）和函数组合的语言中实现。管道适合用来搞些简单的“黑客式”操作，比如在程序之间传递简单的文本流，但不适合用来构建健壮的软件。例如，一篇关于管道的早期论文展示了如何通过串接几个简单程序来实现拼写检查器。这在简洁性上堪称绝技，但却是检查（更别说纠正）文档拼写的糟糕方式。

Shell 脚本中的管道是为了微小的 hack 而优化的。它们赋予程序员“乱搭”简单解决方案的能力，而这些方案通常非常脆弱。这是因为管道在两个程序之间制造了依赖：你不能改变一个程序的输出格式，而不去修改另一个程序的输入处理代码。

大多数程序都会演化：最开始是构思程序的需求，然后是把程序内部“东拼西凑”地组合起来，最后才有人来写程序的输出部分。而管道会阻止这一过程：一旦有人开始把一个半成品的 Unix 工具塞进管道，它的输出格式就被“钉死”了，不管它多么含糊、不标准或低效。

管道并不是程序通信的终极方案。我们最喜欢的那本热爱 Unix 的书是这样说 Macintosh 的（它没有管道功能）：

> 摘自：*Life with Unix*，作者 Libes 和 Ressler
>
> 与之截然相反的是 Macintosh 模型。这个系统不处理字符流。数据文件是极高层次的，通常假设它们是专属于某个应用程序的。你上一次在 Mac 上把一个程序的输出通过管道传给另一个程序是什么时候？（祝你好运找到那个管道符号。）程序是整体的，这样才能完全理解你正在做的事情。你不会把 MacFoo 和 MacBar 连接起来使用。

是啊，那些可怜的 Mac 用户。他们的日子可真难过。因为他们不能在程序之间通过管道传输字节流，那他们怎么能把绘图程序里的插图粘贴到最新的备忘录里，并让文本环绕图像呢？他们又怎么能把电子表格嵌入备忘录？而且，这种用户又怎么能指望文档中的改动能被自动追踪？他们当然不该指望自己拼拼凑凑做出来的备忘录能通过电子邮件横跨全国被无缝接收、编辑，然后毫发无损地传回给自己。我们实在无法想象他们是怎么在过去 10 年里一直透明地使用这些不同的程序，而且这些程序还都能正常工作——全都没用管道。

你上一次觉得 Unix 工作站像 Mac 一样有用是什么时候？你上一次在 Unix 上运行来自不同公司（甚至同一公司不同部门）的程序，并让它们真正互通是什么时候？如果有的话，那也是某个 Mac 软件厂商吐血把程序移植到了 Unix 上，并试图让 Unix 看起来更像 Mac。

Unix 和 Macintosh 操作系统之间的根本区别在于：Unix 是为了取悦程序员而设计的，而 Mac 是为了取悦用户而设计的。（至于 Windows，那是为了取悦会计师而设计的——但那是另一回事了。）

研究表明，管道和重定向难用，并不是因为它们在概念上有什么问题，而是因为存在一堆随意且反直觉的限制。有文献记录表明，只有那些沉浸在 Unix 世界里的人——而不是普通用户——才能欣赏或使用管道的“强大功能”。

> 日期：1991 年 1 月 31 日 星期四 14:29:42 EST
>
> 发件人：Jim Davis <jrd@media-lab.media.mit.edu>
>
> 收件人：Unix 痛恨者
>
> 主题：专业知识
>
> 今天早上我读了一篇发表在 *《人机交互杂志》*（*Journal of Human-Computer Interaction*）上的文章，题为《一种计算机操作系统中的专业知识》（*Expertise in a Computer Operating System*），作者是 Stephanie M. Doane 及另外两人。你猜她研究的是哪个操作系统？Doane 研究了 Unix 初学者、中级用户和专家的知识和表现。以下是几句引用：
>
> > “只有专家能够成功地构造需要使用 Unix 独特功能（例如管道和其他重定向符号）的复合命令。”
>
> 换句话说，Unix 中每一个“新”功能（也就是不是从其他操作系统拙劣或退化地抄来的）都如此晦涩，以至于只有经过多年隐晦学习与练习后才可能使用。
>
> > “这个发现多少有些令人吃惊，因为这些是 Unix 的基本设计特性，而且这些功能是在基础课程中教授的。”
>
> 她还提到了 S. W. Draper 的研究，Doane 引述他的话说：
>
> > “并不存在所谓天真的意义上的 Unix 专家——即那种知识穷尽、无需再学的人。”
>
> 在这里我必须反对。很明显，试图掌握 Unix 的荒谬之处足以让任何人身心俱疲。

有些程序甚至特意确保管道和文件重定向的行为有所不同：

> 发件人：Leigh L. Klotz <klotz@adoc.xerox.com>
>
> 收件人：Unix 痛恨者
>
> 主题：`|` 与 \`<\`\`
>
> 日期：1992 年 10 月 8 日 星期四 11:37:14 PDT
>
> ```sh
> 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 和 Microsoft Windows 拥有功能强大、相对易用且极其可靠的文件定位器。这些文件查找工具是在考虑了人类用户和现代网络环境的基础上设计的。

而 Unix 的文件查找程序 find 并不是为人类用户设计的，而是为 cpio —— 一个 Unix 备份工具设计的。find 无法预见网络环境或文件系统的增强特性，比如符号链接；即便经过大量修改，它依然不能很好地处理这些情况。因此，尽管对丢失文件的用户来说 find 很重要，但它的表现并不可靠，也不够可预测。

Unix 的作者们试图让 find 跟上 Unix 系统的发展，但这是一个艰难的任务。如今的 find 增加了针对 NFS 文件系统、符号链接、执行程序、在用户输入“y”时有条件地执行程序，甚至直接以 cpio 或 cpio-c 格式归档查找到的文件的特殊标志。Sun Microsystems 对 find 进行了修改，增加了一个后台守护进程，构建整个 Unix 文件系统中每个文件的数据库——出于某种奇怪的原因，当你输入“find filename”而不加其他参数时，find 命令会搜索这个数据库。（这简直是安全漏洞！）

尽管进行了这些折衷修改，find 仍然不能正常工作。

例如，csh 会跟踪符号链接，而 find 不会：csh 是在 Berkeley 编写的（符号链接在那里实现），而 find 的设计可以追溯到 AT\&T 的时代，那时还没有符号链接。有时，东西方文化的冲突会导致大混乱。

这是我最喜欢吐槽的之一。我在某个目录下，想用 find 在另一个目录里搜索文件，于是我执行：

> 日期：1990 年 6 月 28 日 星期四 18:14 EDT
>
> 发件人：<pgs@crl.dec.com>
>
> 主题：关于 Unix 更多令人讨厌的地方
>
> 收件人：Unix 痛恨者
>
> ```sh
> po> pwd
> /ath/u1/pgs
> po> find ~halstead -name "*.trace" -print
> po>
> ```
>
> 结果什么文件都没找到。但接下来：
>
> ```sh
> po> cd ~halstead
> ```
>
> ```sh
> po> find . -name "*.trace" -print
> ./learnX/fib-3.trace
> ./learnX/p20xp20.trace
> ./learnX/fib-3i.trace
> ./learnX/fib-5.trace
> ./learnX/p10xp10.trace
> po>
> ```
>
> 嗨，现在文件出现了！只要记得先 `cd` 到某些随机目录，`find` 才能找到里面的东西。真是 Unix 的一大糟糕之处。

可怜的 Halstead 他的家目录在 `/etc/passwd` 中的条目指向了一个符号链接，而该符号链接又指向他的真实目录，所以有些命令对他有效，有些则无效。

为什么不修改 `find` 让它跟踪符号链接呢？因为如果这样做，任何指向树结构上层目录的符号链接都会让 find 陷入无限循环。要设计一个不会反复扫描同一目录的系统，需要仔细的预见和真正的编程。简单粗暴的 Unix 解决办法就是干脆不跟踪符号链接，让用户自己去应付后果。

随着网络系统变得越来越复杂，这些问题也变得越来越难解决：

> 日期：1991 年 1 月 2 日 星期三 16:14:27 PST
>
> 发件人：Ken Harrenstien <klh@nisc.sri.com>
>
> 主题：为什么 find 找不到任何东西
>
> 收件人：Unix 痛恨者
>
> 我刚弄明白为什么“find”程序对我来说不再管用了。尽管语法相当笨拙和难用，我长期依赖它来避免花几个小时徒劳地在复杂的目录层级中上下翻找我知道存在的程序源码（当然，每台机器的位置都不同）。
>
> 事实证明，在这个充满 NFS 和符号链接的新世界里， “find”正变得一文不值。我们这里所谓的文件系统，就是一个把几个不同的文件服务器和大量符号链接杂乱无章地混合在一起的巨大意大利面堆，程序根本不去跟踪这些链接。甚至没有一个选项能让它这样做……结果是搜索空间中巨大的部分被默默地排除在外。我终于意识到这一点，是因为我请求搜索一个相当大的目录时什么都没找到（这并不完全令人惊讶，但它确实没花多少时间），调查后发现那个目录其实是个符号链接，指向别的地方。
>
> 我不想在给 find 的目录树里每个目录都自己去检查——这应该是 find 的工作，见鬼！我不想每次遇到这种缺陷都去改系统软件。我不想浪费时间跟 SUN 或整个 Unix 界的弱鸡们斗争。我不想用 Unix。恨，恨，恨，恨，恨，恨，恨。
>
> —Ken（感觉稍微好些，但仍然生气）

编写一个复杂的 shell 脚本，实际对找到的文件进行操作，会产生奇怪的结果，这悲哀地反映了 shell 传递参数给命令的方法。

> 日期：1992 年 12 月 12 日 星期六 01:15:52 PST
>
> 发件人：Jamie Zawinski <jwz@lucid.com>
>
> 主题：问：‘find’的反义词是什么？答：‘lose’。
>
> 收件人：Unix 痛恨者
>
> 我想在一个目录树中找到所有没有对应 `.elc` 文件的 `.el` 文件。这应该很简单。于是我尝试用 `find`。我当时到底在想什么。
>
> 首先我试了：
>
> ```sh
> % find . -name ’*.el’ -exec ’test -f {}c’  
> find: incomplete statement  
> ```
>
> 哦对，记得它需要分号。
>
> ```sh
> % find . -name ’*.el’ -exec ’test -f {}c’ \;  
> find: Can’t execute test -f {}c:  
> No such file or directory  
> ```
>
> 太好了。它并不像其他大多数命令那样对这个命令进行分词处理。
>
> ```sh
> % find . -name ’*.el’ -exec test -f {}c \;  
> ```
>
> 嗯，什么反应都没有……
>
> ```sh
> % find . -name ’*.el’ -exec echo test -f {}c \;  
> test -f c
> test -f c
> test -f c
> test -f c
> ...
> ```
>
> 太棒了。shell 认为花括号是可以省略的。
>
> ```sh
> % find . -name ’*.el’ -exec echo test -f ’{}’c \;  
> test -f {}c
> test -f {}c
> test -f {}c
> test -f {}c
> ...
> ```
>
> 咦？也许我记错了，{} 并不是真正的 find 用来“替换文件名”的神奇标记。或者也许……
>
> ```sh
> % find . -name ’*.el’ \  
> -exec echo 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 的 man 页后，我终于写出了：
>
> ```sh
> % echo foo.el | sed ’s/$/c/’  
> foo.elc  
> ```
>
> 然后是：
>
> ```sh
> % find . -name ’*.el’ \  
> -exec echo test -f `echo ’{}’ \  
> | sed ’s/$/c/’` \;  
> test -f c
> test -f c
> test -f c
> ...
> ```
>
> 好吧，我们再试试其他 shell 引号组合，直到找到能用的。
>
> ```sh
> % find . -name ’*.el’ \  
> -exec echo test -f "`echo ’{}’ |\  
> sed ’s/$/c/’`" \;  
> ```
>
> Variable syntax.
>
> ```sh
> % 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/"`
> ...
> ```
>
> 嘿，最后那个已经挺接近了。现在我只需要……
>
> ```sh
> % 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/"`
> ...
> ```
>
> 等等，那正是我想要的，但为什么它不把文件名替换成 `{}` 呢？？？看，这里 `{}` 两边都有空格，你还想怎么样，是不是要在满月下献祭一只山羊的血？哦，等等。那个反引号包裹的部分是一个整体的 token。
>
> 也许我可以把这个反引号包裹的内容通过 sed 过滤。嗯，不行。
>
> 所以我花了半分钟想怎么用 `-exec sh -c …` 来做这件事，然后我终于开窍了，写了一些 emacs-lisp 代码来实现。它很简单，很快，而且有效。我很开心，以为事情到此结束了。
>
> 但是今天早上洗澡时我想出了另一种方法。我停不下来，一试再试，这任务的顽固让我着迷。它有点像 Scribe 实现的河内塔游戏的吸引力。我只用了 12 次尝试就做对了。它在遍历目录树里的每个文件时只产生两个进程。这才是 Unix 的风格！
>
> ```sh
> % find . -name ’*.el’ -print \  
> | sed ’s/^/FOO=/’|\  
> sed ’s/$/; if [ ! -f \ ${FOO}c ]; then \  
> echo \ $FOO ; fi/’ | sh  
> ```
>
> 哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈红红火火！！！！
>
> ——Jamie


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://book.bsdcn.org/unix-tong-hen-zhe-shou-ce/di-er-bu-fen-cheng-xu-yuan-de-xi-tong/csh-guan-dao-he-find-qiang-li-gong-ju-da-li-chu-qi-ji.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
