linux diff patch原理及用法学习

当前使用suse内核的源码编译ipvs内核模块。在使用的过程中,发现了源码的一个bug,所以对源码进行了些微的修改。为了保存修改后的源码,学习了一下patch的工作原理。
先利用diff比较原来的文件和修改之后的文件之间的差异,将差异以特定的格式保存成一个patch文件,在需要使用的时候将源码和patch结合,生成修改后的文件。
path我理解经常用的一个场景就是版本的升级。如果一个软件包的v1版本的源码100M。过了一段时间,做了一些改动,添加一些功能之后,升级到v2,假如源码包还是100M。如果用户已经下载过v1,再下载完整的v2是就显得有点冗余。如果这时候基于v1与v2的差异生成一个patch,可能只有几M,甚至几百KB。用户只需要下载这个patch,基于v1,即可生成v2的源码。

diff 格式介绍

diff是linux提供的一个工具,用来以行为单位比较不同文件之间的差异。
差异分格三种格式:

  • 常规格式(normal diff)
  • 上下文格式(context diff)
  • 合并格式(unified diff)

常规格式

最简单的是常规格式,这篇文章做了非常清楚的介绍:
毛帅的博客—超详细举例看懂Unix的diff格式(1/3):diff的常规模式
为了防止文章链接失效,我直接将文章内容拷贝过来了:

在使用git的过程中,难免会用到git diff命令,用于比较文件差异。但初学者对这个命令的输出格式几乎都是一脸懵逼,需仔细研究一番。

我读过阮一峰的《读懂diff》,收获颇大,但还是写了本文。一来,阮一峰文章中的举例过于简单和特殊,有些问题没有解释清楚;二来,也是自己的一份总结。

背景

git的diff,源于Unix的diff命;因此,追本溯源我们要从Unix的diff命令说起。

Unix的diff命令由于历史原因,又分为三种输出格式:

  • 常规格式(normal diff)
  • 上下文格式(context diff)
  • 合并格式(unified diff)

本文是系列的第一篇,介绍diff常规输出格式

diff命令的格式

diff用于比较两个文件的差异,如果f1文件看做原文件,f2文件看做改动后的文件,那么直接执行diff f1 f2,就完成了常规模式下对f1和f2的比较。

在开始之前,我们先要意识到:diff对文本,是按行进行比较的工具,所以你看到的输出,永远是针对行的描述。

准备初始文件f0

为了研究diff命令的使用方法,我们准备了丰富的案例讲解。每个案例对应与一个改动文件,都与原始文件f0进行diff,便于大家学习。

首先,我们的初始文件f0的内容如下,一共14行:

11
22
33
44
55
66
77
88
99
00
aa
bb
cc
dd

为了便于识别,初始文件每行统一为2个字符;同时,任何改动的行,都会超过两个字符;因此,肉眼会很容易的辨别出改动点。

下面,就开始我们的案例之旅吧!

案例0:文件相同

我们让f0和f0自己比较,显然不会有任何差异行。

  • 命令:diff f0 f0
  • 结果:不出所料,diff秉承了Unix的设计哲学:没消息就是好消息。因此没有给出任何输出。

案例c1:修改一行内容

将第5行修改为hello,形成文件c1:

11
22
33
44
hello
66
77
88
99
00
aa
bb
cc
dd

执行命令:diff f0 c1,输出如下:

5c5
< 55
---
> hello

上述输出内容分内4行:

  1. 第1行是一个变动提示5c5共3个字符,第一个5表示变动的行号;第二个c表示变动方式是修改(change),其他的变动方式还有增加a(add)、删除d(delete);第三个5表示变动后在新文件的行号,由于是修改因此,修改前后行号一样。
  2. 第2行代表“删除操作”,<代表删除,后面紧跟着删除的内容55;也就是说删除了55。
  3. 第3行是三个横线,用于分隔删除和增加的内容。
  4. 第4行是增加的内容,用>表示,>后就是具体增加的内容。

初步总结一下diff输出:

  1. diff先用一个字符串表示变动提示,包含:操作的行在旧文件的行号,操作的方式,以新文件的行号。
  2. 操作被分解为“删除”和“新增”两步,分别用<>表示,其后跟上的是具体内容。
  3. 删除和新增两步之间,用---分隔。

