while / until / for:Bashループの入門からwhile readでの実務活用

分岐・反復
スポンサーリンク

「bash while の正しい書き方」「while read がうまく動かない」「パイプを使うと変数が戻らない…」――そんな悩みを最短で解決するための入門〜実務ガイドです。

この記事は、安全な while read の書き方を軸に、パイプ時のサブシェル問題の回避while / until / for の使い分けまでを、コピペで試せる最小コードと実務スニペットで解説します。

この記事が解決すること

  • 空白・タブ・バックスラッシュが壊れる read の誤用を防ぐ
  • cmd | while …ループ内の変数が親に戻らない問題の理解と対処
  • ログ監視・行処理・ポーリング・リトライなど現場で使う書き方の型
  • while / until / for選び方の基準と短い判断フロー

想定読者

  • Bash の while を仕事で使い始めた 初級〜中級者
  • ログ・CSV・ファイル名など壊れやすい入力を扱う方
  • 無限ループ/ポーリング/リトライを安全に書きたい方

この記事で得られること

  • 壊れない 安全な while read テンプレ と各オプションの使いどころ
  • パイプ×サブシェルの落とし穴を避ける正しい書き方(入力リダイレクト/プロセス置換)
  • すぐ使える コピペ例FAQ(エラー原因の特定と対処)

最短の基本形(コピペ可)
以下が、空白・タブ・\ を壊さずに1行ずつ処理できる、もっとも安全な型です。

# 安全な1行読みの基本形:空白・タブ・\ を保持
while IFS= read -r line; do
  printf '%s\n' "$line"
done < input.txt

