管道与过滤器:把小命令组合成文本处理流程

管道与过滤器:把小命令组合成文本处理流程

本课要解决的问题

  • 怎样把现有命令组合起来,得到想要的输出?
  • 怎样只显示输出的一部分?

学完本课,你应能解释管道与过滤器的作用,串联命令得到新的结果,把输出重定向到文件,并说明命令没有收到文件或其他输入时通常会怎样。

掌握几个基础命令后,我们终于可以使用 Shell 最有力量的特性:把已有程序轻松组合成新的工作方式。先进入 shell-lesson-data/exercise-data/alkanes 目录。这里有六个描述简单有机分子的文件。扩展名 .pdb 表示 Protein Data Bank 格式;这种纯文本格式记录分子中各原子的类型和位置。

$ ls

输出:

cubane.pdb    methane.pdb    pentane.pdb
ethane.pdb    octane.pdb     propane.pdb

运行一个示例命令:

$ wc cubane.pdb

输出:

20  156 1158 cubane.pdb

wc 的名字来自“word count”。默认输出从左到右依次是行数、单词数和字节数。原文把第三列称为字符数;严格区分时,字符数应使用 wc -m,而默认第三列是字节数。对于包含多字节字符的文本,这两者可能不同。

运行 wc *.pdb 时,* 匹配零个或更多字符,因此 Shell 会先把 *.pdb 展开为当前目录下所有 .pdb 文件的名称:

$ wc *.pdb

输出:

  20  156  1158  cubane.pdb
  12  84   622   ethane.pdb
   9  57   422   methane.pdb
  30  246  1828  octane.pdb
  21  165  1226  pentane.pdb
  15  111  825   propane.pdb
 107  819  6081  total

最后一行还给出了所有文件的合计。把 wc 改成 wc -l,就只显示各文件的行数:

$ wc -l *.pdb

输出:

  20  cubane.pdb
  12  ethane.pdb
   9  methane.pdb
  30  octane.pdb
  21  pentane.pdb
  15  propane.pdb
 107  total

-m 和 -w 则分别只显示字符数和单词数。

为什么命令好像没有反应?

如果一个命令需要处理文件,而你没有提供文件名,会发生什么?例如只输入:

$ wc -l

因为后面没有文件名,wc 会等你从标准输入交互式地提供数据。从外面看,它只是停在那里。若误入这种状态,可以按一次 Ctrl+C 中断命令,然后松开按键。

保存命令输出

哪个文件的行数最少?只有六个文件时,直接看结果很容易;如果有六千个呢?先把计数结果存起来:

$ wc -l *.pdb > lengths.txt

大于号 > 要求 Shell 把命令输出重定向到文件,而不是显示在终端中。因此执行后屏幕没有输出,wc 本应打印的内容全部写入 lengths.txt。如果文件不存在,Shell 会创建它;如果已经存在,原有内容会被直接覆盖,所以使用重定向时要小心。

确认文件已经建立:

$ ls lengths.txt

输出:

lengths.txt

接着用 cat lengths.txt 查看内容。cat 来自“concatenate”,意为连接;它按顺序打印各输入文件的内容。这里只有一个文件,所以只是显示该文件:

$ cat lengths.txt

输出:

  20  cubane.pdb
  12  ethane.pdb
   9  methane.pdb
  30  octane.pdb
  21  pentane.pdb
  15  propane.pdb
 107  total

分页阅读输出

为保持示例统一,本课继续使用 cat。实际工作中它会一次把整个文件打印到屏幕上,可能不便阅读。less lengths.txt 更适合较长文件:先显示一屏,按空格前进一屏,按 b 后退一屏,按 q 退出。

过滤输出

下一步用 sort 对 lengths.txt 排序。先做一道练习,了解排序方式。

练习:sort -n 做了什么?

shell-lesson-data/exercise-data/numbers.txt 包含:

10
2
19
22
6

直接用 sort 排序,会得到:

10
19
2
22
6

对同一个文件使用 sort -n,得到:

2
6
10
19
22

解答:-n 指定按数值排序,而不是按字符顺序排序。

我们同样使用 -n 对行数进行数值排序。排序命令不会修改输入文件,只把结果输出到屏幕:

$ sort -n lengths.txt

输出:

  9  methane.pdb
 12  ethane.pdb
 15  propane.pdb
 20  cubane.pdb
 21  pentane.pdb
 30  octane.pdb
107  total

也可以把排序结果保存为临时文件 sorted-lengths.txt,再用 head 取出最前面的行:

$ sort -n lengths.txt > sorted-lengths.txt
$ head -n 1 sorted-lengths.txt

输出:

  9  methane.pdb

head -n 1 表示只要第一行;-n 20 则取前二十行。因为文件已经按行数从小到大排好,第一行就是行数最少的文件。

不要把输出重定向回同一个输入文件

让命令读取某个文件,同时把输出重定向到同一文件,是个很危险的做法:

$ sort -n lengths.txt > lengths.txt