案例c2:修改两行内容(相邻)

案例c1只修改了一行,如果我们再多修改一行,会怎样?我们继续把第6行修改为world,形成文件c2:

11
22
33
44
hello
world
77
88
99
00
aa
bb
cc
dd

执行diff f0 c2

5,6c5,6
< 55
< 66
---
> hello
> world

相比案例c1的输出,稍显复杂了:

  1. 变动提示里,c的两边的行号变成了5,6,代表是5到6行。
  2. ---的上下两部分也变成了两行内容。这容易理解,因为删除了两行内容,并新增了两行内容。

总结一下,在连续行修改的情况下:

  1. 第一行操作提示,使用起始行号进行表示范围。
  2. ---上下,用多个<>,标记连续删除和新增的内容。

案例c3:修改两行内容(不相邻)

若干修改的行不相邻,diff会怎么表示?我们把f0的第5行改为hello,第10行为world,修改后形成文件c3:

11
22
33
44
hello
66
77
88
99
world
aa
bb
cc
dd

执行diff f0 c3

5c5
< 55
---
> hello
10c10
< 00
---
> world

结果好像更复杂了,但在案例c1和案例c2的基础,仔细看:拆分看来,其实就是两个案例c1的操作而已,分别以5c510c10开头。

总结:

  1. 当修改内容不连续的时候,diff将修改拆分为多个片段表示,每个片段都是一个完整的连续修改片段
  2. 片段的开始符号是“修改提示”,即“原文件行号(或范围)” + 变动方式 + “新文件行号(或范围)”

案例c4:综合情况

将案例c1-c3的情况综合考虑,形成如下的文件c4:

11
22
33
hello
world
!!!!
77
88
99
00
how are you
bb
cc
fine

文件修改了3处(连续的算作一处):4-6行被修改了;11行变成了how are you;14行变成了fine;初步预测一下,diff应该会给出3个修改片段,其中第1个片段是一个连续的范围。

执行命令diff f0 c4,结果不出所料:

4,6c4,6
< 44
< 55
< 66
---
> hello
> world
> !!!!
11c11
< aa
---
> how are you
14c14
< dd
---
> fine

以上都是原行修改的案例,下面我们看一下增加行的案例。

案例a1:增加一行内容

与修改同理,我们从增加一行开始(在第5行下增加一行hello,变成了新文件的第6行),形成文件a1:

11
22
33
44
55
hello
66
77
88
99
00
aa
bb
cc
dd

执行diff f0 a1

5a6
> hello

嚯,内容要比修改的情况简洁多了,但注意两点:

  1. “变动提示”里,除操作方式用a表示增加(add)外;特别注意第3个6表示新文件的行号。由于是新增行,那么在新行自然是偏移到第6行了
  2. 原来出现的<---都不见了,只剩下表示新增内容的>。由于只存在增加内容,无需分隔,---被去除了,可以理解。

总结:

  1. 变动提示5a6,表示在第5行后面新增,并形成新文件的第6行。
  2. 新增操作不用分解,只保留了>行的内容

案例a2:增加两行内容(连续):

文件在第5行后,增加了两行内容,如下:

11
22
33
44
55
hello
world
66
77
88
99
00
aa
bb
cc
dd

执行diff f0 a2,参考案例c2的经验,不出所料,结果如下,就不多解释了:

5a6,7
> hello
> world

案例a3:增加两行内容(不相邻)

新增后的文件a3如下:

11
22
33
44
55
hello
66
77
88
99
00
world
aa
bb
cc
dd

执行diff f0 a3,结果如下:

5a6
> hello
10a12
> world

不连续的时候,diff给出了两个新增片段,符合预期。

但注意第二个片段a后是12,而不是11,因为第一个新增也影响了新文件,所以到第二个新增操作的时候,其实已经偏移了2行。也就是说,变动提示中,新文件的行号受前面所有修改的影响的。

案例a4:综合情况

多修改几处,形成a4:

nihao
11
22
33
44
55
hello
world
66
77
88
99
00
how are you
aa
i am fine
bb
cc
dd

可以看出,首先在开头插入一行nihao,然后再55后连续增加了两行,接着分别不连续的新增了1行,执行diff f0 a4,分为4个片段,结果如下:

