bash continueの使い方|breakとの違い・終了コード設計まで学ぶループ制御実践

スクリプト設計

ループは Bash スクリプトの心臓部です。ところが実務では、continue を場当たり的に使って“とりあえず先へ進む”うちに、後始末が抜けたり、終了コードが伝わらずに不具合が潜る――そんな崩れ方が起きがちです。本稿は既存の「continue」解説を土台に、break の正しい使いどころ終了コード設計まで含めて、ループの“出口”を意図どおりに制御するための指針をまとめます。

対象は forwhileuntilスキップ(continue)と脱出(break)、そして関数の return/スクリプトの exitの役割分担をはっきりさせ、ネスト時の continue Nbreak N、ストリーム処理の while read -r で起きやすい罠、後始末の一元化までを、短いコード例と設計の意図で解説します。

環境は Bash 5 系、set -euo pipefail 前提。目標はシンプルです。「何をスキップし、いつ中断し、どの失敗をどこへ返すか」をあらかじめ決め、コードに落とし込めるようになること。読み終える頃には、continue を“失敗の隠蔽”にせず、break と終了コードで意図が外に伝わるループを書けるようになります。

continueの基本構文(最初に押さえること)

continue は、forwhileuntil ループの中で使う組み込みコマンドです。実行されるとそれ以降の処理をスキップして、次の周回(イテレーション)へ進みます。ループそのものを終了する break とは異なり、あくまで「今回だけ飛ばす」点がポイントです。

continue [N]

最も基本的な例として、1〜5のうち偶数だけをスキップして表示するスクリプトを見てみましょう。

for i in 1 2 3 4 5; do
  if (( i % 2 == 0 )); then
    continue   # 偶数はスキップして次のiへ
  fi
  echo "$i"
done
# 出力: 1 3 5

while ループでも同様に使えます。

i=0
while (( i < 5 )); do
  ((i++))
  [[ $i -eq 3 ]] && continue   # 3のときだけスキップ
  echo "$i"
done
# 出力: 1 2 4 5

continueとbreakの違い

コマンド動作使いどころ
continue今回の処理だけスキップして次の周回へ不要なデータを無視して処理を続けたいとき
breakループ自体をその場で終了する目的を達成した/これ以上続ける必要がないとき

ネストしたループでの continue N

continue に数値を指定すると、内側から数えて N 番目の外側のループへスキップできます。continue 2 なら、2つ外側のループの次の周回に進みます。

for i in 1 2 3; do
  for j in a b c; do
    [[ "$j" == "b" ]] && continue 2   # 外側ループの次のiへ直接スキップ
    echo "$i-$j"
  done
done
# 出力: 1-a 2-a 3-a

ここまでが continue の基本です。ここから先は、実務のスクリプトで 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: only meaningful in a `for’, `while’, or `until’ loop

continue をループの外や、if だけのブロック(ループではない)で実行すると、次のエラーになります。

bash: continue: only meaningful in a `for', `while', or `until' loop

典型的な原因は、パイプの右側で while がサブシェルとして実行され、期待した位置にループが存在しないケースや、条件分岐 (if) だけで continue を使ってしまうケースです。

# NG: if だけでは continue は使えない(ループが無い)
if true; then
  continue   # エラー
fi

# OK: continue はループの中でのみ使う
for i in 1 2 3; do
  if [[ "$i" == 2 ]]; then
    continue
  fi
  echo "$i"
done

関数の中で「早期に処理を打ち切りたい」だけであれば、continue ではなく return を使います。ループと関数の役割を混同しないことが、このエラーを避ける一番の近道です。

よくある質問(FAQ)

Q1. bashのcontinueとは何ですか?

forwhileuntil ループの中で、それ以降の処理をスキップして次の周回に進む組み込みコマンドです。ループ自体を終了する break とは異なり、あくまで今回の1回分だけを飛ばします。

Q2. for文でcontinueを使うにはどう書きますか?

for i in 1 2 3 4 5; do
  [[ $((i % 2)) -eq 0 ]] && continue
  echo "$i"
done
# 出力: 1 3 5

Q3. 「continue: only meaningful in a…」エラーが出るのはなぜですか?

continue をループの外や if だけのブロックで実行すると発生します。詳しくは記事内の「よくあるエラー」セクションを参照してください。

Q4. continueとbreakはどう使い分けますか?

continue は「不要なデータだけ無視して処理を続けたい」ときに、break は「目的を達成したのでループ自体を終えたい」ときに使います。両者の使い分けは記事内の比較表を参照してください。

まとめ

continue は“先へ進むためのスキップ”、break は“ここで終えるための脱出”です。そして returnexit は「どこへ結果を返すか」を決めるスイッチです。

まずは、何をスキップし、いつ中断し、どの失敗をどこへ返すかを言葉で決めてからコードに落とし込んでみてください。trap による後始末の一元化、while read -r とプロセス置換の組み合わせ、集計変数による終了コード設計――この三点を押さえるだけで、ループはぐっと壊れにくく、読みやすくなります。

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする