- 技術 💻
シェルスクリプトset -eとは|pipefailとtrap後始末
シェルスクリプトの set -e とは、コマンドが 0 以外の終了コードを返した時点で、スクリプトをその場で止める設定です。 何も設定しないシェルスクリプトは、途中のコマンドがエラーになっても次の行へ進みます。set -e を冒頭に書くと、エラーのあとに続きの処理が走るのを防げます。
よく一緒に使われるのが、未定義の変数を見つける -u、パイプの途中のエラーを拾う -o pipefail、そして終了時の後始末を登録する trap です。この記事では、それぞれを付けた時と付けない時の実行結果を1つずつ並べて違いを見ていきます。さらに、set -e が効かないケースを再現つきの一覧表にまとめ、コピペして使える冒頭テンプレートと、bash と sh(dash)での違いも確認しました。実行結果はすべて bash 5.2.21 で動かしたものです。
set -e・-u・pipefail・trapの早見表
| 書き方 | はたらき | 付けない場合 |
|---|---|---|
set -e |
コマンドが 0 以外で終わったら、その場でスクリプトを止める | エラーのあとも次の行へ進む |
set -u |
未定義の変数を使ったら、その場で止める | 空文字として扱われ、そのまま進む |
set -o pipefail |
パイプの途中でエラーがあれば、パイプ全体をエラーとして扱う | 最後のコマンドの終了コードだけを見る |
set -E |
関数やサブシェルの中でも ERR の trap を有効にする | 関数の中のエラーでは ERR の trap が動かない |
trap '処理' EXIT |
スクリプトが終わるときに必ず処理を実行する(後始末) | 途中で止まると一時ファイルが残る |
trap '処理' ERR |
コマンドがエラーになったときに処理を実行する(行番号の表示など) | どこで止まったかの手がかりが少ない |
set -x |
実行するコマンドを + 付きで表示する(調査用) |
表示されない |
冒頭に書く定番の1行は次のとおりです。
#!/usr/bin/env bash
set -euo pipefail
| まとめた書き方 | 内訳 |
|---|---|
set -eu |
-e と -u |
set -euo pipefail |
-e、-u、-o pipefail |
set -Eeuo pipefail |
上に加えて -E(ERR の trap を関数内にも引き継ぐ) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「シェルスクリプトset -eとは|pipefailとtrap後始末」
この記事の確認環境
| 項目 | 内容 |
|---|---|
| シェル | GNU bash 5.2.21 |
| 比較用の sh | dash 0.5.12(Ubuntu 系で /bin/sh が指すシェル) |
| 確認方法 | 記事内のスクリプトをファイルに保存し、bash ファイル名 で実行。直後に終了コードを表示 |
| 仕様の参照元 | bash 5.2 に付属するヘルプ(help set、help trap) |
help set では、3つのオプションは次のように説明されています。
-e Exit immediately if a command exits with a non-zero status.
-u Treat unset variables as an error when substituting.
pipefail the return value of a pipeline is the status of
the last command to exit with a non-zero status,
or zero if no command exited with a non-zero status
出力例の [終了コード: N] は、スクリプトを実行した直後の $? の値です。0 が正常終了、0 以外がエラーを表します。
付けた時と付けない時を並べて比べる
set -e の有無
存在しないファイルをコピーしようとするスクリプトで比べます。
#!/usr/bin/env bash
echo "1: コピーを始めます"
cp /no/such/file /tmp/dest
echo "2: コピーが終わりました"
$ bash e_off.sh
1: コピーを始めます
cp: cannot stat '/no/such/file': No such file or directory
2: コピーが終わりました
[終了コード: 0]
コピーできていないのに「終わりました」と表示され、終了コードも 0(正常) になりました。呼び出し元からは、うまくいったように見えます。2行目に set -e を足すと次のようになります。
#!/usr/bin/env bash
set -e
echo "1: コピーを始めます"
cp /no/such/file /tmp/dest
echo "2: コピーが終わりました"
$ bash e_on.sh
1: コピーを始めます
cp: cannot stat '/no/such/file': No such file or directory
[終了コード: 1]
cp の行で止まり、終了コードは 1 になりました。続きの処理は実行されません。
set -u の有無
変数名を打ち間違えた例です(target_dir を target_dri と書いています)。
#!/usr/bin/env bash
target_dir="/tmp/work"
echo "削除対象: ${target_dri}/cache"
echo "続きの処理"
$ bash u_off.sh
削除対象: /cache
続きの処理
[終了コード: 0]
未定義の変数は空文字になり、/tmp/work/cache のつもりが /cache になりました。 この値を rm -rf に渡していたら、意図しない場所を消すことになります。2行目に set -u を足すと、変数を使っている行(4行目)で止まります。
$ bash u_on.sh
u_on.sh: line 4: target_dri: unbound variable
[終了コード: 1]
エラーメッセージに行番号と変数名が出るので、打ち間違いにすぐ気づけます。
set -o pipefail の有無
パイプ(|)でつないだコマンドの終了コードは、既定では最後のコマンドのものになります。
#!/usr/bin/env bash
set -e
cat /no/such/file | sort | head -n 3
echo "パイプの終了コード: $? / 続きの処理"
$ bash p_off.sh
cat: /no/such/file: No such file or directory
パイプの終了コード: 0 / 続きの処理
[終了コード: 0]
先頭の cat はエラーですが、最後の head が正常に終わったため、パイプ全体は 0 です。set -e を付けていても止まりません。 set -eo pipefail に変えると次のようになります。
$ bash p_on.sh
cat: /no/such/file: No such file or directory
[終了コード: 1]
パイプの各コマンドの終了コードは、bash の配列 PIPESTATUS で個別に確かめられます。
$ bash -c 'false | true | (exit 3); echo "${PIPESTATUS[@]}"'
1 0 3
$ bash -c 'set -o pipefail; true | false | true; echo "rc=$?"'
rc=1
3つの比較まとめ
| 設定 | 付けない時 | 付けた時 |
|---|---|---|
-e |
cp がエラーでも続行、終了コード 0 |
cp の行で停止、終了コード 1 |
-u |
打ち間違えた変数が空文字になり /cache と表示 |
target_dri: unbound variable で停止 |
-o pipefail |
先頭の cat がエラーでもパイプは 0、続行 |
パイプが 1 になり、-e と合わせて停止 |
set -eが効かないケース一覧
set -e には、エラーがあっても止まらない場面がいくつかあります。「コマンドの結果を条件として使っている場所」では止めない、というのが基本の考え方です。それぞれ再現して確かめました。
| No. | ケース | 例 | 止まるか | 対応 |
|---|---|---|---|---|
| 1 | if・while・until の条件 | if grep -q ... ; then |
止まらない | 条件として使うのは想定どおり。そのままでよい |
| 2 | &&・|| の左辺 |
false && echo x |
止まらない | 最後の項がエラーなら止まる点に注意 |
| 3 | local や export と同じ行の代入 |
local v=$(cmd) |
止まらない | 宣言と代入を2行に分ける |
| 4 | パイプの途中 | cat x | wc -l |
止まらない | set -o pipefail を付ける |
| 5 | ! を付けたコマンド |
! false |
止まらない | 終了コードを反転させる用途に限る |
| 6 | 条件として呼ばれた関数の中 | if build; then |
関数内でも止まらない | 関数は単独の行で呼ぶ |
| 7 | 文字列の中のコマンド置換 | echo "$(cmd)" |
止まらない | いったん変数に代入してから使う |
| 8 | バックグラウンド実行 | cmd & |
止まらない | wait PID の終了コードを確かめる |
ケース1:ifの条件
set -e
if grep -q "ERROR" /no/such/file; then
echo "見つかりました"
fi
echo "n1: 続きます"
grep: /no/such/file: No such file or directory
n1: 続きます
[終了コード: 0]
if の条件に書いたコマンドは、結果を判定に使うためのものなので止まりません。止まってほしくないコマンドは if の条件に置く、という使い方ができます。
ケース2:&& や || の左辺
set -e
false && echo "ここは実行されません"
echo "n2a: 続きます"
false || echo "n2b: || の右辺を実行"
echo "n2c: 続きます"
true && false
echo "n2d: ここは表示されません"
n2a: 続きます
n2b: || の右辺を実行
n2c: 続きます
[終了コード: 1]
&& や || でつないだ並びでは、最後のコマンド以外のエラーでは止まりません。 true && false のように最後のコマンドがエラーになると止まります。
この性質は「止めずに終了コードを受け取る」書き方にも使えます。
errors=$(grep -c "ERROR" app.log || true) # 0件でも止めない
diff a.txt b.txt > /dev/null || rc=$? # 終了コードを変数に受ける
ケース3:localと同じ行のコマンド置換
set -e
f() {
local v=$(cat /no/such/file)
echo "n3a: local と同じ行だと続きます (v='$v')"
local w
w=$(cat /no/such/file)
echo "n3b: ここは表示されません"
}
f
cat: /no/such/file: No such file or directory
n3a: local と同じ行だと続きます (v='')
cat: /no/such/file: No such file or directory
[終了コード: 1]
local v=$(...) と書くと、行全体の終了コードは local コマンド自身の結果(成功) になり、コマンド置換のエラーが隠れます。export v=$(...) や readonly v=$(...) も同じ形です。
| 直す前 | 直した後 |
|---|---|
local v=$(cmd) |
local v の次の行に v=$(cmd) |
export PATH_X=$(cmd) |
PATH_X=$(cmd) の次の行に export PATH_X |
ケース4:パイプの途中
set -e
cat /no/such/file | wc -l
echo "n4: 続きます"
cat: /no/such/file: No such file or directory
0
n4: 続きます
[終了コード: 0]
wc -l が「0行」と答えて正常終了するため、止まりません。set -o pipefail を足すと止まります(前の章の p_on.sh と同じ動きです)。
ケース5:! を付けたコマンド
set -e
! false
echo "n5a: ! を付けたコマンドは対象外"
! true
echo "n5b: ! true でも続きます"
n5a: ! を付けたコマンドは対象外
n5b: ! true でも続きます
[終了コード: 0]
! で終了コードを反転させた行は、結果がどちらでも止まりません。
ケース6:条件として呼ばれた関数の中
見落としやすいのがこのケースです。
set -e
build() {
cat /no/such/file
echo "n6: 関数の中で止まらず、ここまで来ます"
}
if build; then
echo "n6: build は成功扱いです"
fi
cat: /no/such/file: No such file or directory
n6: 関数の中で止まらず、ここまで来ます
n6: build は成功扱いです
[終了コード: 0]
関数を if の条件や &&・|| の左辺として呼ぶと、関数の中全体で set -e が効かなくなります。 build || echo "ビルドでエラー" と書いた場合も同じです。関数の中でも止めたいときは、次のように単独の行で呼びます。
build # 単独の行で呼ぶ → 関数内のエラーで止まる
echo "ビルド完了"
ケース7:文字列の中のコマンド置換
set -e
echo "結果: $(cat /no/such/file)"
echo "n7: 続きます"
cat: /no/such/file: No such file or directory
結果:
n7: 続きます
[終了コード: 0]
行の終了コードは echo のものになるため止まりません。コマンド置換は、いったん変数に代入してから使うと、代入の行で止まります。
result=$(cat /no/such/file) # ここで止まる
echo "結果: $result"
ケース8:バックグラウンド実行
set -e
cat /no/such/file &
wait
echo "n12: バックグラウンドのエラーでは止まりません"
cat: /no/such/file: No such file or directory
n12: バックグラウンドのエラーでは止まりません
[終了コード: 0]
& で起動したコマンドの結果は、引数なしの wait では拾われません。pid=$! で PID を控えて wait "$pid" と書くと、そのコマンドの終了コードが wait の結果になり、エラーなら wait の行で止まることを確かめました(終了コード 1)。
逆に、思わぬところで止まるケース
set -e を付けると、エラーではないのに 0 以外を返すコマンドでも止まります。こちらも再現しました。
| ケース | 例 | 止まる理由 | 書き換え |
|---|---|---|---|
| 計算結果が 0 の算術式 | count=0; ((count++)) |
(( )) は結果が 0 だと終了コード 1 |
count=$((count + 1)) |
| grep で0件 | n=$(grep -c ERROR app.log) |
一致なしは終了コード 1 | 末尾に || true |
関数の最後が [ ] && cmd |
[ -f x ] && echo あり |
条件が偽だと関数の戻り値が 1 | if [ -f x ]; then ... fi |
| diff で差分あり | diff a b |
差分ありは終了コード 1 | diff a b || rc=$? |
pipefail 下の head |
yes | head -n 1 |
先に終わった側の相手が SIGPIPE で 141 | 一時的に set +o pipefail |
# n8.sh
set -e
count=0
((count++))
echo "n8: ここは表示されません"
$ bash n8.sh
[終了コード: 1]
メッセージが何も出ずに止まるので、原因に気づきにくい箇所です。count=$((count + 1)) に書き換えると、n8b: count=1 と表示されて最後まで進みました。
# n10.sh
set -e
check() {
[ -f /no/such/file ] && echo "あります"
}
check
echo "n10: ここは表示されません"
$ bash n10.sh
[終了コード: 1]
[ ... ] && コマンド 自体は && の左辺なので止まりませんが、それが関数の最後の行だと、関数の戻り値が 1 になり、呼び出した行で止まります。 if 文で書くと戻り値は 0 になります。
pipefail と head の組み合わせも確かめました。
$ bash n14.sh # set -euo pipefail のあとに yes | head -n 1
y
[終了コード: 141]
head が1行読んで終わると、書き込み先を失った yes がシグナル(SIGPIPE)で終了し、その終了コード 141 がパイプ全体の結果になります。
一時的に止めないようにする書き方
終了コードを自分で調べたいコマンドは、次のどちらかで書きます。
set -e
set +e # いったん -e を外す
diff /etc/hostname /etc/hosts > /dev/null
rc=$?
set -e # 元に戻す
echo "diff の終了コード: $rc"
rc2=0
diff /etc/hostname /etc/hosts > /dev/null || rc2=$?
echo "|| で受けた終了コード: $rc2"
diff の終了コード: 1
|| で受けた終了コード: 1
[終了コード: 0]
1行で済む || rc=$? の形が短く、戻し忘れもありません。
set -uで止まる場面と書き方
set -u を付けると、引数なしで実行したときの $1 も未定義として扱われます。既定値つきの展開 ${変数:-既定値} を使うと止まりません。
set -u
echo "引数1: ${1:-なし}"
echo "HOME の代わり: ${MY_HOME:-/tmp}"
arr=()
echo "要素数: ${#arr[@]}"
echo "中身: ${arr[@]}"
echo "引数1: $1"
$ bash u2.sh
引数1: なし
HOME の代わり: /tmp
要素数: 0
中身:
u2.sh: line 7: $1: unbound variable
[終了コード: 1]
| 書き方 | 意味 |
|---|---|
${1:-} |
未定義なら空文字として扱う |
${1:-default} |
未定義なら default を使う |
${1:?使い方: $0 ファイル名} |
未定義ならメッセージを出して止める |
[ $# -ge 1 ] |
引数の個数を先に確かめる |
空の配列 ${arr[@]} は、bash 5.2 では set -u でも止まりませんでした。bash 4.3 以前では止まることが一般に知られています(今回は未実行)。
trap EXITで後始末をする
set -e で途中終了するようになると、作りかけの一時ファイルが残るという新しい課題が出てきます。trap '処理' EXIT を登録しておくと、正常終了でも途中終了でも、スクリプトが終わるときに必ず処理が実行されます。
#!/usr/bin/env bash
set -euo pipefail
tmpfile=$(mktemp)
cleanup() {
rm -f "$tmpfile"
echo "後始末: 一時ファイルを削除しました"
}
trap cleanup EXIT
echo "作業中: $tmpfile を使います"
date > "$tmpfile"
cat /no/such/file
echo "ここは表示されません"
実行結果(一時ファイル名は実行のたびに変わるため、XXXXXXXXXX に置き換えています):
$ bash t1.sh
作業中: /tmp/tmp.XXXXXXXXXX を使います
cat: /no/such/file: No such file or directory
後始末: 一時ファイルを削除しました
[終了コード: 1]
cat の行で止まったあとに cleanup が動き、終了コードは元の 1 のまま呼び出し元に返っています。
| ポイント | 説明 |
|---|---|
trap は一時ファイルを作った直後に登録する |
登録より前に止まると後始末が動かない |
後始末の中は rm -f のように止まりにくい書き方にする |
後始末の中でエラーが起きると、正常終了のはずが終了コード 1 になった(rm を -f なしで試して確認) |
trap は同じ種類につき1つ |
2回登録すると後のものに置き換わる。処理は1つの関数にまとめる |
kill -9 では動かない |
強制終了はシェルが受け取れないため |
Ctrl+Cやkillのときの動き
bash では、kill(SIGTERM)で止められたときも EXIT の trap が動きました。dash では動かず、trap 'exit 143' TERM を足すと動きました。
# sig.sh
trap 'echo "後始末を実行しました"' EXIT
echo "待機中"
sleep 30 &
wait
| シェル | スクリプト | kill(SIGTERM)を送った結果 |
|---|---|---|
| bash 5.2 | sig.sh | 「後始末を実行しました」と表示、終了コード 143 |
| dash 0.5.12 | sig.sh | 後始末は表示されず、終了コード 143 |
| dash 0.5.12 | trap 'exit 143' TERM を追加 |
「後始末を実行しました」と表示、終了コード 143 |
どのシェルでも後始末を通したいなら、INT と TERM に exit を登録しておくと安心です。あとのテンプレートにも入れてあります。
trap ERRで止まった行番号を出す
set -e で止まったとき、コマンド自身がメッセージを出さないと、どこで止まったかがわかりません。trap '処理' ERR に行番号の表示を登録します。
#!/usr/bin/env bash
set -euo pipefail
trap 'echo "エラー: ${BASH_SOURCE[0]} の ${LINENO} 行目で止まりました (終了コード $?): ${BASH_COMMAND}" >&2' ERR
echo "1: 開始"
cp /no/such/file /tmp/dest
echo "2: ここは表示されません"
$ bash t2.sh
1: 開始
cp: cannot stat '/no/such/file': No such file or directory
エラー: t2.sh の 6 行目で止まりました (終了コード 1): cp /no/such/file /tmp/dest
[終了コード: 1]
| 変数 | 中身 |
|---|---|
$LINENO |
現在の行番号 |
$? |
直前のコマンドの終了コード |
$BASH_COMMAND |
実行していたコマンドの文字列 |
${BASH_SOURCE[0]} |
スクリプトのファイル名 |
trap の中身は一重引用符で囲みます。 二重引用符にすると $LINENO が登録時に展開され、5行目で止まっても trap を書いた2行目の番号が表示されました。
関数の中のエラーには set -E が必要
ERR の trap は、既定では関数の中に引き継がれません。
#!/usr/bin/env bash
set -euo pipefail
trap 'echo "エラー: ${LINENO} 行目 (終了コード $?)" >&2' ERR
copy_file() {
cp /no/such/file /tmp/dest
}
copy_file
$ bash t3.sh
cp: cannot stat '/no/such/file': No such file or directory
[終了コード: 1]
止まってはいますが、trap のメッセージが出ていません。set -Eeuo pipefail に変えると表示されます。
$ bash t3E.sh
cp: cannot stat '/no/such/file': No such file or directory
エラー: 6 行目 (終了コード 1)
[終了コード: 1]
関数を使うスクリプトでは、-E(大文字)も一緒に付けると覚えておくと便利です。help set にも「If set, the ERR trap is inherited by shell functions.」と書かれています。
なお、ERR の trap が動く条件は set -e と同じです。前の章の「効かないケース」(if の条件や && の左辺など)では、trap も動きません。
コピペ用の冒頭テンプレート
ここまでの内容をまとめたテンプレートです。「ここから本処理」より上をそのまま貼って使えます。
#!/usr/bin/env bash
set -Eeuo pipefail
# --- 後始末(正常終了でもエラーでも必ず実行) ---
tmpdir=$(mktemp -d)
cleanup() {
local rc=$?
rm -rf "$tmpdir"
exit "$rc"
}
trap cleanup EXIT
# --- エラー箇所の表示 ---
on_error() {
local rc=$? line=$1
echo "[ERROR] ${BASH_SOURCE[0]}:${line} で止まりました (終了コード ${rc}): ${BASH_COMMAND}" >&2
}
trap 'on_error $LINENO' ERR
# --- Ctrl+C / kill のときも後始末を通す ---
trap 'exit 130' INT
trap 'exit 143' TERM
# --- ここから本処理 ---
main() {
echo "一時フォルダ: $tmpdir"
echo "data" > "$tmpdir/work.txt"
grep -q "nothing" "$tmpdir/work.txt"
echo "ここは表示されません"
}
main "$@"
動作確認のため、本処理に「一致しない grep」を入れてあります。実行結果:
$ bash template.sh
一時フォルダ: /tmp/tmp.XXXXXXXXXX
[ERROR] template.sh:28 で止まりました (終了コード 1): grep -q "nothing" "$tmpdir/work.txt"
[終了コード: 1]
$ ls -d /tmp/tmp.XXXXXXXXXX
ls: cannot access '/tmp/tmp.XXXXXXXXXX': No such file or directory
メッセージを出さない grep -q で止まっても、ファイル名・行番号・コマンドが表示され、一時フォルダも消えていることを確かめました。
| 行 | 役割 |
|---|---|
set -Eeuo pipefail |
エラーで停止、未定義変数で停止、パイプ途中のエラーを拾う、ERR を関数にも引き継ぐ |
cleanup の local rc=$? と exit "$rc" |
後始末のあとも元の終了コードを返す |
trap 'on_error $LINENO' ERR |
止まった行番号を引数で渡す(一重引用符で囲む) |
trap 'exit 130' INT |
Ctrl+C のときも EXIT の後始末を通す |
main "$@" |
本処理を関数にまとめ、変数を local で閉じ込めやすくする |
調査のときは、set -x を足すと実行されるコマンドが + 付きで表示されます。
$ bash x.sh
+ name=taro
+ echo 'hello taro'
hello taro
bashとshでの違い
1行目が #!/bin/sh のスクリプトや、sh script.sh で起動した場合、Ubuntu 系では dash が使われます。同じスクリプトを dash 0.5.12 で動かした結果です。
| 機能 | bash 5.2 | dash 0.5.12(sh) |
|---|---|---|
set -e |
使える | 使える |
set -u |
使える(unbound variable) |
使える(parameter not set) |
set -o pipefail |
使える | Illegal option -o pipefail で停止 |
trap ... EXIT |
使える | 使える |
trap ... ERR |
使える | trap: ERR: bad trap と表示され、登録されない |
set -E |
使える | 未実行(ERR の trap がないため対象外) |
$BASH_COMMAND、PIPESTATUS |
使える | bash 固有の変数のため対象外 |
| シグナルで終了したときの EXIT trap | 動く | 動かない(TERM に exit を登録すると動く) |
((count++)) |
使える | count++: not found |
実際のエラー表示は次のとおりです。
$ sh p_on.sh
p_on.sh: 2: set: Illegal option -o pipefail
[終了コード: 2]
$ sh d5.sh # trap 'echo "ERR trap"' ERR のあとに echo "続き"
trap: ERR: bad trap
続き
[終了コード: 0]
$ sh d3.sh # set -eu のあとに echo "${v}"
d3.sh: 2: v: parameter not set
[終了コード: 2]
set -euo pipefail や trap ERR を使うなら、1行目は #!/usr/bin/env bash(または #!/bin/bash)にして、bash で実行します。 sh script.sh と起動すると1行目の指定は使われず、sh で実行される点にも気をつけます。
sh でも動かす必要があるスクリプトでは、次のように pipefail が使えるときだけ有効にする書き方があります。dash と bash の両方で ok と表示されることを確かめました。
#!/bin/sh
set -eu
(set -o pipefail) 2>/dev/null && set -o pipefail
echo "ok"
| 用途 | おすすめの冒頭 |
|---|---|
| bash で動かすと決めている | #!/usr/bin/env bash と set -Eeuo pipefail、trap は EXIT と ERR |
| sh でも動かす | #!/bin/sh と set -eu、trap は EXIT・INT・TERM |
コマンド置換とサブシェルの中の動き
かっこで囲んだサブシェル ( ... ) の中では set -e が引き継がれ、中のコマンドがエラーになるとサブシェルが終わり、呼び出した側も止まりました。
set -e
(cd /no/such/dir; echo "n11: サブシェルの中")
echo "n11: ここは表示されません"
n11.sh: line 2: cd: /no/such/dir: No such file or directory
[終了コード: 1]
一方、コマンド置換 $( ... ) の中は、bash の既定では set -e が引き継がれません。shopt -s inherit_errexit を有効にすると引き継がれます。
set -e
v=$(false; echo "置換の中で続いた")
echo "既定: v='$v'"
shopt -s inherit_errexit
w=$(false; echo "置換の中で続いた")
echo "ここは表示されません"
既定: v='置換の中で続いた'
[終了コード: 1]
| 場所 | set -e の引き継ぎ(bash 5.2 で確認) |
|---|---|
| 関数の中(単独の行で呼んだ場合) | 引き継がれる |
サブシェル ( ... ) |
引き継がれる |
コマンド置換 $( ... ) |
既定では引き継がれない。inherit_errexit で引き継がれる |
| 条件として呼んだ関数・サブシェル | 効かない(ケース6) |
現在の設定は set -o で一覧できます。set -euo pipefail のあとに確かめた結果です。
$ bash -c 'set -euo pipefail; set -o | grep -E "errexit|nounset|pipefail"'
errexit on
nounset on
pipefail on
書き終えたら確かめるチェックリスト
| 確認すること | 見るポイント |
|---|---|
| 1行目は bash になっているか | pipefail と trap ERR は dash では使えない |
local や export と同じ行でコマンド置換をしていないか |
宣言と代入を2行に分ける |
大事な関数を if の条件や && の左辺で呼んでいないか |
単独の行で呼ぶと、関数内のエラーで止まる |
0 件でもよい grep に || true を付けたか |
一致なしは終了コード 1 |
カウンタを ((i++)) で増やしていないか |
i=$((i + 1)) にする |
引数は ${1:-} や個数の確認で受けているか |
set -u では引数なしの $1 で止まる |
一時ファイルを作った直後に trap ... EXIT を登録したか |
途中で止まっても後始末が動く |
| わざとエラーにして動きを確かめたか | 存在しないファイルを読ませ、行番号の表示と後始末を見る |
よくある質問
Q. set -e と set -o errexit は同じですか?
A. 同じ設定です。-u は -o nounset、-x は -o xtrace という長い名前を持っています。set -euo pipefail を実行したあとに set -o で一覧を表示すると、errexit・nounset・pipefail がすべて on になっていました。
Q. 一部のコマンドだけエラーを無視したいときはどう書きますか?
A. コマンドの後ろに || true を付けます。終了コードを使いたいときは コマンド || rc=$? と書くと、止めずに値を受け取れます。数行まとめて外したいときは set +e と set -e で挟みます。
Q. コマンド置換の中でも set -e を効かせるには?
A. bash では、コマンド置換 $( ) の中は既定で set -e が引き継がれません。v=$(false; echo "続いた") を実行すると、v に「続いた」が入りました。shopt -s inherit_errexit(bash 4.4 以降で使えることが一般に知られています)を有効にしてから同じ行を実行すると、その行で止まることを確かめました。
Q. set -e を付ければエラー処理は十分ですか? A. 止まる場面が増える心強い設定ですが、この記事の一覧のとおり止まらない場面もあります。大事なコマンドは単独の行で実行する、宣言と代入を分ける、pipefail を併用する、の3点を合わせると、意図どおりに止まる範囲が広がります。
まとめ
set -eは、コマンドが 0 以外で終わった時点でスクリプトを止める設定です。付けない時は、エラーのあとも続行して終了コード 0 になりました。set -uは未定義変数、set -o pipefailはパイプ途中のエラーを拾います。3つを合わせたset -euo pipefailが冒頭の定番です。- set -e が効かないのは、if の条件、
&&・||の左辺、local v=$(cmd)、パイプ途中、!付き、条件として呼ばれた関数の中などです。 ((count++))や 0 件の grep のように、エラーではないのに止まる場面もあります。$((count + 1))や|| trueで書き換えます。trap cleanup EXITで一時ファイルの後始末、trap ... ERRで止まった行番号の表示ができます。関数を使うなら-Eも付けます。pipefailとtrap ERRは dash(sh)ではエラーになりました。 使うときは1行目を bash にします。- テンプレートを冒頭に貼り、本処理は
main関数にまとめると、どのスクリプトでも同じ形で書き始められます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「シェルスクリプトset -eとは|pipefailとtrap後始末」
株式会社RJCでは、シェルスクリプトのエラー処理や後始末の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。