Bashスクリプトで日々の定常作業やバッチ処理を自動化する際、最も頻繁に利用される基本構文が `for` 文 です。サーバー一覧に対する一括コマンド実行、特定ディレクトリ内のファイル一括変換、指定回数の連番ループ処理、さらには多重ループによるマトリクス処理まで、インフラ運用・開発の現場において for 文を使わない日はありません。
しかし実務の現場では、「スペースを含むファイル名を処理しようとしてスクリプトが誤作動した」「変数をブレース展開 {1..$N} に渡したら動かなかった」「for 文と while 文の使い分け基準が曖昧で非効率なコードを書いてしまった」といったトラブルに直面するエンジニアが後を絶ちません。シェルスクリプトの基礎を体系的に学びたい方は、親ピラー記事の シェルスクリプト入門 もあわせてご活用ください。
こんにちは、 Bash玄(ばっしゅげん) です。当サイトのモットーは「手作業は負け」「スクリプトはシンプルに」。本記事では、for 文の基本構文3パターンの早見表から、安全なファイル一覧ループ、改行区切りコマンド置換の落とし穴とIFS制御、配列要素・インデックスの走査、二重ループと break 2 / continue による制御、エラーハンドリング連携、そして while 文との使い分け比較マトリクスまで、現場目線で余すところなく徹底解説します。
結論!Bash for文の基本構文3パターン早見表
Bashにおける for 文には、用途に応じて大きく 3つの基本パターン が存在します。まずはファーストビューで、今すぐコピペして使える基本構文と特徴を確認しましょう。
# パターン1: リスト反復(単語・文字列・配列・ファイル一覧を回す)
for item in "apple" "banana" "orange"; do
echo "フルーツ名: $item"
done
# パターン2: ブレース展開による数値連番(固定範囲・ステップ指定・ゼロ埋め)
for i in {1..5}; do
echo "ステップ番号: $i"
done
# パターン3: C言語形式(動的変数・複雑なステップ計算・インデックス制御)
for ((i = 0; i < 5; i++)); do
echo "カウント: $i"
done
3大パターンの特徴・用途比較マトリクス
どの構文を選ぶべきかは、 「何をループ対象にするか(固定リストか、動的な数値範囲か)」 によって明確に決まります。
| パターン名 | 基本構文例 | 最適な用途 | メリット・特徴 | 注意点・落とし穴 |
|---|---|---|---|---|
| 1. リスト反復 | for x in "${list[@]}"; do | 配列走査、ファイル一覧(glob)、引数処理 | 直感的で可読性が高く、多様な要素を順次処理できる | 空白を含む要素は 必ずダブルクォート で囲む必要がある |
| 2. ブレース展開 | for i in {01..10..2}; do | 固定回数の連番、月日(01〜12)、ステップ指定 | 外部コマンド不要で高速、ゼロ埋め対応 | 静的展開 のため変数({1..$N})は解釈されない |
| 3. C言語形式 | for ((i=0; i<N; i++)); do | 変数を含む数値ループ、インデックス加算 | 変数 $N を扱え、他言語出身者にも直感的 | 単純なリスト走査には向かず、Bash専用構文(sh非互換) |
実務で頻出するfor文の活用テクニック集
ここからは、実際のシステム開発やサーバー管理で頻出する for 文の実践的テクニックを解説します。
ディレクトリ内のファイル一覧を安全にループ処理する
ディレクトリ内の複数ファイルを順次処理する場合、 ワイルドカードを用いたglob展開(パス名展開) を使用するのが最も安全で堅牢な手法です。
#!/bin/bash
set -euo pipefail
TARGET_DIR="/var/log/app"
# 対象ファイルが0件の場合にワイルドカード文字列自身が残るのを防ぐ
shopt -s nullglob
for file in "$TARGET_DIR"/*.log; do
# 通常ファイルであることを念のため確認
[[ -f "$file" ]] || continue
echo "処理対象ファイル: $file"
# ここにバックアップやgzip圧縮などの実処理を記述
gzip -k "$file"
done
# 設定を元に戻す(必要に応じて)
shopt -u nullglob
実務での安全設計ポイント
shopt -s nullglobの設定 : デフォルトのBashでは、該当するファイルが1件も存在しない場合、ループ変数fileに"/var/log/app/*.log"というアスタリスク付きの文字列がそのまま渡されてしまいます。nullglobを有効にしておけば、該当なしの場合は空(要素ゼロ)として処理され、ループ自体がスキップされるためエラーを防げます。- ダブルクォートの配置 :
"$TARGET_DIR"/*.logのように、ディレクトリ変数部分のみをクォートし、ワイルドカード記号*はクォートの外側に置きます。全体をクォート("$TARGET_DIR/*.log")してしまうと、ワイルドカードが文字列リテラルとみなされ展開されません。 - 隠しファイルも含める場合 : ドットから始まるファイルも対象に含めたい場合は、
shopt -s dotglob nullglobを併用します。
コマンド実行結果(コマンド置換 $(cat list.txt))をループで回す際の注意点と改行区切りの落とし穴
「サーバー一覧が記載されたテキストファイルやコマンド出力を for 文で回したい」という場面は多いですが、ここには シェルスクリプト最大の落とし穴の一つである「単語分割(Word Splitting)」 が潜んでいます。
# 【危険なアンチパターン】
for line in $(cat servers.txt); do
echo "接続先: $line"
done
Bashのデフォルトの区切り文字変数 IFS(Internal Field Separator)には、 スペース・タブ・改行 が設定されています。そのため、もし行内にスペースが含まれていると、1行のデータがスペースで勝手に分断されて別々のループ周回として処理されてしまいます。
テキストを行単位で確実に for 文処理したい場合は、以下のように一時的に IFS を改行のみに制限します。
#!/bin/bash
set -euo pipefail
SERVER_LIST="web-01.example.com
web-02.example.com
staging server 01.example.com"
# 現在のIFSをバックアップし、改行コードのみに設定
OLD_IFS="$IFS"
IFS=$'\n'
for server in $SERVER_LIST; do
# 前後の空白を取り除き、空行をスキップ
[[ -z "$server" ]] && continue
echo "SSH接続確認中: $server"
done
# IFSを安全にリストア
IFS="$OLD_IFS"
Tips : 外部テキストファイルを1行ずつ読み込む処理においては、
for文よりもメモリ効率が高くストリーム処理に適したwhile IFS= read -r line構文 を使用するのが現場のベストプラクティスです。詳細な比較は後述の使い分けセクションをご覧ください。
配列(Array)の全要素・インデックスをループ処理する
Bashの配列操作と for 文の連携は、パラメータの一括管理において欠かせません。配列の詳細な扱いは 【Bash】シェルスクリプトでの配列の使い方まとめ でも網羅していますが、for 文では以下の2つの書き方を使い分けます。
#!/bin/bash
set -euo pipefail
# インフラ環境一覧の配列定義
environments=("development" "staging" "production-primary" "production-dr")
# 1. 全要素の値をループ処理("${array[@]}")
echo "=== 環境一覧の反復 ==="
for env in "${environments[@]}"; do
echo "デプロイ対象環境: $env"
done
# 2. 配列インデックス(添字)を取得してループ処理("${!array[@]}")
echo "=== インデックス付き反復 ==="
for i in "${!environments[@]}"; do
echo "ID: $i | 環境名: ${environments[i]}"
done
連想配列(ハッシュマップ)のキー・バリュー走査
Bash 4.0以上で利用可能な連想配列(declare -A)も、インデックス展開記法 ${!map[@]} を使ってスマートに全キーを走査できます。
#!/bin/bash
set -euo pipefail
# 連想配列の宣言と初期化
declare -A server_roles=(
["web01"]="Nginx Frontend"
["app01"]="Node.js Backend"
["db01"]="PostgreSQL Primary"
)
# すべてのキーを走査して対応する値を取得
for host in "${!server_roles[@]}"; do
echo "ホスト: $host => 役割: ${server_roles[$host]}"
done
スクリプトのコマンドライン引数(”$@”)をループ処理する
スクリプトに渡された複数の引数を順次処理する場合、特殊変数 "$@" を使った for 文が鉄板です。引数の詳細な仕様については 【Bash】スクリプトのコマンドライン引数受け取り方完全ガイド を参照してください。
#!/bin/bash
set -euo pipefail
# 引数が指定されていない場合は使い方を表示して終了
if (( $# == 0 )); then
echo "使用法: $0 <ファイル1> [ファイル2] ..." >&2
exit 1
fi
echo "指定された引数の総数: $#"
# すべての位置パラメータを1つずつ安全に処理
for arg in "$@"; do
if [[ -f "$arg" ]]; then
echo "[FILE] 存在確認OK: $arg (サイズ: $(wc -c < "$arg") bytes)"
else
echo "[WARN] ファイルが見つかりません: $arg" >&2
fi
done
"$@" は各引数を個別のダブルクォートで囲んだ状態("$1" "$2" ...)として展開されるため、スペースを含む引数(例: "my file.txt")であっても完全に単一要素として維持されます。"$*" を使ってしまうと全引数が1つの文字列に連結されてしまうため、必ず "$@" を使用してください。なお、in "$@" は省略して単に for arg; do と書くことも可能です。
二重ループ(ネスト)によるマトリクス処理とログ整形の実例
実務では、「複数ホスト × 複数サービス」の状態確認や、行列データの整形などでループを入れ子(ネスト)にする 二重ループ が頻出します。
#!/bin/bash
set -euo pipefail
hosts=("web01" "web02" "db01")
services=("httpd" "sshd" "crond")
echo "=== システムヘルスチェック開始 ==="
# 外側ループ: 対象サーバー
for host in "${hosts[@]}"; do
echo "----------------------------------------"
echo "Target Host: $host"
echo "----------------------------------------"
# 内側ループ: 対象サービス
for svc in "${services[@]}"; do
echo " Checking service [$svc] on host [$host]..."
# 疑似チェック処理
# ssh "$host" "systemctl is-active $svc" || echo " [WARN] $svc is DOWN!"
done
done
echo "すべての検査が完了しました。"
二重ループを記述する際は、ループカウンタ変数や要素変数の命名(外側を host、内側を svc など具体的名詞にする)を明確にし、インデントを正確に揃えることで保守性を高めましょう。
ループ制御構文(breakとcontinue)の確実な使い分け
反復処理を意図したタイミングで中断したり、特定の条件のみ処理をスキップするには、制御コマンド break と continue を正しく使いこなす必要があります。
特定条件でループを途中脱出する break(多重ループ脱出 break 2)
break は、実行された時点でループを強制終了し、done の直後の処理へ制御を移します。
#!/bin/bash
set -euo pipefail
echo "正常なノードを探索中..."
for node in "node-A" "node-B" "node-C" "node-D"; do
echo "ノード [$node] を検証中..."
# 条件分岐の書き方は if文完全ガイド(https://bashdo.com/post/bash-if/)を参照
if [[ "$node" == "node-B" ]]; then
echo ">> 目的の健全ノード [$node] を発見しました。探索を終了します。"
break
fi
done
echo "後続処理へ移行します。"
二重ループを一気に脱出する「break 2」
ネストされたループ内で単に break を実行すると、 内側のループのみが脱出 され、外側のループは継続してしまいます。一撃で外側ループまでまとめて脱出したい場合は、引数に階層数を指定する break 2 を使用します。
#!/bin/bash
set -euo pipefail
found_critical_error=false
for cluster in "cluster-alpha" "cluster-beta" "cluster-gamma"; do
echo "クラスタ検証: $cluster"
for node in "node-01" "node-02" "node-03"; do
echo " ノード検査: $node"
# 重大障害を検知した場合、全クラスタの検査を即時完全停止する
if [[ "$cluster" == "cluster-beta" && "$node" == "node-02" ]]; then
echo " [CRITICAL] 致命的異常を検知!全ループを緊急脱出します。"
break 2 # 2階層分のループ(内側と外側)をまとめて脱出
fi
done
done
echo "検査フェーズ終了。"
特定条件で処理をスキップして次の反復へ進む continue
continue は、ループ内のそれ以降の処理を実行せずにスキップし、 次の反復周回(次の要素の取り出し)へ直ちに移行 します。
#!/bin/bash
set -euo pipefail
for file in /etc/*; do
# ディレクトリは除外して通常ファイルのみ処理
[[ -d "$file" ]] && continue
# ドットから始まる隠しファイルは除外
[[ "$(basename "$file")" =~ ^\. ]] && continue
echo "処理対象ファイル: $(basename "$file")"
done
多重ループにおいて外側の次の周回へ進めたい場合は、同様に continue 2 を使用できます。
ループ内のエラーハンドリングと処理継続設計
スクリプト冒頭で set -e(エラー発生時に即時終了)を宣言している本番環境では、ループ内の1つの処理が失敗しただけでスクリプト全体が停止してしまいます。バッチ処理で「失敗したファイルがあっても残りのファイルを最後まで処理し、最後にエラー件数を集計して終了判定したい」という場合は、|| true や終了ステータスチェックを組み合わせます。エラーハンドリング設計の詳細は 【Bash】エラーハンドリング完全設計 を参照してください。
#!/bin/bash
set -euo pipefail
targets=("archive_a.tar.gz" "archive_b.tar.gz" "missing_file.tar.gz" "archive_c.tar.gz")
failed_count=0
for target in "${targets[@]}"; do
echo "展開処理中: $target"
# コマンドの失敗をキャッチして即死を防ぐ
if ! tar -tzf "$target" > /dev/null 2>&1; then
echo "[ERROR] アーカイブ破損または不在: $target" >&2
(( failed_count++ ))
continue
fi
echo "正常に検証完了: $target"
done
if (( failed_count > 0 )); then
echo "[ALERT] 合計 $failed_count 件の処理が失敗しました。" >&2
exit 1
fi
echo "すべてのバッチ処理が正常に完了しました。"
【徹底比較】for文 vs while文の使い分けマトリクス表
シェルスクリプトでループを書く際、「for 文を使うべきか、while 文を使うべきか」で迷うエンジニアは非常に多いです。先行するガイド記事 【Bash】while文の書き方完全ガイド と連動し、両者の本質的な設計思想の違いを整理しました。
設計思想の違い:要素ベース vs 状態・条件ベース
for文(要素反復) : 処理対象となる「要素のリスト(配列、ファイル一覧、固定連番)」が ループ開始前の時点で明確に決まっている場合 に最適です。リスト内の全アイテムを端から順番に取り出して処理します。while文(条件反復) : 「コマンドが成功するまで待機する」「特定の条件を満たしている間だけ繰り返す」「テキストファイルを行末(EOF)まで1行ずつ流し込む」といった、 反復回数が事前には確定せず、状態の変化に依存する場合 に真価を発揮します。
ひと目でわかる使い分け比較マトリクス
| 比較項目 | for 文 | while 文 |
|---|---|---|
| 基本思想 | 与えられた要素リストを走査(既知の有限回数) | 条件が真(終了ステータス0)の間ループ(状態依存・未知回数) |
| 適した代表的用途 | ・ファイル一覧(glob)の一括処理 ・定義済み配列の走査 ・固定の数値連番(1〜100等) ・コマンド引数( "$@")の走査 | ・サーバー応答待ち・リトライ待機(ポーリング) ・常駐監視デーモン(安全な無限ループ while :)・テキスト/CSVの1行ずつ安全な読み込み |
| 巨大ファイルの行処理 | × 非推奨(全行をメモリに一括展開するためメモリ枯渇の危険) | ◎ 最適(ストリームで1行ずつ逐次読み込み、省メモリ) |
| 無限ループの記述 | for ((;;)); do(記述可能だが直感性に欠ける) | while :; do sleep N; done(実務推奨・最軽量) |
| 典型的な構文テンプレート | for file in "$DIR"/*.txt; dofor item in "${array[@]}"; do | while IFS= read -r line; do ... done < filewhile ! ping -c 1 host; do sleep 2; done |
初心者がハマるアンチパターンとエラー対処法
for 文は手軽に書ける反面、シェルの文字列展開ルールを正しく理解していないと思わぬセキュリティインシデントやデータ消失事故を招きます。
アンチパターン1: for file in $(ls *.txt) の禁止
シェルスクリプト初心者の方が最もやってしまいがちな重大なアンチパターンが for file in $(ls *.txt) です。
# 【絶対にやってはいけない危険な書き方】
for file in $(ls *.txt); do
rm -f "$file"
done
なぜ $(ls) を使ってはいけないのか?
もしファイル名に「system backup data.txt」のように 半角スペース が含まれていた場合、コマンド置換 $(ls *.txt) の結果に対してシェルが単語分割(Word Splitting)を発動させます。
その結果、ループは以下のように3回に分断されてしまいます:
- 1回目の周回:
file="system" - 2回目の周回:
file="backup" - 3回目の周回:
file="data.txt"
存在しない system や backup に対して削除コマンドが走り、エラーになるだけでなく、もしカレントディレクトリに system という名前の別ファイルが存在した場合は 意図しない重要データが誤削除される大惨事 に直結します。
# 【安全な正解】glob展開を直接使う
shopt -s nullglob
for file in *.txt; do
[[ -f "$file" ]] || continue
rm -f -- "$file"
done
shopt -u nullglob
パス名展開(glob)を使えば、スペースや改行を含むファイル名であっても1つの要素として完璧に保護されます。
アンチパターン2: 変数のクォート忘れ(”$item”)による単語分割事故の防止
ループブロック内部で変数を展開する際、ダブルクォートを省略($item)するのも非常に危険です。
# NG例: クォートしていない
for item in "My Document" "Work Space"; do
mkdir /tmp/$item # /tmp/My と /tmp/Document の2つのディレクトリができてしまう!
done
# OK例: 必ずダブルクォートで保護する
for item in "My Document" "Work Space"; do
mkdir -p "/tmp/$item" # 「/tmp/My Document」が正しく作成される
done
シェルスクリプトにおいて、 「変数展開はダブルクォートで囲む」 のが鉄則です。意図しない単語分割やglob再展開によるバグを100%防止しましょう。
アンチパターン3: ブレース展開 {1..$N} で変数が展開されない罠
「変数 $N に格納された回数だけループさせたい」と考え、以下のように記述してハマるケースが多発します。
# 動かないNG例
N=5
for i in {1..$N}; do
echo "$i"
done
# 出力結果: {1..5} という文字列がそのまま1回出力されて終わる!
なぜ動かないのか?
Bashの内部処理において、 ブレース展開(Brace Expansion)は変数展開(Parameter Expansion)よりも「前」の評価フェーズで実行される 仕様になっているためです。{1..$N} の段階ではまだ $N は数字に置き換わっておらず、ブレース展開エンジンはこれを数値範囲と認識できません。
正しい解決策:C言語形式構文を使う
変数を使った数値範囲ループには、 C言語形式構文 (( ... )) を使用するのが最もスマートで高速です。
# OK例: C言語形式構文を採用する
N=5
for ((i = 1; i <= N; i++)); do
echo "ステップ: $i"
done
外部コマンド seq(for i in $(seq 1 $N))を呼び出す手法もありますが、ループのたびに別プロセスをフォークするため実行コストがかかります。Bash環境であればC言語形式を選ぶのが最善です。
まとめ & 関連記事
Bashにおける for 文の正しい書き方と安全運用の要点を振り返りましょう。
- 基本構文は用途で使い分ける : 配列・ファイル走査は「リスト反復」、固定数値は「ブレース展開
{1..10}」、変数を含む数値ループは「C言語形式for ((i=0; i<N; i++))」を選ぶ。 - ファイル一覧ループは glob展開を徹底する :
for f in $(ls)はスペース入りファイル名で破綻するため禁止。shopt -s nullglobと直接のパス名展開(for f in *.txt)を組み合わせる。 - ループ内の変数は必ずダブルクォート(
"$var")で囲む : 単語分割や意図せぬパス展開事故を未然に防ぐ。 - 多重ループの即時脱出は
break 2を活用する : 状況に応じてcontinueと使い分け、無駄な処理をスマートにスキップする。 - ファイル1行処理や無限監視は
while文に任せる : 要素が確定している場合はfor文、動的条件やストリーム処理はwhile文という適材適所の設計を守る。
シェルスクリプトの品質をさらに高めるために、以下の関連ガイドもあわせてご活用ください。

コメント