tl;dr
とあるプロジェクトのデプロイを CircleCI で実行していて, いくつかのコマンドをラップしたBash スクリプト (以後, シェルスクリプトと記載) を実行していたんだけど, コマンドのエラーを意図せずハンドル出来ていない現象に気づいて修正したメモです. 結局のところ, シェルスクリプトを書く時には set -e
をつけた方がいいのかなという結論に至った次第です.
for example
以下のようなシェルスクリプトを動かそうとした場合...
#!/bin/bash echo "イメージをビルドします" docker build -t foo/bar:build . echo "イメージタグを設定します" docker tag foo/bar:build foo/bar:xxxxxxx
docker build
で失敗した場合, CI/CD 環境に限らず, シェルスクリプトとしては即座に終了してもらたいのですが, 意図に反して終了せずに次のステップ (docker tag
) の処理に進んでしまいます.
解決策
その 1 〜 がんばってエラーハンドリングを実装する 〜
docker build
において, Dockerfile に不備があったり, ビルド時に実行されるコマンドにエラーが発生した場合にステータスコード 1
で終了します. これをハンドリングするような処理を入れることを検討としました. ステータスコード 1
以外で終了する場合も想定して, 正常終了 (ステータスコード 0
) 以外は全てエラーとしてハンドリングしたいと思います.
#!/bin/bash echo "イメージをビルドします" docker build -t foo/bar:build . [ $? -eq 0 ] && echo "次の処理に進みます" || exit 1 echo "イメージタグを設定します" docker tag foo/bar:build foo/bar:xxxxxxx [ $? -eq 0 ] && echo "次の処理に進みます" || exit 1 echo "引続きの処理を行います"
悪くはないですが, 同じような内容を繰り返し記述する必要があり, 可読性が下がりますし, メンテナンスもし辛いと思います.
その 2 〜 set -e を付与する 〜
インターネット上を検索すると, シェルスクリプトの Shebang に -e
を付与したり, set -e
を付与したりすることで, スクリプト内で実行されるコマンドが正常に終了しなければ, スクリプトを即座に終了することが出来ることが判りました.
パイプライン (1 つの 単純なコマンド からなるものでもよい)、 括弧で囲まれた サブシェル のコマンド、 ブレース (前述の シェルの文法 を参照) で囲まれたコマンドのリストの一部として実行されたコマンドの 1 つ が 0 でないステータスで終了した場合、即座に終了します... (以下, 略)
以下のように書くことで, docker build
や docker tag
のコマンドが失敗した時点でシェルスクリプトも終了します.
#!/bin/bash set -e echo "イメージをビルドします" docker build -t foo/bar:build . echo "イメージタグを設定します" docker tag foo/bar:build foo/bar:xxxxxxx echo "引続きの処理を行います"
ちょっと, 以下のようなシェルスクリプトで動作確認してみたいと思います.
#!/bin/bash echo "step 1" ech "step 1 が終了" echo "step 2" echo "step 2 が終了" echo "step 3" cho "step 3 が終了"
シェルスクリプトは見ての通り, echo
を typo した雑なスクリプトですが, これをこのまま実行すると...
$ ./test.sh step 1 ./test.sh: line 4: ech: command not found step 2 step 2 が終了 step 3 ./test.sh: line 10: cho: command not found
上記のように step 1
と step 3
の分まで実行されていることが判ります. これはこれで良い状況もあるかもしれませんが, CI/CD で実行する場合には, エラーが発生した段階で即座に終了して欲しいです. ということで, set -e
を付与してみます.
#!/bin/bash set -e echo "step 1" ech "step 1 が終了" echo "step 2" echo "step 2 が終了" echo "step 3" cho "step 3 が終了"
実行すると, 以下のように step 1
の段階でエラーとして終了しています.
$ ./test.sh step 1 ./test.sh: line 6: ech: command not found
良い感じです. ちなみに, shebang に -e
オプションを付与しても同じ結果を得ることが出来ます.
#!/bin/bash -e echo "step 1" ech "step 1 が終了" echo "step 2" echo "step 2 が終了" echo "step 3" cho "step 3 が終了"
実行すると, 結果は先述と同様に step 1
の段階で終了します.
$ ./test.sh step 1 ./test.sh: line 6: ech: command not found
注意点
再掲
パイプライン (1 つの 単純なコマンド からなるものでもよい)、 括弧で囲まれた サブシェル のコマンド、 ブレース (前述の シェルの文法 を参照) で囲まれたコマンドのリストの一部として実行されたコマンドの 1 つ が 0 でないステータスで終了した場合、即座に終了します。 ただし、失敗したコマンドが、キーワード while または until の直後のコマンドの一部である場合、予約語 if または elif に続く条件式の一部である場合、 && または || によるコマンドのリストの一部である場合 (最後の && や || の後のコマンドを除く)、 パイプラインの中の最後のコマンド以外である、 コマンドの返り値が ! で反転されている場合、のいずれかであれば、シェルは終了しません。 ERR に対するトラップが設定されていれば、シェルが終了する前に実行されます。 このオプションはシェルの環境と各サブシェルの環境に別々に適用され (前述の コマンド実行環境 を参照)、 サブシェルはサブシェル内の全てのコマンドを実行する前に終了するかもしれません。
set -e
オプションの挙動について, man ページの抜粋を再掲させて頂きます.
個人的に分かりづらい (読みにくい) ですが, set -e
オプションが付与されていても, 幾つかの条件では, 実行されるコマンドがエラーとなっても, シェルスクリプトを終了しないようです.
実例 (1)
以下のようなシェルスクリプトだと, set -e
が付与されていても最後まで処理が行われました.
#!/bin/bash set -e if [ "$(cho 'test')" == "0" ];then echo "success" else echo "failure" fi
例のごとく, echo
を cho
と typo しています. 実際に実行してみると, 以下のように出力されて, シェルスクリプト全体の挙動としては正しいのかなと思います.
$ ./test.sh ./test.sh: line 5: cho: command not found failure
実例 (2) 〜 grep コマンド 〜
例えば, シェルスクリプト内で, 何かしらの条件判断に grep
を使っている場合, set -e
を利用していると意図しない結果になることがあります.
grep
コマンドにおいて, 引数で指定した文字列が含まれている場合, ステータスコード 0
が返ってきます. 逆に含まれていない場合には, ステータスコード 1
が返ってきます.
$ bash -c 'echo "foo" | grep "foo"; echo $?' foo 0 $ bash -c 'echo "foo" | grep "bar"; echo $?' 1
この挙動を利用して, 以下のようなシェルスクリプトを書きました.
#!/bin/bash echo "foofoo" | grep "$1" if [ $? == 0 ];then echo "処理を継続します..." else echo "処理を終了します" fi
これを, このまま実行すると, 以下のように「処理を終了します」が出力されます.
bash -c './test.sh bar; echo $?' 処理を終了します 0
set -e
オプションを付与すると, 前回とは異なる結果となります.
$ bash -c './test.sh bar; echo $?' 1
これを回避する為 (set -e
が付与されていても, 正しく条件判断が行われるようにする) には, 以下のようにシェルスクリプトを書きます.
set -e if echo "foo" | grep -q "$1" ;then echo "処理を継続します..." else echo "処理を終了します" fi
実行すると, 以下のように出力されました.
$ bash -c './test.sh foo; echo $?' 処理を継続します... 0 $ bash -c './test.sh bar; echo $?' 処理を終了します 0
尚, set -e
を使っている際の意図しない挙動への対処については, 以下のブログ記事が参考になりました. ありがとうございます.
以上
set -e
や #!/bin/bash -e
について, ちゃんと使ったことなかったのですが, 実際にトラブったり, 触ってみたりすることで挙動を把握出来て良かったです. また, bash の奥深さの少しだけ垣間見ることが出来た気がします.
以上, ドキュメントをちゃんと読め案件でした.