はわわーっ

はわわわわっ

bashのエラー処理とか

bashのエラー処理まわりわからないので適当にメモ。

#!/bin/bash -eu

exec 3> >(tee e.log) >&3 2>&3

may_fail() {
  test $(( $1 % 3 )) -ne 1
}

commands_may_fail() {
  may_fail $1
  echo "may_fail err $?"
}

main() {
  local i=
  for i in `seq 0 9`; do
    echo "i=$i"
    commands_may_fail $i
  done
}

main

最初に作ったのがこんな感じ。
ログを残しつつループする処理があって、その中でエラーになるかもしれないコマンドがある。
基本的にどっかでエラーになったらその場で終了してほしいので -e つけてる。

とりあえず、このまま実行する。

$ ./e.bash 
i=0
may_fail err 0
i=1

i が 1 のときに may_fail が失敗するのでその場で実行が終わる。

で、ループの commands_may_fail の中でエラーが発生してもスクリプト実行は終了しないで次のループに行くようにしたかった。
ただし、エラーが発生したら commands_may_fail 自体はその場で抜けるようにしたい。

ググってみると、if の条件とか && でコマンドをつなげた場合は bash -e で終了することはないらしいのでそうしてみる。
$? で返り値もとれるらしい。

#!/bin/bash -eu

exec 3> >(tee e.log) >&3 2>&3

may_fail() {
  test $(( $1 % 3 )) -ne 1
}

commands_may_fail() {
  may_fail $1
  echo "may_fail err $?"
}

main() {
  local i=
  for i in `seq 0 9`; do
    echo "i=$i"
    commands_may_fail $i && :
    test $? -eq 0 && echo "success" || echo "error"
  done
}

main

これを実行すると

$ ./e.bash 
i=0
may_fail err 0
success
i=1
may_fail err 1
success
i=2
may_fail err 0
success
i=3
may_fail err 0
success
i=4
may_fail err 1
success
i=5
may_fail err 0
success
i=6
may_fail err 0
success
i=7
may_fail err 1
success
i=8
may_fail err 0
success
i=9
may_fail err 0
success

こんな感じになる。
may_fail がエラーになってもそこで戻ってこないで次の echo が実行されてしまった。
ついでに、echo の戻り値が 0 なので全部 success になってしまっている。
こうなってほしかったんじゃないんだよなぁ。

パイプ使ってコマンドをつなげると最後のコマンドの戻り値で bash -e の終了の判定するようなのでパイプにしてみる。
PIPESTATUS でそれぞれのコマンドの戻り値も取れるらしい。

#!/bin/bash -eu

exec 3> >(tee e.log) >&3 2>&3

may_fail() {
  test $(( $1 % 3 )) -ne 1
}

commands_may_fail() {
  may_fail $1
  echo "may_fail err $?"
}

main() {
  local i=
  for i in `seq 0 9`; do
    echo "i=$i"
    commands_may_fail $i | :
    test ${PIPESTATUS[0]} -eq 0 && echo "success" || echo "error"
  done
}

main

これで実行してみると

$ ./e.bash 
i=0
success
i=1
error
i=2
success
i=3
success
i=4
error
i=5
error
i=6
error
i=7
error
i=8
success
i=9
error

なんか意味不明なことになった。
success/error の出力もおかしいし、echo "may_fail err $?" の部分が出力されてない。
パイプでコマンドつなげたからそっちに向けられてしまったっぽい。

ということで、最終的にこうなった。

#!/bin/bash -eu

exec 3> >(tee e.log) >&3 2>&3

may_fail() {
  test $(( $1 % 3 )) -ne 1
}

commands_may_fail() {
  may_fail $1
  echo "may_fail err $?"
}

main() {
  local i=
  for i in `seq 0 9`; do
    echo "i=$i"
    commands_may_fail $i >&3 2>&3 | :
    test ${PIPESTATUS[0]} -eq 0 && echo "success" || echo "error"
  done
}

main

実行してみると

$ ./e.bash 
i=0
may_fail err 0
success
i=1
error
i=2
may_fail err 0
success
i=3
may_fail err 0
success
i=4
error
i=5
may_fail err 0
success
i=6
may_fail err 0
success
i=7
error
i=8
may_fail err 0
success
i=9
may_fail err 0
success

success/error の判定もちゃんとできているっぽいし、may_fail がエラーになるときは echo "may_fail err $?" が実行されていないのでその場で終了して次のループに進んでいることもわかる。
思ったように動いてるっぽい。

ただし、これだとパイプを使っている以上、set -o pipefail するとたぶん動かなくなりそう。
今のところ pipefail 使ってないからいいんだけど使う必要がでてくるとどう直せばいいんだろう…