理系学生日記

おまえはいつまで学生気分なのか

Perl で Expect

対話的なプログラムを自動化するのに expect があるという話を前に同僚から聞いていましたが、Perl でやるとどうなるんだろうとか思ってると、そのまま Expect っていうモジュールがあった。
インタラクティブなコマンドのクソ面白くない例としてパスワード変更がありますが、それを Expect.pm で書くとたぶんこんな感じになるんだと思います。

#!/usr/bin/perl
use strict;
use warnings;
use Expect;

my $cmd = 'passwd';
my $oldpass = 'aaaa';
my $newpass = 'bbbb';

my $expect = Expect->new;
$expect->log_stdout(0);                 # hide stdout of 'passwd'
$expect->log_file('./passwd.log', 'w'); # logging 
$expect->spawn( $cmd ) or die "cannot spawn '$cmd'";
$expect->expect( 
    undef,
    [ 'Old Password:'        => sub { shift->send( "$oldpass\n" );
                                      exp_continue;
                                } ],
    [ 'New Password:'        => sub { shift->send( "$newpass\n" );
                                      exp_continue;
                                } ],
    [ 'Retype New Password:' => sub { shift->send( "$newpass\n" ); } ],
    [ eof                    => sub {} ],
    );

expect に渡している ARRAYREF は、expect コマンドより多少直感的なように感じます。ちなみにこの ARRAYREF でキーとして使っているもの ('Old Password:' など)は、qr// の正規表現で代替できます。
# 上記のようにリテラルの形で指定しても、内部で正規表現として評価されているっぽい。

これ系のプログラムってデバッグが基本的に大変になりますが、Expect には exp_internal っていうメソッドが用意されていて、これを呼び出すことで Expect が内部で何をしているかを簡単に確認することができます。
実際に、上記の例で $expect->exp_internal(1) とすると、以下のような出力が確認できます。

Changing password for kiririmode.

spawn id(3): Does `Changing password for kiririmode.\r\n'
match:
  pattern #1: -re `Old Password:'? No.
  pattern #2: -re `New Password:'? No.
  pattern #3: -re `Retype New Password:'? No.
  pattern #4: -eof `'? No.

ここでは、"Changing password for kiririmode." という文字列に対して何もヒットしてません。

Old Password:
spawn id(3): Does `Changing password for kiririmode.\r\nOld Password:'
match:
  pattern #1: -re `Old Password:'? YES!!
    Before match string: `Changing password for kiririmode.\r\n'
    Match string: `Old Password:'
    After match string: `'
    Matchlist: ()
Calling hook CODE(0x1008274c0)...
Sending 'aaaa\n' to spawn id(3)
 at /System/Library/Perl/Extras/5.10.0/Expect.pm line 1264
	Expect::print('Expect=GLOB(0x10047bf10)', 'aaaa\x{a}') called at ./expect.pl line 17
	main::__ANON__('Expect=GLOB(0x10047bf10)') called at /System/Library/Perl/Extras/5.10.0/Expect.pm line 760
	Expect::_multi_expect(undef, undef, 'ARRAY(0x100937c90)') called at /System/Library/Perl/Extras/5.10.0/Expect.pm line 565
	Expect::expect('Expect=GLOB(0x10047bf10)', undef, 'ARRAY(0x100937470)', 'ARRAY(0x1009375c0)', 'ARRAY(0x100820448)', 'ARRAY(0x100827c40)') called at ./expect.pl line 24
Continuing expect, restarting timeout...

ここでは、"Old Password:" という文字列にヒットしているので、"aaaa\n" が passwd に渡されていることが確認できます。