`&&` と `||` のショートサーキット評価を利用したトリッキーな bash script の書き方


tl;dr

次のような処理を行う関数を考える。

成功した時は "succeed" を出力して、終了コードは `0`
失敗した時は "failed" を出力して、終了コードは `1`

次に書いた straight 関数と tricky 関数の挙動は同じ。

straight() {
  if $(run-command) ; then
    echo "succeed"
    return 0
  else
    echo "failed"
    return 1
  fi
}

tricky() {
  { $(run-command) && echo "succeed"; } || { echo "failed" && false; } 
}

普段からこの形式で書くことはオススメはしないけど、ちょっとした時の tips として役に立つやも

背景

元々は「成功したら成功メッセージを書く」という処理を書いていたのだけど失敗した時の値を取りたいと思ったのがきっかけ。

# 元々のコード。ちょっとしたテストコード的なもの
assert() {
  local cmd=$1
  local expect=$2
  local actual=$($cmd)

  [[ "$actual" == *"$expect"* ]] && echo "succeed" >&2
}

このコードを、殆ど手直しすること無くデバッグ用のダンプを出そうとした結果

こうなった

# 変更後のコード。元のコードに殆ど手を加える必要がなかった事が勝因
assert() {
  local cmd=$1
  local expect=$2
  local actual=$($cmd)

  { [[ "$actual" == *"$expect"* ]] && echo "succeed" >&2; } \
    || { echo "Actual: $actual" && false; }
}

true コマンド、 false コマンドを使ったシンプルなバージョンに落とし込むと何が起きているか分かりやすい。

$ { true && echo "true"; } || { echo "false" && false; }
true
$ echo $?
0
$ { false && echo "true"; } || { echo "false" && false; }
false
$ echo $?
1

true の時はそのまま && 演算子の右辺の echo "true" が実行される。
この時どちらも成功ステータスなので { true && echo "true"; } の結果は成功ステータス。
そうすると || 演算子の挙動で右辺の評価はスキップされ、かつ成功ステータスが確定する。

ちなみに最初の値の結果によって右辺の評価をスキップする、この挙動を ショートサーキット評価 と呼ぶらしい

逆に false の時は最初の && がショートサーキット評価により失敗ステータスで確定する。
なのでそのまま次の || 演算子の右辺が評価される。今度は echo "false" は成功するものの、そのまま右辺も評価され、 false があることで失敗ステータスであることが確定する。

{} を使ってグルーピングしているのは評価の順番を確実にしたかった為。(というか思ったように動いてくれなかったため←)
{} のグルーピングはちゃんとセミコロン ; で終わらせないと syntax error になるのも注意

ちなみに

echo 関数が失敗したらこちらの目論見は見事に失敗します。
まぁデバッグ用だし、echo コマンドがそうそう失敗するわけないよねと高をくくっている実装です。

つまりは

日々の利用にはオススメしません。