readコマンドの使い方|bashで入力を受け取る方法とオプション11種

コマンドリファレンス

read コマンドは、標準入力から 1行の入力を読み取り、変数に格納 するためのシェル組み込みコマンドです。
対話的にユーザー入力を受け付けたり、スクリプト内で入力値を処理する際に利用されます。

  1. 構文(Syntax)
  2. 主なオプション一覧
  3. 実行例
    1. 基本的な入力の読み取り
    2. プロンプト付きで入力
    3. パスワードを非表示で入力
    4. 5秒以内に入力がなければタイムアウト
    5. 1文字だけ入力を取得
    6. 改行やエスケープをそのまま読み取る
    7. 配列として入力を格納
    8. 区切り文字(:)までを入力として取得
    9. デフォルト値を用意して入力を省略できるようにする
    10. 入力値を検証して、正しくなければ聞き直す
    11. 1文字入力で選ぶメニューを作る
    12. -n と -N の違い(区切り文字を無視するか)
    13. 複数行の入力をまとめて受け取る
    14. 入力を使う前にサニタイズする
    15. 標準入力以外から読む(-u)
    16. エラー例(タイムアウト)
    17. csvファイルの取り扱い
      1. 1) 基本:カンマ区切り(引用・カンマ埋め込みなし想定)
      2. 2) 可変列数を配列で受ける(列数がバラつくCSV向け)
      3. 3) RFC4180 風:ダブルクォート対応(埋め込みカンマ・二重の二重引用符 “” を扱う)
        1. テスト用 sample_quoted.csv
      4. 4) CSVを書き出す(値を安全にエスケープして出力)
      5. 補足(実務Tips)
  4. IFSの役割と読み込み時の注意点
  5. while readループでハマりやすい罠
  6. シェル(bash/sh/zsh/ksh)による read の違い
  7. readarray / mapfile との違い(配列読み込み)
  8. よくある質問(FAQ)
    1. Q. read -r を付けないとどうなる?
    2. Q. read -r を使えばコマンドインジェクションは防げる?
    3. Q. 変数名を省略するとどうなる?
    4. Q. read が入力を待たずにすぐ終了してしまう
    5. Q. shスクリプトでread -pやread -sが使えない
  9. 関連コマンド
  10. 備考
  11. 参考
  12. 用途別のくわしい解説
  13. 関連記事

構文(Syntax)

read [オプション] [変数...]

主なオプション一覧

オプション説明使用例
(なし)入力を1行読み取り、指定した変数に格納read name
-p "PROMPT"入力前にメッセージを表示read -p "Enter your name: " name
-s入力を非表示(パスワード入力向け)read -s password
-t N入力を待つ秒数を指定(タイムアウト)read -t 5 name
-n NN文字だけ読み取るread -n 1 key
-rバックスラッシュをエスケープ文字として扱わないread -r line
-a ARRAY入力を配列に格納read -a items
-d DELIM区切り文字を指定(デフォルトは改行)read -d : input
-N N区切り文字を無視してちょうどN文字読み取るread -N 5 buf
-eReadlineを使って入力(矢印キーでの編集・履歴が有効)read -e path
-i TEXT入力欄にあらかじめTEXTを表示(-eと併用。実質のデフォルト値)read -e -i "yes" ans
-u FD標準入力ではなく指定したファイルディスクリプタから読むread -u 3 line

オプションはread -rp "名前: " name のようにまとめて指定できます。指定できるオプションの正確な一覧は help read で確認できます(bashの組み込みコマンドなので man read では引けません)。

実行例

基本的な入力の読み取り

read name
echo "Hello, $name"

実行例:

Alice   ← ユーザー入力
Hello, Alice

プロンプト付きで入力

read -p "Enter your name: " name
echo "Welcome, $name"

パスワードを非表示で入力

read -s -p "Password: " password
echo
echo "Password entered (hidden)"

5秒以内に入力がなければタイムアウト

read -t 5 -p "Enter your name: " name

1文字だけ入力を取得

read -n 1 -p "Press any key: " key
echo "You pressed '$key'"

改行やエスケープをそのまま読み取る

read -r line

配列として入力を格納

