跨平台使用文本文件时的陷阱

问题主要发生于将 windows 下的文本(源代码、脚本等)拷贝到 Linux 平台;反之,因为 windows 的“主动”,其开发工具、编辑器会强制转换,所以一般不会产生问题。

  • gcc 编译报错让我认识到,utf8 格式的文本还分带 BOM 头,不带 BOM 头;
  • vim 启动有问题认识到换行、回车到现在还在影响着跨平台;

字符集和字符编码

ansi 体系和 unicode

参考:Unicode 字符集和多字节字符集关系

ansi 机构最初的 ascii 字符集,后来各国各自扩充,中国大陆和新加坡等地区使用本地编码是 GB2312 或 GBK,中国港台地区使用的本地编码是 BIG5,韩国和日本的本地编码分别是 euc-kr 和 Shift_JIS。这些从 ANSI 标准派生的字符集被习惯的统称为 ANSI 字符集,它们正式的名称应该是 MBCS(Multi-Byte Chactacter System,即多字节字符系统)。ansi 体系,字符集一般只对应一种字符编码。

再后来,为了大一统,出现了 unicode 字符集。

mbsc 和宽字节

参考:单字节字符集,多字节字符集,Unicode

ASCII 是 SBCS。一个字节表示的 0 用来标志 SBCS 字符串的结束。

DBCS 字符串的结束标志也是一个单字节表示的 0

Unicode 字符串使用两个字节表示的 0 作为它的结束标志。

  • 多字节(mbsc)包括 sbsc 和 dbsc 等。ascii 基本代表了 sbsc。ansi 和 mbsc 基本算同义词。
  • 宽字节,unicode 基本代表了宽字节。

BOM 概念

BOM,就是 utf8-bom 中的 bom。

字节顺序标记(英语:byte-order mark,BOM)是位于码点 U+FEFF 的统一码字符的名称。统一码中,值为 U+FFFE 的码位被保证将不会被指定成一个统一码字符。Unicode 的编码点是唯一的,但表达方式(存储方式)多样。表达方式涉及 utf8,utf16 等;存储方式除了前者还涉及 字节顺序。在 UTF-16 中:

  • 大尾序存储形式:数值的低有效位存储在存储地址高的位置。即 0xFE,0xFF
  • 小尾序存储形式:数值的低有效位存储在存储地址低的位置。即 0xFF,0xFE

UTF-8 是否应该携有 BOM 是历史问题,不做讨论。当其携有 BOM 时,按照其 编码方式,码点 U+FEFF(1111,1110,1111,1111) 会被存储为三个字节 1110(1111),10(111011),10(111111),即 0xef,0xbb,0xbf。虽然携有 BOM,但

它只用来标示一个 UTF-8 的文件,而不用来说明字节顺序

另外,在 c++11 起,新增了两个 转义字符

  • \unnnn, 通用字符名(任意 Unicode 值)可能生成多个字符,表示编码点 U+nnnn
  • \Unnnnnnnn,通用字符名(任意 Unicode 值)可能生成多个字符,表示编码点 U+nnnnnnnn

因为 BMP 基本多文种平面 基本包含了我们目前接触到的所有字符,所以 \U 大写的转移字符一般是用不到的。

由此,如果我们想通过 C++ 的输出流创建 utf8-bom 文件并写入 niel水 有多样的代码可选:

1
2
3
4
5
// UTF-8 data with BOM
std::ofstream("text.txt") << u8"\ufeff" << u8"niel\u6c34";
// or
std::ofstream("text.txt") << "\xef\xbb\xbf" << u8"niel水";
// 两者并无本质区别:我们优先做了转码工作或者交给预处理器去做

error: stray ‘\357’ in program

在 linux 上某次编译时老是报错,错误信息如下:

1
2
3
4
5
$ g++ -I../../include unit_test.cpp -o unit_test
unit_test.cpp:1: 错误: 程序中有游离的'\357'
unit_test.cpp:1: 错误: 程序中有游离的'\273'
unit_test.cpp:1: 错误: 程序中有游离的'\277'
In file included from unit_test.cpp:63:

或在英文系统下:

1
2
3
4
5
$ g++ -I../../include unit_test.cpp -o unit_test
unit_test.cpp:1: error: stray '\357' in program
unit_test.cpp:1: error: stray '\273' in program
unit_test.cpp:1: error: stray '\277' in program
In file included from unit_test.cpp:63:

\357\273\277 (八进制)就是 EF BB BF(十六进制),这是 utf8 格式文本文件的 BOM 头。so……

