第20章:文本处理
所有类 Unix 操作系统都重度依赖文本文件进行数据存储。所以有许多操作文本的工具,也就很合理了。本章中,我们将学习用来「切片和切块」文本的程序。下一章中,将学习更多的文本处理,聚焦那些用来格式化文本以供打印和其它类型的人类需求的程序。
本章中,将重访几个老朋友并介绍几个新朋友:
cat
串联文件并在标准输出中打印sort
对文本文件的行排序uniq
报告或忽略重复的行cut
从文件中的每一行中移除部分paste
合并文件的行join
在公共字段上加入两个文件的行comm
逐行比较两个排序过的文件diff
逐行比较两个文件patch
将差异文件应用到一个原本中tr
翻译或删除字符sed
用于过滤和转换文本的流编辑器aspell
交互式拼写检查
文本应用程序
迄今我们已经学过一对文本编辑器(nano
和 vim
),看过一堆配置文件,也见证了许多命令的输出,所有这些,都是文本。但是,文字还用来做什么?事实证明,还有许多。
文档
许多用户使用普通文本格式撰写文档。不难理解小型文本文件对于保持简单记录会很有用,同时,用文本格式来撰写大型文档也是可能的。一个流行的途径是用文本格式写大型文档,然后嵌入一个标记语言(markup language)来描述所完成文档的格式。许多科学论文就是以这种方式撰写的,因为基于 Unix 的文本处理系统是第一批支持技术学者所需的高级排印输出需求的系统之一。
网页
世界上最流行的电子文档格式可能就是网页了。网页是使用超文本标记语言(HTML Hypertext Markup Language)或可扩展标记语言(XML Extensible Markup Language)作为标记语言以描述文档可视格式的文本文档。
电子邮件
电子邮件本质上是基于文本的媒体。即便是非文本的附件也是被转换成文本表示进行传输。我们可以下载一个电子邮件信息,用 less
来查看。可以看到信息以一个头部(header)开始,它描述信息的来源及其在路途上收到的处理信息,随后是作为该信息的主体(body)的信息内容。
打印机输出
在类 Unix 系统上,发往打印机的输出以纯文本形式发送,或者,如果页面包含图形,则会被转换成文本格式的被称为 PostScript 的页面描述语言(page description language)的文本格式,然后送到生成图形点的程序,去打印。
程序源代码
许多在类 Unix 系统上找到的命令行程序,被创建来支持系统管理员和软件开发人员,文本处理程序也不例外。许多程序都是被设计来解决软件开发问题。文本处理对软件开发人员很重要的原因是所有软件都以文本形式开始。源代码(source code),程序员所编写的程序部分,总是文本格式的。
重访老友
回到第 6 章「重定向」,我们学习了一些可以接收标准输入为命令行参数的命令。当时仅是很简略的接触了一下,现在将更近距离地来看它们是如何被用来执行文本处理的。
cat
cat
程序有很多有趣的选项。很多都是用来帮助更好地使文本内容可视化。一个例子是 -A
选项,用来显示文本中的非打印字符。有时候我们想要知道是否有控制字符被内嵌到其它可视文本中。最常见就是制表符(而非空格)和回车符,它通常出现在 MS-DOS 样式的文本文件行末。另一个常见的状况是一个包含了多行有尾随空格的文本文件。
让我们用 cat
作为原始的字处理程序来创建一个测试文件。我们仅需要键入命令 cat
(随后指定一个文件以供重定向输出),然后键入文本,用 Enter
结束一行文本,用 Ctrl-d
来告诉 cat
我们已经到达文件的末尾。本例中,我们键入了一个前置制表符,并在行末加了一些空格:
接下来用 cat -A
来显示文本:
如我们在结果中看到的,文本中的制表符由 ^I
代表。这是一个常见的记号,表示 Ctrl-i
,事实证明,是和制表符相同的。还看到一个 $
出现在真正的行尾,显示我们的文本含有尾随空格。
MS-DOS 文本和 Unix 文本
你想要使用
cat
的一个理由,可能是想寻找文本中非打印字符,来识别隐藏的回车符。隐藏的回车符从哪里来?DOS 和 Windows!Unix 和 DOS 定义文本文件行尾的方式不一样。Unix 以一个换行符(ASCII 10)结束一行,MS-DOS 及其衍生系统使用回车符(ASCII 13)和换行符序列终结文本中的每一行。有几种途径可以将文件从 DOS 格式转换为 Unix 格式。在很多 Linux 系统上,有
dos2unix
和unix2dos
这种可以转换格式的程序。当然,如果你的系统上没有dos2unix
程序,也不用担心。从 DOS 格式转换到 Unix 格式的进程很简单,涉及移除不合规的回车符。可以通过本章后续会讨论到的一些程序很方便地完成。
cat
还有可以用来修改文本的选项。两个最主要的是可以加行号的 -n
和可以抑制空白行输出的 -s
。演示如下:
本例中,我们创建了一个新版本的 foo.txt
测试文件,包含两行由两个空白行分隔的文本。用带 -ns
选项的 cat
处理之后,额外的空白行被移除,剩下的行被编上了行号。这不是对文本执行的操作,只是一个进程。
sort
sort
程序对标准输入的内容、一个或多个在命令行中被指定的文件排序,并将结果送至标准输出。使用与 cat
中相同的技术,可以直接从键盘演示标准输入的进程如下:
键入命令之后,我们输入了 c
、b
、a
,然后按 Ctrl-d
指示文件结束。然后我们查看生成的文件,并看到三行文字已经按序显示了。
由于 sort
可以接受命令行上的多个文件作为参数,所以可以合并(merge)多个文件,使之成为一个排好序的单个文件。例如,如果我们有三个文本文件,想要合并为一个按序排列的文件,可以这样做:
sort
有几个有趣的选项。表 20-1 包含了部分列表:
表 20-1:常见 sort 选项
选项
长选项
描述
-b
--ignore-leading-blanks
默认情况下,是对整行排序,从行内的第一个字符开始。该选项会导致 sort
忽略行内前置空白字符,按行内第一个非空白字符为基础计算排序。
-f
--ignore-case
使排序对大小写不敏感。
-n
--numeric-sort
基于字符串的数字评估进行排序。使用此选项可以对数值而不是字母值执行排序。
-r
--reverse
倒序排列。结果以降序而非升序排列。
-k
--key=field1[,field2]
基于从 field1
到 field2
的关键字段排序,而非按整行排序。详见后续讨论。
-m
--merge
将每个参数当作已经按文件名预先排序好了的。在合并多个文件到单个文件时,不会执行附加的排序行为。
-o
--output=file
将排序结果输出到 file
文件,而不是到标准输出。
-t
--field-separator=char
定义字段分隔字符。默认的字段以空格或制表符分隔。
尽管这些选项多数都是可以顾名思义的,有些却还不是。首先来看下用作数字排序的 -n
选项。使用该选项,可以按数字的值为排序依据。可以通过对 du
命令的结果排序来确定谁用了最多的磁盘空间来演示。通常情况下,du
命令按路径名顺序列出摘要结果。
在这个例子中,我们将结果管道输入给 head
以限制结果仅显示前十项。我们还可以按此方法来制作一份按数字排序的列表,来显示前十个最大的磁盘空间消耗者。
通过 n
和 r
选项,我们制作了一份降序数字排序,将最大值显示在结果的第一行。之所以能排序是因为数字出现在每行的开头。但是如果我们想要对一个列表基于行内某些数值排序呢?例如,这里有一份 ls -l
的结果:
暂时忽略,ls
可以按结果的大小排序,用 sort
对文件尺寸排序。
许多 sort
的使用牵涉到表格数据(tabular data)的处理,如上面 ls
命令的结果。如果对上表应用数据库术语,那么每一行就是一条记录(record),每条记录由多个字段(field)组成,如文件属性、链接计数、文件名、文件大小、等等。sort
可以处理单个字段。在数据库术语中,我们可以指定一个或多个关键字段(key fields)作为排序键(sort keys)。在前例中,我们指定了 n
和 r
选项,来执行一个倒序的基于数字的排序,并指定了 -k 5
用第五个字段为排序键。
k
选项很有意思,有很多功能,不过,我们首先需要讨论的是 sort
如何定义字段。来考虑下面这个简单的一行文本文件,包含了作者姓名。
默认情况下,sort
将这一行视为具有两个字段。第一个字段包含这些字符:
"William"
第二个字段包含这些字符:
"Shotts"
这意味着空白字符(空格和制表符)被用来作为字段间的界定符,当执行排序时,界定符被包含在字段中。
再来看一下 ls
的输出,我们可以看到一行包含了八个字段,其中第五个是文件尺寸:
为了下一系列的体验,考虑下面的文件,包含从 2006 到 2008 年三个主流 Linux 发行版的历史记录。每一行会有三个字段:发行版的名称、版本、和 MM/DD/YYYY 格式的发行日期。
使用文本编辑器(或许是 vim),我们输入这些数据,将结果保存为 distros.txt
文件。
然后,试着对文件排序,并观察结果:
好,基本上可以了。问题发生在 Fedora 版本数字的排序上。因为 1 在字符集里的顺序在 5 之前,版本 10 最终列在版本顶端,而版本 9 则落到了底部。
要解决这个问题,我们将不得不按多个键来排序。我们想对第一个字段按字母顺序排序,而后对第二个字段按数字排序。sort
允许 -k
选项有多个实例,所以可以指定多个排序键。事实上,一个键可以包含一个序列的字段。如果没指定什么序列(如我们已经在之前的示例中所见),sort
使用一个以指定字段开始并扩展到行尾的键。这里是多键排序的句法:
尽管我们用了长格式选项来声明,-k 1,1 -k 2n
这样的短格式也是等价的。在第一个键选项的实例中,我们指定了要包含在第一个键中的一系列字段。因为我们想要将排序限制在第一个字段,所以指定了 1,1
,意思是「从第一个字段开始,在第一个字段结束」。在第二个实例中,我们指定了 2n
,意思是第二哥字段是排序键,并应按数字大小排序。可以在键指定符的末尾包含一个可选字符,来指示用来执行排序的类型。这些选项字母是 sort
程序中的全局选项:b
(忽略前置空白字符)、n
(按数字排序)、r
(反向排序)等等。
列表中的第三个字段是日期,其格式不便于排序。我们的计算机里,日期通常是以 YYYY-MM-DD 的顺序为格式,以便按时间顺序排序,但是我们现在用的是 MM/DD/YYYY 的美国格式,该如何让我们用时间顺序对此列表排序呢?
幸运的是,sort
提供了一个方法。键选项允许在字段中指定偏移量(offsets),所以我们就可以在字段中定义键了。
通过指定 -k 3.7
来指示 sort
使用第三个字段中从第七个字符开始作为排序键,也就是对应的年份。同样,我们指定 -k 3.1
和 -k 3.4
来分离日期中的月份和日期。还加上了 n
和 r
选项以构建反向数字排序。包含 b
选项以抑制日期字段中的前置空白字符(每行之间的数字不同,可能会影响排序输出)。
有些文件不会使用制表符和空格作为字段分隔符,如 /etc/passwd
文件:
在这个文件中的字段由冒号(:
)分隔,那我们该如何用一个键字段排序呢?sort
提供了 -t
选项来定义字段分隔字符。要按第七个字段(帐户默认 shell)对 passwd
文件排序,可以这样操作:
通过指定冒号字符为字段分隔符,就可以按第七个字段进行排序。
uniq
相对于 sort
,uniq
程序是轻量的。uniq
执行一个看起来很小的微不足道的任务。当给出一份已经排序的文件(或在标准输出),它可以移除任何重复行,并将结果输出到标准输出。它通常用来和 sort
连接使用,以清理输出中的重复项。
提示:
uniq
是一款经常和sort
一起使用的传统 Unix 工具,而 GNU 版本的sort
则提供了一个-u
选项,可以从已经排序好的输出中移除重复项。
让我们来制作一个文件,测试一下:
记得用 Ctrl-d
终止标准输入。现在如果我们对文本文件运行 uniq
,将得到:
结果和原始文件没有任何不同;重复项没有被移除。要让 uniq
起作用,则其输入必须是已经排序好了的。
因为 uniq
仅移除彼此相邻的重复行。
uniq
有几个选项,表 20-2 列出了常用的几个。
表 20-2:常见 uniq 选项
选项
长选项
描述
-c
--count
输出重复行的列表,前面是行出现的次数。
-d
--repeated
仅输出重复的行,而非唯一的行。
-f n
--skip-fields=n
忽略每行中的前 n 个字段。字段如 sort
一般,由空白字符分隔;然而和 sort
不同,uniq
没有选项支持替代的字段分隔符。
-i
--ignore-case
在比较行的过程中忽略大小写区分。
-s n
--skip-chars=n
跳过(忽略)每行前面的 n 个字符。
-u
--unique
仅输出唯一的行。忽略那些重复了的行。
这里我们用 -c
选项来看 uniq
报告我们文本文件中的重复次数。
切片和切块
接下来要讨论的三个程序,用来从文件中剥离文本列,然后以有用的方式重新整合起来。
剪切 cut
cut
程序用来从一行中提取一部分文本将被提取的部分输出到标准输出。它可以接收多个文件参数,或从标准输入中输入。
指定要提取的行中的部分,是有些别扭,并被指定使用表 20-3 中所列的选项列表。
表 20-3:cut 选择选项
选项
长选项
描述
-c list
--characters=list
提取被定义为 list 的部分行。list 可能由一个或多个逗号分隔的数字序列组成。
-f list
--fields=list
提取被定义为 list 的一个或多个字段。list 可以包含一个或多个由逗号分隔的字段或字段序列。
-d delim
--delimeter=delim
当指定 -f
时,使用 delim 作为字段分隔字符。默认情况下,字段必须由一个制表符分隔。
--complement
提取整行,除了那些被 -c
和/或 -f
指定的部分。
我们可以看到,cut
提取文本的方式非常不灵活。cut
最好是用来提取那些其它程序产生的文本文件,而非直接由人类输入的文本。我们将看一下我们的 distros.txt
文件,看看它是否「干净」,足以成为我们 cut
示例的好样本。如果用带 -A
选项的 cat
,可以查看文件是否符合制表符分隔的需求:
看来不错。没有内嵌的空格,在字段间只有一个制表符。由于文件使用制表符而非空格,我们将用 -f
选项来提取一个字段。
因为 distros
是由制表符分隔的,最好用 cut
来提取字段。这是因为一个由制表符分隔的文件和每行包含相同数量的字符的文件不同,后者使计算行内的字符位置变得困难或不可能。在之前的例子中,无论如何,我们现在已经提取了一个字段,很幸运,该字段包含相同的字符长度,所以我们可以展示一下如何提取每行中的年份。
通过第二次对列表运行 cut
,我们可以提取出第 7 到 10 位置的字符,也就是日期字段中相应的年份。7-10
标记,是一个序列的示例。对于如何指定序列,cut
手册页中有完整的描述。
扩展制表符
distros.txt
文件的格式非常适合cut
提取字段。但是,如果我们想要的是一个能被cut
完全操作字符而非字段的文件呢?这就会要求我们将文件中的制表符用相应数量的空格替换掉。幸运的是,GNU 核心工具包里有一个这样的工具。名为expand
的程序接收一个或多个文件参数或者标准输入,并将修改过的文本输出到标准输出。如果我们用
expand
来处理distros.txt
文件,就可以用cut -c
来提取文件中任意序列的字符。例如,我们可以通过扩展文件、使用cut
提取从第 23 个位置到行末的字符的方法,用下列命令从列表中提取发布的年份:核心工具包还提供了一个
unexpand
程序来用制表符替换空格。
当使用字段时,可以指定不同的字段分隔符,不仅仅是制表符。现在可以从 /etc/passwd
文件中提取第一个字段:
用 -d
选项,可以指定冒号为字段分隔符。
粘贴 paste
paste
命令所做的,和 cut
相反。不是从文件中提取一列文本,而是将一列或多列文本加到文件中。通过读取多个文件并且将每个文件中找到的字段合并到标准输出,成为一个单一的流。和 cut
一样,paste
接收多个文件参数和/或标准输入。要演示 paste
如何操作,需要对 distros.txt
文件执行某些手术,以便制作一个按时间排序的发行版清单。
从之前 sort
的工作中,我们先制作一份按日期排序的发行版清单,并将结果存储为 distros-by-date.txt
。
接下来,我们用 cut
提取文件中的第一二个字段(发行版名称和版本),将结果存储为 distro-versions.txt
文件。
最后的准备是提取发行日期,并存储为 distro-dates.txt
文件。
现在我们有了所需的各个部分。要完成整个进程,使用 paste
将日期列放在发行版名称和版本之前,就创建了一个按日期排序的清单了。用 paste
把参数按想要的顺序排列,就可以轻松完成了。
连接 join
在某些方面,join
和 paste
一样可以将一列文本添加到一个文件中,不过它使用的一个独特的办法来完成这个工作。连接(join)通常和关系型数据库(relational databases)有关,数据库里的数据都存放在多个表(table)内,用一个共享的键字段组合成想要得到的结果。join
程序执行相同的操作。它从多个文件中基于一个共享的键字段来连接数据。
要知道在关系型数据库中如何使用结合操作,让我们想象一个由两张表组成的小数据库,每张表内含一条记录。第一张表,名为 CUSTOMERS
,有三个字段,一个客户编号(CUSTNUM
)、一个客户的名(FNAME
)和一个客户的姓(LNAME
):
第二张表叫 ORDERS
,有四个字段:一个订单编号(ORDERNUM
)、客户编号(CUSTNUM
)、数量(QUAN
)和订购的产品(ITEM
)。
两张表都有 CUSTNUM
字段。这很重要,因为这使得两张表之间有了关系。
执行连接操作允许我们将两张表里的字段整合在一起,构建一个合意的结果,如准备一张发票。使用两张表里的 CUSTNUM
字段的匹配值,连接操作可以生成这样的结果:
要演示 join
程序,需要制作一些具有共享键的文件。要做到这一点,可以用 distros-by-date.txt
文件。从这个文件中,可以构建两个其它文件。一个包含了发行日期(在这次演示中将会是我们的共享键)和发行版名称,如下所示:
第二个文件包含发行日期和版本号,如下所示:
现在,我们有了两个具有共享键(「发行日期」字段)的文件。有一点很重要,为了 join
能正常工作,文件必须按键字段排序。
还是那样,默认地,join
使用空白字符作为输入字段的分隔符,并用一个空格作为输出字段的分隔符。这个行为可以通过指定选项来修改。具体请查看 join
的手册页。
比较文本
对比文本文件的版本,常常是有用的。对系统管理员和软件开发人员,这特别重要。一个系统管理员可能,举个例子,需要将一个现有的配置文件和前一版本的作比较以诊断系统问题。同样的,程序员常常需要查看随着时间的推移,都对程序作了哪些变更。
comm
comm
程序比较两个文本文件,并显示每个文件中特有的行以及二者共有的行。要演示这一点,我们将用 cat
创建两个差不多相同的文本文件。
然后,我们用 comm
对比两个文件:
可以看到,comm
生成了三列输出。第一列包含第一个文件参数中特有的行,第二列包含第二个文件参数中特有的行,第三列包含两者公有的行。comm
支持 -n
形式的选项,n
可以是 1,2 或者是 3。当使用时,会抑制显示这些选项指定的列。例如,如果我们想仅输出两者公有的行,我们应该抑制显示输出中的第一和第二列。
diff
和 comm
程序一样,diff
用来检测文件间的差异。然而,diff
是一款更复杂的工具,支持许多输出格式并具有即刻处理大文本文件的能力。diff
经常被软件开发者用来测试程序源代码的不同版本间的差异,因此有递归检查源代码目录的能力,通常被称为源代码树。diff
常见的一个用法是创建差异文件(diff files)或补丁文件(patches),以便如(很快就会讨论到的)patch
之类的程序将一个文件(或多个文件)的某个版本转换为另一个版本。
如果我们用 diff
来查看之前的示例文件:
我们看到其输出的默认样式:对两个文件间差异的紧凑的描述。在默认的格式中,每组变化由一个以范围操作范围(range operation range)形式的变更命令(change command)开头,描述第一个文件变更为第二个文件所需作出变更的位置和类型,如表 20-4 所列大纲。
表 20-4:diff 变更命令
变更
描述
r1ar2
将第二个文件中的 r2
位置的行附加(Append)到第一个文件中的 r1
位置。
r1cr2
用第二个文件中的 r2
位置的行变更(Change)或替换(replace)第一个文件中的 r1
位置。
r1dr2
删除第一个文件中 r1
位置的行,这些行会出现在第二个文件的 r2
位置。
在这个格式中,一个序列是从开始行到结束行的逗号分隔列表。该格式是默认的(主要供 POSIX 兼容,并向后兼容传统的 Unix 版本的 diff
),却不被其它程序广泛采用。两种更流行的格式是上下文格式(context format)和统一格式(unified format)。
当用上下文格式(-c
选项)查看时,可以看到这样:
输出以两个文件的名称及其时间戳开始。第一个文件由星号标记,第二个文件由短横标记。整个清单的余下的部分,这些标记将表示它们各自的文件。接下来,我们看到一组变更,包含周围上下文的行号。在第一组中,是:
指示在第一个文件中的第一到第四行。随后看到:
指示在第二个文件中的第一到第四行。在一组变化中,一行以四种指示符中的一个开头。指示符如表 20-5 所示。
表 20-5:diff 上下文格式变更指示符
指示符
意义
空白
显示上下文的一行。 它不表示两个文件之间的差异。
-
被删去的一行。该行仅出现在第一个文件中,没有出现在第二个文件中。
+
新增的行。该行仅出现在第二个文件中,没有出现在第一个文件中。
!
变更的行。将显示该行的两个版本,每个版本都在更改组的相应部分中。
统一格式和上下文格式类似,不过更加简洁。用 -u
选项指定。
上下文格式和统一格式之间最显著的差异是上下文中重复的行消失了,使得统一格式比上下文格式更短。在上面的例子中,我们看到了和上下文格式中相似的文件时间戳,随后是字符串 @@ -1,4 +1.4 @@
。这指示了变更组中所描述的第一个和第二个文件中的行号。在这之后,就是这些文本行自身了,默认三行上下文。每行都会以三个可能的字符开头,如表 20-6 所示。
表 20-6:diff 统一格式变更指示符
字符
意义
空白
该行是两个文件所共有的。
-
该行已经从第一个文件中被移除。
+
该行被加到第一个文件中。
patch
patch
程序用来将变更应用到文本文件。它接收 diff
的输出,一般情况下用来将老版本文件转换为新版本。让我们考虑一个著名的示例。Linux 核心由一个巨大、组织松散的贡献者团队所开发,持续提交一些微小的变更给源码。Linux 内核有几百万行代码,一个贡献者一次所作的变更是非常小的。每次进行小的更改时,贡献者都会向每个开发人员发送整个内核源代码树是没有意义的。取而代之的是,提交一个差异文件。这个差异文件包含了前一版本到贡献者提交变更的新版本之间的差异。随后,接收者会用 patch
程序将这些变更应用到他自己的源代码树中。使用 diff/patch
提供了两个显著优势。
差异文件相对与完整的源代码树是很小的。
差异文件简洁明了的显示了所作的变更,允许补丁的复查者快速评估。
当然,diff/patch
可以工作在任意文本文件上,不是仅限于源代码。也可以等效地应用在配置文件或其它任意文本中。
要准备一个 diff
文件供 patch
使用,GNU 文档(查看下方的扩展阅读)建议按下列所示使用 diff
:
old_file
和 new_file
都是单个文件或包含文件的目录。r
选项支持在目录树中递归。
当创建了差异文件后,就可以将其应用到旧文件,生成新文件。
我们用自己的测试文件来演示。
本例中,我们创建了一个名为 patchfile.txt
的差异文件,随后用 patch
程序应用了这个补丁。注意,我们没有必要指定目标文件给 patch
,因为差异文件(在统一格式中)已经将文件名包含在头部了。当应用了补丁之后,我们可以看到 file1.txt
现在和 file2.txt
保持一致了。
patch
有大量的选项,也有许多附加的工具程序可以用来分析和编辑补丁。
即时编辑
我们使用的文本编辑器很大程度上是交互(interactive)体验的,意味着我们可以手动地四处移动光标,然后键入我们的更改。然而,也存在那些非交互(non-interactive)的文本编辑方式。这是可能的,例如,用一个命令将一组变更应用到多个文件中。
tr
tr
程序用来直译(transliterate)字符。我们可以将其理解为一种基于字符的查找替换操作。直译是将一个字母变更为另一个字母的进程。例如,将小写字母转换为大写字母,就是一种直译。可以用 tr
执行如下的转换:
如我们所见,tr
对标准输入操作,将结果输出到标准输出。tr
接收两个参数:一组需要转换的字符,和一组相应的要转换到的字符。字符组可以有三种表达方式。
一个枚举列表。例如:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
字符序列。例如:
A-Z
。注意这种方式有时候因为区域排序顺序,会遇到与其它命令相同的问题,所以需要小心使用。POSIX 字符类。例如,
[:upper:]
大多数情况下,两组字符的长度应该相等;然而,前一组字符可以比第二组长,特别是我们想要将多个字符转换为单个字符的时候。
除了直译,tr
还允许从输入的字符串中简单地删除字符。本章早些时候我们讨论过从 MS-DOS 文本文件转换成 Unix 风格的文本的问题。要执行该转换,需要删去每行末的回车符。可以用 tr
执行如下操作:
其中 dos_file
是需要被转换的文件,unix_file
则是转换的结果。这种形式的命令行使用了转义序列 \r
来代表回车符。要查看 tr
所支持的转义序列和字符类的完整清单,可以用下列命令:
ROT13:不那么秘密的解码器环
tr
的一个有趣的应用是执行 ROT13 文本编码。ROT13 是一款基于简单替换暗号的小型加密系统。称 ROT13 为「加密」是很宽泛的说法,「文本混淆」才更准确。有时用来使某些潜在的令人反感的内容隐晦一些。该方法只是简单地将每个字符在字母表中的位置移动 13 位。由于这是 26 个字母的中间值,对文本第二次执行该算法时,会将其恢复为原始形式。用tr
执行该编码如下:再次执行相同的进程导致下列翻译:
一些电子邮件程序和新闻组阅读器支持 ROT13 编码。
维基百科有一篇关于该主题的文章:
tr
还可以执行另一个戏法。使用 -s
选项,tr
可以「挤压」(删除)一个字符的重复实例。
这里,我们有一个包含重复字符的字符串。将字符集 "ab" 指定到 tr
,我们就消除了该字符集的重复实例,同时留下没有包含在字符集中的 "c",不作更改。注意重复字符必须是相邻的。如果没有相邻,挤压就不会奏效。
sed
sed
得名于 stream editor 的缩写。它对一串文本执行编辑,无论是一组指定文件或是标准输入。sed
是强大且有些复杂的程序(关于它,有一整书),所以这里不会完全覆盖到它的功能。
一般情况,sed
的工作方式是被给到单个编辑命令(在命令行上)或者是一个包含多个命令的脚本的文件名,然后在文本流的每一行上执行这些命令。这里有一个简单的 sed
的示例:
本例中,我们用 echo
制造了一个单个词语流的文本,将其管道输入给 sed
。sed
则对文本执行 s/front/back
指令,并制造了 back
这一结果。我们还能识别出该命令和 vi
的替换(查找与替换)命令很相似。
sed
的命令以一个字母开头。前例中,替换命令由字母 s
表示并跟着搜索替换字符串,由斜杠符号作为分隔符。分隔字符的选择是很随意的。出于方便起见,经常使用斜杠字符,但是 sed
可以接收任何紧跟在命令后的字符作为分隔符。我们可以用这种形式执行同样的命令:
通过使用紧跟在命令后的下划线字符,使其成为了分隔符。可以设置分隔符的能力,使得命令变得更具可读性,我们随后就可以看到。
多数 sed
命令可以前置一个地址(address),用以指定哪几行的输入流需要编辑。如果省略了地址,则编辑命令会对输入流中的每一行执行编辑命令。地址的简单形式是一个行号。我们可以加到示例中:
将地址 1
加到命令中,导致在单行输入流的第一行中执行替换。如果指定了其它数字,我们可以看到不会执行编辑,因为我们的输入流中不存在第二行。
地址可以有很多表达方式。表 20-7 列出了常见的几种。
表 20-7:sed
地址记号
地址
描述
n
当 n
是一个正整数时,表示行号。
$
最后一行。
/regexp/
匹配 POSIX 基本正则表达式的行。注意正则表达式由斜杠字符界定。可选的,正则表达式可以有一个替代字符界定,用 \cregexpc
来指定的表达式中,c
就是替代字符。
addr1,addr2
从 addr1
到 addr2
的一个行序列,包含首尾两行。地址可以是上述所列的单个地址任何形式。
first~step
匹配由数字 first
表示的行,以及随后每间隔 step
的行。例如 1~2
指每个奇数行,5~5
指第 5 行和随后的每 5 行。
addr1,+n
匹配从 addr1
及其后 n
行。
addr!
匹配除了 addr
之外的所有行,addr
可以是上述所列的任何形式。
我们用本章早前的 distros.txt
文件来演示不同类型的地址。首先看一下行号序列:
这个例子中,我们打印了一个从第一行到第五行的行序列。我们简单地通过 p
命令来打印匹配的行。然而,要使其更有效,就必须要包含 -n
选项(「不自动打印」)以便使 sed
不默认地每行都打印。
接下来,我们尝试一个正则表达式。
通过包含斜杠分隔符的正则表达式 /SUSE/
,我们可以分离出那些包含这些字符的行,和 grep
的行为相同。
最后,加一个感叹号(!
)在地址上,来试一下否定。
这里,我们得到了期望得到的结果:文件里除了匹配正则表达式的全部的行。
目前为止,我们看到了两个 sed
编辑命令:s
和 p
。表 20-8 提供了更完整的基本编辑命令清单。
表 20-8:sed 基本编辑命令
命令
描述
=
输出当前行号。
a
将文本附加(append)到当前行。
d
删除(delete)当前行。
i
将文本插入(insert)到当前行之前。
p
打印当前行。默认的,sed
打印每一行,且仅编辑文件中匹配指定地址的行。这个默认行为可以通过指定 -n
选项来越过。
q
退出 sed
,不再处理更多的行。如果没有指定 -n
选项,就输出当前行。
Q
退出 sed
,不再处理更多的行。
s/regexp/replacement/
当找到 regexp
时就替换 replacement
。replacement
可以包含特定字符 &
,等价于匹配 regexp
的文本。另外,replacement
可以包含 序列 \1
到 \9
,即在 regexp
中相应的子序列。可以查看下面的 back references 以便获得更详细的内容。在 replacement
后的斜杠之后,可以指定可选标识来修改 s
命令的行为。
y/set1/set2
将 set1
中的字符相应地直译到 set2
中的字符。注意,和 tr
不同,sed
需要两个字符集具有相同的长度。
s
命令是目前最常用的编辑命令。我们会通过对 distros.txt
执行编辑来演示其力量。我们早先讨论过在 distros.txt
中的日期字段,不是一个「对计算机友好」的格式。相对于 MM/DD/YYYY
,YYYY-MM-DD
格式更好(易于排序)。手动执行这一更改,耗时且容易出错,不过用 sed
,就只需要一步。
哇!虽然命令看起来很丑,但是能工作。仅需一步,我们就变更了文件中的日期格式。它也是为什么正则表达式有时被戏称为「只写」媒体的完美示例。我们可以写出来,但有时候却读不懂。在我们被这命令吓跑之前,来看下它是如何构建的。首先,我们要知道这命令有其基本结构。
下一步是识别出隔离日期的正则表达式。因为日期格式是 MM/DD/YYYY
且显示在行末,我们可以使用这个表达式:
该表达式匹配:两个数字、一个斜杠、两个数字、一个斜杠、四个数字和行尾标记。这样就照顾到了 regexp
,但是 replacement
呢?要处理该问题,就必须介绍一个出现在某些应用程序中的使用基本正则表达式新功能。这个功能称之为反向引用(back reference),作用是这样的:如果序列 \n
出现在 replacement
中,n
是从 1 到 9 的数字,这个序列指向之前正则表达式中相应的子表达式。要创建子表达式,只要将其放入括号中就可以了,很简单,如下:
现在我们有三个子表达式了。第一个包含月份,第二个包含日期,第三个包含年份。现在可以构建出 replacement
如下:
这给出一个年份、一个短横、一个月份、一个短横和日期。
现在,我们的命令看起来是这样的:
还剩下两个问题。第一个是,在正则表达式中多余的斜杠会让 sed
尝试解释 s
命令时感到困惑。第二个是,因为 sed
默认情况下仅接收基本正则表达式,有几个在我们正则表达式中出现的字符会被解释为字面值而非元字符。我们可以用自由应用反斜杠来转义那些冲突字符,这样就解决了这两个问题。
这就是全部了!
s
命令的另一个功能是使用跟随在 replacement
后的可选标记。最重要的一个标记是 g
,该标记指示 sed
将搜索替换全局地应用到一行中,而不是如默认情况下,仅仅应用到第一个实例。这里是个例子:
我们看到确实执行了替换,但是仅仅是第一个字母 b
被替换了,剩下的几个实例则没有变更。通过加 g
标记,就可以替换全部实例了。
目前为止,我们仅通过命令行给 sed
一个单独的命令。还可以用 -f
选项,构建更复杂的命令为一个脚本文件。我们用 sed
操作 distros.txt
建立一个报表,来演示此功能。我们的报表会有一个标题,修改过格式的日期,同时将发行版名称都转换为大写字母。要做到这一点,需要写一个脚本,所以启动文本编辑器,键入下列内容:
将脚本保存为 distros.sed
并这样运行:
如我们所见,脚本制造了我们所期望的结果,但是是如何做到的呢?让我们再看一下脚本。用 cat
标上行号。
脚本的第 1 行是注释(comment)。和许多配置文件和 Linux 程序语言一样,注是释以 #
字符开头的可读性文本。注释可以放在脚本的任意位置(尽管不在命令之内),有助于可能需要读取和/或维护脚本的人。
第 2 行是空行。和注释一样,添加空白行有助于增进可读性。
许多 sed
命令支持行地址。用于指定要对哪些输入的行有所操作。行地址可以表示为单个行号、行号序列、和特殊行号 $
,即输入的最后一行。
第 3 到 6 行包含的文本被插入在地址 1,输入的第一行,i
命令后跟反斜杠及回车符,以产生一个转义回车符,或者被称为行继续符(line-continuation character)。这个序列,可以用在包括 shell 脚本在内的很多环境中,它允许将回车符嵌入到文本流中,而不给解释器(这里是 sed
)一个到达行末尾的信号。i
和 a
(附加文本而非插入)和 c
(替换文本)命令允许多行文本,除了最后一个,以行继续符结尾。脚本的第 6 行实际上是我们插入文本的结束,以一个普通的回车符结束,而不是行继续符,发出 i
命令的结束信号。
注意:一个行继续符由一个反斜杠紧跟一个回车符组成。中间不允许有空格。
第 7 行是我们的查找替换命令,因为没有前置一个地址,所以每行输入都受制于该行为。
第 8 行执行将小写字母转换到大写字母的直译。注意和 tr
不同,sed
的 y
命令不支持字符序列(如 [a-z]
),也不支持 POSIX 字符组。还有,因为 y
命令没有前置地址,所以会应用到输入流中的每一行。
喜欢
sed
的人还喜欢……
sed
是一款能干的程序,可以对文本流执行相当复杂的编辑任务。相比那些长脚本而言,它更多地被用在简单的单行任务中。许多用户面对大任务时,更喜欢其它工具。其中最流行的是awk
和perl
。这些已经超出像这里介绍的简单的工具程序,而扩展到完整的编程语言的领域了。perl
,特别是,常用来替代 shell 脚本,以供众多系统管理任务,同时也是一款流行的网页开发介质。awk
则更专业一些。它的特别能力是可以操作表格式的数据。它和sed
类似的是,通常也是逐行处理文本文件,使用类似sed
地址概念的方案,紧跟一个行为。awk
和perl
都超出了本书的范围,两者都是值得 Linux 命令行用户学习的技能。
aspell
最后要学习的工具是 aspell
,一个交互式的拼写检查工具。aspell
程序是一款更早期名为 ispell
的继承者,并在大多数情况下可用作替代程序。aspell
用在其它有拼写检查需求的程序中的同时,还可以作为一款有效的工具,独立用在命令行中。它具有智能检查各种文本变体的能力,包括 HTML 文档、C/C++ 程序、电子邮件和其它种类的文本。
要检查包含简单文本的文件拼写,用法如下:
其中 textfile
是要检查的文件名。作为一个实践,让我们创建一个简单的文本文件,命名为 foo.txt
,包含一些有意而为的拼写错误。
接下来用 aspell
检查文件:
aspell
在检查模式中是交互式的,我们可以看到像这样的一屏:
最上方,我们看到的是有拼写疑问的词被高亮显示了。在中间部分,是十条拼写建议,从 0 到 9 编号,紧跟着一个其它可能的行为列表。最后在底端,我们看到提示符等待接收我们的选择。
如果我们按 1
键,aspell
用 "jumped" 替换错误的单词并移动到下一个拼写错误的单词 "laxy"。如果选择 "lazy",aspell
就会替换掉,并结束程序。一旦 aspell
完成,我们可以检查一下文件并查看已经更正了拼写错误:
除非通过命令行选项 --dont-backup
被告知,aspell
会创建一个包含原始文本的备份文件,以 .bak
为扩展名。
炫耀一下我们的 sed
编辑实力。我们将拼写错误放回到文件中,以便能重新使用。
选项 -i
告诉 sed
就地(in-place)编辑文件,意思是不会将编辑后的结果输出到标准输出,而是会以变更直接重写文件。还看到用分号分隔,可以在一行中放置多个编辑命令。
接下来,会看到 aspell
如何处理不同类型的文本文件。使用文本编辑器如 vim
(胆大的可以尝试 sed
),在文件中加入一些 HTML 标记。
现在如果检查修改过的文件的拼写,我们就陷入一个问题了。如果:
我们会得到:
aspell
会将 HTML 标签视为拼写错误。这个问题可以通过加入检查模式选项 -H
(HTML)来克服,如:
结果如下:
HTML 被忽略了,仅文件中的检查非标记部分。该模式中,HTML 标签中的内容被忽略,不被检查拼写。不过 ALT 标记中的文件在此模式中还是会接受检查的。
注意:默认情况下,
aspell
会忽略文本中的 URL 地址和电子邮件地址。该行为可以通过命令行选项覆盖。还可以指定哪些标签需要检查,哪些需要跳过。请查看aspell
的手册页获取详细资料。
总结
本章中,学习了一些操作文本的命令行工具。下一章中,我们还要再学几个。不可否认,虽然我们试图展示一些使用它们的实际例子,但你可能在日常工作中使用这些工具的方式或原因似乎并不显而易见。我们将在后面的章节中发现,这些工具构成了用于解决许多实际问题的工具集的基础。特别是,当我们学习 shell 脚本时,这些工具才将真正展示它们的价值。
扩展阅读
GNU 项目网站包含了很多本章中讨论的工具的线上指导。
来自 Coreutils 包:
还有很多
sed
的线上资源,特别是:还可以搜索 "sed one liners"、"sed cheat sheets"
额外学分
还有一些更有趣的文本操作命令值得研究。如 split
(拆分文件)、csplit
(基于上下文拆分文件)和 sdiff
(文件差异的并排合并)。
Last updated
Was this helpful?