Contents

linux中的diff命令

1. 概述

在本教程中,我们将*使用*diff 命令深入了解Linux 中的文件比较

2. GNU diffUtils

** diff 命令与其他比较工具(如*cmp sdiff diff3 )捆绑在GNU diffUtils 包**中。因此,大多数 Linux 发行版都预装了diffUtils*包的副本。

因此,如果我们已经拥有diff所需的二进制文件,我们可以跳过安装。但是,如果它丢失或者我们需要不同版本的包,那么我们可能需要自己安装它。

让我们首先检查我们的系统是否有diff二进制文件:

$ which diff
diff not found

好吧,看起来diffUtils包没有安装,所以我们需要安装它。

首先,让我们使用curl 命令从 GNU FTP 服务器 获取包的 tar 存档:

$ curl --silent --output diffutils-3.7.tar.xz ftp://ftp.gnu.org/gnu/diffutils/diffutils-3.7.tar.xz

然后,让我们解压缩包并将二进制文件安装在*/usr/bin* 目录中:

$ tar --extract --file diffutils-3.7.tar.xz
$ cd diffutils-3.7
$ ./configure --prefix=/usr
$ make install

而且,我们准备好了。

3. diff基础

现在我们的系统中有可用的diff二进制文件。让我们学习它的一些基本概念。

3.1. 用法

要使用diff命令,我们需要提供两种类型的信息——即要比较的选项和文件:

diff [OPTION]... FILES

与任何其他 Unix 命令一样,将选项与diff命令一起使用可以让我们获得非默认行为。自然,在我们开始探索各种可用选项之前,我们需要了解默认行为是什么。

3.2. 二进制文件比较

首先,让我们使用diff将其与自身进行比较:

$ /usr/bin/diff /usr/bin/diff /usr/bin/diff
$ echo $?
0

我们可以注意到命令执行优雅地完成,退出代码为 0,表明两个文件相等。 现在,让我们用它来比较两个不同的二进制文件*/bin/mv/bin/cp*:

$ /usr/bin/diff /bin/mv /bin/cp
Binary files /bin/mv and /bin/cp differ
$ echo $?
2

在这种情况下,该命令建议这两个文件不同,但它使用非零返回码提前退出。这实际上是一个误报,因为diff没有对二进制文件进行彻底的比较。

事实上,diff只做最少的事情来确定两个二进制文件是否相同。为了更好地理解这一点,我们可以通过使用*–brief* ( -q ) 选项来模拟这种行为,该选项仅在两个文件不同时输出:

$ diff --brief /bin/mv /bin/cp
Files /bin/mv and /bin/cp differ
$ echo $?
1

3.3. 文本文件比较

与二进制文件比较的情况不同,diff在用于文本数据比较时可以给我们更多的见解。 假设我们正在为学生举办一个 Unix 培训课程,在其中我们向他们介绍“ Hello, World! “ 程序:

$ cat script_v0.sh
#!/bin/sh
/bin/echo "Hello, World!"

现在,作为一项学习任务,学生需要使用不同的 Unix 命令来模拟相同的功能。此外,我们的工作是审查他们提交的代码。 那么,让我们继续看看其中*/bin/echo*命令的使用被shell-builtin echo命令替换的提交之一:

$ cat script_v1.sh
#!/bin/sh
echo "Hello, World!"

好吧,当我们需要对两个代码文件**进行逐行比较时,diff 是一个很好的选择。**所以,让我们用它来比较两个脚本script_v0.shscript_v1.sh

$ diff script_v0.sh script_v1.sh
2c2
< /bin/echo "Hello, World!"
---
> echo "Hello, World!"

**我们可以看到diff为我们提供了一个明确的指令列表,用于将第一个文件更改为与第二个文件完全相同。**如果我们仔细观察,我们可以看到2c2指令表明两个文件的第 2 行是不同的。

接下来,我们再看一个以空行开头并使用shell内置的printf命令的代码提交:

$ cat script_v2.sh
#!/bin/sh
printf "Hello, \n"
printf "World!\n"

