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行是一個變動提示,
5c5
共3個字元,第一個5表示變動的行號;第二個c表示變動方式是修改(change),其他的變動方式還有增加a(add)、刪除d(delete);第三個5表示變動後在新檔案的行號,由於是修改因此,修改前後行號一樣。 - 第2行代表“刪除操作”,
<
代表刪除,後面緊跟著刪除的內容55;也就是說刪除了55。 - 第3行是三個橫線,用於分隔刪除和增加的內容。
- 第4行是增加的內容,用
>
表示,>
後就是具體增加的內容。
初步總結一下diff輸出:
- diff先用一個字串表示變動提示,包含:操作的行在舊檔案的行號,操作的方式,以新檔案的行號。
- 操作被分解為“刪除”和“新增”兩步,分別用
<
和>
表示,其後跟上的是具體內容。- 刪除和新增兩步之間,用
---
分隔。
案例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的輸出,稍顯複雜了:
- 變動提示裡,c的兩邊的行號變成了5,6,代表是5到6行。
---
的上下兩部分也變成了兩行內容。這容易理解,因為刪除了兩行內容,並新增了兩行內容。
總結一下,在連續行修改的情況下:
- 第一行操作提示,使用起始行號進行表示範圍。
---
上下,用多個<
和>
,標記連續刪除和新增的內容。
案例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的操作而已,分別以5c5
和10c10
開頭。
總結:
- 當修改內容不連續的時候,diff將修改拆分為多個片段表示,每個片段都是一個完整的連續修改片段。
- 片段的開始符號是“修改提示”,即“原檔案行號(或範圍)” + 變動方式 + “新檔案行號(或範圍)”
案例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
嚯,內容要比修改的情況簡潔多了,但注意兩點:
- “變動提示”裡,除操作方式用a表示增加(add)外;特別注意第3個6表示新檔案的行號。由於是新增行,那麼在新行自然是偏移到第6行了
- 原來出現的
<
和---
都不見了,只剩下表示新增內容的>
。由於只存在增加內容,無需分隔,---
被去除了,可以理解。
總結:
- 變動提示
5a6
,表示在第5行後面新增,並形成新檔案的第6行。- 新增操作不用分解,只保留了
>
行的內容
案例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
- 和新增類似,這裡只有
<
行,沒有---
和>
行。 - 變動提示中,新檔案的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命令的常規模式總結如下:
- diff可分辨變動的粒度是一行。
- 任何變動,在diff看來,都可以分解為刪除行和增加行。
- 對與連續的變動,diff會用一個變動片段表示。其中變動片段分為4個部分:
a. 第一部分,即第一行,是變動提示,用“原檔案行號(或範圍)” + 變動模式 + “新檔案行號(或範圍)”表示,比如3c3。其中變動模式,分為a(增加)、c(修改)和d(刪除)。
b. 第二部分和第四部分用---
(即第三部分)分隔。
c. 第二部分表示刪除的內容,每一行用<
開頭,緊隨的是具體刪除的內容。
d. 第四部分表示增加的內容,每一行用>
開頭,緊隨的是具體增加的內容。
e. 當變動模式是a和d的時候,第二和第部分,只存在一個,同時---
也會被省略。 - 如果一個變動不連續,則會被拆解為多個變動片段表示。每個片段,都遵循第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.
疑問:
- 資料夾名是否會改變
- 變化前後的檔名中是否包含第一級資料夾名。如果包含的話,如果整個資料夾的名字改了豈不是就不能正常工作了?