Unix 之书
☰ 目录

第 15 章...

标准输入/输出、重定向与管道 (Standard I/O, Redirection and Pipes)

从一开始,Unix 命令行就有着某种 与众不同的特质,使它区别于其他 操作系统。这种"特质"就是我们 所说的 Unix 工具箱:每一个 Unix 和 Linux 系统都包含数量庞大的程序,而你可以 用简单优雅的方式把它们组合起来使用。

本章中,我会先解释 Unix 工具箱背后的 理念,然后示范如何把基本的构件组合成 属于你自己的强大工具。在第 16 章,我们会纵览其中最重要的那些程序, 让你了解日常工作中有哪些资源可供调用。 读完这两章,你便会上路, 即将掌握你所能学到的最有趣、 也最令人愉快的计算机技能。

Unix 哲学
(The Unix Philosophy)

20 世纪 60 年代,后来成为第一批 Unix 开发者的贝尔实验室研究人员,正在开发 一个名为 Multics 的操作系统(见第 1 章)。Multics 的问题之一是过于臃肿难用。 Multics 的设计团队想让他们的系统承担 太多任务,以取悦太多的人。当 Unix 被 设计出来时——起初是在 1969 年,只有 两个人参与——开发者们强烈意识到,必须 避免 Multics 之类操作系统的复杂性。

于是,他们形成了一种近乎严苛的简约态度, 把表达的经济性放在首位。他们推想,每个 程序都应当是单一用途的工具,也许只带 几个基本选项。一个程序只应做一件事, 但要把这件事做好。如果需要完成复杂的 任务,应当——在可能的情况下——通过 组合已有的工具来实现,而不是编写新程序。

举例来说,几乎所有 Unix 程序都会产生 某种输出。当某个程序输出大量数据时, 数据涌出得太快,往往还没等你读完, 大部分内容就已经滚出屏幕。一种解决办法 是要求每个程序都必须能在需要时一次 只显示一屏输出。而这正是最初的 Unix 开发者想要避免的那种解决方案。为什么 所有程序都要内建同样的功能? 难道就没有更简单的办法,确保用户 能够以舒适易读的方式看到输出吗?

 

更进一步说,为什么你运行的每个程序都 必须知道自己的输出要去往何处?有时候 你希望输出显示在屏幕上,有时候你希望 把它保存进文件,甚至还可能希望把输出 送给另一个程序继续处理。

基于这些考虑,Unix 的设计者们构建了 一个专门的工具,它的职责就是逐屏显示数据。 这个工具叫作 more,因为在显示完一屏 数据之后,程序会显示提示符 --More--, 告诉用户还有更多内容。

这个工具用起来很简单。用户读完一屏数据, 然后按 <Space> 显示下一屏。 看完之后,输入 q 退出。

一旦有了 more,程序员就再也不用为 自己程序的输出如何显示而费心了。如果你 是程序员,你知道当用户运行你的程序而 发现输出太多时,只需把它交给 more 即可。(本章后面会讲到怎么做。)如果你是 用户,你知道无论将来用到多少个程序, 都只需学会这一个输出显示工具的用法。

即使在今天,这种做法依然有三大优势。 第一,设计 Unix 工具时可以保持简单。 例如,你不必给每个新程序都加上逐屏 显示数据的能力:已经有工具做这件事了。 同样,也有对输出排序、删去某些列、 删除不含特定模式的行等等工具,不胜枚举 (见第 16 章)。既然所有 Unix 用户 都能使用这些工具,你就不必把这些功能 塞进自己的程序里。

这就带来了第二个优势。由于每个工具 只需做好一件事,作为程序员,你可以把 精力集中起来。当你在设计一个用于在数据 文件中查找特定模式的程序时,你可以把它 做成尽可能出色的模式查找程序;当你在设计 排序程序时,可以把它做成尽可能出色的排序 程序;以此类推。

第三个优势是易于使用。作为用户,一旦你 掌握了控制标准屏幕显示工具的按键,你就 知道该如何控制任何程序的输出。

因此,我可以用两句话概括 Unix 哲学:

• 每个程序或命令都应是一个工具, 只做一件事,并且把这件事做好。

• 当需要新工具时,优先组合已有的工具, 而不是编写新的工具。

我们有时把这种理念表述为:

• "小即是美",或"少即是多"。

新的 Unix 哲学
(The New Unix Philosophy)

Unix 如今已步入它的第四个十年,因此 值得追问一句:长远来看,Unix 哲学是否 被证明是成功的?答案是:既成功,也 没有。

在很大程度上,Unix 哲学至今依然成立。 正如你将在第 16 章看到的,有大量 单一用途的工具,你可以根据需要把它们 方便地组合起来。

此外,由于最初的 Unix 开发者把系统 设计得极为出色,今天,那些已有 30 多年历史的程序仍能与新程序无缝协作。 不妨拿 Windows 或 Macintosh 的世界来 比较一下。

然而,最初的哲学在三个重要方面被证明 是力不从心的。第一,太多人忍不住要为 基本工具创造替代版本。这意味着,为了 完成同一件工作,你有时不得不学会使用 一个以上的工具。

例如,多年来常用的屏幕显示程序有三个: morepgless。 如今大多数人使用 less,它是这三个 程序中最强大(也最复杂)的一个。不过 more 用起来更简单,而且你在所有 系统上都能找到它,所以你确实应该学会 使用它。我猜想在某一天,你会登录一个 用 more 显示输出的系统,而如果你 只懂 less,就会一头雾水。反过来, 只懂 more 也不够,因为在许多系统上 less 才是默认程序(而且 less 是更好的程序)。结论就是:你至少要学会 使用两种不同的屏幕显示程序。

Unix 哲学力不从心的第二个方面,与用户 不断增长的需求有关。"小即是美"这个理念 确实很有吸引力,但随着用户越来越在行、 需求越来越苛刻,人们很快发现,简单的 工具往往不够用。

比如,最早的 Unix 文本编辑器是 ed。 (这个名字是 "editor" 的缩写,读作两个 单独的字母 "ee-dee"。)当时 ed 是为 那种把输出打印在纸上的终端设计的。 ed 程序的命令相对较少,用起来简单, 很快就能学会。如果你早年间用过 Unix, 你会发现 ed 是一个朴素无华、毫不 张扬的工具:它只做一件事(文本编辑), 而且做得很好(*)。

* 脚注

ed 编辑器至今在所有 Unix 和 Linux 系统上都还能用到。有空时不妨试一试。 先读读它的手册页(man ed)。

就编辑器而言,ed 充其量只能算略微 精致一些。然而没过几年,终端得到了改进, Unix 用户的需求也提高了。为了回应这些 需求,程序员们开发出新式编辑器。事实上, 多年来人们开发的文本编辑器多达几十种。

对主流用户来说,ed 被一个叫 ex 的程序取代了。(这个名字是 "extended editor" 的缩写,读作两个单独的字母 "ee-ex"。)随后,ex 本身又被扩展, 产生了 vi("visual editor",读作 "vee-eye")。作为 ed/ex/vi 这一族的替代选择,人们还开发出一套 截然不同的编辑系统,叫作 Emacs。

今天,vi 和 Emacs 是最流行的 Unix 文本编辑器,但谁也不会说它们简单朴素。 的确,vi(第 22 章)和 Emacs 都极其复杂。

最初 Unix 哲学力不从心的第三个方面, 涉及 CLI(命令行界面)的一个根本局限。 如你所知,CLI 是基于文本的。这意味着 它无法处理图形和图片,也无法处理不含 纯文本的文件,例如电子表格或文字处理 文档。

大多数命令行程序读写的都是文本,这正是 它们能够协同工作的原因:它们使用的都是 同一种数据类型。但这也意味着,当你要处理 其他类型的数据——非文本数据——时, 就必须使用其他类型的程序。所以,正如我们在 第 5 章和第 6 章所讨论的,你必须 同时学会使用 CLI 和图形用户界面环境。

