標準入力への bash ファイルのリダイレクトは、Linux 上のシェル (`sh`) とどう違うのでしょうか?

標準入力への bash ファイルのリダイレクトは、Linux 上のシェル (`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 を実行している 2 つの異なるボックスで発生します

答え1

同様のスクリプトですが、 はありませんsudoが、結果は同様です。

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

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

を使用するとbash、スクリプトの残りの部分は への入力として送られsed、 を使用するとdash、シェルがそれを解釈します。

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は入力の読み取りに戻ります。一度に1文字ずつ読み過ぎを避けるためです。fd_to_buffered_stream()input.c) バイトごとに完全なシステム コールを実行することは、原理的にはあまり効果的ではありません。実際には、たとえばシェルが実行するほとんどの処理でまったく新しいプロセスが生成されることと比較すると、読み取りによるオーバーヘッドはそれほど大きくないと思います。

同様の状況は次のとおりです。

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

サブシェルは、read最初の改行のみを読み取り、head次の行を確認するようにする必要があります。(これdashも機能します。)


言い換えれば、Bash は、スクリプト自体とそこから実行されるコマンドの同じソースの読み取りをサポートするために、さらに努力しています。Debiandashにパッケージ化されている、、はzshksh93この点で Bash を採用しています。

答え2

シェルは標準入力からスクリプトを読み込んでいます。スクリプト内では、標準入力を読み取るコマンドを実行します。どの入力がどこに送られるのでしょうか?確実には分からない

シェルの動作は、ソース コードの一部を読み取って解析し、完全なコマンドが見つかった場合はそのコマンドを実行し、残りの部分と残りのファイルに進みます。部分の中に完全なコマンド (最後に終了文字がある — すべてのシェルは行末まで読み取ると思います) が含まれていない場合、シェルは別の部分を読み取って、これを繰り返します。

スクリプト内のコマンドが、シェルがスクリプトを読み取っているのと同じファイル記述子から読み取ろうとすると、コマンドは読み取った最後のチャンクの後に続くものを見つけます。この場所は予測できません。シェルが選択したチャンク サイズによって決まり、シェルとそのバージョンだけでなく、マシン構成、使用可能なメモリなどによっても決まります。

Bash は、コマンドを実行する前に、スクリプト内のコマンドのソース コードの末尾までシークします。これは、他のシェルが行わないだけでなく、シェルが通常のファイルから読み取っている場合にのみ機能するため、当てにできるものではありません。シェルがパイプから読み取っている場合 (例ssh remote-host.example.com <local-script-file.sh)、読み取られたデータは読み取られ、未読にすることはできません。

スクリプト内のコマンドに入力を渡したい場合は、通常はここに文書(複数行の入力にはヒアドキュメントが最も便利ですが、どの方法でもかまいません。)記述したコードは、スクリプトが通常のファイルからシェルへの入力として渡される場合にのみ、いくつかのシェルでのみ機能します。2 番目がwhoamiへの入力として渡されると予想していた場合はsudo …、ほとんどの場合、スクリプトがシェルの標準入力に渡されないことを念頭に置いて、もう一度考えてください。

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

この 10 年間では、 を使用できることに注意してくださいsudo -i root。ランニングはsudo su過去のハックです。

関連情報