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行を読み取って変数に格納します。複数の変数名を並べると、空白(スペースやタブ)で区切ってそれぞれに代入されます。
# 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)代入されました。要素が変数より多い場合は、余りがすべて最後の変数にまとめて入る(zにc d e)ことも確認しました。この「最後の変数が残り全部を受け取る」性質は、「先頭のフィールドだけ分けて、残りは1つにまとめたい」ときに便利です。
while readでファイルを1行ずつ処理する
readが最も活躍するのが、while readループです。ファイルやコマンドの出力を1行ずつ確実に処理できます。ログ解析やデータ変換の超定番パターンです。
# ファイルを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)を付けるのが鉄則です。
# 入力: 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 ← 正しく保持される
実機で確認したところ、path\to\fileというバックスラッシュを含む文字列を-rなしのreadで読むと、結果はpathtofile——バックスラッシュがすべて消えてしまいました。-rを付けるとpath\to\fileと正しく保持されました。さらに、行末がバックスラッシュの場合、-rなしのreadは「行継続」とみなして次の行と連結してしまい(first\+改行+secondがfirstsecondになる)、-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 ← 最後の行も正しく処理される
実機で、最終行Cに改行が無いファイルをwhile readで処理すると、AとBは出力されたのにCが出力されませんでした。readは改行を読んで初めて「1行完成」とみなすため、改行の無い最後の行は「未完成」として捨てられてしまうのです。対策は、ループ条件をwhile read -r line || [ -n "$line" ]; doとすることです。これは「readが成功したら(=改行付きの行を読んだら)ループ、失敗しても変数が空でなければ(=改行無しの最後の行が残っていれば)もう一度ループ」という意味です。実機でもこの対策でCまで正しく処理できました。自分で用意したデータや、末尾に改行があるか分からないファイルを扱うときは、この|| [ -n "$line" ]を付けておくと安全です。地味ですが、データの1行が静かに欠落する事故を防ぐ重要なイディオムです。
IFSで区切り文字を変える(CSVを読む)
readが単語を分割する区切り文字は、IFS(Internal Field Separator)という特殊な変数で決まっています。既定は空白(スペース・タブ・改行)ですが、これを変えるとCSV(カンマ区切り)などを直接読めるようになります。
# 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行ずつフィールドに分けて処理できます。cutやawkでも列を扱えますが、「各行を読みながら複数のフィールドを変数に入れて、複雑な処理をする」用途ではwhile IFS=... readが便利です。
read -pでプロンプトを表示する
対話的にユーザーへ入力を促すときは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=(空)にします。
よくある質問
while read -r line; do 処理; done < ファイルという形が定番です。ファイルの各行が変数lineに順番に入り、1行ごとに処理できます。-rを付けるのがポイントで、バックスラッシュを含む行も壊さずに読めます。実機でも、各行が順番に処理されることを確認しています。\)を特別扱いせず、そのまま読み込むためです。-rを付けないと、readはバックスラッシュをエスケープ文字として解釈し、消したり次の行と連結したりします。実機でも、-rなしでpath\to\fileがpathtofileに壊れることを確認しています。Windowsのパスや正規表現などを扱うため、read -rを常に使うのが安全です。readは改行で1行の区切りを判断するので、改行の無い最後の行を「未完成」として読み落とします。ループ条件をwhile read -r line || [ -n "$line" ]; doとすると、改行の無い最終行も処理できます。実機でも、この対策で最後の行まで読めることを確認しています。while IFS=, read -r a b c; do ...; done < ファイルのように、IFS=,をreadの前に付けます。区切り文字がカンマになり、各フィールドが変数に分割されます。IFS=,をreadと同じ行に書けば、そのreadの間だけ区切りが変わり、他に影響しません。read -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と組み合わせて、堅牢なテキスト処理スクリプトを書いてみましょう。
- grep(文字列検索)
- find(ファイル検索)
- sed(置換・行編集)
- awk(列抽出・集計)
- cut(列の切り出し)
- chmod(パーミッション)
- tar(圧縮・解凍)
- リダイレクトとパイプ
- echo・printf(文字列出力)
- ps・kill(プロセス管理)
- 環境変数とPATH
- which・type(コマンドの正体)
- ln(シンボリックリンク)
- cp・mv・rm(コピー/移動/削除)
- touch・mkdir(ファイル/フォルダ作成)
- basename・dirname(パス分解)
- history(コマンド履歴)
- curl(HTTP通信・API)
- ping(疎通確認)
- dig・nslookup(DNS調査)
- jq(JSON処理)
- vim(基本操作・終了方法)
- cron・crontab(定期実行)
- test・[ ](条件判定)
- read(入力の読み取り)
- date(日付・時刻)
- sort・uniq・wc(並べ替え/集計)
- xargs(一括処理)
- chown・chgrp(所有者/グループ変更)
- diff(ファイル比較)
- du・df(ディスク使用量)
- head・tail(先頭/末尾・ログ監視)
- cat・less(ファイル表示)
- alias(コマンドの別名)
- nohup(SSH切断対策)
- tmux(セッション管理)
- よく使うコマンドまとめ
- cd(ディレクトリ移動)

