在 Linux 中像 MSYS2 一樣處理帶有 CRLF(回車符)的 Bash 腳本?

在 Linux 中像 MSYS2 一樣處理帶有 CRLF(回車符)的 Bash 腳本?

假設我有以下簡單的腳本tmp.sh

echo "testing"
stat .
echo "testing again"

儘管它很簡單,但它以\r\n(即 CRLF,即回車+換行)作為行結尾。由於網頁不會保留行結尾,因此這裡有一個十六進制轉儲:

$ hexdump -C tmp.sh 
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0d 0a  |echo "testing"..|
00000010  73 74 61 74 20 2e 0d 0a  65 63 68 6f 20 22 74 65  |stat ...echo "te|
00000020  73 74 69 6e 67 20 61 67  61 69 6e 22 0d 0a        |sting again"..|
0000002e

現在,它具有 CRLF 行結尾,因為該腳本是在 Windows 上的 MSYS2 下啟動和開發的。因此,當我在 Windows 10 上的 MSYS2 中執行它時,我得到了預期的結果:

$ bash tmp.sh
testing
  File: .
  Size: 0               Blocks: 40         IO Block: 65536  directory
Device: 8e8b98b6h/2391513270d   Inode: 281474976761067  Links: 1
Access: (0755/drwxr-xr-x)  Uid: (197609/      USER)   Gid: (197121/    None)
Access: 2020-04-03 10:42:53.210292000 +0200
Modify: 2020-04-03 10:42:53.210292000 +0200
Change: 2020-04-03 10:42:53.210292000 +0200
 Birth: 2019-02-07 13:22:11.496069300 +0100
testing again

但是,如果我將此腳本複製到 Ubuntu 18.04 電腦並在那裡運行它,我會得到其他內容:

$ bash tmp.sh
testing
stat: cannot stat '.'$'\r': No such file or directory
testing again

在具有相同行結尾的其他腳本中,我在 Ubuntu bash 中也遇到了此錯誤:

line 6: $'\r': command not found

……可能來自空行。

所以,很明顯,Ubuntu 中的某些東西在回車時會被卡住。我見過BASH 和回車行為

它與 Bash 沒有任何關係: \r 和 \n 由終端解釋,而不是由 Bash 解釋

……但是,我想這僅適用於在命令列上逐字輸入的內容;這裡的\r\n已經在腳本本身中輸入了,所以 Bash 一定會解釋這裡\r的。

這是 Ubuntu 中 Bash 的版本:

$ bash --version
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)

……這裡是 MSYS2 中的 Bash 版本:

$ bash --version
GNU bash, version 4.4.23(2)-release (x86_64-pc-msys)

(看起來他們的差距並沒有那麼大…)

無論如何,我的問題是 - 有沒有辦法說服 Ubuntu/Linux 上的 Bash 忽略\r,而不是試圖將其解釋為(可以這麼說)“可打印字符”(在本例中,意味著一個可能是有效命令的一部分,bash 如此解釋)?編輯:沒有必須轉換腳本本身(所以它保持不變,帶有 CRLF 行結尾,如果以這種方式檢查,例如在 git 中)

EDIT2:我更喜歡這種方式,因為與我一起工作的其他人可能會在 Windows 文字編輯器中重新開啟腳本,可能會\r\n再次重新引入腳本並提交它;然後我們可能會得到無休止的提交流,這可能只不過是污染存儲庫的\r\n轉換\n

編輯2:@Kusalananda 在評論中提到dos2unixsudo apt install dos2unix);請注意,只需這樣寫:

$ dos2unix tmp.sh 
dos2unix: converting file tmp.sh to Unix format...

……將就地轉換文件;要將其輸出到 stdout,必須設定 stdin 重定向:

$ dos2unix <tmp.sh | hexdump -C
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0a 73  |echo "testing".s|
00000010  74 61 74 20 2e 0a 65 63  68 6f 20 22 74 65 73 74  |tat ..echo "test|
00000020  69 6e 67 20 61 67 61 69  6e 22 0a                 |ing again".|
0000002b

……然後,原則上,可以在 Ubuntu 上運行它,這似乎在這種情況下有效:

$ dos2unix <tmp.sh | bash
testing
  File: .
  Size: 20480       Blocks: 40         IO Block: 4096   directory
Device: 816h/2070d  Inode: 1572865     Links: 27
Access: (1777/drwxrwxrwt)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2020-04-03 11:11:00.309160050 +0200
Modify: 2020-04-03 11:10:58.349139481 +0200
Change: 2020-04-03 11:10:58.349139481 +0200
 Birth: -
testing again

然而,除了需要記住的稍微混亂的命令之外,這也改變了 bash 語義,因為 stdin 不再是終端;這可能適用於這個簡單的例子,但請參閱例如https://stackoverflow.com/questions/23257247/pipe-a-script-into-bash例如更大的問題。

答案1

據我所知,沒有辦法告訴 Bash 接受 Windows 風格的行結尾。

在涉及 Windows 的情況下,常見的做法是依靠 Git 在提交時使用autocrlf配置標誌自動轉換行結尾的功能。請參閱範例GitHub 有關行結尾的文檔,這不是 GitHub 特有的。這樣,文件就會在儲存庫中以 Unix 風格的行結尾提交,並根據每個客戶端平台進行適當的轉換。

(相反的問題不是問題:MSYS2 在 Windows 上可以很好地處理 Unix 風格的行結尾。)

答案2

你應該使用binfmt_misc為此[1]。

