什么是 GNU?
软件工具哲学是 Unix(Linux 和 GNU 本质上是它的克隆)最初设计和开发中一个重要且不可或缺的概念。不幸的是,在当今互联网和花哨的 GUI 的压力下,它似乎已被人们遗忘。这很可惜,因为它为解决许多类型的问题提供了一个强大的思维模型。
很多人在裤子口袋(或手提包)里随身携带瑞士军刀。瑞士军刀是一种方便的工具:它有几个刀片、一个螺丝刀、镊子、牙签、指甲锉、拔塞钻,以及可能还有其他一些东西。对于日常的、 мелких 杂务,当您需要一个简单、通用的工具时,它正合用。
另一方面,经验丰富的木匠不会用瑞士军刀盖房子。相反,他有一个装满专用工具的工具箱——锯子、锤子、螺丝刀、刨子等等。而且他确切地知道何时何地使用每种工具;你不会看到他用螺丝刀的把手钉钉子。
贝尔实验室的 Unix 开发人员都是专业的程序员和训练有素的计算机科学家。他们发现,虽然“一刀切”的程序可能会因只有一个程序可用而吸引用户,但在实践中,此类程序:a) 难以编写,b) 难以维护和调试,以及 c) 难以扩展以适应新情况。
相反,他们认为程序应该是专用工具。简而言之,每个程序“应该做好一件事”。不多也不少。这样的程序更易于设计、编写和做好——它们只做一件事。
此外,他们发现,有了将程序连接在一起的正确机制,整体大于部分之和。通过组合几个专用程序,您可以完成一个任何程序都未设计的特定任务,并且比您必须编写一个专用程序更快、更容易地完成它。我们将在本专栏的后面部分看到一些(经典)例子。(一个重要的附加点是,如有必要,请绕道而行,首先构建您可能需要的任何软件工具,如果您还没有工具箱中合适的工具。)
希望您熟悉 shell 中 I/O 重定向的基础知识,特别是“标准输入”、“标准输出”和“标准错误”的概念。简而言之,“标准输入”是数据源,数据来自此处。程序不需要知道或关心数据源是磁盘文件、键盘、磁带,甚至是穿孔卡片阅读器。同样,“标准输出”是数据接收器,数据去往此处。程序也不应该知道或关心这可能是哪里。仅读取其标准输入,对数据执行某些操作,然后将其发送出去的程序,被称为“过滤器”,类似于水管线中的过滤器。
使用 Unix shell,很容易设置数据管道
program_to_create_data | filter1 | .... | filterN > final.pretty.data
我们首先创建原始数据;每个过滤器对数据应用一些连续的转换,直到当它从管道中出来时,它处于所需的格式。
这对于标准输入和标准输出来说很好。标准错误在哪里发挥作用呢?好吧,想想上面管道中的 filter1。如果它在它看到的数据中遇到错误会发生什么?如果它将错误消息写入标准输出,它将只会消失在管道中进入 filter2 的输入,用户可能永远不会看到它。因此,程序需要一个地方来发送错误消息,以便用户会注意到它们。这就是标准错误,它通常连接到您的控制台或窗口,即使您已将程序的标准输出重定向到屏幕之外。
为了使过滤器程序协同工作,数据的格式必须达成一致。最直接和最容易使用的格式只是文本行。Unix 数据文件通常只是字节流,行由 ASCII LF(换行符)字符分隔,在 Unix 文献中通常称为“换行符”。(如果您是 C 程序员,则为 '\n'。)这是所有传统过滤程序使用的格式。(许多早期的操作系统都有用于管理二进制数据的复杂设施和专用程序。Unix 一直避开这些东西,其理念是最好能够使用文本编辑器简单地查看和编辑您的数据。)
好的,介绍足够了。让我们看一下一些工具,然后我们将看到如何以有趣的方式将它们连接在一起。在下面的讨论中,我们将仅介绍那些我们感兴趣的命令行选项。正如您应该始终做的那样,请查阅您的系统文档以获取完整的故事。
第一个程序是 who 命令。它本身会生成当前登录用户的列表。虽然我是在单用户系统上编写这篇文章,但我们假装有几个人登录了
$ who arnold console Jan 22 19:57 miriam ttyp0 Jan 23 14:19 (:0.0) bill ttyp1 Jan 21 09:32 (:0.0) arnold ttyp2 Jan 23 20:48 (:0.0)
这里,$ 是通常的 shell 提示符,我在其中键入了 who。有三个人登录了,而我登录了两次。在传统的 Unix 系统上,用户名永远不会超过八个字符长。这个小小的花絮稍后会很有用。who 的输出很好,但数据并不是特别令人兴奋。
我们要看的下一个程序是 cut 命令。此程序剪切输入数据的列或字段。例如,我们可以告诉它仅打印 /etc/passwd 文件中的登录名和全名。/etc/passwd 文件有七个字段,用冒号分隔
arnold:xyzzy:2076:10:Arnold D. Robbins:/home/arnold:/bin/ksh
要获取第一个和第五个字段,我们将像这样使用 cut
$ cut -d: -f1,5 /etc/passwd root:Operator ... arnold:Arnold D. Robbins miriam:Miriam A. Robbins ...
使用 -c 选项,cut 将剪切输入行中的特定字符(即列)。这个命令看起来可能对数据过滤很有用。
接下来我们将看看 sort 命令。这是 Unix 风格系统上最强大的命令之一;当您设置花哨的数据管道时,您会经常发现自己使用它。sort 命令读取并排序命令行上命名的每个文件。然后,它合并排序后的数据并将其写入标准输出。如果在命令行上未给出任何文件,它将读取标准输入(从而使其成为过滤器)。排序基于机器排序序列 (ASCII) 或基于用户提供的排序标准。
最后(至少现在),我们将看看 uniq 程序。在排序数据时,您通常会得到重复的行,即相同的行。通常,您只需要每行的一个实例。这就是 uniq 的用武之地。uniq 程序读取其标准输入,它期望标准输入是已排序的。它仅打印每个重复行的一个副本。它确实有几个选项。稍后,我们将使用 -c 选项,该选项打印每个唯一行,并在其前面加上该行在输入中出现的次数的计数。
现在,假设这是一个大型 BBS 系统,有数十个用户登录。管理层希望系统管理员编写一个程序,该程序将生成已登录用户的排序列表。此外,即使一个用户多次登录,他的名字也应该只在输出中显示一次。
系统管理员可以坐下来查看他的系统文档,并编写一个 C 程序来完成此操作。这将花费他大约几百行代码和大约两个小时来编写、测试和调试它。但是,了解他的软件工具箱,他首先生成已登录用户的列表
$ who | cut -c1-8 arnold miriam bill arnold
接下来,他对列表进行排序
$ who | cut -c1-8 | sort arnold arnold bill miriam
最后,他通过 uniq 运行排序后的列表,以剔除重复项
$ who | cut -c1-8 | sort | uniq arnold bill miriam
sort 命令实际上有一个 -u 选项,可以执行 uniq 的操作。但是,uniq 还有其他用途,sort -u 无法替代。
系统管理员将此管道放入 shell 脚本中,并使其可供系统上的所有用户使用
# cat > /usr/local/bin/listusers who | cut -c1-8 | sort | uniq ^D # chmod +x /usr/local/bin/listusers
这里有四个主要点需要注意。首先,仅使用四个程序,在一行命令行上,系统管理员就能够节省大约两个小时的工作时间。此外,shell 管道几乎与 C 程序一样高效,并且在程序员时间方面效率更高。人员时间比计算机时间贵得多,在我们现代“永远没有足够的时间来完成所有事情”的社会中,节省两个小时的程序员时间绝非易事。
其次,同样重要的是要强调,通过工具的组合,可以完成单个程序的作者从未想象过的特殊用途的工作。
第三,像我们在这里所做的那样,分阶段构建管道也是很有价值的。这使您可以查看管道中每个阶段的数据,这有助于您确信您确实正确地使用了这些工具。
最后,通过将管道捆绑在 shell 脚本中,其他用户可以使用您的命令,而无需记住您为他们设置的花哨管道。就运行它们的方式而言,shell 脚本和编译后的程序是无法区分的。
在之前的热身练习之后,我们将再看两个更复杂的管道。对于它们,我们需要引入另外两个工具。
第一个是 tr 命令,它代表“transliterate”(转换)。tr 命令在字符的基础上工作,更改字符。通常,它用于诸如将大写映射到小写之类的事情
$ echo ThIs ExAmPlE HaS MIXED case! | tr '[A-Z]' '[a-z]' this example has mixed case!
有几个感兴趣的选项
-c 处理列出字符的补集,即操作应用于给定集合中不存在的字符
-d 从输出中删除第一个集合中的字符
-s 将输出中重复的字符压缩为仅一个字符。
我们将在稍后片刻使用所有这三个选项。
我们要看的另一个命令是 comm。comm 命令将两个排序后的输入文件作为输入数据,并将文件的行打印在三列中。输出列是第一个文件独有的数据行,第二个文件独有的数据行,以及两个文件共有的数据行。-1、-2 和 -3 命令行选项省略相应的列。(这不直观,需要一点时间来适应。)例如
$ cat f1 11111 22222 33333 44444 $ cat f2 00000 22222 33333 55555 $ comm f1 f2 00000 11111 22222 33333 44444 55555
单个破折号作为文件名告诉 comm 读取标准输入而不是常规文件。
现在我们准备构建一个花哨的管道。第一个应用是单词频率计数器。这有助于作者确定他或她是否过度使用某些单词。
第一步是将输入文件中所有字母的大小写更改为一种大小写。“The”和“the”在计数时是同一个词。
$ tr '[A-Z]' '[a-z]' < whats.gnu | ...
下一步是去除标点符号。带引号的单词和不带引号的单词应被视为相同;最简单的方法是直接去除标点符号。
$ tr '[A-Z]' '[a-z]' < whats.gnu | tr -cd '[A-Za-z0-9_ \012]' | ...
第二个 tr 命令对列出字符的补集进行操作,这些字符是所有字母、数字、下划线和空格。\012 表示换行符;它必须保持原样。(对于生产脚本,还应包括 ASCII TAB 字符以获得良好的度量。)
此时,我们有由空格分隔的单词组成的数据。单词仅包含字母数字字符(和下划线)。下一步是分解数据,以便我们每行有一个单词。这将使计数操作更加容易,我们很快就会看到。
$ tr '[A-Z]' '[a-z]' < whats.gnu | tr -cd '[A-Za-z0-9_ \012]' | < tr -s '[ ]' '\012' | ...
此命令将空格转换为换行符。-s 选项将输出中多个换行符压缩为仅一个。这有助于我们避免空行。(> 是 shell 的“辅助提示符”。这是 shell 在注意到您尚未完成键入所有命令时打印的内容。)
我们现在有由每行一个单词、没有标点符号、所有大小写相同的数据组成的数据。我们准备好计数每个单词了
$ tr '[A-Z]' '[a-z]' < whats.gnu | tr -cd '[A-Za-z0-9_ \012]' | > tr -s '[ ]' '\012' | sort | uniq -c | ...
此时,数据可能看起来像这样
60 a 2 able 6 about 1 above 2 accomplish 1 acquire 1 actually 2 additional
输出按单词排序,而不是按计数排序!我们想要的是最常用的单词排在最前面。幸运的是,这很容易实现。sort 命令接受另外两个选项
-n 执行数字排序,而不是 ASCII 排序
-r 反转排序顺序
最终的管道看起来像这样
$ tr '[A-Z]' '[a-z]' < whats.gnu | tr -cd '[A-Za-z0-9_ \012]' | > tr -s '[ ]' '\012' | sort | uniq -c | sort -nr 156 the 60 a 58 to 51 of 51 and ...
哇!这有很多东西要消化。然而,同样的原则适用。使用六个命令,在两行上(实际上是一个长行,为了方便而拆分),我们创建了一个程序,该程序完成了一些有趣且有用的事情,花费的时间比我们编写 C 程序来完成相同的事情要少得多。
对上述管道进行少量修改可以为我们提供一个简单的拼写检查器!要确定您是否正确拼写了一个单词,您所要做的就是在字典中查找它。如果它不在那里,那么您的拼写很可能是不正确的。所以,我们需要一本字典。如果您有 Slackware Linux 发行版,则您拥有文件 /usr/lib/ispell/ispell.words,这是一个排序后的 38,400 个单词的字典。
现在,如何将我们的文件与字典进行比较?与之前一样,我们生成一个排序后的单词列表,每行一个
$ tr '[A-Z]' '[a-z]' < whats.gnu | tr -cd '[A-Za-z0-9_ \012]' | > tr -s '[ ]' '\012' | sort -u | ...
现在,我们所需要的只是不在字典中的单词列表。这就是 comm 命令的用武之地。
$ tr '[A-Z]' '[a-z]' < whats.gnu | tr -cd '[A-Za-z0-9_ \012]' | > tr -s '[ ]' '\012' | sort -u | > comm -23 - /usr/lib/ispell/ispell.words
-2 和 -3 选项消除了仅在字典中(第二个文件)的行,以及两个文件中都存在的行。仅在第一个文件(标准输入,我们的单词流)中的行是不在字典中的单词。这些很可能是拼写错误的候选者。此管道是 Unix 上生产拼写检查器的第一个版本。
还有一些其他工具值得简要提及。
grep 在文件中搜索与正则表达式匹配的文本
egrep 类似于 grep,但具有更强大的正则表达式
wc 计数行数、单词数、字符数
tee 数据管道的 T 形接头,将数据复制到文件和标准输出
sed 流编辑器,一种高级工具
awk 数据处理语言,另一种高级工具
软件工具哲学还提倡以下建议:“让别人做困难的部分。” 这意味着,获取一些可以为您提供大部分所需内容的东西,然后将其余部分按摩成您想要的格式。
总结
1. 每个程序应该做好一件事。不多也不少。
2. 将程序与适当的管道结合使用会产生整体大于部分之和的结果。它还会导致程序的新颖用途,这些用途可能是作者从未想过的。
3. 程序永远不应打印无关的标头或尾部数据,因为这些数据可能会被发送到管道的下游。(我们之前没有提到的一点。)
4. 让别人做困难的部分。
5. 了解您的工具箱!适当地使用每个程序。如果您没有合适的工具,请构建一个。
截至撰写本文时,我们讨论的所有程序都在 textutils-1.9 软件包中,可通过匿名 ftp 从 prep.ai.mit.edu 的 /pub/gnu 目录中的文件 textutils-1.9.tar.gz 获得。
我在本专栏中介绍的所有内容都不是新的。软件工具哲学最初在 Brian Kernighan 和 P.J. Plauger 的著作 Software Tools(Addison-Wesley,ISBN 0-201-03669-X)中介绍。本书展示了如何编写和使用软件工具。它写于 1976 年,使用 FORTRAN 的预处理器 ratfor (RATional FORtran)。当时,C 语言不像现在这样无处不在;FORTRAN 才是。最后一章介绍了用 ratfor 编写的 ratfor 到 FORTRAN 处理器。Ratfor 看起来很像 C;如果您了解 C,您就不会有任何问题地理解代码。
1981 年,本书进行了更新,并以 Software Tools in Pascal(Addison-Wesley,ISBN 0-201-10342-7)的形式发布。这两本书仍然在印刷中,如果您是程序员,非常值得一读。它们当然极大地改变了我看待编程的方式。
最初,这两本书中的程序都可以从 Addison-Wesley(通过 9 磁道磁带)获得。不幸的是,现在情况已不再如此,尽管您可能能够在互联网上找到副本。多年来,有一个活跃的软件工具用户组,其成员已将原始的 ratfor 程序移植到基本上每个带有 FORTRAN 编译器的计算机系统。随着 Unix 开始在大学之外传播,该小组的受欢迎程度在 80 年代中期逐渐下降。
随着当前 GNU 代码和其他 Unix 程序克隆的激增,这些程序现在很少受到关注;现代 C 版本效率更高,功能也比这些程序多。然而,作为对良好编程风格的阐述,以及对仍然有价值的哲学的宣传,这些书是无与伦比的,我强烈推荐它们。
致谢:我要感谢贝尔实验室的 Brian Kernighan,最初的软件工具匠,感谢他对本专栏的审阅。
—尾注:
关于本专栏的问题和/或评论可以通过邮寄方式 C/O Linux Journal 发送给作者,或通过电子邮件发送至 arnold@gnu.ai.mit.edu。
Arnold Robbins 是一位专业程序员和半专业的作家。自 1987 年以来,他一直为 GNU 项目做志愿者工作,自 1981 年以来一直与 UNIX 和类 UNIX 系统合作。