那天项目部署的时候,遇到一个很诡异的现象,一度怀疑自己职业生涯到此结束了,后面经排除发现,原来是
Linux
和Windows
下shell
脚本换行符的格式问题,导致脚本一直执行不了 …(到此,标题所说的事故讲完了 …:)看起来很蠢的问题,溜了 :) … 但莎士比亚说过,解决一个问题最好的方式,就是下次不要再犯了。所以我们刨根问底儿,彻底搞清楚换行符在计算机系统中到底是个啥玩意儿。
历史来源
在计算机出现之前,有一种通信设备叫电传打字机(Teletype Mode),每秒输出 10 个字符,平均每个字符 0.1 秒,但有个问题,就是打完一行换行的时候,要消耗 0.2 秒,在这个时间段内,如果有新字符传来,就将丢失,所以有人为了解决这个问题,在每一行后面加两个字符表示该行结束。
那这里就涉及到两个动作:
- 将打印探头(就如上图的那个刻度上的头子)从右边拉回到左边,这就是回车(carriage return)
- 拉回到左边还不够,还要将探头换行(相对应的就是将纸往上吐一行的距离),这就是换行(line feed)
后面这个概念就被搬到了计算机系统上,But,有人的地方就有分歧,各大系统开始秀了。
-
Windows
系统里,每行结尾是回车 + 换行(和上述提到的两个动作一致),\r\n
; -
Unix
系统,每行结尾只有换行\n
; -
Mac
系统,每行结尾只有换行\n
;
PS:老
MAC
系统用的\r
,现在的MAC
都是用的\n
,和Unix
一致了。
不同系统之间的换行规则的不同,导致不同系统下的文件交叉使用的时候,存在不一致,比如最常见的,Unix/Mac
系统下的文件在 Windows
里打开,所有文字会变成一行,原因显而易见。
ASCII 码
二进制 | 十进制 | 十六进制 | 字符 / 缩写 | 解释 |
---|---|---|---|---|
00001001 | 9 | 09 | HT (Horizontal Tab) | 水平制表符 |
00001010 | 10 | 0A | LF/NL(Line Feed/New Line) | 换行键 |
00001011 | 11 | 0B | VT (Vertical Tab) | 垂直制表符 |
00001100 | 12 | 0C | FF/NP (Form Feed/New Page) | 换页键 |
00001101 | 13 | 0D | CR (Carriage Return) | 回车键 |
00001110 | 14 | 0E | SO (Shift Out) | 不用切换 |
实践见真章
我们一步步来实践下上述说的,测试环境是
Windows
和Linux
。
-
Windows
下新建个win.txt
文件,写一句大白话。上面有听讲的同学应该知道,这里的换行符为CRLF
。
talk is cheap,
show me your code.
- 在
Linux
系统用vim
打开刚新建的文件
这看起来不是挺正常的么?和 Windows
上显示的一样啊。注意这里 vim
查看文件的时候,会检测换行符,如果所有的换行符都是 CRLF
,那么它会自动以dos
格式来显示文本内容,最下面 [dos]
里也体现了这一点。
dos
(Disk Operating System 磁盘操作系统)和Windows
一样采用的是CRLF
- 使用
cat -A
选项查看文本所有的字符
可以看到多了 ^M$
,^M
这是 Linux
等系统下规定的特殊标记,占一个字符大小,不是 ^
和M
的组合,只能用 Ctrl+v,Ctrl+m
按出来;而 $
不是换行符,可以理解为 Linux
下用来表示文本结束 EOF 的符号。
- 使用
cat -v
选项显示出非打印字符
- 把第一行的回车符去掉再看看
sed -i '1s/^M//' win.txt
看到第一行的 ^M
不见了,第二行还是保留。
- 再重复第一步看看变化
结果展示 vim
中多了 ^M
这个符号,左下角也没了 [dos]
的标识,这里回应了第二点提到的现象,表示 vim
用Linux
来显示文本内容。
到这一步为止,我们证实了 Windows
和Linux
环境下不同换行规则带来的差异。
验证下回车符的真实存在
看到社区里的小伙伴做了这个尝试,便参考着做了下,可以直观体现什么是“回车”。
- 还原上述的例子,
cat -A
查看所有字符 -
sed -i '1s/.*/& ypm/g' win.txt
在第一行末尾加上ypm
- 再次查看,我们发现上面执行的命令用
.*
匹配整行的时候,不包括换行符^M
,所以ypm
加在了第一行的末尾 - 用
cat
正常查看文本的时候,发现一件奇怪的事情,ypm
覆盖了talk
,怎么解释?
我们要知道
cat
普通模式下输出文本内容,会将^M
理解为回车。
这就可以解释了,遇到回车符,就像打印探头从右回到了左, ypm
这里的四个字符,刚好覆盖了 talk
四个字符。这就直观解释了什么是回车。
如何规避
谈到如何规避这个差异,其实不同方向上有不同方法,比如针对^M
、强制转换文本格式,但个人觉得,能解决其根本问题的,还是在不同系统间,代码编辑的时候,注意到这个格式问题,各大 IDE 都有自己的解决方案。
- 去除回车符号
cat -v win.txt | tr -d '^M' > linux.txt
或者
cat win.txt |tr -d '\015' > linux.txt
或者
cat win.txt |tr -d '\r' > linux.txt
vim 编辑器中输入
:%s/^M//g
或者
:set fileformat=unix
- 终端命令转换
dos2unix win.txt
总结
- 回车符
\r
:CR(carriage return) - 换行符
\n
:LF(line feed) -
Windows
系统遵循最原始的规则,即必须满足回车符 + 换行符,缺少或者顺序调换都不可,即\r\n
-
Unix
系统中遇到换行符\n
就会进行回车 + 换行的操作,而回车符\r
会作为特殊字符^M
显示
没想到这么简单的问题,扯了这么多,其实很多小伙伴也都遇到过这个问题,社区里也经常有因为换行符导致的“血案”,希望没有因此酿成之前那个程序猿小哥手误导致的几千万的损失。在运维、DB 等领域,这些“小问题”可能会被放大,因此还是需要引起重视,虽然有很多规避手段,但在代码编写的初期,就应该养成习惯关注到这一点,排查问题的时候这同样是一个方向。
PS: 可能会有不严谨的地方,也欢迎大家讨论,轻喷 …
参考
http://www.ruanyifeng.com/blog/2006/04/post_213.html
https://www.cnblogs.com/linuxnote/p/3753153.html
https://blog.csdn.net/zhangguangyi888/article/details/8159601