出于这些原因,你学习 Unix 时必须有所 取舍。1979 年,Unix 问世才十年,最初的 设计完好无损,你几乎可以把所有常用命令 学完。而今天的 Unix 要学的内容实在太多, 你不可能全部掌握,甚至连大部分都做不到。 这意味着你必须有所选择,决定要学哪些 程序和工具。而且,在自学某个工具的用法时, 你同样要有所选择,决定要精通哪些选项和 命令。

在阅读下面两章时,有一件重要的事我希望你 记住。你当然应该一边坐在电脑前阅读,一边 随手输入新遇到的命令。这就是学习 Unix 的 方法。不过,我希望你做的不只是死记细节。 我希望你在阅读和动手实验的同时,培养出 一种全局观。时不时地停下片刻,退后一步 问问自己:"我现在正在学的这个工具, 在整个大局中处于什么位置?"

我对你的期望是,假以时日,你会领会我们 所说的新的 Unix 哲学:

• "小即是美,除非它不再美。"

— 提示 —

每当你学习一个新程序的用法时,不要觉得 必须记住每一个细节。你只要回答三个问题:

  1. 这个程序能为我做什么?
  1. 基本要点是什么?也就是说,对大多数人 在大多数时候管用的是什么?
  1. 需要帮助时,我可以去哪里查找?

标准输入、标准输出与标准错误
(Standard Input, Standard Output and Standard Error)

如果说有一个核心思想对高效使用 Unix 最为关键,那就是标准输入与标准输出的 概念。理解了这个思想,你就朝成为 Unix 高手迈出了一大步。

基本思想很简单:每个基于文本的程序, 都应该能够从任意来源接受输入,并向 任意目标写出输出。

举例来说,假设你有一个对文本行排序的 程序。你应当可以自由选择:从键盘输入 文本、从已有文件读取,甚至使用另一个 程序的输出。同样,sort 程序也应该 能够把输出显示在屏幕上、写入文件,或者 送给另一个程序继续处理。

使用这样的系统有两个妙处。第一,作为 用户,你拥有极大的灵活性。运行程序时, 你可以按自己的意愿定义输入和输出(I/O), 这意味着每项任务你只需学一个程序。例如, 同一个程序,既能对少量数据排序并显示在 屏幕上,也能对大量数据排序并存入文件。

以这种方式处理 I/O 的第二个好处是, 它让创建新工具变得容易得多。编写程序时, 你可以依靠 Unix 来替你处理输入和输出, 这样就不必操心各种各样的变化,而可以把 精力集中在工具的设计与编程上。

这里的关键词是:输入的来源和输出的 目标不是由程序员指定的。程序员让程序以 通用的方式读写。之后,在程序运行的 时候,Shell 会根据用户想要使用的输入和 输出把程序与之连接起来(*)。

* 脚注

从历史上看,使用抽象 I/O 设备这一想法, 是为了让程序员能够编写与具体硬件无关的 程序。你能看出这个思想在理念上与我们在 第 5 章讨论的抽象层次、以及我们在 第 7 章讨论的终端描述数据库(Termcap 和 Terminfo)有什么联系吗?

为了实现这一思想,Unix 的开发者们设计了 一种读取数据的通用方式,称为标准输入 (STANDARD INPUT),以及两种写出数据的 通用方式,称为标准输出(STANDARD OUTPUT) 和标准错误(STANDARD ERROR)。之所以设置 两个不同的输出目标,是因为标准输出用于 常规输出,而标准错误用于错误消息。我们 把这些机制统称为标准 I/O(STANDARD I/O, 读作 "standard eye-oh")。

在实际用法中,我们经常非正式地把这 三个名称说得好像它们是实实在在的对象。 比如我们可能说:"要把程序的输出存下来, 就把标准输出写进文件。"我们真正的意思是: "要把程序的输出存下来,就告诉 Shell 把输出目标设为某个文件。"

理解标准输入、标准输出和标准错误的概念, 是把 Unix 用好的关键。而且,这些同样的 概念也用于其他编程语言(例如 C 和 C++) 的 I/O 控制。

— 提示 —

你经常会看到标准输入、标准输出和标准错误 被缩写为 STDIN、STDOUT 和 STDERR。我们在 口头交流中使用这些缩写时,读作 "standard in"、"standard out" 和 "standard error"。

例如,如果你在写一份文档,你可能会写: "sort 程序从 stdin 读取,并把输出 写到 stdout 和 stderr。"如果你把这句话 念给听众听,这些缩写要念成:"sort 程序从 standard in 读取,并把输出写到 standard out 和 standard error。"

重定向标准输出
(Redirecting Standard Output)

当你登录时,Shell 会自动把标准输入设为 键盘,把标准输出和标准错误设为屏幕。 也就是说,默认情况下,大多数程序从键盘 读取,并向屏幕写出。

然而——Unix 的强大之处正在于此——每次 输入命令时,你都可以告诉 Shell 在该命令 运行期间重新设置标准输入、标准输出或 标准错误。

实际上,你可以这样告诉 Shell:"我要运行 sort 命令,并把输出保存到一个叫 names 的文件里。所以仅针对这条 命令,请把标准输出写到那个文件。命令 结束之后,再把标准输出恢复成我的屏幕。"

具体是这样工作的:如果你希望命令的 输出送到屏幕,你什么都不必做,这是 自动的。

如果你希望命令的输出送到某个文件,就在 命令末尾输入 >(大于号),后面跟上 文件名。例如:

sort > names

这条命令会把输出写入一个叫 names 的文件。用 > 这个字符相当贴切, 因为它看起来像一个箭头,指出了输出的 去向。

当你以这种方式把输出写入文件时,该文件 可能已经存在,也可能不存在。如果文件不 存在,Shell 会自动为你创建。在我们的例子 中,Shell 会创建一个叫 names 的文件。

如果文件已经存在,它的内容将被替换, 所以你必须小心。例如,如果 names 文件已经存在,原来的内容就会永久丢失。

有些情况下这样做没问题,因为你确实想 用新的信息替换文件内容。但另一些情况下, 你并不想丢掉文件里已有的内容,而是想把 新数据追加到已有内容之后。为此要使用 >>,即连续两个大于号。这告诉 Shell 把新数据追加到已有文件的末尾。因此, 看看这条命令:

sort >> names

如果 names 文件不存在,Shell 会 创建它;如果它已经存在,新数据会被追加 到文件末尾。什么也不会丢失。

当我们把标准输出送到文件时,我们说把它 "重定向"(REDIRECT)了。因此在上面两个 例子中,我们把标准输出重定向到了一个叫 names 的文件。

现在你可以明白为什么要有两种输出:标准 输出和标准错误。如果你把标准输出重定向 到文件,也不会错过错误消息,因为它们 仍然会显示在你的显示器上。

重定向输出时,小心谨慎、不丢失宝贵数据的 责任在你自己。有两种做法。第一,每次把 输出重定向到文件时,都要仔细想想:你是 想替换文件当前的内容吗?如果是,就用 >。还是说你更愿意把新数据追加到 文件末尾?如果是这样,就用 >>

第二,作为一种保险措施,你可以告诉 Shell 永远不要替换已有文件的内容。这可以通过 设置 noclobber 选项(Bash、Korn shell)或 noclobber Shell 变量 (C-Shell、Tcsh)来实现。下一节我们会 详细讨论。

防止重定向替换或创建文件
(Preventing Files From Being Replaced or Created by Redirection)

在上一节中我说过,当你把标准输出重定向到 文件时,文件中已经存在的数据将会丢失。我 还说过,当你用 >> 把输出追加到文件时, 如果文件不存在,Shell 就会创建它。

有些时候,你并不希望 Shell 替你做这类 假设。例如,假设你有一个叫 names 的文件,里面有 5000 行数据。你想把某条 sort 命令的输出追加到这个文件的末尾。 换句话说,你想输入这条命令:

sort >> names

然而你出了错,不小心输入了:

sort > names

结果如何?你原来的数据全被抹掉了。而且 抹掉得非常快。即使你在按下 <Return> 的瞬间就发现了错误,即使你 立刻按下 ^C 中止程序(发送 intr 信号;见第 7 章),也已经太迟了。文件 里的数据永远消失了。

原因就在这里:你一按下 <Return>, Shell 就会为 sort 程序做好一切准备, 其中就包括清空你的目标文件。由于 Shell 比你快得多,等你中止程序时,目标文件 已经是空的了。

为了避免这样的灾难,你可以告诉 Shell: 用 > 重定向输出时不要替换已存在的 文件。此外,在 C-Shell 一族中,你还可以 告诉 Shell:用 >> 追加数据时不要 创建新文件。这就能确保不会有文件被意外 替换或创建。

要让 Shell 替你采取这类预防措施,你可以 使用我们称为 noclobber 的机制。在 Bourne shell 一族(Bash、Korn shell)中, 你设置 noclobber Shell 选项:

set -o noclobber

要取消这个选项,使用:

set +o noclobber

在 C-Shell 一族(C-Shell、Tcsh)中,你设置 noclobber Shell 变量:

set noclobber

要取消这个变量,使用:

unset noclobber

(关于选项和变量的讨论见第 12 章; 摘要见附录 G。)

一旦设置了 noclobber,你就有了内建的 保护。例如,假设你已经有一个叫 names 的文件,然后你输入:

sort > names

你会看到一条错误消息,告诉你 names 文件已经存在。下面是 Bash 给出的这样的 消息:

bash: names: cannot overwrite existing file

下面是 Tcsh 给出的等价消息:

names: File exists.

两种情况下,Shell 都拒绝执行这条命令, 你的文件因此安然无恙。

如果你确实想替换文件呢?这种情况下, 可以临时越过 noclobber。在 Bourne shell 中,你用 >| 代替 >:

sort >| names

在 C-Shell 中,你用 >! 代替 >:

sort >! names

>|>! 代替 >, 就是告诉 Shell:即使文件已经存在,也要 重定向标准输出。

如我们前面所讨论的,你可以用 >> 代替 > 来重定向标准输出,从而把数据 追加到文件末尾。两种情况下,如果输出文件 不存在,Shell 都会创建它。然而,如果你在 追加数据,看起来你多半是预期文件 已经存在的。因此,如果你用了 >> 而文件并不存在,你很可能正在犯错。这时 noclobber 能帮上忙吗?

在 Bourne shell 中帮不上。如果你用 Bash 或 Korn shell 追加数据,并且设置了 noclobber 选项,一切照旧。C-Shell 和 Tcsh 更聪明一些:它们会告诉你文件不存在, 并拒绝执行命令。

例如,假设你是 C-Shell 或 Tcsh 用户; noclobber Shell 变量已经设置;你有一个 名叫 addresses 的文件,你想往它追加 数据。你输入这条命令:

sort >> address

你会看到一条错误消息:

address: No such file or directory

这时你大概会说:"噢,我应该输入 addresses,而不是 address。 谢谢你,C-Shell 先生。"

当然,有些时候你在向文件追加数据,而你的 确想越过 noclobber。例如,你是 C-Shell 用户,出于安全考虑设置了 noclobber。你想对一个名叫 input 的文件排序,并把数据追加到一个名叫 output 的文件。

如果 output 不存在,你希望创建它。 要紧的是:如果 output 已经存在,你 不希望丢掉其中已有的内容,这正是你选择 追加(>>)而不是替换(>)的原因。 假如没有设置 noclobber,你会用:

sort >> output

由于设置了 noclobber,你必须越过它。 做法很简单:用 >>! 代替 >>:

sort >>! output

这只针对这一条命令越过自动检查。

重定向标准输入
(Redirecting Standard Input)

默认情况下,标准输入被设为键盘。这就是说, 当你运行一个需要读取数据的程序时,程序 期待你用打字的方式一行一行地输入数据。 输入完毕后,你按 ^D(<Ctrl-D>) 发送 eof 信号(见第 7 章)。 按下 ^D 表示没有更多数据了。

下面是一个你可以自己试试的例子。输入:

sort

此时 sort 程序正在等待你从标准输入 (键盘)输入数据。你想输入多少行都可以。 例如,你可能输入:

Harley
Casey
Weedly
Linda
Melissa

在最后一行按下 <Return> 之后,按 ^D 发送 eof 信号。这时 sort 程序会按字母顺序把所有数据排好, 并写到标准输出。标准输出默认是屏幕,所以 你会看到:

Casey
Harley
Linda
Melissa
Weedly

然而,很多时候你会希望重定向标准输入, 从文件而不是从键盘读取数据。只需在命令 末尾输入 <(小于号),后面跟上文件名。

例如,要对一个叫 names 的文件中的 数据排序,使用命令:

sort < names

你可以看到,< 这个字符是个很恰当的 选择,因为它看起来像一个箭头,指出了输入 的来源。

下面又是一个你可以自己试试的例子。正如我 在第 11 章提到的,每个 userid 的基本 信息都存放在 /etc/passwd 文件中。 你可以用下面这条命令显示该文件排序后的 内容:

sort < /etc/passwd

可想而知,同时重定向标准输入和标准输出是 可以的,而且这种做法很常见。看下面这个 例子:

sort < rawdata > report

这条命令从名为 rawdata 的文件读取 数据,排序后把输出写到名为 report 的文件。

文件描述符;用 Bourne Shell 一族重定向标准错误
(File Descriptors; Redirecting Standard Error With the Bourne Shell Family)

虽然下面的讨论偏向 Bourne shell 一族, 但我们谈的是有关 Unix I/O 的重要概念。 因此,无论你此刻用的是哪个 Shell,我都 希望你把这一节完整读一遍。

如我前面所解释的,Shell 提供两种不同的 输出目标:标准输出和标准错误。标准输出 用于常规输出;标准错误用于错误消息。默认 情况下,两种输出都显示在屏幕上。但如果有 需要,你可以把两路输出流分开。

如果你选择分开输出流,会拥有极大的灵活性。 例如,你可以把标准输出重定向到文件保存 起来,同时不去管标准错误,这样就不会错过 任何错误消息(它们会显示在屏幕上)。 或者,你可以把标准输出重定向到一个文件, 把标准错误重定向到另一个文件。也可以把 两种输出重定向到同一个文件。

你还可以把标准输出或标准错误(或两者) 送给另一个程序做进一步处理。讲到管道线时 (本章后面)我会告诉你怎么做。

重定向标准错误的语法,两大 Shell 家族 各不相同。我们先讲 Bourne shell 一族, 再讲 C-Shell 一族。不过在开始之前,我需要 花一点时间解释 Unix 处理 I/O 的一个方面。

在 Unix 进程内部,每个输入来源和每个 输出目标都由一个唯一的编号标识,称为 文件描述符(FILE DESCRIPTOR)。例如,某个 进程可能从 8 号文件读取数据,并向 6 号 文件写出数据。编写程序时,你用文件描述符 来控制 I/O,想用的每个文件对应一个。

在 Bourne shell 一族中,重定向输入或输出的 正式语法是:先写文件描述符的编号,后面 跟上 <(小于号)或 >(大于号)。 例如,假设有一个名为 calculate 的程序, 它被设计成把输出写到文件描述符为 8 的文件。 你可以运行这个程序,并用下面这条命令把 它的输出重定向到名为 results 的文件:

calculate 8> results

默认情况下,Unix 为每个进程提供三个预先 定义好的文件描述符,而大多数时候这也足够 你用了。默认的文件描述符是:0 为标准输入, 1 为标准输出,2 为标准错误。

因此,在 Bourne shell 一族中,重定向标准 输入的语法是用 0<,后面跟上输入文件 的名称。例如:

command 0< inputfile

其中 command 是命令的名称, inputfile 是文件的名称。

