CUBE SUGAR CONTAINER

技術系のこと書きます。

シェルスクリプトで返り値のチェックを一時的に止める

シェルスクリプトで set -e しておくとコマンドの返り値が非ゼロ (エラー) のときにスクリプトを止めることができる。 ただ、常に返り値のチェックが有効だと、スクリプトが意図せず止まってしまうことがある。 そんなとき一時的に止める方法とハマりやすい挙動について。

使った環境は次の通り。

$ sw_vers     
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
Copyright (C) 2007 Free Software Foundation, Inc.

もくじ

set -e について

シェルスクリプトで set -e しておくと、コマンドの返り値が非ゼロ (エラー) のときに、処理をそこで止めることができる。 例えば以下のスクリプトでは bash -c "exit 1" のところで止まる。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# コマンドの返り値が非ゼロなので異常終了
bash -c "exit 1"  # ここで停止する

# ここまで到達しない
echo "Hello, World"

上記を実行してみよう。

$ bash errchk.sh 
+ set -e
+ bash -c 'exit 1'

最後まで到達しないことが分かる。

一応、コマンドがゼロを返すパターンについても確認しておく。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# コマンドの返り値がゼロなので正常終了
bash -c "exit 0"

# エラーがないので、ここまで到達する
echo "Hello, World"

上記を実行すると、今度は最後まで到達する。

$ bash errchk.sh 
+ set -e
+ bash -c 'exit 0'
+ echo 'Hello, World'
Hello, World

返り値のチェックが有効なときの問題点について

ただ、常に返り値のチェックが有効だと困るときもある。 例えば、特定のコマンドの返り値の内容を元に処理を分岐したいとき。

以下のスクリプトは、bash -c ... の実行結果を元に処理を分岐することを意図したサンプルになっている。 しかし、返り値のチェックが有効な状況では後ろの分岐までそもそも到達できない。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# コマンドの実行結果を元に処理を分岐させたい
bash -c "exit 1"  # しかし set -e があると停止してしまう

# 以下の分岐まで到達しない
if [ $? -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

上記を実行してみよう。 コマンドの返り値が非ゼロを返した時点で止まってしまう。

$ bash errchk.sh 
+ set -e
+ bash -c 'exit 1'

返り値のチェックを一時的に止める

この問題点を解消するには、条件分岐のコマンドを実行するときだけ返り値のチェックを止めた方が良い。 set -e で有効にした返り値のチェックは set +e で無効にできる。 以下のサンプルコードでは、条件分岐の間は返り値のチェックを無効にしている。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

set +e  # 一時的に返り値のチェックを止める

# 非ゼロを返す可能性があるコマンドを実行する
bash -c "exit 1"
if [ $? -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

set -e  # 終わったらエラーのチェックを戻す (各分岐処理の中でやっても良い)

上記を実行してみよう。 今度は、ちゃんと最後まで到達している。

$ bash errchk.sh 
+ set -e
+ set +e
+ bash -c 'exit 1'
+ '[' 1 -eq 0 ']'
+ echo ng
ng
+ set -e

返り値のチェックを無効にするのが上手くいかないパターン

ただし、返り値のチェックを無効にする範囲を最小化したいと考えて次のようにすると上手くいかない。 以下のサンプルコードでは条件分岐に使うコマンドを実行した直後に返り値のチェックを元に戻している。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# こうしたいんだけどダメなパターン
set +e
bash -c "exit 1"
set -e  # これもコマンドの実行なので $? が上書きされてしまう

# 分岐が必ず真になってしまう
if [ $? -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

しかし、上記では set -e もコマンドの実行なので、常に条件分岐が真になってしまう。

$ bash errchk.sh 
+ set -e
+ set +e
+ bash -c 'exit 1'
+ set -e
+ '[' 0 -eq 0 ']'
+ echo ok
ok

コマンドの返り値をシェル変数に退避させる

上記の問題を解消するには、コマンドの返り値をシェル変数などに退避させておくのが良いと思う。 これなら返り値のチェックを無効にする範囲をちゃんと小さくできる。

#!/usr/bin/env bash

# コマンドの実行履歴を出力する
set -x

# コマンドの返り値が非ゼロのとき停止する
set -e

# 返り値のチェックを止める影響を最小化する
set +e
bash -c "exit 1"
RET=$?  # コマンドの実行が終わったら返り値を変数に退避させる
set -e  # 退避したらエラーのチェックを戻す

# 分岐では退避させた変数を見る
if [ ${RET} -eq 0 ]; then
  echo "ok"
else
  echo "ng"
fi

上記を実行してみると、ちゃんと意図した通りに動作しているようだ。

$ bash errchk.sh 
+ set -e
+ set +e
+ bash -c 'exit 1'
+ RET=1
+ set -e
+ '[' 1 -eq 0 ']'
+ echo ng
ng

いじょう。