文本操作不仅仅是指复制和粘帖,在不使用 GUI 的情况下,这一点尤其明显。通过本文为 Linux Professional Institute Certification (LPIC) 101 考试做准备,或者仅仅是从中得到乐趣。在这篇文章中,Ian Shields 向您介绍了在 Linux? 上使用 GNU 文本实用程序(textutils)包中的过滤器进行文本操作。在阅读完本文后,您将能够像专家一样熟练地操作文本。
概述
本文提供了对过滤器(filter)的介绍,您将使用过滤器构建复杂的管道(pipeline)来操作文本。您将学习如何显示文本、执行排序、计算单词和行数、转换字符,以及其他任务。您还将了解到如何使用流编辑器 sed。
在本文中,您将了解以下主题:
通过文本实用过滤器发送文本文件和输出流,以修改输出
使用 GNU 文本实用程序包提供的标准 UNIX 命令
使用编辑器 sed 编写脚本,对文本文件做出复杂的修改
本文帮助您准备 Junior Level Administration (LPIC-1) 考试 101 中主题 103 下的目标 103.2。该目标的权值为 3。本文的内容与 2009 年 4 月的 考试 101 目标 相对应。您应当始终参考这些目标,获得明确的考试要求。
文本过滤
文本过滤指的是这样一个过程:获得文本的输入流,并在将文本发送给输出流之前对文本执行一些转换。尽管输入或输出都可以来自某个文件,但是在 Linux 和 UNIX? 环境中,实现过滤的最常见方法是构建一个命令管道,其中来自一个命令的输出被传输 或重定向 以用作下一个命令的输入。关于管道和重定向的内容,我们将在有关流、管道和重定向 的文章中更详细地介绍(可以在 学习 Linux,101: LPIC-1 路线图 中找到),不过现在先让我们看看使用 | 和 > 操作实现管道和基本的输出重定向。
流
流 只不过是一个可以使用库功能读取或写入的字节序列,库功能向应用程序隐藏了底层设备的细节。通过使用流,相同的程序可以使用独立于设备的方式从终端、文件或网络 socket 中读取,或向其中写入。现代编程环境和 shell 使用三种标准的 I/O 流:
- stdin 是标准输入流,为命令提供输入。
- stdout 是标准输出流,显示来自命令的输出。
- stderr 是标准错误流,显示命令中的错误输出。
使用 | 实现管道
输入可以来自您为命令提供的参数,输出可以显示到您的终端上。许多文本处理命令(过滤器)可以从标准输入流或从文件中获取输入。要将命令 command1 的输出作为过滤器 command2 的输入,您需要使用管道操作符 (|) 连接两个命令。清单 1 展示了如何传输 echo
的输出,以对一个小的单词列表进行分类。
清单 1. 将 echo 的输出传输到 sort 的输入
|
任何命令都包含选项或属性。您还可以使用 | 将管道中第二个命令的输出重定向到第三个命令,依此类推。在 Linux 和 UNIX 中,一种常见的执行任务的方式就是构建一个长的命令管道,每个命令只具备有限的功能。您有时会看到使用一个连字符 (-) 来代替文件名作为命令的参数,这意味着输入应当来自 stdin 而不是文件。
使用 > 重定向输出
创建由若干命令组成的管道并在终端观察输出固然不错,然而有些情况下您需要将输出保存到文件中。可以通过输出重定向操作符 (>) 实现这一点。
在本节后面的内容中,我们将使用一些小型文件,因此让我们创建一个名为 lpi103-2 的目录并通过 cd 进入到该目录。随后使用 > 将 echo
命令的输出重定向到名为 text1 的文件。清单 2 展示了整个过程。注意,输出并没有显示在终端上,因为它被重定向到了文件中。
启动 2. 将命令输出重定向到文件
|
现在,我们具备了一些基本的工具来实现管道和重定向,让我们研究一些常见的 UNIX 和 Linux 文本处理命令和过滤器。本节将展示一些基本的功能;查看对应的手册页来获得有关这些命令的详细内容。
Cat、od 和 split
现在您已经创建了 test1 文件,您可能需要检查其中的内容。使用 cat
(concatenate 的简写)命令显示 文件在使用标准输出时的内容。清单 3 检验了前面创建的文件的内容。
清单 3. 使用 cat 显示文件内容
|
如果没有指定文件名(或者如果指定 – 作为文件名)的话,cat
命令将从 stdin 获取输入。让我们结合使用输出重定向来创建另一个文本文件,如清单 4 所示。
清单 4. 使用 cat 创建一个文本文件
|
|
在清单 4 中,cat
将一直从 stdin 中读取文件,直到读完全部文件。使用 Ctrl-d(按下 Ctrl 和 d)组合键来表示已到文件末尾。使用相同的组合键来退出 bash shell。使用 tab 键将水果名排成一列。
注意,cat 是 concatenate 的简写,您可以使用 cat
将多个文件链接在一起一并显示。清单 5 展示了我们刚刚创建的两个文件。
清单 5. 使用 cat 链接两个文件
|
当您使用 cat
显示这两个文本文件时,将注意到一些排列差异。要了解造成这种差异的原因,需要查看文件中的控制字符。这些字符作用于文本显示输出,而不是显示控制字符本身的某些表示,因此我们需要以一种格式来转储 文件,这种格式允许您查找并解释这些特殊字符。GNU 文本实用工具包括了一个 od
(即 Octal Dump)命令,可以实现这一点。
od
提供了若干选项,比如 -A
选项可以控制文件偏移的基数,而 -t
选项可以控制显示的文件内容的形式。可以将基数指定为 o(默认的八进制)、d(十进制)、x(十六进制)或 n(未显示偏移)。您可以将输出显示为八进制、十六进制、十进制、浮点数、包含反斜杠转义符的 ASCII,或者指定的字符(nl 表示换行、ht 表示水平制表符,等等)。清单 6 展示了一些可用于转储 text2 示例文件的格式。
清单 6. 使用 od 转储文件
|
注意:
cat
的-A
选项提供了另一种方法来查看制表符和行结束符的位置。参见手册页获得更多内容。
- 如果您自己的 text2 文件中显示的是空格而不是制表符,那么请参考本文后面的 Expand、unexpand 和 tr 一节,了解在文件中如何在制表符和空格之间切换。
- 如果您拥有大型机方面的背景,那么很可能对 hexdump 工具感兴趣,该工具包含在另一个工具集中。本文并不打算介绍此工具,因此请查阅手册页。
我们的样例文件非常小,但是您有时会遇到特别大的文件,需要将其拆分为比较小的文件。例如,您可能希望将一个大文件拆分为 CD 大小的块,这样您就可以将其写入到 CD 中,以便通过邮件将其发送给可以为您创建 DVD 的人。split
命令将实现这一目的,并且在此过程中可以使用 cat
轻松地重新创建文件。默认情况下,由 split
命令生成的文件在其名称中使用 ‘x’ 作为前缀,后面使用 ‘aa’、‘ab’、‘ac’……‘ba’、‘bb’ 等作为后缀。可以使用选项修改这些默认设置。您还可以控制输出文件的大小,以及控制结果文件中包含的内容(包含整行或是仅包含字节计数)。
清单 7 演示了对两个文本文件执行分解的过程,对输出文件使用了不同的前缀。我们将 text1 分解为至多包含两行文本的文件,将 text2 分解为至多包含 18 个字节的文件。然后使用 cat
分别显示分解后的文件,并使用通配符(globbing)显示完整的文件,我们将在有关基本文件和目录管理 的文章中介绍通配符(可以在 学习 Linux,101: LPIC-1 路线图 中找到相关文章)。
清单 7. 使用 split 和 cat 分解和合并文件
|
注意,名为 yaa 的分解文件并未使用换行符表示结束,因此当使用 cat
显示提示之后,会发现提示出现了偏移。
Wc、head 和 tail
Cat
显示完整的文件。这非常适合本例中的小文件,但是假设您有一个非常大的文件。首先,您可能需要使用 wc
(Word Count)命令查看文件的大小。wc
命令将显示文件中所含的行、单词、字节的数量。您还可以通过 ls -l
命令获得字节数。清单 8 展示了我们的两个文本文件的长(long)格式的目录清单,以及 wc
的输出。
清单 8. 对文本文件使用 wc
|
可以使用选项控制 wc
的输出,或者显示其他信息,比如行的最大长度。参考手册页获得更多细节。
可以使用两个命令显示文件的开始部分(head)或结束部分(tail)。这两个命令就是 head
和 tail
。可以将它们用作过滤器,或者将文件名作为命令的参数。默认情况下,这两个命令将显示文件或流的前 10(或后 10)行。清单 9 使用 dmesg
命令显示启动信息,并结合使用 wc
、tail
和 head
执行下面的操作:发现共有 791 行消息,显示这些消息中的最后 10 条消息,最后显示倒数 15 条消息中的前 6 条消息。某些行在输出中被截短(使用 … 表示)。
清单 9. 使用 wc、head 和 tail 显示启动消息
|
tail
的另一个常见用法就是使用 -f
选项跟踪(follow)文件,通常使用一个行计数 1。当您拥有一个在文件中生成输出的后台进程并且希望执行签入以查看操作时,那么很可能需要利用这点。在这种模式下,tail
将一直运行,直到您将其取消(使用 Ctrl-c),它将在行被写入文件时显示它们。
Expand、unexpand 和 tr
在创建 text1 和 text2 文件时,我们使用制表符创建了 text2。有时候,您可能希望将制表符替换为空格,或者相反。expand
和 unexpand
命令可以实现这个目的。这两个命令的 -t
选项允许您设置选项卡停顿(stop)。设置一个单一值,它将以此为间隔设置重复的制表符。清单 10 展示了如何将 text2 中的制表符展开为单个空格,以及 expand
和 unexpand
如何将 text2 中的文本打散。
清单 10. 使用 expand 和 unexpand
|
不幸的是,您不能使用 unexpand
将 text1 中的空格替换为制表符,因为 unexpand
至少需要两个空格才能替换为制表符。然而,可以使用 tr
命令,它将一个集合(set1)中的字符转换为另一个集合(set2)中的对应字符。清单 11 展示如何使用 tr
将空格转换为制表符。由于 tr
是纯粹的过滤器,因此您将使用 cat
命令为其生成输入。这一示例也解释了如何使用 – 表示 cat
的标准输入,因此我们可以将 tr
的输出与 text2 文件连接起来。
清单 11. 使用 tr
|
如果您不能确定最后两个例子中发生的操作,那么尝试使用 od
来依次终止管道中的每个阶段;例如,cat text1 |tr ‘ ‘ ‘\t’ | od -tc
|
Pr、nl 和 fmt
pr
命令用于格式化文件以执行输出。默认的头部(header)包含文件名和文件创建日期和时间,以及一个页号和两行空白页脚。当从多个文件或标准输入流创建输出时,可以使用当前日期和时间替换文件名和创建日期。可以在列中并排输出文件并通过各种选项控制格式化的各个方面。请参考手册页获得详细信息。
nl
命令可以对行进行编号,这在输出文件时非常方便。也可以使用 cat
命令的 -n
选项对行进行编号。清单 12 展示了如何输出文本文件,以及如何为 text2 编号并将其与 text1 并排输出。
清单 12. 编号和格式化文件以方便输出
|
另一个用于格式化文件的有用命令是 fmt
命令,它将格式化文本以使其能够适合边距。您可以联接多个较短的行,以及拆分较长的行。在清单 13 中,我们使用 !#:*
历史特性的变体创建了包含较长一行文本的 text3,避免了四次输入句子的工作。我们还创建了每行只包含 1 个单词的 text4。然后使用 cat
显示未进行格式化的文件,其中包含一个 ‘$’ 字符表示行结束。最后,使用 fmt
将文件格式化为包含 60 字符的最大宽度。请参考手册页获得其他选项的介绍。
清单 13. 格式化为最大行长度
|
Sort 和 uniq
sort
将按照系统的 locale(LC_COLLATE)的排序序列来对输入进行排序。sort
命令还可以合并已经排序的文件,并检查某个文件是否已经排序。
清单 14 演示了在将 text1 中的空格转换为制表符后,使用 sort
命令对我们的两个文本文件执行排序。由于是按照字符排序的,因此您可能会对结果感到吃惊。幸运的是,sort
命令可以按照数字值或字符值进行排序。您可以针对整条记录或每个字段 指定排序选项。除非您指定了一个不同的字段分隔符,否则将使用空格或制表符分隔字段。清单 14 中的第二个例子显示对第一个字段按照数字顺序排序,而对第二个字段按照字母顺序排序。它还演示了使用 -u
选项来删除任何重复的行,因此只留下唯一的行。
清单 14. 按字符和数值排序
|
注意,我们仍然留有两个包含水果 “apple” 的行,但是刚才已经对两种排序键进行了唯一性检测,在我们的例子中为 k1n 和 k2。想一想如何修改上面的管道或添加一些步骤,以去掉出现的第二个 ‘apple’。
另一个名为 uniq
的命令为我们提供了另一种删除重复行的方式。uniq
命令通常操作已排序的文件,并从文件(不管是否排序)中删除连续的相同行。uniq
命令还可以忽略某些字段。清单 15 将使用第二个字段(水果名)对两个文本文件进行排序,然后去掉相同的行,排序将从第二个字段开始(也就是说,我们在使用 uniq
检测时将跳过第一个字段)。
清单 15. 使用 uniq
|
我们的排序是按照字符序列进行的,因此 uniq
显示的是 “10 apple” 行,而不是 “1 apple”。尝试对关键字段 1 添加一个数值排序,看看如何改变这个结果。
Cut、paste 和 join
让我们看看另外三个命令,它们可以处理文本数据中的字段。这些命令对于处理表格表数据尤其有用。第一个是 cut
命令,它可以从文本文件中提取字段。默认的字段分隔符是制表符。清单 16 使用 cut
来分隔 text2 中的两个列,然后使用空格作为输出分隔符,这是一种将每行中的制表符替换为空格的好方法。
清单 16. 使用 cut
|
paste
命令可以并行粘帖来自两个或多个文件的行,其方式类似于 pr
命令使用其 -m
选项合并文件。清单 17 展示了粘帖两个文本文件的结果。
清单 17. 粘帖文件
|
这些例子展会了比较简单的粘帖,但是 paste
可以使用多种其他方式粘帖来自一个或多个文件的数据。请参考手册页获得详细信息。
最后一个字段操作命令为 join
,它将根据匹配的字段连接文件。这些文件应当根据 join 字段排序。由于 text2 并不是按数值排序的,因此可以对它进行排序并使用 join 命令将具有匹配的 join 字段(在本例中指值为 3 的字段)的两行连接起来。
清单 18. 使用 join 字段连接文件
|
哪里出错了?回忆一下 Sort 和 uniq 小节中介绍的字符和数值排序。将根据 locale 的排序序列对匹配的字符执行 join 命令。它不会对数值字段生效,除非这些字段具有一致的长度。
我们使用 -j 1
选项连接每个文件的字段 1。用于 join 的字段可能会针对每个文件进行单独指定。比如,在执行连接时,可以对某个文件使用字段 3,而对另一个文件使用字段 10。
让我们根据第二个字段(水果名)对 text 1 进行排序,从而生成一个新文件 text5,然后使用制表符替换空格。如果我们针对 text2 的第二个字段对其排序,然后使用每个文件的第二个字段作为 join 字段来将 text2 和 text5 连接起来,那么我们应该得到两个匹配(apple 和 banana)。清单 19 解释了这一连接。
清单 19. 使用 join 字段连接文件
|
Sed
Sed 是一个 流编辑器。一些 developerWorks 文章以及众多书籍和图书章节都介绍了 sed。Sed 的功能极其强大,对它的唯一限制也许就是您的想象力。下面的简单介绍将唤起您对 sed 的兴趣,但是这里并不打算给出全面或详细的介绍。
如同我们目前已经了解的许多文本命令一样,seb 可以充当一个过滤器,或者从文件中获取其输入。输出将被放入到标准输出流。sed 将输出中的行加载到模式空间(pattern space),对模式空间的内容应用 sed 编辑命令,然后将模式空间写入到标准输出中。Sed 可以在模式空间中组合多个行,然后它可能将输出写入到文件中、只写入选择的输出,或者根本不执行写入。
Sed 使用正则表达式语法在模式空间中搜索并有选择地替换文本,以及决定对哪些文本行应用编辑命令。我们将在有关使用正则表达式搜索文本文件 的文章中更详细地介绍正则表达式(可以在 学习 Linux,101: LPIC-1 路线图 中找到)。保持缓存 为文本提供了临时存储。保持缓存可以取代模式空间、被添加到模式空间,或者与模式空间进行互换。Sed 提供了一组有限的命令,但是这些命令结合了正则表达式语法和保持缓存,因而实现了某些非常令人吃惊的功能。sed 命令集常常被称为 sed 脚本。
清单 20 展示了三个简单的 sed 脚本。在第一个脚本中,我们使用 s
(替换)命令来将每一行中的小写 ‘a’ 替换为大写。这个例子仅替换了第一个 ‘a’,因此在第二个例子中,我们添加了 ‘g’(表示全局)标记,以使 sed 替换出现的所有小写 ‘a’。在第三个脚本中,我们引入 d
(删除)命令来删除某行。在我们的例子中,我们使用地址 2 来表示只删除第 2 行。我们使用分号(;)分隔命令,并使用第二个脚本中用到的全局替换来将 ‘a’ 替换为 ‘A’。
清单 20. sed 脚本
|
除了操作单独的行外,sed 还可以操作一个行范围。行的起始和结束由一个逗号(,)分隔,并且可以被指定为行号、正则表达式或表示文件结束的美元符号($)。对于一个地址或一个地址范围,可以在大括号 { 和 } 之间对命令进行分组,使这些命令只用于按范围选择的行。清单 21 展示了对文件的最后两行应用全局替代的两种方式。它还演示了使用 -e
选项向脚本添加多个命令。
清单 21. Sed 地址
|
Sed 脚本也可以存储到文件中。事实上,您很可能希望对经常使用的脚本执行这个操作。回忆一下,我们前面使用 tr
命令将 text1 中的空格修改为制表符。现在让我们使用存储在文件中的 sed 脚本来实现相同的操作。我们将使用 echo
命令来创建文件。结果如清单 22 所示。
清单 22. sed 单行脚本
|
还有许多像清单 22 这样方便的 sed 单行脚本。
最后一个 sed 示例使用 =
命令输出行号,然后再次使用 sed 过滤生成的输出,以模拟使用 nl
命令对行进行编号的效果。清单 23 使用 =
输出行号,然后使用 N
命令将第二个输入行读取到模式空间,最后删除模式空间中两个行之间的换行符(\n)。
|
和我们期望的还有一定差距!我们真正想要的是将编号放到一列中,并在行的前面留一些空格。在清单 24 中,我们输入几行命令(注意第二个 > 提示)。研究这个例子并参考下面的解释。
清单 24. 再一次使用 sed 对行进行编号
|
下面解释了我们执行的操作:
- 我们首先使用
cat
从 text1 和 text2 的副本中创建一个包含 12 行的文件。如果编号的数位相同的话,那么对列中的编号进行格式化则没有什么乐趣。
- bash shell 使用制表键表示命令结束,因此当我们需要一个真正的制表符时,使用一个专用 tab 字符将非常方便。我们使用
echo
命令实现这点并将字符保存到 shell 变量 ‘ht’ 中。
- 像前面一样,我们创建了一个包含行号(后跟数据行)的流,然后通过另一个 sed 副本进行过滤。
- 我们将第二个行读入到模式空间中。
- 在模式的开始部分,我们将 6 个空格添加到行号前面,作为前缀(使用 ^ 表示)。
- 随后使用换行符前面的最后 6 个字符再加上制表符来替换换行符前面的所有行内容。这将把行号排列到输出行的前 6 个列中。注意 ‘s’ 命令的左侧使用 ‘\(’ 和 ‘\)’ 标记我们希望在右侧使用的字符。在右侧部分中,我们将第一个被标记的集合(在本例中只有一个集合)引用为 \1。注意,我们的命令被包含在双引号之间,因此将对 $ht 执行替换。
sed 的第四版包含 info
格式的文档,并附带了许多出色的示例。这些内容都未包含在较早的 3.02 版本中。GNU sed 将接受 sed –version
来显示该版本。