強力なループ戦略:Bashスクリプトでのファイルとリストの反復処理

`for` と `while` を使用した必須のBashループテクニックを習得し、システムタスクを効率的に自動化します。この包括的なガイドでは、リストの反復処理、数値シーケンスの処理、`while IFS= read -r` などのベストプラクティスを使用したファイルの行ごとの堅牢な処理について説明します。基本的な構文、高度なループ制御(`break`、`continue`)、および実用的なコード例を含む、強力で信頼性の高いシェルスクリプトと自動化のための必須テクニックを学びます。

強力なループ戦略:Bashスクリプトでのファイルとリストの反復処理

Bashループは、小さなシェルコマンドを便利な自動化に変えます。ディレクトリ内のすべてのファイルを処理する必要がある場合、タスクを一定回数実行する必要がある場合、または設定データを行ごとに読み取る必要がある場合、ループはコマンドをコピー&ペーストせずに作業を繰り返す構造を提供します。

最もよく使用する2つのループは、forwhile です。配列やファイルグロブなど、既知のアイテムセットがすでにある場合は for を使用します。ループが条件または入力の読み取りによって駆動される場合は while を使用します。この単純な分割により、多くのスクリプトの推論が容易になります。


for ループ:固定セットの反復処理

for ループは、処理する必要があるアイテムのコレクションを事前に把握している場合に最適です。このコレクションは、明示的な値のリスト、コマンドの結果、またはグロブによって検出されたファイルのセットです。

1. 標準リストの反復処理

最も単純なユースケースは、スクリプトに直接記述された短い単語のリストを反復処理することです。

構文

for VARIABLE in LIST_OF_ITEMS; do
    # $VARIABLE を使用するコマンド
done

例:ユーザーリストの処理

# 処理するユーザーのリスト
USERS="alice bob charlie"

for user in $USERS; do
  echo "$user のホームディレクトリを確認中..."
  if [ -d "/home/$user" ]; then
    echo "$user はアクティブです。"
  else
    echo "警告: $user のホームディレクトリが見つかりません。"
  fi
done

このパターンは単純な名前には適しています。アイテムにスペースが含まれる可能性がある場合は、スペース区切りの文字列の代わりに配列を使用します。

USERS=("alice" "bob" "mary jane")

for user in "${USERS[@]}"; do
  echo "$user を確認中"
done

2. Cスタイルの数値反復

カウントや特定の数値シーケンスが必要なタスクの場合、BashはCスタイルの for ループをサポートしており、多くの場合、ブレース展開または seq コマンドと組み合わせて使用されます。

構文(Cスタイル)

for (( INITIALIZATION; CONDITION; INCREMENT )); do
    # コマンド
done

例:カウントダウンスクリプト

# 5回ループ(iは1から始まり、iが5以下の間続く)
for (( i=1; i<=5; i++ )); do
  echo "反復番号: $i"
  sleep 1
done
echo "完了!"

代替案:単純なシーケンスのためのブレース展開の使用

ブレース展開は、連続する整数やシーケンスを生成するために seq を使用するよりも単純で高速です。

# 10から1までの数値を生成
for num in {10..1}; do
  echo "カウントダウン: $num"
done

3. ファイルとディレクトリの反復処理(グロビング)

for ループ内でワイルドカード(*)を使用すると、特定のパターンに一致するファイル(すべてのログファイルやディレクトリ内のすべてのスクリプトなど)を処理できます。

例:ログファイルのアーカイブ

特にスペースや特殊文字を含むファイル名を扱う場合は、変数("$file")を引用符で囲みます。

TARGET_DIR="/var/log/application"

