シェルスクリプト(とよくあるコマンド)でexpectのように自動応答させる

標準入力から応答していくタイプのプログラムがよくあります。

9/19 追記

改行が出力されなくても上手く読み出せる方法があるっぽいのでそれを追記

hello.sh
#!/bin/sh

echo "what's your name?"
echo "input your name:"

read name

echo "age ?:"

read age

echo "hi, $name($age)! nice to meet you!!"

$ sh hello.sh
what's your name?
input your name:
ryozi <= 入力
age ?:
24 <= 入力
hi, ryozi(24)! nice to meet you!!

これを自動化したい!と思ったら、迷わずexpectを使うわけですが、
expectがなく、シェルスクリプトしかない環境で同等なことをやれ!と言われて、
何か間違ってると思いながら、無い頭をひねって考えた結果が以下。(シェルはbash)



こんな感じに、ある入力に対してある出力をするようなコードを書きます。

expect.sh
#!/bin/sh

while read INPUT; do
        echo "INPUT:$INPUT" 1>&2 # デバッグ出力のため

        case $INPUT in
                *"input your name"*)
                        echo "ryozi"
                        ;;
                *"age ?"*)
                        echo "24"
                        ;;
        esac
done

# tailf時の終了のための空出力
echo

$ touch temp
$ chmod 600 temp
$ tailf temp | sh hello.sh | sh expect.sh > temp
INPUT:what's your name?
INPUT:input your name:
INPUT:age ?:
INPUT:hi, ryozi(24)! nice to meet you!!
$ rm temp

標準入力は目に見えないので変な感じですが、いい感じに動いています。
一時ファイルを使うので後始末を忘れずに。


また、名前付きパイプを知ったので、これをどうにか使ってみたのがこちら。
一時ファイルにデータが残らないので衛生的かもしれない。tailfではなく、catを使ってます。

$ mkfifo --mode=600 temp
$ cat temp | sh hello.sh | sh expect.sh > temp
INPUT:what's your name?
INPUT:input your name:
INPUT:age ?:
INPUT:hi, ryozi(24)! nice to meet you!!
$ rm temp


CentOSなら大体標準で入ってるだろう機能だけでできそうですね...

欠点

1行入力は改行文字も含んでいるので、このままcaseでマッチさせるときは*を後ろにつけて前方一致するようにしたりする必要があったり。


上記の例では、入力要求時に一々改行を入れてますが、
見栄え重視な対話プログラムの場合、実際の入力は以下のようになるでしょう。

what's your name?
input your name:ryozi <= 入力
age ?:24 <= 入力
hi, ryozi(24)! nice to meet you!!

こんな感じに、標準入力を求める前の出力で改行が無い場合が多いです。
改行文字が出力されないと、パイプで渡せないっぽいので注意が必要です。(readの問題かもしれないけど)
こういうケースでは、上記のexpect.shでは応答できません。その前の1行で判断するなり対応が必要です。



expectコマンドはこの問題は気にせずに、かつ、きれいに書けますね。

expect << EOF
set timeout 10

spawn sh hello.sh

expect "input your name:"
send "ryozi\n"

expect "age ?:"
send "24\n"

expect eof
EOF

spawn sh hello.sh
what's your name?
input your name:ryozi
age ?:24
hi, ryozi(24)! nice to meet you!!

出力もきれい。なんで使っちゃだめなんや...

9/19 追記

readで指定文字づつ読む -n オプションをつけることで、対応できるかもしれません。
readコマンドはシェルに組み込まれてるコマンドらしいので、もしかしたらうまくいかないかもしれません。

#!/bin/sh

# 空白を読み飛ばさないようにするため、IFSをsetしなおす
IFS=""

# バッファ準備
MESSAGE=

# 期待するメッセージに対するレスポンスを定義(後方一致がおすすめ)
# 基本的に、期待するメッセージを処理できたら0、処理できない場合は1を返す。
expect_message(){
    MESSAGE=$1
    # ここでメッセージ処理
    case $MESSAGE in
        *"input your name")
            echo "ryozi"
            return 0
            ;;
        *"age ?")
            echo "24"
            return 0
            ;;
    esac

    return 1
}

# 読み込み
while read -n 1 CHARACTER; do
    # 読めなかったら、たぶん改行か何かなので、バッファをクリアする
    if [ "$CHARACTER" = "" ]; then
        MESSAGE=
        continue
    fi

    MESSAGE="$MESSAGE$CHARACTER"

    # 処理できたらバッファをクリア
    expect_message $MESSAGE && MESSAGE=
done

# tailf用
echo

上記のexpect.shで対応できなかったものも、対応できるようになります。