檔案格式引起的指令碼執行錯誤

2023-02-15 21:00:17

問題

當我們使用 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 指令碼,記事本是不支援調整換行符的。

除了在編寫階段注意,指令碼編寫完成後,還可以通過 $ 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 

這篇文章首發在我的個人站點 大江小浪 上,更多內容,歡迎存取。