結論: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>&1
と 2>&3
を使わないと
標準エラー出力を取り込むことが出来ません
と書いたのですがのですが、実は IO.popen でもオプション設定により標準エラー出力を取り込めます。
IO.popenで標準エラー出力を取得するオプション
このエントリの先頭にも載せました。
IO.popen(command, :err => [:child, :out]) IO.popen(command, :err => [:child, :out]) { |pipe| … }
なお、ブロックパラメータ pipe は IO インスタンスなので、文字出力として扱う場合には、さらに IO#each_line や IO#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
ということで、今回求めていた内容がようやく出てきました。
ファイルディスクリプタの書き方
ファイルデスクリプタを表すためには、以下が利用できます。
なので、以下のどれであっても同じ意味です。理解しやすいものであればどれでもいいのではないでしょうか。
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 でもリダイレクトなどが行えるようになったようですね。
- ruby 1.9 feature (Ruby 1.8.7)
- open3 のはなし(pdf) ← Rubyist Magazine - RegionalRubyKaigi レポート (14) 東京 Ruby 会議 03
- Rubyist Magazine - Ruby 考古学 消された機能編
- 時間のかかるコマンドを非同期に実行する場合の「Process.spawn」「Kernel.#spawn」メソッド(若手エンジニア/初心者のためのRuby 2.1入門(11):RubyのThread、Fiber、Kernel、forkで並列処理やプロセスの深淵へ (2/3) - @IT)