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"この方法を使うと、awk や cut コマンドを使わずに、Bashの組み込み機能だけで高速にCSVを処理できます。
3. コマンドの実行結果を1行ずつ処理する
ファイルからではなく、他のコマンド(find や ls など)の実行結果を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スクリプトの鉄則です。
- 基本の型は
while IFS= read -r line; do ... done < "file.txt" - CSVなどを分割する場合は
IFS=, read -r col1 col2のようにIFSを指定する - ループ内で変数を更新したい場合は、パイプではなくプロセス置換
< <(...)を使う
この3つのポイントを押さえておけば、意図しないデータの欠落やバグを防ぎ、安全で堅牢なスクリプトを設計することができます。
