Top

D17:常见任务和基本工具之七

文本处理

所有类Unix操作系统都严重依赖文本文件来存储数据。本章将介绍用于“切分”(slice and dice)文本的程序。
本章涉及的命令有: 目录:

文本的应用

文件

小的文本文件有助于保存简单的笔记。
也可以用文本格式编写大型文档,然后嵌入标记语言来描述完成文档的格式。

web页面

web页面是使用超文本标记语言(Hypertex Markup Language,HTML)或可扩展标记语言(Extensible Markup Language,XML)作为标记语言来描述文档视觉格式的文本文档。

email

电子邮件本质上是一种基于文本的媒介。即使是非文本附件也会转换为文本表示以进行传输。
邮件从头部开始(头部描述了消息的来源以及它在传递过程中接收到的处理),然后是消息的主体及其内容。

打印机输出

在类Unix系统上,发送给打印机的输出是纯文本,如果页面包含图形,则将其转换为文本格式的页面描述语言(page description language)比如“PostScript”,然后将其发送到生成要打印的图形点的程序。

程序源代码

文本处理对软件开发人员很重要的原因是,所有软件都是从文本开始的。源代码,程序员实际编写的程序部分,总是以文本格式。

重访老朋友

以下几个命令以前简单讨论过,现在再深入研究一下。

cat

cat命令有一些选项可以帮助更好地可视化文本内容,比如-A选项,用于在文本中显示非打印字符。
文本中tab字符会显示为^I(Ctrl-i),而行尾会有$(这有助于判断行尾是否有空格)。
在FreeBSD的cat命令中没有-A选项,可以使用-e显示结尾的$、用-t显示^I,-v则显示另外一些控制字符,比如^X表示ctl-X、^?表示删除、M-表示非ASCII字符。

Unix以换行符(ASCII 10)结束一行,而MS-DOS以及其派生版本使用序列回车符(sequence carriage return, ASCII 13)和换行符终止一行。
类Unix一般都有名为dos2unix和unix2dos的程序(FreeBSD只有unix2dos,需要额外安装),它们可以将文本文件转换为对应的格式。
这类转换很简单,主要涉及删除违规的回车,后面有例子实现此功能。

cat还有用于修改文本的选项,比如-n(对行进行编号)和-s(抑制多个空行的输出)。
示例:
[me@linuxbox ~]$ cat > foo.txt
The quick brown fox


jumped over the lazy dog.
[me@linuxbox ~]$ cat -ns foo.txt
1 The quick brown fox
2
3 jumped over the lazy dog.
[me@linuxbox ~]$	
以上例子中,第一行文字和最后一行文字之间有两个空行。-ns选项消除了一个重复的空行,并对剩余的行进行了编号。
虽然以上过程并未改变文件本身的内容,但影响了输出效果。

sort

