「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)
(プロセス置換)などへ発展させていきます。
- while文の基本:評価の仕組みと構文
- while read で安全に1行ずつ処理する(IFS= / -r / -d / -t)
- パイプとサブシェル問題:親シェルへ値を戻す正しい書き方
- while / until / for の使い分け早見表
- 実務スニペット集(コピペ可)
- 無限ループの安全設計
- よくあるエラーとトラブルシュート
- read オプション早見表
- ベストプラクティスまとめ
- FAQ(よくある質問)
- Q. while と until の違いは?どちらを使えばいい?
- Q. while read で空白が消える/タブが詰まる/バックスラッシュが壊れる
- Q. パイプで count が増えない(変数が外に戻らない)
- Q. 最終行に改行がないと読み落とします
- Q. [[ が使えない/read: -d: invalid option と言われる
- Q. 改行を含むファイル名を安全に処理したい
- Q. Windows 改行(CRLF)が混ざってエラーになる/\r: command not found
- Q. read -t のタイムアウト時、変数には何が入る?
- Q. echo と printf はどちらを使うべき?
- Q. for line in $(cat file) はダメ?
- 参考文献・公式ドキュメント
- まとめ:Bash while の要点
- 関連記事
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
条件の書き方バリエーション
- コマンドの成否で判定(最もBashらしい)
# プロセスが生きている間ループ
while kill -0 "$pid" 2>/dev/null; do
sleep 1
done
[[ ... ]]
(推奨のテスト構文)
# ファイルが存在する間
while [[ -e /tmp/lock ]]; do
sleep 1
done
- パス展開やパターン比較に強く、引用の罠が少ない。
- POSIX
sh
では使えないため、Bash前提の記事やスクリプトで使用。
- 算術評価
(( ... ))
i=0
while (( i < 5 )); do
printf '%d\n' "$i"
((i++))
done
- 変数名はクォート不要、
<
や++
などC風の記法が使えます。
- 真偽の固定(無限ループ)
# 最軽量の無限ループ
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
:バックスラッシュをエスケープとして解釈せず、文字として保持printf
:echo
の挙動差(解釈・末尾改行)を避けるため 常用< 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 -print0
と read -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 の使い分け早見表
クイック選択
- 件数が不明・ストリーム処理 →
while
(read
と組み合わせて逐次処理) - “成功するまで待つ”ポーリング →
until
(またはwhile ! 条件
) - 回数が決まっている/配列・範囲を回す →
for
(for (( ))
/for x in ...
)
早見表(用途・代表構文・注意点)
状況/目的 | 最適 | 代表構文 | 強み | 注意点 |
---|---|---|---|---|
行単位の逐次処理(ログ/CSV/標準入力) | while | while IFS= read -r line; do ...; done < file | メモリ効率◎、壊れない読み取り | IFS= /-r を忘れない、パイプでサブシェル化しない |
条件が満たされるまで待機(ヘルスチェック、ファイル出現) | until | until 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 '' |
全量をまとめて取り込みたい | mapfile | mapfile -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
(逐次) - 読みやすさ優先の待機:
until
(while !
とどちらかに統一) - 計数ループ:
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 TERM
(EXIT
で後始末の担保) - 多重起動防止:
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" |
-e | Readline 有効化 | 編集/履歴活用 | 端末のみ。-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
)を必ず入れる+指数バックオフと上限 - 停止条件(成功・期限・回数上限・外部フラグ)を用意
trap
でINT
/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
参考文献・公式ドキュメント
- GNU Bash Reference Manual — Bash の公式リファレンス。
https://www.gnu.org/software/bash/manual/ (gnu.org) - bash(1) man ページ(man7) — 詳細なオプション・挙動の仕様。
https://man7.org/linux/man-pages/man1/bash.1.html (man7.org) - POSIX: read ユーティリティ仕様(The Open Group) —
-r
の意味など、POSIX 準拠の定義。
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/read.html (pubs.opengroup.org) - GNU findutils マニュアル —
-print0
など NUL 区切りの根拠。
https://www.gnu.org/software/findutils/manual/html_mono/find.html /-print0
の注意点: 該当節への直リンク (gnu.org) - GNU coreutils:
printf
— 安定出力のためのprintf
仕様。
https://www.gnu.org/software/coreutils/printf (gnu.org) - Wooledge BashFAQ — Bash の実務 FAQ 集(行単位読みの基本も)。
総合: https://mywiki.wooledge.org/BashFAQ / 行読み: https://mywiki.wooledge.org/BashFAQ/001 (mywiki.wooledge.org) - Bash Hackers Wiki —
read
/mapfile
/trap
などの実務的な解説。read
ビルトイン: https://bash-hackers.gabe565.com/commands/builtin/read/ (bash-hackers.gabe565.com) - Advanced Bash-Scripting Guide(TLDP) — 網羅的な入門〜中級ガイド。
https://tldp.org/LDP/abs/html/ (Linux ドキュメンテーション プロジェクト) - Linuxize チュートリアル —
while
/read
の実用的な入門。
while ループ: https://linuxize.com/post/bash-while-loop/ / read: https://linuxize.com/post/bash-read/ / 行読み: https://linuxize.com/post/how-to-read-a-file-line-by-line-in-bash/ (Linuxize) - ShellCheck — 典型的な落とし穴を検出する静的解析。
https://www.shellcheck.net/ (gnu.org)
※ ローカルのヘルプも有用です: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
(orwhile ! 条件
) - 既知回数/配列 →
for (( ))
/for in
- ストリーム/件数不明 →
- ファイル名や特殊文字に強く
改行を含む可能性があるなら-d ''
+find -print0
。
CRLF 混在は${line%$'\r'}
で除去。 - 無限ループは“安全設計”で
sleep
(指数バックオフ+上限)/ 停止条件(期限・回数)/ trap(後始末)/ ロック(flock
)をセット。 - エラー/品質の初期設定
set -Eeuo pipefail
、[[ ... ]]
と(( ... ))
を活用、ShellCheck で静的解析。
最小チェックリスト
while IFS= read -r
を基本にする- 変数は常に
"..."
で引用、出力はprintf
cat | while
/for line in $(cat file)
は使わない- パイプ時は
< <(cmd)
またはmapfile -t
に置換 - 無限ループに 待機・停止条件・trap を必ず入れる
この5点を守れば、while
を使った日常の行処理・待機処理は堅牢かつ読みやすく仕上がります。