シェルスクリプトの条件分岐

こんにちは。 yoichi22 です。先日同僚が書いたシェルスクリプトのコードレビューをしていて、こんな書き方できるんだという発見をしたと思ったけど、よく考えると普通の書き方だったという話をします。シェルとしては bash 前提です。

新発見?

レビューで目にしたコードはこんな感じのコードでした:

FEATURE_X_ENABLED=true
if $FEATURE_X_ENABLED; then
    echo feature X enabled
    # …実際はここに処理が続く...
fi

真偽値で条件分岐してそうな雰囲気があります。実際実行してみると

$ bash foo.sh
feature X enabled

直感的に期待していた通りにうまく動きました。コードを書き換えて真偽を入れ替えたらどうなるか試すと

$ cat foo.sh
FEATURE_X_ENABLED=false
if $FEATURE_X_ENABLED; then
    echo feature X enabled
    # …実際はここに処理が続く…
else
    echo feature X disabled
fi
$ bash foo.sh
feature X disabled

これも期待通りに else の方のパスが実行されました。 おお、boolean 使えるんだ、知らぬ間にシェルスクリプトの書き方も変わっていたのかと感心したのですが…

何か引っかかる気持ちがあったので、さらにtrue/false以外の値だったらどうなるのか試してみました

$ cat foo.sh
FEATURE_X_ENABLED=bar
if $FEATURE_X_ENABLED; then
    echo feature X enabled
    # …実際はここに処理が続く…
else
    echo feature X disabled
fi

これを実行すると、

$ bash foo.sh
foo.sh: line 2: bar: command not found
feature X disabled

偽の方のパスが実行されているけど、何やらエラーメッセージが出ています。というところでしばらく考えたら…、boolean が使えていたわけじゃないことがわかりました。

解説

条件分岐

シェルスクリプトで条件分岐をするときによく見る書き方は、次のようなものだと思います

$ cat foo.sh
if [ -f ${HOME}/.vimrc ]; then
    echo file exists
else
    echo file not found
fi
$ bash foo.sh
file exists

if に続く部分をコマンドと見なして実行したときに exit status が 0 かどうかで then のパスか else のパスのどちらかを実行するかの判定をします。上の例だと [ は bash 内蔵の test コマンドの別名ですが、

$ [ -f ${HOME}/.vimrc ]
$ echo $?
0

と exit status が 0 なので then の方のパスが実行されます。

実行されるコマンドは test コマンドに限定されるわけではないので、

$ cat foo.sh
if echo hello; then
    echo success
else
    echo fail
fi
$ bash foo.sh
hello
success

のように任意のコマンドの exit status で条件分岐ができます。ただこれは例が悪いですね。実際によく使われるのは test コマンドの他には grep コマンドなどだと思います。

true/false

true, false というのもbashの内蔵コマンドで、何もせずにそれぞれ exit status 0, 1 を返します。

$ true
$ echo $?
0
$ false
$ echo $?
1

よく見る使い方は、 set -e しているスクリプトで

set -e
# 先行処理
command-which-may-fail || true
# 後続処理

のように書いて、特定のコマンドの失敗を無視させる場合です。

まとめ

最初に挙げた例では、FEATURE_X_ENABLED に設定された文字列 true (あるいはfalse)をコマンドとして実行してその exit status で分岐するという動作になっており、boolean の値が存在するわけではありませんでした。

シェルスクリプトで定数で分岐させる場合には、慣習的に以下の書き方をよく見ます。

FEATURE_X_ENABLED=1
if [ "$FEATURE_X_ENABLED" -ne 0 ]; then
    echo feature X enabled
    # …実際はここに処理が続く...
fi

私はこの書き方をなんとなく好んで使っていましたが、最初の書き方だと FEATURE_X_ENABLED=true の行をコメントアウトしても then のパスが実行されてしまうのを別の同僚から教えてもらいました。一方でこちらの書き方だと FEATURE_X_ENABLED が未定義の場合 [ のエラー (integer expression expected) になり、間違ったときに気づきやすい利点もあることがわかりました。

以上、シェルスクリプトの if ではコマンドの実行結果で実行するパスを制御していることを復習したのと、慣習的にこう書くという書き方には理由があることを知ったというお話でした。