捐赠?

Harley Hahn
个人主页

给 Harley
发送留言


来自 Harley Hahn 的
个人寄语

Unix 之书
主页

搜索

章节列表

目录

插图列表

各章...
   1   2   3
   4   5   6
   7   8   9
  10  11  12
  13  14  15
  16  17  18
  19  20  21
  22  23  24
  25  26

术语表

各附录...
  A  B  C
  D  E  F
  G  H

命令
摘要...

• 按字母顺序
• 按类别

Unix-Linux
年表

互联网
资源

错误与
勘误

推荐语


教师
与学生
资料...

主页
与概述

练习
与答案

Unix 模范
课程与
课程大纲

教师用
PowerPoint 文件

第 17 章...

过滤器:比较与提取 (Filters: Comparing and Extracting)

在第 16 章中,我们详细讨论了过滤器的概念:一种读取 和写出文本数据的程序,它一次处理一行,从标准输入 读取,写入标准输出。当时我就指出,大多数过滤器 都被设计成把一件事做好的工具。当你读完接下来的 三章 — 我们将在那里讨论许多具体的过滤器 — 这个说法会越来越有道理。

本章要讨论的过滤器,是用来比较文件和从文件中 提取部分内容的。起初你可能会觉得这些话题很枯燥, 我不怪你。毕竟,这一章里的细节多到足以把一匹 大马噎死。不过,随着你读完这些细节,开始看出 过滤器背后设计的巧妙之处,你就会体会到它们其实 多么有趣。事实上,本章讨论的这些过滤器不仅有趣, 还是 Unix 工具箱中最有用、最重要的程序之一。

跳至页首

比较文件
(Comparing Files)

多年来,Unix 程序员创造了各种各样的工具,帮助你回答 这些问题:两个文件包含的数据是否完全相同?如果不是, 数据从一个文件到另一个文件有什么不同?比较两个文件 比你想象的要复杂,因为比较的方式和显示结果的方式 都有很多种。

在接下来几节中,我们将讨论这些工具中最重要的几个。 具体来说,我会解释它们做什么、能比较哪些类型的文件, 以及它们哪些选项最有用。一路上,我还会给你展示例子, 并给你重要的提示。我的目标很简单:每当需要比较文件时, 你都应该能够快速分析情况,决定使用哪个程序和哪些 选项,并且能够解读结果。

图 17-1 总结了最重要的文件比较程序(也就是我们将要 讨论的那些)。为了完整起见,我还加入了相关的排序和 从文件中挑选数据的程序。这些程序我们将在 第 19 章讨论。

图 17-1(Figure 17-1):用于比较、排序和从文件中挑选数据的程序

Unix 有各种各样比较文件的程序,其中最重要的是 commcmpdiffsdiff (后者可以看作 diff 的一种变体)。 与之密切相关的还有排序文件的程序(sort)、 挑选文本行的程序(uniq),以及提取文件 部分内容的程序(cutgreplook)。

这张表通过展示这些程序使用的数据类型(二进制还是文本、 已排序还是未排序)、使用的文件数量以及它们的主要用途, 对这些程序做了总结。详见正文。(排序和挑选数据的程序 在第 19 章讨论。二进制文件和文本文件在 第 19 和 23 章讨论。)

过滤器 用途 文件类型 文件数量
cmp比较两个文件17二进制或文本两个
comm比较两个已排序文件,显示差异17文本:已排序两个
diff比较两个文件,显示差异17文本两个
sdiff比较两个文件,显示差异17文本两个
cut提取指定的数据列/字段17文本一个或多个
paste合并数据列17文本一个或多个
sort对数据排序19文本一个或多个
uniq挑选重复/唯一的行19文本:已排序一个
grep挑选包含指定模式的行19文本一个或多个
look挑选以指定模式开头的行19文本:已排序一个

跳至页首

比较任意两个文件: cmp
(Comparing Any Two Files: cmp)

相关过滤器: commdiffsdiff

你只会在一种情况下使用 cmp:看看两个文件是否 完全相同。语法是:

cmp file1 file2

其中 file1file2 是文件的名称。

cmp 程序一次比较一个字节,看看两个文件是否 相同。如果两个文件中对应的字节完全一样,那么这两个 文件就是一模一样的,这种情况下 cmp 什么也不 做。(没有消息就是好消息。)如果文件并不相同, cmp 会显示一条相应的消息。

比如说,假设你有某个程序的两个版本:calculate-1.0calculate-backup。你想看看它们是否完全相同。 使用:

cmp calculate-1.0 calculate-backup

如果文件相同,你什么也看不到。如果文件不匹配,你会 看到类似下面这样的消息:

calculate-1.0 calculate-backup differ: byte 31, line 4

正如你在图 17-1 中所看到的,还有几个其他程序也可以 用来比较文件(commdiffdiff3sdiff)。所有这些程序处理的都是文本文件。 也就是说,它们期待的是文本行:每行包含零个或多个 常规字符(字母、数字、标点、空白),以换行符结尾。

由于 cmp 一次比较一个字节,它并不关心文件里 是什么类型的数据。因此,你可以用 cmp 比较任何 类型的文件,文本文件或二进制文件都行。例如,上面 那个例子比较的是两个包含可执行程序的二进制文件。 你也可以比较两个音乐文件、两张图片、两个文字处理 文档,等等。(我们将在第 19 和 23 章讨论文本文件和 二进制文件。)

跳至页首

比较已排序的文本文件: comm
(Comparing Sorted Text Files: comm)

相关过滤器: cmpdiffsdiff

comm 程序逐行比较两个已排序的文本文件。当你有 两个相似的文件,想找出其中的差异时,就用 comm。 语法是:

comm [-123] file1 file2

其中 file1file2 是已排序文本文件的 名称。

comm 的妙处在于,它能让你直观地看出两个文件之间 的差异。它把输出显示成三列:第一列包含只在第一个 文件里出现的行;第二列包含只在第二个文件里出现的行; 第三列包含两个文件里都有的行。让我给你举个例子。

一对亲密的朋友 Frick 和 Frack(*),想知道他们还有多少 共同的朋友。他们各自把自己朋友的名字列成一份清单, 把清单输入一个文件,然后用 sort 命令 (第 19 章)对这个文件排序。接着他们用 comm 比较这两个文件。

