【ITニュース解説】set -euo pipefail Is Missing a Letter. My ERR Trap Stayed Silent Until I Added -E.
2026年09月22日に「Dev.to」が公開したITニュース「set -euo pipefail Is Missing a Letter. My ERR Trap Stayed Silent Until I Added -E.」について初心者にもわかりやすく解説しています。
ITニュース概要
Bashスクリプトで`set -euo pipefail`を使っても、関数内のエラーは`trap ERR`が拾えず原因不明なことがある。`-E`を追加し`set -Eeuo pipefail`とすると、エラー詳細がログに出力され、失敗原因が明確になる。これでデバッグが容易になる。
ITニュース解説
シェルスクリプトを書く際、プログラムの信頼性を高め、エラー発生時に何が起こったのかを正確に把握することは、システムエンジニアにとって非常に重要だ。そのためによく使われるのが、set -euo pipefail というコマンドだ。これは「厳格モード」と呼ばれ、スクリプトの冒頭に記述することで、いくつか重要な約束事をシェルに守らせる。
まず、-e は、コマンドが失敗した場合にスクリプト全体を即座に終了させる。これにより、エラーを見落としてスクリプトが不正な状態で続行してしまうことを防ぐ。しかし、-e には例外がある。if 文の条件式や、&& や || のような論理演算子の左側、! の後など、シェルが「このコマンドの終了ステータスをチェックしている」と判断する場所では、コマンドが失敗してもスクリプトは終了しない。さらに注意すべきは、関数内部で呼び出されたコマンドがこれらの「チェックされている」状況から呼び出された場合も、-e が機能しないことがある点だ。これにより、関数内で意図しないエラーが発生しても、スクリプトが沈黙したまま続行してしまうリスクがある。
次に、-u は、未設定の変数を参照しようとするとエラーとしてスクリプトを終了させる。これは、変数名のタイプミスなどによる意図しない動作を防ぐ上で非常に重要だ。例えば、rm -rf "$BUILD_DIR/" のようなコマンドで $BUILD_DIR が未設定の場合、-u がなければ $BUILD_DIR は空文字列とみなされ、コマンドは rm -rf / と解釈されてしまう可能性があり、システムに壊滅的な影響を与えることがある。-u があれば、そのような危険な事態を未然に防げる。
そして、-o pipefail は、パイプライン(| で連結された複数のコマンド)の途中でいずれかのコマンドが失敗した場合、パイプライン全体を失敗とみなす。通常、パイプラインの終了ステータスは最後のコマンドの終了ステータスで決まるため、途中のコマンドが失敗しても、最後のコマンドが成功すればパイプライン全体が成功とみなされてしまうことがある。例えば、curl ... | jq ... のようなパイプラインで curl がデータを取得できずに失敗しても、jq が空の結果を処理して成功してしまうと、エラーに気づかないままスクリプトが続行され、不正確な処理が行われる可能性がある。-o pipefail は、このような問題を防ぐ。
しかし、これらの set -euo pipefail だけでは、スクリプトのエラー処理はまだ完璧ではない。特に問題となるのが、trap ERR というエラーハンドリング機構だ。trap ERR は、-e と同じ条件でコマンドが失敗した際に、指定した関数やコマンドを実行させる仕組みだ。これにより、エラー発生時にデバッグ情報をログに出力したり、特別なクリーンアップ処理を行ったりできる。しかし、デフォルトでは ERR トラップは関数、コマンド置換、サブシェルといった子スコープに引き継がれない。つまり、スクリプトのトップレベルで trap ERR を設定していても、そのスクリプト内で定義された関数の中でエラーが発生した場合、ERR トラップハンドラは実行されない。記事の例では、mkdir が関数の中で失敗したが、ERR トラップは沈黙したままだった。結果として、スクリプトは終了するものの、何が原因で、どの行でエラーが発生したのかという最も重要な情報がログに残らず、デバッグが非常に困難になる。
この沈黙の問題を解決するのが、set -E というフラグだ。-E は errtrace とも呼ばれ、ERR トラップを関数やサブシェルにも引き継がせる役割を果たす。set -Eeuo pipefail と記述することで、スクリプトのどこでエラーが発生しても、ERR トラップハンドラが確実に実行されるようになる。trap ハンドラ内では、BASH_COMMAND 変数で失敗したコマンドの名前を、BASH_LINENO 配列でそのコマンドが実行された行番号を取得できるため、エラー発生時に「何が」「どこで」壊れたのかを正確にログに出力できるようになる。これにより、真夜中にエラーログを見てデバッグする際にも、詳細な情報に基づいて迅速に問題を特定できるようになるだろう。
もう一つ、シェルスクリプトでよく見過ごされがちな「沈黙」の問題がある。それは、local out=$(some_command) のような形で、変数宣言とコマンド置換を同じ行で行う場合だ。この場合、local コマンド自体の終了ステータスが成功(変数を宣言できたという事実)となるため、$(some_command) の中で some_command が失敗したとしても、その失敗はlocal コマンドによってマスクされてしまい、スクリプトはエラーとして終了しない。local は成功したと見なされ、次の行へ処理が進んでしまう。この問題を避けるためには、変数宣言とコマンド置換による代入を別の行に分けるべきだ。つまり、local out で変数を宣言し、その次の行で out=$(some_command) のように値を代入する。こうすることで、some_command の失敗が適切に検出され、スクリプトは終了する。
エラー発生時の処理だけでなく、スクリプトの終了時(成功か失敗かに関わらず)に必ず実行させたい処理には trap EXIT を使う。これは、一時ファイルの削除など、後処理のクリーンアップに非常に役立つ。EXIT トラップハンドラでも、ERR トラップハンドラと同様に、ハンドラの最初の行で $? (直前のコマンドの終了ステータス) をキャプチャすることが重要だ。もしクリーンアップ処理の途中で $? をキャプチャしなければ、クリーンアップコマンド自体の終了ステータス(多くは成功)がスクリプト全体の終了ステータスとして報告されてしまい、スクリプトが失敗したにもかかわらず成功したように見えてしまう可能性がある。
ただし、これらの「厳格モード」のフラグは常に万能というわけではない。例えば、複数のサービスの状態をチェックするヘルスチェッカースクリプトでは、いくつかのチェックが失敗することを想定している場合がある。そのようなスクリプトに -e を適用すると、意図しない終了が頻発し、|| true を多用してエラーを無視するような複雑なロジックを組む必要が出てくる。また、ユーザーが対話的に操作する .bashrc のようなスクリプトで -e を設定すると、一つのコマンドが失敗しただけでシェルが閉じてしまうなど、不都合が生じる。grep -q のように、目的のデータが見つからなかった場合に非ゼロの終了ステータス(エラーではない)を返すコマンドもあるため、状況に応じて厳格モードを適用すべきか判断する必要がある。
結論として、スクリプトの信頼性とデバッグ効率を大幅に向上させるためには、スクリプトの冒頭で set -Eeuo pipefail を設定し、ERR トラップと EXIT トラップを適切に記述することが推奨される。ERR トラップでは BASH_COMMAND と BASH_LINENO を使って詳細なエラー情報を出力し、どちらのトラップハンドラでも $? を最初にキャプチャする。さらに、local 変数への代入は宣言と別々の行で行うことで、エラーがマスクされることを防ぐ。これらの工夫は、スクリプトが単に「終了コード1」を返すだけでなく、「なぜ、どこで」失敗したのかを明確に教えてくれるようになるため、システムの運用や開発において非常に強力な助けとなる。