重定向标准输出和标准错误的语法与此类似。 标准输出:

command 1> outputfile

标准错误:

command 2> errorfile

其中 command 是命令的名称, outputfileerrorfile 是文件的 名称。

作为便利措施,如果你在重定向输入时省略 0,Shell 会认为你指的是标准输入。 因此,下面两条命令是等价的:

sort 0< rawdata
sort < rawdata

同样,如果你在重定向输出时省略 1, Shell 会认为你指的是标准输出。因此,下面 两条命令也是等价的:

sort 1> results
sort > results

当然,你可以在同一条命令中使用多个 重定向。在下面的例子中,sort 命令从 名为 rawdata 的文件读取输入,把输出 写到名为 results 的文件,并把错误 消息写到名为 errors 的文件:

sort 0< rawdata 1> results 2> errors
sort < rawdata > results 2> errors

请注意,只有标准输入和标准输出可以省略 文件描述符。对于标准错误,你必须写上 2。下面这个简单的例子说明了这一点, 其中标准错误被重定向到名为 errors 的文件:

sort 2> errors

重定向标准错误不会影响标准输入或标准输出。 在这个例子里,标准输入仍然来自键盘, 标准输出仍然送往显示器。

和所有重定向一样,当你把标准错误写入一个 已经存在的文件时,新数据会替换文件原有的 内容。在上面最后一个例子中,errors 文件的内容就会丢失。

如果你想把新的输出追加到文件末尾,只需 用 2>> 代替 2>。例如:

sort 2>> errors

在 C-Shell 一族中重定向标准错误要更复杂 一些。在讲它之前,我需要先讨论一个重要 机制——子 Shell。即使你不用 C-Shell 或 Tcsh,我也希望你读一读下一节,因为子 Shell 对每个人都很重要。

子 Shell
(Subshells)

要理解子 Shell 的概念,你需要对 Unix 进程有所了解。第 26 章会详细讨论这个 话题。这里先给出一个简单的概述。

进程(PROCESS)是被载入内存、随时可以运行 的程序,连同程序的数据以及跟踪该程序所 需的信息。当一个进程需要启动另一个进程时, 它会创建一个副本进程。原来那个称为父进程 (PARENT),副本称为子进程(CHILD)。

子进程开始运行,父进程则等待子进程死去 (也就是运行结束)。一旦子进程结束,父 进程就被唤醒,重新取得控制权并继续运行, 此时子进程消失。

把这件事同你分分秒秒的工作联系起来看: 想想你输入一条命令时会发生什么。Shell 解析这条命令,判断它是内部命令(Shell 内建的)还是外部命令(一个独立的程序)。 当你输入一个内置命令时,Shell 就在自己的 进程内直接解释执行它,无需创建新进程。

当你输入一个外部命令时,Shell 会找到 相应的程序,并以新进程的方式运行它。程序 终止后,Shell 重新取得控制权,等待你输入 下一条命令。在这种情况下,Shell 是父进程, 它替你运行的那个程序是子进程。

再想想当你为自己启动一个全新 Shell 时 会发生什么。例如,如果你用的是 Bash, 你输入 bash 命令(或者如果你用的 是 C-Shell,你输入 csh 命令,以此 类推)。

原来的 Shell(父)启动了一个新的 Shell(子)。 每当一个 Shell 启动另一个 Shell 时,我们把 第二个 Shell 称为子 Shell(SUBSHELL)。因此 我们可以说,每当你启动一个新 Shell(输入 bashkshcshtcsh)时,你就创建了一个子 Shell。 你此后输入的命令都将由子 Shell 解释。要 结束子 Shell,你按 ^D 发送 eof 信号(见第 7 章)。此时父 Shell 重新取得 控制权,你此后输入的命令都由原来的 Shell 解释。

子 Shell 创建时,会继承父 Shell 的环境 (见第 12 章)。但是,子 Shell 对环境 所做的任何改动都不会回传给父 Shell。 因此,如果子 Shell 修改或创建了环境变量, 这些改动并不影响原来的 Shell。

这意味着,在子 Shell 里你可以为所欲为, 而不影响父 Shell。这个能力如此好用,以致 Unix 提供了两种使用子 Shell 的方式。

第一,如上所述,你可以输入一条命令来显式 启动一个全新的 Shell。例如,如果你用 Bash, 就输入 bash。现在你可以为所欲为而不 影响原来的 Shell。比如你更改某个环境变量 或 Shell 选项,只要你输入 ^D,这个 改动就会随之消失;也就是说,新 Shell 一死, 原 Shell 重新掌权。

有时候,你想在一个子 Shell 中运行一小 组命令,甚至只是一条命令,却不想为此 应对一整个新 Shell。Unix 为这种情况准备 了专门的机制:把命令用圆括号括起来即可。 这就告诉 Shell 在子 Shell 中运行这些命令。

例如,要在子 Shell 中运行 date 命令, 你可以写:

(date)

当然,date 并没有非要放在子 Shell 里 运行的理由。不过,下面是一个更贴近实际的 例子,用到了目录。

在第 24 章,我们会讨论目录,目录用来 存放文件。你可以创建任意多个目录,并且在 工作过程中从一个目录移动到另一个目录。 在任何时刻,你当前正在工作的目录都称为 你的工作目录。

假设你有两个目录,分别名为 documentsspreadsheets,而你当前正在 documents 目录中工作。你想切换到 spreadsheets 目录并运行一个名为 calculate 的程序。在运行该程序之前, 你需要把环境变量 DATA 设为某个包含 特定原始数据的文件名,这里就是 statistics。程序运行完之后,你需要把 DATA 恢复成原来的值,并切换回 documents 目录。(换句话说,你需要把 环境恢复到之前的状态。)

一种做法是:启动一个新 Shell,然后改变 工作目录、改变 DATA 的值、运行 calculate 程序。这一切做完后,你按 ^D 退出这个 Shell。当新 Shell 结束、 旧 Shell 重新取得控制权时,你的工作目录和 DATA 变量都会回到原来的状态。

假设你用 Bash 作为 Shell,过程看起来是 这样。(我们将在第 24 章遇到的 cd 命令用于改变你的工作目录。暂时 不必在意它的语法。)

bash
cd ../spreadsheets
export DATA=statistics
calculate
^D

用圆括号还有一种更省事的写法:

(cd ../spreadsheets; export DATA=statistics; calculate)

以这种方式使用子 Shell 时,你不必操心 启动或退出新 Shell,这一切自动完成。而且, 在子 Shell 内,你可以对环境做任何改动而 不会留下持久的影响。例如,你可以改变工作 目录、创建或修改环境变量、创建或修改 Shell 变量、改变 Shell 选项,等等。

括号内的这些命令有时被称为一组命令 (GROUPING),尤其在你阅读 C-Shell 一族的 文档时会看到这种说法。例如在我们的例子中, 就用到了一个含三条命令的组合。使用组合和 子 Shell 最常见的原因,是防止 cd (改变目录)命令影响当前 Shell。一般格式是:

(cd directory; command)

用 C-Shell 一族重定向标准错误
(Redirecting Standard Error With the C-Shell Family)

在 Bourne shell 一族中,重定向标准错误很 直接:用 2>,后面跟上文件名。而在 C-Shell 一族(C-Shell、Tcsh)中,重定向 标准错误就没那么简单了,因为存在一个颇有 意味的限制,我马上就会讲到。

在 C-Shell 一族中,重定向标准错误的基本 语法是:

command >& outputfile

其中 command 是命令的名称, outputfile 是文件的名称。

例如,如果你使用 C-Shell 或 Tcsh,下面这条 命令把标准错误重定向到名为 output 的 文件:

sort >& output

如果你想把输出追加到已有文件的末尾,用 >>& 代替 >&。下面的例子把输出 追加到名为 output 的文件:

sort >>& output

如果你设置了 noclobber Shell 变量 (本章前面解释过),又想临时越过它,就用 >&! 代替 >&。例如:

sort >&! output

在这个例子中,即使设置了 noclobber, 文件内容也会被替换。

那么我提到的限制是什么呢?当你使用 >&>&! 时,Shell 会同时 重定向标准输出和标准错误。事实上, 在 C-Shell 一族中,没有简单的方法只重定向 标准错误。因此在最后一个例子中,标准输出和 标准错误都被重定向到了名为 output 的 文件。

恰好有一个办法可以让标准错误独立于标准输出 被重定向。不过要做到这一点,你需要懂得如何 使用子 Shell(上一节已解释)。语法是:

(command > outputfile) >& errorfile

其中 command 是命令的名称, outputfileerrorfile 是文件的 名称。

例如,假设你要使用 sort,并把标准输出 重定向到名为 output 的文件,把标准错误 重定向到名为 errors 的文件。你会这样写:

(sort > output) >& errors

这里,sort 在一个子 Shell 中运行, 而在该子 Shell 内部重定向了标准输出。在子 Shell 之外,剩下的输出——标准错误——被 重定向到另一个文件。最终效果是把两种输出 各自重定向到自己的文件。

当然,如果你愿意,也可以用 >>>>& 来追加输出。例如,要把标准输出 追加到名为 output 的文件、把标准错误 追加到名为 errors 的文件,可以像下面 这样写命令:

(sort >> output) >>& errors

合并标准输出与标准错误
(Combining Standard Output and Standard Error)

所有 Shell 都能让你重定向标准输出和 标准错误。但如果你想把标准输出和标准错误 一起重定向到同一个地方呢?

在 C-Shell 一族中这很容易,因为当你使用 >&(替换)或 >>&(追加)时, Shell 会自动把两路输出合在一起。例如, 在下面的 C-Shell 命令中,标准输出和标准 错误都被重定向到名为 output 的文件:

sort >& output
sort >>& output

在 Bourne shell 一族中,情形就复杂一些。 我们先讲细节,然后再教你一个 Bash 中可用 的捷径。

基本思路是:把一种输出重定向到某个文件, 再把另一种输出重定向到同一个地方。语法是:

command x> outputfile y>&x

其中 command 是命令的名称,xy 是文件描述符,outputfile 是文件的名称。

例如,在下面的 sort 命令中,标准输出 (文件描述符 1)被重定向到名为 output 的文件,然后标准错误(文件描述符 2)被 重定向到与文件描述符 1 相同的地方。总的 效果是把常规输出和错误消息送到同一个文件:

sort 1> output 2>&1

由于文件描述符 1 是重定向输出的默认值, 你可以省略第一处的数字 1:

sort > output 2>&1

在继续之前,我想谈谈一个很容易犯的有趣 错误。如果你把两个重定向的顺序颠倒过来会 怎样?

sort 2>&1 > output

这看起来和上面的例子几乎一样,但它是 无效的。原因如下:

2>&1 这个指令告诉 Shell,把文件描述符 2(标准错误)的输出送到与文件描述符 1 (标准输出)相同的地方。然而在这个写法中, 该指令是在标准输出被重定向之前交给 Shell 的。因此,当 Shell 处理 2>&1 时,标准输出仍然(按默认)送往显示器。这就 意味着标准错误最终被重定向到了显示器,而它 本来要去的就是显示器。

最终结果是:标准错误去显示器,而标准输出 去文件。(请花点时间琢磨一下,直到想通 为止。)

继续往下:如果你想同时重定向标准输出和 标准错误,并且要把输出追加到文件, 怎么办?只需使用 >> 代替 >:

sort >> output 2>&1

这里,使用 >> 会使标准输出和标准 错误都被追加到名为 output 的文件。

你可能会问:能从标准错误开始来合并两种输出 吗?也就是说,先把标准错误重定向到文件, 再把标准输出送到同一个地方?答案是肯定的:

sort 2> output 1>&2
sort 2>> output 1>&2

这些命令与前面的例子写法不同,但效果一样。

如你所见,在 Bourne shell 一族中,合并两路 输出颇为繁琐。难道不能更简单些吗?为什么 不能把标准输出和标准错误直接送到同一个文件? 例如:

sort > output 2> output

这看起来似乎可行,但其实不行,因为如果在 同一条命令中两次重定向到同一个文件,其中 一个重定向会把另一个冲掉。

现在是捷径部分。上述技巧适用于 Bourne shell 一族的所有成员,尤其是 Bash 和 Korn shell。 不过在 Bash 中,你还可以用 &>>&(挑你喜欢的一个)来同时重定向 标准输出和标准错误:

sort &> output
sort >& output

这样你就不必记住那个更复杂的写法。但是, 如果你既想重定向标准输出和标准错误,又想 把输出追加到文件,就必须使用我们 前面讨论的那个写法:

sort >> output 2>&1

读到这里,如果你是正常人,大概已经有点 糊涂了。别担心。前面几节讨论的所有内容, 都在图 15-1 和 15-2(本章后面)中做了 小结。根据我的经验,只要稍加练习,你就会 发现重定向的规则很容易记住。

丢弃输出
(Throwing Away Output)

你为什么要丢弃输出呢?

有时候,你运行某个程序是为了让它执行 特定动作,而并不在意它的输出。另一些 时候,你可能想看常规输出,却不在乎错误 消息。前一种情况下,你会丢弃标准输出; 后一种情况下,你会丢弃标准错误。

要做到这一点,你只需把输出重定向到一个 名为 /dev/null 的特殊文件。(这个名字 读作 "slash-dev-slash-null",不过有时也能 听到人们说 "dev-null"。)等你在第 23 章读完 Unix 文件系统之后,/dev/null 这个名字 就很好理解了。关于 /dev/null,要紧的 是:你送进去的任何东西都会永远消失(*)。 Unix 用户聚在一起时,你偶尔会听到人们带着 戏谑把 /dev/null 称为位桶(BIT BUCKET)。

* 脚注

一位鳏夫在沉默中说道,
"我亡妻实在乏味得无可救药。
若我杀了她,必有人追捕,
抓住我再把我的牢坐,
所以我把她送进了 /dev/null。"

例如,假设你有一个名为 update 的 程序,它读取并修改大量数据文件。干活的 时候,update 会显示有关进展的统计 信息。如果你不想看这些统计信息,只需把 标准输出重定向到 /dev/null:

update > /dev/null

同样,如果你想看常规输出而不想看任何错误 消息,可以重定向标准错误。在 Bourne shell 一族(Bash、Korn shell)中,你会这样写:

update 2> /dev/null

在 C-Shell 一族(C-Shell、Tcsh)中,你会 这样写:

update >& /dev/null

如我前面所解释的,上面这条 C-Shell 命令 同时重定向了标准输出和标准错误,实际上 等于丢弃了所有输出。在 Bourne shell 一族 中,你可以这样达到同样效果:

update > /dev/null 2>&1

那么,如果你用的是 C-Shell,想丢弃标准 错误但不丢弃标准输出,该怎么办?可以用 我们前面讲过的技巧——也就是把标准错误和 标准输出重定向到不同文件时用的那个办法。 当时我们在子 Shell 中这样运行命令:

(update > output) >& errors

这样我们就把两路输出分开了。套用同样的 结构,我们可以把标准错误重定向到 /dev/null,从而把它丢弃;同时把 标准输出重定向到 /dev/tty,从而保留它:

(update > /dev/tty) >& /dev/null

特殊文件 /dev/tty 代表终端。细节我们 会在第 23 章讨论。现在你只需要知道,当你把 输出送到 /dev/tty 时,它是送往显示器的。 用这个办法,我们就能让 C-Shell 和 Tcsh 在 丢弃标准错误的同时,把标准输出送到显示器(*)。

* 脚注

如果你在想:"做一件这么简单的事,不该 费这么大周折吧。"你说得对。这确实是 C-Shell 一族的一个缺陷。不过,我们终究 还是能办到,这一点也挺酷的。

