IO.popenメソッドで標準エラー出力を取得するには

結論:IO.popenで標準エラー出力を取得するオプション

ぐだぐだ長いので、求めているものを先に示しておきます。

IO.popen(command, :err => [:child, :out])
IO.popen(command, :err => [:child, :out]) { |pipe| … }

前置き

まず先日書いた記事の引用を(ちょっと長め)。

tmpdumpのログ出力である標準エラー出力をIO.popenで受けるために

上記のスクリプトではプログレスバーを表示させるために IO.popen を使って rtmpdump のログを受けています。
しかし、IO.popen は標準出力を受けます。逆に言えば、標準エラー出力を取り込むことが出来ません。そのため、単純に

command = %Q[rtmpdump …(snip)… | ffmpeg -i pipe:0 nhk_pickup_news_video_#{yesterday_ddmm}_20.mp4 2>/dev/null]

としただけでは、rtmpdump のログ出力を IO.popen で受ける事が出来ません。
このため次のように rtmpdump の標準エラー出力が標準出力側へ出るよう振り分ける必要があります。

command = %Q[exec 3>&1; rtmpdump …(snip)… 2>&3 | ffmpeg -i pipe:0 nhk_pickup_news_video_#{yesterday_ddmm}_20.mp4 2>/dev/null]

つまり
(exec 3>&1)execで前準備:3番を1番(標準出力)に結びつける
(2>&3)rtmpdumpの2番(標準エラー出力=ログ出力)を3番(前準備で標準出力に結びついているがffmpegへのパイプには結びついていない)に結びつける
と工夫することで、IO.popenへログを送ることを可能にしています。
rtmpdumpとffmpegをつかったライブトランスコーディング - 別館 子子子子子子(ねこのここねこ)

ここで exec 3>&12>&3 を使わないと

標準エラー出力を取り込むことが出来ません

と書いたのですがのですが、実は IO.popen でもオプション設定により標準エラー出力を取り込めます。

IO.popenで標準エラー出力を取得するオプション

このエントリの先頭にも載せました。

IO.popen(command, :err => [:child, :out])
IO.popen(command, :err => [:child, :out]) { |pipe| … }

なお、ブロックパラメータ pipe は IO インスタンスなので、文字出力として扱う場合には、さらに IO#each_lineIO#each_char で扱いましょう。
標準エラー出力を扱う場合には Open3 ライブラリを使うことが多いようですが、わざわざ require 'open3' しなくても IO.popen ですむのであれば、そのほうが簡単でいいのではないでしょうか。

解説

popen(env = {}, command, mode = "r", opt={}) -> IO
popen(env = {}, command, mode = "r", opt={}) {|f| ... } -> object
 :
opt でプロセス起動のためのオプションや、パイプ IO オブジェクトの属性(エンコーディングや 読み書き能力)を指定することができます。 プロセス起動のためのオプションは Kernel.#spawn と、 パイプオブジェクトの属性の指定のオプションは IO.new と共通です。

ということで Kernel.#spawn のオプション解説を確認します。
ちょっと長く引用します(強調部分は引用者註)。

option引数の概要

Hash を options として渡すことで、起動される子プロセスの

  • プロセスグループ
  • resource limit
  • カレントディレクト
  • umask
  • 子プロセスでのリダイレクト

などを変更できます。環境変数のクリアなども指定できます。
 :

リダイレクト関連
Hash のキーに子プロセス側のファイルデスクリプタを、 対応する値に親プロセス側のファイルデスクリプタや ファイル名を指定することでリダイレクトを実現できます。

 :

option引数によるリダイレクトの概要

Hash のキー(子プロセス側)には以下のいずれかが指定できます。

  • 単一のファイルデスクリプタ
  • ファイルデスクリプタの配列

配列を渡すことで複数のファイルデスクリプタを同時にリダイレクトできます。
Hash の値(親プロセス側)には以下のいずれかが指定できます。

  • 単一のファイルデスクリプタ
  • リダイレクト先のファイル名文字列
  • [リダイレクト先のファイル名文字列]、配列の要素にすることで File::RDONLY でファイルを開いてリダイレクトします。
  • [リダイレクト先のファイル名文字列, モード文字列] open(ファイル名, モード, 0644) でファイルを開いてリダイレクト します。
  • [リダイレクト先のファイル名文字列, モード文字列, パーミション(整数)] open(ファイル名, モード, パーミッション) でファイルを 開いてリダイレクトします。
  • [:child, ファイルデスクリプタ] 子プロセス側のファイルデスクリプタを指定できます。
  • :close キーで指定したファイルデスクリプタを子プロセス側で閉じます

長々と引用しましたが、やはり例が無いとイメージが湧きません。
次は用例の部分を引用します。

option引数の詳細および例

 :
リダイレクトは様々なやりかたが使えます。 Hash のキーが子プロセス側、値が親プロセス側です。
# 以下の例はすべて stderr を stdout にリダイレクトします

pid = spawn(command, :err=>:out)
pid = spawn(command, 2=>1)
pid = spawn(command, STDERR=>:out)
pid = spawn(command, STDERR=>STDOUT)

この例では子プロセス側の stdout には触れていないので、 親プロセスから引き継がれます。
 :
例えば、

:err => :out

とすると、子プロセスの stderr を親プロセスの stdout にリダイレクトします。

:err => [:child, :out]

とすると、子プロセスの stderr を子プロセスの stdout にリダイレクトします。 これを用いて、IO.popen で、子プロセスの stderr と stdout を混ぜる例を以下に示します。

io = IO.popen(["sh", "-c", "echo out; echo err >&2", :err=>[:child, :out]])
p io.read #=> "out\nerr\n

ということで、今回求めていた内容がようやく出てきました。

ファイルディスクリプタの書き方

ファイルデスクリプタを表すためには、以下が利用できます。

  • :in 標準入力, ファイルデスクリプタ0
  • :out 標準出力, ファイルデスクリプタ1
  • :err 標準エラー出力, ファイルデスクリプタ2
  • 整数 指定した整数が表すファイルデスクリプタ
  • IO IO#fileno で表されるファイルデスクリプタ

なので、以下のどれであっても同じ意味です。理解しやすいものであればどれでもいいのではないでしょうか。

IO.popen(command, :err => [:child, :out]) { |pipe| … }
IO.popen(command, err: [:child, :out]) { |pipe| … }
IO.popen(command, 2 => [:child, 1]) { |pipe| … }
IO.popen(command, $stderr => [:child, $stdout]) { |pipe| … }
IO.popen(command, STDERR=> [:child, STDOUT]) { |pipe| … }

おまけ:その他のオプション項目

option引数の概要

 :
以下のオプションが指定できます。

:unsetenv_others
これを true にすると、envで指定した環境変数以外をすべてクリアします。 false だとクリアしません。false がデフォルトです。
:pgroup
引数に true or 0 を渡すと新しいプロセスグループを作成し、そこで動きます。 整数を渡すと、指定したプロセスグループに属します nil を渡すとプロセスグループを変更しません。デフォルトは nil です。
:rlimit_core, :rlimit_cpu, etc
resource limit を設定します。詳しくは Process.#setrlimit を見て ください。引数には整数、もしくは整数2つの配列を渡します。
:chdir
指定した文字列をカレントディレクトリにします。
:umask
指定した整数を umask に設定します。
リダイレクト関連
Hash のキーに子プロセス側のファイルデスクリプタを、 対応する値に親プロセス側のファイルデスクリプタや ファイル名を指定することでリダイレクトを実現できます。
:close_others
これを true に設定すると リダイレクトされていない、0(stdin), 1(stdout), 2(stderr) 以外の ファイルデスクリプタをすべて閉じます。 true がデフォルトです。

このうち、:chdir:umask が便利そうだなと思いました。

「:chdir」で子プロセスのカレントディレクトリを変更できます。

pid = spawn(command, :chdir=>"/var/tmp")

「:umask」で子プロセスの umask を指定できます。

pid = spawn(command, :umask=>077)

歴史的補足

どうやら Kernel.#spawn というものが Ruby 1.9 で導入され、それにより IO.popen でもリダイレクトなどが行えるようになったようですね。