bash スクリプトの実行中上書き動作について
このパーサは fgets(2) で読み込まれつつ実行される為、一括でファイルが読み込まれている訳ではない。
普通の fgets
の実装ならまとまった単位でバッファに read
してそのバッファから 1 行ごとに返るだろうので、ファイルサイズが大きくなければ一括で読まれることになるのでは・・?
と思っていたところ下記を見まして、
いや、違うな。スクリプトファイルが変更されてなくても、外部コマンドを実行するたびにその位置まで seek して読み直してるのか。面白いことしてるなぁ。
— とみたまさひろ🍣🍺 (@tmtms) December 29, 2021
実際に strace
してみるとそのような挙動になっていたので、興味本位で Bash のソースを探って見ました。
input.c の sync_buffered_stream の中のこの辺で lseek していそうだけれどもそれ以上のことはわからないhttps://t.co/VDEC3UapJQ
— ngyuki@育児休業 (@ngyuki) December 29, 2021
Bash のソースは bash-5.1.8.tar.gz
です。
スクリプトの読み込み
スクリプトの読み込みは input.c
の with_input_from_buffered_stream
で、init_yy_io
に設定している buffered_getchar
で行われています。最終的にこれは lib/sh/zread.c
の zread
によって read(2)
が呼ばれます。
while ((r = read (fd, buf, len)) < 0 && errno == EINTR)
{
// ...snip...
}
len
の最初のサイズは input.c
の fd_to_buffered_stream
で次のように決められています。
if (fstat (fd, &sb) < 0)
{
// ...snip...
}
size = (fd_is_seekable (fd)) ? min (sb.st_size, MAX_INPUT_BUFFER_SIZE) : 1;
if (size == 0)
size = 1;
ファイルがシーク可能ならファイルサイズと MAX_INPUT_BUFFER_SIZE=8172
の小さい方、シーク不可なら 1 です。
シーク不可となるのは例えばプロセス置換のパイプです。bash <(cat a.sh)
などとすると 1 バイトずつ read
されます。これは strace
ですぐ確認できます。
strace -e trace=read bash <(cat a.sh)
一方、普通のファイルならシーク可能なので、ファイルサイズが 8172 以下なら一括でまとめて read
されます。
だとすれば実行中のスクリプトを上書きしても(スクリプトが 8172 バイト以下なら)問題なさそうですけど・・?
外部コマンド実行時のシーク
Bash が外部コマンドを実行するため(execute_cmd.c
の execute_command
)に子プロセスを作成するとき(jobs.c
の make_child
)、それに先立ってスクリプトのオフセットを実行しようとしているコマンドの直後の位置に lseek(2)
で戻しています。
これは input.c
の sync_buffered_stream
で行われています。
chars_left = bp->b_used - bp->b_inputp;
if (chars_left)
lseek (bp->b_fd, -chars_left, SEEK_CUR);
bp->b_used = bp->b_inputp = 0;
例えば次のようなスクリプトの場合、
/bin/echo 1
/bin/echo 2
/bin/echo 3
最初にファイル全体が read
されますが、その後 /bin/echo 1
の直後まで lseek
で戻り、次は改めてその位置から read
されます。
この動作は外部コマンドを呼ぶときだけで、次のようにビルドインの echo
だとこの lseek
は発生しません。
echo 1
echo 2
echo 3
例えば、次のスクリプトは最初の1行目を実行した時点でスクリプトの内容が空になった後、改めて2行目を読もうとして読めなくて、その結果なにも表示されません。
/bin/echo -n >a.sh
echo ok
ですが次のスクリプトだとシークが発生せず、最初にファイルが一括で読まれた内容で実行されるため ok
が表示されます。
echo -n >a.sh
echo ok
さいごに
Bash が何故そんな動作をしているのかは謎。意図的に実行中の自身を上書きできるようにしているのかと思ったけれども、それだとビルドインコマンドが対象外なのが謎ですし、謎。