重定向:小结与动手实验
(Redirection: Summaries and Experimenting)

重定向标准输入、标准输出和标准错误本身很 直接,但各种变化容易让人糊涂。尽管如此, 我的目标还是让你对两大 Shell 家族的所有 变化都做到熟悉,这需要一些练习。为了让 这件事更容易,我可以在两方面帮到你。

第一,作为速查资料,图 15-1 和 15-2 汇总了 所有重定向元字符。图 15-1 面向 Bourne shell 一族;图 15-2 面向 C-Shell 一族。 在这些小结里,你能看到我们到目前为止讲过的 全部要点。你还会看到关于管道的条目。所谓 管道,是指把一个程序的输出当作另一个程序的 输入来使用,我们在下一节讨论。

我给你的第二项帮助,是一个可以用来动手实验 的例子。要对标准输出和标准错误做实验,你需要 一条简单的命令,它既产生常规输出,又产生 错误消息。我找到的最合适的一条,是 ls 的一种变体。

ls(list,列出)命令显示有关文件的 信息,我们将在第 24 章正式介绍它。 加上 -l(long,长格式)选项,ls 会以详细格式显示文件信息。

思路是用 ls -l 显示两个文件 ab 的信息。文件 a 存在,而文件 b 不存在。于是我们会看到 两种输出:标准输出显示文件 a 的信息; 标准错误显示一条错误消息,说文件 b 不存在。然后你就可以用这条示例命令来练习 重定向标准输出和标准错误。

在开始之前,我们必须先创建文件 a。 这要用到 touch 命令。我们在 第 25 章会讲 touch。现在你只需 知道:如果对不存在的文件使用 touch, 它会以那个名字创建一个空文件。因此,如果 名为 a 的文件不存在,你可以用下面 这条命令创建它:

touch a

现在可以用 ls 显示 a(存在)和 b(不存在)两者的信息了:

ls -l a b

下面是典型的输出:

b: No such file or directory
-rw------- 1 harley staff 0 Jun 17 13:42 a

第一行是标准错误,它是一条错误消息,告诉 我们文件 b 不存在。第二行是标准输出, 它包含文件 a 的信息。(注意文件名在 行的末尾。)不必在意这些细节,我们会在 第 24 章讲解。

现在我们可以用这条示例命令做实验了。看看 图 15-1 和 15-2,挑一个来练习。举例来说, 我们把标准输出重定向到名为 output 的文件:

ls -l a b > output

运行这条命令时,你看不到标准输出,因为它 已经送到了 output 文件。不过你会看到 标准错误:

b: No such file or directory

要查看 output 的内容,使用 cat 命令。(我们在第 16 章会讲 cat。)

cat output

这里,cat 会显示 output 的内容, 也就是上一条命令的标准输出:

-rw------- 1 harley staff 0 Jun 17 13:42 a

再来一个例子。你使用 Bash,想练习把标准 输出和标准错误重定向到两个不同的文件:

ls -l a b > output 2> errors

由于所有输出都被重定向了,你的屏幕上什么也 看不到。要查看标准输出,使用:

cat output

要查看标准错误,使用:

cat errors

在做实验的过程中,你可以用 rm(remove, 删除)命令删除文件。例如,要删除 outputerrors 两个文件:

rm output errors

实验结束后,你可以用下面这条命令删除 a 文件:

rm a

现在你有了一条很好的示例命令 (ls -l a b),也知道如何显示一个 短文件的内容(cat filename), 该动手练习了。

我的建议是:针对图 15-1 和 15-2 中每一种 输出重定向方式,至少做一个例子(*)。虽然把 这张单子做完要花一些时间,但做完之后,你 对重定向的了解就会超过世界上 9944/100 的 Unix 用户。

* 脚注

是的,我要你在两大 Shell 家族中各选至少 一个 Shell 来练习。如果拿不定主意选哪个, 就用 Bash 和 Tcsh。

如果你平时用 Bash,就先试这些例子,然后 输入 tcsh 命令启动一个 Tcsh Shell, 再把例子试一遍。如果你平时用 Tcsh,就先 用那个 Shell,然后输入 bash 命令 启动一个 Bash Shell。

无论你现在用的是哪个 Shell,未来总是难料。 我希望你对基本的 Shell 概念——环境变量、 Shell 变量、选项和重定向——在任何你可能 需要用到的 Shell 中都能理解。

— 提示 —

为了实验重定向,我们使用了 ls 命令的 一种变体:

ls -l a b > output
ls -l a b > output 2> errors

为了让实验更方便,你可以为这条命令起一个 名字简单的别名(见第 13 章)。在 Bourne shell(Bash、Korn shell)中,你可以用:

alias x='ls -l a b'

在 C-Shell(C-Shell、Tcsh)中用:

alias x 'ls -l a b'

有了这样的别名,你的测试命令就简单多了:

x > output
x > output 2> errors
x >& output

这个技巧值得记住。

Figure 15-1: Bourne Shell 一族:标准 I/O 的重定向

大多数命令行程序都使用标准 I/O 进行输入和 输出。输入来自标准输入(stdin);常规输出 送往标准输出(stdout);错误消息送往标准 错误(stderr)。

在 Bourne Shell 一族中,你通过把文件描述符 (stdin=0、stdout=1;stderr=2)与各种元字符 搭配使用来控制标准 I/O。在不会产生歧义的 情况下,你可以省略文件描述符。要防止意外 覆盖已存在的文件,请设置 noclobber Shell 选项。如果设置了 noclobber, 你可以用 >| 强制覆盖。详见正文。

元字符 作用
<重定向 stdin(等同于 0<)
>重定向 stdout(等同于 1>)
>|重定向 stdout;强制覆盖
>>追加 stdout(等同于 1>>)
2>重定向 stderr
2>>追加 stderr
2>&1把 stderr 重定向到 stdout
&>>&重定向 stdout+stderr(仅 Bash)
|把 stdout 通过管道送给另一条命令
2>&1 |把 stdout+stderr 通过管道送给另一条命令

Figure 15-2: C-Shell 一族:标准 I/O 的重定向

在 C-Shell 一族中,你用各种元字符来控制 标准 I/O。要防止意外覆盖已有文件或意外 创建新文件,请设置 noclobber Shell 变量。如果设置了 noclobber,你可以用 一个 ! 字符强制覆盖或强制创建文件。 请注意,与 Bourne Shell 一族(图 15-1)不同, 这里没有办法只重定向 stderr 而不重定向 stdout。详见正文。

元字符 作用
<重定向 stdin
>重定向 stdout
>!重定向 stdout;强制覆盖
>&重定向 stdout+stderr
>&!重定向 stdout+stderr;强制覆盖
>>追加 stdout
>>!追加 stdout;强制创建文件
>>&追加 stdout+stderr
>>&!追加 stdout+stderr;强制创建文件
|把 stdout 通过管道送给另一条命令
|&把 stdout+stderr 通过管道送给另一条命令

管道线
(Pipelines)

本章前面讨论 Unix 哲学时我说过,早期 Unix 开发者的目标之一,是打造小型工具,每个 工具只把一件事做好。他们的设想是,当用户 遇到一个无法用单个工具解决的问题时,他 能够把一组工具组合起来完成任务。

例如,假设你在政府部门工作,手上有三个 大文件,里面含有全国所有聪明人的信息。 每个文件中,每个人占一行信息,包括这个人 的姓名。你的问题是:弄清楚有多少人名叫 Harley。

如果你把这个问题交给一位经验丰富的 Unix 老手,他会非常清楚该怎么做。首先,他会用 cat(catenate,串联)命令把文件合并 起来;然后用 grep 命令提取所有包含 Harley 这个词的行;最后用 wc (word count,字数统计)命令加上 -l (line count,行数统计)选项来统计行数。

我们来看看,仅凭目前学过的知识,可以怎样 拼出这样一个解决方案。我们用重定向把中间 结果存放在临时文件里,工作结束后再把它们 删掉。这些命令的具体工作原理我们暂时轻轻 掠过(书中后面会讲),下面就是完成任务的 命令。为了帮你理解发生了什么,我加了几句 注释:

