Rubyから外部プログラムを起動する方法まとめ

簡単なまとめ

Open3.capture3 Open3.capture2 Open3.capture2e 普通に起動するとき
Open3.popen3 Open3.popen2 Open3.popen2e 外部プログラムにパイプでつなぎたいとき
バッククオート 書き捨てスクリプト用(出力がほしい場合)
system 書き捨てスクリプト用(出力がいらない場合)
IO.popen IO.pipe 使いません。
exec spawn fork など 使いません。
systemu 使いません。

Open3.capture2 Open3.capture2e Open3.capture3

Rubyで外部プログラムを実行する、たいがいのケースはOpen3.capture3でまかなえます。

外部コマンドを実行し、標準出力・標準エラーを文字列で取れ、終了コードも受け取れます。 標準入力に文字列を渡すこともできます。

require "open3"

out, err, status = Open3.capture3("echo a; sort >&2", :stdin_data=>"foo\nbar\nbaz\n")
p out               #=> "a\n"
p err               #=> "bar\nbaz\nfoo\n"
p status.exitstatus #=> 0

また、標準出力は欲しいがエラーはコンソールにそのまま出したい場合や、 標準出力と標準エラーをいっしょくたにして取得したい場合は、 それぞれPpen3.capture2 Open3.capture2e という変種を使います。

Open3モジュールのヘルプ

Open3.popen2 Open3.popen2e Open3.popen3

Open3.capture*では、Rubyスクリプトは外部プログラムが終了するのを待って、それから次の処理に進みます。

しかし、外部プログラムと並列に実行させたい場合は、Open3.popen3 を使います。 Open3.popen3からは標準入力・標準出力・標準エラーのファイルオブジェクトと、 外部プログラムの終了を待つためのスレッドが取れます。

require "open3"

# nroff を実行してその標準入力に man ページを送り込み処理させる。
# nroff プロセスの標準出力から処理結果を受け取る。
stdin, stdout, stderr, wait_thr = *Open3.popen3('nroff -man')

# こちらから書く
Thread.fork {
  File.foreach('/usr/man/man1/ruby.1') do |line|
    stdin.print line
  end
  stdin.close    # または close_write
}

# こちらから読む
stdout.each do |line|
  print line
end

p wait_thr.value.exitstatus # => 0

なお、stdin.closeなどで標準入力を閉じずにstdout.each_lineなどを呼ぶとハングアップする場合があるなど、 Open3.popen3にはやや注意が必要です。

また Open3.popen2 Open3.popen2e という変種もあります。

Open3モジュールのヘルプ

バッククオートと system

バッククオートはコマンドを実行して、その標準出力を文字列として取得します。

一方 system は実行結果によって true(成功) false(失敗) nil(実行できなかった) を返します。 標準出力はそのまま出ていきます。

puts `ruby -v` #=> ruby 2.0.0p353 (2013-11-22 revision 43784) [x86_64-linux]

system "ruby -v"
#(コンソールに)ruby 2.0.0p353 (2013-11-22 revision 43784) [x86_64-linux]

puts $?.exitstatus #=> 0

しかし、バッククオートとsystemでは標準エラー出力はとってこれません。 また、終了コードもグローバル変数$?からとるなど、やや特殊です。

スクリプトでは遅かれ早かれ、エラー処理のために標準エラー出力や終了コードが欲しくなります。 であるならば、初めから Open3.* を使った方が統一的になるでしょう。

もっとも、バッククオートとsystemrequireが不要であり、 irbから呼び出したりエラー処理を気にしない書き捨てスクリプトで使う分には手軽です。

Kernelモジュールのヘルプ

IO.popen IO.pipe exec spawn fork など

使いません 。 これらは、過去との互換性をとったり、C言語のプログラムを移植するためにあります。 特別な理由が無い限りOpen3.*を使いましょう。

systemu

systemuOpen3.*と同じようなことをするgemです。 しかし、 Open3.*は標準で入っている し、systemuは一時ファイルを使っているため遅いようです。 特別な理由がない限り Open3.*を使いましょう。

なお「特別な理由」としては、ブロックを渡して、外部プログラムの実行に時間がかかった時に強制終了させたりできます。

require 'systemu'

looper = %q( ruby -e" loop{ STDERR.puts Time.now.to_i; sleep 1 } " )

status, stdout, stderr =
  systemu looper do |cid|
    sleep 3
    Process.kill 9, cid
  end

ahoward/systemu

共通の注意点

Rubyで外部プログラムを起動する方法はどれも、メタ文字を含む場合はシェル経由で実行します。

メタ文字: * ? {} [] <> () ~ & | \ $ ; ' ` " \n

そのため必要に応じてShellwords.escapeなどで エスケープが必要です。

むすび

Rubyは優れた言語です。

私もRubyを仕事で使っています(Cucumber/Capybara/Sauce Labs でテストをしています)

Enumerable#find_allは大好きです。Hash#fetchも大好きです。 メソッドチェーンで配列からハッシュを構築するとβエンドルフィンが出ます。

でも、どうしても心の底からRubyを愛せないのです。

それは、昔私がまだ少年だった頃、最新のオブジェクト指向言語Ruby(笑)を身につけようとして、 トップレベルでも、オープンクラスでもなく、外部プログラムを起動する方法がわからないせいで*1、習得を諦めた苦い体験があったからかもしれません。

この記事を、新たにRubyに手を染める若い人達に捧げます。

*1:ググレカスと思われるかもしれません。しかし、Windowsしか知らず、周囲に相談できる先輩もいない少年にとり、当時の「IO.popenはpopen(3)です。以上」といった記述は心を砕くのに十分でした。