Perlで対話型コマンドの実行

前回TeraTermマクロで対話型コマンドの実行をがんばった。けど扱いづらい。Perl使いたい。というわけで、telnetコマンドをfork/execしてpipeで標準入出力を操作してみた。*1 *2

まず、ファイルハンドルから値を読み込む関数と書き出す関数。読み出しでは応答が1秒間無かったら入力待ちと判断してる。*3

use strict;
use warnings;
use IO::Select;

sub read_all {
  my ($stdout_r) = @_;
  my $read_line;
  my $s = IO::Select->new($stdout_r);
  while (1) {
    my @ready = $s->can_read(1);
    last if (@ready == 0);
    sysread($stdout_r, my $in, 1);
    $read_line .= $in;
  }
  return $read_line;
}

sub writeln {
  my ($stdin_w, $write_line) = @_;
  syswrite($stdin_w, "$write_line\n", length("$write_line\n"));
}

次がtelnetをfork/execするtelnet関数。ログイン処理と終了処理も行う。*4 *5

sub telnet {
  my ($user, $pass, $sub) = @_;
  pipe(my ($stdin_r,  $stdin_w ));
  pipe(my ($stdout_r, $stdout_w));

  if (my $pid = fork()) { # parent
    close($stdin_r); close($stdout_w);

    print read_all($stdout_r);
    writeln($stdin_w, $user);
    print read_all($stdout_r);
    writeln($stdin_w, $pass);
    print read_all($stdout_r);

    $sub->($stdin_w, $stdout_r);

    writeln($stdin_w, "exit");
    print "\n";
    kill 9, $pid;

    close($stdin_w); close($stdout_r);
    wait();
  }
  elsif (defined $pid) { #child
    close($stdin_w); close($stdout_r);
    close(STDIN); close(STDOUT);

    open STDIN,  '<&', $stdin_r;
    open STDOUT, '>&', $stdout_w;

    close($stdin_r); close($stdout_w);

    exec("telnet localhost");
    exit 1;
  }
  else {
    die 'fork err';
  }
}

最後にtelnet関数を使ってlsをたたく。

$| = 1;
telnet "hoge", "hoge", sub {
  my ($stdin_w, $stdout_r) = @_;
  writeln($stdin_w, "ls");
  print read_all($stdout_r);
}

車輪の再発明。微妙。。


*1:CPANが使えればNet::Telnet使うんだけどな・・・。

*2:直接対話型コマンドをfork/execしても良いけど、よりユーザ環境に近づけるためにtelnetを挟みたかった。

*3:タイムアウト監視にはalarm関数を使う方法もある。

*4:exec失敗で固まったりバグだらけ。

*5:fork/execよりopen3関数を使うほうが簡潔だけど、今回はお勉強のためfork/exec