シェルスクリプトをエラー時に停止させたい

シェルスクリプトはデフォルトでは途中のコマンドが失敗しても処理を続行する。

#!/bin/bash

cp app.conf /etc/app/
echo "deploy done"

cpが失敗しても、後続の処理は実行される。 スクリプト全体の終了コードも0になる。

$ ./deploy.sh
cp: cannot stat 'app.conf': No such file or directory
deploy done
$ echo $?
0

設定ファイルのコピーに失敗したままデプロイ処理が進むと、原因の特定が難しい障害につながる。

set -euo pipefailを指定する

スクリプトの冒頭にset -euo pipefailを指定すると、エラー発生時にスクリプトを停止できる。

#!/bin/bash
set -euo pipefail

cp app.conf /etc/app/
echo "deploy done"
$ ./deploy.sh
cp: cannot stat 'app.conf': No such file or directory
$ echo $?
1

-e-u-o pipefailの3つのオプションをまとめて指定している。 各オプションの効果を以下で説明する。

set -e: コマンドの失敗で即座に終了する

set -eを指定すると、コマンドが0以外の終了コードを返した時点でスクリプトが終了する。 上記のdeploy.shの例ではcpの失敗時点で停止し、echoは実行されない。

set -u: 未定義変数の参照をエラーにする

set -uを指定すると、未定義の変数を参照した時点でエラーになる。

#!/bin/bash
set -u

TMP_DIR=/tmp/app
rm -rf "${TMP_DIRR}/cache"
$ ./cleanup.sh
./cleanup.sh: line 5: TMP_DIRR: unbound variable
$ echo $?
1

set -uを指定しない場合、タイプミスした${TMP_DIRR}は空文字列に展開される。 上記の例ではrm -rf /cacheが実行されてしまう。 変数名のタイプミスが意図しないファイル削除につながるため、set -uによる検出は安全性に直結する。

set -o pipefail: パイプ途中の失敗を検知する

パイプでつないだコマンドの終了コードは、デフォルトでは最後のコマンドの終了コードになる。 パイプの途中で失敗しても検知できない。

$ cat nofile.txt | wc -l
cat: nofile.txt: No such file or directory
0
$ echo $?
0

set -o pipefailを指定すると、パイプ内で失敗したコマンドの終了コードが全体の終了コードになる。

$ set -o pipefail
$ cat nofile.txt | wc -l
cat: nofile.txt: No such file or directory
0
$ echo $?
1

set -eと組み合わせると、パイプ途中の失敗でもスクリプトを停止できる。

注意点: 失敗を許容したいコマンドには || true をつける

grepはマッチする行がない場合に終了コード1を返す。 set -eの指定下では、マッチなしのgrepでスクリプトが停止する。

#!/bin/bash
set -euo pipefail

error_count=$(grep -c ERROR /var/log/app.log)
echo "errors: ${error_count}"
$ ./check.sh
$ echo $?
1

失敗を許容したいコマンドには|| trueをつける。

#!/bin/bash
set -euo pipefail

error_count=$(grep -c ERROR /var/log/app.log || true)
echo "errors: ${error_count}"
$ ./check.sh
errors: 0
$ echo $?
0

grepの終了コードの詳細はgrepコマンドで終了ステータスコードだけほしいときに検索結果を標準出力に出さない方法 を参照。

関連

参考