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?

local を付けただけで、set -e が効かなくなりました

0
Last updated at Posted at 2026-09-24

set -e を書いたスクリプトが、失敗しても止まりませんでした。

原因は1語です。local を付けていました。

先に断っておくと、この挙動は POSIX の仕様どおりで、ShellCheck も SC2155 で警告を出します。ここで書くのは、dash と bash と zsh の3つで測った結果と、local 以外のどのキーワードで起きるかです。

先に結論

# これは終了コードが取れる
out=$(cmd)
echo $?        # cmd の終了コード

# これは取れない。常に 0
local out=$(cmd)
echo $?        # local の終了コード(0)

# 分ければ取れる
local out
out=$(cmd)
echo $?        # cmd の終了コード

local を付けた瞬間、$? はコマンドのものではなく、local そのものの終了コードになります。

3つのシェルで測りました

終了コード7で終わるコマンドを使って、3通り試しました。

out=$(sh -c 'echo hi; exit 7')
echo "$?"
f() {
  local out=$(sh -c 'echo hi; exit 7')
  echo "$?"
}
f
f() {
  local out
  out=$(sh -c 'echo hi; exit 7')
  echo "$?"
}
f

結果です。

書き方 dash bash zsh
out=$(cmd) 7 7 7
local out=$(cmd) 0 0 0
local out と分ける 7 7 7

3つとも同じでした。シェルによる差はありません。

set -e と組み合わせると

こちらが本題です。4通り試しました。

set -e
out=$(sh -c 'exit 7')
echo "ここに来たら set -e が効いていない"

スクリプトは止まりました。終了コード7で終わります。想定どおりです。

set -e
f() { local out=$(sh -c 'exit 7'); echo "local付きなので通過"; }
f
echo "関数のあとも通過"

止まりませんでした。両方の echo が実行されて、スクリプトは終了コード0で終わります。

set -e が見ているのは、実行されたコマンドの終了コードです。local が0を返すので、失敗しなかったことになります。中で走った sh -c 'exit 7' は、コマンド置換の中で終わっていて、set -e の判定には出てきません。

残り2つは、受け止められる形です。

set -e
if out=$(sh -c 'exit 7'); then echo "成功"; else echo "失敗を検知 ($?)"; fi
set -e
out=$(sh -c 'exit 7') || echo "|| で受けた ($?)"

どちらも失敗を検知しました。$? に7が入ります。

dash と bash で、4通りとも同じ結果でした。

local だけではありません

同じことが、他のキーワードでも起きます。

f() { declare out=$(sh -c 'exit 7'); echo "$?"; }   # 0
f() { readonly out=$(sh -c 'exit 7'); echo "$?"; }  # 0
f() { export out=$(sh -c 'exit 7'); echo "$?"; }    # 0
f() { typeset out=$(sh -c 'exit 7'); echo "$?"; }   # 0

4つとも0でした。

宣言と代入を1行でやると、返ってくるのは宣言のほうの終了コードです。キーワードの種類ではなく、宣言と代入を同時にやったことが原因でした。

export は関数の外でも使うので、ここがいちばん踏みやすい気がします。

export TOKEN=$(get-token)   # get-token が失敗しても気づけない

ShellCheck は拾ってくれます

書いたあとに掛けたら、出ていました。

$ shellcheck script.sh
SC2155 (warning): Declare and assign separately to avoid masking return values.

「宣言と代入を分けて、返り値が隠れるのを避けなさい」。そのままです。

#!/bin/sh で書いていると、もう1つ出ます。

SC3043 (warning): In POSIX sh, 'local' is undefined.

local は POSIX の範囲外です。dash では動きますが、仕様としては保証されていません。#!/bin/sh と書くつもりなら、そもそも local を使わないという判断もあります。

わたしは、掛ける前に書いていました。先に通していれば、公開前に気づいていた種類のものです。

どう書くか

宣言と代入を分けます。

f() {
    local out
    out=$(cmd) || return "$?"
    printf '%s\n' "$out"
}

|| return "$?" を付けているのは、set -e に頼らないためです。関数が if や && || の条件として呼ばれると、その関数の中では set -e が効きません。dash・bash・zsh の3つで確かめました。呼ばれ方に左右されないよう、明示的に返しています。

POSIX の範囲で書くなら、local を使わずにこうします。

f() {
    out=$(cmd) || return "$?"
    printf '%s\n' "$out"
}

変数がグローバルになるので、名前を長めにするか、接頭辞を付けて衝突を避けます。

まとめ

  • local out=$(cmd) は、cmd の終了コードを消す。$? は常に0
  • declare readonly export typeset でも同じ
  • set -e があっても止まらない。宣言が成功しているため
  • 分けて書けば取れる。local out してから out=$(cmd)
  • ShellCheck の SC2155 が拾う。書いたら掛ける
  • #!/bin/sh なら local 自体が POSIX 外(SC3043)

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?