所有类Unix操作系统都严重依赖文本文件进行数据存储。因此,有许多工具可以操纵文本是有道理的。在本章中,我们将介绍用于【切片(slice)和切块(dice)】文本的程序。在下一章中,我们将介绍更多的文本处理,重点介绍用于格式化文本以供打印和其他人类消费的程序。
本章将重温一些老朋友,并向我们介绍一些新朋友:
cat
—— 连接(concatenate)文件并在标准输出上打印sort
—— 对文本文件行进行排序uniq
—— 报告或省略重复的行cut
—— 从每行文件中删除部分paste
—— 合并(merge)文件行join
—— 将两个文件的行连接到一个公共字段上tac
—— 反向(reverse)连接和打印文件rev
—— 按字符反转行comm
—— 逐行比较(compare)两个已排序的文件diff
—— 逐行比较文件patch
—— 将diff文件应用于原始(original)文件tr
—— 翻译或删除字符sed
—— 用于过滤和转换文本的流编辑器(stream editor)aspell
—— 交互式拼写检查器第二十章:文本处理文本的应用Documents——文件Web Pages——网页Email——电子邮件Printer Output——打印输出Program Source Code——程序源代码重温老朋友cat
MS-DOS文本与Unix文本sort
uniq
切片和分割cut
展开选项卡paste
join
tac
rev
比较文本comm
diff
patch
在飞行中编辑tr
ROT13:不那么秘密的解码环sed
喜欢 sed
的人也喜欢...aspell
总结
到目前为止,我们已经学习了几个文本编辑器(nano和vim),查看了一堆配置文件,并见证了数十个命令的输出,所有这些命令都是文本形式的。但是,文本还有什么用途呢?事实证明,在很多事情上。
许多人使用纯文本格式编写文档。虽然很容易看出一个小的文本文件对于保存简单的笔记是多么有用,但也可以用文本格式编写大型文档。一种流行的方法是以文本格式编写大型文档,然后嵌入标记语言(markup language)来描述完成文档的格式。许多科学论文都是使用这种方法撰写的,因为基于Unix的文本处理系统是支持技术学科作家所需的高级排版布局的首批系统之一。近年来,Markdown已经成为一种非常流行的标记语言,它使用纯文本进行文档格式化。
世界上最流行的电子文档类型可能是网页。网页是使用超文本标记语言(Hypertext Markup Language,HTML)或可扩展标记语言(Extensible Markup Language,XML)作为标记语言来描述文档视觉格式的文本文档。
【难怪http服务器不能解析markdown,因为它们是为markup设计的】
电子邮件本质上是一种基于文本的媒介。即使是非文本附件也会转换为文本表示以供传输。我们可以通过下载一封电子邮件,然后使用 less
查看它来亲眼看到这一点。我们将看到消息以一个标头(header)开头,该标头描述了消息的来源及其在传输过程中接收到的处理,然后是消息的正文(body)及其内容。
在类Unix系统上,发往打印机的输出以纯文本形式发送,或者如果页面包含图形,则转换为称为 PostScript 的文本格式页面描述语言(page description language),然后将其发送到生成要打印的图形点的程序。
类Unix系统上的许多命令行程序都是为了支持系统管理和软件开发而创建的,文本处理程序也不例外。其中许多旨在解决软件开发问题。文本处理对软件开发人员来说很重要的原因是,所有软件都是从文本开始的。源代码,即程序员实际编写的程序部分,始终采用文本格式。
回到【第六章.重定向】,我们了解了一些除了命令行参数外还可以接受标准输入的命令。我们当时只是简要地谈到了它们,但现在我们将仔细研究如何使用它们来执行文本处理。
cat
cat
程序有很多有趣的选择。其中许多用于帮助更好地可视化文本内容。一个例子是 -A
选项,用于显示文本中的非打印字符。有时我们想知道控制字符是否嵌入到我们可见的文本中。其中最常见的是制表符(而不是空格)和回车符,通常在MS-DOS风格的文本文件中以行尾字符的形式出现。另一种常见的情况是文件包含带有尾随空格的文本行。
让我们使用 cat
作为原始文字处理器创建一个测试文件。为此,我们只需输入命令 cat
(并指定一个用于重定向输出的文件)并键入文本,然后按 enter 键正确结束该行,然后按 Ctrl-d 键向 cat
表示我们已到达文件末尾。在这个例子中,我们输入一个前导制表符,并在行尾添加一些空格:
xxxxxxxxxx
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox jumps over the lazy dog. #后面有若干空格
[me@linuxbox ~]$
接下来,我们将使用带有 -A
选项的 cat
来显示文本:
xxxxxxxxxx
[me@linuxbox ~]$ cat -A foo.txt
^IThe quick brown fox jumps over the lazy dog. $
[me@linuxbox ~]$
正如我们在结果中看到的,文本中的制表符由 ^I
表示。这是一种常见的符号,意思是 Ctrl-i ,事实证明,它与制表符相同。我们还看到 $
出现在行的真正末尾,表示我们的文本包含尾随空格。
您可能希望使用 cat
在文本中查找非打印字符的原因之一是发现隐藏的回车符(carriage returns)。隐藏的回车从哪里来?DOS和Windows!Unix和DOS在文本文件中定义行尾的方式不同。Unix以换行符(linefeed character,ASCII 10)结束一行,而MS-DOS及其派生程序使用序列回车符(sequence carriage return,ASCII 13)和换行符(linefeed)终止每一行文本。
有几种方法可以将文件从DOS转换为Unix格式。在许多Linux系统上,有名为 dos2unix
和 unix2dos
的程序,可以将文本文件转换为DOS格式或从DOS格式转换为文本文件。但是,如果您的系统上没有 dos2unix
,请不要担心。将文本从DOS转换为Unix格式的过程很简单;它涉及删除违规(offending)的回车(carriage returns)。这很容易通过本章稍后讨论的几个程序来实现。
cat
还有用于修改文本的选项。最突出的两个是 -n
,用于对行进行编号,以及 -s
,用于抑制多个空白行的输出。我们可以这样演示:
xxxxxxxxxx
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox
jumps over the lazy dog.
[me@linuxbox ~]$ cat -ns foo.txt
1The quick brown fox
2
3jumps over the lazy dog.
[me@linuxbox ~]$
在这个例子中,我们创建了一个新版本的 foo.txt 测试文件,其中包含两行由两行空白分隔的文本。在 cat
使用 -ns
选项进行处理后,多余的空白行将被删除,剩余的行将被编号。虽然这不是一个在文本上执行的过程,但它是一个过程。
sort
sort
程序对标准输入的内容或命令行上指定的一个或多个文件进行排序,并将结果发送到标准输出。使用与 cat
相同的技术,我们可以直接从键盘演示标准输入的处理,如下所示:
xxxxxxxxxx
[me@linuxbox ~]$ sort > foo.txt
c
b
a
[me@linuxbox ~]$ cat foo.txt
a
b
c
输入命令后,我们键入字母c、b和a,然后按 Ctrl-d 表示文件结束。然后,我们查看结果文件,看到行现在按排序顺序显示。
由于 sort
可以在命令行上接受多个文件作为参数,因此可以将多个文件合并为一个排序的整体。例如,如果我们有三个文本文件,并想将它们组合成一个排序的文件,我们可以这样做:
xxxxxxxxxx
sort file1.txt file2.txt file3.txt > final_sorted_list.txt
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 | 定义字段分隔符(field-separator)。 默认情况下,字段由空格或制表符分隔。 |
虽然这些选项中的大多数都是不言自明的,但也有一些不是。首先,让我们看看用于数字排序的 -n
选项。使用此选项,可以根据数值对值进行排序。我们可以通过对 du
命令的结果进行排序来确定磁盘空间的最大用户来证明这一点。通常, du
命令按路径名顺序列出摘要的结果。
xxxxxxxxxx
[me@linuxbox ~]$ du -s /usr/share/* | head
252 /usr/share/aclocal
96 /usr/share/acpi-support
8 /usr/share/adduser
196 /usr/share/alacarte
344 /usr/share/alsa
8 /usr/share/alsa-base
12488 /usr/share/anthy
8 /usr/share/apmd
21440 /usr/share/app-install
48 /usr/share/application-registry
在这个例子中,我们将结果管道化到 head
中,以将结果限制在前10行。我们可以生成一个数字排序的列表,以这种方式显示10个最大的空间消耗者。
xxxxxxxxxx
[me@linuxbox ~]$ du -s /usr/share/* | sort -nr | head
509940 /usr/share/locale-langpack
242660 /usr/share/doc
197560 /usr/share/fonts
179144 /usr/share/gnome
146764 /usr/share/myspell
144304 /usr/share/gimp
135880 /usr/share/dict
76508 /usr/share/icons
68072 /usr/share/apps
62844 /usr/share/foomatic
通过使用 n
和 r
选项,我们产生了一个反向数值排序,最大值首先出现在结果中。这种排序之所以有效,是因为数值出现在每行的开头。但是,如果我们想根据行中的某个值对列表进行排序呢?例如,以下是 ls -l
的结果:
xxxxxxxxxx
[me@linuxbox ~]$ ls -l /usr/bin | head
total 152948
-rwxr-xr-x 1 root root 34824 2016-04-04 02:42 [
-rwxr-xr-x 1 root root 101556 2007-11-27 06:08 a2p
-rwxr-xr-x 1 root root 13036 2016-02-27 08:22 aconnect
-rwxr-xr-x 1 root root 10552 2007-08-15 10:34 acpi
-rwxr-xr-x 1 root root 3800 2016-04-14 03:51 acpi_fakekey
-rwxr-xr-x 1 root root 7536 2016-04-19 00:19 acpi_listen
-rwxr-xr-x 1 root root 3576 2016-04-29 07:57 addpart
-rwxr-xr-x 1 root root 20808 2016-01-03 18:02 addr2line
-rwxr-xr-x 1 root root 489704 2016-10-09 17:02 adept_batch
暂时忽略 ls
可以按大小对结果进行排序,我们也可以使用 sort
按文件大小对列表进行排序。
xxxxxxxxxx
[me@linuxbox ~]$ ls -l /usr/bin | sort -nrk 5 | head
-rwxr-xr-x 1 root root 8234216 2016-04-07 17:42 inkscape
-rwxr-xr-x 1 root root 8222692 2016-04-07 17:42 inkview
-rwxr-xr-x 1 root root 3746508 2016-03-07 23:45 gimp-2.4
-rwxr-xr-x 1 root root 3654020 2016-08-26 16:16 quanta
-rwxr-xr-x 1 root root 2928760 2016-09-10 14:31 gdbtui
-rwxr-xr-x 1 root root 2928756 2016-09-10 14:31 gdb
-rwxr-xr-x 1 root root 2602236 2016-10-10 12:56 net
-rwxr-xr-x 1 root root 2304684 2016-10-10 12:56 rpcclient
-rwxr-xr-x 1 root root 2241832 2016-04-04 05:56 aptitude
-rwxr-xr-x 1 root root 2202476 2016-10-10 12:56 smbcacls
排序的许多用途涉及处理表格数据(tabular data),例如前面 ls
命令的结果。如果我们将数据库术语应用于前一个表,我们会说每一行都是一条记录,每条记录由多个字段(fields)组成,如文件属性、链接计数、文件名、文件大小等。 sort
能够处理单个字段。在数据库方面,我们能够指定一个或多个键字段用作排序键。在前面的示例中,我们指定 n
和 r
选项以执行反向数字排序,并指定 -k 5
以使排序使用第五个字段作为排序键(key)。
k
选项很有趣,有很多功能,但首先我们需要谈谈sort如何定义字段。让我们考虑以下由一行包含作者姓名的简单文本文件:
xxxxxxxxxx
William Shotts
默认情况下,sort会将此行视为有两个字段。第一个字段包含以下字符:
"William"
第二个字段包含以下字符:
"Shotts"
这意味着空白字符(空格和制表符)用作字段之间的分隔符,并且在执行排序时,分隔符包含在字段中。
再次查看 ls
输出中的一行,如下所示,我们可以看到一行包含八个字段,第五个字段是文件大小:
xxxxxxxxxx
-rwxr-xr-x 1 root root 8234216 2016-04-07 17:42 inkscape
对于我们的下一系列实验,让我们考虑以下文件,其中包含2006年至2008年发布的三个流行Linux发行版的历史。文件中的每一行都有三个字段:分发名称、版本号和MM/DD/YYYY格式的发布日期。
xxxxxxxxxx
SUSE 10.2 12/07/2006
Fedora 10 11/25/2008
SUSE 11.0 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
SUSE 10.3 10/04/2007
Ubuntu 6.10 10/26/2006
Fedora 7 05/31/2007
Ubuntu 7.10 10/18/2007
Ubuntu 7.04 04/19/2007
SUSE 10.1 05/11/2006
Fedora 6 10/24/2006
Fedora 9 05/13/2008
Ubuntu 6.06 06/01/2006
Ubuntu 8.10 10/30/2008
Fedora 5 03/20/2006
使用文本编辑器(可能是 vim
),我们将输入此数据并将生成的文件命名为 distros.txt
。
接下来,我们将尝试对文件进行排序,并观察以下结果:
xxxxxxxxxx
[me@linuxbox ~]$ sort distros.txt
Fedora 10 11/25/2008
Fedora 5 03/20/2006
Fedora 6 10/24/2006
Fedora 7 05/31/2007
Fedora 8 11/08/2007
Fedora 9 05/13/2008
SUSE 10.1 05/11/2006
SUSE 10.2 12/07/2006
SUSE 10.3 10/04/2007
SUSE 11.0 06/19/2008
Ubuntu 6.06 06/01/2006
Ubuntu 6.10 10/26/2006
Ubuntu 7.04 04/19/2007
Ubuntu 7.10 10/18/2007
Ubuntu 8.04 04/24/2008
Ubuntu 8.10 10/30/2008
嗯,它基本上奏效了。问题出现在Fedora版本号的排序中。由于字符集中1在5之前,版本10最终位于顶部,而版本9位于底部。
为了解决这个问题,我们必须对多个键进行排序。我们希望对第一个字段执行字母排序,然后对第二个字段执行数字排序。 sort
允许 -k
选项的多个实例,以便可以指定多个排序键。事实上,一个键可能包含一系列字段。如果没有指定范围(就像我们前面的例子一样), sort
将使用一个以指定字段开头并延伸到行末尾的键。以下是我们的多键排序语法:
xxxxxxxxxx
[me@linuxbox ~]$ sort --key=1,1 --key=2n distros.txt
Fedora 5 03/20/2006
Fedora 6 10/24/2006
Fedora 7 05/31/2007
Fedora 8 11/08/2007
Fedora 9 05/13/2008
Fedora 10 11/25/2008
SUSE 10.1 05/11/2006
SUSE 10.2 12/07/2006
SUSE 10.3 10/04/2007
SUSE 11.0 06/19/2008
Ubuntu 6.06 06/01/2006
Ubuntu 6.10 10/26/2006
Ubuntu 7.04 04/19/2007
Ubuntu 7.10 10/18/2007
Ubuntu 8.04 04/24/2008
Ubuntu 8.10 10/30/2008
虽然为了清楚起见,我们使用了长形式的选项,但 -k 1,1 -k 2n
将完全等价。在键选项的第一个实例中,我们指定了要包含在第一个键中的字段范围。由于我们想将排序限制在第一个字段,因此我们指定了1,1,这意味着【从字段1开始,到字段1结束】。在第二个实例中,我们指定了 2n
,这意味著字段2是排序键,排序应该是数字的。在key说明符的末尾可以包含一个选项字母,以指示要执行的排序类型。这些选项字母与 sort
程序的全局选项相同:b
(忽略前导空格)、n
(数字排序)、r
(反向排序)等。
我们列表中的第三个字段包含一个排序格式不方便的日期。在计算机上,日期通常以 YYYY-MM-DD
顺序格式化,以便于按时间顺序排序,但我们的是 MM/DD/YYYY
的美国格式。我们如何按时间顺序对这个列表进行排序?
幸运的是, sort
提供了一种方法。key选项允许在字段内指定偏移量(offsets),因此我们可以在字段内定义键。
xxxxxxxxxx
[me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt
Fedora 10 11/25/2008
Ubuntu 8.10 10/30/2008
SUSE 11.0 06/19/2008
Fedora 9 05/13/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
Ubuntu 7.10 10/18/2007
SUSE 10.3 10/04/2007
Fedora 7 05/31/2007
Ubuntu 7.04 04/19/2007
SUSE 10.2 12/07/2006
Ubuntu 6.10 10/26/2006
Fedora 6 10/24/2006
Ubuntu 6.06 06/01/2006
SUSE 10.1 05/11/2006
Fedora 5 03/20/2006
通过指定 -k 3.7
,我们指示 sort
使用从第三个字段中的第七个字符开始的排序键,该字段对应于年初。同样,我们指定 -k 3.1
和 -k 3.4
来隔离日期的月份和日期部分。我们还添加了 n
和 r
选项来实现反向数字排序。包含 b
选项是为了抑制日期字段中的前导空格(其数字逐行变化,从而影响排序结果)。
有些文件不使用制表符和空格作为字段分隔符;例如,这是 /etc/passwd 文件:
xxxxxxxxxx
[me@linuxbox ~]$ head /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
此文件中的字段用冒号(:
)分隔,那么我们如何使用关键字字段对此文件进行排序呢? sort
提供了 -t
选项来定义字段分隔符。要在第七个字段(帐户的默认shell)对 passwd 文件进行排序,我们可以这样做:
xxxxxxxxxx
[me@linuxbox ~]$ sort -t ':' -k 7 /etc/passwd | head
me:x:1001:1001:Myself,,,:/home/me:/bin/bash
root:x:0:0:root:/root:/bin/bash
dhcp:x:101:102::/nonexistent:/bin/false
gdm:x:106:114:Gnome Display Manager:/var/lib/gdm:/bin/false
hplip:x:104:7:HPLIP system user,,,:/var/run/hplip:/bin/false
klog:x:103:104::/home/klog:/bin/false
messagebus:x:108:119::/var/run/dbus:/bin/false
polkituser:x:110:122:PolicyKit,,,:/var/run/PolicyKit:/bin/false
pulse:x:107:116:PulseAudio daemon,,,:/var/run/pulse:/bin/false
通过将冒号字符指定为字段分隔符,我们可以对第七个字段进行排序。
uniq
与 sort
相比, uniq
程序是轻量级的。 uniq
执行一项看似微不足道的任务。当给定一个排序文件(或标准输入)时,它会删除任何重复的行,并将结果发送到标准输出。它通常与排序结合使用,以清除重复的输出。
提示:虽然 uniq
是一个经常与 sort
一起使用的传统Unix工具,但GNU版本的 sort
支持 -u
选项,该选项可以从排序输出中删除重复项。
让我们制作一个文本文件来尝试一下,如下所示:
xxxxxxxxxx
[me@linuxbox ~]$ cat > foo.txt
a
b
c
a
b
c
记住键入 Ctrl-d 终止标准输入。现在,如果我们在文本文件上运行 uniq
,我们得到以下结果:
xxxxxxxxxx
[me@linuxbox ~]$ uniq foo.txt
a
b
c
a
b
c
结果与我们的原始文件没有什么不同;重复项未被删除。为了使 uniq
完成其工作,必须首先对输入进行排序。
xxxxxxxxxx
[me@linuxbox ~]$ sort foo.txt | uniq
a
b
c
这是因为 uniq
只删除彼此相邻的重复行。
uniq
有几个选择。下表列出了常见的。
选项 | 长选项 | 描述 |
---|---|---|
-c | --count | 输出一个重复行列表,前面加上行出现的次数。 |
-d | --repeated | 只输出重复的行,而不是唯一的行。 |
-f n | --skip-fields=n | 忽略每行中的 n 个前导字段。 字段在排序时用空格分隔;然而,与 sort 不同, uniq 没有设置备用字段分隔符的选项。 |
-i | --ignore-case | 行比较时忽略大小写。 |
-s n | --skip-chars=n | 跳过(忽略)每行的前n个字符。 |
-u | --unique | 仅输出唯一的行。重复的行将被忽略。 |
在这里,我们看到 uniq
用于使用 -c
选项报告文本文件中发现的重复项数量:
xxxxxxxxxx
[me@linuxbox ~]$ sort foo.txt | uniq -c
2 a
2 b
2 c
slice —— 切片
dice —— 分割
接下来我们将讨论的三个程序用于从文件中剥离(peel)文本列,并以有用的方式重新组合它们。
cut
cut
程序用于从一行中提取一段文本,并将提取的部分输出到标准输出。它可以接受多个文件参数或来自标准输入的输入。
指定要提取的线段有点尴尬,使用下表中列出的选项进行指定。
选项 | 长选项 | 描述 |
---|---|---|
-c list | --characters=list | 提取 list 定义的行部分。 该列表可能由一个或多个逗号分隔的数值范围组成。 |
-f list | --fields=list | 从 list 定义的行中提取一个或多个字段。 该列表可能包含一个或多个用逗号分隔的字段或字段范围。 |
-d delim | --delimeter=delim | 指定 -f 时,使用 delim 作为字段分隔符。默认情况下,字段必须用单个制表符分隔。 |
--complement | 提取整行文本,除了 -c 和/或 -f 指定的部分。 |
正如我们所看到的,剪切提取文本的方式相当不灵活。cut最适合用于从其他程序生成的文件中提取文本,而不是直接由人类键入的文本。我们将查看我们的 distros.txt 文件,看看它是否足够“干净”,可以作为我们剪切示例的良好样本。如果我们使用带有 -A
选项的 cat
,我们可以看到文件是否符合我们对制表符分隔字段的要求:
xxxxxxxxxx
[me@linuxbox ~]$ cat -A distros.txt
SUSE^I10.2^I12/07/2006$
Fedora^I10^I11/25/2008$
SUSE^I11.0^I06/19/2008$
Ubuntu^I8.04^I04/24/2008$
Fedora^I8^I11/08/2007$
SUSE^I10.3^I10/04/2007$
Ubuntu^I6.10^I10/26/2006$
Fedora^I7^I05/31/2007$
Ubuntu^I7.10^I10/18/2007$
Ubuntu^I7.04^I04/19/2007$
SUSE^I10.1^I05/11/2006$
Fedora^I6^I10/24/2006$
Fedora^I9^I05/13/2008$
Ubuntu^I6.06^I06/01/2006$
Ubuntu^I8.10^I10/30/2008$
Fedora^I5^I03/20/2006$
它看起来不错。字段之间没有嵌入空格,只有单个制表符。由于文件使用制表符而不是空格,我们将使用 -f
选项提取字段。
xxxxxxxxxx
[me@linuxbox ~]$ cut -f 3 distros.txt
12/07/2006
11/25/2008
06/19/2008
04/24/2008
11/08/2007
10/04/2007
10/26/2006
05/31/2007
10/18/2007
04/19/2007
05/11/2006
10/24/2006
05/13/2008
06/01/2006
10/30/2008
03/20/2006
因为我们的 distros 文件是以制表符分隔的(tab-delimited),所以最好使用 cut
来提取字段,而不是字符。这是因为当文件以制表符分隔时,每行不太可能包含相同数量的字符,这使得计算行内的字符位置变得困难或不可能。然而,在前面的示例中,我们现在提取了一个幸运地包含相同长度数据的字段,因此我们可以通过从每一行中提取年份来展示字符提取的工作原理。
xxxxxxxxxx
[me@linuxbox ~]$ cut -f 3 distros.txt | cut -c 7-10
2006
2008
2008
2008
2007
2007
2006
2007
2007
2007
2006
2006
2008
2006
2008
2006
通过在列表中第二次运行 cut
,我们能够提取字符位置7到10,这对应于我们日期字段中的年份。 7-10
符号是一个范围的例子。 cut
手册页包含如何指定范围的完整描述。
我们的 distros.txt 文件的格式非常适合使用 cut
提取字段。但是,如果我们想要一个可以用剪切字符而不是字段完全操纵的文件呢?这将要求我们用相应数量的空格替换文件中的制表符。幸运的是,GNU Coreutilspackage包含了一个用于此的工具。该程序名为expand,接受一个或多个文件参数或标准输入,并将修改后的文本输出到标准输出。
如果我们使用 expand
处理我们的 distros.txt 文件,我们可以使用 cut -c
从文件中提取任何字符范围。例如,我们可以使用以下命令从列表中提取发布年份,方法是展开文件并使用 cut
提取从第23位到行尾的每个字符:
xxxxxxxxxx
[me@linuxbox ~]$ expand distros.txt | cut -c 23-
Coreutils还提供了 unexpand
程序,用于用制表符替换空格。
使用字段时,可以指定不同的字段分隔符而不是制表符。在这里,我们将从 /etc/passwd 文件中提取第一个字段:
xxxxxxxxxx
[me@linuxbox ~]$ cut -d ':' -f 1 /etc/passwd | head
root
daemon
bin
sys
sync
games
man
lp
mail
news
使用 -d
选项,我们可以将冒号字符指定为字段分隔符。
paste
paste
命令的作用与 cut
相反。它不是从文件中提取一列文本,而是将一列或多列文本添加到文件中。它通过读取多个文件并将每个文件中的字段组合到标准输出上的单个流中来实现这一点。与 cut
一样,paste
也接受多个文件参数和/或标准输入。为了演示 paste
是如何操作的,我们将对我们的 distros.txt 文件进行一些操作,以生成按时间顺序排列的版本列表。
根据我们之前使用 sort
的工作,我们将首先生成一个按日期排序的发行版列表,并将结果存储在一个名为 distors-by-date.txt 的文件中。
xxxxxxxxxx
[me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt
接下来,我们将使用 cut
从文件中提取前两个字段(发行版名称和版本),并将结果存储在一个名为 distro-versions.txt 的文件中。
xxxxxxxxxx
[me@linuxbox ~]$ cut -f 1,2 distros-by-date.txt > distros-versions.txt
[me@linuxbox ~]$ head distros-versions.txt
Fedora 10
Ubuntu 8.10
SUSE 11.0
Fedora 9
Ubuntu 8.04
Fedora 8
Ubuntu 7.10
SUSE 10.3
Fedora 7
Ubuntu 7.04
最后的准备工作是提取发布日期并将其存储在名为 distro-dates.txt 的文件中。
xxxxxxxxxx
[me@linuxbox ~]$ cut -f 3 distros-by-date.txt > distros-dates.txt
[me@linuxbox ~]$ head distros-dates.txt
11/25/2008
10/30/2008
06/19/2008
05/13/2008
04/24/2008
11/08/2007
10/18/2007
10/04/2007
05/31/2007
04/19/2007
我们现在有了我们需要的零件。为了完成这个过程,我们将使用 paste
将日期列放在发行版名称和版本之前,从而创建一个按时间顺序排列的列表。这只需使用 paste
并按照所需的排列对参数进行排序即可完成。
xxxxxxxxxx
[me@linuxbox ~]$ paste distros-dates.txt distros-versions.txt
11/25/2008 Fedora 10
10/30/2008 Ubuntu 8.10
06/19/2008 SUSE 11.0
05/13/2008 Fedora 9
04/24/2008 Ubuntu 8.04
11/08/2007 Fedora 8
10/18/2007 Ubuntu 7.10
10/04/2007 SUSE 10.3
05/31/2007 Fedora 7
04/19/2007 Ubuntu 7.04
12/07/2006 SUSE 10.2
10/26/2006 Ubuntu 6.10
10/24/2006 Fedora 6
06/01/2006 Ubuntu 6.06
05/11/2006 SUSE 10.1
03/20/2006 Fedora 5
join
在某些方面, join
类似于 paste
,因为它向文件中添加列,但它使用了一种独特的方式。 join
是一种通常与关系数据库(relational databases)相关的操作,其中来自具有共享键字段的多个表(tables)的数据被组合在一起以形成所需的结果。 join
程序执行相同的操作。它基于共享关键字段连接来自多个文件的数据。
为了了解如何在关系数据库中使用连接操作,让我们想象一个由两个表组成的小型数据库,每个表包含一条记录。第一个表名为 CUSTOMERS ,有三个字段:客户编号(CUSTNUM)、客户名字(FNAME)和客户姓氏(LNAME):
CUSTNUM FNAME LNAME ======== ====== ===== 4681934 John Smith
第二个表称为 ORDERS ,包含四个字段:订单号(ORDERNUM)、客户号(CUSTNUM)、数量(QUAN)和订购的商品(ITEM)。
ORDERNUM CUSTNUM QUAN ITEM ========== ======== ===== ===== 3014953305 4681934 1 Blue Widget
请注意,这两个表共享字段 CUSTNUM 。这很重要,因为它允许表之间的关系。
执行联接操作将允许我们组合两个表中的字段以获得有用的结果,例如准备发票。使用两个表的 CUSTNUM 字段中的匹配值,联接操作可能会产生以下结果:
FNAME LNAME QUAN ITEM ====== ====== ===== ==== John Smith 1 Blue Widget
为了演示连接程序,我们需要使用共享密钥创建几个文件。为此,我们将使用我们的distros-by-date.txt 文件。从这个文件中,我们将构造两个附加文件。一个包含发布日期(这将是我们本次演示的共享密钥)和发布名称,如下所示
xxxxxxxxxx
[me@linuxbox ~]$ cut -f 1,1 distros-by-date.txt > distros-names.txt
[me@linuxbox ~]$ paste distros-dates.txt distros-names.txt > distros-key-names.txt
[me@linuxbox ~]$ head distros-key-names.txt
11/25/2008 Fedora
10/30/2008 Ubuntu
06/19/2008 SUSE
05/13/2008 Fedora
04/24/2008 Ubuntu
11/08/2007 Fedora
10/18/2007 Ubuntu
10/04/2007 SUSE
05/31/2007 Fedora
04/19/2007 Ubuntu
第二个文件包含发布日期和版本号,如下所示:
xxxxxxxxxx
[me@linuxbox ~]$ cut -f 2,2 distros-by-date.txt > distros-vernums.txt
[me@linuxbox ~]$ paste distros-dates.txt distros-vernums.txt > distros-key-vernums.txt
[me@linuxbox ~]$ head distros-key-vernums.txt
11/25/2008 10
10/30/2008 8.10
06/19/2008 11.0
05/13/2008 9
04/24/2008 8.04
11/08/2007 8
10/18/2007 7.10
10/04/2007 10.3
05/31/2007 7
04/19/2007 7.04
我们现在有两个具有共享关键字的文件(“release data”字段)。重要的是要指出,文件必须在关键字段上排序,以便 join
正常工作。
xxxxxxxxxx
[me@linuxbox ~]$ join distros-key-names.txt distros-key-vernums.txt | head
11/25/2008 Fedora 10
10/30/2008 Ubuntu 8.10
06/19/2008 SUSE 11.0
05/13/2008 Fedora 9
04/24/2008 Ubuntu 8.04
11/08/2007 Fedora 8
10/18/2007 Ubuntu 7.10
10/04/2007 SUSE 10.3
05/31/2007 Fedora 7
04/19/2007 Ubuntu 7.04
还要注意,默认情况下, join
使用空格作为输入字段分隔符,使用单个空格作为输出字段分隔符。可以通过指定选项来修改此行为。有关详细信息,请参阅 join
手册页。
tac
tac
命令的工作方式与 cat
相反,字面意思是。它以相反的顺序连接文件。使用 tac
的一种方法是重新排序日志文件。日志文件通常以这种方式显示:
xxxxxxxxxx
[me@linuxbox ~]$ tail /var/log/backup.log
Wed Aug 14 06:00:32 AM EDT 2025: backup (0.6) of linuxbox started.
Wed Aug 14 06:08:01 AM EDT 2025: backup of linuxbox finished.
Thu Aug 15 07:36:30 AM EDT 2025: backup (0.6) of linuxbox started.
Thu Aug 15 07:45:57 AM EDT 2025: backup of linuxbox finished.
Fri Aug 16 07:37:57 AM EDT 2025: backup (0.6) of linuxbox started.
Fri Aug 16 07:44:11 AM EDT 2025: backup of linuxbox finished.
正如我们所看到的,日志底部显示了最新条目。如果我们想在列表顶部看到最近的事件,我们可以这样做:
xxxxxxxxxx
[me@linuxbox ~]$ tail /var/log/backup.log | tac
Fri Aug 16 07:44:11 AM EDT 2025: backup of linuxbox finished.
Fri Aug 16 07:37:57 AM EDT 2025: backup (0.6) of linuxbox started.
Thu Aug 15 07:45:57 AM EDT 2025: backup of linuxbox finished.
Thu Aug 15 07:36:30 AM EDT 2025: backup (0.6) of linuxbox started.
Wed Aug 14 06:08:01 AM EDT 2025: backup of linuxbox finished.
Wed Aug 14 06:00:32 AM EDT 2025: backup (0.6) of linuxbox started.
rev
同样, rev
命令反转字符串中的字符:
xxxxxxxxxx
[me@linuxbox ~]$ echo “This is a test.” | rev
.tset a si sihT
很可爱,但它有什么好处呢? rev
命令通常与 cut
一起使用。让我们再次考虑一下我们的 backup.log 文件。想象一下,我们想从每行末尾删除句点字符。由于行的长度各不相同,因此无法指定剪切命令来删除线条末尾的内容。 rev
的作用就在这里。通过颠倒每行中的字符,我们可以将句点移动到开头,这样 cut
就可以很容易地删除它们:
xxxxxxxxxx
[me@linuxbox ~]$ tail /var/log/backup.log | rev | cut -c 2- | rev
Wed Aug 14 06:00:32 AM EDT 2025: backup (0.6) of linuxbox started
Wed Aug 14 06:08:01 AM EDT 2025: backup of linuxbox finished
Thu Aug 15 07:36:30 AM EDT 2025: backup (0.6) of linuxbox started
Thu Aug 15 07:45:57 AM EDT 2025: backup of linuxbox finished
Fri Aug 16 07:37:57 AM EDT 2025: backup (0.6) of linuxbox started
Fri Aug 16 07:44:11 AM EDT 2025: backup of linuxbox finished
在这个例子中,我们使用 rev
反转每行中的字符,将行的内容从位置2(反转字符串中句号后的字符)剪切到行的末尾,然后再次反转字符,使其恢复可读形式。
比较文本文件的版本通常很有用。对于系统管理员和软件开发人员来说,这一点尤为重要。例如,系统管理员可能需要将现有配置文件与以前的版本进行比较,以诊断系统问题。同样,程序员经常需要查看随着时间的推移对程序进行了哪些更改。
comm
comm
程序比较两个文本文件,并显示每个文件独有的行和它们共有的行。为了演示,我们将使用 cat
创建两个几乎相同的文本文件。
xxxxxxxxxx
[me@linuxbox ~]$ cat > file1.txt
a
b
c
d
[me@linuxbox ~]$ cat > file2.txt
b
c
d
e
接下来,我们将使用 comm
比较这两个文件:
xxxxxxxxxx
[me@linuxbox ~]$ comm file1.txt file2.txt
a
b
c
d
e
正如我们所看到的, comm
会产生三列输出。第一列包含第一个文件参数特有的行,第二列包含第二个文件参数独有的行,而第三列包含两个文件共享的行。 comm
支持 -n
格式的选项,其中 n
可以是 1
、 2
或 3
。使用时,这些选项指定要抑制哪些列。例如,如果我们只想输出两个文件共享的行,我们会抑制第一列和第二列的输出。
xxxxxxxxxx
[me@linuxbox ~]$ comm -12 file1.txt file2.txt
b
c
d
diff
与 comm
程序一样, diff
用于检测文件之间的差异。然而, diff
是一个更复杂的工具,支持多种输出格式,并能够一次处理大量文本文件。 diff
经常被软件开发人员用来检查不同版本的程序源代码之间的变化,因此能够递归检查源代码目录,通常称为源树(source trees)。 diff
的一个常见用途是创建 diff files 或补丁(patches),这些文件或补丁由 patch
(我们稍后将讨论)等程序使用,用于将一个或多个文件的一个版本转换为另一个版本。
如果我们使用 diff
来查看前面的示例文件:
xxxxxxxxxx
[me@linuxbox ~]$ diff file1.txt file2.txt
1d0
< a
4a4
> e
我们看到了它的默认输出样式:对两个文件之间差异的简洁描述。在默认格式中,每组更改前面都有一个范围操作范围(range operation range)形式的更改命令(change command),用于描述将第一个文件转换为第二个文件所需的更改位置和类型,如下表所示。
更改 | 描述 |
---|---|
r1ar2 | 将第二个文件中 r2 位置的行追加(append)到第一个文件中 r1 位置。 |
r1cr2 | 将第二个文件中 r1 位置的行替换(replace)为 r2 位置的行。 |
r1dr2 | 删除(delete)第一个文件中位于 r1 位置的行,这些行本应出现在第二个文件的 r2 范围内。 |
在这种格式中,范围是以逗号分隔的起始行和结束行列表。虽然这种格式是默认的(主要是为了符合POSIX标准和与传统Unix版本的 diff
向后兼容),但它并不像其他可选格式那样被广泛使用。两种更流行的格式是上下文格式(context format)和统一格式(unified format)。
当使用上下文格式( -c
选项,context)查看时,我们将看到以下内容:
xxxxxxxxxx
[me@linuxbox ~]$ diff -c file1.txt file2.txt
*** file1.txt2025-12-23 06:40:13.000000000 -0500
--- file2.txt2025-12-23 06:40:34.000000000 -0500
***************
*** 1,4 ****
- a
b
c
d
--- 1,4 ----
b
c
d
+ e
输出以两个文件的名称及其时间戳开始。第一个文件用星号标记,第二个文件用破折号标记。在列表的其余部分,这些标记将表示各自的文件。接下来,我们看到一组变化,包括周围上下文行的默认数量。在第一组中,我们看到:
*** 1,4 ***
其指示第一文件中的第1行到第4行。稍后我们会看到:
--- 1,4 ---
其指示第二文件中的第1行到第4行。在变更组中,行以下表中所示的四个指标之一开头。
指示器 | 含义 |
---|---|
blank | 为上下文而显示的线条。它并不表示这两个文件之间的差异。 |
- | 删除了一行。此行将出现在第一个文件中,但不会出现在第二个文件中。 |
+ | 添加了一行。此行将出现在第二个文件中,但不会出现在第一个文件中。 |
! | 一条线变了。将显示该行的两个版本,每个版本都在更改组的相应部分中。 |
统一格式类似于上下文格式,但更简洁。它是用 -u
选项指定的。
xxxxxxxxxx
[me@linuxbox ~]$ diff -u file1.txt file2.txt
--- file1.txt2008-12-23 06:40:13.000000000 -0500
+++ file2.txt2008-12-23 06:40:34.000000000 -0500
@@ -1,4 +1,4 @@
-a
b
c
d
+e
上下文和统一格式之间最显著的区别是消除了重复的上下文行,使统一格式的结果比上下文格式的结果短。在前面的示例中,我们看到了与上下文格式类似的文件时间戳,后面是字符串 @@ -1,4 +1,4 @@
。这表示更改组中描述的第一个文件和第二个文件中的行。下面是行本身,默认有三行上下文。每行以下表中列出的三个可能字符之一开头。
字符 | 含义 |
---|---|
blank | 此行由两个文件共享。 |
- | 此行已从第一个文件中删除。 |
+ | 此行已添加到第一个文件中。 |
patch
补丁程序用于将更改应用于文本文件。它接受 diff
的输出,通常用于将旧版本文件转换为新版本。让我们考虑一个著名的例子。Linux内核是由一个庞大的、组织松散的贡献者团队开发的,他们不断对源代码进行小修改。Linux内核由数百万行代码组成,而一个贡献者一次所做的更改非常小。每次进行微小更改时,贡献者向每个开发人员发送一个完整的内核源代码树是没有意义的。相反,会提交一个 diff
文件。 diff
文件包含从以前版本的内核到新版本的更改,以及贡献者的更改。然后,接收器使用 patch
程序将更改应用于他自己的源树。使用 diff/patch
有两个显著的优点。
当然,diff/patch
适用于任何文本文件,而不仅仅是源代码。它同样适用于配置文件或任何其他文本。
为了准备一个与 patch
一起使用的 diff文件,GNU文档(请参阅下面的进一步阅读)建议按如下方式使用 diff
:
diff -Naur old_file new_file > diff_file
其中 old_file 和 new_file 是单个文件或包含文件的目录。 r
选项支持目录树的递归。
创建diff文件后,我们可以应用它将旧文件修补到新文件中。
patch < diff_file
我们将用我们的测试文件来演示。
xxxxxxxxxx
[me@linuxbox ~]$ diff -Naur file1.txt file2.txt > patchfile.txt
[me@linuxbox ~]$ patch < patchfile.txt
patching file file1.txt
[me@linuxbox ~]$ cat file1.txt
b
c
d
e
在这个例子中,我们创建了一个名为 patchfile.txt 的diff文件,然后使用 patch
程序应用补丁。请注意,我们不必指定 patch
的目标文件,因为diff文件(统一格式)已经在标头中包含了文件名。应用补丁后,我们可以看到 file1.txt 现在与 file2.txt 匹配。
patch
有很多选项,还有其他实用程序可用于分析和编辑补丁。
我们使用文本编辑器的经验在很大程度上是交互式的(interactive),这意味着我们手动移动光标,然后键入更改。然而,也有非交互式的(non-interactive)方式来编辑文本。例如,可以使用单个命令将一组更改应用于多个文件。
tr
tr
程序用于音译(transliterate)字符。我们可以将其视为一种基于字符的搜索和替换操作。音译是将字符从一个字母表转换为另一个字母的过程。例如,将字符从小写转换为大写就是音译。我们可以用 tr
执行这样的转换,如下所示:
xxxxxxxxxx
[me@linuxbox ~]$ echo "lowercase letters" | tr a-z A-Z
LOWERCASE LETTERS
正如我们所看到的, tr
对标准输入进行操作,并在标准输出上输出其结果。 tr
接受两个参数:要转换的一组字符和要转换的相应一组字符。字符集可以用三种方式之一表示。
ABCDEFGHIJKLMNOPQRSTUVXYZ
。A-Z
。请注意,由于区域设置的排序顺序,此方法有时会遇到与其他命令相同的问题,因此应谨慎使用。[:upper:]
。在大多数情况下,两个字符集的长度应该相等;然而,第一组可能比第二组大,特别是如果我们想将多个字符转换为单个字符。
xxxxxxxxxx
[me@linuxbox ~]$ echo "lowercase letters" | tr [:lower:] A
AAAAAAAAA AAAAAAA
除了音译, tr
还允许从输入流中删除字符。在本章的前面,我们讨论了将MS-DOS文本文件转换为Unix样式文本的问题。要执行此转换,需要从每行末尾删除回车符。这可以用 tr
执行,如下所示:
tr -d '\r' < dos_file > unix_file
其中 dos_file 是要转换的文件, unix_file 是结果。这种形式的命令使用转义序列 \r
来表示回车符。要查看 tr
支持的序列和字符类的完整列表,请尝试以下操作:
xxxxxxxxxx
[me@linuxbox ~]$ tr --help
tr
的一个有趣用途是对文本执行ROT13编码(ROT13 encoding)。ROT13是一种基于简单替换密码的简单加密类型。称ROT13为“加密”是慷慨的(generous;);“文本混淆(text obfuscation)”更准确。它有时用于文本,以掩盖潜在的冒犯性内容。该方法只是将每个字符在字母表中向上移动13个位置。由于这是可能的26个字符的一半,对文本执行第二次算法会将其恢复到原始形式。使用以下命令使用 tr
执行此编码:
xxxxxxxxxx
echo "secret text" | tr a-zA-Z n-za-mN-ZA-M
frperg grkg
第二次执行相同的程序会导致以下翻译:
xxxxxxxxxx
echo "frperg grkg" | tr a-zA-Z n-za-mN-ZA-M
secret text
许多电子邮件程序和Usenet新闻阅读器支持ROT13编码。维基百科上有一篇关于这个主题的好文章:
http://en.wikipedia.org/wiki/ROT13
tr
也可以执行另一个技巧。使用 -s
选项, tr
可以“挤压”(squeeze,效果相当于删除)字符的重复实例。
xxxxxxxxxx
[me@linuxbox ~]$ echo "aaabbbccc" | tr -s ab
abccc
这里有一个包含重复字符的字符串。通过将集合“ab”指定为 tr
,我们消除了集合中字母的重复实例,同时保持集合中缺少的字符(“c”)不变。请注意,重复字符必须相邻。如果没有,挤压将没有效果。
xxxxxxxxxx
[me@linuxbox ~]$ echo "abcabcabc" | tr -s ab
abcabcabc
sed
sed
是 stream editor (流编辑器)的缩写。它对文本流(一组指定文件或标准输入)执行文本编辑。 sed
是一个功能强大且有点复杂的程序(有很多关于它的书),所以我们在这里不会完全介绍它。
一般来说, sed
的工作方式是,它要么被赋予一个编辑命令(在命令行上),要么被赋予包含多个命令的脚本文件的名称,然后在文本流中的每一行上执行这些命令。下面是一个简单的 sed
示例:
xxxxxxxxxx
[me@linuxbox ~]$ echo "front" | sed 's/front/back/'
back
在这个例子中,我们使用 echo
生成一个单词的文本流,并将其导入 sed
。 sed
反过来对流中的文本执行 s/front/back/
指令,并因此产生输出 back
。我们还可以将此命令识别为类似于 vi
中的“替换”(substitution,search-and-replace,搜索和替换)命令。
sed
中的命令以单个字母开头。在前面的示例中,替换命令由字母s表示,后面是搜索和替换字符串,用斜线字符作为分隔符分隔。分隔符的选择是任意的。按照惯例,斜线字符经常被使用,但 sed
将接受紧随命令之后的任何字符作为分隔符。我们可以这样执行相同的命令:
xxxxxxxxxx
[me@linuxbox ~]$ echo "front" | sed 's_front_back_'
back
通过在命令后立即使用下划线字符,它将成为分隔符。设置分隔符的能力可用于使命令更具可读性,正如我们将看到的那样。
sed
中的大多数命令前面都可能有一个地址,该地址指定将编辑输入流中的哪一行。如果省略地址,则对输入流中的每一行执行编辑命令。最简单的地址形式是行号。我们可以在示例中添加一个。
xxxxxxxxxx
[me@linuxbox ~]$ echo "front" | sed '1s/front/back/'
back
将地址 1
添加到我们的命令中会导致我们的替换在单行输入流的第一行上执行。如果我们指定另一个数字,我们看到编辑没有执行,因为我们的输入流没有第 2
行。
xxxxxxxxxx
[me@linuxbox ~]$ echo "front" | sed '2s/front/back/'
front
地址可以用多种方式表达。下表列出了最常见的。
地址 | 描述 |
---|---|
n | 一个行号,其中 n 是正整数。 |
$ | 最后一行 |
/regexp/ | 与POSIX基本正则表达式匹配的行。 请注意,正则表达式由斜线字符分隔。可选地,正则表达式可以用替换字符分隔,方法是使用 \cregexpc 指定表达式,其中 c 是替换字符。 |
addr1,addr2 | 从 addr1 到 addr2 (包括 addr1 和 addr2)的一系列行。地址可以是前面列出的任何单一地址形式。 |
first~step | 匹配由数字 first 表示的行,然后以 step 间隔匹配每一行。例如,1~2表示每条奇数行,5~5表示第五行以及之后的每五行。 |
addr1,+n | 匹配 addr1 和以下 n 行。 |
addr! | 匹配除 addr 之外的所有行,addr 可以是前面列出的任何形式。 |
我们将使用本章前面的 distros.txt 文件演示不同类型的地址。首先,这是一系列行号:
xxxxxxxxxx
[me@linuxbox ~]$ sed -n '1,5p' distros.txt
SUSE 10.2 12/07/2006
Fedora 10 11/25/2008
SUSE 11.0 06/19/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
在这个例子中,我们打印了一系列行,从第1行开始,一直到第5行。为此,我们使用 p
命令,它只会打印匹配的行。然而,为了使其有效,我们必须包含选项 -n
(“no auto-print,无自动打印”选项),以使 sed
不打印默认的每一行。
接下来,我们将尝试使用正则表达式。
xxxxxxxxxx
[me@linuxbox ~]$ sed -n '/SUSE/p' distros.txt
SUSE 10.2 12/07/2006
SUSE 11.0 06/19/2008
SUSE 10.3 10/04/2007
SUSE 10.1 05/11/2006
通过包含斜线分隔的正则表达式 /SUSE/
,我们能够以与 grep
几乎相同的方式隔离包含它的行。
最后,我们将通过在地址中添加感叹号(!
)来尝试否定。
xxxxxxxxxx
[me@linuxbox ~]$ sed -n '/SUSE/!p' distros.txt
Fedora 10 11/25/2008
Ubuntu 8.04 04/24/2008
Fedora 8 11/08/2007
Ubuntu 6.10 10/26/2006
Fedora 7 05/31/2007
Ubuntu 7.10 10/18/2007
Ubuntu 7.04 04/19/2007
Fedora 6 10/24/2006
Fedora 9 05/13/2008
Ubuntu 6.06 06/01/2006
Ubuntu 8.10 10/30/2008
Fedora 5 03/20/2006
这里我们看到了预期的结果:文件中的所有行,除了正则表达式匹配的行。
到目前为止,我们已经研究了 sed
编辑命令中的两个, s
和 p
。下表提供了基本编辑命令的更完整列表。
命令 | 介绍 |
---|---|
= | 输出当前行号。 |
a | 在当前行后附加文本。 |
d | 删除当前行。 |
i | 在当前行前插入文本。 |
p | 打印当前行。 默认情况下, sed 打印每一行,只编辑与文件中指定地址匹配的行。通过指定 -n 选项可以覆盖默认行为。 |
q | 退出 sed ,不再处理任何行。如果未指定 -n 选项,则输出当前行。 |
Q | 退出 sed ,不再处理任何行。 |
s/regexp/replacement/ | 在找到 regexp 的地方替换为 replacemetn 内容。 replacement 可能包括特殊字符 & ,它相当于 regexp 文本。此外,替换可能包括序列 \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
,此更改可以一步完成。
xxxxxxxxxx
[me@linuxbox ~]$ sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt
SUSE 10.2 2006-12-07
Fedora 10 2008-11-25
SUSE 11.0 2008-06-19
Ubuntu 8.04 2008-04-24
Fedora 8 2007-11-08
SUSE 10.3 2007-10-04
Ubuntu 6.10 2006-10-26
Fedora 7 2007-05-31
Ubuntu 7.10 2007-10-18
Ubuntu 7.04 2007-04-19
SUSE 10.1 2006-05-11
Fedora 6 2006-10-24
Fedora 9 2008-05-13
Ubuntu 6.06 2006-06-01
Ubuntu 8.10 2008-10-30
Fedora 5 2006-03-20
哇!多么难看的命令。但它奏效了。只需一步,我们就更改了文件中的日期格式。这也是为什么正则表达式有时被开玩笑地称为“只写”(write-only)介质的一个完美例子。我们可以写它们,但有时我们无法阅读它们。在我们害怕逃离这个命令之前,让我们看看它是如何构建的。
首先,我们知道该命令将具有这种基本结构:
sed 's/regexp/replacement/' distros.txt
我们的下一步是找出一个正则表达式来分离(isolate)日期。因为它是MM/DD/YYYY格式,出现在行尾,我们可以使用这样的表达式:
[0-9]{2}/[0-9]{2}/[0-9]{4}$
这匹配:
所以这就解决了 regexp 的问题,但 replacement 呢?为了解决这个问题,我们必须引入一个新的正则表达式功能,该功能出现在一些使用BRE的应用程序中。此功能称为回引用(back references),其工作原理如下:如果序列出现在替换中,其中 n
是1到9之间的数字,则序列将引用前一个正则表达式中的相应子表达式。要创建子表达式,我们只需将它们括在括号中,如下所示:
([0-9]{2})/([0-9]{2})/([0-9]{4})$
我们现在有三个子表达式。第一个包含月份,第二个包含月份中的某一天,第三个包含年份。现在我们可以按如下方式构建替换:
\3-\1-\2
这给了我们年份、破折号、月份、破折点和日期。
现在,我们的命令看起来像这样:
sed 's/([0-9]{2})/([0-9]{2})/([0-9]{4})$/\3-\1-\2/' distros.txt
我们还有两个问题:
sed
试图解释 s
命令时,正则表达式中的额外斜线会混淆 sed
。sed
只接受基本的正则表达式,因此正则表达式中的几个字符将被视为字面值,而不是元字符。【上面命令直接运行的结果如下:】
xxxxxxxxxx
sed:-e 表达式 #1,字符 25:“s”的未知选项
我们可以通过自由地使用反斜杠来逃避冒犯的角色来解决(转义)这两个问题:
sed 's/([0-9]{2})\/([0-9]{2})\/([0-9]{4})$/\3-\1-\2/' distros.txt
就在那里!
s
命令的另一个功能是使用可选标志,这些标志可能跟在替换字符串后面。其中最重要的是 g
标志,它指示 sed
将搜索和替换全局(globally)应用于一行,而不仅仅是默认的第一个实例。以下是一个示例:
xxxxxxxxxx
[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/'
aaaBbbccc
我们看到替换已经完成,但仅限于字母 b
的第一个实例,而其余实例保持不变。通过添加 g
标志,我们可以更改所有实例:
xxxxxxxxxx
[me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/g' aaaBBBccc
到目前为止,我们只通过命令行给 sed
发出了单个命令。还可以使用 -f
选项在脚本文件中构造更复杂的命令。为了演示,我们将使用 sed
和我们的 distros.txt 文件来构建报告。我们的报告将在顶部显示标题、修改日期和所有转换为大写的分发名称。为此,我们需要编写一个脚本,因此我们将启动文本编辑器并输入以下内容:
xxxxxxxxxx
# sed script to produce Linux distributions report
1 i\
\
Linux Distributions Report\
s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
我们将把 sed
脚本保存为 distros.sed ,并按如下方式运行:
xxxxxxxxxx
[me@linuxbox ~]$ sed -f distros.sed distros.txt
Linux Distributions Report
SUSE 10.2 2006-12-07
FEDORA 10 2008-11-25
SUSE 11.0 2008-06-19
UBUNTU 8.04 2008-04-24
FEDORA 8 2007-11-08
SUSE 10.3 2007-10-04
UBUNTU 6.10 2006-10-26
FEDORA 7 2007-05-31
UBUNTU 7.10 2007-10-18
UBUNTU 7.04 2007-04-19
SUSE 10.1 2006-05-11
FEDORA 6 2006-10-24
FEDORA 9 2008-05-13
UBUNTU 6.06 2006-06-01
UBUNTU 8.10 2008-10-30
FEDORA 5 2006-03-20
【在Debian中运行,所有前面的字母都变成了大写,但只有最后一行的日期如上例更改了,其余行的日期都没有变化。但在FreeBSD中,bash或sh环境下都能得到预期结果】
正如我们所看到的,我们的脚本产生了所需的结果,但它是如何做到的呢?让我们再看看我们的脚本。我们将用 cat
来给行编号。
xxxxxxxxxx
[me@linuxbox ~]$ cat -n distros.sed
1 # sed script to produce Linux distributions report
2
3 1 i\
4 \
5 Linux Distributions Report\
6
7 s/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/
8 y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
我们脚本的第一行是注释(comment)。与Linux系统上的许多配置文件和编程语言一样,注释以 #
字符开头,后面是人类可读的文本。注释可以放置在脚本中的任何位置(尽管不在命令本身中),对任何可能需要识别和/或维护脚本的人都有帮助。
第2行是空白行。与注释一样,可以添加空白行以提高可读性。
许多 sed
命令支持行地址。这些用于指定要对输入的哪些行进行操作。行地址可以表示为单行编号、行号范围和特殊行号 $
,它表示输入的最后一行。
第3、4、5和6行包含要插入地址 1
(输入的第一行)的文本。 i
命令后面是反斜杠序列,然后是回车符,以产生转义回车符,即所谓的行连续字符(line-continuation character)。此序列可用于许多情况,包括shell脚本,它允许在文本流中嵌入回车符,而无需向解释器(在本例中为 sed
)发出已到达行尾的信号。 i
、a
(appends,附加文本,而不是插入文本)和 c
(替换文本)命令允许多行文本,只要除最后一行外的每一行都以行连续字符结尾。脚本的第六行实际上是插入文本的结尾,以纯回车符而不是行连续字符结尾,表示 i
命令的结束。
注意:行连续字符由反斜杠和回车符组成。不允许有中间空白(intermediary spaces)。
第7行是我们的搜索和替换(search-and-replace)命令。由于它前面没有地址,因此输入流中的每一行都受到其操作的影响。
第8行执行小写字母到大写字母的音译。请注意,与 tr
不同, sed
中的 y
命令不支持字符范围(例如 [a-z]
),也不支持POSIX字符类。同样,由于 y
命令前面没有地址,它适用于输入流中的每一行。
sed
的人也喜欢...sed
是一个功能强大的程序,能够对文本流执行相当复杂的编辑任务。它最常用于简单的单行任务,而不是长脚本。许多用户更喜欢其他工具来完成更大的任务。其中最受欢迎的是 awk
和 perl
。这些不仅仅是像这里介绍的程序这样的工具,还扩展到完整的编程语言领域。特别是 perl
,它经常被用于许多系统管理和行政任务,而不是shell脚本,并且是web开发的流行媒介。 awk
有点专业化。它的具体优势在于它能够操纵表格数据。它类似于 sed
,因为 awk
程序通常逐行处理文本文件,使用类似于 sed
概念的地址后跟动作的方案。虽然 awk
和 perl
都不在本书的范围内,但它们是Linux命令行用户可以学习的好技能。
aspell
我们将介绍的最后一个工具是 aspell
,一个交互式拼写检查器。 aspell
程序是名为 ispell
的早期程序的继承者,在很大程度上可以用作插入式替换。虽然 aspell
程序主要由需要拼写检查功能的其他程序使用,但它也可以作为命令行中的独立工具有效地使用。它能够智能地检查各种类型的文本文件,包括HTML文档、C/C++程序、电子邮件和其他类型的专用文本。
要拼写检查包含简单散文(prose)的文本文件,可以这样使用:
aspell check textfile
其中 textfile 是要检查的文件的名称。作为一个实际示例,让我们创建一个名为 foo.txt 的简单文本文件,其中包含一些故意拼写错误。
xxxxxxxxxx
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox jimps over the laxy dog.
接下来,我们将使用 aspell
检查文件:
xxxxxxxxxx
[me@linuxbox ~]$ aspell check foo.txt
由于 aspell
在检查模式下是交互式的,我们将看到这样的屏幕:
xxxxxxxxxx
The quick brown fox jimps over the laxy dog.
1) jumps 6) limps
2) imps 7) pimps
3) gimps 8) wimps
4) jump's 9) comps
5) Jim's 0) gimp's
i) Ignore I) Ignore all
r) Replace R) Replace all
a) Add l) Add Lower
b) Abort x) Exit
?
【Debian会自动套用系统字符集(LANG),若系统字符集是zh_CN.UTF-8,则会按中文检查。可使用 -l en
指定按英文进行检查。】:
xxxxxxxxxx
aspell -l en check foo.txt
在显示器的顶部,我们看到我们的文本突出显示了一个拼写可疑的单词。在中间,我们看到了从零到九的十个拼写建议,然后是其他可能的操作列表。最后,在底部,我们看到一个提示,准备接受我们的选择。
如果我们按 1 键, aspell
会用单词 jumps 替换有问题的单词,并继续移动到下一个拼写错误的单词, laxy 。如果我们选择替换 lazy , aspell
将替换它并终止。 aspell
完成后,我们可以检查我们的文件,看看拼写错误是否已被纠正:
xxxxxxxxxx
[me@linuxbox ~]$ cat foo.txt
The quick brown fox jumps over the lazy dog.
除非通过命令行选项 --dont-backup
另有说明,否则 aspell
会通过在文件名后附加扩展名 .bak 来创建一个包含原始文本的备份文件。
为了展示我们的 sed
编辑能力,我们将把拼写错误放回去,这样我们就可以重用我们的文件。
xxxxxxxxxx
[me@linuxbox ~]$ sed -i 's/lazy/laxy/; s/jumps/jimps/' foo.txt
sed
选项 -i
告诉 sed
“原地”(in-place)编辑文件,这意味着它不会将编辑后的输出发送到标准输出,而是使用所应用的更改来重写文件。我们还看到,通过用分号分隔,可以在一行上放置多个编辑命令。
接下来,我们将看看 aspell
如何处理不同类型的文本文件。使用 vim
等文本编辑器(喜欢冒险的人可能想尝试 sed
),我们将在文件中添加一些HTML标记。
xxxxxxxxxx
<html>
<head>
<title>Mispelled HTML file</title>
</head>
<body>
<p>The quick brown fox jimps over the laxy dog.</p>
</body>
</html>
现在,如果我们试图检查修改后的文件的拼写,就会遇到问题。如果我们这样做:
xxxxxxxxxx
[me@linuxbox ~]$ aspell check foo.txt
我们得到这些:
xxxxxxxxxx
<html>
<head>
<title>Mispelled HTML file</title>
</head>
<body>
<p>The quick brown fox jimps over the laxy dog.</p>
</body>
</html>
1) HTML 4) Hamel
2) ht ml 5) Hamil
3) ht-ml 6) hotel
i) Ignore I) Ignore all
r) Replace R) Replace all
a) Add l) Add Lower
b) Abort x) Exit
?
aspell
将看到HTML标签的内容拼写错误。这个问题可以通过包含 -H
(HTML)检查模式选项来克服,如下所示:
xxxxxxxxxx
[me@linuxbox ~]$ aspell -H check foo.txt
这将导致以下结果:
xxxxxxxxxx
<html>
<head>
<title>Mispelled HTML file</title>
</head>
<body>
<p>The quick brown fox jimps over the laxy dog.</p>
</body>
</html>
1) Mi spelled 6) Misapplied
2) Mi-spelled 7) Miscalled
3) Misspelled 8) Respelled
4) Dispelled 9) Misspell
5) Spelled 0) Misled
i) Ignore I) Ignore all
r) Replace R) Replace all
a) Add l) Add Lower
b) Abort x) Exit
?
HTML被忽略,只检查文件的非标记部分。在此模式下,HTML标记的内容将被忽略,不会检查拼写。但是,ALT标签的内容(受益于检查)将在此模式下进行检查。
注意:默认情况下, aspell
将忽略文本中的URL和电子邮件地址。此行为可以用命令行选项覆盖。还可以指定检查和跳过哪些标记标签。有关详细信息,请参阅 aspell
手册页。
在本章中,我们介绍了许多操作文本的命令行工具中的一些。在下一章中,我们将再看几个。诚然,尽管我们试图展示一些使用这些工具的实际例子,但你每天如何或为什么使用这些工具似乎并不明显。我们将在后面的章节中发现,这些工具构成了用于解决许多实际问题的工具集的基础。当我们进入shell脚本时,尤其如此,这些工具将真正显示出它们的价值。