【Linux】readで入力を読み取る方法|while readの定番・-rの罠・IFSで区切り変更

【Linux】readで入力を読み取る方法|while readの定番・-rの罠・IFSで区切り変更 Linux

readは、標準入力から1行を読み取って変数に入れるコマンドです。キーボードからの対話的な入力を受け取ったり、ファイルを1行ずつ処理したりと、シェルスクリプトで入力を扱う中心的な役割を担います。特にwhile readでファイルを1行ずつループ処理するパターンは、ログ解析やデータ変換で毎日のように使われる定番です。

ただしreadには、知らないとデータが壊れる2つの重要な罠があります。1つはバックスラッシュ(\)を勝手に解釈してしまうこと(-rで防ぐ)、もう1つはファイルの最終行に改行が無いと、最後の1行を読み落とすことです。この記事では、実機のLinux(WSLのDebian)でこれらの罠を実際に再現しながら、readの正しい使い方を整理します。

先に結論

  • read 変数で標準入力から1行を読み、変数に入れます。
  • 複数変数を書くと空白で分割され、余りは最後の変数に入ります。
  • ファイルの1行ずつ処理はwhile read -r 行; do ... done < ファイルが定番です。
  • readはバックスラッシュを解釈するので、ほぼ常に-rを付けます
  • 最終行に改行が無いと最後を読み落とすため|| [ -n "$行" ]で対策します。
  • 区切り文字はIFSで変更でき、read -pでプロンプトを表示できます。

入力元になるパイプはリダイレクトとパイプ、条件判定はtest・[ ]、列の切り出しはcutもあわせて参考になります。

スポンサーリンク

readの基本:1行を変数に読み込む

read 変数名で、標準入力から1行を読み取って変数に格納します。複数の変数名を並べると、空白(スペースやタブ)で区切ってそれぞれに代入されます。

read の基本
# 1行を読んで変数 line に入れる
echo "hello world" | { read line; echo "$line"; }
# hello world

# 複数変数に分割(空白区切り)
echo "taro 30 tokyo" | { read name age city; echo "$name/$age/$city"; }
# taro/30/tokyo

# 変数より要素が多い場合、余りは最後の変数にまとめて入る
echo "a b c d e" | { read x y z; echo "$z"; }
# c d e   ← z に「c d e」がまとめて入る

実機でも、read lineで1行がhello worldとして読み込まれ、read name age city空白区切りの3要素がそれぞれの変数にtaro/30/tokyo)代入されました。要素が変数より多い場合は、余りがすべて最後の変数にまとめて入るzc d e)ことも確認しました。この「最後の変数が残り全部を受け取る」性質は、「先頭のフィールドだけ分けて、残りは1つにまとめたい」ときに便利です。

while readでファイルを1行ずつ処理する

readが最も活躍するのが、while readループです。ファイルやコマンドの出力を1行ずつ確実に処理できます。ログ解析やデータ変換の超定番パターンです。

while read の定番パターン
# ファイルを1行ずつ処理する
while read -r line; do
  echo "処理中: $line"
done < data.txt

# コマンドの出力を1行ずつ処理する(パイプ)
some_command | while read -r line; do
  echo "$line"
done

# 複数フィールドに分けながら処理
while read -r name age; do
  echo "$name は $age 歳"
done < people.txt

実機でも、while read -r line; do ... done < data.txtファイルの各行(line1・line2・line3)が順番に処理されることを確認しました。awkでも行単位の処理はできますが、各行に対して複数のコマンドを実行したり、条件分岐を入れたりと、複雑な処理をしたいときはwhile readのほうが柔軟です。ここでread-rを付けているのがポイントで、その理由を次で説明します。

【最重要】-rを付けないとバックスラッシュが消える

readの最大の罠がこれです。readは既定でバックスラッシュ(\)を「エスケープ文字」として解釈してしまい、そのまま消したり、次の文字と組み合わせたりします。データに\が含まれると壊れるため、ほぼ常に-r(raw)を付けるのが鉄則です。

read と read -r の違い(実証)
# 入力: path\to\file (バックスラッシュを含むパス)

# -r なし: バックスラッシュが解釈されて消えてしまう
printf '%s\n' 'path\to\file' | { read line; echo "$line"; }
# pathtofile     ← \ が全部消えた!

