ループは Bash スクリプトの心臓部です。ところが実務では、continue を場当たり的に使って“とりあえず先へ進む”うちに、後始末が抜けたり、終了コードが伝わらずに不具合が潜る――そんな崩れ方が起きがちです。本稿は既存の「continue」解説を土台に、break の正しい使いどころと終了コード設計まで含めて、ループの“出口”を意図どおりに制御するための指針をまとめます。
対象は for/while/until。スキップ(continue)と脱出(break)、そして関数の return/スクリプトの exitの役割分担をはっきりさせ、ネスト時の continue N/break N、ストリーム処理の while read -r で起きやすい罠、後始末の一元化までを、短いコード例と設計の意図で解説します。
環境は Bash 5 系、set -euo pipefail 前提。目標はシンプルです。「何をスキップし、いつ中断し、どの失敗をどこへ返すか」をあらかじめ決め、コードに落とし込めるようになること。読み終える頃には、continue を“失敗の隠蔽”にせず、break と終了コードで意図が外に伝わるループを書けるようになります。
まずは出口の整理:continue/break/return・exit
continue は「現在の1回分だけスキップして、次の反復へ進む」ふるまいです。break は「いま居るループ(必要なら外側も含めて)から脱出する」ふるまいです。
関数の中なら return、スクリプト全体を終えるなら exit を使い、どこに結果(終了コード)を返すかを決めます。
# 例:1,2,skip,3 を処理。skip は飛ばし、3 で目的達成 → 脱出
set -euo pipefail
nums=(1 2 "skip" 3 4)
for n in "${nums[@]}"; do
[[ "$n" == "skip" ]] && continue # スキップ
if (( n == 3 )); then
echo "見つけたので脱出"
break # 脱出
fi
echo "処理: $n"
done
continue の基本:弱い失敗は“記録して流す”
「検証エラーはログに残して処理は続ける」という方針なら continue が向きます。“握りつぶさない” のがコツです。件数や最初のエラー原因を変数に記録し、ループの外でまとめて判断します。
set -euo pipefail
invalid=0
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue # 空行・コメントは弱い無視
if ! printf '%s\n' "$line" | grep -qE '^[a-z0-9_-]{3,}$'; then
((invalid++))
printf 'WARN: 無効な名前: %s\n' "$line" >&2
continue
fi
echo "OK: $line"
done < <(printf '%s\n' "alice" "" "#comment" "too-long-&&" "bob")
# ループ後にまとめて判断
if (( invalid > 0 )); then
echo "無効行: $invalid 件" >&2
fi
continue を使うと後始末が飛びがちなので、後片付けは trap で一元化しておくと安心です。
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT # continue でも break でも必ず実行される
break の基本:目的達成・上限到達で“きれいに抜ける”
「最初の成功で十分」「一定回数で打ち切る」など、処理を中断する条件がはっきりしているときは break が合います。見つけた結果を変数に保存してから抜けると、ループ外の処理が書きやすくなります。
set -euo pipefail
found=""
for host in 10.0.0.{1..10}; do
if ping -c1 -W1 "$host" >/dev/null 2>&1; then
found="$host"
break
fi
done
if [[ -n "$found" ]]; then
echo "最初に応答したホスト: $found"
# ここで found を使う処理 …
else
echo "応答なし" >&2
exit 1
fi
ネストしたループでは break 2 のように段数を指定できます。ただし深くなるほど読みづらくなるので、関数に切り出して return で早期退出する方が保守しやすいことが多いです。
search() {
local target="$1"
local i j
for i in {1..3}; do
for j in {a..c}; do
[[ "$i$j" == "$target" ]] && { printf '%s\n' "$i$j"; return 0; }
done
done
return 1
}
if result="$(search "2b")"; then
echo "見つかった: $result"
else
echo "見つからない" >&2
fi
return と exit:どこに結果を返すのか
関数の成否は return の終了コードで返し、呼び出し元で判断します。スクリプト全体の成否は exit で返します。ループの中から直接 exit してしまうと、片付けやログが飛ぶことがあるため、基本は break→外で判断→必要なら exit の順がおすすめです。
process_item() {
local x="$1"
[[ "$x" =~ ^[0-9]+$ ]] || { echo "NG:$x" >&2; return 1; }
echo "OK:$x"
}
final=0
for x in "42" "oops" "7"; do
if ! process_item "$x"; then
final=1 # 失敗は記録して続ける
continue
fi
done
exit "$final" # 最終結果として一度だけ外へ返す
ストリーム処理:while read -r とパイプの罠
… | while read の形は、多くの環境で while がサブシェルになり、ループ内で更新した変数が外へ戻りません。回避策として プロセス置換 < <(...) を使うと安全です。
set -euo pipefail
count=0
# 悪い例: 変数が外に反映されない可能性
# printf '%s\n' a b c | while read -r s; do ((count++)); done
# echo "$count" # 0 のまま…
# 良い例: プロセス置換で同一シェル内
while IFS= read -r s; do
((count++))
done < <(printf '%s\n' a b c)
echo "件数: $count" # 3
失敗の設計:終了コードをどう決めて、どう伝えるか
終了コードは「0=成功、非0=失敗」が約束事です。ループや複数処理のときに大切なのは、どんなポリシーで最終結果を決めるかを先に決めることです。例として三つの考え方を示します。
「全件成功なら0」
どれか1つでも失敗したら非0にする、もっとも厳格な判定です。
final=0
for path in "${files[@]}"; do
if ! do_task "$path"; then
final=1
fi
done
exit "$final"
「最初の失敗コードを返す」
最初に起きた失敗の種類(終了コード)を外へ伝えたいときに向きます。
first_fail=0
for path in "${files[@]}"; do
if ! do_task "$path"; then
first_fail=$? # 0 以外が入る
break # ここで打ち切る方針もあり
fi
done
exit "$first_fail"
「1件でも成功したら0」
フェイルオーバーや候補列の試行で、成功が一つでもあれば OK とする判定です。
ok=1
for endpoint in "${candidates[@]}"; do
if try_connect "$endpoint"; then
ok=0
break
fi
done
exit "$ok"
ミニ実践:continue と break を組み合わせる
入力データの検証エラーはスキップしつつ、目的が達成できたら脱出、最後に結果を一度だけ返す流れです。
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
target="admin"
found=""
invalid=0
while IFS= read -r user; do
[[ -z "$user" || "$user" == \#* ]] && continue
if ! [[ "$user" =~ ^[a-z][a-z0-9_-]{2,}$ ]]; then
((invalid++))
printf 'WARN: 無効ユーザー: %s\n' "$user" >&2
continue
fi
if [[ "$user" == "$target" ]]; then
found="$user"
break
fi
done < users.txt
if [[ -n "$found" ]]; then
echo "見つかった: $found"
exit 0
fi
printf 'INFO: 見つからず(無効行 %d 件)\n' "$invalid" >&2
exit 1
この形だと、continue は「弱い失敗の整理」、break は「目的達成の早期終了」、最終の exit は「外へ結果を1回だけ伝える」という役割分担になります。
後始末が必要なら、先に触れたように trap を仕掛けておくと、どの出口を通ってもクリーンに終われます。
まとめ
continue は“先へ進むためのスキップ”、break は“ここで終えるための脱出”です。そして return と exit は「どこへ結果を返すか」を決めるスイッチです。
まずは、何をスキップし、いつ中断し、どの失敗をどこへ返すかを言葉で決めてからコードに落とし込んでみてください。trap による後始末の一元化、while read -r とプロセス置換の組み合わせ、集計変数による終了コード設計――この三点を押さえるだけで、ループはぐっと壊れにくく、読みやすくなります。