この型を起点に、用途に応じて -d(区切り変更)や -t(タイムアウト)、< <(cmd)(プロセス置換)などへ発展させていきます。

  1. while文の基本:評価の仕組みと構文
    1. ループが続く条件(評価の仕組み)
    2. 基本構文
    3. 条件の書き方バリエーション
    4. until との関係(対になる構文)
    5. break / continue と終了ステータス
    6. 1行で書く・長く書く(可読性の指針)
  2. while read で安全に1行ずつ処理する(IFS= / -r / -d / -t)
    1. 安全な基本形(コピペ可)
    2. IFS= の役割と効果
    3. -r:バックスラッシュを保持する
    4. -d:区切り文字を変更(NUL区切り対応)
    5. -t:タイムアウトを付けて非同期/対話入力に強くする
    6. 行末改行がない最終行も取りこぼさない
    7. CRLF(Windows改行)への対処
    8. よくある落とし穴と対策(ショートリスト)
  3. パイプとサブシェル問題:親シェルへ値を戻す正しい書き方
    1. 症状(よくあるハマり)
    2. なぜ起きるのか(原因)
    3. 回避策1:入力リダイレクトに置き換える(最も単純)
    4. 回避策2:mapfile / readarray で配列に受ける
    5. 回避策3:lastpipe を使う(条件付きの上級テク)
    6. これは解決にならないパターン
    7. 使い分けの指針(ショートメモ)
    8. 実務スニペット(安全な書き換え例)
  4. while / until / for の使い分け早見表
    1. クイック選択
    2. 早見表(用途・代表構文・注意点)
    3. 同じ課題を3種類で比較
    4. NG/非推奨パターンと置き換え
    5. 選び分けの実務メモ
  5. 実務スニペット集(コピペ可)
    1. 1) ファイル整形:空行スキップ・CRLF除去・簡易置換
    2. 2) ログ監視:ERROR を検知して通報
    3. 3) 対話入力:タイムアウトとマスク入力
    4. 4) リトライ:指数バックオフでヘルスチェック
    5. 5) 安全なファイルループ:NUL 区切りで壊れない
    6. 6) 行数カウント/末尾行の取得(mapfile 応用)
    7. 7) 簡易CSV処理(カンマ区切り・引用なしの想定)
    8. 8) バッチ置換:ファイル群を編集して上書き
  6. 無限ループの安全設計
    1. 原則(チェックリスト)
    2. 安全スケルトン(コピペ可)
    3. 早期終了条件の設計
    4. バックオフとジッター
    5. シグナルと後始末
    6. 多重起動防止(最小例)
    7. CPU暴走を避ける
    8. アンチパターン
    9. 小さな完成形(サービス起動待ち)
  7. よくあるエラーとトラブルシュート
    1. [: too many arguments]
    2. syntax error near unexpected token 'do'
    3. command not found: [[ / read: -d: invalid option
    4. 変数が外に戻らない(パイプ×サブシェル)
    5. 最終行が処理されない(改行なしファイル)
    6. 空白が消える/タブが詰まる
    7. バックスラッシュが壊れる
    8. 改行を含むファイル名で崩れる
    9. CRLF 起因の不具合(\r: command not found 等)
    10. integer expression expected
    11. bad substitution / <(cmd) が使えない
    12. echo の挙動差で意図しない出力
  8. read オプション早見表
    1. よく使う組み合わせテンプレ
  9. ベストプラクティスまとめ
    1. 入力の読み取り
    2. 変数展開と出力
    3. パイプとデータ受け渡し
    4. ループ設計
    5. 無限ループの安全運用
    6. エラー処理・終了ステータス
    7. 可搬性・環境
    8. 可読性・保守
  10. FAQ(よくある質問)
    1. Q. while と until の違いは?どちらを使えばいい?
    2. Q. while read で空白が消える/タブが詰まる/バックスラッシュが壊れる
    3. Q. パイプで count が増えない(変数が外に戻らない)
    4. Q. 最終行に改行がないと読み落とします
    5. Q. [[ が使えない/read: -d: invalid option と言われる
    6. Q. 改行を含むファイル名を安全に処理したい
    7. Q. Windows 改行(CRLF)が混ざってエラーになる/\r: command not found
    8. Q. read -t のタイムアウト時、変数には何が入る?
    9. Q. echo と printf はどちらを使うべき?
    10. Q. for line in $(cat file) はダメ?
  11. 参考文献・公式ドキュメント
  12. まとめ:Bash while の要点
    1. 最小チェックリスト
  13. 関連記事

while文の基本:評価の仕組みと構文

ループが続く条件(評価の仕組み)

  • while「条件部が成功(= 終了ステータス 0) の間」 本体を実行します。
  • 条件部は コマンド列 です。最後に実行されたコマンドの終了ステータスで判定されます。
  • 1回目の評価で失敗すれば 本体は1度も実行されません(前判定ループ)。
# 例: ping が成功する間だけ待つ
while ping -c1 -W1 127.0.0.1 >/dev/null 2>&1; do
  sleep 1
done

基本構文

while 条件部のコマンド列; do
  本体のコマンド
done
  • do を同じ行に書くときは ; が必須while 条件; do ...
  • 条件部は複数コマンドを使えます(&& / || で論理結合可能)。
# 論理結合の例: Aが成功 かつ Bが成功 の間ループ
while commandA && commandB; do
  ...
done

条件の書き方バリエーション

  1. コマンドの成否で判定(最もBashらしい)
# プロセスが生きている間ループ
while kill -0 "$pid" 2>/dev/null; do
  sleep 1
done
  1. [[ ... ]](推奨のテスト構文)
# ファイルが存在する間
while [[ -e /tmp/lock ]]; do
  sleep 1
done
  • パス展開やパターン比較に強く、引用の罠が少ない。
  • POSIX sh では使えないため、Bash前提の記事やスクリプトで使用。
  1. 算術評価 (( ... ))
i=0
while (( i < 5 )); do
  printf '%d\n' "$i"
  ((i++))
done
  • 変数名はクォート不要、<++ などC風の記法が使えます。
  1. 真偽の固定(無限ループ)
# 最軽量の無限ループ
while :; do
  # 何らかの処理
  sleep 1
done
  • : は常に成功を返すビルトイン(true より高速・依存少)。

until との関係(対になる構文)

  • until「条件部が失敗の間」 本体を実行し、成功したら終了
  • while ! 条件; do ...; done とほぼ同等で、「成功するまで待つ」 ポーリングに向きます。
# サービスの /health が成功するまで待つ
until curl -fsS http://localhost:8080/health >/dev/null; do
  sleep 1
done

break / continue と終了ステータス

count=0
while :; do
  (( count++ ))
  (( count >= 10 )) && break    # ループを抜ける
  (( count % 2 )) && continue   # 奇数のときスキップ
  do_something "$count"
done
  • ループ全体の終了ステータスは 最後に実行されたコマンド に依存します。
  • スクリプトから呼ぶ関数では、必要に応じて明示的に return / exit を使い分けてください。

1行で書く・長く書く(可読性の指針)

  • 短いワンライナー: while read -r x; do echo "$x"; done < file
  • 実務では 本体が複雑になりがち。可読性優先で 関数化改行 を使いましょう。

while read で安全に1行ずつ処理する(IFS= / -r / -d / -t)

安全な基本形(コピペ可)

# 空白・タブ・バックスラッシュを壊さず1行ずつ読む
while IFS= read -r line; do
  printf '%s\n' "$line"
done < input.txt
  • IFS=:区切り(フィールド分割)を無効化し、行全体をそのまま受け取る
  • -r:バックスラッシュをエスケープとして解釈せず、文字として保持
  • printfecho の挙動差(解釈・末尾改行)を避けるため 常用
  • < input.txt:明示的な入力リダイレクトでシンプルに

変数は必ず "$line" のように二重引用して展開します。


IFS= の役割と効果

read は既定で $IFS(通常は空白・タブ・改行)による分割を行います。IFS=(空文字)を指定すると分割自体を行わず、入力行を最初の変数に丸ごと格納します。これにより、先頭/末尾の空白やタブも欠落しません。

# IFS= を外すとフィールド分割される例
while read -r col1 col2; do
  printf '[%s] [%s]\n' "$col1" "$col2"
done < "a  b.txt"   # 想定外の分割が起きうる

-r:バックスラッシュを保持する

-r を付けないと、\n などのエスケープや行末の \ が解釈されてしまいます。ログ・パス・正規表現など、バックスラッシュを含むデータを扱う場合は必須です。

# -r なし: \t がタブに変換されうる
while read line; do printf '%s\n' "$line"; done < sample.txt

# -r あり: 文字列としての \t を保持
while IFS= read -r line; do printf '%s\n' "$line"; done < sample.txt

-d:区切り文字を変更(NUL区切り対応)

改行を含むファイル名を安全に処理するなら、NUL 区切りが定番です。find -print0read -r -d '' を組み合わせます(-d '' は NUL まで読み取る指定)。

# 改行を含む可能性のあるパスも安全に処理
while IFS= read -r -d '' path; do
  printf '%q\n' "$path"
done < <(find . -type f -print0)

-t:タイムアウトを付けて非同期/対話入力に強くする

-t 秒 を付けると、指定時間内に入力が来なければ失敗(非0)で返ります。ポーリングや対話プロンプトに便利です。

# 1秒以内に入力があれば処理、なければスキップ
while IFS= read -r -t 1 line; do
  process "$line"
done < queue.txt

対話入力の例

printf '名前を入力してください: ' >&2
if IFS= read -r -t 5 name; then
  printf 'Hello, %s\n' "$name"
else
  printf 'タイムアウトしました\n' >&2
fi

行末改行がない最終行も取りこぼさない

ファイルの最後の行に改行が無いと、read は**最後に失敗(非0)**を返すことがあります。以下のイディオムで確実に処理します。

while IFS= read -r line || [[ -n $line ]]; do
  printf '%s\n' "$line"
done < input.txt

CRLF(Windows改行)への対処

CRLF のファイルを扱うと、行末に \r が残ることがあります。必要なら都度取り除きます。

while IFS= read -r line || [[ -n $line ]]; do
  line=${line%$'\r'}           # 末尾の \r を削除
  printf '%s\n' "$line"
done < input_crlf.txt

よくある落とし穴と対策(ショートリスト)

  • 未引用展開$line を裸で使う → 必ず "$line"
  • echo の癖-e 解釈や末尾改行の違い → printf を使用
  • フィールド分割IFS= を忘れて空白が消える → IFS= を明示
  • バックスラッシュ崩れ-r を忘れる → 常に -r
  • 最終行の取りこぼし:改行なしのファイル → || [[ -n $line ]]
  • 改行を含む名称-d ''-print0 を使用

これらを守るだけで、行処理に関する大半の不具合は回避できます。

パイプとサブシェル問題:親シェルへ値を戻す正しい書き方

症状(よくあるハマり)

count=0
grep -n 'ERROR' app.log | while IFS= read -r line; do
  ((count++))
done
echo "$count"   # => 0 のまま(増えていない)

見た目は正しくカウントしているのに、ループ外では値が更新されていない――これが“パイプ×サブシェル問題”です。

なぜ起きるのか(原因)

Bash では パイプラインの各コマンドは別プロセス(サブシェル)で実行されます(※最後のコマンドのみ例外化できる設定あり)。
そのため、grep ... | while ...while 本体がサブシェルで動き、変数変更が親シェルへ戻らないのが原因です。


回避策1:入力リダイレクトに置き換える(最も単純)

パイプをやめて < でデータを渡すと、while は親シェルで動きます。

count=0
while IFS= read -r line; do
  ((count++))
done < <(grep -n 'ERROR' app.log)   # ← プロセス置換で入力を渡す
echo "$count"   # => 期待どおりの件数

ポイント:ファイルがすでにある場合は単純に < file でOK。コマンドの出力を渡したいときは プロセス置換 < <(cmd) を使います。


回避策2:mapfile / readarray で配列に受ける

1回で全行を配列に取り込んでから、親シェルで処理します。

mapfile -t lines < <(grep -n 'ERROR' app.log)   # Bash 4+
echo "${#lines[@]}"

# 必要なら後段で通常の for/while で処理
for line in "${lines[@]}"; do
  printf '%s\n' "$line"
done
  • 行数カウントや再利用が簡単。
  • 大きな出力ではメモリ消費が増える点だけ注意。

回避策3:lastpipe を使う(条件付きの上級テク)

Bash 4.2+ には 最後のパイプ要素を親シェルで実行する lastpipe オプションがあります。
非対話シェルでのみ有効(bash -c '...' やスクリプト実行時)。

shopt -s lastpipe   # 非対話シェルで有効化されている前提
count=0
grep -n 'ERROR' app.log | while IFS= read -r line; do
  ((count++))       # ここが親シェルの変数に反映される
done
echo "$count"
  • 制約が多く、**環境差(Bashのバージョン/インタラクティブかどうか)**に左右されます。
  • 挙動を安定させたいなら、基本は 回避策1 or 2 を選びましょう。

これは解決にならないパターン

  • { ...; } でグループ化しても、パイプに載せた時点でサブシェル実行の問題は残ります。
  • (...)(サブシェルグループ)は当然、親に影響しません
  • set -o pipefail終了ステータスの伝播に関する設定で、変数スコープ問題は解決しません

使い分けの指針(ショートメモ)

  • “値を外へ持ち帰る必要がある” → パイプをやめて リダイレクト or プロセス置換
  • 全量を一気に扱ってもよいmapfile -t で配列に。
  • 既存のパイプ構文を崩したくない & 環境が揃う → lastpipe を検討(非推奨寄り)。

実務スニペット(安全な書き換え例)

NG → OK(プロセス置換)

# NG
total=0
grep -n 'ERROR' app.log | while IFS= read -r _; do ((total++)); done
echo "$total"   # 0

# OK
total=0
while IFS= read -r _; do ((total++)); done < <(grep -n 'ERROR' app.log)
echo "$total"

NG → OK(mapfile)

# NG: 変数が戻らない
last=""
journalctl -u my.service | while IFS= read -r line; do last=$line; done
printf '%s\n' "$last"   # 空

# OK: 配列に受ける
mapfile -t logs < <(journalctl -u my.service)
printf '%s\n' "${logs[-1]}"

この原則(“パイプに while を載せない”)を守るだけで、変数スコープ由来のバグの多くを未然に防げます。

while / until / for の使い分け早見表

クイック選択

  • 件数が不明・ストリーム処理whileread と組み合わせて逐次処理)
  • “成功するまで待つ”ポーリングuntil(または while ! 条件
  • 回数が決まっている/配列・範囲を回すforfor (( )) / for x in ...

早見表(用途・代表構文・注意点)

状況/目的最適代表構文強み注意点
行単位の逐次処理(ログ/CSV/標準入力)whilewhile IFS= read -r line; do ...; done < fileメモリ効率◎、壊れない読み取りIFS=/-r を忘れない、パイプでサブシェル化しない
条件が満たされるまで待機(ヘルスチェック、ファイル出現)untiluntil curl -fsS /health; do sleep 1; done意図が読みやすいwhile ! 条件 と同義、どちらかに統一
既知回数の反復(0..N-1)for(算術)for ((i=0; i<N; i++)); do ...; done境界が明確、速いループ変数の型/範囲に注意
配列・単語リストfor(in)for x in "${arr[@]}"; do ...; doneシンプルワード分割/グロブ展開に注意・必ず引用
ファイル群(*.log など)for(in)for f in *.log; do ...; done短く書ける改行を含む名前は不可。安全重視なら find -print0 + while read -d ''
全量をまとめて取り込みたいmapfilemapfile -t lines < file一括処理が簡単大きな入力はメモリ使用増

同じ課題を3種類で比較

1) 行処理(逐次)— while

# 大きなファイルも逐次で安全に処理
while IFS= read -r line; do
  process "$line"
done < input.txt

2) 条件成立まで待つ — until

# /health が 200 を返すまで待機
until curl -fsS http://localhost:8080/health >/dev/null; do
  sleep 1
done

3) 既知回数の反復 — for(算術)

# 0..9 を処理
for ((i=0; i<10; i++)); do
  printf '%d\n' "$i"
done

NG/非推奨パターンと置き換え

  • NG:for line in $(cat file)
    • 理由:単語分割・グロブ展開で行が壊れる。
    • 置換: while IFS= read -r line; do ...; done < file
  • NG:grep ... | while read ...; do ...; done
    • 理由:サブシェルで変数が親に戻らない。
    • 置換: while read ...; do ...; done < <(grep ...) もしくは mapfile -t

選び分けの実務メモ

  • メモリ効率優先while read(逐次)
  • 読みやすさ優先の待機untilwhile ! とどちらかに統一)
  • 計数ループfor (( ))(境界・増分が明確)
  • 名称がややこしいファイル群find -print0 + while read -d ''
  • 後段再利用したい出力mapfile -t で配列化(入力が大きすぎない場合)