read -a items
echo "First item: ${items[0]}"

入力例:

apple banana cherry

出力例:

First item: apple

区切り文字(:)までを入力として取得

read -d : var <<< "abc:def:ghi"
echo "$var"

出力例:

abc

ここで echo "abc:def:ghi" | read -d : var とパイプで渡してはいけません。パイプの右側はサブシェルで実行されるため、read が代入した var は元のシェルに残らず、後続の echo "$var" は空行を出力します。ヒアストリング(<<<)やリダイレクトを使うのが確実です。

デフォルト値を用意して入力を省略できるようにする

-e(Readlineを使う)と -i(初期テキスト)を組み合わせると、入力欄にあらかじめ既定値が表示され、そのままEnterを押すだけで採用できます。

read -e -i "yes" -p "続行しますか? " ans
echo "ans=[$ans]"

出力例(何も入力せずEnterを押した場合):

続行しますか? yes
ans=[yes]

-e / -i は端末(tty)に接続しているときだけ効きます。パイプやリダイレクトで入力を流し込んだ場合は無視され、流し込んだ値がそのまま入ります。端末かどうかに関係なく既定値を効かせたいなら、変数展開で書くほうが確実です。

read -rp "続行しますか? [yes] " ans
ans=${ans:-yes}     # 空ならyesを採用
echo "ans=[$ans]"

入力値を検証して、正しくなければ聞き直す

対話スクリプトでは、読み取った直後に値を検証して、条件を満たすまでループさせます。

while true; do
  read -rp "年齢を入力してください: " age
  if [[ "$age" =~ ^[0-9]+$ ]] && (( age >= 0 && age <= 120 )); then
    break
  fi
  echo "0〜120の数値で入力してください。" >&2
done
echo "年齢: $age"

[[ ... =~ ... ]] で書式を、(( ... )) で範囲を確認しています。数値かどうかを確かめずに (( age >= 0 )) だけで判定すると、文字列が入ったときに意図しない評価になるため、書式チェックを先に置くのが安全です。

1文字入力で選ぶメニューを作る

-n 1 はEnterを待たずに1文字で確定するので、簡易メニューに向いています。

cat <<'MENU'
1) バックアップを作成
2) 一覧を表示
q) 終了
MENU
read -rsn 1 -p "番号を選択: " key
echo
case "$key" in
  1) echo "バックアップを作成します" ;;
  2) echo "一覧を表示します" ;;
  q) echo "終了します" ;;
  *) echo "不正な選択です: $key" >&2 ;;
esac

-s を併せると押したキーが画面に出ません。選択肢に番号を振らず文字列で受け取りたい場合は、シェル組み込みの select のほうが簡潔に書けます。

-n と -N の違い(区切り文字を無視するか)

どちらも文字数を指定しますが、-n は指定数に達する前に改行が来ればそこで打ち切り、-N は改行を無視してちょうど指定数を読み取ります。

printf 'ab\ncd' | { read -n 5 v; echo "-n 5: [$v]"; }
printf 'ab\ncd' | { read -N 5 v; echo "-N 5: [$v]"; }

出力例:

-n 5: [ab]
-N 5: [ab
cd]

固定長のデータを切り出したいときは -N、行入力を1文字・数文字で確定させたいときは -n を使います。

複数行の入力をまとめて受け取る

read は1行ずつしか読めないため、複数行をまとめたいときはループで蓄積します。IFS= を付けて先頭・末尾の空白を保ったまま読み取るのが定石です。

echo "複数行を入力してください(Ctrl-Dで終了):"
input=""
while IFS= read -r line; do
  input+="$line"$'\n'
done
printf '受け取った内容:\n%s' "$input"

1行ずつ加工する必要がなく、まるごと変数に入れたいだけなら cat のほうが簡潔です。

input=$(cat)     # Ctrl-D まで読み込む
echo "$input"

ただし $(...) は末尾の改行を取り除くため、末尾の空行を保持したい場合はループ版を使ってください。

入力を使う前にサニタイズする

読み取った値をファイル名やユーザー名として使う場合は、想定する文字種以外を落としてから使います。