* 脚注

Frick 和 Frack 是两位著名喜剧滑冰演员 Werner Groebli 和 Hans Mauch 的艺名。Groebli 和 Mauch 于 1937 年从 他们的祖国瑞士来到美国,几十年来作为最初版 Ice Follies(冰雪狂欢)演出的成员四处表演。他们最 著名的动作是"悬臂分腿滑行"(可以到网上找照片看看)。

Frick 朋友名单的排序结果在一个叫作 frick 的文件里:

Alison Wonderland
Barbara Seville
Ben Dover
Chuck Wagon
Noah Peel

Frack 朋友名单的排序结果在一个叫作 frack 的文件里:

Alison Wonderland
Barbara Seville
Candy Barr
Chuck Wagon
Noah Peel
Sue Perficial

Frick 和 Frack 用下面这条命令比较这两份名单:

comm frick frack

输出是:

        Alison Wonderland
        Barbara Seville
Ben Dover
    Candy Barr
        Chuck Wagon
        Noah Peel
    Sue Perficial

注意这三列。第一列只有一个名字(Ben Dover)。这说明 只有唯一的一行是第一个文件(frick)所特有的。 第二列有两个名字(Candy Barr、Sue Perficial)。这说明 有两行是第二个文件(frack)所特有的。第三列有 四个名字(Alison Wonderland、Barbara Seville、Chuck Wagon、Noah Peel)。这说明有四行是两个文件共有的。 于是 Frick 和 Frack 得出结论:他们有四个共同的朋友。

在这个例子里,文件很小 — 两个文件加起来,Frick 和 Frack 一共只有七个朋友 — 你大概在想,他们何必 费劲去建立两个文件、排序、运行 comm 命令, 再解读输出。Frick 和 Frack 直接互相问一问: "你认识 Alison 吗?你认识 Barbara 吗?"等等,岂不是 更简单?

答案当然是:确实更简单,但这只是个刻意编出来的例子。 如果你处理的是有几百行甚至几千行的已排序文件 — 比如客户记录、统计数据,或者你的 MP3 播放器里的一份 长长的歌曲清单 — 又当如何?这种情况下,靠手工 比较清单几乎是不可能的。你必须有一个像 comm 这样的程序。

事实上,当你有两份略微不同的已排序文件 — 也许是 因为一点小错误 — 而你想找出这个不同时,comm 格外有用。例如,假设你有两份很长的已排序数字清单。 在清单的某个地方,有一些数字对不上。你用 comm 比较这两个文件,然后在输出的中间看到:

        01023331
        01023340
        01023356
01023361
    01023362
        01023378
        01023391
        01023401

这就准确地告诉了你两个文件不匹配的位置。

为了让你能控制输出,comm 允许你分别用 -1-2-3 选项来屏蔽第一、 第二或第三列的输出。在刚才最后一个例子中,你可以用 -3 屏蔽第三列,这样就去掉了所有不必要的输出:

01023361
    01023362

现在你看到的只是那些不同的行,而这正是你想看的。 想象一下,如果文件里有几千个数字,这能为你省下多少 时间。

如果你想屏蔽多于一列,只要把选项组合起来用。例如, 考虑我们上面讨论过的 Frick 和 Frack 的朋友名单。 假设 Frack 只想显示那些是他的朋友、却不是 Frack 的 朋友的人。他要做的只是屏蔽第一列和第三列:

comm -13 frick frack

输出只显示那些为第二个文件所特有的行:

Candy Barr
Sue Perficial

— 提示 —

comm 的表现不符合预期,最常见的原因是输入文件 没有排序。

如果你需要比较两个文件,但又不想把行排序 — 比如 排序会把数据搞乱 — 那你就不能用 comm。 你应该改用 diff(本章后面会讲)。比较一个程序 不同版本的源代码时,就是这种情况。

跳至页首

比较未排序的文本文件: diff
(Comparing Unsorted Text Files: diff)

相关过滤器: cmpcommsdiff

comm 程序能直观地告诉你两个文本文件有何不同。 不过,comm 有两个局限。第一,输入文件必须是 已排序的,而很多情况下这是办不到的。例如,假设你有 一个长文件的两个不同版本,比如一个计算机程序或一篇 文章,你想知道它们有何不同。由于程序或文章的行是没有 排序的,你就不能用 comm。当然,你可以先给文件 排序,但那样输出的就毫无意义了:你也许能找出差异, 却丢掉了上下文。

此外,comm 的输出在比较小文件、甚至中等大小的 文件时表现不错,但处理大文件时就会让人摸不着头脑。 这同样是个上下文的问题。比较大型文件时,重要的是 输出不仅要显示出差异,还要显示出它们的位置,并且 要以一种易于找到差异所在行的方式呈现。

diff 程序正是为克服这些局限而设计的。因此,当 你想要(1)比较未排序的文件,或者(2)比较大型文件时, 就用 diff。更广泛地说,凡是那种不时有增量添加、 删除或修改的作品,diff 都可以用来找出其中的 差异。例如多年来,程序员一直用 diff(或类似 diff 的工具)来跟踪一个程序在修改过程中各版本 之间的变化。

开始之前,我必须提醒你:diff 的输出一开始看起来 会有点晦涩,直到你习惯为止。不过,你一定会习惯 的。无论如何,diff 是一个强大而有用的程序, 你有必要学会如何使用它,如果你是程序员就更是如此。

diff 的语法如下:

diff [-bBiqswy] [-c|-Clines|-u|-Ulines] file1 file2

其中 file1file2 是文本文件的名称, lines 是要显示的上下文行数。

当你比较两个完全相同的文件时,diff 不显示任何 输出(与 cmp 类似)。如果文件不一样,diff 默认会显示一组操作说明:照着做,就能把第一个文件 变成第二个文件。这里举个例子。

我们有两个文件。文件 old-names 的内容是:

Gene Pool
Will Power
Paig Turner
Mark Mywords

文件 new-names 的内容是:

Gene Pool
Will Power
Paige Turner
Mark Mywords

你会注意到,唯一的区别是第三行中 "Paige" 的拼写。 要比较这两个文件,我们使用:

diff old-names new-names

输出是:

3c3
< Paig Turner
---
> Paige Turner