最后,让我们通过将其与script_v0.sh进行比较来查看此脚本:

$ diff script_v0.sh script_v2.sh
0a1
>
2c3,4
< /bin/echo "Hello, World!"
---
> printf "Hello, \n"
> printf "World!\n"

同样,diff将两个脚本识别为不同的。但这一次,我们可以在输出中看到两条指令。

0a1指令后跟*>*表明如果我们在第一个脚本的开头添加一个空行,那么我们将获得第二个脚本的第一行。另一方面,2c3,4指令表明我们需要将第一个脚本的第 2 行更改为第二个文件中的一组行 (3-4)。

和前面一样,第一个和第二个脚本的内容分别具有前缀符号“ < ”和“ > ”。此外,三个三连字符通过将每个文件的内容分开来提高可读性。

4. 非违约行为

既然我们已经对diff命令表现出的默认行为有了一个公平的理解,现在是时候探索它的各种选项了。

4.1. 忽略大小写敏感

假设我们需要将受邀学生列表与参加培训课程的实际学生列表进行比较。

首先,我们需要营销团队的帮助,为我们提供按字母顺序排列的受邀参加培训的学生名单:

$ cat all_invitations.txt
BILLY
ROHAN
TOM

现在,假设我们的物流管理团队在出勤*.txt*文件中向我们发送了按字母顺序排列的实际参加培训的学生姓名列表:

$ cat attendance.txt
BILLY
Kiran
tom

请注意,attendance.txt混合了大写和小写字符,而我们的all_inviations.txt文件中的所有内容都是大写的。在这种情况下,我们可能需要使用*–ignore-case*选项

$ diff --ignore-case all_invitations.txt attendance.txt
2c2
< Rohan
---
> kiran

所以,我们可以看到罗汉被邀请了,但他没有参加培训。相反,基兰没有被邀请,但确实参加了会议。

4.2. 空格和空行

好吧,有些学生在课程中没有集中注意力。结果,他们从朋友那里复制了代码,并添加了空格和空白行,以使他们的代码看起来独一无二。

现在,如果我们要抓住抄袭,那么我们需要确保diff能够将复制的解决方案视为相同。

首先,让我们看看 student-9 提交的脚本,他从script_v1.sh中复制了代码。此外,通过在echo命令后添加一个空行和 8 个空格来更改脚本,以使脚本看起来与script_v1.sh不同:

$ cat -te script_v9.sh
#!/bin/sh$
$
echo        "Hello, World!"$

好吧,diff的正常行为 会发现这两个文件是不相同的:

$ diff script_v1.sh script_v9.sh
2c2,3
< echo "Hello, World!"
---
>
> echo        "Hello World"

因此,我们需要使用*–ignore-blank-lines* ( -B ) 和*–ignore-space-change* ( -b ) 选项来捕捉此类抄袭

$ diff -B -b -s script_v1.sh script_v9.sh
Files script_v1.sh and script_v9.sh are identical

我们还可以注意到*-report-identical-files* ( -s ) 选项的使用明确表明两个文件具有相同的内容。

此外,如果我们在某些情况下想要更严格的方法,那么我们甚至可以使用*–ignore-all-space* ( -w ) 选项来忽略所有空格

4.3. 正则表达式

一组学生使用的另一种抄袭策略是在他们抄袭的剧本副本中使用评论。

让我们预览从script_v1.sh复制而来的script_v8.sh中的代码:

$ cat script_v8.sh
#!/bin/sh
#
# Hello World Program
#
echo "Hello World"

同样,我们可能无法使用diff命令的默认行为来捕捉这种抄袭:

$ diff script_v1.sh script_v8.sh
1a2,4
> #
> # Hello World Program
> #

现在,要忽略以 # 开头的注释,我们可以使用带有 ^#正则表达式 值的*–ignore-matching-lines*选项

$ diff -s --ignore-matching-lines="^#" script_v1.sh script_v8.sh
Files script_v1.sh and script_v8.sh are identical

5. 比较多个文件

到目前为止,我们已经使用diff一次准确地比较两个文件。让我们学习如何使用它对多个文件进行比较。