0a1
> nihao
5a7,8
> hello
> world
10a14
> how are you
11a16
> i am fine

值得关注的是,在文件头增加的时候,操作提示中用0表示原文件的位置。

案例d1:删除一行

我们把f0文件的第5行删除,形成文件d1:

11
22
33
44
66
77
88
99
00
aa
bb
cc
dd

执行diff f0 d1,结果如下:

5d4
< 55
  1. 和新增类似,这里只有<行,没有--->行。
  2. 变动提示中,新文件的4表示删除内容近邻的上一行在新文件的位置

案例d2:删除两行(连续)

f0删除5,6两行后,形成文件d2:

11
22
33
44
77
88
99
00
aa
bb
cc
dd

执行diff f0 d2,结果如下:

5,6d4
< 55
< 66

同理,不难理解。

案例d3:删除两行(不连续)

f0删除5和10两行后,形成d3:

11
22
33
44
66
77
88
99
aa
bb
cc
dd

执行diff f0 d2,结果如下:

5d4
< 55
10d8
< 00

形成了两个删除片段。

案例d4:综合

删除第1行,删除第5,6行,删除第12行,形成文件d4:

22
33
44
77
88
99
00
aa
cc
dd

执行diff f0 d4

1d0
< 11
5,6d3
< 55
< 66
12d8
< bb

有三个删除片段,结合前边的例子,不难理解。

综合案例acd:增删改

有了上面的基础,我们模拟一下复杂的情况,包含了增删改的混合。与之前不同,我先给出diff的操作结果,看看你能不能反向推测出修改后的文件acd呢?

执行diff f0 acd结果如下:

0a1,2
> today is sunday
> i went to china
2d3
< 22
3a5
> yes it is
6,8c8
< 66
< 77
< 88
---
> hello
11c11,13
< aa
---
> first
> secend
> third

看起来是有点晕,最好准备一个编辑器,自己模拟一下。先将f0的内容拷贝到编辑器内,跟着操作:

  • 0a1,2,说明在文件头增加两行,修改后如下(为了方便,我在每行前显示了行号):
  1 today is sunday
  2 i went to china
  3 11
  4 22
  5 33
  6 44
  7 55
  8 66
  9 77
 10 88
 11 99
 12 00
 13 aa
 14 bb
 15 cc
 16 dd
  • 2d3,即将第2行删掉,注意是原文件的第2行(根据<后看,也可以确认就是内容22的行):
  1 today is sunday
  2 i went to china
  3 11
  4 33
  5 44
  6 55
  7 66
  8 77
  9 88
 10 99
 11 00
 12 aa
 13 bb
 14 cc
 15 dd
  • 3a5,第3行增加一行,形成新文件的第5行,内容是yes it is:
  1 today is sunday
  2 i went to china
  3 11
  4 33
  5 yes it is
  6 44
  7 55
  8 66
  9 77
 10 88
 11 99
 12 00
 13 aa
 14 bb
 15 cc
 16 dd
  • 6,8c8,这个和之前的有些区别,修改操作,但修改了原文件三行,新文件只有一行。不过只要理解修改就是删除+新增,变不难了。其实,就是将原文件的第6-8行删除,在替换为hello即可,于是形成:
  1 today is sunday
  2 i went to china
  3 11
  4 33
  5 yes it is
  6 44
  7 55
  8 hello
  9 99
 10 00
 11 aa
 12 bb
 13 cc
 14 dd
  • 最后一个操作:11c11,13,和上一个操作类似,修改后的行比修改前多。分析一下不过是删除了原文件11行,并在原位置插入了3行内容。最终修改成的文件就是:
  1 today is sunday
  2 i went to china
  3 11
  4 33
  5 yes it is
  6 44
  7 55
  8 hello
  9 99
 10 00
 11 first
 12 secend
 13 third
 14 bb
 15 cc
 16 dd

到此,diff的常规模式(normal)的输出情况,通过几个栗子几乎覆盖了。尤其是最后一个例子,反推回去如果能搞懂,就没什么问题了。

总结