実務スニペット集(コピペ可)

1) ファイル整形:空行スキップ・CRLF除去・簡易置換

# input.txt を整形して output.txt へ
while IFS= read -r line || [[ -n $line ]]; do
  line=${line%$'\r'}                    # CRLF の \r を除去
  [[ $line =~ ^[[:space:]]*$ ]] && continue  # 空行/空白のみはスキップ
  line=${line//foo/bar}                 # 文字列置換(必要なければ削除)
  printf '%s\n' "$line"
done < input.txt > output.txt

2) ログ監視:ERROR を検知して通報

# tail の出力をプロセス置換で安全に受ける(親シェルで実行)
while IFS= read -r line; do
  [[ $line == *ERROR* ]] || continue
  printf '[ALERT] %s\n' "$line"        # 任意:mail/Slack/Webhook に差し替え
done < <(tail -F /var/log/app.log)

3) 対話入力:タイムアウトとマスク入力

# 5秒以内に名前、パスワードはマスク入力(失敗時は中断)
printf '名前: ' >&2
IFS= read -r -t 5 name || { printf 'Timeout\n' >&2; exit 1; }

IFS= read -r -s -p 'パスワード: ' pass && printf '\n' >&2
[[ -n $pass ]] || { printf '空のパスワード\n' >&2; exit 1; }

