问题

当我们使用 Windows 桌面下的编辑器编写一个 Shell 文件时,很容易将文件使用的换行符保存为 dos 格式。如果将文件上传到 Linux 服务器执行时,可能会遇到下面的错误。这是因为

# 显示一个简单的shell文件
$ cat dosnewline.sh                                   
#!/bin/sh

echo "This is a file with dos newline"


# 该文件使用了 dos 格式的换行符
$ od -bc dosnewline.sh
0000000   043 041 057 142 151 156 057 163 150 015 012 015 012 145 143 150
           #   !   /   b   i   n   /   s   h  \r  \n  \r  \n   e   c   h
0000020   157 040 042 124 150 151 163 040 151 163 040 141 040 146 151 154
           o       "   T   h   i   s       i   s       a       f   i   l
0000040   145 040 167 151 164 150 040 144 157 163 040 156 145 167 154 151
           e       w   i   t   h       d   o   s       n   e   w   l   i
0000060   156 145 042 015 012 015 012 015 012                            
           n   e   "  \r  \n  \r  \n  \r  \n                            
0000071
# 使用 sh 执行的时候就会有一个报错
$ h dosnewline.sh    
: command not found 2: 
This is a file with dos newline
: command not found 4: 
: command not found 5: 
# 获取脚本的返回码也不是0,在一些自动化调用的场景中就会认为脚本执行失败,从而引发后续的问题
$ echo $?             
127
# 退出码 127 的意思是 command not foud,对应具体的 dos 换行符所在的行

换行符

我们通常所说的换行符在 ASCII 码表中对应下面两个字符。

十进制

十六进制

字符

编程时

10

A

LF(Line feed,New Line)

\n

13

D

CR(Carriage return)

\r

这两个字符被用作换行的标志,但是在不同操作系统中使用的不一样,具体如下:

操作系统

换行符

Unix(包括 Linux)

\n

Windows

\r\n

MacOS X 之前的版本

\r

MacOS X 及之后的版本

\n

为什么 Windows 中会用两个字符而其他系统使用一个字符呢?

据说很久以前,人们在使用老式电传打字机作为输入设备的年代,这种设备内部使用两个字符来另起新行。一个字符把滑动架移回首位 (称为回车),另一个字符把纸上移一行 (称为换行)。

当电子计算机问世后,由于存储器曾经非常昂贵。有些人认定没必要用两个字符来表示行尾。于是 UNIX 开发者决定他们可以用一个字符(LF)来表示行尾,Apple 开发者规定了用 (CR)来表示行尾,而 MS-DOS(以及后来的 Windows)开发人员则沿用了老式的两个字符 。

正是因为不同操作系统默认的换行符不同,导致在 Windows 下编写的文件采用了 Windows 下的换行符。而不幸的是 sh 做为 Linux 下的应用,只认识 Unix(包括 Linux)下的换行符,引发的文章开头的问题。

解决方法

解决的方法有很多,从脚本来源上说,最好我们在编辑过程中就指定使用的换行符,大多数编码常用编辑器例如 Notepadd++ 等都支持这个选项,如下图在 Notepadd++ 的右下角会显示换行符的类型。千万不要使用 Windows 自带的记事本来编写 shell 脚本,记事本是不支持调整换行符的。

文件格式引起的脚本执行错误_bc

除了在编写阶段注意,脚本编写完成后,还可以通过 $ sh -x hello.sh 的方式来检查脚本是否有语法错误,对于本文提供的示例来说输出结果如下,可以看到输出结果给出提示多了 \r 的字符。

$ sh -x dosnewline.sh 
+ $'\r'
: command not found 2: 
' echo 'This is a file with dos newline
This is a file with dos newline
+ $'\r'
: command not found 4: 
+ $'\r'
: command not found 5:

最后如果不小心,这样的脚本已经进入了生产环境,也还有很多的方法来进行修改。很多文章推荐使用 dos2unix 这个命令来快速修改,这个命令使用起来比较方便,但是对于一些生产环境管理严格的单位来说,这个命令未必允许在生产环境安装。

那就还可以用一般都有的 trawksed 命令来实现,下面给出具体示例。

$ tr -d '\r' < dosnewline.sh > dosnewline.sh-tr
# 使用 od 比较两个文件,后续的脚本可类似方式比较
$ od -bc dosnewline.sh-tr 
0000000   043 041 057 142 151 156 057 163 150 012 012 145 143 150 157 040
           #   !   /   b   i   n   /   s   h  \n  \n   e   c   h   o    
0000020   042 124 150 151 163 040 151 163 040 141 040 146 151 154 145 040
           "   T   h   i   s       i   s       a       f   i   l   e    
0000040   167 151 164 150 040 144 157 163 040 156 145 167 154 151 156 145
           w   i   t   h       d   o   s       n   e   w   l   i   n   e
0000060   042 012 012 012                                                
           "  \n  \n  \n                                                
0000064
$ od -bc dosnewline.sh   
0000000   043 041 057 142 151 156 057 163 150 015 012 015 012 145 143 150
           #   !   /   b   i   n   /   s   h  \r  \n  \r  \n   e   c   h
0000020   157 040 042 124 150 151 163 040 151 163 040 141 040 146 151 154
           o       "   T   h   i   s       i   s       a       f   i   l
0000040   145 040 167 151 164 150 040 144 157 163 040 156 145 167 154 151
           e       w   i   t   h       d   o   s       n   e   w   l   i
0000060   156 145 042 015 012 015 012 015 012                            
           n   e   "  \r  \n  \r  \n  \r  \n                            
0000071
$ awk '{ sub("\r$", ""); print }' dosnewline.sh > dosnewline.sh-awk 
$ sed 's/\r//' dosnewline.sh > dosnewline.sh-sed

这篇文章首发在我的个人站点 大江小浪 上,更多内容,欢迎访问。