概要
シェルスクリプトは、最初にすべてのファイルを読み込むわけではなく、1 処理ずつ読み込まれて実行される。
1 処理というのは、改行で区切られた 1 行のことを指す。ただし、\
で改行を継続させている場合は複数行になる。
スクリプトが逐次で読み込まれるため、シェルスクリプトが実行されている最中にファイルを変更すると意図しない挙動につながることがある、という例。
なお、動作確認は以下の環境で行った。
$ bash --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)
Copyright (C) 2007 Free Software Foundation, Inc.
例
以下のようなスクリプトを用意する。
このスクリプトを実行すると、以下のようになる。
次に、スクリプトを実行中に、変数をリファクタリングしたとして、以下のようにスクリプトを修正したとする。
$ ./script.sh &
$ cat - > script.sh <<'EOF'
#!/bin/sh
NEW_TARGET_DIR=/tmp
sleep 10 # very heavy process
echo "the target dir is ${NEW_TARGET_DIR}"
EOF
すると、以下のような実行結果が表示される。
変数宣言で NEW
を追加したことで、3 文字分の文字列が追加されたため、読み込んでいた位置がずれてしまい、process
の後半 3 文字分 ess
をコマンドとして認識してしまった。 今回の環境では ess
というコマンドは存在しないため、エラーが発生している。
そして、このシェルスクリプトにはエラー発生時に実行を止める設定が記述されていなかったため、 エラーが発生してもそのまま次の処理に進んでしまい、echo "the target dir is ${NEW_TARGET_DIR}"
が実行された。 しかし、新しいスクリプトの 1 行目は実行されておらず NEW_TARGET_DIR
は宣言されていない。 そのため、the target dir is
と表示された。
対策
- シェルスクリプトを使わない
- シェルスクリプトを使う場合は、エラーが発生した場合に実行を止める設定を記述する。具体的には、
set -e
を記述する。 - シェルスクリプトの実行中に inode を維持したままでファイルを更新するようなことをしない。
- たとえば、
vim
コマンドは inode を変更するため、上記のようなことは起こらない
- たとえば、
- 変数を使うときは、シェルスクリプトの変数展開機能をフル活用する
- たとえば、次のようにすることでデフォルト値を設定できる
echo "the target dir is ${NEW_TARGET_DIR:-DefaultDir}"
- あるいは、次のようにすることで変数の存在チェックができる
echo "the target dir is ${NEW_TARGET_DIR:?Variable is not set or null}"
- bash の man ページの
Parameter Expansion
の項目を参照
- たとえば、次のようにすることでデフォルト値を設定できる
現実の事例
京大のスーパーコンピューターの納入会社が、シェルスクリプトを不用意に更新したことによって、データが消失したという事例がある。