5.1. –from-file和*–to-file*

默认情况下,diff需要两个文件操作数。并且,它总是将对应于第二个文件名的文件与由第一个文件名标识的文件进行比较。

但是,如果我们需要一次比较多个文件,那么我们可以使用*–from-file–to-file*选项

$ diff [--from-file | --to-file] named_file list_of_files

因此,named_file是除 – ( stdin )之外的任何文件名。此外,当我们使用*–from-file选项时,diffnamed_file与剩余文件列表进行比较,而使用–to-file选项时,diff将文件列表与named_file*进行比较。

现在,让我们使用–from -file选项将script-v0.shscript-v1.shscript-v2.sh进行比较:

$ diff --side-by-side --from-file script_v0.sh \
script_v1.sh script_v2.sh
#!/bin/sh                                                       #!/bin/sh
/bin/echo "Hello, World!"                                     | echo "Hello World"
                                                              >
#!/bin/sh                                                       #!/bin/sh
/bin/echo "Hello, World!"                                     |	printf "Hello, \n"
                                                              >	printf "World!\n"

为了提高可读性,我们还使用了–side-by-side ( -y ) 输出格式选项**。因此,我们可以注意到左侧显示的是script_v0.sh文件,而右侧显示的是script_v1.shscript_v2.sh文件。此外,一些符号被用作不同行的前缀:

  • | – 管道符号表示一行中文本的部分变化
  • – 右尖括号表示该行已添加

接下来,让我们使用*–to-file*选项进行反向比较:

$ diff --side-by-side --to-file script_v0.sh \
script_v1.sh script_v2.sh
#!/bin/sh                                                       #!/bin/sh
echo "Hello World"                                            |	/bin/echo "Hello, World!"
                                                              <
#!/bin/sh                                                       #!/bin/sh
printf "Hello, \n"                                            |	/bin/echo "Hello, World!"
printf "World!\n"                                             <

我们可以注意到输出已经改变了方向。左侧现在显示script_v1.shscript_v2.sh文件,而右侧输出script_v0.sh。就像我们之前看到的,前缀符号表示更改,但是这一次,我们有一个左尖括号 < 表示缺少一行。

5.2. 比较目录中的文件

假设物流部门的分析师在分析历史数据时需要我们的帮助。为此,我们提供了一份原始数据的副本,其中包括以类似日历的目录结构组织的出勤报告:

attendance_calendar
├── 2019
│   ├── logistics_incharge.txt
│   ├── reports
│   │   └── January
│   │        ├── 01.txt
│   │        └── 02.txt
│   └── training_incharge.txt
└── 2020
    ├── marketing_incharge.txt
    ├── logistics_incharge.txt
    ├── reports
    │   └── January
    │        ├── 01.txt
    │        └── 02.txt
    └── training_incharge.txt

为了更好的可读性,仅显示一月份的几天。但是,我们可以假设 2019 年和 2020 年所有相关日期的出勤数据都可用。

现在,请记住,在 Unix 中目录也被视为文件,让我们使用diff的普通行为 来比较 2019 年和 2020 年的数据:

attendance_calendar$ diff --side-by-side 2019/ 2020/
... 2019/logistics_incharge.txt 2020/logistics_incharge.txt
Mrs. Hudson						      |	Mr. Watson
Only in 2020/: marketing_incharge.txt
Common subdirectories: 2019/reports and 2020/reports
... 2019/training_incharge.txt 2020/training_incharge.txt
Mr. Thomson						      |	Mr. Richard

好吧,我们可以看到diff选择了按字母顺序进行比较的文件,但只选择了直接位于指定目录下的文件。而且,我们还得到了目录结构的一级比较。

5.3. 递归比较

默认情况下,diff不对位于子目录下的文件执行递归比较。但是,我们可以使用–recursive ( -r ) 选项来启用它**。

假设我们需要比较 2019 年和 2020 年第一季度每个月的第一天的出勤率。递归比较应该有效,但是,我们需要排除一组文件。

