一、概述
diff和patch是一对相辅相成的工具,在数学上来说,diff类似于对两个集合的差运算,patch类似于对两个集合的和运算。diff比较两个文件或文件集合的差异,并记录下来,生成一个diff文件,这也是我们常说的patch文件,即补丁文件。 patch能将diff文件运用于原来的两个集合之一,从而得到另一个集合。
举个例子来说文件A和文件B,经过diff之后生成了补丁文件C,那么这个过程相当于 |A –B| = C ,那么patch的过程就是B + C = A 或A - C = B(A + C = B或者B – C = A)。因此我们只要能得到A, B, C三个文件中的任何两个,就能用diff和patch这对工具生成另外一个文件。这就是diff和patch的妙处。
补丁Patch是天才程序员、Perl的发明者Larry Wall发明的,它应高效地交流程序源代码之需求而生,随着以Linux为代表的开发源代码运行的蓬勃发展,patch这个概念已经成为开放源代码发起者、贡献者和参与者的集体无意识的一部分。patch只包含了对源代码修改的部分,这对于开放源代码社区的协同开发模式具有重要意义,意味的软件新版本的发布和对软件的缺陷或改进可以以更小的文件发布,可以减少网络的传输量,方便软件维护者的管理工作。
patch文件有多种格式,不同平台上所支持的格式不尽相同,但最常见的是context格式和unified格式。context格式被广泛使用,是 patch文件格式事实上的标准。该格式包含了差异部分及其邻近的若干行,邻近就是所谓的上下文,这些行虽然没有变化,但它们出现在patch文件使得还原patch的程序具备更强的容错性。unified格式常见于GNU的patch实现,以patch形式发布的linux内核就使用了该格式。
此外,还有其它比较少用的格式,如Normal格式,并排对比模式(side-by-side),ed script和RCS script模式等。除了并排对比模式方便用户观察文件差异,其它格式大多数是为了兼容旧的patch格式。
二、工具的用法
1、 diff的用法
diff后面可以接两个文件名或两个目录名,例如:
diff [option] oldfile newfile
如果是一个目录名加一个文件名,那么只作用在那么个目录下的同名文件。例如:
diff /usr/xu mine
把目录/usr/xu 中名为mine的文件与当前目录中的mine文件进行比较。
Diff常用的option选项有:
l -r 比较目录时,进行递归比较,用于产生整个代码树的patch
l -u 输出统一格式,diff有"传统"和"统一"两种格式,现在一般使用"统一"格式,比较而言,统一格式生成的文件大,但包含了更多的信息,有利于阅读与定位
l -N 表示如果文件不存在则将其等价为空文件,这个用于产生有文件增加或删除的patch
l -a 补丁中包含二进制文件缺省时,diff向标准输出打印,所以一般都重定向到文件并以patch为后缀,也就是所谓的补丁文件。关于二进制文件的说明:binary文件可以原始方式存入patch文件。diff可以生成(加-a选项),patch也可以识别。如果觉得这样的patch文件太难看,解决方法之一是用uuencode处理该binary文件。
如果是两个目录的话,作用于该目录下的所有文件,不递归。如果我们希望递归执行,需要使用-r参数。不加任何参数生成的diff文件格式是一种简单的格式,这种格式只标出了不一样的行数和内容。我们需要一种更详细的格式,可以标识出不同之处的上下文环境,这样更有利于提高patch命令的识别能力。这个时候可以用-c开关。可以参考表1 diff的命令行选项和参数。
表1 diff的命令行选项和参数
选项 描述
-a 将所有的文件看作文本,既使文件看起来像是二进制的也不例外,并且进行逐行比较
-b 忽略块中空白数目的改变
-B 忽略插入或删除空行造成的改变
-c 产生"上下文"(context)格式的输出
-C[num] 产生"上下文"(context)格式的输出,显示块前后num行的内容,如果不指定num的值,则显示块前后3行的内容
-H 修改diff处理大文件的方式
-i 忽略大小写
-I regexp 忽略插入或删除与正则表达式regexp匹配的行
-l 将输出结果通过pr命令处理加上页码
-p 显示出现块的C函数
-q 只报告文件是否不同;不输出差别
-r 比较目录时,进行递归比较,用于产生整个代码树的patch
-s 报告两个文件相同(默认的行为是不报告相同的文件)
-t 输出时tab扩展为空白
-u 产生"统一"(unified)格式的输出
-U[num] 产生"统一"(unified)格式的输出,显示块前后num行的内容,如果不指定num的值,则显示块前后3行的内容
-v 打印diff的版本号
-w 逐行比较时忽略空白
-W cols 如果产生并排格式的输出(参见-y),让输出的每一列有cols个字符宽
-x pattern 当比较目录时,忽略匹配模式pattern的任何文件和子目录
-y 产生并排格式的输出
举例:
void main() { printf("hello the world!\n"); }
void main() { printf("HELLO THE WORLD!\n"); }
使用以下命令生成补丁文件hello.patch:
$diff -u hello.c hello-new.c > hello.patch
diff可以对整个目录进行比较,生成补丁文件 例如有hello-1.0 和hello-1.1两个目录,其中hello-1.1为hello-1.0的更新命令:
$diff -ruNa hello-1.0 hello-1.1 > hello-1.1.patch
2、 Diff和patch结合使用
Patch用途:根据原文件和补丁文件生成目标文件。举例来说:
patch A C 就能得到B, 这一步叫做对A打上了B的补丁,这个补丁的名字叫做C。经过这一步,你的文件A就变成了文件B。如果你打完补丁之后想恢复到A怎么办呢?
patch -R B C 就可以重新还原到A了。所以不用担心会失去A的问题。
其实patch在具体使用的时候是不用指定原文件的,因为补丁文件中都已经记载了原文件的路径和名称。patch足够聪明可以认出原文件的路径和名称来。但是有时候会有点小问题:
比如一般对两个目录diff的时候可能已经包含了原目录的名字,但是我们打补丁的时候会进入到目录中再使用patch,这个时候就需要你告诉 patch命令怎么处理补丁文件中的路径。可以利用-pn开关,告诉patch命令忽略的路径分隔符的个数。举例如下:
A文件在 DIR_A下,修改后的B文件在DIR_B下,一般DIR_A和DIR_B在同一级目录DIR_P。我们为了对整个目录下的所有文件一次性diff,我们一般会到DIR_A和DIR_B的父目录DIR_P下执行以下命令:
diff -rc DIR_A DIR_B > C
这个时候补丁文件C中会记录了原始文件的路径为 DIR_A/A
现在另一个用户得到了A文件和C文件,其中A文件所在的目录也是DIR_A。一般,他会比较喜欢在DIR_A目录下面进行patch操作,它会执行
patch < C
但是这个时候patch分析C文件中的记录,认为原始文件是./DIR_A/A,但实际上是./A,此时patch会找不到原始文件。为了避免这种情况我们可以使用-p1参数如下
patch -p1 < C
此时,patch会忽略掉第1个”/”之前的内容,认为原始文件是 ./A,这样就正确了。
patch附带有一个很好的帮助,其中罗列了很多选项,但是99%的时间只要两个选项就能满足我们的需要:
patch -p1 < [patchfile]
patch -R < [patchfile] (used to undo a patch)
-p1选项代表patchfile中文件名左边目录的层数,顶层目录在不同的机器上有所不同。要使用这个选项,就要把你的patch放在要被打补丁的目录下,然后在这个目录中运行path -p1 < [patchfile]。
当然,被比较的文件或目录也可以从标准输入获得。如果用”-”表示file1或file2,则表示标准输入。
实例:
cat build.xml | diff -y -W 100 - build-1.10.xml
以每列100个字符的宽度并排对比输出build.xml和build-1.10.xml之间的差异到屏幕(标准输出)
diff -c web.xml web2.xml > web.xml.diff
产生web2.xml相对于web.xml的修改的上下文格式补丁文件到web.xml.diff中去
diff -crN src src_XFIRE > xfire-patch.diff
产生代码树src_XIRE相对于代码树src的上下文补丁文件到xfire-patch.diff中去,在src_XFIRE中新增的文件的内容也会被包含在补丁中。
打补丁可以使用命令行工具patch。它的基本用法是:
patch -pnum < 补丁文件
打补丁时将工作目录切换到需要打补丁源代码顶层目录(注意:运行patch所在的目录应该与用diff生成补丁的时候一致)。
3、 如何然后确定p后面的数字?
该数字表示需要去掉的补丁文件中目录的层数,该数字和补丁创建时候工作目录和代码目录的相对位置有关,一般补丁的作者会在补丁文档中指明。如果没有指定,可以通过观察补丁文件中列出的文件完整路径和代码树中该文件所在相对路径得出。
4、 如果patch失败
如果patch成功,缺省是不建备份文件的(注:FreeBSD下的patch工具缺省是保存备份),如果你需要,可以加上 b 开关。这样把修改前的文件以“原文件名.orig”的名字做备份。如果你喜欢其它后缀名,也可以用“b 后缀”来指定。
如果patch失败,patch会把成功的patch行给patch上,同时(无条件)生成备份文件和一个.rej文件,并生成原文件的备份“原文件名.orig”,这里“-b”选项可以指定原文件的备份“原文件名.orig”的后缀名。.rej文件里是没有成功提交的patch行,需要手工patch上去。这种情况在原码升级的时候有可能会发生。
5、 对整个软件包做修改
通常,我们需要对整个软件包做修改,并生成一个patch文件,下面是典型的操作过程。这里就要用到前面介绍的几个命令行开关了:
tar xzvf software.tar.gz # 展开原始软件包,其目录为software
cp -a software software-orig # 做个修改前的备份
cd software
[修改,测试……]
cd ..
diff -ruNa software-orig software > software-my.patch
现在我们就可以保存software-my.patch做为这次修改的结果,至于原始软件包,可以不必保存。等到下次需要再修改的时候,可以用 patch命令把这个补丁打进原始包,再继续工作。比如是在linux kernel 上做的工作,就不必每次保存几十兆修改后的源码了。
这是好处之一,好处之二是维护方便,由于unified patch格式有一定的模糊匹配能力,能减少原软件包升级带来的维护工作量。