正如我所解释的,diff 的目标是显示出那些操作 说明,你照着做就能把第一个文件变成第二个文件。这些 说明的语法简单而精炼,理解它可能需要一点练习。但是, 我确实希望你熟悉这类说明,因为它们是 Unix 文化的标准 组成部分。事实上,你会在各种各样的场合遇到这种语法, 不仅仅是在使用 diff 的时候。

diff 的输出使用三种不同的单字符说明:c (change,修改)、d(delete,删除)和 a (append,追加)(*)。在上面的例子中,你只看到一条 c 说明。这意味着,要把第一个文件变成第二个 文件,你只需要做一处修改,一个简单的改动。

* 脚注

为什么是这三种说明?对于两个彼此大致相似的文件,你 总可以通过修改、删除和追加操作的某种组合,把其中一个 变成另一个。花点时间想一想,直到你觉得这理所当然。

在每个 cda 字符的左边和 右边,你会看到一串行号。可能只有一个行号(比如上面的 3),也可能是一串连续的行(比如 16,18)。 左边的数字指第一个文件中的行;右边的数字指第二个文件 中的行。在我们的例子里,说明 3c3 告诉我们要把 第一个文件的第 3 行改成第二个文件的第 3 行。

每当 diff 要求修改时,它会显示出每个文件中的 实际内容行。来自第一个文件的行用 <(小于号) 字符标记。来自第二个文件的行用 >(大于号) 字符标记。为了便于阅读,两组行之间用一行由几个连字符 组成的线(---)隔开。

再来看一个例子,其中 old-names 的内容是:

Gene Pool
Paige Turner
Mark Mywords

new-names 的内容是:

Gene Pool
Will Power
Paige Turner
Mark Mywords

这种情况下,唯一的区别是第一个文件里没有 "Will Power" 这个名字。当你对这两个文件使用 diff 时,输出是:

1a2
> Will Power

这告诉了你如何把第一个文件变成第二个文件。你要做的 只是给第一个文件追加一行。具体来说,就是把第二个 文件的第 2 行追加到第一个文件的第 1 行之后。注意, diff 显示了需要追加的实际那一行。> 字符 告诉你,这一行来自第二个文件。

现在看第三个例子,其中 old-names 的内容是:

Gene Pool
Will Power
Paige Turner
Mark Mywords

new-names 的内容是:

Gene Pool
Will Power
Paige Turner

这里的区别是第二个文件里没有 "Mark Mywords" 这个 名字。当你使用 diff 时,输出是:

4d3
< Mark Mywords

这种情况下,diff 是在告诉你:要把第一个文件 变成第二个文件,你需要删除第一个文件的第 4 行。同样, 实际的行也显示了出来。< 字符告诉你这一行 来自第一个文件。(记住,diff 的目标是告诉你 如何把第一个文件变成第二个文件。)

注意:在 d 说明中,d 后面的那个数字 (本例中是 3)一般可以忽略。它表示 diff 在第二个文件中发现差异的位置。

为了让这部分讨论更完整,让我用一个更贴近实际的例子 给你展示 diff 是如何工作的。考虑下面两个文件, 每个文件都包含某个 Perl 脚本中的一些代码。(不必担心 这些代码做什么;只要关注 diff 命令的输出就行。) 第一个文件 command-1.01.pl 的内容是:

# Check for illegal content
# in order to prevent spam
# If the address contains a URL, abort
  if ($required{"address"} =~ m/(http):\/\// ) {
  $error_count += 1;
  }

第二个文件 command-1.02.pl 的内容是:

# If the address contains a URL, abort
  if ($required{"address"} =~ m/(http|https|ftp):\/\// ) {
  $error_count += 1;
  &error_exit ("No URLs allowed");
  }

下面这条 diff 命令比较这两个文件:

diff command-1.01.pl command-1.02.pl

输出是:

1,2d0
< # Check for illegal content
< # in order to prevent spam
4c2
< if ($required{"address"} =~ m/(http):\/\// ) {
---
> if ($required{"address"} =~ m/(http|https|ftp):\/\// ) {
5a4
>   &error_exit ("No URLs allowed");

有两种方式来解读这份输出。照字面理解,它告诉我们要 照哪些说明做,才能把第一个文件变成第二个文件:

• 删除第一个文件的第 1 行和第 2 行。
• 把第一个文件的第 4 行改成第二个文件的第 2 行。
• 把第二个文件的第 4 行追加到第一个文件的第 5 行之后。

更好的解读方式是,你能读懂它,并且在瞬间 — 以一种 对你来说有意义的方式 — 明白这两个文件有何不同。 当然,这正是你学习使用 diff 的原因。关键在于能 读懂 c(修改)、d(删除)和 a(追加) 这三种说明,并立刻领会它们的含义。你可以想象,这需要 练习。但只要假以时日,你就能又快又轻松地读懂并理解 这样的输出。

跳至页首

配合 diff 使用的选项
(Options to Use With diff)

diff 程序相当复杂:它有很多选项,还能以多种 方式产生输出。在这一节和下一节里,我会讨论最重要的 那些选项。完整的细节,请查看你系统上的 man 手册页 (man diff)。

前面几个选项是告诉 diff 在比较时忽略某些差异。 -i(case insensitive,忽略大小写)选项告诉 diff 忽略大写与小写字母之间的任何差异。例如, 当你使用 -i 时,diff 认为下面三行是 相同的:

This is a BIG test.
this is a big test.
THIS IS A BIG TEST.

-w-b 选项让你可以控制 diff 如何处理空白字符(空格和制表符)。当你的数据用你希望 忽略的空格或制表符排版时,这些选项很有用。-w (whitespace,空白)选项忽略所有空白字符。例如, 用了 -w 之后,下面两行会被认为是相同的。

XX
X     X

-b 选项与此类似,但它不会忽略所有空白字符; 它只忽略空白字符数量上的差异。例如,如果你使用 -b,上面那两行就会被认为是相同的,因为 第二行有空白字符而第一行没有。不过,下面两行会被认为 是相同的:

X  X
X     X

这是因为两行都含有空白字符,它们只是在空白字符的数量 上有所不同。-w-b 之间的区别很微妙, 所以如果你遇到空白字符方面的问题并被搞糊涂了,不妨 两个选项都试一试,看看哪个更适合你的具体数据。

-B(blank lines,空行)选项告诉 diff 忽略所有空行。例如,假设你有两个文件,里面是你写的 一篇文章的不同版本。你想比较它们,但一份是单倍行距, 另一份是双倍行距。如果你使用 -B 选项,diff 就会忽略空行,只看有文本的行。

其余的 diff 选项控制 diff 如何显示它的 结果。-q(quiet,安静)选项告诉 diff: 当两个文件不同时,不要列出细节。例如,如果你比较 frickfrack 两个不同的文件,并且使用了 -q,你只会看到:

Files frick and frack differ

因此,用 diff -q 比较两个文件,本质上与使用 cmp(本章前面讲过)是一样的。最大的区别在于 diff 只比较文本文件,而 cmp 可以处理 任何类型的文件。

正如我前面提到的,当 diff 发现两个文件相同时, 它什么都不显示。这在许多 Unix 程序中很常见:当它们 无话可说时,就保持沉默。不过,有时候你会希望得到一个 明确的提示,告诉你两个文件是一模一样的。这种情况下, 你可以使用 -s(same,相同)选项。例如,如果你 比较 frickfrack 两个文件而它们相同, 通常你什么也看不到。但如果你使用 -s,你会看到:

Files frick and frack are identical

跳至页首

比较文件时的输出格式:
diff, sdiff
(Output Formats When Comparing Files: diff, sdiff)

正如我们前面讨论的,当 diff 比较两个文件时, 默认输出由一些说明(cda)连同 行号组成。照着这些说明做,就能把第一个文件变成第二个 文件。这种输出的优点是简洁,而且一旦你习惯了,它其实 是可读的。以我的经验,大多数时候这些就够你用了。

然而,默认格式的缺点是,等你习惯了它,你大脑里的 灰质就已经被不可逆转地改变了。最大的问题是,当你读 这份输出时,几乎没有上下文。你看到的只是一些行号加上 需要修改的行。出于这个原因,diff 提供了三个选项 (-c-u-y),它们能产生更易读 的输出。另外还有一个程序 sdiff,它可以并排比较 两个文件。下面就是具体细节。

diff 加上 -c(context,上下文)选项, 会以一种不如默认输出简洁、但更容易理解的格式显示 两个文件之间的差异。diff 不再只给出说明和行号, 而是显示实际不同的那些行,并在其上下各多显示两行。 这里举个例子。

你有两个文件要比较。第一个文件 smart-friends 的内容是:

Alba Tross
Dee Compose
Pat D. Bunnie
Phil Harmonic

第二个文件 rich-friends 的内容是:

Alba Tross
Dee Compose
Mick Stup
Pat D. Bunnie

首先,让我们用常规方式比较这两个文件:

diff smart-friends rich-friends

输出很简洁,但有点晦涩:

2a3
> Mick Stup
4d4
< Phil Harmonic

现在,让我们使用 -c 选项:

diff -c smart-friends rich-friends

输出长得多,但也更容易理解:

*** smart-friends 2009-02-14 15:33:50.000000000 -0700
--- rich-friends 2009-02-14 15:34:04.000000000 -0700
***************
*** 1,4 ****
  Alba Tross
  Dee Compose
  Pat D. Bunnie
- Phil Harmonic
--- 1,4 ----
  Alba Tross
  Dee Compose
+ Mick Stup
  Pat D. Bunnie

最上面两行给出了有关这两个文件的信息。第一个文件用 *(星号)字符标记;第二个文件用 -(连字符) 字符标记。在这些行之后,你可以看到从每个文件中截取的 片段,准确显示了要让两个文件完全相同需要改哪些地方。

虽然这种格式比默认输出更容易理解,但它有一个明显的 缺点:由于 diff 会显示两个文件的片段,其中有 重复的文本,于是输出量很大。想想我们的例子只是比较了 两个只有简单差异的短文件,你就能想象,如果比较的是两 个有很多差异的大文件,输出会有多长。这种情况下, -c 的输出比默认格式啰嗦得多。

作为一种折中,你可以使用 -u(unified output, 统一输出)选项。它产生的输出与 -c 类似,但不会 重复那些相同的行。例如,当你使用:

diff -u smart-friends rich-friends

输出是:

--- smart-friends 2009-02-14 15:33:50.000000000 -0700
+++ rich-friends 2009-02-14 15:34:04.000000000 -0700
@@ -1,4 +1,4 @@
 Alba Tross
 Dee Compose
+Mick Stup
 Pat D. Bunnie
-Phil Harmonic

— 提示 —

默认情况下,当你把 diff-c-u 一起使用时,输出会在每处差异的上下各显示 两行上下文。

如果你想显示不同数量的上下文行,可以用 -C (大写 "C")代替 -c,用 -U(大写 "U") 代替 -u。在 -C-U 后面写上你 想要的额外行数,例如:

diff -C5 file1 file2
diff -U3 file1 file2

最后一个输出选项会生成并排格式,在这种格式里,第一个 文件的每一行都显示在第二个文件对应行的旁边。要使用 这种格式,请用 -y:

diff -y smart-friends rich-friends

并排输出看起来像这样:

Alba Tross       Alba Tross
Dee Compose      Dee Compose
               > Mick Stup
Pat D. Bunnie    Pat D. Bunnie
Phil Harmonic  <

你可以看到这种输出的优点:差异一目了然。例如在我们的 例子里,很明显有三个名字是两个文件共有的(Alba、Dee 和 Pat),一个名字只在第二个文件里(Mick),一个名字只 在第一个文件里(Phil)。当然,缺点就是:对于长文件,你 会得到大量的输出。

如果你喜欢这种输出,有一个专门的程序 sdiff (side-by-side diff),你可以用它 代替 diff -y。例如,下面两条命令产生相同的 输出:

diff -y smart-friends rich-friends
sdiff smart-friends rich-friends

在做并排比较时,许多人更喜欢用 sdiff,因为它有 很多专门的选项,能为你提供极大的控制力。sdiff 的语法是:

sdiff [-bBilsW] [-w columns] file1 file2

其中 file1file2 是文本文件的名称, columns 是列的宽度。

使用 sdiff 很简单。例如,要比较我们例子中的那 两个文件,我们会用:

sdiff smart-friends rich-friends

输出是:

Alba Tross       Alba Tross
Dee Compose      Dee Compose
               > Mick Stup
Pat D. Bunnie    Pat D. Bunnie
Phil Harmonic  <

正如我提到的,sdiff 有很多选项。我们来看最重要 的几个,其中一些与 diff 的选项相同。想了解其余 选项,请查看你系统上的 man 手册页(man sdiff)。

首先,有几个选项可以让你减少不必要的输出。第一, -l(小写 "L")选项在两个文件有相同行时只显示 左列。例如,如果你使用:

sdiff -l smart-friends rich-friends

输出是:

Alba Tross     (
Dee Compose    (
               > Mick Stup
Pat D. Bunnie  (
Phil Harmonic  <

-s(same,相同)选项进一步减少了输出:它告诉 sdiff 不要显示两个文件中相同的任何行。例如:

sdiff -s smart-friends rich-friends

输出极少,也很容易理解:

               > Mick Stup
Phil Harmonic  <

当你处理行很短的文件时(就像我们的例子),你常常会发现 sdiff 默认的列宽太宽。这种情况下,你可以用 -w 选项改变列的宽度。只要在 -w 后面写上 你希望每列拥有的字符数即可。例如:

sdiff -w 30 smart-friends rich-friends

当然,你可以同时组合多个选项。我最喜欢的策略是先从 -s-w 30 这两个选项开始。例如:

sdiff -s -w 30 smart-friends rich-friends

看到输出之后,我再根据自己数据的情况调整列宽。

最后,还有四个选项与 diff 的类似。-i 选项 忽略大小写字母之间的差异;-W 忽略所有空白字符; -b 忽略空白字符数量上的差异;-B 忽略 空行。(请注意,sdiff 用的是 -W,而 diff 用的是 -w。这种差别源于历史原因, 一直没有改动。)

跳至页首

差异与补丁
(Diffs and Patches)

多年来,diff 一直是非常重要的工具,程序员用它 来跟踪自己程序的不同版本。例如,假设你是程序员,正在 开发一个名为 Foo 的 C 程序。当前版本是 2.0,保存在 文件 foo-2.0.c 中。此刻你正在做 2.1 版,保存在 foo-2.1.c 中。2.1 版一旦完成,你就可以运行下面 这条命令,把改动记录下来:

diff foo-2.0.c foo-2.1.c > foo-diff-2.1

输出文件(foo-diff-2.1)现在包含了一组说明: 照着做,就能把 foo-2.0.c 变成 foo-2.1.c

一般来说,能把一个文件变成另一个文件的一组说明就叫 作 DIFF(差异)。于是我们可以说,foo-diff-2.1 包含的是把 foo-2.0.c 变成 foo-2.1.c 的 diff。

程序员创建 diff 有两个原因:备份自己的作品时节省存储 空间,以及把改动分发给其他人。

假设 Foo 是一个非常大的程序。为每一个版本都做 备份是明智的,但那会占用大量存储空间。你可以只备份 基础版本的完整副本(foobar-2.0.c)。从那以后, 你只需备份那些很小的 diff:foo-diff-2.1foo-diff-2.2,依此类推。(到了 3.0 版,你可以再 保存一份完整副本。)

假设你正在做 2.7 版,这时一场灾难让你丢掉了原始文件。 要从备份中恢复它们,你先是复制基础文件 foo-2.0.c。然后用第一个 diff 重建 foo-2.1.c,用第二个 diff 重建 foo-2.2-c, 依此类推,直到 foo-2.7.c。换句话说,只要备份一份 基础副本加上一系列 diff,你就能重建程序的所有不同 版本。事实上,用这个技术,你可以备份任何以文本文件 保存的东西的不同版本:一篇小说、一篇文章、一份销售 演示文稿,等等。

当你以这种方式利用一个 diff — 从一个文件重建 另一个文件 — 我们就说你是在应用(APPLY)这个 diff。用来应用 diff 的程序叫作 patch(它的细节 超出了本书的范围)。在我们的例子里,你要恢复丢失的 文件,就要从备份中复制基础版本和所有 diff,然后用 patch 逐个应用这些 diff。

程序员使用 diff 的第二种方式,是把程序的改动分发给 别人。例如,假设很多人已经拥有了你的 Foo 程序 2.0 版 的源代码。每个人都花了些时间才下载并安装好这个程序, 但现在他们装好了。当你准备发布 2.1 版时,你该怎么做?

你可以让所有人下载整个新程序。然而,那会花很长时间。 其实你只要分发很小的 diff 就够了。要升级到 2.1 版, 你的用户只需用 patch 应用这个 diff。如果出于 某种原因他们觉得新版本有问题,他们可以用 patch 撤销(un-apply)这个 diff,退回到 2.0 版。

当程序员以这种方式利用 diff 时,它通常被称为 PATCH(补丁)。于是在我们的例子里,我们会说你分发 了 2.1 版的一个补丁,而你的用户用 patch 程序 应用了这个补丁。

以 diff 的形式分发改动,其优点是:通过打补丁来更新 软件,比下载并安装全新版本要快得多得多。确实,在 互联网早期,下载速度慢得可怜,要更新大型程序,唯一 可行的办法就是分发补丁,由用户自己来应用。

在 Unix 早期,程序员用 diffpatch 来维护、备份和分发他们的程序是很平常的事。不过很久 以来,已经有了好得多的系统来自动完成这类任务,直接 使用 diffpatch 的程序员相对少了。 取而代之,他们使用复杂的版本控制系统(VERSION CONTROL SYSTEM),有时也称为源代码控制系统(SOURCE CODE CONTROL SYSTEM,SCCS)或修订控制系统(REVISION CONTROL SYSTEM, RCS)。软件开发人员和工程师常用这类系统来管理大型 程序、文档、图纸等的开发。事实上,如果没有现代的版本 控制系统,大型团队就不可能在创造性项目上协同工作。

然而,无论复杂程度如何,所有版本控制系统都依赖于 创建 diff、分发 diff 和应用 diff 这些基本概念。这就是 为什么理解这些基本思想对你很重要。

名称的由来

Diff


diff 这个词来自 diff 程序,它是用来比较两个文件 的。在 Unix 人士中间,把 "diff" 同时用作名词和动词 是很常见的。

例如,你可能会听人说:"把你那个 Foo 程序的 diffs 发给我",意思是"把包含 Foo 程序更新内容的文件发给我"。

你还会听到人们把 diff 当动词用:"如果你想看看我 对你那篇新闻稿做了哪些改动,把两个文件 diff 一下就行。"

跳至页首

提取数据列: cut
(Extracting Columns of Data: cut)

相关过滤器: colrmjoin paste

cut 程序是一种过滤器,它提取指定的数据列, 而把其他一切都丢弃。(这与 colrm 正好相反, 它删除指定的数据列,保留其他内容。)

cut 程序非常灵活。你既可以提取每行中指定的 列,也可以提取每行中由分隔符隔开的部分(称为字段)。 如果你是数据库专家,可以把 cut 看作实现了 关系的投影操作。(如果你不是数据库专家,别担心; 你的人生依然完整。)

本节中,我重点讲如何用 cut 提取数据列。 下一节,我们再讲如何提取数据字段。

cut 的语法(提取列时)是:

cut -c list [file...]

其中 list 是要提取的列的清单,file 是 输入文件的名称。

你用这个清单告诉 cut 你想提取哪些数据列。 给出一个或多个列号,用逗号隔开。清单内部不要加空格。 例如,要只提取第 10 列,就用 10。要提取 第 1、8 和 10 列,就用 1,8,10

你还可以用一个连字符把范围的起止两端连起来,从而 指定一段连续的列号。例如,要提取第 10 到 15 列, 就用 10-15。要提取第 1、8 以及 10 到 15 列, 就用 1,8,10-15

下面是使用 cut 的一个例子。假设你有一个名为 info 的文件,里面记录了一群人的信息。每一行 包含与一个人有关的数据。特别地,第 14-30 列是姓名, 第 42-49 列是电话号码。以下是样例数据:

012-34-5678  Ambercrombie, Al  01/01/72  555-1111
123-45-6789  Barton, Barbara   02/02/73  555-2222
234-56-7890  Canby, Charles    03/03/74  555-3333
345-67-8901  Danfield, Deann   04/04/75  555-4444

要只显示姓名,使用:

cut -c 14-30 info

你会看到:

Ambercrombie, Al
Barton, Barbara
Canby, Charles
Danfield, Deann

要显示姓名和电话号码,使用:

cut -c 14-30,42-49 info

你会看到:

Ambercrombie, Al 555-1111
Barton, Barbara  555-2222
Canby, Charles   555-3333
Danfield, Deann  555-4444

如果你愿意,可以把 -c 后面的空格省略掉。事实上 大多数人就是这么做的。因此,下面这条命令与上一条是 等价的:

cut -c14-30,42-49 info

回到我们的例子,你可以通过把标准输出重定向到一个文件 来保存这些信息,例如:

cut -c 14-30,42-49 info > phonelist

cut 程序在管道中使用非常方便。这里举个例子。 你与他人共用一台多用户 Linux 计算机,你想列一份当前 登录到系统的 userid 清单。由于某些 userid 可能登录了 不止一次,你想显示每个 userid 登录的次数。

who 开始(第 8 章)。这条命令会生成一份 报告,每个登录的 userid 占一行。这里是一份典型的样例:

harley   console Jul  8 10:30
casey    ttyp1   Jul 12 17:46
weedly   ttyp4   Jul 12 21:22
harley   ttyp0   Jul 12 16:45
linda    ttyp3   Jul 12 17:41

你可以看到,userid 显示在第 1 到 8 列。因此,我们可以 用下面这条命令提取这些 userid:

who | cut -c 1-8

输出是:

harley
casey
weedly
harley
linda

现在,我们再多做一点。用 sort 给这份 userid 清单排序,再用 uniq -c 统计重复的次数。 (sortuniq 都在第 19 章讲解。) 把整件事拼在一起,我们就有:

who | cut -c 1-8 | sort | uniq -c

(注意,在管道中使用选项没有任何问题。)输出是:

1 casey
2 harley
1 linda
1 weedly

作为这条管道一个有意思的变体,我们不妨问问:怎样才能 显示恰好登录了两次的 userid 的名字?解决办法是在 uniq 的输出中查找所有包含 "2" 的行(*)。你可以 用 grep 来做到这一点(第 19 章):

who | cut -c 1-8 | sort | uniq -c | grep "2"

输出是:

2 harley

* 脚注

严格说来,这条 grep 命令会找出任何包含字符 "2" 的行。例如,如果有人登录了 12 次或 20 次,也会被找出来。 更好的解决办法是用 grep "\<2\>",它会用到 我们在第 20 章要讨论的技术。这条命令只会找出 "2" 单独出现的那些行。

— 提示 —

要给一张表的列重新排列,请先用 cut,再用 paste

跳至页首

记录、字段与分隔符;
提取数据字段: cut
(Records, Fields and Delimiters; Extracting Fields of Data: cut)

在上一节,我向你展示了如何用 cut 程序提取指定的 数据列。不过,cut 还有另一个用途:它可以提取 数据字段。要理解这是怎么运作的,我们需要先讨论几个 基本概念。

考虑两个不同的文件。第一个文件包含下面这些行:

Ambercrombie  Al       123
Barton        Barbara  234
Canby         Charles  345
Danfield      Deann    456

第二个文件包含:

Ambercrombie:Al:123
Barton:Barbara:234
Canby:Charles:345
Danfield:Deann:456

在两个文件里,每一行都包含一个姓、一个名和一个标识号, 实际上就是同样的信息。然而,这两个文件之间有个很大的 区别。

第一个文件很容易让人读,因为信息整整齐齐地排成了列。 第二个文件更适合程序来读,因为每行的三个部分 之间用 :(冒号)字符隔开。用我们在第 12 章 讨论过的术语来说,我们可以称第一个文件是人可读的 (HUMAN-READABLE),第二个文件是机器可读的 (MACHINE-READABLE)。

当你处理那些打算让程序来加工的数据时,你会经常遇到 类似我们第二个例子这样的机器可读文件。对于这类数据, 每一行被称为一条记录(RECORD);每行中各个分开的部分 称为字段(FIELDS);起分隔作用的字符是分隔符 (DELIMITERS)。在我们的例子里,有 4 条记录,每条记录 有 3 个字段(姓、名、标识号)。在每条记录中,分隔符 是冒号。

当然,分隔符并不总是冒号。原则上,任何不会出现在 实际数据中的字符都可以用作分隔符。最常见的分隔符是 逗号、空格、制表符和空白字符(即制表符与空格的组合)。

事实上,逗号作为分隔符用得如此频繁,以至于对于用逗号 分隔的数据有一个专门的名称。这样的数据被说成是以 CSV("comma-separated value",逗号分隔值)格式 存储的(*)。

* 脚注

直到几年前,CSV 格式仍是在程序之间交换的数据(尤其是 像 Microsoft Excel 这样的电子表格程序)最常用的存储 格式。如今,XML(Extensible Markup Language,可扩展 标记语言)使用得更广泛,因为它能处理许多种类型的数据。 CSV 格式虽然容易理解,但要受限得多,因为它只能用于 纯文本。

供你参考(万一你需要的话),下面是我给出的 CSV 格式 全面而技术性的定义:

"CSV 格式用来存储组织成记录的文本数据,每条记录都以 一个换行符结尾(在 Windows 上是回车-换行)。在每条 记录中,字段之间用逗号分隔。字段前后任何空白字符 (空格或制表符)都被忽略。字段可以用双引号括起来, 双引号本身会被忽略。如果一个字段包含逗号、双引号或 换行符,或者它以空格或制表符开头或结尾,那么这个 字段必须用双引号括起来。在字段内部,一个双引号 字符用连续两个双引号表示。"

使用分隔符的机器可读文件,也许最有意思的例子就是 Unix 的密码文件(PASSWORD FILE,/etc/passwd), 我们在第 11 章讨论过它。密码文件为系统上的每个 userid 保存一行。在每一行中,各个字段之间用 : 字符分隔。如果某个字段是空的,你会看到连着 的两个 : 字符。

要想看看你系统上的密码文件,使用下面命令之一(*):

cat /etc/passwd
less /etc/passwd

* 脚注

在旧版本的 Unix 中,口令(当然是加密过的)保存在 密码文件里,因此得名。在现代 Unix 中,真正的口令并 不保存在这个文件里。正如我们在第 11 章讨论的,出于 安全考虑,加密后的口令存放在另一个叫作影子文件的 不同文件中(/etc/shadow)。

现在基础已经打好,让我向你展示如何用 cut 程序从文件的各行中提取字段。语法是:

cut -c list [file...]
cut -f list [-d delimeter] [-s] [file...]

其中 list 是要提取的字段的清单,delimiter 是用来分隔字段的那个分隔符,file 是输入文件的 名称。

字段清单使用的格式与你使用 -c 选项时相同。 你给出一个或多个数字,用逗号隔开。清单内部不要加 任何空格。例如,要只提取第 10 个字段,就用 10。要提取第 1、8 和 10 个字段,就用 1,8,10

你也可以用一个连字符把范围的起止两端连起来,从而指定 一段连续的字段。例如,要提取第 10 到 15 个字段,就用 10-15。要提取第 1、8 以及 10 到 15 个字段, 就用 1,8,10-15

这里举个例子。在密码文件(/etc/passwd)中, 每行的第一个字段是 userid。假设你想看到系统上注册的 所有 userid 的清单。记住这个文件用 : 作为 分隔符,你要做的只是从密码文件的每一行中提取第一个 字段。命令是:

cut -f 1 -d ':' /etc/passwd

如果你想把这份清单排序,只要把输出用管道交给 sort(第 19 章):

cut -f 1 -d ':' /etc/passwd | sort

下面这个例子从同一个文件中提取第 1、3、4、5 个字段:

cut -f 1,3-5 -d ':' /etc/passwd | sort

你会注意到,我给分隔符(那个 :)加上了引号。 这是一个好习惯,它能确保 Shell 解析这条命令时不会 错误地理解这个分隔符。在这个例子里,不加引号也没关系, 但如果你的分隔符是空格、制表符或元字符,你就必须 给它加上引号。

如果 cut 遇到一行里根本没有分隔符,会发生什么? 默认情况下,这样的行会被原样传递过去,写到标准输出。 如果你想丢弃这样的行,可以使用 -s(suppress, 屏蔽)选项。

最后一点。与我们上一节讨论的 -c 选项一样, -f-d 后面的空格也可以省略。因此, 下面两条命令与我们最后两个例子是等价的:

cut -f1 -d':' /etc/passwd | sort
cut -f1,3-5 -d':' /etc/passwd | sort

大多数有经验的 Unix 用户都会省略这些空格。

— 提示 —

当你想从一个既有分隔符又有定宽列的文件中提取字段时, cut -dcut -c 都可以用。这种情况下, 你会发现基于分隔符(-d)的方式是更好的选择, 因为它更不容易出错。

跳至页首

合并数据列: paste
(Combining Columns of Data: paste)

相关过滤器: colrmcutjoin

paste 程序用来合并数据列。这个程序非常灵活。 你可以把若干个各自只有一列数据的文件合并成一张大表。 你也可以把连续的数据行合并起来,构建出多列。本节中, 我重点讲 paste 最有用的功能:合并彼此分开的 文件。如果你想了解更多 paste 能为你做什么, 请查看 man 手册页(man paste)。

paste 程序的语法是:

paste [-d char...] [file...]

其中 char 是用作分隔符的字符,file 是 输入文件的名称。

你用 paste 把各列数据合并成一张大表。如果需要, 你可以通过重定向标准输出把这张表保存到一个文件里。 这里举个例子。你有四个文件,分别名为 idnumbernamebirthdayphone。这些文件的 内容如下。

文件 idnumber:

012-34-5678
123-45-6789
234-56-7890
345-67-8901

文件 name:

Ambercromby, Al
Barton, Barbara
Canby, Charles
Danfield, Deann

文件 birthday:

01/01/85
02/02/86
03/03/87
04/04/88

最后,文件 phone:

555-1111
555-2222
555-3333
555-4444

你想建立一个名为 info 的大文件,把所有这些数据 合并成一张表。在这张表里,来自每个文件的数据应该各自 占一列。要使用的命令是:

paste idnumber name birthday phone > info

info 的内容是:

012-34-5678     Ambercromby, Al 01/01/85        555-1111
123-45-6789     Barton, Barbara 02/02/86        555-2222
234-56-7890     Canby, Charles  03/03/87        555-3333
345-67-8901     Danfield, Deann 04/04/88        555-4444

你会注意到输出的间隔有点奇怪。这是因为默认情况下, paste 在每个列项之间放一个制表符,而 Unix 假定 制表位每 8 个位置设一次,从第 1 个位置开始。换句话说, Unix 假定制表位在第 1、9、17、25 等位置。(我们将在 第 18 章讨论 Unix 如何使用制表符的细节。)

要告诉 paste 在列之间使用另一个(非制表符) 字符,请使用 -d(delimiter,分隔符)选项,后面 跟上单引号括起来的替代字符。例如,要建立同一张表、但 列之间用空格分隔,使用:

paste -d ' ' idnumber name birthday phone

现在你的输出看起来像这样:

012-34-5678 Ambercromby, Al 01/01/72 555-1111
123-45-6789 Barton, Barbara 02/02/73 555-2222
234-56-7890 Canby, Charles 03/03/74 555-3333
345-67-8901 Danfield, Deann 04/04/75 555-4444

如果你指定了多个分隔符,paste 会依次使用每一个, 必要时循环重复。例如,下面这条命令指定了两个不同的 分隔符,一个是 |(竖线),另一个是 % (百分号)。

paste -d '|%' idnumber name birthday phone

输出是:

012-34-5678|Ambercromby, Al%01/01/85|555-1111
123-45-6789|Barton, Barbara%02/02/86|555-2222
234-56-7890|Canby, Charles%03/03/87|555-3333
345-67-8901|Danfield, Deann%04/04/88|555-4444

— 提示 —

你可以把 paste 想象成与 cat 类似。区别 在于,paste 是横向合并数据,而 cat 是 纵向合并数据。

依次使用 cutpaste,你可以改变一张表 中各列的顺序。例如,假设你有一个名为 pizza 的 文件,里面是你要为一次聚会做的四种不同比萨的信息:

mushrooms regular sausage
olives    thin    pepperoni
onions    thick   meatball
tomato    pan     liver

你想交换第一列和第二列的顺序。首先,把每一列分别保存 到一个文件中:

cut -c 1-9 pizza > vegetables
cut -c 11-17 pizza > crust
cut -c 19-27 pizza > meat

现在把这三列合并成一张表,并且按你想要的顺序给出:

paste -d ' ' crust vegetables meat > pizza

由于这是个短文件,你可以用 cat 显示它 (参见第 16 章的讨论)。

cat pizza

现在数据看起来是这样:

regular mushrooms sausage
thin    olives    pepperoni
thick   onions    meatball
pan     tomato    liver

(当然,这是个很小的、刻意编出来的例子,但想想看,如果 你必须在一个有几百行甚至几千行的文件里交换列,这个 技术会有多重要。)

一旦你做出了想要的改动,还剩两件事要做。首先,用 rm 程序(第 25 章)删除那三个临时文件:

rm crust vegetables meat

其次,想想看你能邀请谁来参加聚会,而这位朋友愿意吃 一份猪肝番茄比萨。

跳至页首



练习
(Exercises)

复习问题 #1:

用来比较、排序和从文件中挑选数据的重要 Unix 程序有 十个。这十个程序分别是什么?

为什么会有这么多程序?

复习问题 #2:

默认情况下,comm 程序比较两个文件并产生三列 输出。请解释每一列的用途。如何屏蔽其中某一列?

复习问题 #3:

diffsdiff 程序都能比较文件。你什么 时候用 diff,什么时候用 sdiff?

复习问题 #4:

给你一个大型文本文件。你会用哪个程序来挑选: a) 重复的行,b) 唯一的行,c) 包含特定模式的行, d) 以特定模式开头的行,e) 数据列?

