外部プログラムを起動し,そのプロセスからの標準出力・ 標準エラー出力を受け取ったり,逆にそのプロセスの標準入力へ データを送り込んだりする方法をいくつか示す。
起動したプロセスの標準出力のみを受け取る場合は IO.popenを使うのがもっとも簡単である。
以下のプログラムは cal コマンドを起動し,カレンダー出力を 加工して出力するものである。
#!/usr/bin/env ruby # coding: euc-jp lineno = 0 # 行番号をつける IO.popen("cal", "r") do |c| while line=c.gets printf("%d: %s", lineno+=1, line) end end
IO.popen
でのモードを "r+"
にすると,起動したプロセスとの間の読み書き両方できるようになる。
子プロセスのやりとりが,
という短いデータの1往復の流れなら,
IO.popen(子プロセス, "r+") do |c|
c.puts データ
c.close_write
while line = c.gets
「lineを利用した処理」
end
end
のように書く。close_write
は,出力のみをクローズする
メソッドで,これがない場合実際には puts
をしても
子プロセスまでデータが伝わらない。
標準入出力経由のデータは何バイト分かを内部バッファに溜めてから
まとめて書き出されるので,データを書く側が送ったつもりでも読み取り側に
伝わらないことが多い。クローズすると,これ以上溜めるべきデータがないと
みなされ実際に読み取り側に送られることとなる。
以上の書き方は比較的単純であるが,これで済む状況ばかりではない。
「親→子」への出力が複数行に渡る場合や,親と子のやりとりが
何往復にも渡る場合は両プロセスでの入力処理(親となるRubyスクリプトでは
gets
)が完了するタイミングが合わない。先述のバッファは
有限サイズなので,書き込む側がどんどん書いて,
いっこうに読まれないといずれ溢れる。
溢れそうなときは書き込み自体に待ったがかかるため,処理がそこで停止する。
たとえば以下のプログラムの繰り返し指定数値をいろいろ変更して試してみよ。
#!/usr/bin/env ruby # coding: euc-jp # ./cat-n.rb 5000 とすれば5000回。省略時1000回。 repeat = (ARGV[0] || 1000).to_i IO.popen("cat -n", "r+") do |c| 0.upto(repeat) {|i| c.puts i} c.close_write while line=c.gets print line end end
別プロセスとの読み書きを行なう場合は,書き込みと読み込み両方を よどみなく行なう必要がある。そのために,書き込み用の処理と読み込み用の 処理を並列で動かせばよい。
スレッドを利用し,読み書きそれぞれの処理を並列で動かす例を示す。
#!/usr/bin/env ruby
# coding: euc-jp
# ./cat-n.rb 5000 とすれば5000回。省略時1000回。
repeat = (ARGV[0] || 1000).to_i
IO.popen("cat -n", "r+") do |c|
Thread.new {
0.upto(repeat) {|i| c.puts i}
c.close_write
}
while line=c.gets
print line
end
end
IO.popen
では,子プロセスからの標準エラー出力を
受け取ることができない。子プロセスの標準エラー出力も受け取りたいときは
Ruby固有の Open3 や,C由来の言語に共通の pipe+fork+exec
の組み合わせを利用する。
以下に2つの方法を示すが,標準入出力・エラー出力を同時に利用する 外部プログラムの例として以下のシェルスクリプトを利用する。
#!/bin/sh echo -n "何ヶ月分?: " 1>&2 read n if [ "$n" -le 0 ]; then # 数字以外があるとコケる。要エラーチェック。 echo "無効な指定です。1以上の数を指定して下さい。" 1>&2 exit 1 fi m=`date +%m` y=`date +%Y` while [ "$n" -gt 0 ]; do if [ $m -lt 12 ]; then m=1; y=`expr $y + 1` fi cal $m $y m=`expr $m + 1` n=`expr $n - 1` done
このスクリプトは,標準エラー出力に案内文を出し, 標準入力から値を取得し,結果を標準出力に書き出す。 実際に実行した例を示す。
./ioe.sh # 標準エラー出力は色を変えて示す 何ヶ月分?: 3 January 2012 S M Tu W Th F S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 January 2013 S M Tu W Th F S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 January 2014 S M Tu W Th F S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Open3を利用すると子プロセスの標準出力,標準エラー出力,標準入力 と接続したファイル記述子を生成して,それらに対して入出力を 行なうことが可能となる。Open3は,Open3::popen3 メソッドに起動するコマンド 行と処理ブロックを渡す。ブロックでは標準入力,標準出力, 標準エラー出力のファイルハンドルを引数として受けて処理を進める。
require 'open3' Open::popen3("コマンド") do |in, out, err| 〜〜 対話入力処理 〜〜 end
これを用いて ioe.sh
を子プロセスで実行するプログラムを以下に示す。
#!/usr/bin/env ruby
# coding: euc-jp
require 'open3'
prog = "./ioe.sh"
STDERR.print "指定した月数分だけカレンダーを出力します。
何ヶ月分出しますか: "
n = gets.to_i # 英字だけなどは0になる。
Open3.popen3(prog) do |i, o, e|
Thread.new { # 子プロセスの標準入力
i.puts n.to_s # つまり自分(親)からの出力
i.close
}
Thread.new {
while line=e.gets # 子プロセスの標準エラー出力
# ここでは特に処理しない
end
}
while line=o.gets # 子プロセスの標準出力
print line
end
end
親プロセスと子プロセスの間の入出力を直接作成して制御する 流れを示す。Ruby以外の言語で,C由来のものはほぼこれと同じ 手順で子プロセスとの双方向通信ができるので覚えておくとよい。
パイプは,プロセス間で通信を行なうための機構で Ruby では
IO.pipe
で作成する。IO.pipe
を呼ぶと,2つのファイル記述子を要素に持つ配列が返される。
最初の(第0)要素は読み込み用(入力端),
次の(第1)要素は書き込み用(出力端)に利用する。
出力端に書き出したデータは,同じものが同じ順で入力端から読み出せる。
簡単な例で効果を示す。
#!/usr/bin/env ruby # coding: euc-jp repeat = (ARGV[0] || 5).to_i # ./pipe.rb 10 とすると10回,省略時5回 io = IO.pipe io[1].puts "あいうえお "*repeat # 文字列を指定した回数繰り返したものを書き出す io[1].flush print io[0].gets
この例では,パイプに対してデータを書き込んで, すぐにそのパイプから読み込んでいる。まったく同じものが読み取れる。 パイプは,forkした先のプロセスでも共有され,通常は他のプロセスとの データの授受に利用する。標準入出力,標準エラー出力を介して 子プロセスとのやりとりを行なうプログラムの流れは以下のようになる。
このような流れをプログラム化したのが
ioe-pfe.rb
である。
#!/usr/bin/env ruby # coding: euc-jp prog = "./ioe.sh" STDERR.print "指定した月数分だけカレンダーを出力します。 何ヶ月分出しますか: " n = gets.to_i # 英字だけなどは0になる。 STDOUT.flush i = IO.pipe # 子プロセスの標準入力とのパイプ o = IO.pipe # 子プロセスの標準出力とのパイプ e = IO.pipe # 子プロセスの標準エラー出力とのパイプ STDOUT.flush if pid=fork i[1].puts n.to_s i[1].close # 書き出しが終了したのでクローズ i[0].close # 以下,使わないのでクローズ o[1].close; e[1].close while line=o[0].gets print ":"+line # コロンを付けて出力 end else i[1].close; o[0].close; e[0].close # 使わないのでクローズ STDIN.reopen(i[0]) # STDINの置き換え STDOUT.reopen(o[1]) # STDOUTの置き換え STDERR.reopen(e[1]) # STDERRの置き換え exec(prog) end
このプログラムでは,独自の入力プロンプト文字列を出して 値を入力し,それを起動した子プロセスの標準入力に流し込み, 最後に子プロセスの標準出力を読み取り,それを加工した結果を 最終出力している。その実行例を示す。
./ioe-pfe.rb
指定した月数分だけカレンダーを出力します。
何ヶ月分出しますか: 2
: January 2012
: S M Tu W Th F S
: 1 2 3 4 5 6 7
: 8 9 10 11 12 13 14
:15 16 17 18 19 20 21
:22 23 24 25 26 27 28
:29 30 31
:
: January 2013
: S M Tu W Th F S
: 1 2 3 4 5
: 6 7 8 9 10 11 12
:13 14 15 16 17 18 19
:20 21 22 23 24 25 26
:27 28 29 30 31
:
対話的プログラムを作る場合,ユーザインタフェースを複数用意しておけば, それだけ多くの人の要望に応えられる可能性が上がる。そのためには,実際に ユーザとの対話を行なう部分と,その指示で計算機資源を制御する部分を 独立したものとして作成する。制御部分は(最低)1つ作成しておき, ユーザインタフェースは簡素なものにしておく。そのまま利用させてもよいが, 対話機能を受け持つ別のプログラムから制御プログラムを操作するように設計すると 構造を単純化でき,見通しのよいシステムとなる。
たとえば,mpg123 は, MP3ファイルを音楽信号にデコードし,音源デバイスに送り込む 音楽再生プログラムであるが,このプログラム自身はコマンドラインに渡された ファイルを順次再生する機能だけを有する。いっぽう, 複数のファイルを表示してユーザに再生したいファイルを選択させたりなどの きめこまやかな対話処理を行なうプログラムを作り,それがmpg123プログラムを 子プロセスとして利用することで,みかけ上ひとつの音楽再生ソフトウェアが 完成する。
このように,実処理は裏で呼ぶプログラムに任せ,ユーザとの対話処理に 特化したプログラムのことをフロントエンドプロセッサ という(逆に裏で実処理を行なうものをバックエンドプロセッサという)。
mpg123プログラムは,他のプログラムの子プロセスとして利用しやすいよう 設計されている。mpg123プログラムを起動すると,標準エラー出力に現在 再生中の曲の総フレーム数,再生中フレーム番号が出される。また mpg123 プロセスにINTシグナルを送ると再生中の曲を停めて次の曲の再生に移る。
ここで、子プロセスとして起動し制御する対象とするmpg123プログラムが 標準エラー出力に吐き出す情報を簡単に詳解しておく。実際にmpg123プログラム を起動できる場合はそれも見た方がよい。
以下の出力例は、mpg123プログラムに2つのMP3ファイルを渡し再生させ, 1曲目の途中で C-c をタイプしてINTシグナルを送ったときの ものである。
mpg123 -v music1.mp3 music2.mp3 High Performance MPEG 1.0/2.0/2.5 Audio Player for Layers 1, 2 and 3 version 1.13.1; written and copyright by Michael Hipp and others free software (LGPL/GPL) without any warranty but with best wishes Decoder: SSE Playing MPEG stream 1 of 2: music1.mp3 ... MPEG 1.0, Layer: III, Freq: 44100, mode: Joint-Stereo, modext: 2, BPF : 365 Channels: 2, copyright: No, original: Yes, CRC: No, emphasis: 0. Bitrate: 112 kbit/s Extension value: 0 Frame# 74 [ 6579], Time: 00:01.93 [02:51.85], RVA: off, Vol: 100(100)^C [0:01] Decoding of music1.mp3 finished. Playing MPEG stream 2 of 2: music2.mp3 ... MPEG 1.0, Layer: III, Freq: 44100, mode: Joint-Stereo, modext: 2, BPF : 365 Channels: 2, copyright: No, original: Yes, CRC: No, emphasis: 0. Bitrate: 112 kbit/s Extension value: 0 Frame# 53 [ 7493], Time: 00:01.38 [03:15.73], RVA: off, Vol: 100(100)^C [0:01] Decoding of music2.mp3 finished.
3段落に分かれている出力のうち,1段落めはコピーライト等の表示, 2段落めは1曲目(music1.mp3)の再生, 3段落めは2曲目(music2.mp3)の再生のときのものである。 着目すべきは下線で示した部分でこれらは,これから再生する音楽ファイル名と, 再生中のフレーム番号を示している。フレーム番号は再生が進むにつれて 数が随時増えていく。mpg123プログラムを子プロセスとして起動したプログラムでは, これらの情報をパターンマッチで検出し,その後の処理に利用していくことになる。
パイプを利用して子プロセスの標準エラー出力を読み取り, 標準入力から得たユーザの司令を子プロセスにシグナル送信で伝える プログラムの例を示す。
#!/usr/bin/env ruby # coding: euc-jp require 'kconv' # 漢字コードを揃えるため(1.8+1.9) if !ARGV[0] STDERR.puts "再生したいファイルを指定して下さい。\n用法:" STDERR.puts "\t#{$0} <MP3ファイル(群)>" exit 1 end frame = nil # 生成中フレーム# の保存用 e = IO.pipe # mpg123からは標準エラー出力のみ読み取る pid = fork do # fork&exec を行なう。PIDを記憶。 STDERR.printf("ちわ,子です。%s を演奏します。", ARGV.join(" ")) STDERR.reopen(e[1]) e[0].close # 入力端は使わないので閉じる exec "mpg123", "-v", *ARGV # *ARGVで配列を展開して渡す end # ここから親の処理 e[1].close # 親の出力端は使わないので閉じる def prompt(file) STDERR.printf("\n[%s]再生中 - コマンド(nで次の曲,qで終了): ", file) end Thread.new do # 子プロセス(mpg123)と通信するスレッド while not e[0].closed? line = "" while l = e[0].getc # 1字ずつ読み、lineに足していく。 if l != "\r"[0] && l != "\n"[0] # CRかLF以外なら line << l.chr # lineに追加 else # CRかLFが来たら行(line)の完成 break # ループを抜けてパターンマッチに移る end end if /Frame\#\s*(\d+)/ =~ line.toeuc STDERR.printf "%s ", $1 if $DEBUG frame = $1.to_i elsif /stream .* (.+) \.\.\./ =~ line.toeuc prompt($1) # 再生ファイル名パターンが来たらプロンプト出力 end end end Thread.new do # プロセスの終了を監視するスレッド Process.wait # mpg123終了まで待機 printf("Stopping at frame %s\n", frame) exit 0 # mpg123が終了したらこのプログラムも終わる。 end while true a = STDIN.gets # promptが出ているはず if a.nil? || /q/ =~ a Process.kill(:INT, pid) # mpg123コマンドに Process.kill(:INT, pid) # 2連続でINTシグナルを送ると終了 break elsif /n/ =~ a Process.kill(:INT, pid) # 1回INTシグナル送信で次の曲へ end end