现在,对我们来说好消息是diff提供了两个选项,–exclude=PATTERN和*–exclude-from=PATTERN_FILE*来满足这样的用例。因此,让我们考虑需要从比较中排除的文件集:

  • 必须排除除01.txt之外的所有文件
  • 必须排除三月之后的所有文件

因此,让我们创建一个名为exclude_patterns.txt的文件,并将所有模式保持为小写:

$ cat excluded_patterns.txt
0[2-9].txt
[1-3][0-9].txt
april
may
june
july
august
september
october
november
december

稍后,我们可以使用*–exclude-from–ignore-file-name-case*选项的组合来优雅地处理两个目录中的月份名称仅因大小写而异的情况。

最后,让我们把事情放在一起,看看我们的递归比较的实际效果:

$ diff \
--side-by-side \
--ignore-file-name-case \
--exclude-from=excluded_patterns.txt \
--recursive 2019/reports 2020/reports
... 2019/reports/February/01.txt 2020/reports/February/01.txt
Invitations Sent: 800					      |	Invitations Sent: 1200
Actually Attended: 275 					      |	Actually Attended: 575
... 2019/reports/January/01.txt 2020/reports/January/01.txt
Invitations Sent: 500					      |	Invitations Sent: 1000
Actually Attended: 150					      |	Actually Attended: 300
Only in 2019/reports/March: 01.txt

由于我们没有 2020 年 3 月的名为01.txt的文件,因此缺少 3 月的详细比较报告。

5.4. 缺席文件

使用目录时,diff通常只比较通过两个目录下的相似路径可访问的文件。

让我们使用与生成 2019 年和 2020 年 Q1 的第 1 天比较报告相同的选项调用diff。但是,这一次,我们也使用*–starting-file*选项仅在找到文件时才开始比较匹配关键字“三月”的路径:

$ diff \
--side-by-side \
--starting-file=March \
--ignore-file-name-case \
--exclude-from=excluded_patterns.txt \
--recursive \
--from-file 2019/reports 2020/reports
Only in 2019/reports/March: 01.txt

我们可以注意到diff抱怨在 2020 目录下无法访问文件01.txt 。但是,它没有为此类文件提供更多比较见解。

由于01.txt 仅存在于 2019 目录下,因此查看比较的更好方法是将缺失的文件视为空文件。有趣的是,diff有两个选项可以将这个计划付诸实施:

  • –new-file ( -N ) 将任一目录下的丢失文件视为空的新文件
  • –unidirectional-new-file 仅将第一个目录中不存在的文件视为空文件

因此,让我们首先使用*–new-file*选项并检查它是否符合我们的预期:

$ diff \
--side-by-side \
--starting-file=March \
--ignore-file-name-case \
--exclude-from=excluded_patterns.txt \
--new-file \
--recursive \
--from-file 2019/reports 2020/reports
... --from-file 2019/reports 2019/reports/March/01.txt 2020/reports/March/01.txt
Invitations Sent: 750					      <
Actually Attended: 350					      <

嗯,是!它确实按预期工作,并为我们提供了更多见解。

现在,让我们也看看受限的*–unidirectional-new-file*选项的效果:

$ diff \
--side-by-side \
--starting-file=March \
--ignore-file-name-case \
--exclude-from=excluded_patterns.txt \
--unidirectional-new-file \
--recursive \
--from-file 2019/reports 2020/reports
Only in 2019/reports/March: 01.txt

啊! 我们通过使用*–unidirectional-new-file而不是–new-file*回到第一方。这是预期的行为,因为第一个目录中不存在该文件。

最后,让我们使用*–to-file–unidirectional-new-file*选项生成反向比较报告:

$ diff \
--side-by-side \
--starting-file=March \
--ignore-file-name-case \
--exclude-from=excluded_patterns.txt \
--unidirectional-new-file \
--recursive \
--to-file 2019/reports 2020/reports
... --to-file 2019/reports 2020/reports/March/01.txt 2019/reports/March/01.txt
							      >	Invitations Sent: 750
							      >	Actually Attended: 350