printf 'Hello, %s\n' "$name"

4) リトライ:指数バックオフでヘルスチェック

trap 'printf "停止要求、後始末中...\n" >&2; exit 130' INT TERM

url="http://localhost:8080/health"
delay=1 max=30 deadline=$((SECONDS+180))   # 最大30秒間隔、3分で諦める

while :; do
  if curl -fsS "$url" >/dev/null; then
    printf 'READY\n'
    break
  fi
  (( SECONDS > deadline )) && { printf 'Timeout\n' >&2; exit 1; }
  sleep "$delay"
  (( delay = delay*2 > max ? max : delay ))
done

5) 安全なファイルループ:NUL 区切りで壊れない

# *.tmp を安全に削除(改行・空白・奇妙な文字を含む名前にも対応)
while IFS= read -r -d '' path; do
  printf 'remove: %q\n' "$path"
  rm -- "$path"
done < <(find . -type f -name '*.tmp' -print0)

6) 行数カウント/末尾行の取得(mapfile 応用)

# 全行を配列に読み込み(中~小規模の入力向け)
mapfile -t lines < input.txt
printf '行数: %d\n' "${#lines[@]}"
printf '末尾: %s\n' "${lines[-1]}"

7) 簡易CSV処理(カンマ区切り・引用なしの想定)

