- 技術 💻
シェルスクリプトの終了ステータス $?とexitコード一覧・判定方法
終了ステータスとは、コマンドが終わるときにシェルへ返す0〜255の整数で、0が「成功」、0以外が「成功しなかった」を表します。 直前のコマンドの値は特殊変数 $? に入り、if、&&、|| はすべてこの値を見て次の動きを決めています。
この記事では、0・1・2・126・127・128+n・130・137・143・255 の意味を、その値を実際に出せる再現コマンドつきの一覧表にまとめました。あわせて、$? が次のコマンドで上書きされる様子、&& と || をつないだときの評価順、パイプでの PIPESTATUS と set -o pipefail、set -e で止まらないケース、関数の return と exit の違い、256以上を指定したときの値を、bash 5.2.21(Ubuntu 24.04)で実行した結果とともに示します。
終了ステータスの早見表(exitコード一覧と再現コマンド)
「再現コマンド」はすべて実行して、表の値になることを確かめています。
| 値 | 意味 | 再現コマンド | 補足 |
|---|---|---|---|
| 0 | 成功 | true |
if や && が「真」と扱う唯一の値 |
| 1 | 一般的なエラー | false、grep -q zzz /etc/hostname、cd /nonexistent |
grep は「一致なし」が 1 |
| 2 | 使い方の誤り | [ 1 -eq ]、bash -c "if then"、exit abc |
組み込みコマンドの引数ミスや構文エラー。ls /nonexistent も 2 を返す |
| 124 | timeout による打ち切り | timeout 1 sleep 5 |
timeout コマンド独自の値 |
| 126 | 見つかったが実行できない | ./noexec.sh(実行権限なし)、/tmp |
権限不足、ディレクトリを実行 |
| 127 | コマンドが見つからない | nosuchcmd |
PATH の設定や綴りを確認 |
| 128+n | シグナル n で終了 | 下の3行を参照 | n はシグナル番号 |
| 129 | SIGHUP(1) で終了 | sleep 5 & kill -HUP $!; wait $! |
端末が切れたときなど |
| 130 | SIGINT(2) で終了 | sleep 5 & kill -INT $!; wait $! |
Ctrl+C に相当 |
| 137 | SIGKILL(9) で終了 | sleep 5 & kill -KILL $!; wait $! |
強制終了。メモリ不足による停止もこの値 |
| 141 | SIGPIPE(13) で終了 | yes | head -1 の yes 側 |
読み手が先に閉じたパイプへの書き込み |
| 143 | SIGTERM(15) で終了 | sleep 5 & kill -TERM $!; wait $! |
通常の停止要求 |
| 255 | 範囲の上限 | exit 255、exit -1 |
exit -1 は 255 になる |
$? の基本:直前のコマンドの結果を読む
ls /etc/hostname
echo $?
ls /nonexistent
echo $?
/etc/hostname
0
ls: cannot access '/nonexistent': No such file or directory
2
$? は「直前に実行した1つのコマンド」の終了ステータスです。値の意味は基本的にコマンドごとに決まっていて、共通しているのは 0 が成功、0以外が成功以外 という点だけです。たとえば grep は次のように3通りを使い分けます。
| grep の終了ステータス | 意味 |
|---|---|
| 0 | 一致する行があった |
| 1 | 一致する行がなかった |
| 2 | ファイルが開けないなどのエラー |
grep -q root /etc/passwd
case $? in
0) echo "見つかった" ;;
1) echo "見つからない" ;;
*) echo "grep 自体のエラー" ;;
esac
見つかった
「1 だから異常」とは限らないので、コマンドごとの意味は man の EXIT STATUS の節で確かめるのが確実です。diff も「差分あり」で 1 を返します(diff <(echo a) <(echo b) で 1 になることを確認しました)。
126 と 127 の違い
よく似た2つですが、原因が違います。
$ nosuchcmd
bash: line 1: nosuchcmd: command not found
=> 127
$ ./noexec.sh
bash: line 1: ./noexec.sh: Permission denied
=> 126
$ /tmp
bash: line 1: /tmp: Is a directory
=> 126
| 値 | 状態 | 主な対処 |
|---|---|---|
| 127 | コマンドが見つからない | 綴り、PATH、インストールの有無、cron での PATH を確認 |
| 126 | 見つかったが実行できない | chmod +x で実行権限を付ける、パスがディレクトリでないか確認 |
cron や CI で「手元では動くのに 127 になる」ときは、PATH が対話シェルと違っていることが多く、コマンドをフルパスで書くと解消します。
128+n:シグナルで終了したとき
プロセスがシグナルで終了すると、bash は 128 にシグナル番号を足した値を終了ステータスにします。
$ sleep 5 & kill -INT $!; wait $!
=> 130
$ sleep 5 & kill -KILL $!; wait $!
bash: line 1: 3027 Killed sleep 5
=> 137
$ sleep 5 & kill -TERM $!; wait $!
=> 143
終了ステータスからシグナル名を調べるには kill -l が使えます。
$ kill -l 137; kill -l 143; kill -l 130
KILL
TERM
INT
これを使うと、「シグナルで終わったかどうか」をスクリプトで判定できます。
sleep 30 & pid=$!
kill -9 $pid
wait $pid 2>/dev/null
rc=$?
if [ $rc -gt 128 ]; then
echo "シグナル $((rc-128)) ($(kill -l $rc)) で終了"
fi
シグナル 9 (KILL) で終了
| よく見る値 | シグナル | よくあるきっかけ |
|---|---|---|
| 130 | SIGINT (2) | 実行中に Ctrl+C を押した |
| 137 | SIGKILL (9) | kill -9、メモリ不足による強制終了、コンテナのメモリ上限 |
| 143 | SIGTERM (15) | kill(既定)、サービスやコンテナの停止処理 |
| 124 | (timeout 独自) | timeout の制限時間に達した |
timeout は少し特別で、制限時間で打ち切ると 124 を返します。ただし timeout -s KILL 1 sleep 5 のように KILL を送る指定では 137 になりました。
$? は次のコマンドで上書きされる
$? を扱うときに最もつまずきやすい点です。echo も [ ] も1つのコマンドなので、実行した時点で $? はそのコマンドの結果に置き換わります。
ls /nonexistent 2>/dev/null
echo "1回目: $?"
echo "2回目: $?"
ls /nonexistent 2>/dev/null
if [ $? -ne 0 ]; then echo "if の中: $?"; fi
1回目: 2
2回目: 0
if の中: 0
2回目の $? は「1回目の echo が成功した」ことを表す 0 です。if の中でも、[ $? -ne 0 ] という判定自体が成功したので 0 になっています。値をあとで使うなら、直後に変数へ移します。
ls /nonexistent 2>/dev/null
rc=$?
echo "rc=$rc"; echo "まだ rc=$rc"
rc=2
まだ rc=2
| 書き方 | $? の扱い |
向いている場面 |
|---|---|---|
cmd; rc=$? |
変数に保存 | 値そのものをログに出したい、複数の値で分岐したい |
if cmd; then ... |
if が直接見る |
成功かどうかだけわかればよい |
if ! cmd; then ... |
反転して見る | 成功しなかったときだけ処理したい |
cmd || 処理 |
|| が直接見る |
1行で済ませたい |
終了ステータスで分岐する:if・&&・|| の書き方
if にコマンドを直接書く
if の後ろには条件式ではなくコマンドを書きます。[ ... ] も実は [ というコマンドです。そのため、次の2つは同じ意味で、後者のほうが短く、$? の上書きも起きません。
# 直す前:$? を経由する
grep -q zzz /etc/hostname
if [ $? -ne 0 ]; then
echo "含まれていない"
fi
# 直した後:コマンドを直接書く
if ! grep -q zzz /etc/hostname; then
echo "含まれていない"
fi
含まれていない
エラーのときの値も使いたい場合は、else の先頭で受け取ります。
cmd() { return 2; }
if cmd; then
echo "成功"
else
echo "else: $?"
fi
else: 2
&& と || の評価順
A && B は「A が 0 なら B を実行」、A || B は「A が 0 以外なら B を実行」です。&& と || は優先順位が同じで、左から順に評価されます。 つないだときの結果を実際に確かめました。
true && echo A || echo B
false && echo A || echo B
true && false || echo "C"
false || echo D && echo E
true || echo F && echo G
false || false && echo J; echo "status=$?"
A
B
C
D
E
G
status=1
| 式 | 表示 | 読み方 |
|---|---|---|
true && echo A || echo B |
A | 左が成功 → A。A も成功したので B は飛ばす |
false && echo A || echo B |
B | 左が 1 → A を飛ばす。ここまでが 1 なので B |
true && false || echo C |
C | && の右が 1 を返すと、|| の右も動く |
false || echo D && echo E |
D と E | D が成功 → E も実行 |
true || echo F && echo G |
G | F は飛ばすが、「ここまで成功」なので G は実行 |
false || false && echo J |
なし | ここまでが 1 なので J を飛ばす。全体も 1 |
3行目が要注意です。A && B || C は if-else の省略形のように見えますが、B が 0 以外を返すと C も実行されます。B が必ず成功する(echo など)場合だけ省略形として使い、それ以外は if で書くと意図どおりになります。
# 省略形として使える(echo はまず成功する)
[ -f config.yml ] && echo "あり" || echo "なし"
# B が成功しない可能性があるなら if で書く
if [ -d work ]; then
rm -r work
else
echo "work はありません"
fi
! は終了ステータスを反転します。! false は 0、! true は 1 になりました。
パイプの終了ステータス:PIPESTATUS と pipefail
パイプでつないだとき、$? に入るのは一番右のコマンドの値だけです。
false | true; echo "false | true => $?"
false | true => 0
左側の結果も知りたいときは、bash の配列変数 PIPESTATUS を使います。パイプの各コマンドの値が左から順に入っています。
ls /nonexistent 2>/dev/null | sort | wc -l
echo "PIPESTATUS=${PIPESTATUS[*]}"
0
PIPESTATUS=2 0 0
PIPESTATUS も $? と同じく、次のコマンドで上書きされます。 間に echo を1つ挟むと、その echo の結果(要素1つ)に変わりました。
ls /nonexistent 2>/dev/null | sort | wc -l; echo "x"; echo "echo後: ${PIPESTATUS[*]}"
0
x
echo後: 0
複数の値を使うなら、直後に配列ごとコピーします。
ls /nonexistent 2>/dev/null | sort | wc -l
st=("${PIPESTATUS[@]}")
echo "ls=${st[0]} sort=${st[1]} wc=${st[2]}"
0
ls=2 sort=0 wc=0
set -o pipefail でパイプ全体の結果にする
set -o pipefail を有効にすると、パイプの $? が「0 以外を返したコマンドのうち、一番右のものの値」になります。全部が 0 のときだけ 0 です。
set -o pipefail
false | true; echo "false | true => $?"
ls /nonexistent 2>/dev/null | sort | wc -l; echo "ls | sort | wc => $?"
exit 3 | exit 5 | true; echo "3 | 5 | 0 => $?"
false | true => 1
0
ls | sort | wc => 2
3 | 5 | 0 => 5
| 方法 | わかること | 使える場所 |
|---|---|---|
$?(既定) |
右端のコマンドの値 | どのシェルでも |
${PIPESTATUS[@]} |
すべてのコマンドの値 | bash(配列を使うため) |
set -o pipefail |
0 以外があったかどうかを $? で |
bash など |
pipefail には1つ知っておきたい動きがあります。head のように途中で読むのをやめるコマンドを右に置くと、左のコマンドが SIGPIPE(141)で終わり、パイプ全体が 141 になります。
set -o pipefail
yes | head -1 >/dev/null; echo "yes | head -1 => $? (${PIPESTATUS[*]})"
yes | head -1 => 141 (141 0)
これは処理としては期待どおりなので、該当する行だけ || true を付けるか、PIPESTATUS で 141 を許容する判定にすると扱いやすくなります。
なお、Ubuntu の /bin/sh は dash で、PIPESTATUS は使えません(sh -c 'echo ${PIPESTATUS[0]}' は Bad substitution になりました)。PIPESTATUS を使うスクリプトは1行目を #!/bin/bash にします。
set -e で止まるケースと止まらないケース
set -e は「コマンドが 0 以外を返したら、その場でスクリプトを終える」設定です。ただし、終了ステータスが「判定に使われている」位置では止まりません。 1つずつ実行した結果が次の表です(「到達」は、その後ろの echo "到達" が実行されたかどうか)。
コード(先頭に set -e;) |
到達 | 全体の値 | 理由 |
|---|---|---|---|
false; echo "到達" |
しない | 1 | 基本の動作 |
false || echo "右"; echo "到達" |
する | 0 | || の左は判定扱い |
if false; then :; fi; echo "到達" |
する | 0 | if の条件は判定扱い |
! false; ! true; echo "到達" |
する | 0 | ! を付けたコマンドは対象外 |
false | true; echo "到達" |
する | 0 | パイプは右端の値で判断 |
set -o pipefail; false | true; echo "到達" |
しない | 1 | pipefail と併用すると止まる |
false && true; echo "到達" |
する | 0 | && の左は判定扱い |
x=$(false); echo "到達" |
しない | 1 | 代入だけの文は $(...) の値になる |
local x=$(false)(関数内) |
する | 0 | local 自体が成功するため |
export Y=$(false); echo "到達" |
する | 0 | export 自体が成功するため |
i=0; ((i++)); echo "到達" |
しない | 1 | 式の値が 0 だと (( )) は 1 を返す |
$ bash -c 'set -e; i=0; ((i++)); echo "到達"'
=> 1
最後の行は見落としやすい動きです。((i++)) は「加算前の値 0」が式の値になり、0 は偽なので終了ステータス 1 を返します。set -e のもとでは i=$((i+1)) と書くと止まりません。
関数を if や || の左で呼ぶと、関数の中でも止まらない
set -e
f() { false; echo "f の中で継続"; }
f || echo "f は非0扱い"
echo "到達"
f の中で継続
到達
f を || の左で呼んだため、関数の中の false でも止まらず、次の行まで進みました。しかも関数の最後の echo が成功したので、f 全体は 0 になり、|| の右も実行されていません。同じ関数を単独で f と呼んだ場合は、false の時点でスクリプトが終了しました(全体の値は 1)。
対策は、関数の中で必要な箇所に || return 1 を明示することです。
f() {
step1 || return 1
step2 || return 1
}
local と export は代入と宣言を分ける
# 直す前:$(...) の結果が local の成功で隠れる
local x=$(some_command)
# 直した後:宣言と代入を分ける
local x
x=$(some_command)
set -e の詳しい使い方や trap による後始末は別のテーマになるため、ここでは終了ステータスに関わる動きに絞りました。
関数の return と exit の違い
return n |
exit n |
|
|---|---|---|
| 終わるもの | その関数だけ | スクリプト(シェル)全体 |
| 呼び出し元 | 続きから実行される | 実行されない |
| 値の受け取り | 呼び出し直後の $? |
親プロセスの $? |
| 使える場所 | 関数の中、source で読み込まれたファイル |
どこでも |
| 数値を省略したとき | 直前のコマンドの値 | 直前のコマンドの値 |
#!/bin/bash
check_file() {
[ -f "$1" ] || return 3
echo "found: $1"
}
die() {
echo "die: $1" >&2
exit 4
}
check_file /etc/hostname; echo "check_file(あり) => $?"
check_file /nonexistent; echo "check_file(なし) => $?"
noreturn() { false; }
noreturn; echo "return なしの関数 => $? (最後のコマンドの値)"
out=$(die "サブシェル内"); echo "\$(die) の後も続く => $?"
die "ここで終了"
echo "この行は実行されない"
found: /etc/hostname
check_file(あり) => 0
check_file(なし) => 3
return なしの関数 => 1 (最後のコマンドの値)
die: サブシェル内
$(die) の後も続く => 4
die: ここで終了
スクリプト全体の終了ステータスは 4 でした。ここから3つのことがわかります。
returnを書かない関数は、最後に実行したコマンドの値を返します。 関数の最後がechoだと、途中でエラーがあっても 0 になります。$(...)の中のexitは、その中(サブシェル)だけを終わらせます。 外側のスクリプトは続き、値は$?で受け取れます。( ... )で囲んだ場合も同じで、(exit 3); echo $?は 3 を表示しました。- 関数の中で
exitを呼ぶと、関数ではなくスクリプト全体が終わります。
関数の外で return を使うとどうなるか
$ bash lib.sh # 中身は「return 7」だけ
lib.sh: line 1: return: can only `return' from a function or sourced script
=> 2
$ bash -c 'source ./lib.sh; echo "source後 => $?"'
source後 => 7
source(または .)で読み込むファイルでは return が使えます。逆に、読み込まれる側のファイルに exit 7 と書くと、読み込んだ側のシェルごと終了します(source ./lib2.sh の次の行は実行されず、全体が 7 で終わりました)。ライブラリとして読み込むファイルでは return を使うのが安全です。
return で返せるのは数値だけ
return は終了ステータスを返す仕組みなので、0〜255 の整数しか返せません。文字列や大きな数を返したいときは、echo で出力して $(...) で受け取ります。
get_name() { echo "report_$(date +%Y%m%d).csv"; }
name=$(get_name)
よく使うコマンドが返す値の一覧
「0 以外=異常」と決めつけないために、身近なコマンドが実際に返した値を並べます(すべて今回実行した結果です)。
| コマンド | 値 | 意味 |
|---|---|---|
[ -f /nonexistent ]、test 1 -gt 2 |
1 | 条件が偽 |
grep -q zzz /etc/hostname |
1 | 一致なし |
grep zzz /nonexistent |
2 | ファイルが開けない |
cmp -s /etc/hostname /etc/passwd |
1 | 内容が違う |
diff <(echo a) <(echo b) |
1 | 差分あり |
diff /etc/hostname /nonexistent |
2 | 比較できなかった |
mkdir src(すでにある) |
1 | 作成できなかった |
cp /nonexistent x |
1 | コピー元がない |
ls /nonexistent、sort /nonexistent、tar -tf /nonexistent.tar |
2 | ファイルが開けない |
find /nonexistent |
1 | 探索中にエラー |
command -v nosuchcmd |
1 | コマンドが見つからない(127 ではない) |
kill -0 999999 |
1 | そのプロセスは存在しない |
curl -s http://127.0.0.1:1/ |
7 | 接続できなかった(curl 独自の値) |
python3 -c "raise ValueError()" |
1 | 例外で終了 |
python3 -c "import sys; sys.exit(3)" |
3 | 指定した値 |
node -e "process.exit(5)" |
5 | 指定した値 |
command -v は「コマンドがあるかどうか」を終了ステータスで答えてくれるので、スクリプトの冒頭で必要な道具がそろっているかを確かめるのに向いています。
if command -v jq >/dev/null; then
echo "jq あり"
else
echo "jq なし"
fi
grep の「一致なし」を set -e のもとで扱う
set -eo pipefail のスクリプトで件数を数えようとすると、一致が 0 件のときに grep が 1 を返し、そこでスクリプトが終わります。
$ bash -c 'set -eo pipefail; x=$(grep zzz /etc/hostname | wc -l); echo "x=$x"'
=> 1
「0 件も正常」として続けたいときは、|| true で 1 を受け止めます。
set -eo pipefail
n=$(grep -c zzz /etc/hostname || true)
echo "n=$n"
n=0
ループで複数のファイルを処理し、最後にまとめて返す
1件でもうまくいかなかったら最終的に 1 を返したい、という場面では、途中で止めずに変数へ記録しておき、最後に exit へ渡します。
for f in /etc/hostname /nonexistent /etc/passwd; do
if ! [ -f "$f" ]; then
echo "skip $f"
err=1
continue
fi
echo "ok $f"
done
exit ${err:-0}
ok /etc/hostname
skip /nonexistent
ok /etc/passwd
全体の終了ステータスは 1 でした。${err:-0} は「err が未設定なら 0」という書き方です。
$(...) と代入の終了ステータス
コマンド置換 $(...) の値がどこに現れるかは、置かれた場所で変わります。
x=$(exit 4); echo "代入 => $?"
echo "$(exit 5)"; echo "echo の引数内 => $?"
代入 => 4
echo の引数内 => 0
代入だけの文では $(...) の値が $? になり、他のコマンドの引数として使った場合は、そのコマンド(ここでは echo)の値になります。 コマンド置換の結果を確かめたいときは、いったん変数に代入してから使うと、値を取りこぼしません。
256以上や負の数を指定したとき
終了ステータスは8ビット(0〜255)で親プロセスに渡されます。範囲の外の値を指定すると、256 で割った余りになります。
$ exit 255
=> 255
$ exit 256
=> 0
$ exit 257
=> 1
$ exit 1000
=> 232
$ exit -1
=> 255
| 指定した値 | 実際の終了ステータス | 計算 |
|---|---|---|
| 255 | 255 | そのまま |
| 256 | 0 | 256 を 256 で割った余り |
| 257 | 1 | 余り 1 |
| 300 | 44 | 余り 44 |
| 1000 | 232 | 1000 = 256×3 + 232 |
| -1 | 255 | 256 - 1 |
exit 256 は 0、つまり「成功」として伝わる点に気をつけます。件数やエラー数をそのまま exit に渡すと、ちょうど 256 件のときに成功扱いになります。件数は標準出力で返し、終了ステータスは「0 か、少数の決まった値」にすると混乱がありません。
関数の return も同じで、return 256 は 0、return 300 は 44 になりました。bash 以外でも同様で、sh -c 'exit 256'(dash)は 0、python3 -c 'import sys; sys.exit(300)' は 44 でした。
数値以外を渡した場合は、メッセージが出て 2 になります。
$ exit abc
bash: line 1: exit: abc: numeric argument required
=> 2
自分で決めるときの目安
| 値 | 使い方の目安 |
|---|---|
| 0 | 成功 |
| 1 | 一般的なエラー |
| 2 | 引数や使い方の誤り |
| 3〜125 | スクリプト独自の意味(入力なし、設定の誤り など)を割り当てる |
| 126〜165、255 | シェルやシグナルが使う値と重なるので、独自の意味には使わない |
bash のマニュアル(Exit Status の節)にも、終了ステータスは 0〜255 で、シェルが 125 より大きい値を特別に使うことがあると書かれています。独自の値は 3〜125 の範囲から選ぶと、126・127・128+n と区別できます。
コピペで使えるひな形:終了ステータスを使い分けるスクリプト
ここまでの内容を1つにまとめた例です。引数の誤りは 2、入力が見つからないときは 66、処理が完了しなかったときは 1 を返します。
#!/bin/bash
# backup.sh : 終了ステータスを使い分けるひな形
set -euo pipefail
readonly E_USAGE=2 # 使い方の誤り
readonly E_NOINPUT=66 # 入力が見つからない
readonly E_FAILED=1 # 処理がうまくいかなかった
usage() {
echo "usage: $0 <コピー元ディレクトリ> <出力先ファイル.tar.gz>" >&2
exit "$E_USAGE"
}
[ $# -eq 2 ] || usage
src=$1
dest=$2
if [ ! -d "$src" ]; then
echo "error: $src が見つかりません" >&2
exit "$E_NOINPUT"
fi
trap 'rc=$?; [ "$rc" -eq 0 ] || echo "終了ステータス $rc で止まりました" >&2' EXIT
if tar -czf "$dest" -C "$src" . ; then
echo "ok: $dest を作成しました"
else
rc=$?
echo "error: tar が $rc を返しました" >&2
exit "$E_FAILED"
fi
count=$(tar -tzf "$dest" | wc -l)
echo "entries: $count"
src/ に2つのファイルを置いて実行した結果です。
$ bash backup.sh; echo "=> $?"
usage: backup.sh <コピー元ディレクトリ> <出力先ファイル.tar.gz>
=> 2
$ bash backup.sh /nonexistent out.tar.gz; echo "=> $?"
error: /nonexistent が見つかりません
=> 66
$ bash backup.sh src out.tar.gz; echo "=> $?"
ok: out.tar.gz を作成しました
entries: 3
=> 0
$ bash backup.sh src /nonexistent/out.tar.gz; echo "=> $?"
tar (child): /nonexistent/out.tar.gz: Cannot open: No such file or directory
tar (child): Error is not recoverable: exiting now
tar: /nonexistent/out.tar.gz: Cannot write: Broken pipe
tar: Child returned status 2
tar: Error is not recoverable: exiting now
error: tar が 2 を返しました
終了ステータス 1 で止まりました
=> 1
呼び出す側は、値に応じて次の動きを決められます。
bash backup.sh src out.tar.gz && echo "次の処理へ" || echo "通知する"
ok: out.tar.gz を作成しました
entries: 3
次の処理へ
このひな形のポイントは次のとおりです。
- エラーメッセージは
>&2で標準エラーに出す。 標準出力は結果のデータ用に空けておきます。 if コマンド; then ... else rc=$?の形で、値をelseの先頭で受け取る。trap ... EXITの中の$?は、終了しようとしている値。 終了時に共通の記録を残せます。なお、trap の中でexit 9のように書くと、最終的な終了ステータスは 9 に置き換わりました。値を保ちたいときは trap の中でexitを書かないようにします。
つまずきやすい点のチェックリスト
| 症状 | 確かめること |
|---|---|
$? がいつも 0 になる |
間に echo や [ ] を挟んでいないか。直後に rc=$? で保存する |
| パイプの左のエラーに気づけない | set -o pipefail か ${PIPESTATUS[@]} を使う |
set -e なのに止まらない |
if・&&・||・! の中、local x=$(...) になっていないか |
set -e で意図せず止まる |
((i++))、grep の一致なし(1)。必要な行に || true を付ける |
| 127 が返る | コマンド名の綴り、PATH、cron 環境での PATH |
| 126 が返る | 実行権限(chmod +x)、パスがディレクトリでないか |
| 137 が返る | 強制終了。メモリ使用量や停止処理の待ち時間を確認 |
exit の値が思った数字にならない |
256 以上や負の数を渡していないか |
| 関数が常に 0 を返す | 関数の最後が echo になっていないか。return を明示する |
| 未定義の変数で止まった | set -u のもとでは未定義変数の参照でスクリプトが終了する(bash -c での確認では 127 でした) |
よくある質問
Q. 終了ステータスと「戻り値」は同じものですか?
A. シェルスクリプトでは同じものを指して使われることが多い言葉です。コマンドや関数が返す 0〜255 の整数が終了ステータスで、$? で読み取れます。他の言語の「戻り値」のように文字列や配列は返せないため、データは標準出力で受け渡します。
Q. if [ $? -eq 0 ] と if コマンド はどちらがよいですか?
A. 成功したかどうかだけを見るなら if コマンド; then が短く、$? の上書きも起きません。値そのものをログに出したい、複数の値で分岐したいときは、直後に rc=$? で保存してから case "$rc" in で分けると読みやすくなります。
Q. 終了ステータス 1 と 2 はどう使い分けますか? A. 決まった規則はありませんが、bash の組み込みコマンドは使い方の誤り(引数不足や不正なオプション)に 2 を返します。自作スクリプトでも「引数の誤りは 2、それ以外のエラーは 1」とそろえると、呼び出す側が原因を切り分けやすくなります。
Q. バックグラウンドで動かしたコマンドの終了ステータスはどう取りますか?
A. wait プロセスID の終了ステータスが、そのコマンドの値になります。(exit 42) & wait $!; echo $? は 42 を表示しました。$! は直前にバックグラウンドで起動したプロセスの ID です。
まとめ
- 終了ステータスは 0〜255 の整数で、0 が成功。 意味はコマンドごとに決まっており、grep や diff の 1 は「一致なし」「差分あり」を表します。
- 126 は実行できない、127 は見つからない、128+n はシグナル n で終了。 130 は Ctrl+C、137 は強制終了、143 は停止要求です。
$?は次のコマンドで上書きされるので、直後にrc=$?で保存するか、if コマンド; thenと直接書きます。&&と||は左から順に評価される。A && B || Cは B が 0 以外でも C が動きます。- パイプの
$?は右端の値。 途中の値は${PIPESTATUS[@]}、全体として知りたいならset -o pipefailを使います。 set -eはif・&&・||・!の中とlocal x=$(...)では止まりません。returnは関数だけ、exitはスクリプト全体を終える。 256 以上は 256 で割った余りになり、exit 256は 0 になります。
参考・出典
- GNU Bash Reference Manual「Exit Status」(今回開いて、0〜255 の範囲、126・127・128+N、組み込みコマンドの 2 の記述を確認)
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。