我们现在确实看到了*–unidirectional-new-file*选项的效果,因为比较中不存在文件的顺序是相反的。

6. 打补丁

修补是一种机制,它可以帮助我们通过对数据应用一组类似diff的更改来修改它。

在使用提供给我们的数据副本准备几份分析报告时,我们对其进行了一些更改。此外,我们认为有必要要求物流团队对原始数据进行这些更改。让我们看看diff如何帮助我们进行修补。

6.1. ed脚本

首先,让我们看看我们对工作文件01.txt.modified所做的更改,该文件最初是从2020/reports/January/01.txt文件中克隆的:

$ diff 01.txt 01.txt.modified
2a3
> Empty Seats: 700

我们可以看到我们添加了一条新行,其中提到了培训期间当天空着的座位。

现在,为了让保留数据原始副本的团队吸收这些更改,我们可以做两件事:

  • 发送完整数据的副本并要求他们将其用作完整的替代品
  • 仅发送我们所做的更改集并请求他们修补更改的数据

嗯,第二个选择更集中,因为我们发送的数据最少,仍然可以完成工作。

此外,diff可以以编辑( ed )脚本的形式生成输出,这可以帮助我们自动化修补过程。为此,我们需要使用–ed ( -e ) 选项*。

接下来,让我们为我们对01.txt文件所做的更改生成一个ed脚本:

$ diff --ed 01.txt.modified 01.txt > 01.txt.ed
$ cat 01.txt.ed
2a
Empty Seats: 700
.

因此,一旦团队收到我们的ed脚本,首先他们需要创建一个名为01.txt.orig的备份文件。然后,他们可以使用01.txt.ed脚本来更新01.txt文件:

$ (cat 01.txt.ed && echo w) | ed - 01.txt

在这里,我们将01.txt.ed脚本中的指令写入stdin,然后是write ( w ) 命令。此外,这些指令通过管道传递给ed命令。

最后,团队可以验证更新的文件并删除备份文件:

$ cat 01.txt
Invitations Sent: 1000
Actually Attended: 300
Empty Seats: 700
$ rm 01.txt.orig

6.2. diff -upatch

好吧,ed脚本可能是创建和应用补丁的最古老的方法之一。但是,它们对用户不太友好,并且熟悉ed概念会给接收者带来开销。

所以,为了让事情运行得更顺畅,我们也可以使用diff的*–unified* ( -u ) 输出格式化选项来创建补丁文件

$ diff --unified 01.txt 01.txt.modified > 01.txt.diff
$ cat 01.txt.diff
--- 01.txt	2020-04-01 14:37:00.000000000 +0530
+++ 01.txt.modified	2020-04-01 14:36:54.000000000 +0530
@@ -1,2 +1,3 @@
 Invitations Sent: 1000
 Actually Attended: 300
+Empty Seats: 700

一开始,新的风格可能会让我们不知所措,所以让我们一点一点地理解这一点:

  • 两行标题指示源文件目标文件以及最后修改的时间戳
  • @@line-range-1, [email protected] @有助于分别在from-fileto-file中定位此更改块
  • 使用 – 和 + 意味着在第一个文件中删除或添加一行

现在我们基本上知道了它是如何工作的,让我们将01.txt.diff文件发送给团队,他们可以使用patch 命令来应用它

$ patch 01.txt 01.txt.diff
patching file 01.txt
$ cat 01.txt
Invitations Sent: 1000
Actually Attended: 300
Empty Seats: 700

因此,diff –unifiedpatch命令一起完成了统一格式的补丁工作流程。

7. 输出格式

到目前为止,我们已经使用了一些输出格式,例如*–side-by-side*、–normal(默认)和*–unified*。让我们学习如何进一步控制输出格式。

7.1. 线型和组型

在比较两个文件时,diff将两个文件中的整个文本段分成相同行和不同行的序列,称为hunks 。最终,diff 为我们提供了有关这些大块的信息,以衡量两个文件之间的差异。

一般来说,即使是单行也属于这一类,其中一组大小为 1,其中起始行号和结束行号相等。

