Bash 入門

Bash read コマンド

Bashスクリプトにおいて、read コマンドはユーザー(標準入力)またはファイルから入力を読み取り、その内容を1つまたは複数の変数(Variable)に代入するために使用されます。このコマンドがあることで、スクリプトは真の「インタラクティブ(Interactive)」な性質を持つことができます。

前章で学んだ $1$@ などのコマンドライン引数や、あらかじめ設定されたハードコードな値だけに頼るのではなく、read コマンドを使用すれば、スクリプトの実行中にユーザーへ問いかけ、必要な重要情報を能動的に取得することが可能になります。

1. readによる基本のユーザー入力取得

read の最も直接的な使い方は、ユーザーに入力を促すメッセージを表示し、それを変数に格納することです。引数なしで read を実行すると、標準入力から1行すべてを読み取り、REPLY という名前の特殊な内部変数に自動的に代入します。しかし、より一般的で推奨される方法は、明示的に変数名を指定することです。

#!/bin/bash
echo "名前を入力してください:"
read user_name
echo "こんにちは、 $user_name さん!このスクリプトへようこそ。"

この例では以下の処理が行われています:

  • echo "名前を入力してください:":ユーザーに何を力すべきかを示すプロンプトを表示します。
  • read user_name:スクリプトの実行を一時停止します。ユーザーがキーボードで文字を入力し、エンターキー(Enter)を押すのを待機します。エンターを押すまでに入力された内容はすべて user_name 変数に保存されます。
  • echo "こんにちは、 $user_name さん! ...":ユーザーが入力した値を呼び出します。

1.1 -p オプションでプロンプトを直接出力

プロンプトを表示するためにわざわざ echo コマンドを書く代わりに、read には非常に便利な -p(Prompt)オプションがあります。これにより、プロンプト文字列と read コマンドを1行で記述でき、コードがよりクリーンでコンパクトになります。

#!/bin/bash
read -p "あなたの好きな色は何ですか? " fav_color
echo "あなたの好きな色は $fav_color です。"

ポイント: プロンプト文字列 "あなたの好きな色は何ですか? " の末尾にあるスペースに注目してください。このスペースを入れることで、ターミナル上でプロンプトとユーザーの入力カーソル位置に適度な距離ができ、ユーザーエクスペリエンスと可読性が大幅に向上します。

2. 複数の変数への読み込み

read コマンドは、1行の入力テキストを「分割」して複数の異なる変数に代入するのも得意です。デフォルトでは、read は空白文字(スペース、タブ、改行)を区切り文字として入力を分割し、指定された変数に順番に割り当てます。

すべての変数に値が割り当てられた後、さらに入力に単語が残っている場合、残りのすべての単語が最後の変数にまとめて格納されます。

#!/bin/bash
# 例 1: 2つの単語を読み込む
read -p "名と姓を入力してください (スペース区切り): " first_name last_name
echo "名は: $first_name"
echo "姓は: $last_name"

# 例 2: 3つの数字を読み込む
read -p "3つの数字を入力してください: " num1 num2 num3
echo "1番目の数字: $num1"
echo "2番目の数字: $num2"
echo "3番目の数字: $num3"

# 例 3: 「最後の変数」が残りをすべて受け取る特性を確認
read -p "詳細な住所を入力してください (番地, 市, 州, 郵便番号): " street city state zip
echo "番地: $street"
echo "市: $city"
echo "州: $state"
echo "郵便番号(および残りの内容): $zip"

例3において、ユーザーが "123 Main St Anytown CA 90210" と入力した場合、street"123"city"Main"state"St" となり、zip には残りの "Anytown CA 90210" がすべて入ることになります。

ベストプラクティス: 住所のようにスペースを含む複雑なデータの場合、各パーツ(番地、市など)を個別に問いかけるか、あるいは1つの変数に1行すべてを読み込んでから、後述する awksed などのツールを使用して精密にパース(解析)することをお勧めします。

3. readコマンドの高度なオプション

read コマンドには、動作を精密に制御するための強力なオプションがいくつか備わっています。これにより、スクリプトのインタラクティブな体験とセキュリティが大幅に強化されます。

3.1 サイレントモード (-s) で入力を隠す

