diff 命令详解与参数实测(Ubuntu 22.04)
从 1c2/<、---、> 逐段解读到 -u 的 @@ hunk,详解 GNU diff 的 normal、unified、context、并排四种输出格式与递归目录比较、忽略类参数、--color 色码及 0/1/2 退出码语义
diff 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 GNU diff 的逐行比较能力——默认 normal 格式(
1c2/</---/>)逐段解读、-uunified 格式(@@ -l,n +l,n @@)、-ccontext 格式、-y并排对比(-W控宽)、-r递归目录比较、-q简报模式、-i/-w/-b/-B忽略类参数、--color彩色输出的色码形态与 0/1/2 三种退出码语义,全部命令与回显已在 Ubuntu 22.04(GNU diffutils 3.8)实测通过。
一、环境信息
本节列出本文所有实测所依赖的环境与测试数据,便于读者按同规格复现。
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- diff 版本:GNU diffutils 3.8(
diff --version实测) - diff 属于 diffutils 软件包,Ubuntu 22.04 默认预装,无需额外安装;字节级比较工具 cmp 与 diff 同属 diffutils
- 演示目录:
/root/demo/diff/ - 测试数据:
a.txt(4 行)与b.txt(5 行)组成「大小写差异 + 尾行改写 + 新增行」的文本对;w1.txt、w2.txt组成「大小写 + 空行数量」差异对;ws1.txt、ws2.txt组成「行内空白」差异对;old/与new/两棵目录树各含app.conf、conf/db.conf、same.txt,并分别独有only-old.txt、only-new.txt
二、命令概览
2.1 语法与测试数据准备
diff(difference)的基本语法是 diff [OPTION]... FILES:逐行比较两个文件的内容差异,输出「如何把第一个文件改成第二个文件」的编辑指令;当比较对象是两个目录时,逐对比较目录中的同名文件。diff 是代码评审、配置比对、补丁生成的基石命令。
先准备一套覆盖各类差异形态的演示数据,后续所有参数实测都基于这套数据:
rm -rf /root/demo/diff && mkdir -p /root/demo/diff/old/conf /root/demo/diff/new/conf
printf 'hello world\nthis is line 2\ncommon line 3\nold tail line\n' > /root/demo/diff/a.txt
printf 'hello world\nthis is LINE 2\ncommon line 3\nnew tail line\nextra last line\n' > /root/demo/diff/b.txt
printf 'apple\nBanana\ncherry\n\n\nend\n' > /root/demo/diff/w1.txt
printf 'apple\nbanana\ncherry\n\nend\n' > /root/demo/diff/w2.txt
printf 'hello world\nend\n' > /root/demo/diff/ws1.txt
printf 'hello world\nend\n' > /root/demo/diff/ws2.txt
printf 'port=8080\nlog=info\n' > /root/demo/diff/old/app.conf
printf 'port=9090\nlog=info\n' > /root/demo/diff/new/app.conf
printf 'db=192.168.1.10\n' > /root/demo/diff/old/conf/db.conf
printf 'db=192.168.1.20\nextra=true\n' > /root/demo/diff/new/conf/db.conf
printf 'shared content\n' > /root/demo/diff/old/same.txt
printf 'shared content\n' > /root/demo/diff/new/same.txt
printf 'only in old\n' > /root/demo/diff/old/only-old.txt
printf 'only in new\n' > /root/demo/diff/new/only-new.txt
验证两个核心文本文件的内容(后续 normal 格式解读以它们为样本):
$ cat -n /root/demo/diff/a.txt
1 hello world
2 this is line 2
3 common line 3
4 old tail line
$ cat -n /root/demo/diff/b.txt
1 hello world
2 this is LINE 2
3 common line 3
4 new tail line
5 extra last line
验证目录树结构(两棵树同名文件路径一一对应,差异内容各不相同):
$ tree /root/demo/diff
/root/demo/diff
|-- a.txt
|-- b.txt
|-- new
| |-- app.conf
| |-- conf
| | `-- db.conf
| |-- only-new.txt
| `-- same.txt
|-- old
| |-- app.conf
| |-- conf
| | `-- db.conf
| |-- only-old.txt
| `-- same.txt
|-- w1.txt
`-- w2.txt
4 directories, 12 files
2.2 --help 全量回显
diff --help 输出全部参数清单(实测原样保留,可作为参数速查表):
Usage: diff [OPTION]... FILES
Compare FILES line by line.
Mandatory arguments to long options are mandatory for short options too.
--normal output a normal diff (the default)
-q, --brief report only when files differ
-s, --report-identical-files report when two files are the same
-c, -C NUM, --context[=NUM] output NUM (default 3) lines of copied context
-u, -U NUM, --unified[=NUM] output NUM (default 3) lines of unified context
-e, --ed output an ed script
-n, --rcs output an RCS format diff
-y, --side-by-side output in two columns
-W, --width=NUM output at most NUM (default 130) print columns
--left-column output only the left column of common lines
--suppress-common-lines do not output common lines
-p, --show-c-function show which C function each change is in
-F, --show-function-line=RE show the most recent line matching RE
--label LABEL use LABEL instead of file name and timestamp
(can be repeated)
-t, --expand-tabs expand tabs to spaces in output
-T, --initial-tab make tabs line up by prepending a tab
--tabsize=NUM tab stops every NUM (default 8) print columns
--suppress-blank-empty suppress space or tab before empty output lines
-l, --paginate pass output through 'pr' to paginate it
-r, --recursive recursively compare any subdirectories found
--no-dereference don't follow symbolic links
-N, --new-file treat absent files as empty
--unidirectional-new-file treat absent first files as empty
--ignore-file-name-case ignore case when comparing file names
--no-ignore-file-name-case consider case when comparing file names
-x, --exclude=PAT exclude files that match PAT
-X, --exclude-from=FILE exclude files that match any pattern in FILE
-S, --starting-file=FILE start with FILE when comparing directories
--from-file=FILE1 compare FILE1 to all operands;
FILE1 can be a directory
--to-file=FILE2 compare all operands to FILE2;
FILE2 can be a directory
-i, --ignore-case ignore case differences in file contents
-E, --ignore-tab-expansion ignore changes due to tab expansion
-Z, --ignore-trailing-space ignore white space at line end
-b, --ignore-space-change ignore changes in the amount of white space
-w, --ignore-all-space ignore all white space
-B, --ignore-blank-lines ignore changes where lines are all blank
-I, --ignore-matching-lines=RE ignore changes where all lines match RE
-a, --text treat all files as text
--strip-trailing-cr strip trailing carriage return on input
-D, --ifdef=NAME output merged file with '#ifdef NAME' diffs
--GTYPE-group-format=GFMT format GTYPE input groups with GFMT
--line-format=LFMT format all input lines with LFMT
--LTYPE-line-format=LFMT format LTYPE input lines with LFMT
These format options provide fine-grained control over the output
of diff, generalizing -D/--ifdef.
LTYPE is 'old', 'new', or 'unchanged'. GTYPE is LTYPE or 'changed'.
GFMT (only) may contain:
%< lines from FILE1
%> lines from FILE2
%= lines common to FILE1 and FILE2
%[-][WIDTH][.[PREC]]{doxX}LETTER printf-style spec for LETTER
LETTERs are as follows for new group, lower case for old group:
F first line number
L last line number
N number of lines = L-F+1
E F-1
M L+1
%(A=B?T:E) if A equals B then T else E
LFMT (only) may contain:
%L contents of line
%l contents of line, excluding any trailing newline
%[-][WIDTH][.[PREC]]{doxX}n printf-style spec for input line number
Both GFMT and LFMT may contain:
%% %
%c'C' the single character C
%c'\OOO' the character with octal code OOO
C the character C (other characters represent themselves)
-d, --minimal try hard to find a smaller set of changes
--horizon-lines=NUM keep NUM lines of the common prefix and suffix
--speed-large-files assume large files and many scattered small changes
--color[=WHEN] color output; WHEN is 'never', 'always', or 'auto';
plain --color means --color='auto'
--palette=PALETTE the colors to use when --color is active; PALETTE is
a colon-separated list of terminfo capabilities
--help display this help and exit
-v, --version output version information and exit
FILES are 'FILE1 FILE2' or 'DIR1 DIR2' or 'DIR FILE' or 'FILE DIR'.
If --from-file or --to-file is given, there are no restrictions on FILE(s).
If a FILE is '-', read standard input.
Exit status is 0 if inputs are the same, 1 if different, 2 if trouble.
Report bugs to: bug-diffutils@gnu.org
GNU diffutils home page: <https://www.gnu.org/software/diffutils/>
General help using GNU software: <https://www.gnu.org/gethelp/>
帮助末尾一句话概括了退出码约定(Exit status is 0 if inputs are the same, 1 if different, 2 if trouble),这是 3.9 节实测的理论依据;-W 的默认宽度 130、-c/-u 的默认上下文行数 3 也都出自这份帮助原文。
2.3 版本确认
确认实测环境的 diff 版本,参数行为与版本相关(本文所有结论基于 diffutils 3.8):
$ diff --version | head -1
diff (GNU diffutils) 3.8
三、参数逐项实测
3.1 默认格式(normal diff)
不加任何参数时,diff 输出 normal 格式:由一组组「变更指令」构成,每条指令回答「第一个文件的第几行,做什么操作,变成第二个文件的第几行」。实测前面准备好的 a.txt 与 b.txt:
$ diff /root/demo/diff/a.txt /root/demo/diff/b.txt
2c2
< this is line 2
---
> this is LINE 2
4c4,5
< old tail line
---
> new tail line
> extra last line
逐段解读这段输出:
2c2:变更指令。格式为「文件1行号 + 动作字母 + 文件2行号」,动作字母有三种——c(change,改动)、d(delete,删除)、a(append,新增)。2c2读作「文件1第 2 行改动后对应文件2第 2 行」;< this is line 2:<开头的行来自文件1(a.txt);---:两个文件内容的分隔线;> this is LINE 2:>开头的行来自文件2(b.txt);4c4,5:逗号表示范围,「文件1第 4 行」对应「文件2第 5 到 5 行」——即文件1的 1 行被改写成文件2的 2 行,所以下面跟了 1 行<和 2 行>。
再实测两个完全相同的文件,diff 静默退出、无任何输出:
$ diff /root/demo/diff/a.txt /root/demo/diff/a.txt
💡 技术拓展:diff 的输出格式不止 normal 一种,常见的有六类,横向对比——
格式 触发参数 特点 适用场景 normal 默认 最紧凑,只有变更指令 快速看差异 unified -u带上下文, @@定位,补丁事实标准代码评审、git diff、打补丁 context -c带上下文, ***/---头老式补丁(传统 patch) ed 脚本 -e可被 ed 编辑器直接执行的脚本 程序化处理 RCS -nRCS 版本控制系统格式 RCS 工具链 并排 -y左右两栏对照 人工逐行核对 本文以实际工程中最常用的 normal(默认)与 unified(
-u)为主线,其余格式在 3.3、3.4 节实测。
3.2 -u:unified 格式(最常用)
-u(--unified)输出带上下文的 unified 格式,是 git、GitHub、patch 工具链的事实标准格式。实测同一对文件:
$ diff -u /root/demo/diff/a.txt /root/demo/diff/b.txt
--- /root/demo/diff/a.txt 2026-10-15 07:35:52.446944785 +0000
+++ /root/demo/diff/b.txt 2026-10-15 07:35:52.447944853 +0000
@@ -1,4 +1,5 @@
hello world
-this is line 2
+this is LINE 2
common line 3
-old tail line
+new tail line
+extra last line
逐段解读:
---开头行:源文件(文件1)路径与修改时间;+++开头行:目标文件(文件2)路径与修改时间;@@ -1,4 +1,5 @@:hunk(块)头,-1,4表示文件1从第 1 行起共 4 行参与本块,+1,5表示文件2从第 1 行起共 5 行;行数省略 1 时只写起始行号(如@@ -3 +3 @@);- 空格开头行(
hello world、common line 3):上下文行,两个文件都有的内容,用于在打补丁时定位; -开头行:文件1中将被删除的行;+开头行:文件2中将新增的行。
与 normal 格式相比,unified 把相邻变更合并进同一个 hunk 并附带 3 行默认上下文(可用 -U NUM 调整),人工阅读和机器打补丁都更友好。
3.3 -c:context 格式
-c(--context)输出 context 格式,同样带 3 行默认上下文,是传统 patch 命令支持的另一种格式:
$ diff -c /root/demo/diff/a.txt /root/demo/diff/b.txt
*** /root/demo/diff/a.txt Thu Oct 15 07:35:52 2026
--- /root/demo/diff/b.txt Thu Oct 15 07:35:52 2026
***************
*** 1,4 ****
hello world
! this is line 2
common line 3
! old tail line
--- 1,5 ----
hello world
! this is LINE 2
common line 3
! new tail line
! extra last line
解读:*** 段属于文件1、--- 段属于文件2(对应 *** 1,4 **** 与 --- 1,5 ---- 两个行号头);! 标记该行在两个文件间有差异,空格开头为上下文行,+/- 标记纯新增/纯删除行(本例没有纯增删故未出现);*************** 是块分隔线。context 与 unified 的差异主要在头部标记风格,工程上新补丁一律用 unified。
3.4 -y:并排对比(配合 -W 控宽)
-y(--side-by-side)把两个文件左右两栏并排输出,中间列放差异标记(| 表示该行不同、> 表示仅文件2有的行、< 表示仅文件1有的行)。默认总宽 130 列,终端较窄时用 -W 指定宽度。默认宽度实测:
$ diff -y /root/demo/diff/a.txt /root/demo/diff/b.txt
hello world hello world
this is line 2 | this is LINE 2
common line 3 common line 3
old tail line | new tail line
> extra last line
用 -W 80 控制为 80 列后输出更紧凑:
$ diff -y -W 80 /root/demo/diff/a.txt /root/demo/diff/b.txt
hello world hello world
this is line 2 | this is LINE 2
common line 3 common line 3
old tail line | new tail line
> extra last line
配合 --suppress-common-lines 隐藏相同行,只看差异行:
$ diff -y -W 60 --suppress-common-lines /root/demo/diff/a.txt /root/demo/diff/b.txt
this is line 2 | this is LINE 2
old tail line | new tail line
> extra last line
💡 说明:
-y输出中栏间由制表符与空格混合填充对齐,上面代码块原样保留了实测字符;它适合人工逐行核对,不适合作为补丁输入。
3.5 -r:递归比较目录
-r(--recursive)在比较两个目录时递归进入所有子目录,逐对比较同名文件。实测 old 与 new 两棵目录树:
$ diff -r /root/demo/diff/old /root/demo/diff/new
diff -r /root/demo/diff/old/app.conf /root/demo/diff/new/app.conf
1c1
< port=8080
---
> port=9090
diff -r /root/demo/diff/old/conf/db.conf /root/demo/diff/new/conf/db.conf
1c1,2
< db=192.168.1.10
---
> db=192.168.1.20
> extra=true
Only in /root/demo/diff/new: only-new.txt
Only in /root/demo/diff/old: only-old.txt
输出规则:内容不同的同名文件,先打印一行 diff -r 文件1 文件2 指明比较对象,再紧跟该文件对的 normal 格式差异;单侧独有的文件输出 Only in 目录: 文件名;两棵树中都有的 same.txt 内容一致,不出现在输出里。
3.6 -q:只报告是否不同
-q(--brief)不输出差异细节,只报告「哪些文件不同」,适合在脚本里快速获得变更文件清单:
$ diff -q /root/demo/diff/a.txt /root/demo/diff/b.txt
Files /root/demo/diff/a.txt and /root/demo/diff/b.txt differ
目录模式下同样适用,子目录会以 Common subdirectories 提示(需加 -r 才会深入比对):
$ diff -q /root/demo/diff/old /root/demo/diff/new
Files /root/demo/diff/old/app.conf and /root/demo/diff/new/app.conf differ
Common subdirectories: /root/demo/diff/old/conf and /root/demo/diff/new/conf
Only in /root/demo/diff/new: only-new.txt
Only in /root/demo/diff/old: only-old.txt
💡 说明:
-q与-r组合(diff -qr 目录1 目录2)是发布前核对两套目录差异的最常用姿势——先-q列出变更文件清单,再对清单中的文件单独出详细 diff。帮助原文中的-s, --report-identical-files是它的反面:把相同的文件也报告出来。
3.7 -i / -w / -b / -B:忽略大小写与空白
这组参数改变「什么算差异」的判定口径。先用 w1.txt(Banana 大写、两个空行)与 w2.txt(banana 小写、一个空行)实测默认、-i、-B 三种口径。默认口径下大小写与空行都算差异:
$ diff /root/demo/diff/w1.txt /root/demo/diff/w2.txt
2c2
< Banana
---
> banana
4d3
<
-i(--ignore-case)忽略大小写后,2c2 消失,只剩空行差异:
$ diff -i /root/demo/diff/w1.txt /root/demo/diff/w2.txt
4d3
<
-w(--ignore-all-space)只忽略行内空白,不忽略空行数量,大小写也照常比较,因此输出与默认口径相同:
$ diff -w /root/demo/diff/w1.txt /root/demo/diff/w2.txt
2c2
< Banana
---
> banana
4d3
<
-B(--ignore-blank-lines)忽略「整行都是空白」的行的增删,空行差异消失,只剩大小写差异:
$ diff -B /root/demo/diff/w1.txt /root/demo/diff/w2.txt
2c2
< Banana
---
> banana
再用 ws1.txt(hello world,三个空格)与 ws2.txt(hello world,一个空格)实测空白专项。默认口径下空白数量变化算差异:
$ diff /root/demo/diff/ws1.txt /root/demo/diff/ws2.txt
1c1
< hello world
---
> hello world
-w 忽略全部空白后两文件视为相同(退出码 0、无输出):
$ diff -w /root/demo/diff/ws1.txt /root/demo/diff/ws2.txt; echo 退出码=$?
退出码=0
-b(--ignore-space-change)忽略空白「数量」变化,结果与 -w 一致:
$ diff -b /root/demo/diff/ws1.txt /root/demo/diff/ws2.txt; echo 退出码=$?
退出码=0
四个参数的口径差异小结:-i 管大小写,-B 管空行增删,-w 抹掉所有空白差异,-b 只抹空白数量差异(保留有无空白之别)。帮助原文中还有 -Z, --ignore-trailing-space(只忽略行尾空白)与 -E, --ignore-tab-expansion(忽略制表符展开),按同一机制工作。
3.8 --color:彩色输出与色码
--color=always 强制输出 ANSI 彩色序列(默认 auto 仅在终端时着色,重定向进文件时需用 always)。管道接到 cat -v 把不可见控制符显形,看色码的真实形态:
$ diff --color=always /root/demo/diff/a.txt /root/demo/diff/b.txt | cat -v
^[[36m2c2^[[0m
^[[31m< this is line 2^[[0m
---
^[[32m> this is LINE 2^[[0m
^[[36m4c4,5^[[0m
^[[31m< old tail line^[[0m
---
^[[32m> new tail line^[[0m
^[[32m> extra last line^[[0m
解读:^[[ 是 ESC 控制字符的显形写法。^[[36m(青色)着色变更指令行(行号头),^[[31m(红色)着色文件1删除行 <,^[[32m(绿色)着色文件2新增行 >,^[[0m 复位颜色;分隔线 --- 不着色。配色方案可通过 --palette 自定义(帮助原文:terminfo 能力的冒号分隔列表)。
3.9 退出码 0 / 1 / 2
帮助原文明确「Exit status is 0 if inputs are the same, 1 if different, 2 if trouble」,三种取值逐一实测。文件不同,退出码 1:
$ diff /root/demo/diff/a.txt /root/demo/diff/b.txt > /dev/null; echo 退出码=$?
退出码=1
文件相同,退出码 0:
$ diff /root/demo/diff/a.txt /root/demo/diff/a.txt > /dev/null; echo 退出码=$?
退出码=0
任一文件不存在(trouble),报错并退出码 2:
$ diff /root/demo/diff/a.txt /no/such/file > /dev/null; echo 退出码=$?
diff: /no/such/file: No such file or directory
退出码=2
退出码不依赖输出,是脚本里判断「有无变更」「命令是否跑对」的可靠依据,可与 3.6 的 -q 组合实现静默检查。
四、实战示例
4.1 比较两条命令的输出
借助 Bash 进程替换 <(命令),diff 可以直接比较两条命令的输出流,无需先落盘。实测比较两个目录的文件清单:
$ diff <(ls /root/demo/diff/old) <(ls /root/demo/diff/new)
3c3
< only-old.txt
---
> only-new.txt
第 3 行是两个目录清单唯一的差异:old 独有 only-old.txt、new 独有 only-new.txt。同一技巧可用于比较 ps、df 等任意命令两次执行的输出变化。
4.2 生成补丁文件
-u 输出重定向到文件,就是一份可直接分发的 unified 补丁。实测把 a.txt 到 b.txt 的差异存为补丁:
$ diff -u /root/demo/diff/a.txt /root/demo/diff/b.txt > /root/demo/diff/a-to-b.patch
$ head -5 /root/demo/diff/a-to-b.patch
--- /root/demo/diff/a.txt 2026-10-15 07:35:52.446944785 +0000
+++ /root/demo/diff/b.txt 2026-10-15 07:35:52.447944853 +0000
@@ -1,4 +1,5 @@
hello world
-this is line 2
💡 说明:这份 patch 文件正是 patch 命令的输入——在持有旧版本 a.txt 的机器上执行
patch 原文件 < 补丁文件即可把变更复现出来,这就是「diff 出补丁、patch 打补丁」的经典分工,git 的 commit 本质上也是仓库对象间的 diff。
五、注意事项与常见问题
Q1:diff 和 cmp 怎么选?
diff 是行级比较,输出人可读的行差异,适合文本与代码;cmp 是字节级比较,定位第一个不同字节的位置,适合二进制文件或「只需要知道是否完全一致」的场景。对同一个二进制文件,diff 只会说 Binary files ... differ,而 cmp 能精确到 char 5, line 1(见《cmp 命令详解与参数实测(Ubuntu 22.04)》)。
Q2:为什么两个文件有差异,diff -q 却静默退出码 0?
最常见原因是路径传反或路径不存在导致退出码 2;其次应确认比较的确实是同一路径下的同一文件。排查方法:去掉 -q 直接跑一次,看 3.9 节的退出码与报错输出。
Q3:-u 输出的第一行 --- 与分隔线 --- 会混淆吗?
不会。文件头行以 --- 路径<TAB>时间戳 开头且与 +++ 成对出现;hunk 内部的 --- 是独立一行,前后是 </> 语义的上下文,解析补丁时按行头+成对关系即可区分。
Q4:-w 和 -b 有什么区别?
-w, --ignore-all-space 忽略所有空白(行内的有无与数量全忽略);-b, --ignore-space-change 只忽略空白数量的变化,一行「完全无空白」与「有空白」仍算差异。本文 3.7 实测的用例中两者结论相同,是因样例只涉及数量差异;-Z(只忽略行尾空白)粒度更细,按需选择。
Q5:比较目录时 Only in 输出能不能让单侧缺失文件参与文本比较?
可以。加 -N, --new-file 后,单侧缺失的文件按空文件参与比较,缺失侧会整文件显示为新增或删除,目录对账时更直观(依据帮助原文;本文 3.5 实测了默认口径的 Only in 行为)。
六、参考链接
- GNU diffutils 官方手册(diff 章节):https://www.gnu.org/software/diffutils/manual/html_node/diff-Options.html
- GNU diffutils 首页:https://www.gnu.org/software/diffutils/
- 本机帮助:
diff --help、man diff(Ubuntu 22.04 内置)