假設我有以下簡單的腳本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 在評論中提到dos2unix
(sudo 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.sh
CRLF 行結尾為/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
.sh
read -p
答案4
您可以在 bash 腳本中每行的末尾插入井號 (#)。這樣,Unix 上的 shell 就會將 CR 視為註釋,而不會在乎它。
“十六進制”,任何行都應以
0x23 0x0D 0x0A
例子:
echo "testing" #
stat . #
echo "testing again" #