Bashスクリプトで業務自動化やシステム運用を推進する際、反復処理の要となる基本構文が while 文です。サーバーの常駐ヘルスチェックや定期監視、指定回数のループ、サービスの起動待機、さらにはログやCSVデータの1行ごとの安全な処理まで、インフラ運用のあらゆる現場で日常的に活用されています。
しかし、「for 文との明確な使い分けがわからない」「無限ループでCPU使用率を 100% に張り付かせてしまった」「while read でループ内の変数が外側で 0 に戻ってしまい半日溶かした……」といったトラブルに直面した経験を持つエンジニアも少なくありません。
こんにちは、 Bash玄(ばっしゅげん) です。当サイトのモットーは「手作業は負け」「スクリプトはシンプルに」。本記事では、while 文の基本書式や終了ステータス判定の仕組みから、算術式 (( ... )) による数値ループ、安全な無限ループの設計、コマンド終了ステータスを直接評価する動的監視、ターミナル直打ちワンライナー、for 文との使い分け比較マトリクス、そして現場の鉄板である while read ファイル処理テンプレートまで体系的に徹底解説します。
Bashのwhile文とは?基本構文と動作の仕組み
Bashにおける while 文は、「指定した条件が真(True)である間、ブロック内の処理を繰り返し実行する」制御構文です。まずは最もシンプルな基本構文を確認しましょう。
基本構文(while 〜 do 〜 done)の書き方
while 文の最小限の書式は次の通りです。do から done までの間に記述されたコマンド群が、条件が満たされている限り反復実行されます。
while [ 条件式 ]; do
# 条件が成立している間(終了ステータス 0 の間)繰り返し実行される処理
echo "処理を実行中..."
done
スクリプトファイル内では可読性を高めるために上記のように複数行で記述するのが一般的です。do は while と同じ行にセミコロン ; で繋げて配置するのが現場のスタンダードです。
条件判定のルール(終了ステータス0でループ継続、0以外で終了)
Bashの while 文を正しく理解する上で最も重要な原則は、 「条件判定部は終了ステータスが 0 かどうかのみを判定している」 という点です。
PythonやJavaScriptなどのプログラミング言語ではブール値(True / False)を評価しますが、シェルスクリプトの世界では異なります。直前のコマンドが 0(正常終了・成功)を返したときに「真」とみなしてループを継続し、 0 以外(異常終了・失敗・EOF検知)を返した瞬間に「偽」とみなしてループを即座に終了します。
角括弧 [ ... ](test コマンド)だけでなく、grep や ping、curl といった通常のLinuxコマンドをそのまま while の直後に置けるのは、この「終了ステータス 0 判定」の仕組みがあるからです。
1. 数値条件で指定回数ループする(カウントアップ/カウントダウン)
「処理を 5 回繰り返す」「1 から 10 まで順にバッチを実行する」「残り試行回数を 10 から 0 までカウントダウンする」といった指定回数の数値ループは、日常的なスクリプト作成で頻出します。
【推奨】算術式評価 (( i < 10 )) を使った直感的な記法
Bash環境で数値ループを記述する場合、最も推奨されるのが二重丸括弧 (( ... )) を用いた算術式評価構文です。他言語(C言語やJavaなど)に近い直感的な記述ができ、可読性が抜群です。
#!/bin/bash
set -euo pipefail
# カウントアップ(0から4まで5回実行)
i=0
while (( i < 5 )); do
echo "カウントアップ中: $i"
(( i++ ))
done
# カウントダウン(リトライ回数などを減算)
count=5
while (( count > 0 )); do
echo "残り回数: $count"
(( count-- ))
done
echo "カウントダウン完了!"
算術式構文の大きなメリットは、変数展開の $ を省略できる点と、< や >、<=、>= といった不等号記号をエスケープなしでそのまま直感的に記述できる点です。クォートミスによる事故を防ぎ、コードを非常にシンプルに保てます。
汎用的な [ $i -lt 10 ](test構文)を使った比較
POSIX互換(/bin/sh)を重視する環境や古いBourne Shell環境では、単一角括弧 [ ... ](test コマンド)を使用します。不等号記号(< や >)はシェルによってリダイレクトと誤認されるため、専用の数値比較演算子を用います。
#!/bin/sh
i=1
while [ "$i" -le 5 ]; do
echo "ステップ: $i"
i=$(( i + 1 ))
done
test 構文で使用する数値比較演算子の早見表は以下の通りです。
| 演算子 | 英語の略称 | 意味 | 記述例 |
|---|---|---|---|
-eq | Equal | 等しい(==) | [ "$i" -eq 5 ] |
-ne | Not Equal | 等しくない(!=) | [ "$i" -ne 0 ] |
-lt | Less Than | より小さい・未満(<) | [ "$i" -lt 10 ] |
-le | Less Than or Equal | 以下(<=) | [ "$i" -le 10 ] |
-gt | Greater Than | より大きい・超過(>) | [ "$i" -gt 0 ] |
-ge | Greater Than or Equal | 以上(>=) | [ "$i" -ge 1 ] |
注意点として、変数が未定義(空文字)の場合の構文崩れを防ぐため、必ず "$i" のようにダブルクォートで囲み、角括弧の内側には必ず半角スペースを空けてください。条件分岐の詳細は 【Bash】if文の書き方完全ガイド|[ ]と[[ ]]の違い・条件分岐演算子早見表と実務テンプレ でも解説しています。
カウントアップ・インクリメントの書き方((( i++ )) / expr)
カウンタ変数のインクリメント(加算)にはいくつかの記法が存在します。かつて多用された外部コマンド expr(例: i=`expr $i + 1`)は、ループの周回ごとに別プロセスをフォーク生成するため、現代のシェルスクリプトでは パフォーマンスの観点から明確に非推奨 です。必ずシェル組み込みの算術演算を使用してください。
ここで実務上、極めて重要な落とし穴があります。スクリプト冒頭で set -e(エラー時に即終了)を宣言している場合、 i=0 の状態で (( i++ )) を実行するとスクリプトが即座に強制終了してしまう罠 です。
# set -e 環境下でのクラッシュ事例と安全な対策
set -e
i=0
# (( i++ )) だと「後置インクリメント」のため評価値が 0(偽)となり、set -e でスクリプトが即死する!
# 安全な対策パターン
(( ++i )) # パターン1: 前置インクリメント(評価値が 1 となり安全)
i=$(( i + 1 )) # パターン2: POSIX標準の算術展開代入(確実で移植性が高い)
(( i++ )) || true # パターン3: || true で終了ステータスを保護
予期せぬスクリプト停止を防ぐため、実務コードでは (( ++i )) または i=$(( i + 1 )) を採用するのが最も堅牢です。
2. 安全な無限ループの作り方と制御(常駐監視・デーモン処理)
バックグラウンドで動作する常駐デーモン、メッセージキューの定期ポーリング、外部サービスの疎通監視などでは、意図的に無限ループを構築します。しかし、設計を誤ると本番サーバーに甚大な負荷を与えるリスクがあります。
while true と while : の書き方と違い
Bashにおける無限ループの代表的な書き方は以下の2通りです。
# パターンA: コロン構文(実務推奨・最軽量)
while :; do
echo "常駐監視中..."
sleep 5
done
# パターンB: true コマンド構文(可読性重視)
while true; do
echo "常駐監視中..."
sleep 5
done
両者の違いとして、true はシェルの組み込みコマンド(環境によっては /bin/true という外部バイナリも存在)であるのに対し、コロン : はシェルに最初から組み込まれている「何もしないヌルコマンド(Null Command)」です。コロン構文は常に終了ステータス 0 を即座に返し、構文解析のオーバーヘッドが最小であるため、 実務のスクリプト現場では while : が最も好まれます 。
CPU高負荷(100%張り付き)を防ぐ sleep の必須指定
無限ループを実装する際に絶対に忘れてはならない鉄則が sleep コマンドの適切な挿入 です。
もし sleep を挟まずに空回りループを実行すると、CPUコアの使用率が瞬時に 100% に張り付きます。本番環境のサーバーであれば、他の業務プロセスを巻き込んで高負荷アラートを発報させたり、最悪の場合はサーバーハングアップを引き起こす重大障害に直結します。
# 安全な待機インターバル設計
while :; do
check_job_queue
# 通常監視なら数秒〜数十秒待機
sleep 5
# 高速なポーリングが必要な場合でも、必ずサブセカンド(小数秒)待機を挟む
# sleep 0.2
done
レスポンス性を高めたい場合でも、sleep 0.5 や sleep 0.1 などの小数を指定し、わずかでもOSにCPU処理権を明け渡す設計を徹底してください。
break によるループ強制終了と continue による次回スキップ
無限ループは、目的の条件を達成した時やエラー発生時に安全に脱出するロジックを必ず組み込んでおかなければなりません。ループの中断には break、周回のスキップには continue を使用します。
#!/bin/bash
set -euo pipefail
attempt=0
max_attempts=5
while :; do
(( ++attempt ))
echo "ヘルスチェック試行中 ($attempt / $max_attempts)..."
# 正常応答(HTTP 200)ならループを強制終了して脱出
if curl -sf "https://api.example.com/health" > /dev/null 2>&1; then
echo "サービス疎通を確認しました。次フェーズへ進みます。"
break
fi
# 最大試行回数に達したらエラー終了
if (( attempt >= max_attempts )); then
echo "エラー: タイムアウト(最大試行回数超過)" >&2
exit 1
fi
echo "再試行まで3秒待機..."
sleep 3
done
break は呼び出された時点でループを即時離脱し、done の直後の処理へ制御を移します。一方 continue は以降のブロックをスキップしてループの先頭(次回の条件判定)へ直ちにジャンプします。
3. 条件式のバリエーション
while 文は数値の比較だけでなく、文字列の一致判定、外部コマンドの実行結果、そして複数条件の複合論理判定など、柔軟な条件設定が可能です。
文字列の比較判定(一致・不一致・空文字チェック)
文字列を判定する場合は、拡張テスト構文 [[ ... ]] を使用するのが安全です。文字列の一致(==)、不一致(!=)、空文字判定(-z: 文字列長がゼロなら真、-n: 文字列が存在すれば真)を活用します。
#!/bin/bash
# 対話型スクリプトで正しい入力が得られるまで再入力を促す例
answer=""
while [[ "$answer" != "yes" && "$answer" != "no" ]]; do
read -p "処理を続行しますか? (yes/no): " answer
# 入力を小文字に正規化(Bash 4+)
answer="${answer,,}"
done
echo "入力値: $answer"
[[ ... ]] を使用することで、変数が空の場合のワードスプリット(単語分割)事故を防ぎ、安全に文字列を比較できます。
コマンドの実行結果(終了ステータス)を直接条件にする書き方
コマンドの実行成否を判定する際、わざわざ変数に終了コード $? を代入してから比較する必要はありません。while の直後にコマンドを直接記述できます。
# 例1: ホストが立ち上がるまで ping で待機(否定 ! を使用)
while ! ping -c 1 192.168.1.50 > /dev/null 2>&1; do
echo "ホストの起動を待機中..."
sleep 2
done
echo "ホストがオンラインになりました!"
# 例2: MySQLコンテナの起動準備完了を待機
while ! mysqladmin ping -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" --silent; do
echo "データベース起動待機中..."
sleep 1
done
echo "データベース準備完了。マイグレーションを実行します。"
コマンドの先頭に !(否定演算子)を置くことで、「コマンドが失敗(終了ステータス 0 以外)している間繰り返す」という待機スクリプトが驚くほどシンプルに書けます。
複数条件の結合(論理積 && / 論理和 ||)
複数の条件を「AND(かつ)」や「OR(または)」で結合する場合は、[[ ... && ... ]] や [[ ... || ... ]] を使用します。
# AND条件: ロックファイルが存在し、かつリトライ回数が10未満の間ループ
retry=0
while [[ -f "/tmp/app.lock" && $retry -lt 10 ]]; do
echo "ロック解除を待機中 ($retry/10)..."
(( ++retry ))
sleep 2
done
# OR条件: ジョブステータスが "running" または "pending" の間待機
status="pending"
while [[ "$status" == "running" || "$status" == "pending" ]]; do
status=$(get_job_status)
sleep 5
done
従来の [ ... ] で -a(AND)や -o(OR)を使う記法は、引数展開順序による予期せぬバグの温床となるためPOSIXでも非推奨に指定されています。必ず [[ 条件1 && 条件2 ]] または [ 条件1 ] && [ 条件2 ] の形式を用いてください。
4. 【実務頻出】ファイルを1行ずつ安全に処理する(while read構文)
インフラエンジニアの業務において、ログファイル、サーバー一覧、CSVデータなどを1行ずつ安全かつ高速に読み込んでバッチ処理する際、最も信頼されているのが while read 構文 です。
鉄板テンプレート:while IFS= read -r line; do … done < file.txt
実務の現場でそのままコピペして使える「黄金の鉄板テンプレート」は以下の通りです。
#!/bin/bash
set -euo pipefail
INPUT_FILE="server_list.txt"
# ファイルの存在チェック
[[ ! -f "$INPUT_FILE" ]] && { echo "エラー: ファイルが存在しません: $INPUT_FILE" >&2; exit 1; }
# 現場標準の黄金テンプレート
while IFS= read -r line || [[ -n "$line" ]]; do
# 空行およびコメント行(#始まり)を安全にスキップ
[[ -z "$line" || "$line" =~ ^# ]] && continue
echo "処理対象行: $line"
# ここに各行に対する処理(SSH実行、curl疎通確認等)を記述
done < "$INPUT_FILE"
IFS=(前後の空白保持)と -r(バックスラッシュ解釈防止)の重要性
なぜこのテンプレートには IFS= と -r が必須なのでしょうか。その理由を理解していないと、入力データが静かに破損する重大なバグを引き起こします。
1. IFS=(内部フィールド区切り文字のクリア)の役割
Bashのデフォルト IFS はスペース・タブ・改行に設定されています。IFS= を先頭に付与しないと、行頭や行末にあるインデントや半角空白が 勝手に自動削除(トリミング) されてしまいます。
# IFS未指定(行頭・行末の空白が削られる)
echo " インデントされたテキスト " | while read -r line; do echo "[$line]"; done
# 出力: [インデントされたテキスト]
# IFS= 指定(空白を忠実に保持)
echo " インデントされたテキスト " | while IFS= read -r line; do echo "[$line]"; done
# 出力: [ インデントされたテキスト ]
2. -r(Rawモード)の役割read コマンドはデフォルトでバックスラッシュ()をエスケープ文字として解釈します。-r を付け忘れると、Windowsパスの区切り文字(C:Users...)や正規表現パターンに含まれる がすべて消滅してしまいます。生データをそのまま正確に読み込むために -r は必須です。
最終行が改行なしでも読み飛ばさない安全対策
read コマンドは改行文字(LF)を読み込んだときに終了ステータス 0 を返します。しかし、外部から受領したテキストやWindowsで作成されたファイルの中には、 ファイルの最終行末尾に改行がない ケースが多々あります。
末尾に改行がない場合、read は最終行のテキストを変数に格納した上で、ファイル末尾(EOF)を検知して 終了ステータス 1(失敗) を返します。その結果、通常の while read では最終行の処理が実行されずに読み飛ばされてしまいます。
条件式に || [[ -n "$line" ]] を追加することで、「read の終了ステータスが 1 であっても、変数 $line にテキストが残っていればループを実行する」というフェイルセーフが働き、最終行の欠損を 100% 防止できます。
【最重要】パイプ渡しによる「サブシェル問題(変数が消える罠)」とその回避策
シェルスクリプト初心者が最もハマり、半日をデバッグで溶かしてしまう最大の落とし穴が パイプ渡しによる「サブシェル問題」 です。
# 初心者が必ずハマる失敗例
count=0
cat access.log | while IFS= read -r line; do
(( ++count ))
done
echo "集計行数: $count" # 期待は数百件なのに、なんと「0」と出力される!
なぜループ内でインクリメントしたはずの $count が 0 に戻ってしまうのでしょうか?
Unix/Linuxシェルにおいて、パイプライン(|)で接続されたコマンドは それぞれ独立したサブシェル(子プロセス) としてフォークされて実行されます。子プロセスの中で環境変数やローカル変数をどれだけ更新しても、親プロセスのメモリ空間には一切反映されません。そしてループ終了と同時に子プロセスは破棄されるため、親プロセスの $count は初期値の 0 のまま残ってしまうのです。
この致命的な罠を回避する安全な解決策は以下の2通りです。
# 解決策1: 入力リダイレクト渡し(単一ファイルの場合に最適)
count=0
while IFS= read -r line || [[ -n "$line" ]]; do
(( ++count ))
done < "access.log"
echo "集計行数: $count" # 正しい件数が出力される!
# 解決策2: プロセス置換 < <(コマンド)(コマンド出力を渡す場合に最適)
error_count=0
while IFS= read -r line || [[ -n "$line" ]]; do
(( ++error_count ))
done < <(grep "ERROR" /var/log/syslog)
echo "エラー行数: $error_count" # 親シェルでループが回るため変数も保持される!
入力リダイレクト < "file" やプロセス置換 < <(command) を使用すれば、while ループ自体が現在の親シェル内で実行されるため、集計変数やフラグの状態が完全に維持されます。無駄な cat のパイプを排除する意味でも、この記法を現場の標準にしてください。
カンマ区切り(CSV)の複数カラム分割やヘッダースキップ処理の詳細は bashでCSVを扱う方法まとめ|読み込み・ループ・配列処理の実践例 を参照してください。
5. ターミナルで即実行できる「1行ワンライナー」の書き方
スクリプトファイルを新規作成するまでもなく、SSHログイン中のターミナル上で「コマンドを一定間隔で叩いて結果を監視したい」「特定サービスが起動するまで待機したい」といった場面では、while 文を1行にまとめたワンライナーが威力を発揮します。
コマンドライン直打ちの基本ルール(セミコロン ; の位置)
複数行の while 文を1行に凝縮する際の基本構造は以下の通りです。
# ワンライナーの基本構文骨格
while 条件; do 処理1; 処理2; done
ポイントはセミコロン ; を置く位置です。改行の代わりに ; を挟むため、 do の直前 と 各処理の末尾(done の直前) に必ずセミコロンが必要です。
定期コマンド監視・ヘルスチェックのワンライナー例
現場で即座にコピペして使える実務ワンライナー集です。
# ワンライナー例1: 毎秒の日時と特定プロセスの稼働監視
while sleep 1; do date +'%T'; ps aux | grep -E 'node|python' | grep -v grep; done
# ワンライナー例2: WebサービスのHTTPステータスコードを2秒おきに監視
while true; do curl -s -o /dev/null -w "%{http_code} (%{time_total}s)n" https://example.com; sleep 2; done
# ワンライナー例3: デプロイ完了待ち(ポート8080のリスニング待機)
while ! nc -z localhost 8080; do echo "ポート開放待機中..."; sleep 1; done; echo "サービス開始!"
# ワンライナー例4: ディスク使用量の定期監視
while :; do df -h / | tail -n 1; sleep 10; done
特に while sleep 間隔; do コマンド; done という書き方は、条件部に sleep を直接指定することでコード長をさらに短縮できるスマートなテクニックです。終了したいときは Ctrl+C でいつでも安全に停止できます。
6. while文とfor文の違い・使い分け基準【比較マトリクス】
シェルスクリプトでループ処理を実装する際、「for 文と while 文のどちらを使うべきか」で迷うエンジニアは非常に多いです。両者には明確な設計思想の違いがあります。
回数や配列・リストが固定されている場合は「for文」
for 文は、あらかじめ処理対象のリストや要素数、繰り返し回数が決定している場合に最適です。
- ディレクトリ内の特定拡張子ファイル一覧(
for f in *.log; do) - 定義済みの配列要素のループ(
for env in "${ENV_LIST[@]}"; do) - 明確な数値範囲(ブレース展開:
for i in {1..10}; do)
動的条件やファイル読み込み・無限ループは「while文」
対して while 文は、繰り返し回数が事前に予測できず、状態の変化や外部リソースの応答に依存する場合に真価を発揮します。
- サーバーが応答するまでの待機や再試行(リトライ処理)
- デーモンやポーリング監視などの意図的な無限ループ(
while :) - 巨大なテキストファイルを行単位でストリーム処理するメモリ効率的な読み込み(
while read)
ひと目でわかる使い分け比較表
while 文と for 文の使い分け基準をまとめた比較マトリクス表です。
| 比較項目 | while 文 | for 文 |
|---|---|---|
| ループの基本思想 | 条件が真の間ループ(状態依存・未知回数) | 与えられた要素を順次取り出し(要素依存・既知回数) |
| 適した用途 | 常駐監視、リトライ待機、ファイル行処理 | 配列走査、固定回数ループ、コマンド引数処理 |
| 巨大ファイル処理 | ◎ 1行ずつ逐次読み込むためメモリ消費が極小 | × 全行を一括展開するためメモリ枯渇の危険大 |
| 無限ループ | ◎ while :; do で簡潔・安全に実装可能 | △ for ((;;)); do も可能だが可読性で劣る |
| 典型的な構文例 | while IFS= read -r line; do ... done < file | for item in "${items[@]}"; do ... done |
for 文のより詳細な構文バリエーションについては、関連記事 Bashのfor文は3種類|最適な書き方と安全テンプレ14選 をあわせてご活用ください。
よくあるエラーと落とし穴
while 文の実装時に頻発する代表的な構文エラーとその解決手順をまとめました。
括弧 [ ] の前後に半角スペースがない構文エラー
シェルスクリプト初心者で最も多い構文エラーがこちらです。
# NG例(スペースがない)
while [$i -lt 10]; do
# エラー: bash: [0: command not found
# OK例(前後に必ずスペースを配置)
while [ "$i" -lt 10 ]; do
なぜスペースが必要かというと、Bashにおいて [ は単なる括弧記号ではなく、 /usr/bin/[ という実体を持つ独立したコマンド だからです。スペースを空けないと [0 という名前の存在しないコマンドを実行しようとして構文エラーになります。
do や done の閉じ忘れ・配置ミス
do や done の位置関係や記述漏れによって発生するエラーも多発します。
# NG例1: 同一行にセミコロンなしで do を記述
while (( i < 5 )) do
echo "$i"
done
# エラー: syntax error near unexpected token `do`
# NG例2: done を閉じ忘れてスクリプト末尾に到達
while (( i < 5 )); do
echo "$i"
# エラー: syntax error: unexpected end of file
同一行に do を書く場合は必ず直前にセミコロン ; を置き、ブロックの末尾には必ず対応する done を配置してください。コードのインデントを正しく揃える習慣をつけることで、閉じ忘れを確実に防止できます。
まとめ
Bashの while 文を実務で安全かつ強力に使いこなすための重要ポイントは以下の4点です。
- 数値ループは
(( i < N ))、無限ループはwhile :+sleepを選ぶ : ネイティブの組み込み構文を活用することで、オーバーヘッドをなくし最速・安全に動作させる。 - 条件判定は「終了ステータス 0」が真であることを理解する : ブール値ではなくコマンドの成否を評価するため、通常のLinuxコマンドをそのまま条件部に配置可能。
- ファイル読み込みは
while IFS= read -r line || [[ -n "$line" ]]を徹底する : 行頭・行末の空白保持、バックスラッシュ消失防止、改行なし最終行の読み飛ばしを完全防御。 - パイプ渡しのサブシェル変数値消失はリダイレクト
done <やプロセス置換< <(...)で回避する : 親シェルでループを実行し、集計変数やフラグの状態を確実に維持する。
「手作業は負け」。適切な構文と安全対策を身につければ、日々の手動オペレーションをシンプルで堅牢なスクリプトへと置き換えることができます。ぜひ本記事のテンプレートを活用して、日々の業務自動化を一歩進めてください。