首先,定義一個處理以 開頭的檔案的 magic #! /bin/bash<CR><LF>,然後為其建立一個可執行解釋器。解釋器可以是另一個腳本:

INTERP=/path/to/bash-crlf

echo ",bash-crlf,M,,#! /bin/bash\x0d\x0a,,$INTERP," > /proc/sys/fs/binfmt_misc/register
cat > "$INTERP" <<'EOT'; chmod 755 "$INTERP"
#! /bin/bash
script=$1; shift; exec bash <(sed 's/\r$//' "$script") "$@"
EOT

測試一下:

$ printf '%s\r\n' '#! /bin/bash' pwd >/tmp/foo; chmod 755 /tmp/foo
$ cat -v /tmp/foo
#! /bin/bash^M
pwd^M
$ /tmp/foo
/tmp

範例解釋器有兩個問題:1.由於它通過不可查找的文件(管道)傳遞腳本,bash 將逐字節讀取它,效率非常低,並且2.任何錯誤訊息都將引用/dev/fd/63或類似的名稱而不是原始腳本的名稱。

[1] 當然,您可以建立一個/bin/bash^M指向解釋器的符號鏈接,而不是使用 binfmt_misc,這也適用於 OpenBSD 等其他系統:

ln -s /path/to/bash-crlf $'/bin/bash\r'

但在 Linux 上,shebanged 執行檔比 binfmt_misc 沒有任何優勢,並且將垃圾放入系統目錄中並不是正確的策略,並且會讓任何系統管理員搖頭;-)

答案3

好的,我找到了一些解決方法,透過:

“聯結”符號鏈接

現代 UNIX 系統有一種方法可以使任意資料顯示為文件,而與儲存方式無關:保險絲。使用 FUSE,對檔案的每個操作(建立、開啟、讀取、寫入、列出目錄等)都會呼叫程式中的某些程式碼,並且程式碼可以執行您想要的任何操作。看創建一個實際上是命令的虛擬文件。你可以嘗試一下腳本檔案系統或者熔絲,或者如果您雄心勃勃,也可以自己動手。

... 和創建一個實際上是命令的虛擬文件

您可能正在尋找命名管道

因此,方法是:建立一個命名管道,向其dos2unix輸出,然後呼叫bash該命名管道。

這裡我有原始的tmp.shCRLF 行結尾為/tmp;首先,讓我們建立命名管道:

tmp$ mkfifo ftmp.sh

現在,如果您執行以下命令:

tmp$ dos2unix <tmp.sh >ftmp.sh

……你會注意到它會阻塞;如果你這樣做了,請說:

~$ cat /tmp/ftmp.sh | hexdump -C
00000000  65 63 68 6f 20 22 74 65  73 74 69 6e 67 22 0a 73  |echo "testing".s|
00000010  74 61 74 20 2e 0a 65 63  68 6f 20 22 74 65 73 74  |tat ..echo "test|
00000020  69 6e 67 20 61 67 61 69  6e 22 0a                 |ing again".|
0000002b

....您會注意到轉換已完成 - 並且在命令cat運行完畢後,dos2unix <tmp.sh >ftmp.sh先前阻止的命令已退出。

dos2unix因此,我們可以在「無限」while 循環中設定對命名管道的寫入:

tmp$ while [ 1 ] ; do dos2unix <tmp.sh >ftmp.sh ; done

……即使它是一個「緊密」循環,也不應該成為問題,因為大多數時候在 while 循環內的指令是阻塞的。

然後我可以這樣做:

~$ bash /tmp/ftmp.sh
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
Device: 801h/2049d  Inode: 5276132     Links: 7
...
testing again
$

...顯然,腳本運作良好。

這種方法的好處是我可以tmp.sh在文字編輯器中開啟原始文件;並編寫新程式碼 - 以 CRLF 結尾 - 然後儲存tmp.sh;在Linux下運行bash /tmp/ftmp.sh將運行最新保存的版本。

這樣做的問題是,像read -p "Enter user: " user這樣依賴實際終端機 stdin 的命令將會失敗;或者更確切地說,不要失敗,但如果你嘗試,請說成/tmp/tmp.sh

echo "testing"
stat .
echo "testing again"
read -p "Enter user: " user
echo "user is: $user"

……然後將輸出:

$ bash /tmp/ftmp.sh
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
...
 Birth: -
testing again
Enter user: tyutyu
user is: tyutyu
testing
  File: .
  Size: 4096        Blocks: 8          IO Block: 4096   directory
...
 Birth: -
testing again
Enter user: asd
user is: asd
testing
...

....等等 - 也就是說,終端機中鍵盤的 stdin 被正確解釋,但由於某種原因,腳本開始循環,並從頭開始一遍又一遍地執行(如果我們沒有命令,則不會發生這種情況read -p ...)原本的tmp.sh)。也許有一些重定向的東西(例如,在循環命令中添加一些0>1&或其他內容while;實際上,我有一個.sh腳本也開始像這樣循環,並且簡單地在腳本末尾wget添加顯式似乎可以停止腳本循環)這也可以處理這個問題,-但到目前為止,我需要使用的腳本沒有類似的命令,所以這種方法可能對我有用。exit.shread -p

答案4

您可以在 bash 腳本中每行的末尾插入井號 (#)。這樣,Unix 上的 shell 就會將 CR 視為註釋,而不會在乎它。

“十六進制”,任何行都應以

0x23 0x0D 0x0A

例子:

echo "testing" #
stat . #
echo "testing again" #

相關內容