【ITニュース解説】Bash reads your script as it goes
2026年11月03日に「Dev.to」が公開したITニュース「Bash reads your script as it goes」について初心者にもわかりやすく解説しています。
ITニュース概要
実行中のBashスクリプトにコメントを追加したらジョブがクラッシュした。Bashはファイルをバイトオフセットで途中から読むため、実行中のファイル変更で、読み込み位置が別の内容を指しエラーや誤作動を招く。安全な更新は、一時ファイルに書き込み、リネームでの置き換えが安全だ。
ITニュース解説
システムエンジニアを目指す初心者が、プログラミングやシステム運用に関わる中で遭遇する可能性のある、Bashスクリプトの意外な挙動と、それに伴うトラブルの回避策について解説する。この記事は、実行中のシェルスクリプトのファイルを安易に変更することが、いかに予期せぬ重大な問題を引き起こしうるかという具体的な事例を提示している。
筆者は、長時間実行される重要なジョブが、自分がスクリプトにたった一行のコメントを追加しただけで突然クラッシュしたという不可解な経験をした。追加したコメント自体はプログラムの動作に影響を与えないはずの内容だったが、「line 172: syntax error near unexpected token `measured'」というエラーメッセージが表示された。この「measured」という単語は、まさに筆者が追加したコメント内に含まれるものだった。しかし、ジョブはそのコメントが存在するはるか以前からスクリプトを読み込んで実行していたはずで、なぜ後から追加したコメントがエラーの原因になったのかが最初の疑問だった。
この謎を解く鍵は、Bashがスクリプトをどのように読み込み、実行するかという基本的なメカニズムにある。多くのプログラミング言語、例えばPythonでは、通常スクリプトファイルを一度に全て読み込み、それを解釈してから実行を開始する。しかし、Bashは異なるアプローチをとる。Bashはスクリプトを一度に全て読み込むのではなく、少しずつ読み込み、そこに書かれたコマンドを実行し、そして停止したファイル内の位置から再び読み込みを再開するという動きをする。この「停止した位置」は、ファイル内の文字がディスク上で何番目のバイトにあるかを示す「バイトオフセット」という数値で管理されている。これは「何行目」といった行番号とは異なる、ファイル内の物理的な位置情報である。
そのため、スクリプトが実行中に、そのファイルの内容が外部から変更されると問題が生じる可能性がある。Bashは以前に記憶していたバイトオフセットに戻って読み込みを再開するが、その位置に存在するテキストが、ファイルが変更される前とは全く異なる内容になっているかもしれないからだ。
これを具体的に確認するための簡単な実験がある。例えば、「echo start」「sleep 2」「echo done」という3行のBashスクリプトを用意し、これを実行する。スクリプトが「sleep 2」(2秒間停止)している間に、同じスクリプトファイルの内容を上書きする。このとき、元のスクリプトの先頭に「# tidy up the output later」といったコメント行を追加して保存し直す。
すると、sleepが終了した後、Bashは以前のバイトオフセット、例えば19番目のバイト位置から読み込みを再開する。しかし、ファイルが変更された後の状態では、この19番目のバイト位置は、元のファイルでは「echo done」の始まりだったものが、新しいファイルでは「# tidy up the output later」というコメントの途中、例えば「t later」のような部分に当たってしまう。その結果、Bashはこのコメントの一部である「t」をコマンドとして解釈しようとし、「t: command not found」といったエラーを発生させる。さらに、読み込みを続けた結果、「echo start」を再び見つけて実行してしまうといった予期せぬ動作につながる可能性もある。もし、この繰り返された行が、誰かにメッセージを送ったり、重要なフォルダを削除したりするようなコマンドだったら、大変な事態になっていたはずだ。
このような問題を防ぐためには、実行中のファイルを直接編集するのではなく、安全な方法で更新することが重要になる。Bashのような実行中のプロセスは、ファイル名ではなく、そのファイルがディスク上で持つ真の識別子である「inode(アイノード)」という情報を使ってファイルを開いている。これは、ファイル名が変更されても、プロセスは元のinodeが指すファイルを読み続けられることを意味する。
この性質を利用した安全な更新方法は、「新しいバージョンのスクリプトを一時ファイルに書き込み、その一時ファイルを元のスクリプトファイル名にリネームする」という手順である。この「リネーム」操作は、ファイル名が指すinodeを切り替えるだけで、実際にファイルの内容を上書きするわけではない。そのため、実行中のBashプロセスは、以前開いていた古いinodeを指すファイルを引き続き読み続けることができる。
一時ファイルを作成する際は、mktempコマンドを使うのが便利だが、注意点がある。一時ファイルを元のスクリプトと同じディレクトリ(ファイルシステム)内に作成する必要がある。mktempを引数なしで実行すると、通常/tmpのような別のファイルシステムに一時ファイルが作成される。この状態でmvコマンドを使ってリネームしようとすると、mvはファイルシステムを跨ぐ移動と判断し、実際には古いファイルをコピーして新しいファイルに置き換え、元のファイルを削除する「コピー&削除」の動作に切り替わってしまう。これは結果として、ファイルをインプレースで編集するのと同じ危険な挙動となる。同じディレクトリ内であれば、mvは真のリネーム操作となり、inodeの参照だけを安全に切り替える。
sed -iコマンドは、内部的に一時ファイルを生成し、その後リネームするという安全な方法を採るため、インプレース編集のように見えても実際には安全な操作である。しかし、シェルリダイレクトによる上書き(>)や、多くのエディタが行う「ファイルを切り詰めてから上書きする」といった操作は、実行中のスクリプトに直接影響を与える危険なインプレース編集に該当する。
筆者はこの経験から、二つの対策を講じた。一つは習慣の変更で、実行中の可能性のあるシェルスクリプトを更新する際は、常に一時ファイルを作成してリネームする方法を用いるようにした。もう一つは、より確実なシステム的な対策として、ファイル編集を行う前に、そのファイルが現在実行中のスクリプトによって開かれていないかを自動的にチェックする仕組みを導入し、もし開かれている場合は編集を拒否するようにした。これにより、不注意による事故を防ぐことを目指している。
この一件から得られた最も重要な教訓は、予期せぬシステムトラブルが発生した際の原因特定のアプローチについてである。筆者は最初、自分がデバッグ作業をしていた部分に問題があると決めつけて調査していた。しかし、実際にはそのジョブは全く問題なく動作しており、自分が「直前に変更を加えた」コメントの追加が原因だった。このため、長いジョブがクラッシュした際は、まず「自分が最後に何に触ったか」を最初に確認するという新しい習慣を身につけた。システムエンジニアとして、問題が発生したときに直前の変更点に目を向けることは、トラブルシューティングの効率を大幅に向上させる重要な視点となるだろう。