bash 檔案重定向到標準與 Linux 上的 shell (`sh`) 有何不同?

bash 檔案重定向到標準與 Linux 上的 shell (`sh`) 有何不同?

我編寫了一個在運行時切換使用者的腳本,並使用檔案重定向到標準輸入來執行它user-switch.sh

#!/bin/bash

whoami
sudo su -l root
whoami

運行它bash給了我我期望的行為

$ bash < user-switch.sh
vagrant
root

但是,如果我使用 運行腳本sh,我會得到不同的輸出

$ sh < user-switch.sh 
vagrant
vagrant

為什麼bash < user-switch.sh給出的輸出與 不同sh < user-switch.sh

筆記:

  • 發生在兩個運行 Debian Jessie 的不同機器上

答案1

類似的腳本,沒有sudo,但結果類似:

$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

使用 時bash,腳本的其餘部分將作為 的輸入sed,使用 時dash,shell 會對其進行解釋。

運行strace這些:dash讀取腳本的一個區塊(這裡是 8 kB,足以容納整個腳本),然後產生sed

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 8192) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

這意味著文件句柄位於文件末尾,並且sed不會看到任何輸入。剩餘部分被緩衝在dash. (如果腳本長於區塊大小 8 kB,則剩餘部分將由 讀取sed。)

另一方面,Bash 會回溯到最後一個指令的結尾:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 36) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
...
lseek(0, -7, SEEK_CUR)                  = 29
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

如果輸入來自管道,如下所示:

$ cat script.sh | bash

無法進行倒帶,因為管道和套接字不可找到。在這種情況下,Bash 會回退到讀取輸入一次一個字符以避免過度閱讀。 (fd_to_buffered_stream()input.c)原則上,對每個位元組進行完整的系統呼叫並不是很有效。在實踐中,我認為與 shell 所做的大多數事情都涉及產生全新進程的事實相比,讀取不會是很大的開銷。

類似的情況是這樣的:

echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'

子 shell 必須確保read只讀取第一個換行符,以便head看到下一行。 (這也適用dash。)


換句話說,Bash 付出了額外的努力來支援讀取腳本本身以及從中執行的命令的相同來源。dash沒有。 Debian 中打包的zsh、 和ksh93與 Bash 一起使用。

答案2

shell 正在從標準輸入讀取腳本。在腳本內,您執行一個也想讀取標準輸入的命令。哪個輸入將去往何處?你無法可靠地判斷

shell 的工作方式是讀取一段原始程式碼,對其進行解析,如果找到完整的命令,則執行該命令,然後繼續處理該區塊的其餘部分和檔案的其餘部分。如果該區塊不包含完整的命令(末尾有一個終止字元 - 我認為所有 shell 都會讀取到一行的末尾),則 shell 會讀取另一個區塊,依此類推。

如果腳本中的命令嘗試從 shell 從中讀取腳本的相同檔案描述符中讀取,則該命令將找到它讀取的最後一個區塊之後的任何內容。這個位置是不可預測的:它取決於 shell 選擇的區塊大小,並且不僅取決於 shell 及其版本,還取決於機器配置、可用記憶體等。

Bash 在執行命令之前會在腳本中尋找命令原始碼的末尾。這不是您可以指望的事情,不僅因為其他 shell 不這樣做,而且還因為只有當 shell 從常規檔案讀取時才有效。如果 shell 正在從管道讀取資料(例如ssh remote-host.example.com <local-script-file.sh),則已讀取的資料將被讀取並且無法取消讀取。

如果要將輸入傳遞給腳本中的命令,則需要明確執行此操作,通常使用這裡的文檔。 (here 文件對於多行輸入通常是最方便的,但任何方法都可以。)您編寫的程式碼只能在少數 shell 中運行,只有當腳本作為輸入從常規文件傳遞到 shell 時才有效;如果您預期第二個whoami將作為輸入傳遞給sudo …,請再想一想,記住大多數時候腳本不會傳遞到 shell 的標準輸入。

#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF

請注意,這十年,您可以使用sudo -i root.跑步sudo su是過去的一種技巧。

相關內容