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