超详细举例看懂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条同样的规则。
-------------------------
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可。转载请注明来源:https://imshuai.com/unix-diff-examples-series1-normal 欢迎指正或在下方评论。