# -r あり: バックスラッシュがそのまま保持される
printf '%s\n' 'path\to\file' | { read -r line; echo "$line"; }
# path\to\file   ← 正しく保持される
readは-r無しだとバックスラッシュを壊す(実証)

実機で確認したところ、path\to\fileというバックスラッシュを含む文字列を-rなしのreadで読むと、結果はpathtofile——バックスラッシュがすべて消えてしまいました-rを付けるとpath\to\fileと正しく保持されました。さらに、行末がバックスラッシュの場合、-rなしのread「行継続」とみなして次の行と連結してしまい(first\+改行+secondfirstsecondになる)、-rありならfirst\のまま止まることも確認しました。WindowsのパスやJSON、正規表現など、バックスラッシュを含むデータは世の中にあふれています。それらをreadで読むときに-rを忘れると、データが静かに壊れてしまうのです。実害を防ぐため、readを使うときは反射的にread -rと書く習慣をつけてください。-rを付けて困ることはまずありません。

【罠】最終行に改行が無いと読み落とす

もう一つの重要な罠です。while read「改行で1行の区切り」を判断するため、ファイルの最後の行に改行が無いと、その最後の行を読み落とすことがあります。手で作ったファイルや、一部のプログラムが出力したファイルで起こりがちです。

最終行の改行なし対策
# 最終行(C)に改行が無いファイルを処理すると…
# 中身: A[改行]B[改行]C   ← C の後ろに改行なし

while read -r line; do echo "$line"; done < noeol.txt
# A
# B
#   ← C が出力されない!(改行が無いので読み落とす)

# 対策: || [ -n "$line" ] を付ける
while read -r line || [ -n "$line" ]; do
  echo "$line"
done < noeol.txt
# A
# B
# C      ← 最後の行も正しく処理される
|| [ -n “$line” ]で最終行を拾う(実証)

実機で、最終行Cに改行が無いファイルをwhile readで処理すると、ABは出力されたのにCが出力されませんでしたreadは改行を読んで初めて「1行完成」とみなすため、改行の無い最後の行は「未完成」として捨てられてしまうのです。対策は、ループ条件をwhile read -r line || [ -n "$line" ]; doとすることです。これは「readが成功したら(=改行付きの行を読んだら)ループ、失敗しても変数が空でなければ(=改行無しの最後の行が残っていれば)もう一度ループ」という意味です。実機でもこの対策でCまで正しく処理できました。自分で用意したデータや、末尾に改行があるか分からないファイルを扱うときは、この|| [ -n "$line" ]を付けておくと安全です。地味ですが、データの1行が静かに欠落する事故を防ぐ重要なイディオムです。

IFSで区切り文字を変える(CSVを読む)

readが単語を分割する区切り文字は、IFS(Internal Field Separator)という特殊な変数で決まっています。既定は空白(スペース・タブ・改行)ですが、これを変えるとCSV(カンマ区切り)などを直接読めるようになります。

IFSで区切りを変更
# IFS をカンマにして CSV を読む
echo "taro,30,tokyo" | { IFS=, read name age city; echo "$name/$city"; }
# taro/tokyo

# while read と組み合わせて CSV ファイルを処理
while IFS=, read -r name age city; do
  echo "$name さん($age歳) - $city"
done < people.csv

# コロン区切りの /etc/passwd を読む例
while IFS=: read -r user _ uid _; do
  echo "$user: UID=$uid"
done < /etc/passwd

実機でも、IFS=, read name age cityでカンマ区切りのCSVが各変数に正しく分割される(taro/tokyo)ことを確認しました。IFS=,readと同じ行に書くと、そのreadコマンドの間だけ区切り文字が変わり、後続に影響しません。while IFS=: read ...のように書けば、コロン区切りの/etc/passwdなども1行ずつフィールドに分けて処理できます。cutawkでも列を扱えますが、「各行を読みながら複数のフィールドを変数に入れて、複雑な処理をする」用途ではwhile IFS=... readが便利です。

read -pでプロンプトを表示する

対話的にユーザーへ入力を促すときはread -pでプロンプト(案内メッセージ)を表示できます。パスワード入力を隠す-sもあわせて紹介します。

read -p / -s
# -p: プロンプトを表示して入力を待つ
read -p "お名前は? " name
echo "こんにちは、$name さん"