# "a,b,c" 形式を素朴に分割(高度なCSVは専用ツールを使用)
while IFS=, read -r col1 col2 col3 || [[ -n $col1 || -n $col2 || -n $col3 ]]; do
  printf '1:%s 2:%s 3:%s\n' "$col1" "$col2" "$col3"
done < data.csv

8) バッチ置換:ファイル群を編集して上書き

# *.conf 内の PORT= を 8080→9090 に置き換え(バックアップ作成)
while IFS= read -r -d '' f; do
  tmp=$(mktemp)
  while IFS= read -r line || [[ -n $line ]]; do
    line=${line%$'\r'}
    line=${line//PORT=8080/PORT=9090}
    printf '%s\n' "$line"
  done < "$f" > "$tmp"
  cp -a -- "$f" "$f.bak"
  mv -- "$tmp" "$f"
done < <(find . -type f -name '*.conf' -print0)

ポイント共通ルール

  • 文字列は 常に二重引用 "..." で展開(分割・グロブ暴走を防止)
  • 入力は IFS=-r が基本、ファイル名などは -d ''(NUL区切り)で安全化
  • パイプより 入力リダイレクト/プロセス置換 を優先(サブシェル問題の回避)
  • 出力は printf を常用(echo の挙動差を避ける)

無限ループの安全設計

無限ループは “止まらない” ことが価値ですが、暴走しない・止められる・後始末できる が最低条件です。ここでは実務で壊れにくい設計指針とスケルトンを示します。


原則(チェックリスト)

  • 停止条件を必ず用意する:成功したら break、失敗が続いたら 期限/回数上限で終了
  • 待機を入れる:sleep(必要に応じて指数バックオフ + 上限
  • 割り込み対応trap 'cleanup; exit' INT TERMEXIT で後始末の担保)
  • 多重起動防止flock やPIDロックで並行実行を抑止
  • 観測性:ログに タイムスタンプ/試行回数/戻り値 を出す
  • 安全な構文while :; do ...; done: は常に成功するビルトイン)

安全スケルトン(コピペ可)

#!/usr/bin/env bash
set -Eeuo pipefail

log() { printf '%(%Y-%m-%dT%H:%M:%S%z)T [%s] %s\n' -1 "$1" "${2-}"; }
cleanup() { rm -f -- "${LOCKFILE:-}"; log INFO "cleanup done"; }
on_abort() { log WARN "signal caught, shutting down"; cleanup; exit 130; }

trap cleanup EXIT
trap on_abort INT TERM

# 多重起動防止(flock or lockfile)
LOCKFILE=/tmp/myjob.lock
exec 9>"$LOCKFILE"
if ! flock -n 9; then
  log ERROR "already running"; exit 1
fi

deadline=$((SECONDS + 180))  # 3分で諦める
delay=1                      # 初期待機
max_delay=30                 # バックオフ上限
attempt=0

while :; do
  ((attempt++))
  log INFO "attempt=$attempt"

  if my_task; then           # ここを実処理に置き換え
    log INFO "task succeeded"; break
  fi

  # 期限・回数上限(どちらか片方/両方でも可)
  (( SECONDS > deadline )) && { log ERROR "timeout"; exit 1; }
  (( attempt >= 20 )) && { log ERROR "too many attempts"; exit 1; }

  # 指数バックオフ + ジッター(群発回避に薄い揺らぎを入れる)
  jitter=$(( RANDOM % 3 ))                # 0〜2秒
  sleep "$(( delay + jitter ))"
  (( delay = delay * 2 > max_delay ? max_delay : delay * 2 ))
done

my_task成功で 0、失敗で 非0 を返す関数/コマンドに置き換えてください。