パスワードや APIキー などの機密情報を扱う際、ユーザーが入力した文字を画面にそのまま表示してはいけません。-s(Silent)オプションを使用すると、入力内容を表示させない「ブラインド入力」を実現し、第三者による覗き見(Shoulder-surfing)を防止できます。

#!/bin/bash
read -sp "パスワードを入力してください: " user_password
echo # サイレント入力後に手動で改行を出力し、ターミナルの表示崩れを防ぐ
echo "パスワードを安全に受信しました (デモ用出力): $user_password"

ユーザーがパスワードを入力してエンターを押しても、画面にはアスタリスクなどの文字すら表示されません。read -sp の直後にある引数なしの echo コマンドは非常に重要です。なぜなら -s モードではユーザーのエンター操作による改行すら画面に反映されないため、この echo がないと次の行の出力がプロンプトと同じ行に繋がってしまうからです。

3.2 入力タイムアウトの設定 (-t)

時には、スクリプトが無期限に待機し続けるわけにはいかない場合があります。ユーザーが一定時間内に反応しない場合、デフォルトのロジックで続行する必要があります。-t(Timeout)オプションを使用すると、秒単位でカウントダウンを設定できます。タイムアウトが発生した場合、read コマンドは非ゼロの終了ステータスコード(失敗を意味する)を返します。

#!/bin/bash
echo "5秒以内に判断してください..."

# 5秒待機
read -t 5 -p "続行しますか? (yes/no): " choice

# read の終了ステータスコードを確認
if [ $? -eq 0 ]; then
    if [ "$choice" == "yes" ]; then
        echo "実行を継続します..."
    else
        echo "操作を取り消しました。"
    fi
else
    # ステータスコードが非ゼロ=タイムアウト発生
    echo -e "\nタイムアウトしました。デフォルトの 'no' ロジックを実行します。"
fi

ここで、前章で学んだ特殊変数 $? を活用しています。ステータスコード 0 は制限時間内に正常に入力を受信したことを意味し、非ゼロはタイムアウトを意味します。

3.3 入力文字数の制限 (-n)

-n(Number of characters)オプションは、read が自動的に読み込みを終了して戻るまでの最大文字数を指定します。これは Y/N の確認プロンプトのような単一文字入力を処理する際に最適です。指定した文字数に達した瞬間にスクリプトが続行されるため、ユーザーはエンターキーを押す手間が省けます。

#!/bin/bash
read -n 1 -p "同意する場合は 'y' を、拒否する場合は 'n' を押してください: " answer
echo # 入力後に手動で改行して表示を整える

if [ "$answer" == "y" ] || [ "$answer" == "Y" ]; then
    echo "同意を選択しました。"
elif [ "$answer" == "n" ] || [ "$answer" == "N" ]; then
    echo "拒否を選択しました。"
else
    echo "無効な入力です。"
fi

3.4 特定のファイル記述子からの読み込み (-u)

read は通常、キーボード(標準入力ファイル記述子 0)から読み取りますが、-u オプションを使用すると、指定した任意のファイル記述子からデータを読み取ることができます。スクリプト内でユーザー入力とファイルストリームを同時に処理する必要がある場合に非常に有効な高度なテクニックです。

#!/bin/bash
# 1. デモ用のテンポラリファイルを作成
echo "ファイル内の1行目" > temp_input.txt
echo "ファイル内の2行目" >> temp_input.txt

# 2. ファイルをカスタムの「ファイル記述子 3」にバインドして読み込み用に開く
exec 3< temp_input.txt

# 3. -u オプションでファイル記述子 3 から1行ずつ読み込む
read -u 3 line_from_file1
read -u 3 line_from_file2

echo "読み込んだ1行目: $line_from_file1"
echo "読み込んだ2行目: $line_from_file2"

# 4. ファイル記述子 3 を閉じ、リソースを解放する
exec 3<&-
rm temp_input.txt

3.5 デフォルト値のスマートな設定方法

read コマンド自体には「デフォルト値」を設定する直接的なパラメータはありません。しかし、業界標準とも言えるエレガントな手法があります。それは、Bash の「パラメータ展開(Parameter Expansion)」を利用して、ユーザーがそのままエンターを押した(入力が空の)場合にデフォルト値を割り当てる方法です。