Shell 可能在命令读取之前就截断目标文件,造成错误结果或把 lengths.txt 的内容清空。应当用另一个输出文件保存结果。

练习:>> 是什么意思?

我们已经用过 >。另一个相似的运算符 >> 有不同的行为。先看 echo,它会打印文本:

$ echo The echo command prints text

输出:

The echo command prints text

试着分别连续执行下面两个命令两次,然后检查各输出文件:

$ echo hello > testfile01.txt
$ echo hello >> testfile02.txt

解答:第一个命令每次把 hello 写入 testfile01.txt,并覆盖文件原有内容。第二个命令写入 testfile02.txt;如果文件已经存在,就把新文本追加到末尾,因此执行两次会留下两行。

练习:追加数据

head 从文件开头取行,tail 则从末尾取行。对 shell-lesson-data/exercise-data/animal-counts/animals.csv 执行:

$ head -n 3 animals.csv > animals-subset.csv
$ tail -n 2 animals.csv >> animals-subset.csv

animals-subset.csv 中是什么?

  1. animals.csv 的前三行。
  2. animals.csv 的最后两行。
  3. animals.csv 的前三行,接着是最后两行。
  4. animals.csv 的第二、第三行。

解答:第三项正确。第一项只执行 head 即可;第二项只执行 tail 即可。如果要得到第四项,可以把 head 的输出交给 tail -n 2:

head -n 3 animals.csv | tail -n 2 > animals-subset.csv

把输出传给另一个命令

寻找行数最少文件的流程目前用了 lengths.txt 和 sorted-lengths.txt 两个中间文件。即使已经知道 wc、sort、head 的用途,中间文件仍然让流程不够直观。把排序与取第一行放在一起,就更容易看懂:

$ sort -n lengths.txt | head -n 1

输出:

  9  methane.pdb

两个命令之间的竖线 | 就是管道。它告诉 Shell:把左边命令的输出作为右边命令的输入。这样就不需要 sorted-lengths.txt 了。

组合多个命令

管道可以连续串联。先把 wc 的输出直接交给 sort:

$ wc -l *.pdb | sort -n

输出:

   9 methane.pdb
  12 ethane.pdb
  15 propane.pdb
  20 cubane.pdb
  21 pentane.pdb
  30 octane.pdb
 107 total

再经过一个管道,交给 head:

$ wc -l *.pdb | sort -n | head -n 1

输出:

   9  methane.pdb

这样连一个中间文件也不需要了。这与把数学函数嵌套起来的做法相似:先求 *.pdb 的行数,再排序,最后取开头的行。下图对比直接输出、文件重定向,以及把输出传递给后续命令这三种方式。

wc 输出到终端;使用大于号写入文件;使用管道先传给 sort 再传给 head,最后输出到终端
重定向与管道。原图来自 Software Carpentry,CC BY 4.0。

练习:把命令接起来

在当前目录中找出行数最少的三个文件,以下哪条命令正确?

  1. wc -l * > sort -n > head -n 3
  2. wc -l * | sort -n | head -n 1-3
  3. wc -l * | head -n 3 | sort -n
  4. wc -l * | sort -n | head -n 3

解答:第四项正确。| 用来连接前一个命令的输出与后一个命令的输入,> 则把标准输出写入文件。可以在 shell-lesson-data/exercise-data/alkanes 目录中尝试。

为组合而设计的工具

程序之间可以彼此连接,是 Unix 广泛成功的原因之一。Unix 程序员往往不去编写包办一切的巨型程序,而是制作许多功能单一、能够彼此配合的小工具。这种模型叫作“管道与过滤器”。我们已经见过管道;像 wc 和 sort 这样把输入流转换成输出流的程序就是过滤器。几乎所有标准 Unix 工具都能这样工作。

没有其他指定时,它们从标准输入读取数据,进行处理,再写入标准输出。只要程序都按行读取和输出文本,就能与其他遵循同样约定的程序组合。你也可以这样设计自己的程序,让自己和别人通过管道组合它们。

练习:读懂管道

shell-lesson-data/exercise-data/animal-counts/animals.csv 包含以下数据:

2012-11-05,deer,5
2012-11-05,rabbit,22
2012-11-05,raccoon,7
2012-11-06,rabbit,19
2012-11-06,deer,2
2012-11-06,fox,4
2012-11-07,rabbit,16
2012-11-07,bear,1

下面各段管道和最后的重定向分别传递了哪些文本?sort -r 表示逆序排序。可以逐段建立管道,检查自己的理解。

$ cat animals.csv | head -n 5 | tail -n 3 | sort -r > final.txt

解答:head 先取前五行,tail 从这五行中取最后三行,sort -r 再逆序排列这三行。最终结果写入 final.txt。用 cat final.txt 检查,内容应为:

2012-11-06,rabbit,19
2012-11-06,deer,2
2012-11-05,raccoon,7

练习:构造管道

对上一题的 animals.csv 执行:

$ cut -d , -f 2 animals.csv

