0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

set -e は、関数を if や && で呼ぶと中で効きません。bash だけコマンド置換でも抜けます

0
Posted at

関数の中で 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まわりのことを書いています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?