应用你的知识 #1:

你有兴趣把自己和两位朋友 Claude、Eustace 各自最爱的 食物比较一下。创建三个文件,分别命名为 meclaudeeustace。每个文件按排序顺序包含 五行,每行是一种食物的名称。只用两条 Unix 命令, 显示一份在这三份清单中都出现的食物清单。提示: 你可能需要建立一个临时文件。

应用你的知识 #2:

comm 程序用来比较已排序的文件;diff 程序比较未排序的文件。

举三个例子,说明哪些类型的数据你会用 comm 来比较。

举三个例子,说明哪些类型的数据你会用 diff

有没有什么情况下,这两个程序中的哪一个都能用?

应用你的知识 #3:

Unix 密码文件(/etc/passwd)的每一行都包含 一个 userid 的信息。在这一行中,各个数据字段之间用 :(冒号)字符分隔。其中有一个字段包含该 userid 的 Shell 名称。使用下面这条命令显示并研究你 系统上密码文件的格式:

less /etc/password

(在 less 中,你可以按 <Space> 向下翻一页, 按 q 退出。)

你会用哪条命令来读取密码文件,并显示你系统上所使用的 各种 Shell 的清单?

如何让输出排序,从而更易读?

如何去掉重复项?

应用你的知识 #4:

CSV 格式(逗号分隔值格式)描述的是一种包含机器可读 数据的文件,其中字段之间用逗号分隔。你有五个文件 — data1data2data3data4data5 — 每个文件只包含 一列数据。你要用哪条命令把这五列合并成一个名为 csvdata 的 CSV 格式文件?

如果其中一个文件的行数比其他文件少,会发生什么?

进一步思考 #1:

diff 程序的目的是通过给出让第一个文件变成第二个 文件的简洁说明,来突出显示差异。创建两个文件,命名为 ab。比较下面几条命令的输出:

diff a b diff b a

你注意到了哪些规律?

进一步思考 #2:

为什么 diff 命令的输出如此紧凑?

它是不是应该更容易理解一些?

进一步思考 #3:

给你一个有 10,000 行的文本文件。文件包含两列数据, 你必须交换这两列的顺序。用 cutpaste 命令,你可以又快又准确地做到这一点。假设你没有这些 命令。用任何其他你手上的工具,你可能会怎样 完成这项工作?

可以考虑用文本编辑器、文字处理程序、电子表格程序、 自己写一个程序,等等。你能想到有什么比 Unix 的 cutpaste 程序更简单的办法吗? 原因是什么?

本章的这些程序有哪些品质,使它们如此有用?

跳至页首