Expect の基本と初期設定

筆者は より良く、安全に Expect を使うために初期設定することをおすすめします。

Expect は コンピュータ側からの標準出力をバッファに貯め、その中でパターンマッチを取りながら、標準入力に対しキー入力をしていきます。 あたかも人がやっているかのように動くのです。

このパターンマッチとキー入力がずれてしまうと、アカウントを訊いているのにパスワードを返してしまい、パスワードが丸見えになってしまったり、 意図しないディレクトリのファイルを全て削除してしまったり、 といった 事故がおこります。

そういった事故を防止するためにも しっかり理解して Expect を使って頂きたいと思います。

早速ですが、こちらは FTP を Expect で自動化させた最も初歩のスクリプトの例です。

  1   #!/usr/bin/expect
  2   spawn ftp ftp.server.jp
  3   expect "(ftp.server.jp:accountA): "
  4   send "accountB\r"
  5   expect "Password:"
  6   send "passwordB\r"
  7   expect "ftp> "
  8   interact

注: 上にあげたスクリプトは最も初歩的なもので、何らテクニックを使っていません。このままですとFTPサーバーとの通信が途絶した場合にパスワードが丸見えになってしまう事がありえますので、おすすめは出来ません。
また、スクリプトには暗号化されていないパスワードが書かれているので、ファイルのパーミッションを-rwx------となるようにして下さい。

では、一行ずつ説明していきます。

spawn

spawn とは 卵を産む という意味の英単語ですが、 Expect ではインタラクティブのセッションを Expect の中に産み出します。

Expect は標準入出力を使うので、結果が標準出力に現れないコマンドは扱えません。
例えば リダイレクトで command > file というようなコマンドは扱えません。 また、 xterm のようなターミナルも扱えません。 ということを理解しておいて下さい。

まずは、上の例で ftp を上げましたが、 csh や bash のようなシェルも インタラクティブ セッション ですので、 Expect で扱うことが出来ます。 その場合、環境変数から普段使っているシェルを spawn で起動させることが出来ます。

では、シェルを起動してから ftp を実行させるように作り変えると以下のようになります。

  1   #!/usr/bin/expect
  2   spawn $env(SHELL)
  3   expect "$ "
  4   send "ftp ftp.server.jp\r"
  5   expect "(ftp.server.jp:accountA): "
  6   send "accountB\r"
  7   expect "Password:"
  8   send "passwordB\r"
  9   expect "ftp> "
 10   interact

2行目が変わりました。 これで環境変数の SHELL で設定させているシェルが起動します。 起動したら、シェルはプロンプトを返すハズですので、 3行目は プロンプトを期待する文になっています。

ちなみに、シェルのプロンプトは他の標準出力された文字とマッチしにくいユニークなものが良いですね。 上の例では、 $ とスペース をいれてマッチさせていますが、十分ユニークとは言えそうにありません。

expect

expect 文 これがまさに Expect の肝になるので、説明しきれないほどバリエーションがたくさんあるのですが、ここでは基本的なものだけ説明しておきます。

expect に続いて " " で囲まれた部分の文字列に パターンマッチ すれば次の実行に進む というのが expect 文の基本です。

実はこれまでの例にあげた expect 文は、パターンマッチしたら何を実行する という記述がされていません。 ですので、パターンマッチしても何も実行せずに、次の行に進むという記述になっています。

パターンマッチすれば次の行に進みますので、上手くパターンマッチすれば、あたかもスクリプトが見事に動作しているように見えるのですが、もし、パターンマッチしなかったら、どうなるでしょう?

パターンマッチするまで永遠に待ち続けることはありません。 実際は、設定されていない初期状態では10秒でタイムアウトして次の文に移動します。 となると意図しないコマンド入力がされてしまったり、パスワードが丸見えといった事故が起こります。

