Bashのwhile文とreadコマンド:テキストファイルを1行ずつ安全に処理するスクリプト設計

設計パターン&テンプレ

Bashスクリプトで「CSVファイルやログファイルなどのテキストデータを1行ずつ読み込んで処理する」という要件は、実務で非常によく発生します。この処理を行う際、最も確実で安全な方法が while ループと read コマンドの組み合わせです。

しかし、単に while read を書いただけでは、行末の空白が消えたり、バックスラッシュ(\)がエスケープ文字として解釈されたりといった「落とし穴」にハマることがあります。

本記事では、テキストファイルを1行ずつ処理するための「壊れにくい」スクリプト設計の型(テンプレート)と、その仕組みを解説します。

1. 最も安全な「1行ずつ読み込む」基本の型

まずは結論から。テキストファイルを1行ずつ処理する場合、以下の構文を「基本の型」として使用してください。

#!/bin/bash

# 処理対象のファイル
INPUT_FILE="data.txt"

# ファイルが存在するか確認
if [[ ! -f "$INPUT_FILE" ]]; then
  echo "Error: File not found: $INPUT_FILE" >&2
  exit 1
fi

# IFS= と -r オプションを必ずつける
while IFS= read -r line; do
  # 1行ごとの処理をここに書く
  echo "Processing: $line"
done < "$INPUT_FILE"

この型には、安全に処理するための2つの重要なポイントが含まれています。

ポイント1:read -r(バックスラッシュの解釈を防ぐ)

read コマンドに -r オプションを付けないと、読み込んだ行にバックスラッシュ(\)が含まれていた場合、エスケープ文字として解釈されてしまい、意図しない文字列に変換されてしまいます。

ファイルの内容を「そのまま」変数に格納するためには、-r(raw モード)が必須です。

ポイント2:IFS=(行頭・行末の空白を保持する)

IFS(Internal Field Separator)は、Bashが単語を区切るための文字を定義する環境変数です。デフォルトではスペース、タブ、改行が含まれています。

read コマンドは、読み込んだ行を IFS の文字で分割しようとします。そのため、IFS=(空にする)を指定せずに読み込むと、行頭や行末にあるスペースやタブが削除(トリミング)されてしまいます。

ログファイルなど、フォーマットが厳密なファイルを処理する場合は、空白も重要な意味を持つことが多いため、IFS= を指定して元の文字列を完全に保持する必要があります。

2. CSVファイルなど、特定の文字で分割して読み込む

カンマ(,)区切りのCSVファイルなどを処理する場合は、IFS にカンマを指定し、read コマンドに複数の変数を渡すことで、行を読み込みながら同時にフィールドを分割できます。

#!/bin/bash

CSV_FILE="users.csv"

# 1行目(ヘッダー)をスキップしたい場合は、ループの前に1回 read する
# read -r header < "$CSV_FILE"

# IFS にカンマを指定し、3つの変数(id, name, email)に分割して読み込む
while IFS=, read -r id name email; do
  # 空行をスキップする処理(必要に応じて)
  if [[ -z "$id" ]]; then
    continue
  fi

  echo "ID: $id, Name: $name, Email: $email"
done < "$CSV_FILE"

この方法を使うと、awkcut コマンドを使わずに、Bashの組み込み機能だけで高速にCSVを処理できます。

3. コマンドの実行結果を1行ずつ処理する

ファイルからではなく、他のコマンド(findls など)の実行結果を1行ずつ処理したい場合は、パイプ(|)を使って while ループに渡します。

#!/bin/bash

# /var/log 配下の .log ファイルを検索し、1つずつ処理する
find /var/log -name "*.log" -type f | while IFS= read -r log_file; do
  echo "Found log file: $log_file"
  # ここでファイルのバックアップや圧縮などの処理を行う
  # gzip "$log_file"
done

パイプと while ループの注意点(サブシェル問題)

パイプを使って while ループにデータを渡すと、ループ全体が「サブシェル(別プロセス)」で実行されます。そのため、ループ内で変数の値を変更しても、ループを抜けた後には元の値に戻ってしまうという罠があります。

#!/bin/bash

count=0

# パイプを使うとサブシェルになる
cat data.txt | while IFS= read -r line; do
  ((count++))
done

# ループを抜けると count は 0 のまま!
echo "Total lines: $count"

これを回避するには、パイプを使わずに「プロセス置換」を使用します。

#!/bin/bash

count=0

# プロセス置換 < <(...) を使う
while IFS= read -r line; do
  ((count++))
done < <(cat data.txt)

# ループを抜けても count の値が保持される
echo "Total lines: $count"

まとめ

テキストファイルを1行ずつ処理する際は、for ループではなく while read を使用するのがBashスクリプトの鉄則です。

  1. 基本の型は while IFS= read -r line; do ... done < "file.txt"
  2. CSVなどを分割する場合は IFS=, read -r col1 col2 のように IFS を指定する
  3. ループ内で変数を更新したい場合は、パイプではなくプロセス置換 < <(...) を使う

この3つのポイントを押さえておけば、意図しないデータの欠落やバグを防ぎ、安全で堅牢なスクリプトを設計することができます。

Bash玄

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

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

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

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

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

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

Bash玄をフォローする