# ターゲットディレクトリ内の .log で終わるすべてのファイルをループ
for logfile in "$TARGET_DIR"/*.log; do

  # ファイルが実際に存在するか確認(ファイルが一致しない場合、リテラルの "*.log" で実行されるのを防ぐ)
  if [ -f "$logfile" ]; then
    echo "$logfile を圧縮中..."
    gzip "$logfile"
  fi
done

while ループ:条件ベースの実行

while ループは、指定された条件が真である限り、一連のコマンドを実行し続けます。これは、入力ストリームの読み取り、条件の監視、または反復回数が不明なタスクの処理によく使用されます。

1. 基本的な while ループ

構文

while CONDITION; do
    # コマンド
done

例:リソースの待機

このループは、test コマンド([ ])を使用して、続行する前にディレクトリが存在するかどうかを確認します。

RESOURCE_PATH="/mnt/data/share"

while [ ! -d "$RESOURCE_PATH" ]; do
  echo "リソース $RESOURCE_PATH がマウントされるのを待機中..."
  sleep 5
done

echo "リソースが利用可能です。バックアップを開始します。"

2. 堅牢な while read パターン

while ループの最も強力なアプリケーションは、ファイルの内容または出力ストリームを行ごとに読み取ることです。このパターンは、cat の出力に for ループを使用するよりもはるかに優れており、スペースや特殊文字を確実に処理します。

ベストプラクティス:行ごとの読み取り

最大の堅牢性を確保するために、3つの主要なコンポーネントを利用します。

  1. IFS=: 内部フィールド区切り文字をクリアし、先頭/末尾のスペースを含む行全体が変数に読み取られるようにします。
  2. read -r: -r オプションはバックスラッシュの解釈を防ぎ(raw読み取り)、パスや複雑な文字列にとって重要です。
  3. 入力リダイレクション(<: ファイルの内容をループにリダイレクトし、ループが現在のシェルコンテキストで実行されるようにします(サブシェルの問題を防ぎます)。
# データを含むファイル、1行に1アイテム
CONFIG_FILE="/etc/app/servers.txt"

while IFS= read -r server_name; do
  
  # 空行またはコメント行をスキップ
  if [[ -z "$server_name" || "$server_name" =~ ^# ]]; then
    continue
  fi

  echo "サーバーにpingを送信: $server_name"
  ping -c 1 "$server_name"

done < "$CONFIG_FILE"

ヒント:ループ内での cat の回避

ファイルを読み取る場合は、cat file | while ... よりも while ... done < file を優先します。ほとんどのBash設定では、パイプラインはサブシェルでループを実行するため、ループ内で変更された変数はループが終了すると失われます。

3. find からのファイル名の処理

再帰的なファイル処理の場合、プレーンな find 出力を行ごとに解析することは避けてください。ファイル名にはスペースや、まれに改行が含まれる場合があります。null区切りの出力を使用します。

find /var/log/application -type f -name '*.log' -print0 |
while IFS= read -r -d '' logfile; do
  echo "ログを発見: $logfile"
  gzip -- "$logfile"
done

-print0read -d '' のペアは、nullバイトを区切り文字として扱います。"$logfile" の前の -- は、後続の値がオプションではなくオペランドであることを gzip に伝え、ダッシュで始まるファイル名から保護します。

高度なループ制御とテクニック

効果的なスクリプトには、実行時の条件に基づいてループ実行を制御する機能が必要です。

1. フローの制御:breakcontinue

  • break: 残りの反復や条件に関係なく、ループ全体を即座に終了します。
  • continue: 現在の反復をスキップし、すぐに次の反復にジャンプします(または while 条件を再評価します)。

例:検索と停止

SEARCH_TARGET="target.conf"

for file in /etc/*; do
  if [ -f "$file" ] && [[ "$file" == *"$SEARCH_TARGET"* ]]; then
    echo "ターゲット設定を発見: $file"
    break  # 見つかったら処理を停止
  elif [ -d "$file" ]; then
    continue # ディレクトリをスキップ、ファイルのみチェック
  fi
  echo "ファイルを確認中: $file"
done

2. IFS を使用した複雑な区切り文字の処理

ファイルを行ごとに読み取るには IFS をクリアする必要がありますが、異なる文字(カンマなど)で区切られたリストを反復処理するには、一時的に IFS を設定する必要があります。

CSV_DATA="data1,data2,data3,data4"
OLD_IFS=$IFS # 元のIFSを保存
IFS=','       # IFSをカンマ文字に設定

for item in $CSV_DATA; do
  echo "アイテムを発見: $item"
done

IFS=$OLD_IFS # ループの直後に元のIFSを復元

警告:グローバルな IFS の変更

スクリプト内で $IFS を変更する前に、必ず元の $IFS を保存してください(例:OLD_IFS=$IFS)。元の値を復元しないと、後続のコマンドで予期しない動作が発生する可能性があります。

堅牢なBashループのためのベストプラクティス

プラクティス 根拠
常に変数を引用符で囲む "$variable" を使用して、特にファイルの反復処理において、単語分割とグロブ展開を防ぎます。
while IFS= read -r を使用する ファイルを行ごとに処理する最も信頼性の高い方法で、スペースや特殊文字を正しく処理します。
存在を確認する グロビング(*.txt)を使用する場合、ループが一致するファイルがない場合にリテラルパターン名を処理しないように、常にチェック(if [ -f "$file" ];)を含めます。
変数をローカライズする 関数内で local キーワードを使用して、ループ変数が誤ってグローバル変数を上書きしないようにします。
外部コマンドよりも組み込みコマンドを使用する パフォーマンスのために、seq などの外部コマンドを起動する代わりに、ブレース展開({1..10})またはCスタイルループを使用します。

実用的な経験則

メモリ内リストには配列、単純なファイルセットにはグロブ、行指向の入力には while IFS= read -r、再帰的なファイル名処理にはnull区切りの find 出力を使用します。デフォルトで展開を引用符で囲みます。グロブの周りに存在チェックを追加します。複雑な制御フローを隠す方法としてではなく、ループを読みやすくする場合にのみ breakcontinue を使用します。

ほとんどのBashループのバグは、単語分割、予期しないファイル名、または入力が実際よりもクリーンであると想定することから発生します。ループがスペース、空行、コメント、および欠落している一致を意図的に処理する場合、実際の自動化作業で生き残ることができます。