From 87c7905bdc985a9f04b01dd5b38f44bdd6808d4f Mon Sep 17 00:00:00 2001 From: akr Date: Thu, 4 Dec 2008 10:58:32 +0000 Subject: * lib/open3.rb (Open3.popen3): simplified. (Open3.popen_run): extracted from Open3.popen3. (Open3.popen2): new method. (Open3.popen2e): new method. (Open3.pipeline_rw): new method. (Open3.pipeline_r): new method. (Open3.pipeline_w): new method. (Open3.pipeline_run): new private method. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@20512 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 11 ++ lib/open3.rb | 317 ++++++++++++++++++++++++++++++++++++++++++++--------- test/test_open3.rb | 138 +++++++++-------------- 3 files changed, 327 insertions(+), 139 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4363053491..f516ecae82 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,14 @@ +Thu Dec 4 19:56:20 2008 Tanaka Akira + + * lib/open3.rb (Open3.popen3): simplified. + (Open3.popen_run): extracted from Open3.popen3. + (Open3.popen2): new method. + (Open3.popen2e): new method. + (Open3.pipeline_rw): new method. + (Open3.pipeline_r): new method. + (Open3.pipeline_w): new method. + (Open3.pipeline_run): new private method. + Thu Dec 4 19:16:28 2008 Tanaka Akira * process.c (check_exec_fds): resolve cascaded child fd reference. diff --git a/lib/open3.rb b/lib/open3.rb index 454cd44ee4..55915bb512 100644 --- a/lib/open3.rb +++ b/lib/open3.rb @@ -51,7 +51,7 @@ module Open3 # stdin, stdout, stderr, wait_thr = Open3.popen3(cmd... [, opts]) # pid = wait_thr[:pid] # pid of the started process. # ... - # stdin.close # stdin, stdout and stderr should be closed in this form. + # stdin.close # stdin, stdout and stderr should be closed explicitly in this form. # stdout.close # stderr.close # exit_status = wait_thr.value # Process::Status object returned. @@ -61,6 +61,7 @@ module Open3 # # Open3.popen3("echo a") {|i, o, e, t| ... } # Open3.popen3("echo", "a") {|i, o, e, t| ... } + # Open3.popen3(["echo", "argv0"], "a") {|i, o, e, t| ... } # # If the last parameter, opts, is a Hash, it is recognized as an option for Kernel#spawn. # @@ -68,75 +69,105 @@ module Open3 # p o.read.chomp #=> "/" # } # - # opts[STDIN], opts[STDOUT] and opts[STDERR] in the option are set for redirection. - # - # If some of the three elements in opts are specified, - # pipes for them are not created. - # In that case, block arugments for the block form and - # return values for the non-block form are decreased. + # wait_thr.value waits the termination of the process. + # The block form also waits the process when it returns. # - # # No pipe "e" for stderr - # Open3.popen3("echo a", STDERR=>nil) {|i,o,t| ... } - # i,o,t = Open3.popen3("echo a", STDERR=>nil) + # Closing stdin, stdout and stderr does not wait the process. # - # If the value is nil as above, the elements of opts are removed. - # So standard input/output/error of current process are inherited. + def popen3(*cmd, &block) + if Hash === cmd.last + opts = cmd.pop.dup + else + opts = {} + end + + in_r, in_w = IO.pipe + opts[STDIN] = in_r + in_w.sync = true + + out_r, out_w = IO.pipe + opts[STDOUT] = out_w + + err_r, err_w = IO.pipe + opts[STDERR] = err_w + + popen_run(cmd, opts, [in_r, out_w, err_w], [in_w, out_r, err_r], &block) + end + module_function :popen3 + + # Open3.popen2 is similer to Open3.popen3 except it doesn't make a pipe for + # the standard error stream. # - # If the value is not nil, it is passed as is to Kernel#spawn. - # So pipeline of commands can be constracted as follows. + # Block form: # - # Open3.popen3("yes", STDIN=>nil, STDERR=>nil) {|o1,t1| - # Open3.popen3("head -10", STDIN=>o1, STDERR=>nil) {|o2,t2| - # o1.close - # p o2.read #=> "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\n" - # p t1.value #=> # - # p t2.value #=> # - # } + # Open3.popen2(cmd... [, opts]) {|stdin, stdout, wait_thr| + # pid = wait_thr.pid # pid of the started process. + # ... + # exit_status = wait_thr.value # Process::Status object returned. # } # - # wait_thr.value waits the termination of the process. - # The block form also waits the process when it returns. - # - # Closing stdin, stdout and stderr does not wait the process. + # Non-block form: + # + # stdin, stdout, wait_thr = Open3.popen2(cmd... [, opts]) + # ... + # stdin.close # stdin and stdout should be closed explicitly in this form. + # stdout.close # - def popen3(*cmd) + def popen2(*cmd, &block) if Hash === cmd.last opts = cmd.pop.dup else opts = {} end - child_io = [] - parent_io = [] - - if !opts.include?(STDIN) - pw = IO.pipe # pipe[0] for read, pipe[1] for write - opts[STDIN] = pw[0] - pw[1].sync = true - child_io << pw[0] - parent_io << pw[1] - elsif opts[STDIN] == nil - opts.delete(STDIN) - end + in_r, in_w = IO.pipe + opts[STDIN] = in_r + in_w.sync = true - if !opts.include?(STDOUT) - pr = IO.pipe - opts[STDOUT] = pr[1] - child_io << pr[1] - parent_io << pr[0] - elsif opts[STDOUT] == nil - opts.delete(STDOUT) - end + out_r, out_w = IO.pipe + opts[STDOUT] = out_w + + popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block) + end + module_function :popen2 - if !opts.include?(STDERR) - pe = IO.pipe - opts[STDERR] = pe[1] - child_io << pe[1] - parent_io << pe[0] - elsif opts[STDERR] == nil - opts.delete(STDERR) + # Open3.popen2e is similer to Open3.popen3 except it merges + # the standard output stream and the standard error stream. + # + # Block form: + # + # Open3.popen2e(cmd... [, opts]) {|stdin, stdout_and_stderr, wait_thr| + # pid = wait_thr.pid # pid of the started process. + # ... + # exit_status = wait_thr.value # Process::Status object returned. + # } + # + # Non-block form: + # + # stdin, stdout_and_stderr, wait_thr = Open3.popen2e(cmd... [, opts]) + # ... + # stdin.close # stdin and stdout_and_stderr should be closed explicitly in this form. + # stdout_and_stderr.close + # + def popen2e(*cmd, &block) + if Hash === cmd.last + opts = cmd.pop.dup + else + opts = {} end + in_r, in_w = IO.pipe + opts[STDIN] = in_r + in_w.sync = true + + out_r, out_w = IO.pipe + opts[[STDOUT, STDERR]] = out_w + + popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block) + end + module_function :popen2e + + def popen_run(cmd, opts, child_io, parent_io) # :nodoc: pid = spawn(*cmd, opts) wait_thr = Process.detach(pid) child_io.each {|io| io.close } @@ -151,7 +182,185 @@ module Open3 end result end - module_function :popen3 + module_function :popen_run + class << self + private :popen_run + end + + # Open3.pipeline_rw starts list of commands as a pipeline with pipes + # which connects stdin of the first command and stdout of the last command. + # + # Open3.pipeline_rw(cmd1, cmd2, ... [, opts]) {|stdin, stdout, wait_threads| + # ... + # } + # + # stdin, stdout, wait_threads = Open3.pipeline_rw(cmd1, cmd2, ... [, opts]) + # ... + # stdin.close + # stdout.close + # + # Each cmd is a string or an array. + # If it is an array, the elements are passed to Kernel#spawn. + # + # The option to pass Kernel#spawn is constructed by merging + # +opts+, the last hash element of the array and + # specification for the pipe between each commands. + # + # Example: + # + # Open3.pipeline_rw("sort", "cat -n") {|stdin, stdout, wait_thrs| + # stdin.puts "foo" + # stdin.puts "bar" + # stdin.puts "baz" + # stdin.close # send EOF to sort. + # p stdout.read #=> " 1\tbar\n 2\tbaz\n 3\tfoo\n" + # } + def pipeline_rw(*cmds, &block) + if Hash === cmds.last + opts = cmds.pop.dup + else + opts = {} + end + + in_r, in_w = IO.pipe + opts[STDIN] = in_r + in_w.sync = true + + out_r, out_w = IO.pipe + opts[STDOUT] = out_w + + pipeline_run(cmds, opts, [in_r, out_w], [in_w, out_r], &block) + end + module_function :pipeline_rw + + # Open3.pipeline_r starts list of commands as a pipeline with a pipe + # which connects stdout of the last command. + # + # Open3.pipeline_r(cmd1, cmd2, ... [, opts]) {|stdout, wait_threads| + # ... + # } + # + # stdout, wait_threads = Open3.pipeline_r(cmd1, cmd2, ... [, opts]) + # ... + # stdout.close + # + # Example: + # + # Open3.pipeline_r("yes", "head -10") {|r, ts| + # p r.read #=> "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\n" + # p ts[0].value #=> # + # p ts[1].value #=> # + # } + # + def pipeline_r(*cmds, &block) + if Hash === cmds.last + opts = cmds.pop.dup + else + opts = {} + end + + out_r, out_w = IO.pipe + opts[STDOUT] = out_w + + pipeline_run(cmds, opts, [out_w], [out_r], &block) + end + module_function :pipeline_r + + # Open3.pipeline_w starts list of commands as a pipeline with a pipe + # which connects stdin of the first command. + # + # Open3.pipeline_w(cmd1, cmd2, ... [, opts]) {|stdin, wait_threads| + # ... + # } + # + # stdin, wait_threads = Open3.pipeline_w(cmd1, cmd2, ... [, opts]) + # ... + # stdin.close + # + # Example: + # + # Open3.pipeline_w("cat -n", "bzip2 -c", STDOUT=>"/tmp/z.bz2") {|w, ts| + # w.puts "hello" + # w.close + # p ts[0].value + # p ts[1].value + # } + # + def pipeline_w(*cmds, &block) + if Hash === cmds.last + opts = cmds.pop.dup + else + opts = {} + end + + in_r, in_w = IO.pipe + opts[STDIN] = in_r + in_w.sync = true + + pipeline_run(cmds, opts, [in_r], [in_w], &block) + end + module_function :pipeline_w + + def pipeline_run(cmds, pipeline_opts, child_io, parent_io, &block) # :nodoc: + if cmds.empty? + raise ArgumentError, "no commands" + end + + opts_base = pipeline_opts.dup + opts_base.delete STDIN + opts_base.delete STDOUT + + wait_thrs = [] + r = nil + cmds.each_with_index {|cmd, i| + cmd_opts = opts_base.dup + if String === cmd + cmd = [cmd] + else + cmd_opts.update cmd.pop if Hash === cmd.last + end + if i == 0 + if !cmd_opts.include?(STDIN) + if pipeline_opts.include?(STDIN) + cmd_opts[STDIN] = pipeline_opts[STDIN] + end + end + else + cmd_opts[STDIN] = r + end + if i != cmds.length - 1 + r2, w2 = IO.pipe + cmd_opts[STDOUT] = w2 + else + if !cmd_opts.include?(STDOUT) + if pipeline_opts.include?(STDOUT) + cmd_opts[STDOUT] = pipeline_opts[STDOUT] + end + end + end + pid = spawn(*cmd, cmd_opts) + wait_thrs << Process.detach(pid) + r.close if r + w2.close if w2 + r = r2 + } + result = parent_io + [wait_thrs] + child_io.each {|io| io.close } + if defined? yield + begin + return yield(*result) + ensure + parent_io.each{|io| io.close unless io.closed?} + wait_thrs.each {|t| t.join } + end + end + result + end + module_function :pipeline_run + class << self + private :pipeline_run + end + end if $0 == __FILE__ diff --git a/test/test_open3.rb b/test/test_open3.rb index a484d4fb9b..7d45eb543e 100644 --- a/test/test_open3.rb +++ b/test/test_open3.rb @@ -74,21 +74,6 @@ class TestOpen3 < Test::Unit::TestCase } end - def test_disable - Open3.popen3(RUBY, '-e', '', STDIN=>nil) {|o,e,t| - assert_kind_of(Thread, t) - } - Open3.popen3(RUBY, '-e', '', STDOUT=>nil) {|i,e,t| - assert_kind_of(Thread, t) - } - Open3.popen3(RUBY, '-e', '', STDERR=>nil) {|i,o,t| - assert_kind_of(Thread, t) - } - Open3.popen3(RUBY, '-e', '', STDIN=>nil, STDOUT=>nil, STDERR=>nil) {|t| - assert_kind_of(Thread, t) - } - end - def with_pipe r, w = IO.pipe yield r, w @@ -106,100 +91,83 @@ class TestOpen3 < Test::Unit::TestCase old.close if old && !old.closed? end - def test_disable_stdin - with_pipe {|r, w| - with_reopen(STDIN, r) {|old| - Open3.popen3(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDERR.print s+"e"', STDIN=>nil) {|o,e,t| - assert_kind_of(Thread, t) - w.print "x" - w.close - assert_equal("xo", o.read) - assert_equal("xe", e.read) - } - } - } - end - - def test_disable_stdout + def test_popen2 with_pipe {|r, w| - with_reopen(STDOUT, w) {|old| + with_reopen(STDERR, w) {|old| w.close - Open3.popen3(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDERR.print s+"e"', STDOUT=>nil) {|i,e,t| + Open3.popen2(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDERR.print s+"e"') {|i,o,t| assert_kind_of(Thread, t) - i.print "y" + i.print "z" i.close - STDOUT.reopen(old) - assert_equal("yo", r.read) - assert_equal("ye", e.read) + STDERR.reopen(old) + assert_equal("zo", o.read) + assert_equal("ze", r.read) } } } end - def test_disable_stderr + def test_popen2e with_pipe {|r, w| with_reopen(STDERR, w) {|old| w.close - Open3.popen3(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDERR.print s+"e"', STDERR=>nil) {|i,o,t| + Open3.popen2e(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDOUT.flush; STDERR.print s+"e"') {|i,o,t| assert_kind_of(Thread, t) i.print "y" i.close STDERR.reopen(old) - assert_equal("yo", o.read) - assert_equal("ye", r.read) + assert_equal("yoye", o.read) + assert_equal("", r.read) } } } end - def test_plug_pipe - Open3.popen3(RUBY, '-e', 'STDOUT.print "1"') {|i1,o1,e1,t1| - Open3.popen3(RUBY, '-e', 'STDOUT.print STDIN.read+"2"', STDIN=>o1) {|o2,e2,t2| - assert_equal("12", o2.read) + def test_pipeline_rw + Open3.pipeline_rw([RUBY, '-e', 'print STDIN.read + "1"'], + [RUBY, '-e', 'print STDIN.read + "2"']) {|i,o,ts| + assert_kind_of(IO, i) + assert_kind_of(IO, o) + assert_kind_of(Array, ts) + assert_equal(2, ts.length) + ts.each {|t| assert_kind_of(Thread, t) } + i.print "0" + i.close + assert_equal("012", o.read) + ts.each {|t| + assert(t.value.success?) } } end - def test_tree_pipe - ia,oa,ea,ta = Open3.popen3(RUBY, '-e', 'i=STDIN.read; STDOUT.print i+"a"; STDERR.print i+"A"') - ob,eb,tb = Open3.popen3(RUBY, '-e', 'i=STDIN.read; STDOUT.print i+"b"; STDERR.print i+"B"', STDIN=>oa) - oc,ec,tc = Open3.popen3(RUBY, '-e', 'i=STDIN.read; STDOUT.print i+"c"; STDERR.print i+"C"', STDIN=>ob) - od,ed,td = Open3.popen3(RUBY, '-e', 'i=STDIN.read; STDOUT.print i+"d"; STDERR.print i+"D"', STDIN=>eb) - oe,ee,te = Open3.popen3(RUBY, '-e', 'i=STDIN.read; STDOUT.print i+"e"; STDERR.print i+"E"', STDIN=>ea) - of,ef,tf = Open3.popen3(RUBY, '-e', 'i=STDIN.read; STDOUT.print i+"f"; STDERR.print i+"F"', STDIN=>oe) - og,eg,tg = Open3.popen3(RUBY, '-e', 'i=STDIN.read; STDOUT.print i+"g"; STDERR.print i+"G"', STDIN=>ee) - oa.close - ea.close - ob.close - eb.close - oe.close - ee.close - - ia.print "0" - ia.close - assert_equal("0abc", oc.read) - assert_equal("0abC", ec.read) - assert_equal("0aBd", od.read) - assert_equal("0aBD", ed.read) - assert_equal("0Aef", of.read) - assert_equal("0AeF", ef.read) - assert_equal("0AEg", og.read) - assert_equal("0AEG", eg.read) - ensure - ia.close if !ia.closed? - oa.close if !oa.closed? - ea.close if !ea.closed? - ob.close if !ob.closed? - eb.close if !eb.closed? - oc.close if !oc.closed? - ec.close if !ec.closed? - od.close if !od.closed? - ed.close if !ed.closed? - oe.close if !oe.closed? - ee.close if !ee.closed? - of.close if !of.closed? - ef.close if !ef.closed? - og.close if !og.closed? - eg.close if !eg.closed? + def test_pipeline_r + Open3.pipeline_r([RUBY, '-e', 'print "1"'], + [RUBY, '-e', 'print STDIN.read + "2"']) {|o,ts| + assert_kind_of(IO, o) + assert_kind_of(Array, ts) + assert_equal(2, ts.length) + ts.each {|t| assert_kind_of(Thread, t) } + assert_equal("12", o.read) + ts.each {|t| + assert(t.value.success?) + } + } end + + def test_pipeline_w + command = [RUBY, '-e', 's=STDIN.read; print s[1..-1]; exit s[0] == ?t'] + str = 'ttftff' + Open3.pipeline_w(*[command]*str.length) {|i,ts| + assert_kind_of(IO, i) + assert_kind_of(Array, ts) + assert_equal(str.length, ts.length) + ts.each {|t| assert_kind_of(Thread, t) } + i.print str + i.close + ts.each_with_index {|t, i| + assert_equal(str[i] == ?t, t.value.success?) + } + } + end + end -- cgit v1.2.3