有时打开两个文件,一个字都不差,diff 却说所有行都改了,或者只有第一行、最后一行不同。原因通常是屏幕上看不见的字符。本文整理了这类“看不见的差异”的种类及解决方法。
换行符:CRLF、LF、CR
不同操作系统换行的惯例不同。
| 写法 | 字节 | 主要使用场景 |
|---|---|---|
| LF | 0A (\n) | Linux、macOS、大多数开发工具 |
| CRLF | 0D 0A (\r\n) | Windows 记事本等许多程序,电子邮件、HTTP 等互联网协议 |
| CR | 0D (\r) | 非常老的 Mac OS(版本 9 以前) |
如果一个文件以 CRLF 保存、另一个以 LF 保存,按行比较时每一行末尾那个看不见的 \r 都会被当成差异,看起来就像整个文件都改了。GNU diff 可以用 --strip-trailing-cr、git diff 可以用 --ignore-cr-at-eol 忽略这种差异。
文件末尾的换行符
按照 POSIX 惯例,文本文件的每一行都以换行符结尾。因此很多工具会在最后一行后面也加上换行符,但有些编辑器不加。这种差异在 unified diff 中显示如下:
@@ -1,2 +1,2 @@ host=example.com-port=8080\ No newline at end of file+port=8080
\ No newline at end of file 表示紧挨着的上一行末尾没有换行符。上例中内容相同,只是修改后的文件多了结尾换行符。许多代码风格规范要求文件以换行符结尾,可以通过编辑器的“在文件末尾插入换行符”设置来统一。
BOM:只有第一行不同时
UTF-8 文件开头有时会带有 EF BB BF 三个字节,这称为字节顺序标记(BOM)。UTF-8 本身不需要字节顺序,但有些程序会加上它作为“此文件为 UTF-8”的标记。BOM 在屏幕上看不见,所以如果结果显示只有第一行不同,就该怀疑是 BOM。本站在载入文件时会去掉开头的 BOM;开启忽略空白选项后,粘贴文本中的 BOM(U+FEFF)也会被当作空白处理。
制表符、空格与特殊空白
- 制表符与空格:一个制表符和四个空格在屏幕上看起来一样,却是不同的字符。
- 不换行空格(NBSP,U+00A0):经常混在从网页或文字处理软件复制来的文本中。
- 全角空格(U+3000):可能由中文、日文、韩文输入法输入。
- 零宽空格(U+200B):宽度为 0,完全看不见,但会被算作一个字符。
开启忽略空白选项后,这些字符大多会被过滤掉(不过像 U+200B 这样不属于空白字符类别的字符,即使开启选项也可能保留)。把行内比较切换为字符,就能通过高亮找到看不见的字符在什么位置。
Unicode 规范化:同一个字,不同的字节
带声调的拼音字母“é”,既可以写成一个预组合字符(U+00E9),也可以写成“e”加上组合用尖音符(U+0301)。韩文“가”同样既可以是一个完整字符(U+AC00),也可以由辅音 ㄱ(U+1100)和元音 ㅏ(U+1161)组合而成。Unicode 把前者称为 NFC 规范化形式,后者称为 NFD(UAX #15)。两段文本在屏幕上看起来完全一样,比较时却是不同的字符。在 macOS 上创建的文件名到了其他操作系统上,带声调字母或韩文字母被拆开显示,就是典型的例子。解决办法是在比较前把一侧转换为 NFC,使两边一致。
在 git 中统一换行符
如果仓库由多个操作系统的用户共同使用,可以用 git 设置统一换行符。
core.autocrlf:设为true时,检出时把 LF 转为 CRLF,提交时把 CRLF 转为 LF(主要用于 Windows)。设为input时只在提交时把 CRLF 转为 LF。.gitattributes:按仓库规定规则,因此即使每个人的设置不同,结果也一致。
* text=auto
*.sh text eol=lf
*.bat text eol=crlf
text=auto 会把 git 判定为文本的文件在仓库内的换行符规范化为 LF,eol=lf、eol=crlf 则指定检出到工作目录时使用的换行符。
在本工具中试一试
文本对比工具 的统一换行符默认开启,会把 CRLF、CR、LF 都视为同一种换行,并忽略文件末尾是否有换行符。关闭该选项后,行尾的 CR 显示为 ␍,没有结尾换行符的最后一行标记为 ∅,就能看到原本看不见的差异。空白相关选项整理在 忽略空白选项 中,结果中各符号的含义整理在 unified diff 阅读指南 中。