堅牢なスクリプトを書く
目標
本番サーバーに載せてもよいスクリプトの条件を、1つずつ実装します。失敗を隠さず、後始末を保証し、2回動いても安全である、という条件です。
なぜ重要なのか
シェルは、デフォルトでは寛容です。存在しない変数は空文字列として通り、パイプの途中の失敗は最後のコマンドが成功すれば埋もれ、スクリプトが死んでも一時ファイルは残ります。この寛容さが、「毎日成功と報告されていたバックアップが、実は0バイトだった」のような事故を生みます。set -euo pipefailは、問題を発見した地点で止める仕組みで、trap ... EXITは、どんな経路で終わっても後始末が動くようにする仕組みです。2つは対で使ってこそ意味があります。
ステップ
/root/robust/strict.shを作ってください。先頭にset -euo pipefailを入れ、引数を1つ受け取ってそのまま出力します。strict.sh helloはhelloを出力して0で、引数なしで実行すると0以外のコードで終了する必要があります。/root/robust/pipe.shを作ってください。引数がokなら成功するパイプラインを、brokenなら前の段階が失敗するパイプラインを実行します。brokenのとき、終了コードが0であってはいけません。/root/robust/tmpwork.shを作ってください。mktempで一時ファイルを作ってそのパスを標準出力に1行出力し、終了するときにtrapで必ず削除します。引数としてfailを渡すと失敗で終わりますが、そのときも一時ファイルが残ってはいけません。/root/robust/once.shを作ってください。実行すると約3秒間作業して0で終わります。すでに実行中のときに再度実行すると、already runningを知らせて終了コード9で終わる必要があります。前の実行が終わった後は、再び正常に実行できる必要があります。/root/robust/count.sh <디렉터리>を作ってください。そのディレクトリの直下にある通常ファイルの個数だけを出力します。ディレクトリのパスとファイル名の両方に空白があっても正確である必要があり、引数がなければ0以外のコードで終わる必要があります(プレースホルダーはディレクトリです)。/root/robust/backup.sh <원본디렉터리> <결과파일>を作ってください(プレースホルダーは、元のディレクトリと結果ファイルです)。- 引数がなければ、標準エラー出力に
usageを含む案内を出し、終了コード64で終了します - 元のパスがなければ、終了コード
66で終了します - 正常なら、サブディレクトリまで含めてtar.gzを作り、終了コード
0で終了します
- 引数がなければ、標準エラー出力に
/root/robust/retry.sh <횟수> <명령...>を作ってください。コマンドが失敗したら最大でその回数までリトライしますが、試行の間に最低1秒は休み、成功したらすぐに止まります。試行のたびに、attemptという単語が入ったログを残します(プレースホルダーは、回数とコマンドです)。/root/robust/audit.sh <스크립트경로>を作ってください。対象のスクリプトにシバンと厳格モードの宣言があるかを検査して、strict=yes|noとshebang=yes|noの2行を出力します。両方yesなら終了コード0、そうでなければ0以外のコードです。ファイルがなくても、0以外のコードである必要があります(プレースホルダーはスクリプトのパスです)。
参考
trapハンドラーの先頭でlocal rc=$?として終了コードをつかんでおき、最後にexit "$rc"で返してやらないと、トラップが終了コードを飲み込みます。find "$d" -maxdepth 1 -type fで、最上位の通常ファイルだけを数えられます。- よくある間違い1: ステップ4でロックファイルを作るだけで削除しないと、次の実行が永遠にブロックされます。
- よくある間違い2: ステップ7で
$*を使ってコマンドを再実行すると、空白を含む引数が壊れます。
厳格モードを有効にする
/root/robust/strict.shを作ってください。先頭にset -euo pipefailを入れ、引数を1つ受け取ってそのまま出力します。strict.sh helloはhelloを出力して0で、引数なしで実行すると0以外のコードで終了する必要があります。
set -euo pipefailの1行で十分です。-uが有効なら、存在しない$1を読んだ瞬間に死ぬ必要があります。
パイプラインの失敗を伝播させる
/root/robust/pipe.shを作ってください。引数がokなら成功するパイプラインを、brokenなら前の段階が失敗するパイプラインを実行します。brokenのとき、終了コードが0であってはいけません。
デフォルトのシェルは、最後のコマンドの状態だけを見ます。前の段階が失敗したときも、失敗で終わるようにしてください。
一時ファイルを必ず削除する
/root/robust/tmpwork.shを作ってください。mktempで一時ファイルを作ってそのパスを標準出力に1行出力し、終了するときにtrapで必ず削除します。引数としてfailを渡すと失敗で終わりますが、そのときも一時ファイルが残ってはいけません。
trap ... EXITは、正常終了とエラー終了の両方で動きます。一時ファイルはmktempで作ってください。
重複実行を防ぐ
/root/robust/once.shを作ってください。実行すると約3秒間作業して0で終わります。すでに実行中のときに再度実行すると、already runningを知らせて終了コード9で終わる必要があります。前の実行が終わった後は、再び正常に実行できる必要があります。
flockやmkdirベースのロックを使います。2回目の実行はすぐに終了コード9で終わる必要があり、前の実行が終わったらロックが解除される必要があります。
空白を含むパスを安全に扱う
/root/robust/count.sh <디렉터리>を作ってください。そのディレクトリの直下にある通常ファイルの個数だけを出力します。ディレクトリのパスとファイル名の両方に空白があっても正確である必要があり、引数がなければ0以外のコードで終わる必要があります(プレースホルダーはディレクトリです)。
パスにもファイル名にも空白があります。変数を囲まないと、個数が狂います。
使い方と終了コードの規約
/root/robust/backup.sh <원본디렉터리> <결과파일>を作ってください(プレースホルダーは、元のディレクトリと結果ファイルです)。
- 引数がなければ、標準エラー出力に
usageを含む案内を出し、終了コード64で終了します - 元のパスがなければ、終了コード
66で終了します - 正常なら、サブディレクトリまで含めてtar.gzを作り、終了コード
0で終了します
使い方の案内は標準エラー出力へ、状況ごとに異なる終了コードで。64は使い方のエラー、66は入力なしです。
リトライヘルパー
/root/robust/retry.sh <횟수> <명령...>を作ってください。コマンドが失敗したら最大でその回数までリトライしますが、試行の間に最低1秒は休み、成功したらすぐに止まります。試行のたびに、attemptという単語が入ったログを残します(プレースホルダーは、回数とコマンドです)。
コマンドをそのまま再実行するには、"$@"が必要です。成功したらすぐに止まり、試行の間には休む必要があります。
スクリプトの監査ツール
/root/robust/audit.sh <스크립트경로>を作ってください。対象のスクリプトにシバンと厳格モードの宣言があるかを検査して、strict=yes|noとshebang=yes|noの2行を出力します。両方yesなら終了コード0、そうでなければ0以外のコードです。ファイルがなくても、0以外のコードである必要があります(プレースホルダーはスクリプトのパスです)。
他のスクリプトファイルを読んで、シバンと厳格モードの有無を判定します。存在しないファイルも処理する必要があります。