# -s: 入力を画面に表示しない(パスワード向き)
read -sp "パスワード: " pass
echo   # 改行を入れる

# タイムアウトを設定(-t 秒)
read -t 10 -p "10秒以内に入力: " answer

read -p "メッセージ "とすると、入力待ちの前にメッセージを表示できます(実機でも入力が変数に格納されることを確認しています)。パスワードのように画面に表示したくない入力は-s(silent)を付けます(入力文字が見えなくなります)。-t 秒タイムアウトも設定でき、一定時間入力が無ければ処理を進められます。対話的なスクリプトを作るときの基本オプションです。

主な書き方一覧

readの要点をまとめます。

書き方 働き
read 変数 標準入力から1行読む
read -r 変数 バックスラッシュを解釈しない(推奨)
while read -r l; do ... done < file ファイルを1行ずつ処理
|| [ -n "$l" ] 最終行に改行が無くても読む
IFS=, read a b c 区切り文字をカンマに変更
read -p / -s / -t プロンプト / 非表示 / タイムアウト

よくある失敗

バックスラッシュを含むデータが壊れる

read-rなしだとバックスラッシュを解釈します。read -rを使います。

ファイルの最後の行が処理されない

最終行に改行が無いと読み落とします。while read -r l || [ -n "$l" ]で対策します。

CSVが空白で区切られてしまう

既定の区切りは空白です。IFS=,をreadの前に付けてカンマ区切りにします。

パイプのwhile readで変数が消える

cmd | while readはサブシェルで動くため、ループ内の変数がループ後に残りません。while read ... done < <(cmd)やリダイレクトを使います。

入力の前後の空白が消える

IFSの働きで先頭・末尾の空白が除去されます。保持したいならIFS=(空)にします。

よくある質問

Qreadでファイルを1行ずつ処理するには?
Awhile read -r line; do 処理; done < ファイルという形が定番です。ファイルの各行が変数lineに順番に入り、1行ごとに処理できます。-rを付けるのがポイントで、バックスラッシュを含む行も壊さずに読めます。実機でも、各行が順番に処理されることを確認しています。
Qread -rの-rは何のために付けるのですか?
Aバックスラッシュ(\)を特別扱いせず、そのまま読み込むためです。-rを付けないと、readはバックスラッシュをエスケープ文字として解釈し、消したり次の行と連結したりします。実機でも、-rなしでpath\to\filepathtofileに壊れることを確認しています。Windowsのパスや正規表現などを扱うため、read -rを常に使うのが安全です。
Qファイルの最後の行だけ処理されないのはなぜですか?
Aその行の末尾に改行が無いためです。readは改行で1行の区切りを判断するので、改行の無い最後の行を「未完成」として読み落とします。ループ条件をwhile read -r line || [ -n "$line" ]; doとすると、改行の無い最終行も処理できます。実機でも、この対策で最後の行まで読めることを確認しています。
QCSVをreadで1行ずつ読むには?
Awhile IFS=, read -r a b c; do ...; done < ファイルのように、IFS=,readの前に付けます。区切り文字がカンマになり、各フィールドが変数に分割されます。IFS=,をreadと同じ行に書けば、そのreadの間だけ区切りが変わり、他に影響しません。
Qユーザーに入力を求めるプロンプトを出すには?
Aread -p "メッセージ " 変数を使います。メッセージを表示してから入力を待ちます。パスワードのように画面に表示したくない入力は-s(非表示)、一定時間で打ち切りたいときは-t 秒(タイムアウト)を付けます。対話的なスクリプトを作るときの基本です。

まとめ

  • read 変数で1行を読み、複数変数なら空白で分割(余りは最後の変数)。
  • ファイルの1行ずつ処理はwhile read -r 行; do ... done < fileが定番。
  • readはほぼ常に-rを付ける(バックスラッシュを壊さないため)。
  • 最終行に改行が無い対策は|| [ -n "$行" ]
  • IFSで区切りを変更、read -pでプロンプト表示。

readは、シェルスクリプトで入力を扱うための基本コマンドです。「read-r」「最終行対策は|| [ -n "$行" ]」——この2つの罠対策さえ覚えておけば、データを壊さず確実に1行ずつ処理できます。test・[ ]cutと組み合わせて、堅牢なテキスト処理スクリプトを書いてみましょう。