产生原因:

文本文件(源代码文件 .cpp .h 等也是文本文件)的编码格式各种各样,没有明确的区分。而一些浏览文本文件的软件大多是用猜测的算法来区分这些编码,这里涉及内容很多,不多说。windows 下为了区分 UTF-8 编码格式,在以 UTF-8 编码的文本文件前写入三个字节的标志(0xef 0xbb 0xbf)来区分 UTF-8 编码的文本文件,也就是带 BOM 的 UTF-8。而 linux 下的一些编译器不识别 BOM,所以就会报错。

一般在 windows 下的文件都存成 ansi 格式,为了在 linux 下能通用,建议保存成 UTF-8 不带 BOM 编码格式,因为目前 gcc 和 g++ 不支持 UTF-8 带 BOM 编码格式。

延伸阅读:UTF8最好不要带BOM,附许多经典评论 (很值得一看)

解决方法:UTF-8编码中BOM的检测与删除 【需测试,验证…】

学习 od 命令

如何判断文件是否是使用 UTF-8 BOM 存储的?执行下面的命令:

1
2
3
$ cat unit_test.cpp |hd -n 10
00000000 ef bb bf 2f 2a 2a 2a 2a 2a 2a |.../******|
0000000a

ps: hd 命令在 13x 系列服务器上不存在,在 Debian8 中有。 猜测应该是 hexdump 命令??

回车、换行是两个字符

虽然很早就意识到回车、换行的区别,在不同平台上不一致。但在新的系统、新的应用中,这个问题一般会被“抹掉”,不会再暴露出来。但在 N 年前的老机子就得注意了!

回车和换行

Not an editor command: ^M

将 windows 下的 vim 配置文件 _vimrc 拷贝到 mac 下,重命名为 .vimrc,本指望实现共用配置文件。但在启动 vim 时却报了以下错误:

1
2
3
4
E492: Not an editor command: ^M
E488: Trailing characters: nocompatible^M
E15: Invalid expression: has("syntax")^M
E171: Missing :endif

报错信息

从网上搜到的相关问题的解决办法: vim 替换^M

使用 dos2unix 命令。一般的分发版本中都带有这个小工具(如果没有可以根据下面的连接去下载),使用起来很方便:$ dos2unix myfile.txt

上面的命令会去掉行尾的^M。

执行成功。推荐。

脚本莫名其妙的打印

2015年11月4日 16:19:54

先说结论:windows 下编辑的文件拿到 linux 下用,可以,但要谨慎,反之亦然。出现问题时先排除是不是回车、换行的问题。

以前接触 linux 很少,shell 更是一点都不了解。今天调试一个项目,需要运行 sh 文件。可是太蛋疼了,详述如下:

在 suse11 的环境(公司 10.1.72.57 的服务器上)下,运行 bug.sh 和 normal.sh,两者显示内容一致,如下

1
2
3
4
#!/bin/bash
SRCPATH="hello"
rm -f bench_test
echo ${SRCPATH}"CAT"

运行结果却大相径庭

Linux 下执行结果

而在 windows 系统下,安装的 Git Bash Here 窗口中运行,结果一致

Windows 下执行结果

在 UltraCompare 中使用二进制窗口看出最终的区别,也验证了猜想

二进制形式查看

0x0d 0x0a0x0a 的区别,是 windows 和 linux 的区别,当文件从 windows 拿到 linux 时,我们无法保证正在使用的 linux 版本在兼容性方面做得完美。比如 git 虽然是 linux 的背景,做的就很好;而 suse 显然在这方面尚有不足,让人在没有这方面意识、没有针对性的前提下,浪费大量时间,无从把握。

windows 系统下,回车是由两个字符构成的,0x0d0x0a

名称 代码 ASCII码 十六进制 备注
回车 CR \r 0x0d 回车的作用只是移动光标至该行的起始位置;
换行 LF \n 0x0a 换行至下一行行首起始位置;

在键盘上敲下回车键,在不同软件下获得字符大有不同。Windows 下在 txt 文件中敲下回车键,然后十六进制进制观察,你会发现获得了 2 个字符,0x0d0x0a,这个大家都知道,但这不意味着,在任何情况下敲下回车键,都会获得 0x0d0x0a。在 linux 下,你对一个文件,敲下回车键,你就会发现,它每次只增加一个字符 0x0a

延伸阅读:回车 换行 0x0D 0x0A CR LF \r \n的来龙去脉