それでは事故を防止するために expect 文を修正しましょう。

  1   #!/usr/bin/expect
  2   set timeout 5
  3   spawn $env(SHELL)
  4   expect {
  5           "$ " { }
  6           timeout { exit }
  7   }
  8   send "ftp ftp.server.jp\r"
  9   expect {
 10           "(ftp.server.jp:accountA): " { }
 11           timeout { exit }
 12   }
 13   send "accountB\r"
 14   expect {
 15           "Password:" { }
 16           timeout { exit }
 17   }
 18   send "passwordB\r"
 19   expect {
 20           "ftp> " { }
 21           timeout { exit }
 22   }
 23   interact

expect 文は { } でくくり、分岐を作りました。 そしてパターンマッチとtimeoutで分岐させています。

timeoutのほうは、timeoutのときの実行文が付いていて、exitします。 パターンマッチのほうは、実行文がないので expect 文を抜けて次の文を実行する流れになります。

つまり、expect 文の分岐は先に条件にマッチしたほうを実行するものなので、timeoutまでにパターンマッチすれば次の文に移行し、そうでなければスクリプトは強制終了します。

timeout

上の例に出てきた timeout については、見ての通り Expect の環境変数で set 文で設定します。 上の例では タイムアウトする時間を5秒に設定しています。

timeout はスクリプトの中で何度も設定しても構いません。 また、グローバルの timeout だけでなく、 サブルーチンの中でセットするとローカルな timeout になります。

実行時間が長いコマンドのときは、timeout を長くして、そうでないときは短い時間に設定するといったように使います。

send_slow

Expect は標準入力に対して人に代わってコマンドを送ってくれるのですが、ターミナル(キーボード)によってはバッファが小さいシリアル通信になっています。 人が打ち込む速度なら問題ないのは当然ですが、 Expect が長いコマンドを一気に送ってしまうと受けきれないため、コマンドエラーとなってしまいます。

そこで send_slow という環境変数で、 何文字かに分割、分割した文字数ずつ間隔をおいて送る設定が出来ます。
筆者もこの問題に会い、 send_slow を設定することによって解決した経験があります。 必ず Expect スクリプトの先頭で設定することをおすすめします。

下記は send_slow を加えて修正したスクリプトです。

  1   #!/usr/bin/expect
  2   set send_slow {1 .01}
  3   set timeout 5
  4   spawn $env(SHELL)
  5   expect {
  6           "$ " { }
  7           timeout { exit }
  8   }
  9   send -s -- "ftp ftp.server.jp\r"
 10   expect {
 11           "(ftp.server.jp:accountA): " { }
 12           timeout { exit }
 13   }
 14   send -s -- "accountB\r"
 15   expect {
 16           "Password:" { }
 17           timeout { exit }
 18   }
 19   send -s -- "passwordB\r"
 20   expect {
 21           "ftp> " { }
 22           timeout { exit }
 23   }
 24   interact

send_slow には 2つの引数があります。 1つ目の引数が一度に送る文字数、 2つ目の引数が送る時間間隔です。 上の例では {1 .01} ですので、 1文字ずつ 0.01秒間隔で送ることを意図しています。

send_slow の設定を受け継いで標準入力に送るには send に -s オプションを付けた send -s -- 使います。 " " の中の文字列が標準入力に送られ、 \r はエンターキーを意味します。

interact

interact とは、 Expect から ユーザーに 標準入力を渡すコマンドです。 これで人がキー入力した文字が Expect を介して spawn で産み出されたセッションに送られます。

上にあげた例ですと、 Expect は ftpサーバーへの接続を代行してくれて、接続が成功すると人にその後の入力を任せるという動きになります。

interact は 一時的に人にキー入力を任せるコマンドなので、 必要なキー入力が終われば、 権限を Expect に返さなければなりません。 その方法は 特殊なキー入力で interact を終わるように interact 文で指定します。 ただそれは別のところで説明します。

上の例では、interact が スクリプトの最後にあり、 この先に Expect が実行するコマンドがありません。 この場合、 interact の抜ける特殊なキー入力をしてする必要はなく、 spawn で産み出されたセッションを終了することで interact から抜け出し、Expect スクリプトは終了します。 上の例では ftp が spawn で産み出されたセッションなので、 bye で終了となります。