シェルスクリプトでset -eしているときに処理を中断せずエラーを扱う方法

シェルスクリプトで set -e (errexit) しておくと、スクリプト中に実行したコマンドの終了ステータスが「非0」だった場合、つまりコマンドが失敗したときにそこでスクリプトを中断してくれるようになります。(終了ステータスとは「Exit code」「Return status」などと呼ばれるものです。)

都度エラー判定を書かなくてもよくなるので便利なオプションですが、個別にエラーハンドリングをしたくなった時にも意図せずスクリプトが中断されてしまい困ったことになります。

例えば、以下の例は false コマンドの実行で失敗扱いとなり、そこでスクリプトが中断されるため「finish」が出力されることはありません。

set -e

# false コマンドは必ず終了ステータスに 1 (失敗) を返すコマンド
# エラーが起きたとみなされスクリプトは中断される
false

# このコマンドは実行されない
echo "finish"

-e オプションで困ってしまうケースの例

例えば、grep コマンドで検索結果が見つからなかった場合にエラーメッセージを出力したいケースがあったとしましょう。

grep は検索結果が何か見つかれば 0 (成功)、見つからなければ 1 (失敗) の終了ステータスを返す仕様ですが、以下の例では grep で見つからなかった場合、そこでスクリプトが中断されてしまうので、以降の if 文が実行されることはありません。

set -e
grep "apple" hoge.txt >/dev/null
ret=$?

if [[ $ret != 0 ]]; then
  # ここに入ってくることはない!
  echo "hoge.txt には Apple を含む行が見つかりませんでした。" 1>&2
  exit 1
fi

これについて対処法を4つメモしておきます。

対処法1: コロンコマンドを利用する (:コマンド)

コロンコマンド : は一つの built-in command で、何もせず常に 0 (成功) の終了ステータスを返す虚無的なコマンドです。

以下の例はスクリプトが中断されることはなく「Exit code: 1」が出力されます。

set -e

grep "unmatch pattern" hoge.txt && :
echo "Exit code: $?"

これは -e オプションにおける特殊な振る舞いを利用した方法です。

式の評価の流れ:

  • grep の終了ステータスが 0 (成功) だった場合、&& 演算子により右側も評価され : を実行。一連のコマンドの終了ステータスは 0 (成功)。
  • grep の終了ステータスが 1 (失敗) だった場合、&& 演算子によりそこで評価を終了。なので一連のコマンドの終了ステータスは 1 (失敗)。

更に set -e オプションには &&|| で連結されたコマンドリストの最後にある(一番右側の)コマンドが失敗だった場合のみスクリプトを中断するという特性があるため、そのままスクリプトを続行できます。$? にもちゃんと grep の終了ステータスが入っています。

そのためちょっと直感的ではないですが、以下のコマンドの出力は route1のみ となります。

set -e

false && false
echo "route1"

false
echo "route2"

対処法2: true コマンドを利用する

以下の例は「Exit code: 1」が出力されます。

set -e

grep "unmatch pattern" hoge.txt && true
echo "Exit code: $?"

これも対処法1と同じ理由です。true コマンドも : と同じく何もせず常に 0 (成功) を返すコマンドです。
実質的に対処法1と同じ方法と言えますが、こちらの方が他の人に意図は伝わりやすいかもしれません。

対処法3: 終了ステータスを取得せず if 文でそのまま判定する

set -e

# grep は検索結果が見つからなかった時に 1 (失敗) を返すコマンド
# --quiet は標準出力に何も書き出さないオプション
if ! cat hoge.txt | grep --quiet "Apple"; then
  echo "hoge.txt には Apple を含む行が見つかりませんでした。" 1>&2
  exit 1
fi

これは最も自然な解決方法だと思います。if 文の評価式として実行したコマンドに関しては失敗しても中断されないという特性があるため、スクリプトを続行できます。

対処法4: 一時的に set +e して -e オプションを解除する

以下の例は「Exit code: 1」が出力されます。

set +e
grep "unmatch pattern" hoge.txt
echo "Exit code: $?"
set -e

この方法が楽そうならこれで。最後に set -e で元に戻すのを忘れずに。

感想

対処法3のケースなど普段何気なく実現できていたことも、ちょっとした挙動の特性により実現出来ていたことが理解できました(^^)
個人的にはコロンコマンドを使用した方法がシンプルで好きですが、時と場合で使い分けたいと思います。

参考資料

編集履歴

  • 2018/11/22: 全体的に例や内容をブラッシュアップしました。

PHP5.3でHTMLPurifier4.2を使おうと思ったらNOTICEエラーが発生。

悪意あるHTMLコードをきれいにするHTMLPurifierを試してみました。
試したPHPコードは以下の通り。

// HTMLPurifierを生成する
$dirty_str = 'hogehoge';

$config = HTMLPurifier_Config::createDefault();
$config->set('Core', 'Encoding', 'UTF-8');
$config->set('Core', 'Language', 'ja'); 
$config->set('Attr', 'AllowedFrameTargets', array('_blank','_self'));

$hp = new HTMLPurifier($config);
//危険なHTMLタグを無効化
$clean_str = $hp->purify( $dirty_str );

他の参考サイトにならってconfigオブジェクトに設定を格納しようと思ったら以下のようなエラーが出ました。

Notice: Using deprecated API: use $config->set('Core.Encoding', ...) instead on line 8 in file ... in Config.php on line 564
Notice: Using deprecated API: use $config->set('Core.Language', ...) instead on line 9 in file ... in Config.php on line 564
Notice: Using deprecated API: use $config->set('Attr.AllowedFrameTargets', ...) instead on line 10 in file ... in Config.php on line 564

NOTICEエラーなのでerror_reporting(0);とか書いておけばもちろん非表示に出来ますがそれだと気持ち悪いので修正しました。
setメソッドに渡す値を引数で連結せずに.(ドット)で連結するとエラーが消えるみたいです。
修正したコードは以下の通りです。

// HTMLPurifierを生成する
$dirty_str = 'hogehoge';

$config = HTMLPurifier_Config::createDefault();
$config->set('Core.Encoding', 'UTF-8');
$config->set('Core.Language', 'ja'); 
$config->set('Attr.AllowedFrameTargets', array('_blank','_self'));

$hp = new HTMLPurifier($config);
//危険なHTMLタグを無効化
$clean_str = $hp->purify( $dirty_str );