该死的 ^M
^M
,神奇的字符!相信很多人写 Shell 脚本的时候都被这个字符坑过,我自己也至少被坑过两次。最近周围的好几个小伙伴又被 ^M
坑,花了好几个小时检查脚本的错误,结果发现是 ^M
导致的。所以写了这篇文章讲一下什么是 ^M
,当 ^M
出现的时候一般会伴随着什么样的现象,出现了我们可以用什么手段去解决。
^M
是何方神圣
这个得先从 Windows 和 Unix 下的换行符开始说起,在我的 Intellij IDEA 的右下方的状态栏上,有一块是展示当前文件的换行符的:
可以看到在 Windows 下,换行符是 \r\n
,在 Unix 下换行符是 \n
。如果我们用把一个文件的换行符换成 Windows 的换行符,那么当我们用 cat -v
来看的时候,就可以看到:
实际上 ^M
就是 Windows 下的换行符中的 \r
部分。因为 Unix 下的换行符是 \n
,所以当一个用 Windows 下的换行符的文件放在 Unix 下的时候,单行的最后一个字符就变成了 \r
,\r
在 ASCII 码中是 0xD
,而 0xD
在 VIM 和 cat -v
则刚好被显示为 ^M
。
刚才之所以用
cat -v
而不用普通的cat
是因为^M
是不可见的字符,如果仅仅用cat
,是看不到这个字符的。cat
的-v
参数的作用就是显示不可打印的字符。
^M
会导致什么样的问题?
我们已经知道了 ^M
实际上就是 \r
,而 \r
是回车符(Carriage Return),回车符的作用是将设备的位置重置到当前行的开头。
知道了 \r
的作用时候,我们来看一个现象:
### 有一个普通文件,存放了一个路径,当前行的最后以 ^M 结尾
$ cat -v Main
/home/admin/khotyn.huangt/test/^M
### Echo 一下,神奇了!
$ echo "`cat Main`/where am i"
/where am i/khotyn.huangt/test/
看到后面那个 echo 命令,它将 Main 文件中的内容提取出来,再在后面加上 /where am i
这个字符串,结果我们看到,/where am i
在打印的结果中跑到最前面去了,这正是 \r
这个字符的作用,因为 cat Main
的执行的结果的最后一个字符是 \r
,所以一遇到这个字符,设备指针就直接回到了当前行的开头,所以 \r
后面的 /where am i
就直接显示在了最前面。
所以,当你看到什么奇怪的路径,这个路径中莫名其妙地少了一些字符,出现了一些莫名其妙的字符串的,很可能就是 ^M
导致的。
如何逃离 ^M
的魔掌
当你发现了 ^M
导致的问题的时候,最直截了当的方式就是将 ^M
从文件中去掉。
一、临时解决的几个方法
如果的机器上安装有 dos2unix,那么恭喜你,直接运行
dos2unix /path/to/file
就可以将一个文件的换行符从 Windows 的转换成 Unix 的。
但是,如果机器上没有装 dos2unix,而你又没法装上去(在一家公司工作总是会有各种各样的让你感觉很丧气的权限控制),那么你可以用 sed 来替换:
sed --in-place='' 's/^M//g' /path/to/file
注意:上面的
^M
只是显示的效果,输入的时候需要用组合键输入,先Ctrl + V
,然后马上Ctrl + M
就可以在终端中输入^M
了。
当然,用 tr
之类的命令也可以,不过我一般用 sed
的原因是 sed
加上 --in-place
参数可以做到直接替换原文件,而不用产生临时的文件(危险而高效的操作)。
二、预防此问题
不过,前面说的只是当出现问题的时候如何解决,那么如何预防这个问题呢?
第一个方法当然是直接放大招,换个 Mac 啥的,或者把你的机器上的 Windows 格了,装个 Ubuntu 也好啊。真心觉得 Windows 对于程序员来说真的没有啥好处(我好想听说连微软都开源 .Net 了,并且会提供多平台的支持)。
第二个方法嘛,当在 Windows 下使用各种编辑器的时候,尽量将换行符设置成 Unix 的换行符。不要偷懒用 Windows 的换行符,出现了问题就是好几个小时的排查时间。(目前没有发现有什么场景下有必须用到 Windows 下的换行符的,如果有同学知道有这样的场景的话,不吝赐教)。
三、防止被别人坑
虽然我个人觉得不应该用 Windows,不过还是有同学的确是喜欢用,或者因为不可抗拒的因素而暂时在使用,为了防止出现这个问题,可以在版本管理软件上做控制,比如 Git 就可以设置换行符,当你提交文件的时候,可以将你的所有文本的换行符替换成你设定的换行符,详细可以看 https://help.github.com/articles/dealing-with-line-endings/