read コマンドの使い方 - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

2013-12-14(Sat) [sh][shell] [更新履歴]

拡張 POSIX シェルスクリプト Advent Calendar 2013、13日目の記事です。

今日のネタは組込みコマンド read の解説と使い方を紹介します。

read コマンドとは?

read の基本仕様・動作は次の通りです。

代表的なオプションは次の通りです。

以下に使い方の例を示します。 (ksh の場合、組込みの echo コマンドの動作が環境に依存して異なるため、 echo -E 〜 の代わりに print -r 〜 を用いてください)

単純な入力と受け取り

特に難しくはないですね。

$ read i1 i2 <<<'foo bar'; echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo] i2=[bar]

連続した区切り文字はまとめて一つの区切りとして扱われます。

$ read i1 i2 <<<'foo   bar'; echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo] i2=[bar]

read はユーザーの確認待ちにも利用されることがあります。

#!/bin/sh

echo '◯◯処理を開始します。Enter キーで継続します。(中断は Ctrl+C)'
read

## …何かの処理…

echo '◯◯処理が完了しました。'
exit 0
#!/bin/sh

echo '◯◯処理を開始しますか? (yes or no) '
read answer
case "$answer" in
yes)
  : OK
  ;;
*)
  echo '中断します。'
  exit 1
  ;;
esac

## …何かの処理…

echo '◯◯処理が完了しました。'
exit 0

入力の語数 vs. 指定された変数の数

入力の語数のほうが少ない場合、語が足りない分の変数は空になります。

$ i1=xxxx; i2=yyyyy
$ read i1 i2 <<<''; echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[] i2=[]
$ i1=xxxx; i2=yyyyy
$ read i1 i2 <<<'foo'; echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo] i2=[]

入力の語数のほうが多い場合、残りの語が最後の変数にまとめて入ります。 残りの語の直前と以降の区切り文字は維持されます。

$ echo foo bar baz
foo bar baz
$ read i1 i2 < <(echo foo bar baz); echo "i1=[$i1] i2=[$i2]"
i1=[foo] i2=[bar baz]
$ echo 'foo   bar     baz'
foo   bar     baz
$ read i1 i2 < <(echo 'foo   bar     baz'); echo "i1=[$i1] i2=[$i2]"
i1=[foo] i2=[bar     baz]

EOF

入力がなければ変数は空になり、終了コードは 1 になります。

$ read i1 i2 </dev/null; echo -E "status=$? i1=[$i1] i2=[$i2]"
status=1 i1=[] i2=[]

入力はあるが改行コードなしに EOF になった場合、 変数は設定され、終了コードは 1 はなります。