cat file1 file2 file3 > tempfile1    # combine files
grep Harley < tempfile1 > tempfile2  # extract lines
wc -l < tempfile2                    # count lines
rm tempfile1 tempfile2               # delete temp files

请仔细看看这段内容。继续往下之前,一定要 确认自己理解了:通过重定向标准输出和标准 输入,我们如何把数据保存在临时文件里, 从而从一个程序传给另一个程序。

上面这一串命令完全可行。不过它们有一个 缺点:把一切粘合在一起的东西——使用临时 文件的重定向——让整个方案难以理解。而且, 事情越复杂,就越容易出错。

为了让这类方案更简单,Shell 允许你创建 一连串命令,让上一个程序的标准输出自动 送到下一个程序的标准输入。这样做时,两个 程序之间的连接称为管道(PIPE),而这一串 命令本身称为管道线(PIPELINE)。

要创建管道线,你把想用的命令写出来,命令 之间用 |(竖线字符,即管道符号) 分隔。例如,前面那四条命令可以用一条 管道线代替:

cat file1 file2 file3 | grep Harley | wc -l

要理解一条管道线,你从左到右读这行命令。 每看到一个管道符号,就想象上一个程序的 标准输出变成了下一个程序的标准输入。

管道线之所以如此简单,是因为 Shell 处理了 所有细节,所以你不必使用临时文件。在我们 的例子中,Shell 自动把 cat 的标准输出 连到 grep 的标准输入,再把 grep 的标准输出连到 wc 的标准 输入。

在 Bourne shell 一族中,你可以把标准输出和 标准错误合并,一起送给另一个程序。语法是:

command1 2>&1 | command2

其中 command1command2 是命令。

在下面的例子中,ls 命令的标准输出和 标准错误都被送给了 sort 命令:

ls -l file1 file2 2>&1 | sort

在 C-Shell 一族中,语法是:

command1 |& command2

例如:

ls -l file1 file2 |& sort

谈到管道线时,我们经常把 pipe 一词当作 动词用,指把数据从一个程序送到另一个程序。 例如在第一个例子里,我们把 cat 的输出 用管道送给 grep,又把 grep 的输出 用管道送给 wc。在第二个例子里,我们把 ls 的标准输出和标准错误用管道送给 sort

想到上面这类例子时,人们很容易在脑海中 浮现一幅管道线的图像:数据从一头进去, 从另一头出来。不过更好的比喻是流水线 (assembly line)。原始数据从一头进去, 依次由一个又一个程序加工,最后以成品的 形态从另一头出来。

创建管道线时,你必须使用那些按标准输入 读取文本、按标准输出写出文本来编写的程序。 我们把这类程序称为"过滤器"(filters), 这样的程序有很多。第 16-19 章会讲最 重要的那些过滤器。如果你是程序员,你可以 编写自己的过滤器,从而做出自己的工具。

实践中你会发现,你的大多数管道线只连续 使用两三个命令。迄今为止,管道线最常见的 用途,是把某个命令的输出用管道送给 less(见第 21 章),以便逐屏显示 该命令的输出。例如,要显示 2008 年的日历, 你可以用:

cal 2008 | less

(cal 程序在第 8 章解释。)

掌握 Unix 这门手艺的基本功之一,就是学会 何时以及如何通过把程序组合成管道线来解决 问题。创建管道线时,你需要多少个过滤器就 用多少个,因此你有时会看到由五六个甚至 更多程序巧妙组合而成的管道线。的确,在 构造管道线这件事上,限制你的只有你的智慧 和你对过滤器的了解(*)。

* 脚注

这不必让人担忧。读完第 16-19 章之后, 你就会懂得如何使用那些最重要的过滤器。 而且,作为我的读者,你的智商显然高于 平均水平。

— 提示 —

当你使用带管道或重定向标准 I/O 的命令时, <>| 这些字符两边 并不必须加空格。不过,加上空格是个好主意。 例如,与其写:

ls -l a b >output 2>errors
cat f1 f2 f3|grep Harley|wc -l

不如这样写:

ls -l a b > output 2> errors
cat f1 f2 f3 | grep Harley | wc -l

这样使用空格能减少打错字的机会,也让你的 命令更容易读懂。当你编写 Shell 脚本时, 这一点尤其重要。

拆分管道线:tee
(Splitting a Pipeline: tee)

有些时候,你会希望一个程序的输出同时去往 两个地方。例如,你可能想同时把输出送往 一个文件和另一个程序。为了说明我的意思, 看看下面这个例子:

cat names1 names2 names3 | grep Harley

这条管道线的用途,是显示 names1names2names3 三个文件中所有 含有 "Harley" 这个词的行。(细节:cat 合并这三个文件;grep 提取所有含有 "Harley" 这些字符的行。这两条命令分别在 第 16 章和第 19 章讨论。)

假设你想把合并后的文件也保存一份。也就是 说,你既想把 cat 的输出送进一个文件, 想同时把它送给 grep

为此,你使用 tee 命令。tee 的 用途是从标准输入读取数据,并把它的副本 同时送往标准输出和一个文件。语法是:

tee [-a] file...

其中 file 是你想把数据送去的文件名称。

通常,你会给 tee 一个文件名,例如:

cat names1 names2 names3 | tee masterlist | grep Harley

在这个例子中,cat 的输出被保存在 一个叫 masterlist 的文件里,同时也 通过管道送给了 grep

使用 tee 时,你可以通过指定多个文件 名来保存输出的多份副本。例如,在下面的 管道线中,teecat 的输出 复制到 d1d2 两个文件:

cat names1 names2 names3 | tee d1 d2 | grep Harley

如果 tee 命令中指定的文件不存在, tee 会为你创建它。但你要小心,因为 如果文件已经存在,tee 会覆盖它, 原有内容就会丢失。

如果你想让 tee 把数据追加到文件末尾, 而不是替换整个文件,就使用 -a(append, 追加)选项。例如:

cat names1 names2 names3 | tee -a backup | grep Harley

这条命令把 cat 的输出保存到名为 backup 的文件。如果 backup 已经 存在,什么也不会丢失,因为输出会被追加到 文件末尾。

tee 命令在管道线的末尾特别好用, 当你既想查看某个命令的输出,想把它 保存到文件时。例如,假设你想用 who 命令(第 8 章)显示当前登录 到你系统的 userid 信息。不过你不仅想显示 这些信息,还想把它们保存到文件 status。一种做法是用两条独立的命令:

who
who > status

而借助 tee,你可以一步到位:

who | tee status

请特别留意这个写法:我希望你把它记住:

command | tee file

注意,在 tee 之后你并不必再接一个 程序。这是因为 tee 会把输出送往 标准输出,而标准输出默认就是屏幕。

在我们的例子里,tee 从标准输入读取 who 的输出,并同时写到 status 文件和屏幕上。如果你发现输出 太长,可以把它用管道送给 less,逐屏 显示:

who | tee status | less

名称的由来

tee


在水管工的世界里,"tee"(三通接头)把两截 管子连成一条直线,同时提供一个额外的出口, 让水以直角方向分流。例如,你可以用一个三通 让水既从左向右流,又向下流。实际的接头形状 就像一个大写字母 "T"。

当你使用 Unix 的 tee 命令时,可以 想象数据从一个程序流向另一个程序,方向 从左到右;与此同时,数据的一份副本沿着 这个 "T" 的竖管向下流入一个文件。

管道线的重要性
(The Importance of Pipelines)

1964 年 10 月 11 日,贝尔实验室的研究人员 Doug McIlroy 写了一份 10 页长的内部备忘录, 提出了许多建议和想法。备忘录的最后一页 是他思想的摘要。开头是这样写的:

"用最简练的话说出我最强烈的关切:

"我们应该有某种办法,像接花园水管那样把 程序连接起来——需要用另一种方式加工数据 时,就拧上一节新管子……"

回过头看,我们可以明白 McIlroy 说的是: 把程序拼装起来解决当前问题,应当是一件 很容易的事。这个想法虽然重要,却直到 五年多以后才结出果实。

到 20 世纪 70 年代初,最初的 Unix 项目在 贝尔实验室已经顺利推进(见第 2 章)。 当时 McIlroy 是 Unix 诞生所在研究部门的 经理。他在许多研究领域做出了重要贡献, 其中也涉及 Unix 的某些方面。例如,正是 McIlroy 要求 Unix 的手册页必须简短而准确。

McIlroy 已经倡导他关于输入输出流动的设想 相当一段时间了。然而直到 1972 年,Ken Thompson(见第 2 章)才终于为 Unix 加上 了管道线。为了加入管道机制,Thompson 不得不 修改大多数已有程序,把输入来源从文件改为 标准输入。

这件事完成、并且设计出合适的记法之后, 管道线就成为 Unix 不可分割的一部分,而用户 表现出的创造力也超出了任何人的预期。按 McIlroy 的说法,改动完成后的那个早晨, "……我们掀起了一场单行命令的狂欢。人人都 有一条单行命令。看看这个,看看那个……"

事实上,管道线的实现正是催生 Unix 哲学的 催化剂。McIlroy 回忆道:"……大家开始提出 Unix 哲学。写只做一件事并把这件事做好的 程序。写能彼此协作的程序。写处理文本流 的程序,因为那是通用的接口……"

三十多年后的今天,Unix 的管道机制与 1972 年基本无异:这是一项了不起的成就。的确, 正是管道线和标准 I/O 在很大程度上造就了 Unix 命令行界面的强大。因此,我鼓励你花 时间学会把管道线用好,并在有机会时不断 练习,把它们融入你的日常工作。

为了帮你踏上 Unix 世界的"黄砖路",我用 第 16-19 章来介绍过滤器——它们正是你 用来巧妙构造实用解决方案的原材料。

不过在转向讨论过滤器之前,还有最后一个 话题我要讲:条件执行。

条件执行
(Conditional Execution)

有些时候,你会希望只有在前一条命令成功 结束之后才执行某条命令。为此,使用语法:

command1 && command2

另一些时候,你会希望只有在前一条命令没有 成功结束时才执行某条命令。这时的语法是:

command1 || command2

这种思想——只有前一条命令成功或失败时才 执行命令——称为条件执行(CONDITIONAL EXECUTION)。

条件执行大多用在 Shell 脚本内部。不过偶尔 在你输入命令时,它也能帮上大忙。这里有 几个例子。

假设你有一个名为 people 的文件,里面 存有若干人的信息。你想对 people 的 内容排序,并把输出保存到名为 contacts 的文件。但只有当 people 中某处含有 "Harley" 这个名字时,你才这样做。

首先,我们怎样才能看出一个文件是否含有 "Harley" 这个名字?我们用 grep 命令 (见第 19 章)显示文件中所有含有 "Harley" 的行。命令是:

grep Harley people

如果 grep 成功,它会把含有 "Harley" 的行显示到标准输出上;如果 grep 失败, 它就保持沉默。在我们的场合,如果 grep 成功,我们就接着运行这条命令:

sort people > contacts

如果 grep 不成功,我们什么都不想做。

下面这一行命令用条件执行来完成这件事:

grep Harley people && sort people > contacts

这条命令虽然有效,却留下一个小问题:如果 grep 在文件里找到任何符合我们条件的 行,它会把这些行显示在屏幕上。多数情况下 这样做是合理的,但在这个场合我们其实不想 看到任何输出。我们要做的只是运行 grep,并检验它是否成功。

解决办法是把 grep 的输出丢弃,即把它 重定向到 /dev/null:

grep Harley people > /dev/null && sort people > contacts

偶尔你会希望只在前一条命令失败时才执行 某条命令。例如,假设你要运行一个名为 update 的程序,它要独自忙上好几分钟 做这样那样的事。如果 update 顺利结束, 万事皆好;如果不是,你希望得到通知。下面 这条命令会显示一条警告消息,但仅当 update 失败时才显示:

update || echo "The update program failed."

— 提示 —

如果你需要中止一条正在运行的管道线,只需 按下 ^C 发送 intr 信号(见 第 7 章)。

当管道线中的某个程序因为等待输入而停下来时, 这是夺回控制权的好办法。

练习
(Exercises)

复习问题 #1:

概括 Unix 哲学。

复习问题 #2:

在第 10 章,我给过你三个问题,让你每次 学习一个新程序的语法时都问一问自己:

• 这条命令做什么?
• 我怎样使用选项?
• 我怎样使用参数?

同样地,每当你开始学习一个新程序时,应该 提出(并回答)哪三个问题?

复习问题 #3:

术语"标准 I/O"统指标准输入、标准输出和 标准错误。请给这三个术语下定义,它们的 缩写分别是什么?

重定向标准 I/O 是什么意思?请演示如何 重定向这三种标准 I/O。

复习问题 #4:

什么是管道线?

你用哪个元字符来分隔管道线的各个组成部分?

在管道线的末尾,你会用哪个程序来逐屏 显示输出?

复习问题 #5:

当数据流经管道线时,你用哪个程序来保存它 的一份副本?

应用你的知识 #1:

说明如何把 date 命令的标准输出 重定向到名为 currentdate 的文件。

应用你的知识 #2:

下面这条管道线统计当前登录到系统的 userid 数量。(wc -w 命令统计单词数;见 第 18 章。)

users | wc -w

在不改变管道线输出的前提下,修改这条命令, 把 users 的输出的一份副本保存到名为 userlist 的文件。

应用你的知识 #3:

口令文件(/etc/passwd)中,每个向系统 注册的 userid 占一行。请创建一条单独的 管道线,对口令文件的各行排序、保存到名为 userids 的文件,然后显示系统上 userid 的数量。

应用你的知识 #4:

在下面的管道线中,find 命令(第 25 章 解释)会搜索 /etc 之下的所有目录, 查找由 userid root 拥有的文件。所有 这类文件的名称随后会写到标准输出,每行一个。 find 的输出通过管道送给 wc -l 以统计行数:

find /etc -type f -user root -print | wc -l

find 干活时,它会产生各种你不想 看到的错误消息。你的目标是重写这条管道线, 把错误消息丢弃,同时不影响其余输出。请 说明在 Bourne Shell 一族中如何做到这一点。

想要额外的嘉奖,就试试能否为 C-Shell 一族 设计一种做法。(提示:在子 Shell 中再用一个 子 Shell。)

进一步思考 #1:

Unix 哲学的一个重要部分是这样的:当你需要 新工具时,与其编写新工具,不如组合已有的 工具。当你试图把这条准则应用到基于图形用户 界面的工具上时,会发生什么?

这是好事还是坏事?

进一步思考 #2:

在 Bourne Shell 一族中,分别重定向标准输出和 标准错误很简单,这让我们可以轻松地选择 保存或丢弃错误消息。而在 C-Shell 一族中, 要把这两种输出分开就复杂得多。这有多重要?

C-Shell 是 Bill Joy 设计的,他是那个时代 才华横溢的程序员。你认为他为什么要设计出 如此复杂的体系?

进一步思考 #3:

一般而言,计算机世界变化飞快。你认为, 为什么这么多基本的 Unix 设计原则在诞生 30 多年之后依然如此有效?

• 20 世纪 70 年代,Unix 用户群体很小, 开发者可以尽情试验并快速做出改动。与今天的 操作系统程序员不同,最初的开发者不必担心 庞大的既有用户群中那些不懂技术的用户, 也不必顾忌强大的公司利益。

• 灵活、协作与共享这些产生协同效应的 理念,从系统诞生之初就被内建其中。结果 就是,任何有好点子的人,都很可能把自己的 想法被采纳进这个操作系统。