1. 程式人生 > >linux diff patch原理及用法學習

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,會報錯:

[email protected]:/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會直接在原檔案上進行修改

[email protected]:/opt/diffTest/patchTest# cat v1.file
11
22
33
44
55
66
77
88
99
00
aa
bb
cc
dd
[email protected]:/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
[email protected]:/opt/diffTest/patchTest#
[email protected]:/opt/diffTest/patchTest#
[email protected]:/opt/diffTest/patchTest# patch v1.file -i patchFromV1ToV2.patch
patching file v1.file
[email protected]:/opt/diffTest/patchTest#
[email protected]:/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
[email protected]:/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
[email protected]:/opt/diffTest/patchTest#
[email protected]:/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
[email protected]:/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. 變化前後的檔名中是否包含第一級資料夾名。如果包含的話,如果整個資料夾的名字改了豈不是就不能正常工作了?