$ read i1 i2 < <(echo -n 'foo'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=1 i1=[foo] i2=[]

このため、次の例のように while と組み合せて使用したときに、 最後の入力の処理が抜け落ちてしまう可能性があります。 行指向の入力を扱う場合は問題ありませんが、 そうでない入力が想定される場合は注意が必要です。

#!/bin/sh

while read foo bar; do
  ## …何かの処理…
done

区切り文字のエスケープ

区切り文字はバックスラッシュ \ でエスケープすることができます。 エスケープされた区切り文字はそのまま変数に設定されます。(\ はなくなる)

$ echo -E 'foo\ bar'
foo\ bar
$ read i1 i2 < <(echo 'foo\ bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo bar] i2=[]
$ echo -E 'foo\   bar'
foo\   bar
$ read i1 i2 < <(echo -E 'foo\   bar\n'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo ] i2=[bar]
$ echo -E 'foo \  bar'
foo \  bar
$ read i1 i2 < <(echo -E 'foo \  bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo] i2=[  bar]

行末のバックスラッシュ \ は入力行の区切りをエスケープします。 このときは \ だけでなく改行文字もなくなり、次の行が連結されます。

$ echo -E 'foo bar\'; echo -E 'baz'
foo bar\
baz
$ read i1 i2 < <(echo -E 'foo bar\'; echo -E 'baz'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo] i2=[barbaz]

エスケープの抑制

入力中のバックスラッシュ \ のエスケープ効果を抑制してそのまま変数に受け取るには -r オプションを指定します。このとき、区切り文字のエスケープはできなくなります。

$ echo -E 'foo\ bar'
foo\ bar
read -r i1 i2 < <(echo -E 'foo\ bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo\] i2=[bar]
$ echo -E 'foo\   bar'
foo\   bar
$ read -r i1 i2 < <(echo -E 'foo\   bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo\] i2=[bar]
$ echo -E 'foo\\  bar'
foo\\  bar
$ read -r i1 i2 < <(echo -E 'foo\\  bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo\\] i2=[bar]

区切り文字の指定

シェル変数 $IFS に含まれる文字が区切り文字として利用されます。

$ echo -E 'foo : bar'
foo : bar
$ read i1 i2 < <(echo -E 'foo : bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo] i2=[: bar]
$ IFS=: read i1 i2 < <(echo -E 'foo : bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo ] i2=[ bar]
$ IFS=': ' read i1 i2 < <(echo -E 'foo : bar'); echo -E "status=$? i1=[$i1] i2=[$i2]"
status=0 i1=[foo] i2=[bar]

ただし、改行文字は区切り文字には利用できません。 改行文字はデフォルトの $IFS に含まれていますが、無視されます。

: 区切りのテキストデータである passwd(5), group(5) を処理する例:

$ getent passwd |while IFS=: read name x uid x; do
  echo "name=$name, uid=$uid"
done
$ getent passwd |while read -d : name x uid x; do
  echo "name=$name, uid=$uid"
done

入力をそのまま受け取る

入力をそのまま変数に受け取りたいとき、-r オプションでエスケープを抑制して変数を一つだけ指定すれば済むと思うかもしれませんが、 それだけでは不完全です。 次の例のように行の先頭と末尾の区切り文字が失なわれます。

$ echo -E '    foo \ \\  bar  \'
    foo \ \\  bar  \
$ read -r line < <(echo -E '    foo \ \\  bar  \'); echo "status=$? line=[$line]"
status=0 line=[foo \ \  bar  \]
$ echo -E 'foo  bar    '
foo  bar
$ read -r line < <(echo -E 'foo  bar    '); echo "status=$? line=[$line]"
status=0 line=[foo  bar]

さらに $IFS を空にする必要があります。

$ echo -E '    foo \ \\  bar  \'
    foo \ \\  bar  \
$ IFS= read -r line < <(echo -E '    foo \ \\  bar  \'); echo "status=$? line=[$line]"
status=0 line=[    foo \ \  bar  \]
$ echo -E 'foo  bar    '
foo  bar    
$ IFS= read -r line < <(echo -E 'foo  bar    '); echo "status=$? line=[$line]"
status=0 line=[foo  bar    ]

タイムアウトの判定

入力待ちをタイムアウトするには -t オプションに待つ秒数を指定します。 -t 0.5 のように小数点以下の精度も指定可能です。 残念ながら、ksh 88 の read-t オプションがありません。

#!/bin/sh

echo '◯◯処理を開始します。10秒経過あるいは Enter キーで継続します。(中断は Ctrl+C)'
read -t 10
## …何かの処理…

タイムアウトが発生したときの挙動はシェルの種類とバージョンに依存します。

bash 4 以降と ksh, zsh であれば、次のようにタイムアウト時の判定ができます。

read -t 60 input
if [[ $? -gt 128 ]] || [[ -z ${input+set} ]]; then
  ## タイムアウト時の処理
fi

ところで、12月25日はクリスマスな上に、 OSS 界隈で地味に活躍されているふみやすさんの誕生日ですね!
http://www.amazon.co.jp/registry/wishlist/27M7TV8CEEF6G?sort=priority

逆に、あなたの書いた OSS や Blog や Advent Calendar が気に入ったら何か送りたく なってしまうかもしれないので、プロフィールや Web サイトに あなたの Amazon 欲しいものリストの URL を貼っておいてくださいね!


私が勤める OSSTech っていう某弊社で社員募集しているようです。 人材紹介会社を介さなければ、入社後に 20万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


よろしければ、これまで参加した/参加予定のほかの Advent Calendar もどうぞ。