シェルスクリプトの 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?