早期終了条件の設計

  • 成功break:達成したら即終了(例:ヘルスチェックが200を返したら終了)
  • 期限deadline=$((SECONDS+N)) で実時間の上限を持つ
  • 回数上限attempt >= N で制御(APIレート制限/外部依存が重いときに有効)
  • 外部スイッチ:運用で止めたい場合は “停止ファイル” を見る [[ -e /tmp/stop.flag ]] && { log WARN "stop.flag found"; exit 0; }

バックオフとジッター

  • 指数バックオフdelay = min(delay*2, max_delay)
    混雑の自己是正に有効(ネットワーク/API系)
  • ジッター:小さな乱数を足すと、同時多発アクセスを緩和
    sleep "$(( delay + RANDOM % 3 ))"

シグナルと後始末

  • trap 'cleanup; exit' INT TERM:Ctrl-C や停止要求で安全に終了
  • trap 'cleanup' EXITエラー/成功に関わらず最後に実行される保険
  • cleanup では、一時ファイル/ロック/子プロセスの終了を確実に

多重起動防止(最小例)

lock=/tmp/job.lock
exec 9>"$lock" || exit 1
flock -n 9 || { echo "already running"; exit 1; }
# ... ここに処理 ...

flock が使えない環境では PID ファイル+kill -0 で代替。


CPU暴走を避ける

  • 空回りを防ぐ 最低限の sleep を必ず入れる
  • サブプロセスの出力待ちには read -tタイムアウト付き待機も有効
# 入力キューからの取得を 0.5秒でタイムアウト
if IFS= read -r -t 0.5 line; then
  handle "$line"
fi

アンチパターン

  • while true; do work; done待機なし)→ CPUを占有する
  • 停止条件なしの無限ループ → 障害時に復帰不能
  • ロックなしで多重起動 → データ破壊・API過負荷
  • trap 未設定 → 中断時に一時ファイル/ロックが残る

小さな完成形(サービス起動待ち)

url="http://127.0.0.1:8080/health"
delay=1; max=16; deadline=$((SECONDS+120))
trap 'echo "stopping..."; exit 130' INT TERM

while :; do
  curl -fsS "$url" >/dev/null && { echo "ready"; break; }
  (( SECONDS > deadline )) && { echo "timeout"; exit 1; }
  sleep "$delay"; (( delay = delay*2 > max ? max : delay*2 ))
done

この設計をベースに、停止条件・待機・trap・ロック・ログの5点を組み合わせると、運用に耐える無限ループになります。

よくあるエラーとトラブルシュート

[: too many arguments]

症状[ $x = foo bar ] などでエラー。
原因:未引用の変数が空白で分割される。
対処

# 推奨:[[ ... ]] と引用を使う
[[ $x == "foo bar" ]]

# test( [ ) を使うなら必ず引用
[ "$x" = "foo bar" ]

syntax error near unexpected token 'do'

症状while 条件 do のような構文エラー。
原因; や改行が足りない/クォート不整合/CRLF混入。
対処

# 正:同一行に置く場合は ; が必要
while CMD; do
  ...
done

# CRLFが混ざっている場合は \r を除去
sed -i 's/\r$//' script.sh   # もしくは dos2unix

