1#
2# = open3.rb: Popen, but with stderr, too
3#
4# Author:: Yukihiro Matsumoto
5# Documentation:: Konrad Meyer
6#
7# Open3 gives you access to stdin, stdout, and stderr when running other
8# programs.
9#
10
11#
12# Open3 grants you access to stdin, stdout, stderr and a thread to wait the
13# child process when running another program.
14# You can specify various attributes, redirections, current directory, etc., of
15# the program as Process.spawn.
16#
17# - Open3.popen3 : pipes for stdin, stdout, stderr
18# - Open3.popen2 : pipes for stdin, stdout
19# - Open3.popen2e : pipes for stdin, merged stdout and stderr
20# - Open3.capture3 : give a string for stdin.  get strings for stdout, stderr
21# - Open3.capture2 : give a string for stdin.  get a string for stdout
22# - Open3.capture2e : give a string for stdin.  get a string for merged stdout and stderr
23# - Open3.pipeline_rw : pipes for first stdin and last stdout of a pipeline
24# - Open3.pipeline_r : pipe for last stdout of a pipeline
25# - Open3.pipeline_w : pipe for first stdin of a pipeline
26# - Open3.pipeline_start : run a pipeline and don't wait
27# - Open3.pipeline : run a pipeline and wait
28#
29
30module Open3
31
32  # Open stdin, stdout, and stderr streams and start external executable.
33  # In addition, a thread for waiting the started process is noticed.
34  # The thread has a pid method and thread variable :pid which is the pid of
35  # the started process.
36  #
37  # Block form:
38  #
39  #   Open3.popen3([env,] cmd... [, opts]) {|stdin, stdout, stderr, wait_thr|
40  #     pid = wait_thr.pid # pid of the started process.
41  #     ...
42  #     exit_status = wait_thr.value # Process::Status object returned.
43  #   }
44  #
45  # Non-block form:
46  #
47  #   stdin, stdout, stderr, wait_thr = Open3.popen3([env,] cmd... [, opts])
48  #   pid = wait_thr[:pid]  # pid of the started process.
49  #   ...
50  #   stdin.close  # stdin, stdout and stderr should be closed explicitly in this form.
51  #   stdout.close
52  #   stderr.close
53  #   exit_status = wait_thr.value  # Process::Status object returned.
54  #
55  # The parameters +cmd...+ is passed to Process.spawn.
56  # So a commandline string and list of argument strings can be accepted as follows.
57  #
58  #   Open3.popen3("echo abc") {|i, o, e, t| ... }
59  #   Open3.popen3("echo", "abc") {|i, o, e, t| ... }
60  #   Open3.popen3(["echo", "argv0"], "abc") {|i, o, e, t| ... }
61  #
62  # If the last parameter, opts, is a Hash, it is recognized as an option for Process.spawn.
63  #
64  #   Open3.popen3("pwd", :chdir=>"/") {|i,o,e,t|
65  #     p o.read.chomp #=> "/"
66  #   }
67  #
68  # wait_thr.value waits the termination of the process.
69  # The block form also waits the process when it returns.
70  #
71  # Closing stdin, stdout and stderr does not wait the process.
72  #
73  # You should be careful to avoid deadlocks.
74  # Since pipes are fixed length buffer,
75  # Open3.popen3("prog") {|i, o, e, t| o.read } deadlocks if
76  # the program generates many output on stderr.
77  # You should be read stdout and stderr simultaneously (using thread or IO.select).
78  # However if you don't need stderr output, Open3.popen2 can be used.
79  # If merged stdout and stderr output is not a problem, you can use Open3.popen2e.
80  # If you really needs stdout and stderr output as separate strings, you can consider Open3.capture3.
81  #
82  def popen3(*cmd, &block)
83    if Hash === cmd.last
84      opts = cmd.pop.dup
85    else
86      opts = {}
87    end
88
89    in_r, in_w = IO.pipe
90    opts[:in] = in_r
91    in_w.sync = true
92
93    out_r, out_w = IO.pipe
94    opts[:out] = out_w
95
96    err_r, err_w = IO.pipe
97    opts[:err] = err_w
98
99    popen_run(cmd, opts, [in_r, out_w, err_w], [in_w, out_r, err_r], &block)
100  end
101  module_function :popen3
102
103  # Open3.popen2 is similer to Open3.popen3 except it doesn't make a pipe for
104  # the standard error stream.
105  #
106  # Block form:
107  #
108  #   Open3.popen2([env,] cmd... [, opts]) {|stdin, stdout, wait_thr|
109  #     pid = wait_thr.pid # pid of the started process.
110  #     ...
111  #     exit_status = wait_thr.value # Process::Status object returned.
112  #   }
113  #
114  # Non-block form:
115  #
116  #   stdin, stdout, wait_thr = Open3.popen2([env,] cmd... [, opts])
117  #   ...
118  #   stdin.close  # stdin and stdout should be closed explicitly in this form.
119  #   stdout.close
120  #
121  # See Process.spawn for the optional hash arguments _env_ and _opts_.
122  #
123  # Example:
124  #
125  #   Open3.popen2("wc -c") {|i,o,t|
126  #     i.print "answer to life the universe and everything"
127  #     i.close
128  #     p o.gets #=> "42\n"
129  #   }
130  #
131  #   Open3.popen2("bc -q") {|i,o,t|
132  #     i.puts "obase=13"
133  #     i.puts "6 * 9"
134  #     p o.gets #=> "42\n"
135  #   }
136  #
137  #   Open3.popen2("dc") {|i,o,t|
138  #     i.print "42P"
139  #     i.close
140  #     p o.read #=> "*"
141  #   }
142  #
143  def popen2(*cmd, &block)
144    if Hash === cmd.last
145      opts = cmd.pop.dup
146    else
147      opts = {}
148    end
149
150    in_r, in_w = IO.pipe
151    opts[:in] = in_r
152    in_w.sync = true
153
154    out_r, out_w = IO.pipe
155    opts[:out] = out_w
156
157    popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
158  end
159  module_function :popen2
160
161  # Open3.popen2e is similer to Open3.popen3 except it merges
162  # the standard output stream and the standard error stream.
163  #
164  # Block form:
165  #
166  #   Open3.popen2e([env,] cmd... [, opts]) {|stdin, stdout_and_stderr, wait_thr|
167  #     pid = wait_thr.pid # pid of the started process.
168  #     ...
169  #     exit_status = wait_thr.value # Process::Status object returned.
170  #   }
171  #
172  # Non-block form:
173  #
174  #   stdin, stdout_and_stderr, wait_thr = Open3.popen2e([env,] cmd... [, opts])
175  #   ...
176  #   stdin.close  # stdin and stdout_and_stderr should be closed explicitly in this form.
177  #   stdout_and_stderr.close
178  #
179  # See Process.spawn for the optional hash arguments _env_ and _opts_.
180  #
181  # Example:
182  #   # check gcc warnings
183  #   source = "foo.c"
184  #   Open3.popen2e("gcc", "-Wall", source) {|i,oe,t|
185  #     oe.each {|line|
186  #       if /warning/ =~ line
187  #         ...
188  #       end
189  #     }
190  #   }
191  #
192  def popen2e(*cmd, &block)
193    if Hash === cmd.last
194      opts = cmd.pop.dup
195    else
196      opts = {}
197    end
198
199    in_r, in_w = IO.pipe
200    opts[:in] = in_r
201    in_w.sync = true
202
203    out_r, out_w = IO.pipe
204    opts[[:out, :err]] = out_w
205
206    popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
207  end
208  module_function :popen2e
209
210  def popen_run(cmd, opts, child_io, parent_io) # :nodoc:
211    pid = spawn(*cmd, opts)
212    wait_thr = Process.detach(pid)
213    child_io.each {|io| io.close }
214    result = [*parent_io, wait_thr]
215    if defined? yield
216      begin
217        return yield(*result)
218      ensure
219        parent_io.each{|io| io.close unless io.closed?}
220        wait_thr.join
221      end
222    end
223    result
224  end
225  module_function :popen_run
226  class << self
227    private :popen_run
228  end
229
230  # Open3.capture3 captures the standard output and the standard error of a command.
231  #
232  #   stdout_str, stderr_str, status = Open3.capture3([env,] cmd... [, opts])
233  #
234  # The arguments env, cmd and opts are passed to Open3.popen3 except
235  # opts[:stdin_data] and opts[:binmode].  See Process.spawn.
236  #
237  # If opts[:stdin_data] is specified, it is sent to the command's standard input.
238  #
239  # If opts[:binmode] is true, internal pipes are set to binary mode.
240  #
241  # Example:
242  #
243  #   # dot is a command of graphviz.
244  #   graph = <<'End'
245  #     digraph g {
246  #       a -> b
247  #     }
248  #   End
249  #   layouted_graph, dot_log = Open3.capture3("dot -v", :stdin_data=>graph)
250  #
251  #   o, e, s = Open3.capture3("echo abc; sort >&2", :stdin_data=>"foo\nbar\nbaz\n")
252  #   p o #=> "abc\n"
253  #   p e #=> "bar\nbaz\nfoo\n"
254  #   p s #=> #<Process::Status: pid 32682 exit 0>
255  #
256  #   # generate a thumnail image using the convert command of ImageMagick.
257  #   # However, if the image stored really in a file,
258  #   # system("convert", "-thumbnail", "80", "png:#{filename}", "png:-") is better
259  #   # because memory consumption.
260  #   # But if the image is stored in a DB or generated by gnuplot Open3.capture2 example,
261  #   # Open3.capture3 is considerable.
262  #   #
263  #   image = File.read("/usr/share/openclipart/png/animals/mammals/sheep-md-v0.1.png", :binmode=>true)
264  #   thumnail, err, s = Open3.capture3("convert -thumbnail 80 png:- png:-", :stdin_data=>image, :binmode=>true)
265  #   if s.success?
266  #     STDOUT.binmode; print thumnail
267  #   end
268  #
269  def capture3(*cmd)
270    if Hash === cmd.last
271      opts = cmd.pop.dup
272    else
273      opts = {}
274    end
275
276    stdin_data = opts.delete(:stdin_data) || ''
277    binmode = opts.delete(:binmode)
278
279    popen3(*cmd, opts) {|i, o, e, t|
280      if binmode
281        i.binmode
282        o.binmode
283        e.binmode
284      end
285      out_reader = Thread.new { o.read }
286      err_reader = Thread.new { e.read }
287      i.write stdin_data
288      i.close
289      [out_reader.value, err_reader.value, t.value]
290    }
291  end
292  module_function :capture3
293
294  # Open3.capture2 captures the standard output of a command.
295  #
296  #   stdout_str, status = Open3.capture2([env,] cmd... [, opts])
297  #
298  # The arguments env, cmd and opts are passed to Open3.popen3 except
299  # opts[:stdin_data] and opts[:binmode].  See Process.spawn.
300  #
301  # If opts[:stdin_data] is specified, it is sent to the command's standard input.
302  #
303  # If opts[:binmode] is true, internal pipes are set to binary mode.
304  #
305  # Example:
306  #
307  #   # factor is a command for integer factorization.
308  #   o, s = Open3.capture2("factor", :stdin_data=>"42")
309  #   p o #=> "42: 2 3 7\n"
310  #
311  #   # generate x**2 graph in png using gnuplot.
312  #   gnuplot_commands = <<"End"
313  #     set terminal png
314  #     plot x**2, "-" with lines
315  #     1 14
316  #     2 1
317  #     3 8
318  #     4 5
319  #     e
320  #   End
321  #   image, s = Open3.capture2("gnuplot", :stdin_data=>gnuplot_commands, :binmode=>true)
322  #
323  def capture2(*cmd)
324    if Hash === cmd.last
325      opts = cmd.pop.dup
326    else
327      opts = {}
328    end
329
330    stdin_data = opts.delete(:stdin_data) || ''
331    binmode = opts.delete(:binmode)
332
333    popen2(*cmd, opts) {|i, o, t|
334      if binmode
335        i.binmode
336        o.binmode
337      end
338      out_reader = Thread.new { o.read }
339      i.write stdin_data
340      i.close
341      [out_reader.value, t.value]
342    }
343  end
344  module_function :capture2
345
346  # Open3.capture2e captures the standard output and the standard error of a command.
347  #
348  #   stdout_and_stderr_str, status = Open3.capture2e([env,] cmd... [, opts])
349  #
350  # The arguments env, cmd and opts are passed to Open3.popen3 except
351  # opts[:stdin_data] and opts[:binmode].  See Process.spawn.
352  #
353  # If opts[:stdin_data] is specified, it is sent to the command's standard input.
354  #
355  # If opts[:binmode] is true, internal pipes are set to binary mode.
356  #
357  # Example:
358  #
359  #   # capture make log
360  #   make_log, s = Open3.capture2e("make")
361  #
362  def capture2e(*cmd)
363    if Hash === cmd.last
364      opts = cmd.pop.dup
365    else
366      opts = {}
367    end
368
369    stdin_data = opts.delete(:stdin_data) || ''
370    binmode = opts.delete(:binmode)
371
372    popen2e(*cmd, opts) {|i, oe, t|
373      if binmode
374        i.binmode
375        oe.binmode
376      end
377      outerr_reader = Thread.new { oe.read }
378      i.write stdin_data
379      i.close
380      [outerr_reader.value, t.value]
381    }
382  end
383  module_function :capture2e
384
385  # Open3.pipeline_rw starts a list of commands as a pipeline with pipes
386  # which connects stdin of the first command and stdout of the last command.
387  #
388  #   Open3.pipeline_rw(cmd1, cmd2, ... [, opts]) {|first_stdin, last_stdout, wait_threads|
389  #     ...
390  #   }
391  #
392  #   first_stdin, last_stdout, wait_threads = Open3.pipeline_rw(cmd1, cmd2, ... [, opts])
393  #   ...
394  #   first_stdin.close
395  #   last_stdout.close
396  #
397  # Each cmd is a string or an array.
398  # If it is an array, the elements are passed to Process.spawn.
399  #
400  #   cmd:
401  #     commandline                              command line string which is passed to a shell
402  #     [env, commandline, opts]                 command line string which is passed to a shell
403  #     [env, cmdname, arg1, ..., opts]          command name and one or more arguments (no shell)
404  #     [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
405  #
406  #   Note that env and opts are optional, as Process.spawn.
407  #
408  # The option to pass Process.spawn is constructed by merging
409  # +opts+, the last hash element of the array and
410  # specification for the pipe between each commands.
411  #
412  # Example:
413  #
414  #   Open3.pipeline_rw("tr -dc A-Za-z", "wc -c") {|i,o,ts|
415  #     i.puts "All persons more than a mile high to leave the court."
416  #     i.close
417  #     p o.gets #=> "42\n"
418  #   }
419  #
420  #   Open3.pipeline_rw("sort", "cat -n") {|stdin, stdout, wait_thrs|
421  #     stdin.puts "foo"
422  #     stdin.puts "bar"
423  #     stdin.puts "baz"
424  #     stdin.close     # send EOF to sort.
425  #     p stdout.read   #=> "     1\tbar\n     2\tbaz\n     3\tfoo\n"
426  #   }
427  def pipeline_rw(*cmds, &block)
428    if Hash === cmds.last
429      opts = cmds.pop.dup
430    else
431      opts = {}
432    end
433
434    in_r, in_w = IO.pipe
435    opts[:in] = in_r
436    in_w.sync = true
437
438    out_r, out_w = IO.pipe
439    opts[:out] = out_w
440
441    pipeline_run(cmds, opts, [in_r, out_w], [in_w, out_r], &block)
442  end
443  module_function :pipeline_rw
444
445  # Open3.pipeline_r starts a list of commands as a pipeline with a pipe
446  # which connects stdout of the last command.
447  #
448  #   Open3.pipeline_r(cmd1, cmd2, ... [, opts]) {|last_stdout, wait_threads|
449  #     ...
450  #   }
451  #
452  #   last_stdout, wait_threads = Open3.pipeline_r(cmd1, cmd2, ... [, opts])
453  #   ...
454  #   last_stdout.close
455  #
456  # Each cmd is a string or an array.
457  # If it is an array, the elements are passed to Process.spawn.
458  #
459  #   cmd:
460  #     commandline                              command line string which is passed to a shell
461  #     [env, commandline, opts]                 command line string which is passed to a shell
462  #     [env, cmdname, arg1, ..., opts]          command name and one or more arguments (no shell)
463  #     [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
464  #
465  #   Note that env and opts are optional, as Process.spawn.
466  #
467  # Example:
468  #
469  #   Open3.pipeline_r("zcat /var/log/apache2/access.log.*.gz",
470  #                    [{"LANG"=>"C"}, "grep", "GET /favicon.ico"],
471  #                    "logresolve") {|o, ts|
472  #     o.each_line {|line|
473  #       ...
474  #     }
475  #   }
476  #
477  #   Open3.pipeline_r("yes", "head -10") {|o, ts|
478  #     p o.read      #=> "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"
479  #     p ts[0].value #=> #<Process::Status: pid 24910 SIGPIPE (signal 13)>
480  #     p ts[1].value #=> #<Process::Status: pid 24913 exit 0>
481  #   }
482  #
483  def pipeline_r(*cmds, &block)
484    if Hash === cmds.last
485      opts = cmds.pop.dup
486    else
487      opts = {}
488    end
489
490    out_r, out_w = IO.pipe
491    opts[:out] = out_w
492
493    pipeline_run(cmds, opts, [out_w], [out_r], &block)
494  end
495  module_function :pipeline_r
496
497  # Open3.pipeline_w starts a list of commands as a pipeline with a pipe
498  # which connects stdin of the first command.
499  #
500  #   Open3.pipeline_w(cmd1, cmd2, ... [, opts]) {|first_stdin, wait_threads|
501  #     ...
502  #   }
503  #
504  #   first_stdin, wait_threads = Open3.pipeline_w(cmd1, cmd2, ... [, opts])
505  #   ...
506  #   first_stdin.close
507  #
508  # Each cmd is a string or an array.
509  # If it is an array, the elements are passed to Process.spawn.
510  #
511  #   cmd:
512  #     commandline                              command line string which is passed to a shell
513  #     [env, commandline, opts]                 command line string which is passed to a shell
514  #     [env, cmdname, arg1, ..., opts]          command name and one or more arguments (no shell)
515  #     [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
516  #
517  #   Note that env and opts are optional, as Process.spawn.
518  #
519  # Example:
520  #
521  #   Open3.pipeline_w("bzip2 -c", :out=>"/tmp/hello.bz2") {|i, ts|
522  #     i.puts "hello"
523  #   }
524  #
525  def pipeline_w(*cmds, &block)
526    if Hash === cmds.last
527      opts = cmds.pop.dup
528    else
529      opts = {}
530    end
531
532    in_r, in_w = IO.pipe
533    opts[:in] = in_r
534    in_w.sync = true
535
536    pipeline_run(cmds, opts, [in_r], [in_w], &block)
537  end
538  module_function :pipeline_w
539
540  # Open3.pipeline_start starts a list of commands as a pipeline.
541  # No pipe made for stdin of the first command and
542  # stdout of the last command.
543  #
544  #   Open3.pipeline_start(cmd1, cmd2, ... [, opts]) {|wait_threads|
545  #     ...
546  #   }
547  #
548  #   wait_threads = Open3.pipeline_start(cmd1, cmd2, ... [, opts])
549  #   ...
550  #
551  # Each cmd is a string or an array.
552  # If it is an array, the elements are passed to Process.spawn.
553  #
554  #   cmd:
555  #     commandline                              command line string which is passed to a shell
556  #     [env, commandline, opts]                 command line string which is passed to a shell
557  #     [env, cmdname, arg1, ..., opts]          command name and one or more arguments (no shell)
558  #     [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
559  #
560  #   Note that env and opts are optional, as Process.spawn.
561  #
562  # Example:
563  #
564  #   # run xeyes in 10 seconds.
565  #   Open3.pipeline_start("xeyes") {|ts|
566  #     sleep 10
567  #     t = ts[0]
568  #     Process.kill("TERM", t.pid)
569  #     p t.value #=> #<Process::Status: pid 911 SIGTERM (signal 15)>
570  #   }
571  #
572  #   # convert pdf to ps and send it to a printer.
573  #   # collect error message of pdftops and lpr.
574  #   pdf_file = "paper.pdf"
575  #   printer = "printer-name"
576  #   err_r, err_w = IO.pipe
577  #   Open3.pipeline_start(["pdftops", pdf_file, "-"],
578  #                        ["lpr", "-P#{printer}"],
579  #                        :err=>err_w) {|ts|
580  #     err_w.close
581  #     p err_r.read # error messages of pdftops and lpr.
582  #   }
583  #
584  def pipeline_start(*cmds, &block)
585    if Hash === cmds.last
586      opts = cmds.pop.dup
587    else
588      opts = {}
589    end
590
591    if block
592      pipeline_run(cmds, opts, [], [], &block)
593    else
594      ts, = pipeline_run(cmds, opts, [], [])
595      ts
596    end
597  end
598  module_function :pipeline_start
599
600  # Open3.pipeline starts a list of commands as a pipeline.
601  # It waits the finish of the commands.
602  # No pipe made for stdin of the first command and
603  # stdout of the last command.
604  #
605  #   status_list = Open3.pipeline(cmd1, cmd2, ... [, opts])
606  #
607  # Each cmd is a string or an array.
608  # If it is an array, the elements are passed to Process.spawn.
609  #
610  #   cmd:
611  #     commandline                              command line string which is passed to a shell
612  #     [env, commandline, opts]                 command line string which is passed to a shell
613  #     [env, cmdname, arg1, ..., opts]          command name and one or more arguments (no shell)
614  #     [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
615  #
616  #   Note that env and opts are optional, as Process.spawn.
617  #
618  # Example:
619  #
620  #   fname = "/usr/share/man/man1/ruby.1.gz"
621  #   p Open3.pipeline(["zcat", fname], "nroff -man", "less")
622  #   #=> [#<Process::Status: pid 11817 exit 0>,
623  #   #    #<Process::Status: pid 11820 exit 0>,
624  #   #    #<Process::Status: pid 11828 exit 0>]
625  #
626  #   fname = "/usr/share/man/man1/ls.1.gz"
627  #   Open3.pipeline(["zcat", fname], "nroff -man", "colcrt")
628  #
629  #   # convert PDF to PS and send to a printer by lpr
630  #   pdf_file = "paper.pdf"
631  #   printer = "printer-name"
632  #   Open3.pipeline(["pdftops", pdf_file, "-"],
633  #                  ["lpr", "-P#{printer}"])
634  #
635  #   # count lines
636  #   Open3.pipeline("sort", "uniq -c", :in=>"names.txt", :out=>"count")
637  #
638  #   # cyclic pipeline
639  #   r,w = IO.pipe
640  #   w.print "ibase=14\n10\n"
641  #   Open3.pipeline("bc", "tee /dev/tty", :in=>r, :out=>w)
642  #   #=> 14
643  #   #   18
644  #   #   22
645  #   #   30
646  #   #   42
647  #   #   58
648  #   #   78
649  #   #   106
650  #   #   202
651  #
652  def pipeline(*cmds)
653    if Hash === cmds.last
654      opts = cmds.pop.dup
655    else
656      opts = {}
657    end
658
659    pipeline_run(cmds, opts, [], []) {|ts|
660      ts.map {|t| t.value }
661    }
662  end
663  module_function :pipeline
664
665  def pipeline_run(cmds, pipeline_opts, child_io, parent_io) # :nodoc:
666    if cmds.empty?
667      raise ArgumentError, "no commands"
668    end
669
670    opts_base = pipeline_opts.dup
671    opts_base.delete :in
672    opts_base.delete :out
673
674    wait_thrs = []
675    r = nil
676    cmds.each_with_index {|cmd, i|
677      cmd_opts = opts_base.dup
678      if String === cmd
679        cmd = [cmd]
680      else
681        cmd_opts.update cmd.pop if Hash === cmd.last
682      end
683      if i == 0
684        if !cmd_opts.include?(:in)
685          if pipeline_opts.include?(:in)
686            cmd_opts[:in] = pipeline_opts[:in]
687          end
688        end
689      else
690        cmd_opts[:in] = r
691      end
692      if i != cmds.length - 1
693        r2, w2 = IO.pipe
694        cmd_opts[:out] = w2
695      else
696        if !cmd_opts.include?(:out)
697          if pipeline_opts.include?(:out)
698            cmd_opts[:out] = pipeline_opts[:out]
699          end
700        end
701      end
702      pid = spawn(*cmd, cmd_opts)
703      wait_thrs << Process.detach(pid)
704      r.close if r
705      w2.close if w2
706      r = r2
707    }
708    result = parent_io + [wait_thrs]
709    child_io.each {|io| io.close }
710    if defined? yield
711      begin
712        return yield(*result)
713      ensure
714        parent_io.each{|io| io.close unless io.closed?}
715        wait_thrs.each {|t| t.join }
716      end
717    end
718    result
719  end
720  module_function :pipeline_run
721  class << self
722    private :pipeline_run
723  end
724
725end
726
727if $0 == __FILE__
728  a = Open3.popen3("nroff -man")
729  Thread.start do
730    while line = gets
731      a[0].print line
732    end
733    a[0].close
734  end
735  while line = a[1].gets
736    print ":", line
737  end
738end
739