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

返り値が非ゼロになるコマンドを実行したい

ただ、この機能を有効にしていると困るときもある。 例えば、返り値が非ゼロになるコマンドをどうしても実行したい場合について。

まずは正攻法から。 次のように set +e を使って一時的に返り値のチェックを無効化できる。

#!/usr/bin/env bash

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

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

set +e  # 一時的に機能を無効化する
bash -c "exit 1"  # 返り値が非ゼロになるコマンドを実行する
set -e  # コマンドの実行が終わったら再び有効化する

# ちゃんとここまで到達する
echo "Hello, World"

上記を実行してみよう。 ちゃんと返り値が非ゼロになるコマンドも実行できていることがわかる。

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

別解

ただ、上記は割と冗長なので、以下のようにもできるようだ。 一連のコマンドの返り値が必ずゼロになるようにするアプローチ。

#!/usr/bin/env bash

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

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

# 一連のコマンドとして返り値が必ずゼロになる
bash -c "exit 1" || true

# ちゃんとここまで到達する
echo "Hello, World"

上記を実行してみよう。 ちゃんと返り値が非ゼロになるコマンドも実行できている。

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

返り値を使って条件分岐したい

続いては、特定のコマンドの返り値を使って条件分岐したいというパターン。

以下のサンプルコードでは、何もケアをしていないため返り値が非ゼロになると処理が止まってしまう。

#!/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'

ゼロか非ゼロかしか見ない場合

まず、コマンドの返り値がゼロか非ゼロかしか見ないのであれば if に直接コマンドを埋め込んでしまえば良い。 if は元々内側のコマンドの返り値がゼロか非ゼロかで分岐する構文になっているため。

このアプローチを採用したサンプルコードは次の通り。

#!/usr/bin/env bash

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

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

# if であれば直接コマンドを埋め込んでしまえば良い
if bash -c "exit 1"; then
  echo "ok"
else
  echo "ng"
fi

上記を実行した結果が次の通り。 set -e の影響を受けることなく意図通りに処理されていることがわかる。

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

それ以外の返り値も確認したい場合

それ以外の返り値も確認したい場合には、ちょっと面倒になる。 まず、基本となるアプローチとしては直前に実行したコマンドの返り値が得られる変数 $? を使うことになる。 しかし、以下のように非ゼロになりうるコマンドの前後で無効化・有効化をしてしまうとうまくいかない。

#!/usr/bin/env bash

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

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

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

# 分岐が意図通りにいかない
if [ $? -eq 100 ]; then
  echo "ok"
else
  echo "ng"
fi

上記を実行してみよう。 本来なら "ok" になってほしいところが "ng" になってしまった。 これは set -e によって $? の値が上書きされてしまったため。

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

そのため、次のようにコマンドの返り値を別の変数に退避しておく必要がある。

#!/usr/bin/env bash

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

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

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

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

上記を実行してみよう。 今度はちゃんとうまくいった。

$ bash errchk.sh 
+ set -e
+ set +e
+ bash -c 'exit 100'
+ RET=100
+ set -e
+ '[' 100 -eq 100 ']'
+ echo ok
ok

別解 (別プロセスのシェルで実行できる場合)

なお、別プロセスのシェルで実行できる処理であれば、コマンド置換を使って以下のように書くこともできる。 この方法であれば set -e されていない別シェルで実行しているのと同じ扱いになるので、一時的に set +e する必要がない。

#!/usr/bin/env bash

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

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

# コマンド置換を使って返り値を求める
RET=$(bash -c "exit 100"; echo $?)

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

上記を実行してみよう。 ちゃんとうまくいっているようだ。

$ bash errchk.sh 
+ set -e
++ bash -c 'exit 100'
++ echo 100
+ RET=100
+ '[' 100 -eq 100 ']'
+ echo ok
ok

また、このやり方であれば以下のように条件分岐の中に埋め込むこともできる。 これは if 文に test コマンドを使わず直接実行したいコマンドを埋め込んだ最初のパターンに近い簡便さになってると思う。

#!/usr/bin/env bash

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

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

# 条件分岐に埋め込むパターン
if [ $(bash -c "exit 100"; echo $?) -eq 100 ]; then
  echo "ok"
else
  echo "ng"
fi

別解 (set +e を使わないアプローチ)

もうひとつ、最初の非ゼロのコマンドを実行するときの別解に似たやり方が次の通り。 コマンドの実行結果をワンライナーで変数に退避させることで set +e が不要になっている。

#!/usr/bin/env bash

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

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

# 変数を初期化する
RET=0
# 一連のコマンドとして返り値をゼロにするアプローチ
bash -c "exit 100" || RET=$?

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

実行結果は次の通り。

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

いじょう。