diff 命令详解与参数实测(Ubuntu 22.04)

从 1c2/<、---、> 逐段解读到 -u 的 @@ hunk,详解 GNU diff 的 normal、unified、context、并排四种输出格式与递归目录比较、忽略类参数、--color 色码及 0/1/2 退出码语义

Linux命令Linux系统UbuntuUbuntu 22.04命令行文本处理2026/10/15

diff 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 GNU diff 的逐行比较能力——默认 normal 格式(1c2/</---/>)逐段解读、-u unified 格式(@@ -l,n +l,n @@)、-c context 格式、-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 行为)。

六、参考链接