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