最后,对diff命令的常规模式总结如下:

  1. diff可分辨变动的粒度是一行
  1. 任何变动,在diff看来,都可以分解为删除行和增加行。
  2. 对与连续的变动,diff会用一个变动片段表示。其中变动片段分为4个部分:
    a. 第一部分,即第一行,是变动提示,用“原文件行号(或范围)” + 变动模式 + “新文件行号(或范围)”表示,比如3c3。其中变动模式,分为a(增加)、c(修改)和d(删除)。
    b. 第二部分和第四部分用---(即第三部分)分隔。
    c. 第二部分表示删除的内容,每一行用<开头,紧随的是具体删除的内容。
    d. 第四部分表示增加的内容,每一行用>开头,紧随的是具体增加的内容。
    e. 当变动模式是a和d的时候,第二和第部分,只存在一个,同时---也会被省略。
  3. 如果一个变动不连续,则会被拆解为多个变动片段表示。每个片段,都遵循第3条同样的规则。

常规格式保存的信息就是文件行号和变化的内容。如果对一个错误的源文件应用patch,会发生什么呢?
首先将diff输出的内容保存到patch文件中,然后修改原文件的内容。然后再对原文件应用patch,会报错:

root@debian:/opt/diffTest# patch v1.file -i patchFromV1ToV2.patch -o v2FromPatch.file.test
patching file v2FromPatch.file.test (read from v1.file)
Hunk #2 FAILED at 4.
1 out of 5 hunks FAILED -- saving rejects to file v2FromPatch.file.test.rej

常规格式未保存文件名信息(也就无法同时应用到多个文件?),应用patch会直接在原文件上进行修改

root@debian:/opt/diffTest/patchTest# cat v1.file
11
22
33
44
55
66
77
88
99
00
aa
bb
cc
dd
root@debian:/opt/diffTest/patchTest# cat patchFromV1ToV2.patch
0a1,2
> today is sunday
> i went to china
2d3
< 22
3a5
> yes it is
6,8c8
< 66
< 77
< 88
---
> hello
11c11,13
< aa
---
> first
> second
> third
root@debian:/opt/diffTest/patchTest#
root@debian:/opt/diffTest/patchTest#
root@debian:/opt/diffTest/patchTest# patch v1.file -i patchFromV1ToV2.patch
patching file v1.file
root@debian:/opt/diffTest/patchTest#
root@debian:/opt/diffTest/patchTest# cat v1.file
today is sunday
i went to china
11
33
yes it is
44
55
hello
99
00
first
second
third
bb
cc
dd
root@debian:/opt/diffTest/patchTest# ll
total 8
-rw-r--r-- 1 root root 143 Nov 29 01:32 patchFromV1ToV2.patch
-rw-r--r-- 1 root root  94 Nov 29 01:33 v1.file
root@debian:/opt/diffTest/patchTest#
root@debian:/opt/diffTest/patchTest# cat v1.file
today is sunday
i went to china
11
33
yes it is
44
55
hello
99
00
first
second
third
bb
cc
dd
root@debian:/opt/diffTest/patchTest#

常规格式无法查看上下文,不便于通过patch来理解具体的修改。所以有了上下文格式。

上下文格式

以下版本复制自:
http://www.ruanyifeng.com/blog/2012/08/how_to_read_diff.html
上个世纪80年代初,加州大学伯克利分校推出BSD版本的Unix时,觉得diff的显示结果太简单,最好加入上下文,便于了解发生的变动。因此,推出了上下文格式的diff。

它的使用方法是加入c参数(代表context)。

$ diff -c f1 f2

显示结果如下:


  *** f1	2012-08-29 16:45:41.000000000 +0800
  --- f2	2012-08-29 16:45:51.000000000 +0800
***************
  *** 1,7 ****
   a
   a
   a
  !a
   a
   a
   a
  --- 1,7 ----
   a
   a
   a
  !b
   a
   a
   a

这个结果分成四个部分。

第一部分的两行,显示两个文件的基本情况:文件名和时间信息。


  *** f1	2012-08-29 16:45:41.000000000 +0800
  --- f2	2012-08-29 16:45:51.000000000 +0800

"***“表示变动前的文件,”—"表示变动后的文件。

第二部分是15个星号,将文件的基本情况与变动内容分割开。

