Diff、Patch 和它们的朋友们

作者:Michael K. Johnson

Diff 旨在逐行向您展示文件之间的差异。它从根本上来说使用简单,但需要稍加练习。不要被本文的长度吓到;您只需阅读前一两页就可以开始使用 diff。本文的其余部分是为那些不满足于非常基本用途的人准备的。

虽然开发者经常使用 diff 来显示源代码文件不同版本之间的差异,但它的用途远不止源代码。例如,当编辑在多人之间来回传递的文档时,diff 就派上用场了,可能通过电子邮件传递。在Linux Journal,我们对此深有体会。通常编辑和作者同时处理一篇文章,我们需要确保每个人所做的每个(正确的)更改都进入到正在编辑的文章的最终版本中。可以通过查看两个文件之间的差异来找到这些更改。

然而,很难展示 diff 在查找这些类型的差异方面有多么有用。为了用足够大的文件来演示 diff 的功能,我们需要将整个杂志都用来刊登这一篇文章。相反,因为我们的读者中很少有人能像精通英语的人那样精通拉丁语,至少相比之下是这样,我们将给出一个拉丁语的例子,来自 Alexander Leonard 翻译的 A. A. Milne 的 Winnie The PoohWinnie Ille Pu(ISBN 0-525-48335-7)。这将使普通读者更难一眼看出差异,并展示这些工具在查找更大文档中的更改时有多么有用。

现在快速找出这两段文字之间的差异

Ecce Eduardus Ursus scalis nunc tump-tump-tump
occipite gradus pulsante post Christophorum
Robinum descendens. Est quod sciat unus et solus
modus gradibus desendendi, non nunquam autem
sentit, etiam alterum modum exstare, dummodo
pulsationibus desinere et de no modo meditari
possit. Deinde censet alios modos non esse. En,
nunc ipse in imo est, vobis ostentari paratus.
Winnie ille Pu.
Ecce Eduardus Ursus scalis nunc tump-tump-tump
occipite gradus pulsante post Christophorum
Robinum descendens. Est quod sciat unus et solus
modus gradibus descendendi, nonnunquam autem
sentit, etiam alterum modum exstare, dummodo
pulsationibus desinere et de eo modo meditari
possit. Deinde censet alios modos non esse. En,
nunc ipse in imo est, vobis ostentari paratus.
Winnie ille Pu.

您可能在仔细比较后能够找到一两个更改,但您确定您找到了每一个更改吗?可能没有:对两个文件进行乏味的、逐字符的比较应该是计算机的工作,而不是您的工作。

使用 diff 程序来避免眼睛疲劳和精神错乱

diff -u 1 2
--- 1   Sat Apr 20 22:11:53 1996
+++ 2   Sat Apr 20 22:12:01 1996
 -1,9 +1,9
 Ecce Eduardus Ursus scalis nunc tump-tump-tump
 occipite gradus pulsante post Christophorum
 Robinum descendens. Est quod sciat unus et solus
-modus gradibus desendendi, non nunquam autem
+modus gradibus descendendi, nonnunquam autem
 sentit, etiam alterum modum exstare, dummodo
-pulsationibus desinere et de no modo meditari
+pulsationibus desinere et de eo modo meditari
 possit. Deinde censet alios modos non esse. En,
 nunc ipse in imo est, vobis ostentari paratus.
 Winnie ille Pu.

这里有几件事需要注意

  • 文件名和上次修改日期显示在顶部的“标题”中。如果您比较的是通过电子邮件来回传递的文件,日期可能没有任何意义,但在其他情况下它们会非常有用。

  • 文件名(在本例中为 1 和 2)前面分别带有 ---+++

  • 标题之后是一行包含数字的行。我们稍后将讨论该行。

  • 文件中没有更改的行前面带有空格显示;不同文件中不同的行前面带有字符,该字符显示它们来自哪个文件。仅存在于文件名在标题中以 --- 开头的文件中的行前面带有 - 字符,反之,文件名以 +++ 开头的文件中的行前面带有 + 字符。记住这一点的另一种方法是看到前面带有 - 字符的行是从第一个 (---) 文件中删除的,而前面带有 + 字符的行是添加到第二个 (+++) 文件中的。

  • 已进行了三处拼写更改:“desendendi”已更正为“descendendi”,“non nunquam”已更正为“nonnunquam”,并且“no”已更正为“eo”。

也许最主要需要注意的是,您不需要这份关于如何解释 diff 输出的描述就可以找到差异。比较两个相邻的行并看到差异是相当容易的。

并非总是如此容易

不幸的是,如果太多相邻的行被更改,解释就不是那么直观了;但是通过知道每个标记的行都以某种方式被更改了,您可以弄清楚。例如,在这个比较中,文件 3 包含损坏的内容,而文件 4(与前一个示例中的文件 2 相同)包含正确的内容,连续三行被更改,现在每一行有差异的行都没有直接显示在更正的行之上