cut 可以从每行中切出指定部分供后续处理,而不改变原始文件。默认情况下,它按制表符把一行划分为字段;这样的分隔字符叫作分隔符。上面的 -d 指定以逗号分隔,-f 2 指定取第二个字段,因此输出为:

deer
rabbit
raccoon
rabbit
deer
fox
rabbit
bear

uniq 会过滤掉相邻的重复行。怎样结合 uniq 和另一个命令,列出文件中出现过的动物名,并且不重复?

解答:先排序,把相同动物名放到一起,再去除相邻重复:

$ cut -d , -f 2 animals.csv | sort | uniq

练习:选哪条管道?

animals.csv 有八行,格式如下:

2012-11-05,deer,5
2012-11-05,rabbit,22
2012-11-05,raccoon,7
2012-11-06,rabbit,19
...

uniq -c 会统计每个连续重复行组的出现次数。当前目录是 shell-lesson-data/exercise-data/animal-counts,要得到各种动物在文件中出现了几行,应选哪条命令?

  1. sort animals.csv | uniq -c
  2. sort -t, -k2,2 animals.csv | uniq -c
  3. cut -d, -f 2 animals.csv | uniq -c
  4. cut -d, -f 2 animals.csv | sort | uniq -c
  5. cut -d, -f 2 animals.csv | sort | uniq -c | wc -l

解答:第四项正确。这里统计的是每个动物名称出现的记录行数,并不是第三列动物数量的总和。如果不理解,可以逐段运行管道。第三项没有先排序,非相邻的相同动物名不会被合并;第五项则只得到动物种类的行数。

Nelle 的处理流程:检查数据文件

Nelle 已经把样本送入检测设备,在之前提到的 north-pacific-gyre 目录里生成了十七个文件。从 shell-lesson-data 目录开始,她先快速检查:

$ cd north-pacific-gyre
$ wc -l *.txt

输出有十八行,大致如下:

300 NENE01729A.txt
300 NENE01729B.txt
300 NENE01736A.txt
300 NENE01751A.txt
300 NENE01751B.txt
300 NENE01812A.txt
... ...

接着查看最短的几个文件:

$ wc -l *.txt | sort -n | head -n 5

输出:

 240 NENE02018B.txt
 300 NENE01729A.txt
 300 NENE01729B.txt
 300 NENE01736A.txt
 300 NENE01751A.txt

有一个文件比其他文件少六十行!回头检查后,她发现那次检测是在星期一早晨八点做的:可能周末有人用过设备,而她忘记重新设置。重新测量那个样本前,她还要检查有没有文件包含过多数据:

$ wc -l *.txt | sort -n | tail -n 5

输出:

 300 NENE02040B.txt
 300 NENE02040Z.txt
 300 NENE02043A.txt
 300 NENE02043B.txt
5040 total

数字看起来正常,但倒数第三行的名称为什么带着 Z?她的样本应该标为 A 或 B;实验室约定用 Z 标记缺失信息的样本。她继续寻找同类文件:

$ ls *Z.txt

输出:

NENE01971Z.txt    NENE02040Z.txt

查看笔记后,她确认这两个样本都没有记录深度,也已经无法补得,因此本次分析必须排除它们。虽然可以用 rm 删除文件,但以后也许会做不依赖深度的分析,所以保留文件更合适。后续选择输入时,必须注意使用 NENE*A.txt NENE*B.txt 这样的通配符表达式。

练习:删除不需要的文件

假设你想保留原始数据和处理脚本,删除处理后的数据以节省空间。原始数据以 .dat 结尾,处理结果以 .txt 结尾。以下哪项只删除所有处理结果?这是一道理解通配符的题,执行真实删除前必须先确认目录和匹配结果。

  1. rm ?.txt
  2. rm *.txt
  3. rm * .txt
  4. rm *.*

解答:第二项正确。第一项只匹配主文件名只有一个字符的 .txt 文件;第三项中的空格把 * 和 .txt 分开,前者会展开为当前目录中的所有非隐藏匹配项,命令还会尝试删除名为 .txt 的文件;第四项匹配名称中至少有一个点的文件,既包括处理结果,也包括原始数据。

要点

  • wc 统计行、单词和字节;字符数使用 -m。
  • cat 显示输入内容;sort 对输入排序。
  • 没有额外参数时,head 和 tail 默认分别显示前十行和后十行。
  • command > file 将输出写入文件并覆盖已有内容;command >> file 追加到末尾。
  • first | second 把第一个命令的输出作为第二个命令的输入。
  • 通过管道组合功能单一的过滤器,是发挥 Shell 能力的重要方式。

原文:Pipes and Filters,Software Carpentry 课程维护者与贡献者;页面标注更新日期为 2026-03-24。Copyright (c) The Carpentries。教学材料及原图采用 CC BY 4.0。本页将正文译为中文,并校正 wc 的字节/字符表述、补充删除命令和计数口径提示;代码和示例输出按原文保留。原项目许可。

示例软件 MIT 许可声明

Copyright (c) The Carpentries.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容