command not found: [[ / read: -d: invalid option

症状[[read -d が使えない。
原因/bin/sh(dash 等)で実行している、または古い Bash。
対処

# 冒頭に Bash を明示
#!/usr/bin/env bash

# sh で実行しているなら bash で
bash script.sh

# macOS などで古い Bash の場合は更新、または代替構文を選択

変数が外に戻らない(パイプ×サブシェル)

症状

count=0
grep -n ERROR app.log | while read -r _; do ((count++)); done
echo "$count"   # => 0

原因:パイプの右側 while がサブシェルで実行。
対処

# プロセス置換で親シェルに渡す
count=0
while IFS= read -r _; do ((count++)); done < <(grep -n ERROR app.log)
echo "$count"

# あるいは配列で受ける
mapfile -t lines < <(grep -n ERROR app.log)
echo "${#lines[@]}"

最終行が処理されない(改行なしファイル)

症状:最後の1行が無視される。
対処

while IFS= read -r line || [[ -n $line ]]; do
  printf '%s\n' "$line"
done < input.txt

空白が消える/タブが詰まる

原因IFS によるフィールド分割。
対処

while IFS= read -r line; do   # IFS を空に
  printf '%s\n' "$line"
done < file

バックスラッシュが壊れる

原因read\ をエスケープとして解釈。
対処

while IFS= read -r line; do   # -r を付ける
  printf '%s\n' "$line"
done < file

改行を含むファイル名で崩れる

症状for f in $(find ...) で名前が壊れる。
対処

while IFS= read -r -d '' path; do
  printf '%q\n' "$path"
done < <(find . -type f -print0)

CRLF 起因の不具合(\r: command not found 等)

対処

# 事前に変換
dos2unix file.txt

# ループ内で除去
line=${line%$'\r'}

integer expression expected

症状(( i < 10 )) で整数でない/未設定の値。
対処

[[ $i =~ ^[0-9]+$ ]] || i=0
(( i < 10 )) && ...

bad substitution / <(cmd) が使えない

原因:古い Bash / 非Bash。
対処

  • Bash 4+ に更新、または ${var,,}tr に置換。
  • プロセス置換 < <(cmd) が不可なら、一時ファイル/名前付きパイプで代替。
cmd > /tmp/out && while read -r x; do ...; done < /tmp/out

echo の挙動差で意図しない出力

症状-e 解釈や末尾改行の差異。
対処printf を常用

printf '%s\n' "$line"

デバッグ小技

  • 冒頭に set -Eeuo pipefail を入れて早期に落とす
  • 失敗時の直近コマンドを表示:trap 'echo "ERR:$BASH_COMMAND ($?)"' ERR
  • ループ内ログ:printf '%q\n' "$line" >&2(目で壊れを確認)

read オプション早見表

Bash の組み込み read で実務頻度が高いものを中心に整理しました。右端は“そのまま試せる”最短例です。

オプション / 設定役割主な用途注意点1行ミニ例
-r\ をエスケープ扱いしないパス/正規表現/ログを壊さず取得常用推奨。付け忘れると \n 等が解釈されるwhile IFS= read -r line; do :; done < f
IFS=フィールド分割を無効化先頭/末尾空白やタブを保持オプションではなく「環境変数」。空文字にするwhile IFS= read -r line; do :; done < f
-d <char>区切り文字を変更NUL 区切り(ファイル名安全)-d '' で NUL。区切り文字自体は消費されるwhile IFS= read -r -d '' p; do :; done < <(find . -print0)
-t <sec>タイムアウト対話/ポーリングタイムアウト時は 非0。変数は未定義の可能性→分岐必須if IFS= read -r -t 1 x; then use "$x"; fi
-n <N>N 文字読んだら即返す単キー入力/簡易メニュー端末入力向け。改行不要。N 文字に満たないと待機read -rn1 key
-N <N>ちょうど N 文字読むバイナリ/固定長必ず N 文字揃うまでブロック(改行不要)read -rN8 header < file.bin
-s入力を表示しないパスワード端末入力のみ(パイプ不可)。終了後に改行を明示出力read -rs -p 'PW: ' pw; echo
-p <str>プロンプト表示対話プロンプト端末向け。標準エラーに出すなら printf を推奨read -rp 'Name: ' name
-u <fd>指定 FD から読む多重入力/プロセス間事前に exec 3<file 等で FD を開くexec 3< list.txt; read -ru 3 line
-a <arr>配列に格納1行を単語配列へIFS 分割が効く(空白が区切り)。安全に扱うIFS=, read -ra cols <<<"a,b,c"
-eReadline 有効化編集/履歴活用端末のみ。-i と組み合わせで初期値read -re -i 'default' -p 'Input: ' x

よく使う組み合わせテンプレ

  • 壊さない行読み(基本形)
    while IFS= read -r line; do …; done < file
  • NUL 区切りで安全にファイル名処理
    while IFS= read -r -d '' path; do …; done < <(find . -print0)
  • タイムアウト付き対話
    if IFS= read -r -t 5 ans; then echo "$ans"; else echo 'timeout' >&2; fi
  • パスワード入力
    read -rs -p 'Password: ' pw; echo

ベストプラクティスまとめ

入力の読み取り

  • 行処理は while IFS= read -r line; do …; done < file を基本形にする
  • 最終行に改行が無いファイルは || [[ -n $line ]] で取りこぼし防止
  • ファイル名などは NUL区切り-d '' + find -print0)で壊れない処理
  • CRLF混在に備えて必要に応じて ${line%$'\r'} を適用
  • NG: for line in $(cat file)(分割・展開事故の元)

変数展開と出力

  • 変数は常に 二重引用 "..." で展開する
  • 出力は printf 常用echo の挙動差を避ける)

パイプとデータ受け渡し

  • パイプ右側に while を置かない(サブシェルで変数が戻らない)
  • 代わりに 入力リダイレクト/プロセス置換while …; done < <(cmd)
  • 一括処理で良ければ mapfile -t lines < <(cmd) を活用
  • lastpipe は環境依存が強い。基本は使わない方針で

ループ設計

  • 条件判定は [[ … ]]/算術は (( … )) を使う
  • カウンタ/範囲は for ((i=0; i<N; i++))、逐次ストリームは while read
  • 「成功するまで待つ」は until(または while ! 条件)で意図を明確に
  • 適切に break / continue を使い、早期終了とスキップを設計

無限ループの安全運用

  • 待機sleep)を必ず入れる+指数バックオフ上限
  • 停止条件(成功・期限・回数上限・外部フラグ)を用意
  • trapINT/TERM/EXIT を捕捉し cleanup を保証
  • 多重起動防止flock(または PID ロック)

エラー処理・終了ステータス

  • スクリプト先頭に set -Eeuo pipefail(未定義変数/パイプ失敗を拾う)
  • コマンドの成否は && / || / if で即時判定
  • 関数は return、スクリプトは exit で明示的に終了
  • デバッグ時は trap 'echo "ERR:$BASH_COMMAND ($?)"' ERR などで原因特定

可搬性・環境

  • 冒頭に #!/usr/bin/env bash を明記(/bin/sh 実行を避ける)
  • 使う機能に応じて Bashのバージョン要件(例:mapfile は Bash 4+)を把握
  • POSIX互換が必要なら [[/<<</<( ) を避け、代替を検討

可読性・保守

  • 本体が長くなる前に 関数化早期 return
  • ログ関数(タイムスタンプ/レベル)を用意して観測性を上げる
  • ShellCheck で静的解析、スタイル/命名を統一
  • UUOC回避cat file | while は使わない)、リダイレクトを基本にする

上記を満たせば、while ベースの処理は 壊れにくく運用しやすい 形に収まります。

FAQ(よくある質問)

Q. while と until の違いは?どちらを使えばいい?

A. while は「条件が成功している間」回す、until は「条件が失敗している間」回す構文です。

  • 待機/ポーリングuntil(または while ! 条件)が読みやすい
  • ストリーム処理(行入力など)は while が基本
# until:ヘルスチェックが成功(=0)するまで待つ
until curl -fsS http://localhost:8080/health >/dev/null; do sleep 1; done

Q. while read で空白が消える/タブが詰まる/バックスラッシュが壊れる

A. IFS=-r を付けた 安全形 を使います。

while IFS= read -r line; do
  printf '%s\n' "$line"
done < input.txt

Q. パイプで count が増えない(変数が外に戻らない)

A. パイプ右側の while はサブシェルで動くためです。プロセス置換mapfile にします。

# プロセス置換
count=0
while IFS= read -r _; do ((count++)); done < <(grep -n ERROR app.log)

# mapfile
mapfile -t lines < <(grep -n ERROR app.log)
echo "${#lines[@]}"

Q. 最終行に改行がないと読み落とします

A. つぎのイディオムで回避します。

while IFS= read -r line || [[ -n $line ]]; do
  printf '%s\n' "$line"
done < input.txt

Q. [[ が使えない/read: -d: invalid option と言われる

A. Bash ではなく /bin/sh(dash 等)で実行しているか、古い Bash です。Shebang を明示し、Bash で実行してください。

#!/usr/bin/env bash
bash script.sh

Q. 改行を含むファイル名を安全に処理したい

A. NUL 区切り-d '' + find -print0)を使います。

while IFS= read -r -d '' path; do
  printf '%q\n' "$path"
done < <(find . -type f -print0)

Q. Windows 改行(CRLF)が混ざってエラーになる/\r: command not found

A. 事前に変換するか、ループ内で \r を落とします。

# 事前変換 例: dos2unix file.txt
line=${line%$'\r'}   # ループ内で末尾の \r を削除

Q. read -t のタイムアウト時、変数には何が入る?

A. 終了ステータスは非0、変数は未定義(または前回値が残る場合あり)なので、if で分岐してください。

if IFS= read -r -t 3 ans; then use "$ans"; else echo "timeout" >&2; fi

Q. echo と printf はどちらを使うべき?

A. printf 推奨です。echo は実装差や -e/-n の扱いに揺れがあります。

printf '%s\n' "$line"

Q. for line in $(cat file) はダメ?

A. ダメです。 単語分割・グロブ展開で行が壊れます。while IFS= read -r を使ってください。

while IFS= read -r line; do ...; done < file

参考文献・公式ドキュメント

※ ローカルのヘルプも有用です:help read, help printf, help trap, help shopt(URLはありませんが、手元環境で即参照できます)。

まとめ:Bash while の要点

  • 壊れない行処理の基本形はこれ一択 while IFS= read -r line; do printf '%s\n' "$line" done < input.txt
    • IFS= で分割無効、-r\ を保持、出力は printf、展開は常に "$line"
  • パイプの落とし穴を避ける
    cmd | while ... はサブシェルで変数が戻らない
    入力リダイレクト/プロセス置換< <(cmd))か mapfile -t を使う。
  • 使い分けの指針
    • ストリーム/件数不明 → while
    • 成功まで待つポーリング → until(or while ! 条件
    • 既知回数/配列 → for (( )) / for in
  • ファイル名や特殊文字に強く
    改行を含む可能性があるなら -d '' + find -print0
    CRLF 混在は ${line%$'\r'} で除去。
  • 無限ループは“安全設計”で
    sleep(指数バックオフ+上限)/ 停止条件(期限・回数)/ trap(後始末)/ ロックflock)をセット。
  • エラー/品質の初期設定
    set -Eeuo pipefail[[ ... ]](( ... )) を活用、ShellCheck で静的解析。

最小チェックリスト

  • while IFS= read -r を基本にする
  • 変数は常に "..." で引用、出力は printf
  • cat | whilefor line in $(cat file) は使わない
  • パイプ時は < <(cmd) または mapfile -t に置換
  • 無限ループに 待機・停止条件・trap を必ず入れる

この5点を守れば、while を使った日常の行処理・待機処理は堅牢かつ読みやすく仕上がります。

スポンサーリンク
Bash玄

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

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

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

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

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

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

Bash玄をフォローする