read -rp "ユーザー名: " username
sanitized=$(printf '%s' "$username" | sed 's/[^a-zA-Z0-9_]//g')
echo "サニタイズ後: [$sanitized]"

実行例:

ユーザー名: al;ice rm -rf
サニタイズ後: [alicermrf]

この方法は「危険な文字を消す」ぶん値そのものが変わってしまうので、変えたくない場合は除去ではなく検証して弾く(前項の検証ループ)ほうが適切です。どちらの場合も、値を eval に渡さない・展開時は必ずクォートする、という原則は変わりません。

標準入力以外から読む(-u)

ループ内で標準入力を別の用途に使いたい場合、-u で読み取り元のファイルディスクリプタを分けられます。

exec 3< /etc/hostname
read -u 3 host
exec 3<&-
echo "host=[$host]"

ファイルを1行ずつ処理しながら、ループの中でユーザーに確認を求めたい場合などは、ファイルをfd 3に割り当てて while read -u 3 line とし、確認用の read は標準入力(端末)から読むと衝突を避けられます。

エラー例(タイムアウト)

read -t 2 name
echo "status=$?"

出力例(2秒間入力がなかった場合):

status=142

タイムアウトすると変数は空のまま、終了ステータスは128より大きい値(SIGALRMの14を足した142)になります。EOF(入力が尽きた)の場合は1が返るため、if read -t 5 name; then ... fi のように終了ステータスを見れば「時間切れ」と「入力あり」を区別できます。

csvファイルの取り扱い

read コマンドを使うことでcsvファイルに記載された情報による処理の繰り返し実行に対応できます。

1) 基本:カンマ区切り(引用・カンマ埋め込みなし想定)

#!/usr/bin/env bash
# basic_csv_read.sh
# 例: ./basic_csv_read.sh sample.csv

set -o errexit -o nounset -o pipefail

csv_file="${1:-sample.csv}"

# ヘッダーを読み飛ばしたいときは tail -n +2 を使う
tail -n +2 "$csv_file" | while IFS=, read -r id name age; do
  # 空行スキップ
  [[ -z "${id}${name}${age}" ]] && continue
  printf 'id=%s, name=%s, age=%s\n' "$id" "$name" "$age"
done
  • 前提:"Alice, Bob" のようなフィールド内カンマ二重引用符が無いCSV向け。

2) 可変列数を配列で受ける(列数がバラつくCSV向け)

#!/usr/bin/env bash
# csv_to_array.sh
# 例: ./csv_to_array.sh sample.csv

set -o errexit -o nounset -o pipefail
csv_file="${1:-sample.csv}"

