関数の中で false を実行しても、スクリプトが止まりませんでした。set -e は書いてあります。
止まらなかったのは、関数を if の条件として呼んでいたからです。
もう1つ、bash だけ、x=$(f) の形でも止まりませんでした。dash と zsh は止まります。
先に断っておくと、条件として呼ばれたときに set -e が無効になるのは POSIX の仕様どおりです。bash のコマンド置換の扱いも、マニュアルに inherit_errexit として書いてあります。ここで書くのは、dash・bash・zsh の3つに同じスクリプトを渡して、6通りの呼び方を並べた結果です。3つが揃わない箇所が2つありました。
前回の記事で、local out=$(cmd) が終了コードを消す件を書きました。あちらは宣言と代入の話で、今回は呼び出し方の話です。別の罠です。
先に結果
関数の中身はこれです。
set -e
f() { false; echo "関数内: false のあとも続いた"; }
false で止まれば、echo は実行されません。止まらなければ、echo が出ます。これを6通りの呼び方で試しました。
| 呼び方 | dash | bash | zsh |
|---|---|---|---|
f |
止まる | 止まる | 止まる |
if f; then :; fi |
続く | 続く | 続く |
f && : |
続く | 続く | 続く |
f || : |
続く | 続く | 続く |
! f |
続く | 続く | 止まる |
x=$(f) |
止まる | 続く | 止まる |
素直に呼んだときだけ、3つとも止まりました。
if && || の3つは、3つとも続きます。ここは揃っています。
揃わなかったのは ! f と x=$(f) です。
条件として呼ぶと、中で効かない
まず、揃っている部分です。
if f の形で呼ぶと、f の中の false で止まりません。関数の最後まで走って、echo が出ます。&& の左辺と || の左辺も同じでした。
これは POSIX の決まりです。条件として評価されるコマンドの中では、set -e が無効になります。そうしないと、if grep ... のように失敗を想定した条件分岐が書けなくなるからです。
問題は、この無効化が関数の中まで及ぶことです。if f と書いた瞬間、f の中身全体が「条件の評価中」になります。関数の中で何が失敗しても、止まりません。
関数を書いた側は、set -e が効くつもりで書いています。呼ぶ側が if を付けただけで、前提が崩れます。
! f は zsh だけ違いました
! で否定して呼ぶと、dash と bash は最後まで続きました。
zsh は、関数の中は続きましたが、**呼び出しのあとで止まりました。**終了コードは1です。
関数の中で set -e が無効になる点は、3つとも同じです。違うのは、! f という式そのものの結果を set -e が見るかどうかでした。POSIX では ! の付いたパイプラインは set -e の対象外ですが、zsh はここで止めています。
! で否定して呼ぶ書き方は多くないと思いますが、zsh で書いたものを dash や bash に持っていくと、止まる場所が変わります。
bash は、コマンド置換に set -e を引き継ぎません
こちらのほうが、踏む機会は多いと思います。
set -e
f() { false; echo "関数内: 続いた"; }
x=$(f)
echo "呼び出しのあとも続いた x=[$x]"
dash と zsh は、x=$(f) で止まりました。終了コード1です。
bash は止まりませんでした。
$ bash test.sh
呼び出しのあとも続いた x=[関数内: 続いた]
$ echo $?
0
x に「関数内: 続いた」が入っています。つまり f の中で false のあとも処理が続いて、echo の出力がそのまま代入されました。
bash は、コマンド置換 $(...) の中に set -e を引き継ぎません。中で false が失敗しても、置換の中では止まらず、最後の echo が成功して、代入全体が成功扱いになります。
揃える方法
bash には、引き継がせる指定があります。
set -e
shopt -s inherit_errexit
これを入れると、bash でも止まりました。
$ bash test.sh # inherit_errexit あり
$ echo $?
1
もう1つ、bash を POSIX モードで動かした場合も止まりました。
$ bash --posix test.sh
$ echo $?
1
POSIX モードでは、この引き継ぎが既定で有効になるようです。
どう書くか
set -e に、関数の中の失敗を任せないことにしました。
呼ばれ方で効いたり効かなかったりするので、関数の中で失敗しうる箇所は、自分で返します。
f() {
cmd1 || return "$?"
cmd2 || return "$?"
echo "done"
}
こうしておけば、if f で呼ばれても、x=$(f) で呼ばれても、f は失敗した時点で抜けます。set -e が効くかどうかに関係なく、同じ動きになります。
bash で書くなら、冒頭に shopt -s inherit_errexit も入れておきます。
#!/bin/bash
set -eu
shopt -s inherit_errexit
ただ、これは bash だけの指定です。#!/bin/sh で書くなら使えません。どちらにしても、|| return を書いておくのがいちばん確実でした。
余談
最初にこの表を取ったとき、1つ目の行を「( f ) || echo」で測っていました。
素直に呼ぶつもりで書いたのですが、|| を付けた時点で条件の文脈になっています。3つとも続く、という結果が出て、しばらく「素直に呼んでも止まらないのか」と思いこんでいました。
測るためのコードが、測りたい条件を壊していたわけです。f だけを1行で書き直して、ようやく3つとも止まることを確かめられました。
まとめ
-
set -eは、関数をif&&||の条件として呼ぶと、その関数の中で効かない(3シェル共通) -
! fは、関数の中は続くが、zsh だけ呼び出しのあとで止まる -
x=$(f)は、bash だけ止まらない。コマンド置換にset -eを引き継がないため - bash は
shopt -s inherit_errexitか--posixで揃う - 関数の中の失敗は、
|| return "$?"で自分で返すのが確実
dash 0.5.12 / bash 5.2.21 / zsh 5.9 で確かめました。
ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。