第三部分显示变动前的文件,即f1。

  *** 1,7 ****
   a
   a
   a
  !a
   a
   a
   a

这时不仅显示发生变化的第4行,还显示第4行的前面三行和后面三行,因此一共显示7行。所以,前面的"*** 1,7 ****"就表示,从第1行开始连续7行。

另外,文件内容的每一行最前面,还有一个标记位。如果为空,表示该行无变化;如果是感叹号(!),表示该行有改动;如果是减号(-),表示该行被删除;如果是加号(+),表示该行为新增。

第四部分显示变动后的文件,即f2。
  — 1,7 ----
   a
   a
   a
  !b
   a
   a
   a
除了变动行(第4行)以外,也是上下文各显示三行,总共显示7行。

合并格式的diff

如果两个文件相似度很高,那么上下文格式的diff,将显示大量重复的内容,很浪费空间。1990年,GNU diff率先推出了"合并格式"的diff,将f1和f2的上下文合并在一起显示。

它的使用方法是加入u参数(代表unified)。
  $ diff -u f1 f2
显示结果如下:

  --- f1	2012-08-29 16:45:41.000000000 +0800
  +++ f2	2012-08-29 16:45:51.000000000 +0800
  @@ -1,7 +1,7 @@
   a
   a
   a
  -a
  +b
   a
   a
   a

它的第一部分,也是文件的基本信息。
  — f1 2012-08-29 16:45:41.000000000 +0800
  +++ f2 2012-08-29 16:45:51.000000000 +0800

"—“表示变动前的文件,”+++"表示变动后的文件。

第二部分,变动的位置用两个@作为起首和结束。

@@ -1,7 +1,7 @@

前面的"-1,7"分成三个部分:减号表示第一个文件(即f1),"1"表示第1行,“7"表示连续7行。合在一起,就表示下面是第一个文件从第1行开始的连续7行。同样的,”+1,7"表示变动后,成为第二个文件从第1行开始的连续7行。

第三部分是变动的具体内容。

a
   a
   a
  -a
  +b
   a
   a
   a

除了有变动的那些行以外,也是上下文各显示3行。它将两个文件的上下文,合并显示在一起,所以叫做"合并格式"。每一行最前面的标志位,空表示无变动,减号表示第一个文件删除的行,加号表示第二个文件新增的行。

diff patch应用于文件夹

摘自:
https://stackoverflow.com/questions/9980186/how-to-create-a-patch-for-a-whole-directory-to-update-it

Run an appropriate diff on the two directories, old and new:

diff -ruN orig/ new/ > file.patch
# -r == recursive, so do subdirectories
# -u == unified style, if your system lacks it or if recipient
#       may not have it, use "-c"
# -N == treat absent files as empty

If a person has the orig/ directory, they can recreate the new one by running patch.
To Recreate the new folder from old folder and patch file:
Move the patch file to a directory where the orig/ folder exists
This folder will get clobbered, so keep a backup of it somewhere, or use a copy.

patch -s -p0 < file.patch
(或者patch -s -p0 -i file.patch)
# -s == silent except errors
# -p0 == needed to find the proper folder

At this point, the orig/ folder contains the new/ content, but still has its old name, so:

mv orig/ new/    # if the folder names are different

关于-p0选项的含义,man手册还是解释的比较清楚,能明白个大概吧:

-pnum or --strip=num
Strip the smallest prefix containing num leading slashes from each file name found in the patch file. A sequence of one or more adjacent slashes is counted as a single slash. This controls how file names found in the patch file are treated, in case you keep your files in a different directory than the person who sent out the patch. For example, supposing the file name in the patch file was
/u/howard/src/blurfl/blurfl.c

setting -p0 gives the entire file name unmodified, -p1 gives

u/howard/src/blurfl/blurfl.c

without the leading slash, -p4 gives

blurfl/blurfl.c

and not specifying -p at all just gives you blurfl.c. Whatever you end up with is looked for either in the current directory, or the directory specified by the -d option.

疑问:

  1. 文件夹名是否会改变
  2. 变化前后的文件名中是否包含第一级文件夹名。如果包含的话,如果整个文件夹的名字改了岂不是就不能正常工作了?

猜你喜欢

转载自blog.csdn.net/qq_31567335/article/details/84595462