コマンドの標準出力/標準エラー/終了ステータスを保持したい

ruby スクリプトからコマンドを実行したいのだがコマンドの標準出力、標準エラー、終了ステータスはその都度保持しておきたい。「system/IO.popen/Open3.popen3 を使えばなんとかなるのでは…」と思っていたのだが、実現できなかったので自分で新たに Command クラスを作成してみた。内部で Process.fork を使って子プロセスを生成し、IO.pipe を使って子から親に標準出力/標準エラーを渡した後に、Process.waitpid2 で終了ステータスを取得している。データは do メソッドを呼ぶ度に @info に格納し、履歴として @history にどんどん @info を追加していく。

■要求

  • - コマンドを実行し、標準出力/標準エラー/終了ステータスをデータとして格納すること。
  • - 上記のデータをいつでも参照できること。

■実装(独自 Command クラス定義)

class Command
  attr_reader :info
  attr_reader :history

  def initialize ()
    @info = {}
    @history = []
  end

  def do(*elem)
    line = elem.join(" ")
    time = Time.now

    rp1, wp1 = IO.pipe
    rp2, wp2 = IO.pipe
    Process.fork do
      rp1.close
      rp2.close
      STDOUT.reopen(wp1)
      STDERR.reopen(wp2)
      exec line
      wp1.close
      wp2.close
    end

    wp1.close
    wp2.close
    stdout = rp1.read
    stderr = rp2.read
    rp1.close
    rp2.close

    pid, status = Process.waitpid2
    @info = { :line => line, :pid => pid, :status => status,
      :exitstatus => status.exitstatus, :time => time,
      :stdout => stdout.chomp, :stderr => stderr.chomp }
    @history.push(@info)
  end
end

■使い方

cmd = Command.new()

cmd.do("ls -l", "/etc")
cmd.do("date")
cmd.do("ls -l noExistFile")

print "=== command history ===\n"
cmd.history.each do |h|
  print "time => ", h[:time], "\n"
  print "line => ", h[:line], "\n"
  print "exitstatus => ", h[:exitstatus], "\n"
  print "status => ", h[:status], "\n"
  print "stdout => ", h[:stdout], "\n"
  print "stderr => ", h[:stderr], "\n"
  print "-----\n\n"
end

■出力結果

=== command history ===
time => Tue May 26 12:42:09 +0900 2009
line => ls -l /etc
exitstatus => 0
status => 0
stdout => 合計 1456
drwxr-xr-x  4 root  root     4096 2009-04-20 23:01 ConsoleKit
drwxr-xr-x  4 root  root     4096 2009-04-20 23:09 NetworkManager
drwxr-xr-x  2 root  root     4096 2009-04-20 23:09 PolicyKit
drwxr-xr-x  9 root  root     4096 2009-05-21 13:20 X11
drwxr-xr-x  8 root  root     4096 2009-05-21 15:37 acpi
(...snip...)
stderr => 
        • -
time => Tue May 26 12:42:09 +0900 2009 line => date exitstatus => 0 status => 0 stdout => 2009年 5月 26日 火曜日 12:42:09 JST stderr =>
        • -
time => Tue May 26 12:42:09 +0900 2009 line => ls -l noExistFile exitstatus => 2 status => 512 stdout => stderr => ls: noExistFileにアクセスできません: No such file or directory
        • -

■不採用案

  • - system だとコマンドの標準出力/標準エラーが端末に出てしまう(STDOUT/ERR.reopen で system をはさめば OK)。
  • - IO.popen だと標準エラーが端末に出てしまう。
  • - Open3.popen3 だと終了ステータスが取れない。
$ # 標準出力、標準エラーともに格納できない。 ret に true/false, $? に終了ステータスは入る
$ ruby -e 'ret = system("ls -l .bashrc"); print ret, " ", $?, "\n"'
\-rw-r--r-- 1 kyagi kyagi 3176 2009-05-24 00:38 .bashrc
true 0
$ ruby -e 'ret = system("ls -l noExitFile"); print ret, " ", $?, "\n"'
ls: noExitFileにアクセスできません: No such file or directory
false 512
$ # 標準出力は格納できるが標準エラーが格納できない?
$ ruby -e 'f = IO.popen("ls -l");'
$ ruby -e 'f = IO.popen("ls -l noExistFile");
ls: noExistFileにアクセスできません: No such file or directory