From 852b4629c16a2a568c075d0645fee58b95788610 Mon Sep 17 00:00:00 2001 From: akr Date: Tue, 2 Dec 2008 10:23:48 +0000 Subject: [PATCH] * lib/open3.rb (Open3.popen3): merge hash options if given. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@20448 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 4 + lib/open3.rb | 108 +++++++++++++++++++----- test/test_open3.rb | 205 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 19 deletions(-) create mode 100644 test/test_open3.rb diff --git a/ChangeLog b/ChangeLog index 143335fed6..319e9dfdc7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +Tue Dec 2 19:22:13 2008 Tanaka Akira + + * lib/open3.rb (Open3.popen3): merge hash options if given. + Tue Dec 2 15:31:42 2008 Yukihiro Matsumoto * lib/net/protocol.rb (Net::BufferedIO#rbuf_fill): use diff --git a/lib/open3.rb b/lib/open3.rb index d776de7445..454cd44ee4 100644 --- a/lib/open3.rb +++ b/lib/open3.rb @@ -32,15 +32,23 @@ # module Open3 - # + # Open stdin, stdout, and stderr streams and start external executable. # In addition, a thread for waiting the started process is noticed. - # The thread has a thread variable :pid which is the pid of the started - # process. + # The thread has a pid method and thread variable :pid which is the pid of + # the started process. + # + # Block form: + # + # Open3.popen3(cmd... [, opts]) {|stdin, stdout, 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, stderr, wait_thr = Open3.popen3(cmd) + # 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. @@ -48,11 +56,43 @@ module Open3 # stderr.close # exit_status = wait_thr.value # Process::Status object returned. # - # Block form: + # The parameters +cmd...+ is passed to Kernel#spawn. + # So a commandline string and list of argument strings can be accepted as follows. # - # Open3.popen3(cmd) { |stdin, stdout, stderr, wait_thr| ... } + # Open3.popen3("echo a") {|i, o, e, t| ... } + # Open3.popen3("echo", "a") {|i, o, e, t| ... } # - # The parameter +cmd+ is passed directly to Kernel#spawn. + # If the last parameter, opts, is a Hash, it is recognized as an option for Kernel#spawn. + # + # Open3.popen3("pwd", :chdir=>"/") {|i,o,e,t| + # 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. + # + # # No pipe "e" for stderr + # Open3.popen3("echo a", STDERR=>nil) {|i,o,t| ... } + # i,o,t = Open3.popen3("echo a", STDERR=>nil) + # + # If the value is nil as above, the elements of opts are removed. + # So standard input/output/error of current process are inherited. + # + # If the value is not nil, it is passed as is to Kernel#spawn. + # So pipeline of commands can be constracted as follows. + # + # 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 #=> # + # } + # } # # wait_thr.value waits the termination of the process. # The block form also waits the process when it returns. @@ -60,26 +100,56 @@ module Open3 # Closing stdin, stdout and stderr does not wait the process. # def popen3(*cmd) - pw = IO::pipe # pipe[0] for read, pipe[1] for write - pr = IO::pipe - pe = IO::pipe + if Hash === cmd.last + opts = cmd.pop.dup + else + opts = {} + end - pid = spawn(*cmd, STDIN=>pw[0], STDOUT=>pr[1], STDERR=>pe[1]) + 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 + + 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 + + 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) + end + + pid = spawn(*cmd, opts) wait_thr = Process.detach(pid) - pw[0].close - pr[1].close - pe[1].close - pi = [pw[1], pr[0], pe[0], wait_thr] - pw[1].sync = true + child_io.each {|io| io.close } + result = [*parent_io, wait_thr] if defined? yield begin - return yield(*pi) + return yield(*result) ensure - [pw[1], pr[0], pe[0]].each{|p| p.close unless p.closed?} + parent_io.each{|io| io.close unless io.closed?} wait_thr.join end end - pi + result end module_function :popen3 end diff --git a/test/test_open3.rb b/test/test_open3.rb new file mode 100644 index 0000000000..a484d4fb9b --- /dev/null +++ b/test/test_open3.rb @@ -0,0 +1,205 @@ +require 'test/unit' +require 'open3' +require 'shellwords' +require_relative 'ruby/envutil' + +class TestOpen3 < Test::Unit::TestCase + RUBY = EnvUtil.rubybin + + def test_exit_status + Open3.popen3(RUBY, '-e', 'exit true') {|i,o,e,t| + assert_equal(true, t.value.success?) + } + Open3.popen3(RUBY, '-e', 'exit false') {|i,o,e,t| + assert_equal(false, t.value.success?) + } + end + + def test_stdin + Open3.popen3(RUBY, '-e', 'exit STDIN.gets.chomp == "t"') {|i,o,e,t| + i.puts 't' + assert_equal(true, t.value.success?) + } + Open3.popen3(RUBY, '-e', 'exit STDIN.gets.chomp == "t"') {|i,o,e,t| + i.puts 'f' + assert_equal(false, t.value.success?) + } + end + + def test_stdout + Open3.popen3(RUBY, '-e', 'STDOUT.print "foo"') {|i,o,e,t| + assert_equal("foo", o.read) + } + end + + def test_stderr + Open3.popen3(RUBY, '-e', 'STDERR.print "bar"') {|i,o,e,t| + assert_equal("bar", e.read) + } + end + + def test_block + r = Open3.popen3(RUBY, '-e', 'STDOUT.print STDIN.read') {|i,o,e,t| + i.print "baz" + i.close + assert_equal("baz", o.read) + "qux" + } + assert_equal("qux", r) + end + + def test_noblock + i,o,e,t = Open3.popen3(RUBY, '-e', 'STDOUT.print STDIN.read') + i.print "baz" + i.close + assert_equal("baz", o.read) + ensure + i.close if !i.closed? + o.close if !o.closed? + e.close if !e.closed? + end + + def test_commandline + commandline = Shellwords.join([RUBY, '-e', 'print "quux"']) + Open3.popen3(commandline) {|i,o,e,t| + assert_equal("quux", o.read) + } + end + + def test_pid + Open3.popen3(RUBY, '-e', 'print $$') {|i,o,e,t| + pid = o.read.to_i + assert_equal(pid, t[:pid]) + assert_equal(pid, t.pid) + } + 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 + ensure + r.close if !r.closed? + w.close if !w.closed? + end + + def with_reopen(io, arg) + old = io.dup + io.reopen(arg) + yield old + ensure + io.reopen(old) + 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 + with_pipe {|r, w| + with_reopen(STDOUT, w) {|old| + w.close + Open3.popen3(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDERR.print s+"e"', STDOUT=>nil) {|i,e,t| + assert_kind_of(Thread, t) + i.print "y" + i.close + STDOUT.reopen(old) + assert_equal("yo", r.read) + assert_equal("ye", e.read) + } + } + } + end + + def test_disable_stderr + 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| + assert_kind_of(Thread, t) + i.print "y" + i.close + STDERR.reopen(old) + assert_equal("yo", o.read) + assert_equal("ye", 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) + } + } + 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? + end +end