sort程序对标准输出或命令行上指定的一个或多个文件的内容进行排序,并将结果发送到标准输出。例如:
[me@linuxbox ~]$ sort > foo.txt
c
b
a
[me@linuxbox ~]$ cat foo.txt
a
b
c	
sort可以接受命令行上的多个文件作为参数,因此可以将多个文件合并为一个已排序的整体:
sort file1.txt file2.txt file3.txt > final_sorted_list.txt
下表是sort常用选项:
选项 长选项 说明
-b --ignore-leading-blanks 默认情况下,从行的第一个字符开始对整行执行排序。
此选项会导致排序忽略行中的前导空格,并根据行中的第一个非空白字符计算排序。
-f --ignore-case 使排序不区分大小写
-n --numeric-sort 根据字符串的数值计算执行排序。
使用此选项可以对数值而不是字母值进行排序。
-r --reverse 按相反顺序排序。
结果按降序而不是升序排列。
-k --key=field1[,field2] field1field2关键字段排序,而不是整行。
-m --merge 将每个参数视为预排序文件的名称。将多个文件合并到一个排序结果中,无需执行任何其他排序。
-o --output=file 将结果发送到文件,而不是标准输出。
-t --filed-separator=char 定义字段分隔符字符。默认情况下,字段由空格或制表符分隔。
以下示例通过对du命令的结果进行排序来确定磁盘空间的最大用户(通常du命令会按路径名顺序列出摘要的结果):
[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个最大空间占用者:
[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	
通过使用nr选项,生成了一个反向数字排序,最大值首先出现在结果中。
这种排序之所以有效,是因为数值出现在每行的开头。但是如果想根据行中的某个值对列排序该怎么办?
以下是ls -l的执行结果:
[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	
使用sort按文件大小排序:
[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	
注意,如果ls使用了-h选项,sort排序时对比的是数字部分,很可能会出现849K大于34M的情况。
为避免这种情况,ls不要使用-h选项,或者使用ls -lhS | head命令来实现此效果(-S是按size逆序排列)。

对于sort而言,空白字符(空格和制表符)用作字段之间的分隔符,并在执行排序是,分隔符包含在字段中。
以下是ls输出中的一行,一共包含8个字段,第五个字段是文件大小:
-rwxr-xr-x 1 root root 8234216 2016-04-07 17:42 inkscape

以下一系列实验基于一个文件——distros.txt,该文件包含从2006年到2008年发行的三种流行的Linux发行版的历史。文件的每一行都有三个字段:发行名、版本号、发布日期,日期的格式为MM/DD/YYY。
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	
接下来尝试排序:
[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	
因为在字符集中,1在5之前,所以Fedora版本10位于顶部,而版本9却位于底部。
要解决这个问题,需要对多个键进行排序:第一个字段按字母排序、第二个字段执行数字排序。
-k选项可以使用多个实例,因此可以指定多个排序键。事实上,一个键可以包括一列字段。如果没有指定范围,sort将使用一个从指定字段开始并延申到行尾的键。
以下是多键排序的语法:
[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完全等效。
key选项的第一实例中,指定了包含在第一个key中的一系列字段。因为我们将排序限制在第一个字段,所以指定了1,1,这意味着“从字段1开始,到字段1结束”。
第二个实例制定了2n,这意味着字段2是排序键,排序应该是数字。
键说明符的末尾可以包含选项字母,以指示要执行的排序类型。这些字母与sort程序的全局选项相同:b表示忽略前导空格、n表示数字排序、r表示反向排序等等。

列表中的第三个字段包含一个日期,其格式不便于排序。
在计算机上,日期通常按YYY-MM-DD的顺序进行格式化,以便于按时间顺序排序。
sortkey选项允许指定字段内的偏移量,因此可以在字段内定义键:
[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来分离出月和日。
使用nr选项实现数字反向排序。b选项用于抑制日期字段中的前导空格。

有些文件不使用制表符或空格作为字段分隔,例如/etc/passwd文件:
[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)排序:
[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是轻量级的。当给定一个排序文件(或标准输入)时,它会删除所有重复的行,并将结果发送到标准输出。
通常与sort结合使用,以清除重复的输出。例如:sort foo.txt | uniq,其效果与sort -u foo.txt相同。

以下是示例:
[me@linuxbox ~]$ cat > foo.txt
a
b
c
a
b
c	
[ctrl-d]
[me@linuxbox ~]$ uniq foo.txt  uniq只删除相邻的重复行
a
b
c
a
b
c
[me@linuxbox ~]$ sort foo.txt | uniq
a
b
c
[me@linuxbox ~]$ sort -u foo.txt
a
b
c
uniq常用选项:
选项 长选项 描述
-c --count 输出一个重复列表,前面是行出现的次数
-d --repeated 只输出重复的行,而不输出唯一的行
-f n --skip-fields=n 忽略每行中的n个前导字段。像在sort中一样,字段用空白(空格或制表符)分隔,但uniq没有设置备用字段分隔符的选项(sort -t)。
-i --ignor-case 进行行比较时忽略大小写
-s n --skip-chars=n 忽略每行前导n个字符
-u --unique 仅输出唯一的行。忽略具有重复项的行。
以下示例使用uniq -c报告文本文件中发现的重复数:
[me@linuxbox ~]$ sort foo.txt | uniq -c
  2 a
  2 b
  2 c

切片和切割

cut

cut程序用于从一行中提取一段文本,并将提取出的部分输出到标准输出。
它可以接受多个文件参数或来自标准输入的输入。
使用以下选项指定要提取的行的位置:
选项 长选项 描述
-c list --characters=list 提取list定义的行的部分。
list可能由多个逗号分隔的数字范围组成。
-f list --fields=list list定义的行中提取一个或多个字段。
list可能包含一个或多个字段或字段范围,用逗号分隔。
-d delim --delimeter=delim 当指定了-f,使用delim作为字段分隔符。
默认情况下,字段必须由单个制表符分隔。
--complement 提取整行文本,但-c和/或-f指定的部分除外。
cut提取文本的方式相当死板,最好用于从其他程序生成的文件中提取文本,而不是由人类直接键入的文本。
使用cat -A distros.txt命令查看以前创建的文件:
[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选项来提取字段:
[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是以制表符分隔的,所以最好用cut来提取字段(-f)而不是字符(-c)。这是因为当文件以制表符分隔时,每一行不太可能包含相同数量的字符,这使得计算行中的字符位置变得困难或不可能。
在上一个例子中,我们获得了一组每行长度相同且格式也相同的数据,此时可以通过提取每行的年份来展示字符提取的工作原理:
[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。

GNU Coreutils包含了一个名为expand的程序,它接受一个或多个文件参数或标准输入,将输入的文本中的制表符替换为相应数量的空格,然后将修改后的文本输出到标准输出。例如:
[me@linuxbox ~]$ expand distros.txt|cat -A     
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$
 以上输出结果中已经没有^I了
[me@linuxbox ~]$ expand distros.txt | cut -c 23-
2006
2008
2008
2008
2007
2007
2006
2007
2007
2007
2006
2006
2008
2006
2008
2006
Coreutils还提供了unexpend程序,用制表符替代空格。

使用字段时,可以指定不同的字段分隔符:
[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也接受多个文件参数和/或标准输入。

distros.txt文件进行操作,以生成按时间倒序(-r)排列的文件:distros-by-date.txt
[me@linuxbox ~]$ sort -k 3.7nbr -k 3.1nbr -k 3.4nbr distros.txt > distros-by-date.txt
接下来使用cut提取前两个字段(发行版名称和版本号),并保存到文件distros-version.txt文件中:
[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	
最后一项准备工作时提取发布日期,并将其保存到名为distros-dates.txt的文件中:
[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命令将日期列置于发行版名称和版本之前,从而创建一个按时间顺序排列的列表:
[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

在某些方面,joinpaste一样,它向文件中添加列,但它使用了一种独特的方式。
join是一种通常与关系数据库相关联的操作。在关系数据库中,来自多个具有共享键字段的表的数据被组合以形成所需的结果。
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字段中的匹配值,join操作可以产生以下结果:
FNAME   LNAME   QUAN   ITEM
=====   =====   ====   ====
John    Smith   1      Blue Widget	

为演示join程序,我们需要制作几个带有共享关键字的文件。
基础文件使用distros-by-date.txt,从这个文件构建另外两个文件,其中一个包含发布日期和发布名称:
[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	
第二个文件包含发行日期和版本号:
[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 date”字段)。文件必须在关键字段上排序,以便join正常工作:
[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使用空白(空格或制表符)作为输入字段分隔符,使用单个空格作为输出字段分隔符。可以通过指定选项来修改此行为。

对比文本

comm

comm程序比较两个文本文件,并显示每一个文件的唯一行以及它们的共同行。
为了演示,我们先创建两个几乎一样的文件:
[me@linuxbox ~]$ cat > file1.txt
a
b
c
d
[me@linuxbox ~]$ cat > file2.txt
b
c
d
e
然后用comm对比它们:
[me@linuxbox ~]$ comm file1.txt file2.txt
a
       b
       c
       d
   e	
comm产生三列输出:第一列包含第一个文件独有的行;第二列包含第二个文件独有的行;第三列包含两个文件共有的行。
comm支持-n形式的选项,n可以是1、2或3。这些选项指定要抑制的列。
例如,如果只想输出两个文件共有的行,则抑制第一列和第二列的输出:
[me@linuxbox ~]$ comm -12 file1.txt file2.txt
b
c
d	

diff

diff也用于检测文件之间的差异,比comm复杂得多,它支持多种输出格式,并能够同时处理大量文本文件。
diff的一个常见用途是创建diff文件或补丁,这些文件或补丁由path等程序使用,以将文件的一个版本转换为另一个版本。
使用diff来看一下上面例子中创建的文件:
[me@linuxbox ~]$ diff file1.txt file2.txt
1d0
< a
4a4
> e	
默认输出对两个文件之间的差异进行了简洁描述。
默认格式下,每组更改之前都会有一个“change command",以“range operation range”的形式,描述将第一个文件转换为第二个文件所需更改位置和类型,如下表所示:
更改 描述
r1ar2 将第二个文件中位置r2处的行附加到第一个文件中位置r1处。
r1cr2 将第二个文件中位置r1处的行更改(替换)为位置r2处的行。
r1dr2 删除第一个文件中位置r1处的行,该行将出现在第二个文件中的范围r2处
在这种格式中,范围是以逗号分隔的起始行和结束行列表。虽然此格式是默认格式(主要用于POSIX兼容性和与传统Unix版本的diff的向后兼容性),但它不像其他可选格式那样广泛使用。
两种比较流行的格式是上下文格式(context format)和统一格式(unified format)。

当使用上下文格式(-c选项)时结果如下:
[me@linuxbox ~]$ diff -c file1.txt file2.txt
*** file1.txt2008-12-23 06:40:13.000000000 -0500
--- file2.txt2008-12-23 06:40:34.000000000 -0500
***************
*** 1,4 ****
- a
  b
  c
  d
--- 1,4 ----
  b
  c
  d
+ e	
输出以两个文件的名称及其时间戳开始。
第一个文件用星号标记,第二个文件用破折号标记。在清单的其余部分,这些标记将表示各自的文件。
接下来将看到一组更改,包括周围上下文行的默认数量。
第一组我们看到:
*** 1,4 ***
这表示第一个文件中的第一行到第四行。
第二组:
--- 1,4 ---
这表示第二个文件的第一行到第四行。

在变更组中,行的开头以四个指标之一开始,如下表所示:
指示信号 含义
blank(空) 为上下文显示的一行。这并不表明这两个文件之间存在差异。
- 删除了一行。这一行将出现在第一个文件中,但不会出现在第二个文件中。
+ 添加了一行。这一行将出现在第二个文件中,但不会出现在第一个文件中。
! 改变了一行。将显示该行的两个版本,每个版本位于更改组的相应部分。
统一格式与上下文格式类似,但更简洁。用-u选项指定:
[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

patch程序用于将更改应用于文本文件。它接受diff的输出,通常用于将旧版本文件转换为新版本。

使用diff/patch有两个显著的优点: GNU文档建议使用以下格式:
diff -Naur old_file new_file > diff_file
以下演示:
[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	

即时编辑

使用文本编辑器是一种交互式操作,需要手动移动光标,然后键入所做的修改。
下面介绍的即时编辑命令,实际是一种非交互式操作。

tr

tr程序用于“移译”(transliterate)字符。可以将其视为一种基于字符的搜索和替换操作。Transliterate是将字符从一个字母表转换到另一个字母表的过程。
以下例子将小写字母移译为大写字母:
[me@linuxbox ~]$ echo "lowercase letters" | tr a-z A-Z
LOWERCASE LETTERS	
tr对标准输入进行操作,然后将结果输出到标准输出。
tr允许两个参数:一组要转换的字符,和一组相应的要转换的字符。
字符集可以用三种方式之一表示: 大多数情况下,两个字符集的长度应该相等;但第一个字符集可能比第二个字符集大,特别是如果我们想将多个字符转换成单个字符:
[me@linuxbox ~]$ echo "lowercase letters" | tr [:lower:] A
AAAAAAAAA AAAAAAA

除了移译,tr允许从输入流中简单地删除字符。
前面提到过将MS-DOS文本文件转换为Unix样式文本地时候需要从每行末尾删除回测字符。可以通过tr执行:
tr -d '\r' < dos_file > unix_file

tr的一个有趣用途是文本进行ROT13(rotate by 13 places)编码。
ROT13是一种基于简单替换密码的普通加密类型。与其说是“加密”,不如说是“文本混淆”。
该方法只需将每个字符在字母表中上移13位,对文本执行第二次操作就可以将其恢复原样。
echo "secret text" | tr a-zA-Z n-za-mN-ZA-M
frperg grkg
echo "frperg grkg" | tr a-zA-Z n-za-mN-ZA-M
secret text	
实作例子:
echo "This is a secret text." | tr a-zA-Z n-za-mN-ZA-M
Guvf vf n frperg grkg.	
echo "This is a secret text." | tr a-zA-Z n-za-mN-ZA-M | tr a-zA-Z n-za-mN-ZA-M
This is a secret text.
	
许多电子邮件程序和Usernet新闻组阅读器都支持ROT13编码。

tr也可以执行另一个技巧,使用-s选项可以“挤压”(删除)重复的字符:
[me@linuxbox ~]$ echo "aaabbbccc" | tr -s ab
abccc	
[me@linuxbox ~]$ echo "abcabcabc" | tr -s ab
abcabcabc
[me@linuxbox ~]$ echo "aaaaaabcabbbbbcaaaaabc" | tr -s ab
abcabcabc
重复字符必须相邻,否则挤压没有效果。

sed

sed是stream editor的缩写。它对文本流(一组指定文件或标准输入)执行文本编辑。
sed非常复杂,它的工作方式是要么被赋予一个编辑命令,要么被赋予包含多个命令的脚本文件的名称,然后在文本流的每一行上执行这些命令。
[me@linuxbox ~]$ echo "front" | sed 's/front/back/'
back	
以上例子,echo生成一个单子的文本流,并将其导入sed。sed一次对文本流中的文本执行指令s/front/back并生成输出“back”。
这类似与vi中的“substitution”(search and replace)命令。

以上例子中,sed中的命令以一个字母开头。替换命令由字母s表示,后跟搜索和替换字符串,以斜杠字符作为分隔符分隔。分隔符字符的选择是任意的。按照惯例一般使用斜杠。
以下例子与上例等效:
[me@linuxbox ~]$ echo "front" | sed 's_front_back_'
back	
sed的大多数命令前面都可以有一个“地址”,该地址指定要编辑输入流的哪一行。如果省略地址,则在输入流中的每一行上执行编辑命令。
最简单的地址形式是行号。
[me@linuxbox ~]$ echo "front" | sed '1s/front/back/'
back	
[me@linuxbox ~]$ echo -e "front\nfront" | sed '2s/front/back/'
front
back
[me@linuxbox ~]$ echo -e "front\nfront" | sed 's/front/back/'
back
back
地址可以有多种表达方式:
地址 解释
n 行号。n为整数。
$ 最后一行
/regexp/ 与POSIX基本正则表达式匹配的行。
注意,正则表达式由斜杠字符分隔。
或者,正则表达式可以用替换字符分隔,方法是使用\cregexpc指定表达式,其中c是替换字符。
addr1,addr2 addr1addr2的一系列行。
地址可以是前面列出的任何一种单一地址格式。
first~step 首先匹配first数字匹配的行,然后以step间隔匹配后续的行。
例如,1~2表示每个奇数行;5~5表示从第五行开始,然后每隔武行。
addr1,+n 匹配addr1和后续的n
addr! 匹配除了addr以外的所有行。
以下是针对以前创建的distros.txt文件做的一系列实验。
首先,列出一个范围内的行:
[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	
以上例子打印一系列行,从第一行开始,一直打印到第五行。
-n选项(“no auto-print"选项)抑制sed默认打印每一行。
p命令则表示打印匹配的行。
下一步尝试正则表达式:
[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 'SUSE' distros.txt相同的效果。

最后,我们在地址后面加上感叹号来否定那个地址:
[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编辑命令除了sp之外还有:
命令 解释
= 输出当前行号。
a 在当前行后追加文本。
d 删除当前行。
i 在当前行前面插入文本。
p 打印当前行。
默认情况下,sed打印每一行,并且只编辑与文件中指定地址匹配的行。可以通过指定-n选项来覆盖默认行为。
q 退出sed,不处理更多行。
如果没有指定-n选项,则输出当前行。
Q 退出sed,不处理更多行。
s/regexp/replacement/ 将匹配regexp的内容更换为replacement
replacement可能包含特殊字符&,它相当于由regexp匹配的文本。
此外,replacement可能包含序列\1到\9,它们是regexp中相应子表达式的内容。
替换后的尾部斜杠之后,可以指定一个可选标志来修改s命令的行为。
y/set1/set2 通过将set1中的字符转换为set2中的相应字符来执行移译。
tr不同,sed要求两个集合的长度相同。
s命令是sed最常用的编辑命令。它的格式是 sed 's/regexp/replacement/
[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	
以上例子中,sed命令将日期的MM/DD/YYYY修改成了YYYY-MM-DD格式。
regexp是:\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\);而replacement是:\3-\1-\2。
对于出现在行尾的MM/DD/YYYY格式的日期,它的正则表达式为:
[0-9]{2}/[0-9]{2}/[0-9]{4}$
它匹配两个数字、一个斜杠、两个数字、一个斜杠、四个数字、行尾。
这样就可以处理regexp了,但是replacement该如何创建呢?
为了解决这个问题,需要引入一个新的正则表达式特性——back references,回调引用。它出现在一些使用BRE的应用程序中。
回调引用的原理如下:如果序列\n出现在replacement中,其中n是1到9之间的数字,则该序列将引用前面正则表达式中相应的子表达式。
要创建子表达式,只需要将它们括在括号中:
([0-9]{2})/([0-9]{2})/([0-9]{4})$
现在我们有了三个子表达式:第一个包含月份、第二部包含日期、第三个包含年份。然后按以下方式构造replacement
\3-\1-\2
就得到了年-月-日。现在命令看起来像这样:
sed 's/([0-9]{2})/([0-9]{2})/([0-9]{4})$/\3-\1-\2/' distros.txt
但是以上写法有两个问题:
  • sed试图解释s命令时,正则表达式中的额外斜杠会使sed感到困惑。
  • 由于sed默认只接受基本正则表达式,因此正则表达式中的几个字符(本例中为左右括号、左右花括号和斜杠)被视为文本,而不是元字符。
  • 可以使用反斜杠来解决这个问题:
    sed 's/\([0-9]\{2\}\)\/\([0-9]\{2\}\)\/\([0-9]\{4\}\)$/\3-\1-\2/' distros.txt

    s命令的另一个功能是在替换字符串后面使用可选标志。其中最重要的标志是g,它指示sed将搜索和替换全局应用于一行,而不仅仅是第一个实例(默认情况下)。
    [me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/'
    aaaBbbccc
    [me@linuxbox ~]$ echo "aaabbbccc" | sed 's/b/B/g'
    aaaBBBccc	
    

    可以使用-f选项在脚本文件中构造更复杂的命令。
    创建一个名为distros.sed的脚本,内容如下:
    # 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/	
    
    然后运行以下命令:
    [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	
    
    上面脚本的含义是什么?我们使用cat命令列出行号,然后一行行解释:
    [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/	
    	
    第一行是注释。与Linux系统上许多配置文件和编程语言一样,注释以#字符开头。注释可以放在脚本的任何位置,但不能放在命令中间。
    第二行是空行,可以用来提高可读性。 第三、四、五、六行包含要插入到地址1(输入的第一行)的文本。i命令后面是一个反斜杠序列,然后是一个回车符序列,以生成转义回车符,或者称为行连续字符。
    这个序列可以在许多情况下使用,包括shell脚本,它允许在文本流中嵌入回车符,而无需向解释器(本例中为sed)发出到达行末尾的信号。
    i、a(追加文本,而不是插入文本)和c(替换文本)命令允许多行文本,只要每行(除最后一行)以行连续字符结尾。
    第六行实际上是插入文本的结尾,并以一个普通的回车符结束,而不是一个行延续字符,表示i命令结束。
    行连续字符由反斜杠和回车组成,不允许中间由空格。
    第七行是搜索和替换命令。因为它前面没有地址,所以输入流中的每一行都受其操作的影响。
    第八行将小写字母移译为大写字母。注意,sed中的y命令不支持字符范围(例如[a-z]),也不支持POSIX类。

    aspell

    aspell是一个交互式拼写检查工具。debian和FreeBSD都需要单独安装。FreeBSD下不会用。
    debian下简单格式为:
    aspell check textfile
    实例:
    [me@linuxbox ~]$ cat > foo.txt
    The quick brown fox jimped over the laxy dog.
    [me@linuxbox ~]$ aspell check foo.txt
    The quick brown fox jimped over the laxy dog.
    
    1) jumped   6) wimped
    2) gimped   7) camped
    3) comped   8) humped
    4) limped   9) impede
    5) pimped   0) umped
    i) Ignore   I) Ignore all
    r) Replace  R) Replace all
    a) Add      l) Add Lower
    b) Abort    x) Exit
    ?	
    
    通过输入对应的字母选择修改或放弃修改。
    如果对文件进行了修改,aspell将生成一个添加了.bak扩展名的文件以备份原始文件。如果使用了--dont-backup选项,则不生成备份。
    使用sed命令可以将拼写错误还原:
    sed -i 's/lazy/laxy/; s/jumped/jimped/' foo.txt
    -i选项告诉sed“就地(in-place)”编辑文件,这意味着它不会将编辑的输出发送到标准输出,而是直接重写文件。
    分号可以将多个编辑命令分隔开,从而在行中放置多个编辑命令。

    aspell无法分辨一些特殊关键字,比如HTML文件中的html。这需要使用特定选项来指定文件的格式。比如-H(HTML)表示忽略HTML标记:
    aspell -H check foo.txt
    默认情况下,aspell会忽略文本中的URL和电子邮件地址。可以用命令行选项覆盖此行为。还可以指定检查和跳过哪些标记。