Unengineered Weblog

PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND

シェルスクリプトでpickを作った(シェルスクリプトのwhile read...の中で対話的なreadをする方法)

Brian W. KernighanとRob Pikeの「Unixプログラミング環境」にはpickと呼ばれるコマンドが紹介されている。 これは対話的にプログラムを処理するためにシェルスクリプトで書かれたツールである。pick args...とすると、引数を一度に1つずつ表示しユーザーの入力を待つ。pickはユーザーがyを入力した引数のみ出力し、それ以外は捨てる。

$ ls
file1 file2 file3 file4
$ rm $(pick *)
file1? y
file2?
file3? q
$ ls
file2 file3 file4 # file1 has been removed!

その書籍の後半では、引数のかわりに標準出力を使うオプション-を追加するためにCで書き直している(今回私が作成したpickでは-iオプションにした)。当時のシェルでは実現しづらかったからである。 しかしながら、現代のシェル(POSIX shellでも)ではCを用いずに完全版pickを実装できる。それは僕が疑問であった「シェルスクリプトのwhile read...の中で対話的なreadをする方法」の回答でもある。(ネットであまりシンプルな回答を見つけることができなかった。)

github.com

では早速pickのメインルーチンを見てみよう。

if [ "$iflag" = true ]
then
    cat -- ${1+"$@"}
else
    for i
    do
        echo "$i"
    done
fi |
while read -r line
do
    printf '%s ?' "$line" > /dev/tty
    read -r a < /dev/tty # On old Unix, it did not work
    case "$a" in
    y*) printf '%s\n' "$line";;
    q*) exit 2;;
    esac
done

コメントのところに注目してほしい。「Unixプログラミング環境」にはread var < filenameはできないと書いてある。しかしながら現代のshellではこのプログラムは正常に作動する(dash, bash, kshで確認した)。ここで、read -r a < /dev/ttyは明示的に/dev/ttyを指定しなければならない。標準入力は一行目のwhile read -r lineが読んでいるからだ。

表題の答えは

whlie read ...の中で対話的なreadをする方法」はread var < /dev/ttyが答えとなる。ちなみに「確実に」対話的につかうプログラムを除いて、基本的に/dev/ttyを使うべきでない(ed(1)ですら使っていない)。出入力を固定すると他のプログラムと組み合わせることが不可能になるからである。ファイル名を指定してデータを入出力する方法も同様である。基本的にはstdin, stdout, stderrを使ってデータを入出力し、パイプを通して他のプログラムと通信できるように設計すべきだ。