diff -u 3 4
--- 3   Sun Apr 21 18:57:08 1996
+++ 4   Sun Apr 21 18:56:45 1996
 -1,9 +1,9
 Ecce Eduardus Ursus scalis nunc tump-tump-tump
 occipite gradus pulsante post Christophorum
 Robinum descendens. Est quod sciat unus et solus
-modus gradibus desendendi, non nunquam autem
-sentit, etiam alterum nodum exitare, dummodo
-pulsationibus desinere et de no modo meditari
+modus gradibus descendendi, nonnunquam autem
+sentit, etiam alterum modum exstare, dummodo
+pulsationibus desinere et de eo modo meditari
 possit. Deinde censet alios modos non esse. En,
 nunc ipse in imo est, vobis ostentari paratus.
 Winnie ille Pu.

需要做更多的工作才能找到添加的错误:“nodum”代替“modum”,“exitare”代替“exstare”。想象一下,如果连续 50 行的每一行都只有一个字符的更改。这开始类似于遍历整个文件,逐字符查找更改的旧工作。我们所做的只是(可能)缩小了您必须进行的比较量。

幸运的是,有几种工具可以更轻松地查找这些类型的差异。GNU Emacs 具有“word diff”功能。还有一个 GNU “wdiff”程序,可以帮助您在不使用 Emacs 的情况下找到这些类型的差异。

让我们首先看看 GNU Emacs。对于这个例子,文件 5 和 6 分别与之前的文件 3 和 4 完全相同。我在 X 下启动 emacs(它为我提供了彩色文本),然后输入

M-x ediff-files RET
5 RET
6 RET

在新弹出的窗口中,我按下空格键,这告诉 Emacs 高亮显示差异。查看图 1,看看找到每个更改的单词有多么容易。

图 1. ediff-files 5 6

GNU wdiff 也非常有用,尤其是在您没有运行 X 的情况下。只需要一个分页器(例如 less)——而且这仅在差异很大时才需要。使用命令 wdiff -t 5 6 比较完全相同的文件集(5 和 6),如图 2 所示。

图 2. wdiff -t 5 6