#!/bin/bash
# プロンプト内の [ ] でデフォルト値を示すのが UX の慣例
read -p "好きなOSを入力してください [Linux]: " os_choice

# os_choice が空(未設定または空文字列)の場合、"Linux" を代入
os_choice=${os_choice:-Linux}

echo "あなたの好きなOSは: $os_choice です。"

構文 ${variable:-default_value} は、「もし variable が空なら default_value を使い、そうでなければ variable の実際の値を使う」という意味です。この方法はインタラクティブなスクリプトにおける「寛容性」を大幅に高めます。

4. 実戦ケーススタディ

4.1 システム管理実務:対話型ログクリーナー

システム管理スクリプトの例として、ディレクトリを機械的に削除するのではなく、管理者が実行前に確認できるインタラクティブなスクリプトへ改造してみましょう。

#!/bin/bash
echo "--- 対話型ログクリーニング・ユーティリティ ---"

# 1. 対象ディレクトリの取得とデフォルト値の設定
read -p "クリーニング対象のログディレクトリを入力してください [/var/log]: " log_dir
log_dir=${log_dir:-/var/log}

# 2. ディレクトリの妥当性チェック
if [ ! -d "$log_dir" ]; then
    echo "エラー:ディレクトリ '$log_dir' は存在しません。"
    exit 1
fi

echo " '$log_dir' 内で7日以上前の古いログファイルを検索中..."

# 安全のためシミュレーション表示のみ。rm コマンドはコメントアウトしています。
# find コマンドで7日前の .log ファイルを探し、逐次処理
find "$log_dir" -type f -name "*.log" -mtime +7 -print0 | while IFS= read -r -d $'\0' file_to_delete; do
    echo "古いファイルを発見: $file_to_delete"
    
    # 3. ファイルごとに確認を要求 (1文字制限)
    read -n 1 -p "このファイルを削除しますか? (y/N): " confirm_delete
    echo
    
    if [ "$confirm_delete" == "y" ] || [ "$confirm_delete" == "Y" ]; then
        echo "  --> $file_to_delete を削除中..."
        # rm "$file_to_delete" # 本番環境ではこの行を有効化して削除を実行
        echo "  --> (シミュレーション:削除成功)"
    else
        echo "  --> $file_to_delete をスキップしました。"
    fi
done

echo "クリーニングウィザードが終了しました。"

このケースは、デフォルト値の設定、ディレクトリのバリデーション、-n 1 による高頻度な確認を組み合わせた、システム運用におけるクラシックな防御的プログラミングのパターンです。

4.2 応用:テキストアドベンチャーゲーム

Bash でシンプルなテキストアドベンチャーゲームを作ると想像してみてください。read はプレイヤーの行動を制御するためのコアエンジンになります。

#!/bin/bash
echo "ホラー洋館アドベンチャーへようこそ!"
echo "あなたは不気味な洋館の大きな門の前に立っています。"

player_name=""
# 名前が入力されるまでループ
while [ -z "$player_name" ]; do
    read -p "勇者よ、名前を教えてくれ: " player_name
    if [ -z "$player_name" ]; then
        echo "名乗らぬ者に道は開けぬ。名前を入力せよ!"
    fi
done

echo "よし、$player_name。君には '入る(enter)' か '逃げる(flee)' かの選択肢がある。"

choice=""
# 有効な選択肢が入力されるまでバリデーションループ
while [ "$choice" != "enter" ] && [ "$choice" != "flee" ]; do
    read -p "どうする? " choice
    case "$choice" in
        enter)
            echo "君は勇気を振り絞り、重い樫の扉を開けて中に入った。"
            echo "冷たい風が吹き抜け、背筋が凍るような感覚に襲われる。"
            ;;
        flee)
            echo "君は命が惜しくなり、全速力で逃げ出した。"
            echo "ゲームオーバー。少なくとも君は生き延びた。"
            exit 0
            ;;
        *)
            echo "無効なコマンドだ。'enter' または 'flee' と入力してくれ。"
            ;;
    esac
done

echo "つづく..."

このゲームスクリプトは、readwhileループを組み合わせて厳格な入力バリデーションを行う方法を示しています。ユーザーが不適切な入力をしても、正しいコマンドが入力されるまでループがそれをブロックし続けます。