# ヘッダー飛ばし
{ read -r _header; cat; } < "$csv_file" | \
while IFS=, read -r -a cols; do
  [[ ${#cols[@]} -eq 0 ]] && continue
  echo "列数=${#cols[@]} / 先頭=${cols[0]}"
  # 例: 3列目を安全に参照
  if (( ${#cols[@]} >= 3 )); then
    echo "3列目=${cols[2]}"
  fi
done
  • read -a で配列に展開できます(ただし引用・埋め込みカンマは未対応)。

3) RFC4180 風:ダブルクォート対応(埋め込みカンマ・二重の二重引用符 “” を扱う)

Bash だけで素朴にパースする小さな関数。改行を含むフィールドは未対応ですが、一般的な "a, b","x""y" を処理できます。

#!/usr/bin/env bash
# csv_read_rfcish.sh
# 例: ./csv_read_rfcish.sh sample_quoted.csv

set -o errexit -o nounset -o pipefail

csv_read_line() { # usage: csv_read_line array_name ; reads 1 logical line from stdin
  local __arr_name=$1
  local line
  IFS= read -r line || return 1

  local -a fields=()
  local field="" c next in_quotes=0
  local i=0 len=${#line}

  while (( i < len )); do
    c=${line:i:1}
    if (( in_quotes )); then
      if [[ $c == '"' ]]; then
        next=${line:i+1:1}
        if [[ $next == '"' ]]; then
          field+='"'         # 連続する "" はエスケープされた "
          ((i++))
        else
          in_quotes=0        # クォート終了
        fi
      else
        field+="$c"
      fi
    else
      case "$c" in
        '"') in_quotes=1 ;;  # クォート開始
        ',') fields+=("$field"); field="" ;;
        $'\r') ;;            # CR 無視(Windows 改行対策)
        *) field+="$c" ;;
      esac
    fi
    ((i++))
  done
  fields+=("$field")

  # nameref 経由で呼び出し側の配列に代入(bash 4.3+)
  local -n __ref=$__arr_name
  __ref=("${fields[@]}")
  return 0
}

csv_file="${1:-sample_quoted.csv}"

# ヘッダーを先に1行読む(必要なければ削除)
{
  IFS= read -r header || true
  echo "HEADER: $header"
  while csv_read_line cols; do
    # 空行スキップ
    [[ ${#cols[@]} -eq 1 && -z ${cols[0]} ]] && continue
    printf 'Row:'
    for ((i=0; i<${#cols[@]}; i++)); do
      printf ' [%d]=<%s>' "$i" "${cols[$i]}"
    done
    printf '\n'
  done
} < "$csv_file"
テスト用 sample_quoted.csv
id,name,comment
1,"Alice","Hello, world"
2,"Bob","He said ""Good luck!"" yesterday"
3,"Carol","no quotes"

4) CSVを書き出す(値を安全にエスケープして出力)

#!/usr/bin/env bash
# csv_write.sh
# 例: ./csv_write.sh > out.csv

set -o errexit -o nounset -o pipefail

csv_escape() {
  local s=$1
  s=${s//\"/\"\"}  # " を "" に
  if [[ "$s" == *[,\"$'\n'$'\r']* ]]; then
    printf '"%s"' "$s"
  else
    printf '%s' "$s"
  fi
}

# 見出し
printf '%s,%s,%s\n' "id" "name" "note"

# データ例
id=1;   name='Alice'; note='Hello, world'
printf '%s,%s,%s\n' \
  "$(csv_escape "$id")" \
  "$(csv_escape "$name")" \
  "$(csv_escape "$note")"

id=2;   name='Bob';   note='He said "Good luck!"'
printf '%s,%s,%s\n' \
  "$(csv_escape "$id")" \
  "$(csv_escape "$name")" \
  "$(csv_escape "$note")"

補足(実務Tips)

  • 行末 \r が混ざるCSV(Windows由来)は、読み取り時に read -r しつつ上の実装のように \r を無視するか、事前に dos2unix 等で正規化すると安定します。
  • 完全なCSV互換(改行入りフィールドなど)を厳密にやるなら、mlr, awk(FPAT), python -c 'import csv' などの専用パーサを併用するのが安全です。

IFSの役割と読み込み時の注意点

read は入力を IFS(Internal Field Separator) で区切って変数に格納します。デフォルトのIFSは半角スペース・タブ・改行です。複数の変数を指定した場合、余った分はすべて最後の変数にまとめて入ります。

echo "a b c d" | { read w1 w2 rest; echo "w1=$w1 / w2=$w2 / rest=$rest"; }
# → w1=a / w2=b / rest=c d

CSVのようにカンマ区切りで読みたい場合は、IFS=, のように一時的にIFSを変更します。

IFS=, read -r col1 col2 col3 <<< "a,b,c"
echo "$col1 / $col2 / $col3"
# → a / b / c

while readループでハマりやすい罠

command | while read line; do ... done のようにパイプで while read に渡すと、ループ内がサブシェルで実行されます。そのため、ループ内で変更した変数はループの外から参照できません。

# NG例:countがループ外で0のまま
count=0
seq 1 3 | while read -r n; do
  count=$((count + 1))
done
echo "count=$count"
# → count=0(サブシェル内でしか更新されていない)
# OK例:プロセス置換でサブシェル化を避ける
count=0
while read -r n; do
  count=$((count + 1))
done < <(seq 1 3)
echo "count=$count"
# → count=3

詳しくは サブシェルの落とし穴|変数消失の防止 も参照してください。

シェル(bash/sh/zsh/ksh)による read の違い

read は各シェルの組み込みコマンドですが、オプションの対応状況が異なります。sh(dash)向けにスクリプトを書く場合は特に注意してください。

オプションbashdash(sh)zshksh
-p(プロンプト)×
-s(非表示)×
-t(タイムアウト)×
-a(配列)×△(-A
-n(文字数指定)×

#!/bin/sh で始まるスクリプトが dash で実行される環境(Debian/Ubuntu系)では、-p-a などのbash拡張オプションはエラーになります。移植性を重視するなら read var の基本形とリダイレクト・パイプの組み合わせに留めるのが安全です。

readarray / mapfile との違い(配列読み込み)

read -a は1行分の入力を空白区切りで配列に格納しますが、複数行をまとめて配列化したい場合は mapfilereadarray)を使います。

mapfile -t lines < sample.txt
echo "行数: ${#lines[@]}"
echo "1行目: ${lines[0]}"

1行ずつ処理しつつ変数に安全に格納したいなら while read、ファイル全体を配列として一括保持したいなら mapfile が適しています。詳しくは mapfile – 標準入力を配列に読み込む を参照してください。

よくある質問(FAQ)

Q. read -r を付けないとどうなる?

A. -r を付けないと、入力中のバックスラッシュ(\)がエスケープ文字として解釈され、そのまま消えてしまいます。Windowsのパスを読ませると次のように壊れます。

$ printf 'C:\\Users\\test\n' | { read line; echo "[$line]"; }
[C:Userstest]          ← \ が消えた

$ printf 'C:\\Users\\test\n' | { read -r line; echo "[$line]"; }
[C:\Users\test]        ← 入力どおり

行末が \ だった場合はさらに「次の行へ継続」と解釈され、2行分がつながって1つの値になります。ファイルパスやパスワードなど \ を含みうる入力を扱うなら、常に read -r を使うのが安全です。

Q. read -r を使えばコマンドインジェクションは防げる?

A. 防げません。-r読み取り時にバックスラッシュを解釈しないだけで、読み取った値をどう使うかは別問題です。危険なのは受け取った変数を eval に渡したり、クォートせずに展開したりする側です。

# NG: 入力がそのままコマンドとして実行されてしまう
read -r cmd
eval "$cmd"

# NG: クォートしていないため単語分割・グロブ展開が起きる
read -r file
rm $file

# OK: 必ずダブルクォートで囲み、値は引数として渡す
read -r file
rm -- "$file"

ユーザー入力は「文字列データ」として扱い、変数展開は必ず "$var" とクォートしてください。許可する値が決まっているなら、case でホワイトリスト検証してから使うのが確実です。

Q. 変数名を省略するとどうなる?

A. 読み取った1行がそのまま組み込み変数 REPLY に入ります。

$ echo hello | { read; echo "REPLY=[$REPLY]"; }
REPLY=[hello]

Q. read が入力を待たずにすぐ終了してしまう

A. 標準入力がすでにパイプやリダイレクトで別のデータに置き換わっている、もしくは -t でタイムアウトが短く設定されている可能性があります。対話的に入力させたい場合は、標準入力が端末(/dev/tty)に接続されているか確認してください。

Q. shスクリプトでread -pやread -sが使えない

A. -p/-s/-t/-a などはbash拡張のオプションで、POSIX準拠の sh(dash)では未対応です。#!/bin/sh のスクリプトでは printf でプロンプトを表示してから read var を呼ぶ形に書き換えてください。

関連コマンド

  • echo : 変数やメッセージの出力
  • printf : 書式付きの出力
  • select : シェル組み込み、メニュー形式で選択を受け付け
  • IFS と read -r の安全読込 : 空白・改行耐性のある読み込み方
  • mapfile : 標準入力を配列に読み込む(複数行の一括取得)
  • while / until / for : while readでの実務活用

備考

  • read は多くのシェル(bash, zsh, ksh, dash)において シェル組み込み コマンドです。
  • read で指定した変数は、そのシェルの環境でのみ有効。外部コマンドではありません。
  • パスワード入力のように非表示にする場合は -s を利用しますが、シェルの種類によって挙動が異なる場合があります。

参考

用途別のくわしい解説

このページは read の文法とオプションをまとめた基本ガイドです。次の2つは read の中でも扱いが難しく、単独で解説しています。

Bash玄

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

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

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

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

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

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

Bash玄をフォローする

コメント