如果您得到的额外字符序列如 ESC[24 而不是下划线和反相视频,则可能是因为您正在使用 less,默认情况下它不会传递所有转义字符。请改用 less -r,或使用 more 分页器。两者都应该有效。

wdiff 使用 termcap 数据库(这就是 -t 选项的用途)来查找如何启用下划线和反相视频,但并非所有 termcap 条目都是正确的。在某些情况下,我发现 linux termcap 条目对于其他终端也适用,因为用于打开和关闭下划线和反相视频的代码在不同终端之间差异不大。要使用 linux termcap 条目,您可以这样做

TERM=linux wdiff -t 5 6 | less -r

这仅适用于 bourne shell 衍生产品,如 bash,不适用于 csh 或 tesh。但由于您只需要这样做来纠正损坏的 termcap 数据库,因此这种限制不应成为太大的问题。

wdiff 并非总是内置支持下划线和反相视频所需的 termcap 支持,即使您有一个工作的 termcap 数据库,这也不总是您想要的,因此有一种替代输出格式同样容易理解。我们将一石二鸟,既展示 wdiff 处理重新换行的段落的能力,又展示其在没有下划线和反相视频的情况下工作的能力。文件 8 与本文开头显示的正确文件 2 相同,但文件 7(损坏的文件)现在具有更短的行,这使得它们更难“用眼睛”比较

Ecce Eduardus Ursus scalis
nunc tump-tump-tump occipite
gradus pulsante post
Christophorum Robinum
descendens. Est quod sciat
unus et solus modus gradibus
desendendi, non nunquam autem
sentit, etiam alterum nodum
exitare, dummodo pulsationibus
desinere et de no modo
meditari possit. Deinde censet
alios modos non esse. En, nunc
ipse in imo est, vobis
ostentari paratus.
Winnie ille Pu.

wdiff 不会被不同换行的行搞糊涂。命令 wdiff 7 8 产生以下输出

Ecce Eduardus Ursus scalis nunc tump-tump-tump
occipite gradus pulsante post Christophorum
Robinum descendens. Est quod sciat unus et solus
modus gradibus
[-desendendi, non nunquam-]
{+descendendi, nonnunquam+} autem
sentit, etiam alterum [-nodum
exitare,-] {+modum exstare,+} dummodo
pulsationibus desinere et de [-no-] {+eo+}
modo meditari
possit. Deinde censet alios modos non esse. En,
nunc ipse in imo est, vobis ostentari paratus.
Winnie ille Pu.

还记得 +- 字符吗?它们在 wdiff 中的含义与在 diff 中的含义相同。(一致的用户界面非常棒。)

代码块

在本文的开头,我承诺解释这一行

 -1,9 +1,9

它描述了 diff 找到差异的代码块。在每个文件中,代码块从第 1 行开始,并延伸到第一行之后的 9 行。然而,对于这个小例子,示例中显示的代码块包含整个文件。对于较大的文件,仅显示更改周围的行,称为上下文

在文件 9 和 10 中,我在段落中间插入了许多空行,以便显示多个代码块的外观。文件 9 已损坏,文件 10 是正确的(除了段落中间的空行)

<h3>diff -u 9 10</h3>
--- 9   Mon Apr 22 15:46:37 1996
+++ 10  Mon Apr 22 15:46:14 1996
 -1,7 +1,7
 Ecce Eduardus Ursus scalis nunc tump-tump-tump
 occipite gradus pulsante post Christophorum
 Robinum descendens. Est quod sciat unus et solus
-modus gradibus desendendi, non nunquam autem
+modus gradibus descendendi, nonnunquam autem
 -33,7 +33,7
 sentit, etiam alterum modum exstare, dummodo
-pulsationibus desinere et de no modo meditari
+pulsationibus desinere et de eo modo meditari
 possit. Deinde censet alios modos non esse. En,
 nunc ipse in imo est, vobis ostentari paratus.
 Winnie ille Pu.

所以您看到这里显示了一个从第 1 行开始的七行代码块和一个从第 33 行开始的七行代码块。

您应该注意以下几点

  • 每个代码块的顶部都有一个标题。

  • 空行作为代码块上下文的一部分包含在内。

  • 未更改且不在已更改行三行范围内的行不包含在任何代码块中。

“补丁”(或“差异”)是 diff 程序的输出。它们包括两个文件之间所有更改的代码块。

其他格式

这只是 diff 的冰山一角。首先,未更改上下文的三行是可配置的。除了使用 -u 选项,您还可以使用 -U lines 选项来指定任意合理行数的上下文。如果您不想使用任何上下文,甚至可以指定 -U 0,但这很少有用。

-u(或 -U lines)参数是什么意思?它指定了统一差异格式,这是本文涵盖的特定格式。其他格式包括

  • “上下文差异”,它具有与统一差异相同的信息,但不太紧凑且可读性较差

  • “ed 脚本差异”或“普通差异”,其格式可以很容易地转换为一种形式,该形式可用于使(几乎过时的)编辑器 ed 自动更改旧文件的另一个副本以匹配新文件。此格式没有上下文,并且可以很容易地被 -U 0 替换,除了与旧软件和 POSIX 标准的兼容性之外。

您几乎永远不会想要创建上下文或普通差异,但有时识别它们可能很有用。上下文差异通过使用字符 ! 来标记更改来标记,普通差异通过使用字符 <> 来标记更改来标记。

以下是一些示例

diff -c 1 2
*** 1   Sat Apr 20 22:11:53 1996
--- 2   Sat Apr 20 22:12:01 1996
***************
*** 1,9 ****
  Ecce Eduardus Ursus scalis nunc tump-tump-tump
  occipite gradus pulsante post Christophorum
  Robinum descendens. Est quod sciat unus et solus
! modus gradibus desendendi, non nunquam autem
  sentit, etiam alterum modum exstare, dummodo
! pulsationibus desinere et de no modo meditari
  possit. Deinde censet alios modos non esse. En,
  nunc ipse in imo est, vobis ostentari paratus.
  Winnie ille Pu.
--- 1,9 ----
  Ecce Eduardus Ursus scalis nunc tump-tump-tump
  occipite gradus pulsante post Christophorum
  Robinum descendens. Est quod sciat unus et solus
! modus gradibus descendendi, nonnunquam autem
  sentit, etiam alterum modum exstare, dummodo
! pulsationibus desinere et de eo modo meditari
  possit. Deinde censet alios modos non esse. En,
  nunc ipse in imo est, vobis ostentari paratus.
  Winnie ille Pu.
diff 1 2
4c4
< modus gradibus desendendi, non nunquam autem
---
> modus gradibus descendendi, nonnunquam autem
6c6
< pulsationibus desinere et de no modo meditari
---
< pulsationibus desinere et de eo modo meditari

这里还有一些其他重要事项需要注意

  • 在上下文差异中,* 字符用于代替统一差异的 - 字符,- 字符用于代替 + 字符。上下文差异格式是在统一差异格式之前设计的,但统一差异格式的字符选择是助记符,因此更可取。

  • 上下文差异为每个代码块重复所有上下文两次。这在文件中浪费空间,但更重要的是,它将更改分隔得太远,使补丁的人类可读性降低。

  • 普通的旧式差异非常紧凑,占用空间非常小。它们在您通常不希望人类阅读它们的情况下很有用,在这些情况下,节省空间非常有意义,并且它们永远不会应用于已更改的文件。例如,RCS(在 1996 年 5 月的 LJ 期刊中介绍)使用几乎与旧式差异相同的格式来存储文件版本之间的更改。这节省了空间和时间,因为在任何情况下,任何上下文都将是空间的浪费。

使用补丁

当有人更改了其他人拥有副本的文件(源代码、文档或几乎任何其他文本文件)时,他们通常发送补丁,而不是(或除了)提供整个新文件。如果您有旧文件和补丁,您可能希望有一个程序来应用补丁。您可能会认为普通差异格式(旨在看起来像 ed 程序的输入)是完成此操作的最佳方法。

事实证明,情况并非如此。

已经编写了一个名为 patch 的程序,专门用于将补丁应用于文件(按照补丁中的指定更改文件)。它可以正确识别所有格式的补丁并应用它们。使用统一和上下文差异,即使从文件中添加或删除了行,patch 通常也可以通过查找未更改的上下文行来应用补丁。只有当上下文行本身已更改时,patch 才可能失败。

要使用 patch 应用补丁,您通常需要一个包含补丁的文件(我们将其称为 patchfile),然后运行 patch

patch < patchfile

Patch 非常冗长。如果它对任何事情感到困惑,它会停止并用英语询问您(它是由语言学家而不是计算机科学家编写的)您想做什么。如果您想了解更多关于 patch 的信息,man 手册异常易读。

其他相关工具

如果您阅读了 5 月刊中的 RCS 文章(Take Command: Keeping Track of Change,《LJ》#25,1996 年 5 月),您可能已经注意到该文章稍微谈到了一个名为 rcsdiff 的程序。rcsdiff 实际上只是 diff 的前端。也就是说,它查找它理解的参数(例如修订号和文件名),并准备两个文件来表示您正在检查的文件的两个版本。然后,它使用剩余的选项调用 diff。RCS 文章使用 -u 来获取统一格式,但没有解释其含义,但您可以使用 -c 来获取上下文差异,或使用 -U lines 来选择您在统一差异中获得的上下文量,或使用您喜欢的任何其他 diff 选项。

您可能会注意到 rcsdiff 产生的输出比普通 diff 更冗长。从 RCS 文章中

rcsdiff -u -r1.3 -r1.6 foo
==============================================
RCS file: foo,v
retrieving revision 1.3
retrieving revision 1.6
diff -u -r1.3 -r1.6
--- foo 1996/02/01 00:34:15     1.3
+++ foo 1996/02/01 01:05:28     1.6
 -1,2 +1,6
 This is a test of the emergency
-RCS system.  This is only a test.
+RCS version control system.
+This is only a test.
+
+I'm now adding a few lines for
+the next version.

它看起来就像一个普通的统一差异,除了前 5 行。

这不会阻止您向人们发送补丁。patch 程序在忽略无关信息方面非常出色。它甚至可以忽略新闻或邮件标头、在补丁文件外部编写的额外注释以及补丁后面的个人签名。当 patch 确定文本是否是补丁的一部分时,它会说“Hmm...”来告诉您。

如果您不关心两个文件如何不同,而只想知道它们是否不同,cmp 程序会告诉您。它不仅适用于文本文件,也适用于二进制文件。在此示例中,文件 5 和 6 不同;2 和 4 相同

cmp 5 6
5 6 differ: char 159, line 4
cmp 2 4

请注意,当两个文件相同时,cmp 什么也不说。它只在文件已更改时明确告诉您。为了在编写 shell 脚本时使用,cmp 在文件相同时也返回 true,在文件不同时返回 false,如以下 shell 会话所示

if cmp 5 6 ; then
  echo "same"
else
  echo "different"
fi
5 6 differ: char 159, line 4
different
if cmp 2 4 ; then
  echo "same"
else
  echo "different"
fi
same

还有几个其他具有相关功能的程序。特别是,diff3 可用于合并两个不同的文件,这两个文件都是从公共祖先文件编辑而来的。必须存在该公共祖先,diff3 才能正常工作。

diff 附带的信息页面可能已安装在您的系统上。如果您想了解更多关于 diff 的信息,请尝试命令 info diff 或从 emacs 或 jed 中使用 info 模式。

diff、wdiff、patch 和 emacs 可以通过 ftp 从规范的 GNU ftp 存档 prep.ai.mit.edu 的 /pub/gnu/ 目录中获取。

Michael K. Johnson 他的妻子 Kim 喜欢 A. A. Milne,并且曾短暂学习过拉丁语(与 Michael 不同,Michael 对拉丁语的经验仅限于在合唱团唱歌),这就是为什么她拥有 Winnie Ille Pu 以及 Tela Charlottae (Charlotte's Web) 的原因。

加载 Disqus 评论