因此,diff命令能够找出两组内的变化。因此,它可以将组分为四种类型:旧的、新的、未更改的和已更改的。

此外,diff 在这些组中进行迭代以进行逐行比较。因此,当涉及到单个行时,它可以在内部将它们分为三种类型:旧的、新的和未更改的。

简而言之,线型和组型被称为LTYPEGTYPE

7.2. 行格式和组格式

diff生成的输出是不同组类型中每一行的信息的集合。此外,它还提供了三个选项 - 即*-line-format*、-LTYPE-line-format和*-GTYPE-group-format* - 用于精细控制输出格式。

现在,如果我们将LTYPEGTYPE替换为它们的可能值,那么我们将得到一堆选项,例如*–old-line-format*、–new-line-format–unchanged-line-format–old-group-format–new-group-format–unchanged-group-format和*–changed-group-format*。

自然,行和组具有不同的特征。因此,diff 有两种格式化类别,即Line Format ( LFMT )Group Format ( GFMT ) 。让我们看一下用于识别属于第一个和第二个文件的行组边界的不同GFMT符号:

/uploads/diff_command/1.gif

GFMT 中将行号和行内容符号的含义可视化后,清楚地了解行内容的格式化过程也很重要。那么,让我们看看GFMTLFMT格式之间的联系:

/uploads/diff_command/2.png 嗯,有*三个GFMT符号,即*%<%=%> ,它们标识两个文件中不同或相同行的值。此外,这些符号中的每一个都由LTYPE格式化规则格式化,之后输出呈现在stdout上。

7.3. 定制

现在,让我们应用diffLFMTGFMT规则来模拟自定义的并排输出格式

首先,让我们看一下2019年和2020年的培训师数据:

$ cat trainers_2019.txt
Bill
Catherine
Dave
Eve
Raymond
Susan
Zack
$ cat trainers_2020.txt
Bill
Eve
Feynman
Gabrina
Raymond
Susan
Zoe

现在,让我们使用 –LTYPE-line-format–GTYPE-group-format 选项来生成我们自己版本的并排输出格式:

diff \
--old-group-format='[%(f=l?L%df:L%df,L%dl)] vs [❌]:
%<' \
--new-group-format='[❌] vs [%(F=L?L%dF:L%dF,L%dL)]: %>' \
--changed-group-format='[%(f=l?L%df:L%df,L%dl)] vs [%(F=L?L%dF:L%dF,L%dL)]:
%< %>' \
--unchanged-group-format='[%(f=l?L%df:L%df,L%dl)] vs [%(F=L?L%dF:L%dF,L%dL)]:
%=' \
--old-line-format='-	%L' \
--new-line-format='				+	%L' \
--unchanged-line-format='✔️	%l				%L' \
trainers_2019.txt trainers_2020.txt

尽管我们的命令包含许多来自 LFMTGFMT 的熟悉符号,但如果我们仔细观察,我们可能会看到一些模式:

  • 制表符用于模拟列视图
  • %d 前缀为行号符号提供了预期的数字含义,例如 flFL
  • 行格式符号 %L%l 打印带或不带尾随换行符的行内容
  • %(condition? value1 : value2) 演示三元运算符的功能
  • 符号 ✔️、+ 和 – 分别拼出 NoopAddDelete 指令
  • ❌ 表示其中一个文件中不存在相应的组

最后,让我们看看最后一个命令生成的漂亮、整洁、并排的 diff

[L1] vs [L1]:
✔️      Bill                            Bill
[L2,L3] vs [❌]:
-       Catherine
-       Dave
[L4] vs [L2]:
✔️      Eve                             Eve
[❌] vs [L3,L4]: 
                                +       Feynman
                                +       Gabrina
[L5,L6] vs [L5,L6]:
✔️      Raymond                         Raymond
✔️      Susan                           Susan
[L7] vs [L7]:
-       Zack
                                +       Zoe

我们从演示角度创建了自定义输出格式,因此,它可能无法处理所有边缘情况。 因此,对于生产代码,我们必须通过处理边缘情况来提高其可靠性,或者我们可以使用 –side-by-side 选项。