├── .github ├── dependabot.yml └── workflows │ ├── test-jruby.yml │ └── test.yml ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── open3.rb └── open3 │ ├── jruby_windows.rb │ └── version.rb ├── open3.gemspec └── test ├── lib └── helper.rb └── test_open3.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/test-jruby.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 8 | strategy: 9 | matrix: 10 | ruby: [ 2.7, 2.6, head, jruby-9.3 ] 11 | os: [ ubuntu-latest, macos-latest ] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | bundler-cache: true # 'bundle install' and cache 20 | - name: Run test 21 | run: bundle exec rake test 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 2.6 11 | 12 | test: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest ] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | bundler-cache: true # 'bundle install' and cache 27 | - name: Run test 28 | run: bundle exec rake test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | gem "test-unit-ruby-core" 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open3 2 | 3 | Open3 gives you access to stdin, stdout, and stderr when running other 4 | programs. 5 | 6 | ## Installation 7 | 8 | Add this line to your application's Gemfile: 9 | 10 | ```ruby 11 | gem 'open3' 12 | ``` 13 | 14 | And then execute: 15 | 16 | $ bundle 17 | 18 | Or install it yourself as: 19 | 20 | $ gem install open3 21 | 22 | ## Usage 23 | 24 | Open3 grants you access to stdin, stdout, stderr and a thread to wait for the child process when running another program. 25 | You can specify various attributes, redirections, current directory, etc., of the program in the same way as for Process.spawn. 26 | 27 | - Open3.popen3 : pipes for stdin, stdout, stderr 28 | - Open3.popen2 : pipes for stdin, stdout 29 | - Open3.popen2e : pipes for stdin, merged stdout and stderr 30 | - Open3.capture3 : give a string for stdin; get strings for stdout, stderr 31 | - Open3.capture2 : give a string for stdin; get a string for stdout 32 | - Open3.capture2e : give a string for stdin; get a string for merged stdout and stderr 33 | - Open3.pipeline_rw : pipes for first stdin and last stdout of a pipeline 34 | - Open3.pipeline_r : pipe for last stdout of a pipeline 35 | - Open3.pipeline_w : pipe for first stdin of a pipeline 36 | - Open3.pipeline_start : run a pipeline without waiting 37 | - Open3.pipeline : run a pipeline and wait for its completion 38 | 39 | ## Development 40 | 41 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 42 | 43 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 44 | 45 | ## Contributing 46 | 47 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/open3. 48 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "open3" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/open3.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # = open3.rb: Popen, but with stderr, too 5 | # 6 | # Author:: Yukihiro Matsumoto 7 | # Documentation:: Konrad Meyer 8 | # 9 | # Open3 gives you access to stdin, stdout, and stderr when running other 10 | # programs. 11 | # 12 | 13 | # 14 | # Open3 grants you access to stdin, stdout, stderr and a thread to wait for the 15 | # child process when running another program. 16 | # You can specify various attributes, redirections, current directory, etc., of 17 | # the program in the same way as for Process.spawn. 18 | # 19 | # - Open3.popen3 : pipes for stdin, stdout, stderr 20 | # - Open3.popen2 : pipes for stdin, stdout 21 | # - Open3.popen2e : pipes for stdin, merged stdout and stderr 22 | # - Open3.capture3 : give a string for stdin; get strings for stdout, stderr 23 | # - Open3.capture2 : give a string for stdin; get a string for stdout 24 | # - Open3.capture2e : give a string for stdin; get a string for merged stdout and stderr 25 | # - Open3.pipeline_rw : pipes for first stdin and last stdout of a pipeline 26 | # - Open3.pipeline_r : pipe for last stdout of a pipeline 27 | # - Open3.pipeline_w : pipe for first stdin of a pipeline 28 | # - Open3.pipeline_start : run a pipeline without waiting 29 | # - Open3.pipeline : run a pipeline and wait for its completion 30 | # 31 | 32 | require 'open3/version' 33 | 34 | # \Module \Open3 supports creating child processes 35 | # with access to their $stdin, $stdout, and $stderr streams. 36 | # 37 | # == What's Here 38 | # 39 | # Each of these methods executes a given command in a new process or subshell, 40 | # or multiple commands in new processes and/or subshells: 41 | # 42 | # - Each of these methods executes a single command in a process or subshell, 43 | # accepts a string for input to $stdin, 44 | # and returns string output from $stdout, $stderr, or both: 45 | # 46 | # - Open3.capture2: Executes the command; 47 | # returns the string from $stdout. 48 | # - Open3.capture2e: Executes the command; 49 | # returns the string from merged $stdout and $stderr. 50 | # - Open3.capture3: Executes the command; 51 | # returns strings from $stdout and $stderr. 52 | # 53 | # - Each of these methods executes a single command in a process or subshell, 54 | # and returns pipes for $stdin, $stdout, and/or $stderr: 55 | # 56 | # - Open3.popen2: Executes the command; 57 | # returns pipes for $stdin and $stdout. 58 | # - Open3.popen2e: Executes the command; 59 | # returns pipes for $stdin and merged $stdout and $stderr. 60 | # - Open3.popen3: Executes the command; 61 | # returns pipes for $stdin, $stdout, and $stderr. 62 | # 63 | # - Each of these methods executes one or more commands in processes and/or subshells, 64 | # returns pipes for the first $stdin, the last $stdout, or both: 65 | # 66 | # - Open3.pipeline_r: Returns a pipe for the last $stdout. 67 | # - Open3.pipeline_rw: Returns pipes for the first $stdin and the last $stdout. 68 | # - Open3.pipeline_w: Returns a pipe for the first $stdin. 69 | # - Open3.pipeline_start: Does not wait for processes to complete. 70 | # - Open3.pipeline: Waits for processes to complete. 71 | # 72 | # Each of the methods above accepts: 73 | # 74 | # - An optional hash of environment variable names and values; 75 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 76 | # - A required string argument that is a +command_line+ or +exe_path+; 77 | # see {Argument command_line or exe_path}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Argument+command_line+or+exe_path]. 78 | # - An optional hash of execution options; 79 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 80 | # 81 | module Open3 82 | 83 | # :call-seq: 84 | # Open3.popen3([env, ] command_line, options = {}) -> [stdin, stdout, stderr, wait_thread] 85 | # Open3.popen3([env, ] exe_path, *args, options = {}) -> [stdin, stdout, stderr, wait_thread] 86 | # Open3.popen3([env, ] command_line, options = {}) {|stdin, stdout, stderr, wait_thread| ... } -> object 87 | # Open3.popen3([env, ] exe_path, *args, options = {}) {|stdin, stdout, stderr, wait_thread| ... } -> object 88 | # 89 | # Basically a wrapper for 90 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 91 | # that: 92 | # 93 | # - Creates a child process, by calling Process.spawn with the given arguments. 94 | # - Creates streams +stdin+, +stdout+, and +stderr+, 95 | # which are the standard input, standard output, and standard error streams 96 | # in the child process. 97 | # - Creates thread +wait_thread+ that waits for the child process to exit; 98 | # the thread has method +pid+, which returns the process ID 99 | # of the child process. 100 | # 101 | # With no block given, returns the array 102 | # [stdin, stdout, stderr, wait_thread]. 103 | # The caller should close each of the three returned streams. 104 | # 105 | # stdin, stdout, stderr, wait_thread = Open3.popen3('echo') 106 | # # => [#, #, #, #] 107 | # stdin.close 108 | # stdout.close 109 | # stderr.close 110 | # wait_thread.pid # => 2210481 111 | # wait_thread.value # => # 112 | # 113 | # With a block given, calls the block with the four variables 114 | # (three streams and the wait thread) 115 | # and returns the block's return value. 116 | # The caller need not close the streams: 117 | # 118 | # Open3.popen3('echo') do |stdin, stdout, stderr, wait_thread| 119 | # p stdin 120 | # p stdout 121 | # p stderr 122 | # p wait_thread 123 | # p wait_thread.pid 124 | # p wait_thread.value 125 | # end 126 | # 127 | # Output: 128 | # 129 | # # 130 | # # 131 | # # 132 | # # 133 | # 2211047 134 | # # 135 | # 136 | # Like Process.spawn, this method has potential security vulnerabilities 137 | # if called with untrusted input; 138 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 139 | # 140 | # Unlike Process.spawn, this method waits for the child process to exit 141 | # before returning, so the caller need not do so. 142 | # 143 | # If the first argument is a hash, it becomes leading argument +env+ 144 | # in the call to Process.spawn; 145 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 146 | # 147 | # If the last argument is a hash, it becomes trailing argument +options+ 148 | # in the call to Process.spawn; 149 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 150 | # 151 | # The single required argument is one of the following: 152 | # 153 | # - +command_line+ if it is a string, 154 | # and if it begins with a shell reserved word or special built-in, 155 | # or if it contains one or more metacharacters. 156 | # - +exe_path+ otherwise. 157 | # 158 | # Argument +command_line+ 159 | # 160 | # \String argument +command_line+ is a command line to be passed to a shell; 161 | # it must begin with a shell reserved word, begin with a special built-in, 162 | # or contain meta characters: 163 | # 164 | # Open3.popen3('if true; then echo "Foo"; fi') {|*args| p args } # Shell reserved word. 165 | # Open3.popen3('echo') {|*args| p args } # Built-in. 166 | # Open3.popen3('date > date.tmp') {|*args| p args } # Contains meta character. 167 | # 168 | # Output (similar for each call above): 169 | # 170 | # [#, #, #, #] 171 | # 172 | # The command line may also contain arguments and options for the command: 173 | # 174 | # Open3.popen3('echo "Foo"') { |i, o, e, t| o.gets } 175 | # "Foo\n" 176 | # 177 | # Argument +exe_path+ 178 | # 179 | # Argument +exe_path+ is one of the following: 180 | # 181 | # - The string path to an executable to be called. 182 | # - A 2-element array containing the path to an executable 183 | # and the string to be used as the name of the executing process. 184 | # 185 | # Example: 186 | # 187 | # Open3.popen3('/usr/bin/date') { |i, o, e, t| o.gets } 188 | # # => "Wed Sep 27 02:56:44 PM CDT 2023\n" 189 | # 190 | # Ruby invokes the executable directly, with no shell and no shell expansion: 191 | # 192 | # Open3.popen3('doesnt_exist') { |i, o, e, t| o.gets } # Raises Errno::ENOENT 193 | # 194 | # If one or more +args+ is given, each is an argument or option 195 | # to be passed to the executable: 196 | # 197 | # Open3.popen3('echo', 'C #') { |i, o, e, t| o.gets } 198 | # # => "C #\n" 199 | # Open3.popen3('echo', 'hello', 'world') { |i, o, e, t| o.gets } 200 | # # => "hello world\n" 201 | # 202 | # Take care to avoid deadlocks. 203 | # Output streams +stdout+ and +stderr+ have fixed-size buffers, 204 | # so reading extensively from one but not the other can cause a deadlock 205 | # when the unread buffer fills. 206 | # To avoid that, +stdout+ and +stderr+ should be read simultaneously 207 | # (using threads or IO.select). 208 | # 209 | # Related: 210 | # 211 | # - Open3.popen2: Makes the standard input and standard output streams 212 | # of the child process available as separate streams, 213 | # with no access to the standard error stream. 214 | # - Open3.popen2e: Makes the standard input and the merge 215 | # of the standard output and standard error streams 216 | # of the child process available as separate streams. 217 | # 218 | def popen3(*cmd, &block) 219 | if Hash === cmd.last 220 | opts = cmd.pop.dup 221 | else 222 | opts = {} 223 | end 224 | 225 | in_r, in_w = IO.pipe 226 | opts[:in] = in_r 227 | in_w.sync = true 228 | 229 | out_r, out_w = IO.pipe 230 | opts[:out] = out_w 231 | 232 | err_r, err_w = IO.pipe 233 | opts[:err] = err_w 234 | 235 | popen_run(cmd, opts, [in_r, out_w, err_w], [in_w, out_r, err_r], &block) 236 | end 237 | module_function :popen3 238 | 239 | # :call-seq: 240 | # Open3.popen2([env, ] command_line, options = {}) -> [stdin, stdout, wait_thread] 241 | # Open3.popen2([env, ] exe_path, *args, options = {}) -> [stdin, stdout, wait_thread] 242 | # Open3.popen2([env, ] command_line, options = {}) {|stdin, stdout, wait_thread| ... } -> object 243 | # Open3.popen2([env, ] exe_path, *args, options = {}) {|stdin, stdout, wait_thread| ... } -> object 244 | # 245 | # Basically a wrapper for 246 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 247 | # that: 248 | # 249 | # - Creates a child process, by calling Process.spawn with the given arguments. 250 | # - Creates streams +stdin+ and +stdout+, 251 | # which are the standard input and standard output streams 252 | # in the child process. 253 | # - Creates thread +wait_thread+ that waits for the child process to exit; 254 | # the thread has method +pid+, which returns the process ID 255 | # of the child process. 256 | # 257 | # With no block given, returns the array 258 | # [stdin, stdout, wait_thread]. 259 | # The caller should close each of the two returned streams. 260 | # 261 | # stdin, stdout, wait_thread = Open3.popen2('echo') 262 | # # => [#, #, #] 263 | # stdin.close 264 | # stdout.close 265 | # wait_thread.pid # => 2263572 266 | # wait_thread.value # => # 267 | # 268 | # With a block given, calls the block with the three variables 269 | # (two streams and the wait thread) 270 | # and returns the block's return value. 271 | # The caller need not close the streams: 272 | # 273 | # Open3.popen2('echo') do |stdin, stdout, wait_thread| 274 | # p stdin 275 | # p stdout 276 | # p wait_thread 277 | # p wait_thread.pid 278 | # p wait_thread.value 279 | # end 280 | # 281 | # Output: 282 | # 283 | # # 284 | # # 285 | # # 286 | # 2263636 287 | # # 288 | # 289 | # Like Process.spawn, this method has potential security vulnerabilities 290 | # if called with untrusted input; 291 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 292 | # 293 | # Unlike Process.spawn, this method waits for the child process to exit 294 | # before returning, so the caller need not do so. 295 | # 296 | # If the first argument is a hash, it becomes leading argument +env+ 297 | # in the call to Process.spawn; 298 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 299 | # 300 | # If the last argument is a hash, it becomes trailing argument +options+ 301 | # in the call to Process.spawn; 302 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 303 | # 304 | # The single required argument is one of the following: 305 | # 306 | # - +command_line+ if it is a string, 307 | # and if it begins with a shell reserved word or special built-in, 308 | # or if it contains one or more metacharacters. 309 | # - +exe_path+ otherwise. 310 | # 311 | # Argument +command_line+ 312 | # 313 | # \String argument +command_line+ is a command line to be passed to a shell; 314 | # it must begin with a shell reserved word, begin with a special built-in, 315 | # or contain meta characters: 316 | # 317 | # Open3.popen2('if true; then echo "Foo"; fi') {|*args| p args } # Shell reserved word. 318 | # Open3.popen2('echo') {|*args| p args } # Built-in. 319 | # Open3.popen2('date > date.tmp') {|*args| p args } # Contains meta character. 320 | # 321 | # Output (similar for each call above): 322 | # 323 | # # => [#, #, #] 324 | # 325 | # The command line may also contain arguments and options for the command: 326 | # 327 | # Open3.popen2('echo "Foo"') { |i, o, t| o.gets } 328 | # "Foo\n" 329 | # 330 | # Argument +exe_path+ 331 | # 332 | # Argument +exe_path+ is one of the following: 333 | # 334 | # - The string path to an executable to be called. 335 | # - A 2-element array containing the path to an executable 336 | # and the string to be used as the name of the executing process. 337 | # 338 | # Example: 339 | # 340 | # Open3.popen2('/usr/bin/date') { |i, o, t| o.gets } 341 | # # => "Thu Sep 28 09:41:06 AM CDT 2023\n" 342 | # 343 | # Ruby invokes the executable directly, with no shell and no shell expansion: 344 | # 345 | # Open3.popen2('doesnt_exist') { |i, o, t| o.gets } # Raises Errno::ENOENT 346 | # 347 | # If one or more +args+ is given, each is an argument or option 348 | # to be passed to the executable: 349 | # 350 | # Open3.popen2('echo', 'C #') { |i, o, t| o.gets } 351 | # # => "C #\n" 352 | # Open3.popen2('echo', 'hello', 'world') { |i, o, t| o.gets } 353 | # # => "hello world\n" 354 | # 355 | # 356 | # Related: 357 | # 358 | # - Open3.popen2e: Makes the standard input and the merge 359 | # of the standard output and standard error streams 360 | # of the child process available as separate streams. 361 | # - Open3.popen3: Makes the standard input, standard output, 362 | # and standard error streams 363 | # of the child process available as separate streams. 364 | # 365 | def popen2(*cmd, &block) 366 | if Hash === cmd.last 367 | opts = cmd.pop.dup 368 | else 369 | opts = {} 370 | end 371 | 372 | in_r, in_w = IO.pipe 373 | opts[:in] = in_r 374 | in_w.sync = true 375 | 376 | out_r, out_w = IO.pipe 377 | opts[:out] = out_w 378 | 379 | popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block) 380 | end 381 | module_function :popen2 382 | 383 | # :call-seq: 384 | # Open3.popen2e([env, ] command_line, options = {}) -> [stdin, stdout_and_stderr, wait_thread] 385 | # Open3.popen2e([env, ] exe_path, *args, options = {}) -> [stdin, stdout_and_stderr, wait_thread] 386 | # Open3.popen2e([env, ] command_line, options = {}) {|stdin, stdout_and_stderr, wait_thread| ... } -> object 387 | # Open3.popen2e([env, ] exe_path, *args, options = {}) {|stdin, stdout_and_stderr, wait_thread| ... } -> object 388 | # 389 | # Basically a wrapper for 390 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 391 | # that: 392 | # 393 | # - Creates a child process, by calling Process.spawn with the given arguments. 394 | # - Creates streams +stdin+, +stdout_and_stderr+, 395 | # which are the standard input and the merge of the standard output 396 | # and standard error streams in the child process. 397 | # - Creates thread +wait_thread+ that waits for the child process to exit; 398 | # the thread has method +pid+, which returns the process ID 399 | # of the child process. 400 | # 401 | # With no block given, returns the array 402 | # [stdin, stdout_and_stderr, wait_thread]. 403 | # The caller should close each of the two returned streams. 404 | # 405 | # stdin, stdout_and_stderr, wait_thread = Open3.popen2e('echo') 406 | # # => [#, #, #] 407 | # stdin.close 408 | # stdout_and_stderr.close 409 | # wait_thread.pid # => 2274600 410 | # wait_thread.value # => # 411 | # 412 | # With a block given, calls the block with the three variables 413 | # (two streams and the wait thread) 414 | # and returns the block's return value. 415 | # The caller need not close the streams: 416 | # 417 | # Open3.popen2e('echo') do |stdin, stdout_and_stderr, wait_thread| 418 | # p stdin 419 | # p stdout_and_stderr 420 | # p wait_thread 421 | # p wait_thread.pid 422 | # p wait_thread.value 423 | # end 424 | # 425 | # Output: 426 | # 427 | # # 428 | # # 429 | # # 430 | # 2274763 431 | # # 432 | # 433 | # Like Process.spawn, this method has potential security vulnerabilities 434 | # if called with untrusted input; 435 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 436 | # 437 | # Unlike Process.spawn, this method waits for the child process to exit 438 | # before returning, so the caller need not do so. 439 | # 440 | # If the first argument is a hash, it becomes leading argument +env+ 441 | # in the call to Process.spawn; 442 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 443 | # 444 | # If the last argument is a hash, it becomes trailing argument +options+ 445 | # in the call to Process.spawn; 446 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 447 | # 448 | # The single required argument is one of the following: 449 | # 450 | # - +command_line+ if it is a string, 451 | # and if it begins with a shell reserved word or special built-in, 452 | # or if it contains one or more metacharacters. 453 | # - +exe_path+ otherwise. 454 | # 455 | # Argument +command_line+ 456 | # 457 | # \String argument +command_line+ is a command line to be passed to a shell; 458 | # it must begin with a shell reserved word, begin with a special built-in, 459 | # or contain meta characters: 460 | # 461 | # Open3.popen2e('if true; then echo "Foo"; fi') {|*args| p args } # Shell reserved word. 462 | # Open3.popen2e('echo') {|*args| p args } # Built-in. 463 | # Open3.popen2e('date > date.tmp') {|*args| p args } # Contains meta character. 464 | # 465 | # Output (similar for each call above): 466 | # 467 | # # => [#, #, #] 468 | # 469 | # The command line may also contain arguments and options for the command: 470 | # 471 | # Open3.popen2e('echo "Foo"') { |i, o_and_e, t| o_and_e.gets } 472 | # "Foo\n" 473 | # 474 | # Argument +exe_path+ 475 | # 476 | # Argument +exe_path+ is one of the following: 477 | # 478 | # - The string path to an executable to be called. 479 | # - A 2-element array containing the path to an executable 480 | # and the string to be used as the name of the executing process. 481 | # 482 | # Example: 483 | # 484 | # Open3.popen2e('/usr/bin/date') { |i, o_and_e, t| o_and_e.gets } 485 | # # => "Thu Sep 28 01:58:45 PM CDT 2023\n" 486 | # 487 | # Ruby invokes the executable directly, with no shell and no shell expansion: 488 | # 489 | # Open3.popen2e('doesnt_exist') { |i, o_and_e, t| o_and_e.gets } # Raises Errno::ENOENT 490 | # 491 | # If one or more +args+ is given, each is an argument or option 492 | # to be passed to the executable: 493 | # 494 | # Open3.popen2e('echo', 'C #') { |i, o_and_e, t| o_and_e.gets } 495 | # # => "C #\n" 496 | # Open3.popen2e('echo', 'hello', 'world') { |i, o_and_e, t| o_and_e.gets } 497 | # # => "hello world\n" 498 | # 499 | # Related: 500 | # 501 | # - Open3.popen2: Makes the standard input and standard output streams 502 | # of the child process available as separate streams, 503 | # with no access to the standard error stream. 504 | # - Open3.popen3: Makes the standard input, standard output, 505 | # and standard error streams 506 | # of the child process available as separate streams. 507 | # 508 | def popen2e(*cmd, &block) 509 | if Hash === cmd.last 510 | opts = cmd.pop.dup 511 | else 512 | opts = {} 513 | end 514 | 515 | in_r, in_w = IO.pipe 516 | opts[:in] = in_r 517 | in_w.sync = true 518 | 519 | out_r, out_w = IO.pipe 520 | opts[[:out, :err]] = out_w 521 | 522 | popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block) 523 | ensure 524 | if block 525 | in_r.close 526 | in_w.close 527 | out_r.close 528 | out_w.close 529 | end 530 | end 531 | module_function :popen2e 532 | 533 | def popen_run(cmd, opts, child_io, parent_io) # :nodoc: 534 | pid = spawn(*cmd, opts) 535 | wait_thr = Process.detach(pid) 536 | child_io.each(&:close) 537 | result = [*parent_io, wait_thr] 538 | if defined? yield 539 | begin 540 | return yield(*result) 541 | ensure 542 | parent_io.each(&:close) 543 | wait_thr.join 544 | end 545 | end 546 | result 547 | end 548 | module_function :popen_run 549 | class << self 550 | private :popen_run 551 | end 552 | 553 | # :call-seq: 554 | # Open3.capture3([env, ] command_line, options = {}) -> [stdout_s, stderr_s, status] 555 | # Open3.capture3([env, ] exe_path, *args, options = {}) -> [stdout_s, stderr_s, status] 556 | # 557 | # Basically a wrapper for Open3.popen3 that: 558 | # 559 | # - Creates a child process, by calling Open3.popen3 with the given arguments 560 | # (except for certain entries in hash +options+; see below). 561 | # - Returns as strings +stdout_s+ and +stderr_s+ the standard output 562 | # and standard error of the child process. 563 | # - Returns as +status+ a Process::Status object 564 | # that represents the exit status of the child process. 565 | # 566 | # Returns the array [stdout_s, stderr_s, status]: 567 | # 568 | # stdout_s, stderr_s, status = Open3.capture3('echo "Foo"') 569 | # # => ["Foo\n", "", #] 570 | # 571 | # Like Process.spawn, this method has potential security vulnerabilities 572 | # if called with untrusted input; 573 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 574 | # 575 | # Unlike Process.spawn, this method waits for the child process to exit 576 | # before returning, so the caller need not do so. 577 | # 578 | # If the first argument is a hash, it becomes leading argument +env+ 579 | # in the call to Open3.popen3; 580 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 581 | # 582 | # If the last argument is a hash, it becomes trailing argument +options+ 583 | # in the call to Open3.popen3; 584 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 585 | # 586 | # The hash +options+ is given; 587 | # two options have local effect in method Open3.capture3: 588 | # 589 | # - If entry options[:stdin_data] exists, the entry is removed 590 | # and its string value is sent to the command's standard input: 591 | # 592 | # Open3.capture3('tee', stdin_data: 'Foo') 593 | # # => ["Foo", "", #] 594 | # 595 | # - If entry options[:binmode] exists, the entry is removed and 596 | # the internal streams are set to binary mode. 597 | # 598 | # The single required argument is one of the following: 599 | # 600 | # - +command_line+ if it is a string, 601 | # and if it begins with a shell reserved word or special built-in, 602 | # or if it contains one or more metacharacters. 603 | # - +exe_path+ otherwise. 604 | # 605 | # Argument +command_line+ 606 | # 607 | # \String argument +command_line+ is a command line to be passed to a shell; 608 | # it must begin with a shell reserved word, begin with a special built-in, 609 | # or contain meta characters: 610 | # 611 | # Open3.capture3('if true; then echo "Foo"; fi') # Shell reserved word. 612 | # # => ["Foo\n", "", #] 613 | # Open3.capture3('echo') # Built-in. 614 | # # => ["\n", "", #] 615 | # Open3.capture3('date > date.tmp') # Contains meta character. 616 | # # => ["", "", #] 617 | # 618 | # The command line may also contain arguments and options for the command: 619 | # 620 | # Open3.capture3('echo "Foo"') 621 | # # => ["Foo\n", "", #] 622 | # 623 | # Argument +exe_path+ 624 | # 625 | # Argument +exe_path+ is one of the following: 626 | # 627 | # - The string path to an executable to be called. 628 | # - A 2-element array containing the path to an executable 629 | # and the string to be used as the name of the executing process. 630 | # 631 | # Example: 632 | # 633 | # Open3.capture3('/usr/bin/date') 634 | # # => ["Thu Sep 28 05:03:51 PM CDT 2023\n", "", #] 635 | # 636 | # Ruby invokes the executable directly, with no shell and no shell expansion: 637 | # 638 | # Open3.capture3('doesnt_exist') # Raises Errno::ENOENT 639 | # 640 | # If one or more +args+ is given, each is an argument or option 641 | # to be passed to the executable: 642 | # 643 | # Open3.capture3('echo', 'C #') 644 | # # => ["C #\n", "", #] 645 | # Open3.capture3('echo', 'hello', 'world') 646 | # # => ["hello world\n", "", #] 647 | # 648 | def capture3(*cmd) 649 | if Hash === cmd.last 650 | opts = cmd.pop.dup 651 | else 652 | opts = {} 653 | end 654 | 655 | stdin_data = opts.delete(:stdin_data) || '' 656 | binmode = opts.delete(:binmode) 657 | 658 | popen3(*cmd, opts) {|i, o, e, t| 659 | if binmode 660 | i.binmode 661 | o.binmode 662 | e.binmode 663 | end 664 | out_reader = Thread.new { o.read } 665 | err_reader = Thread.new { e.read } 666 | begin 667 | if stdin_data.respond_to? :readpartial 668 | IO.copy_stream(stdin_data, i) 669 | else 670 | i.write stdin_data 671 | end 672 | rescue Errno::EPIPE 673 | end 674 | i.close 675 | [out_reader.value, err_reader.value, t.value] 676 | } 677 | end 678 | module_function :capture3 679 | 680 | # :call-seq: 681 | # Open3.capture2([env, ] command_line, options = {}) -> [stdout_s, status] 682 | # Open3.capture2([env, ] exe_path, *args, options = {}) -> [stdout_s, status] 683 | # 684 | # Basically a wrapper for Open3.popen3 that: 685 | # 686 | # - Creates a child process, by calling Open3.popen3 with the given arguments 687 | # (except for certain entries in hash +options+; see below). 688 | # - Returns as string +stdout_s+ the standard output of the child process. 689 | # - Returns as +status+ a Process::Status object 690 | # that represents the exit status of the child process. 691 | # 692 | # Returns the array [stdout_s, status]: 693 | # 694 | # stdout_s, status = Open3.capture2('echo "Foo"') 695 | # # => ["Foo\n", #] 696 | # 697 | # Like Process.spawn, this method has potential security vulnerabilities 698 | # if called with untrusted input; 699 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 700 | # 701 | # Unlike Process.spawn, this method waits for the child process to exit 702 | # before returning, so the caller need not do so. 703 | # 704 | # If the first argument is a hash, it becomes leading argument +env+ 705 | # in the call to Open3.popen3; 706 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 707 | # 708 | # If the last argument is a hash, it becomes trailing argument +options+ 709 | # in the call to Open3.popen3; 710 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 711 | # 712 | # The hash +options+ is given; 713 | # two options have local effect in method Open3.capture2: 714 | # 715 | # - If entry options[:stdin_data] exists, the entry is removed 716 | # and its string value is sent to the command's standard input: 717 | # 718 | # Open3.capture2('tee', stdin_data: 'Foo') 719 | # 720 | # # => ["Foo", #] 721 | # 722 | # - If entry options[:binmode] exists, the entry is removed and 723 | # the internal streams are set to binary mode. 724 | # 725 | # The single required argument is one of the following: 726 | # 727 | # - +command_line+ if it is a string, 728 | # and if it begins with a shell reserved word or special built-in, 729 | # or if it contains one or more metacharacters. 730 | # - +exe_path+ otherwise. 731 | # 732 | # Argument +command_line+ 733 | # 734 | # \String argument +command_line+ is a command line to be passed to a shell; 735 | # it must begin with a shell reserved word, begin with a special built-in, 736 | # or contain meta characters: 737 | # 738 | # Open3.capture2('if true; then echo "Foo"; fi') # Shell reserved word. 739 | # # => ["Foo\n", #] 740 | # Open3.capture2('echo') # Built-in. 741 | # # => ["\n", #] 742 | # Open3.capture2('date > date.tmp') # Contains meta character. 743 | # # => ["", #] 744 | # 745 | # The command line may also contain arguments and options for the command: 746 | # 747 | # Open3.capture2('echo "Foo"') 748 | # # => ["Foo\n", #] 749 | # 750 | # Argument +exe_path+ 751 | # 752 | # Argument +exe_path+ is one of the following: 753 | # 754 | # - The string path to an executable to be called. 755 | # - A 2-element array containing the path to an executable 756 | # and the string to be used as the name of the executing process. 757 | # 758 | # Example: 759 | # 760 | # Open3.capture2('/usr/bin/date') 761 | # # => ["Fri Sep 29 01:00:39 PM CDT 2023\n", #] 762 | # 763 | # Ruby invokes the executable directly, with no shell and no shell expansion: 764 | # 765 | # Open3.capture2('doesnt_exist') # Raises Errno::ENOENT 766 | # 767 | # If one or more +args+ is given, each is an argument or option 768 | # to be passed to the executable: 769 | # 770 | # Open3.capture2('echo', 'C #') 771 | # # => ["C #\n", #] 772 | # Open3.capture2('echo', 'hello', 'world') 773 | # # => ["hello world\n", #] 774 | # 775 | def capture2(*cmd) 776 | if Hash === cmd.last 777 | opts = cmd.pop.dup 778 | else 779 | opts = {} 780 | end 781 | 782 | stdin_data = opts.delete(:stdin_data) 783 | binmode = opts.delete(:binmode) 784 | 785 | popen2(*cmd, opts) {|i, o, t| 786 | if binmode 787 | i.binmode 788 | o.binmode 789 | end 790 | out_reader = Thread.new { o.read } 791 | if stdin_data 792 | begin 793 | if stdin_data.respond_to? :readpartial 794 | IO.copy_stream(stdin_data, i) 795 | else 796 | i.write stdin_data 797 | end 798 | rescue Errno::EPIPE 799 | end 800 | end 801 | i.close 802 | [out_reader.value, t.value] 803 | } 804 | end 805 | module_function :capture2 806 | 807 | # :call-seq: 808 | # Open3.capture2e([env, ] command_line, options = {}) -> [stdout_and_stderr_s, status] 809 | # Open3.capture2e([env, ] exe_path, *args, options = {}) -> [stdout_and_stderr_s, status] 810 | # 811 | # Basically a wrapper for Open3.popen3 that: 812 | # 813 | # - Creates a child process, by calling Open3.popen3 with the given arguments 814 | # (except for certain entries in hash +options+; see below). 815 | # - Returns as string +stdout_and_stderr_s+ the merged standard output 816 | # and standard error of the child process. 817 | # - Returns as +status+ a Process::Status object 818 | # that represents the exit status of the child process. 819 | # 820 | # Returns the array [stdout_and_stderr_s, status]: 821 | # 822 | # stdout_and_stderr_s, status = Open3.capture2e('echo "Foo"') 823 | # # => ["Foo\n", #] 824 | # 825 | # Like Process.spawn, this method has potential security vulnerabilities 826 | # if called with untrusted input; 827 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 828 | # 829 | # Unlike Process.spawn, this method waits for the child process to exit 830 | # before returning, so the caller need not do so. 831 | # 832 | # If the first argument is a hash, it becomes leading argument +env+ 833 | # in the call to Open3.popen3; 834 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 835 | # 836 | # If the last argument is a hash, it becomes trailing argument +options+ 837 | # in the call to Open3.popen3; 838 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 839 | # 840 | # The hash +options+ is given; 841 | # two options have local effect in method Open3.capture2e: 842 | # 843 | # - If entry options[:stdin_data] exists, the entry is removed 844 | # and its string value is sent to the command's standard input: 845 | # 846 | # Open3.capture2e('tee', stdin_data: 'Foo') 847 | # # => ["Foo", #] 848 | # 849 | # - If entry options[:binmode] exists, the entry is removed and 850 | # the internal streams are set to binary mode. 851 | # 852 | # The single required argument is one of the following: 853 | # 854 | # - +command_line+ if it is a string, 855 | # and if it begins with a shell reserved word or special built-in, 856 | # or if it contains one or more metacharacters. 857 | # - +exe_path+ otherwise. 858 | # 859 | # Argument +command_line+ 860 | # 861 | # \String argument +command_line+ is a command line to be passed to a shell; 862 | # it must begin with a shell reserved word, begin with a special built-in, 863 | # or contain meta characters: 864 | # 865 | # Open3.capture2e('if true; then echo "Foo"; fi') # Shell reserved word. 866 | # # => ["Foo\n", #] 867 | # Open3.capture2e('echo') # Built-in. 868 | # # => ["\n", #] 869 | # Open3.capture2e('date > date.tmp') # Contains meta character. 870 | # # => ["", #] 871 | # 872 | # The command line may also contain arguments and options for the command: 873 | # 874 | # Open3.capture2e('echo "Foo"') 875 | # # => ["Foo\n", #] 876 | # 877 | # Argument +exe_path+ 878 | # 879 | # Argument +exe_path+ is one of the following: 880 | # 881 | # - The string path to an executable to be called. 882 | # - A 2-element array containing the path to an executable 883 | # and the string to be used as the name of the executing process. 884 | # 885 | # Example: 886 | # 887 | # Open3.capture2e('/usr/bin/date') 888 | # # => ["Sat Sep 30 09:01:46 AM CDT 2023\n", #] 889 | # 890 | # Ruby invokes the executable directly, with no shell and no shell expansion: 891 | # 892 | # Open3.capture2e('doesnt_exist') # Raises Errno::ENOENT 893 | # 894 | # If one or more +args+ is given, each is an argument or option 895 | # to be passed to the executable: 896 | # 897 | # Open3.capture2e('echo', 'C #') 898 | # # => ["C #\n", #] 899 | # Open3.capture2e('echo', 'hello', 'world') 900 | # # => ["hello world\n", #] 901 | # 902 | def capture2e(*cmd) 903 | if Hash === cmd.last 904 | opts = cmd.pop.dup 905 | else 906 | opts = {} 907 | end 908 | 909 | stdin_data = opts.delete(:stdin_data) 910 | binmode = opts.delete(:binmode) 911 | 912 | popen2e(*cmd, opts) {|i, oe, t| 913 | if binmode 914 | i.binmode 915 | oe.binmode 916 | end 917 | outerr_reader = Thread.new { oe.read } 918 | if stdin_data 919 | begin 920 | if stdin_data.respond_to? :readpartial 921 | IO.copy_stream(stdin_data, i) 922 | else 923 | i.write stdin_data 924 | end 925 | rescue Errno::EPIPE 926 | end 927 | end 928 | i.close 929 | [outerr_reader.value, t.value] 930 | } 931 | end 932 | module_function :capture2e 933 | 934 | # :call-seq: 935 | # Open3.pipeline_rw([env, ] *cmds, options = {}) -> [first_stdin, last_stdout, wait_threads] 936 | # 937 | # Basically a wrapper for 938 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 939 | # that: 940 | # 941 | # - Creates a child process for each of the given +cmds+ 942 | # by calling Process.spawn. 943 | # - Pipes the +stdout+ from each child to the +stdin+ of the next child, 944 | # or, for the first child, from the caller's +stdin+, 945 | # or, for the last child, to the caller's +stdout+. 946 | # 947 | # The method does not wait for child processes to exit, 948 | # so the caller must do so. 949 | # 950 | # With no block given, returns a 3-element array containing: 951 | # 952 | # - The +stdin+ stream of the first child process. 953 | # - The +stdout+ stream of the last child process. 954 | # - An array of the wait threads for all of the child processes. 955 | # 956 | # Example: 957 | # 958 | # first_stdin, last_stdout, wait_threads = Open3.pipeline_rw('sort', 'cat -n') 959 | # # => [#, #, [#, #]] 960 | # first_stdin.puts("foo\nbar\nbaz") 961 | # first_stdin.close # Send EOF to sort. 962 | # puts last_stdout.read 963 | # wait_threads.each do |wait_thread| 964 | # wait_thread.join 965 | # end 966 | # 967 | # Output: 968 | # 969 | # 1 bar 970 | # 2 baz 971 | # 3 foo 972 | # 973 | # With a block given, calls the block with the +stdin+ stream of the first child, 974 | # the +stdout+ stream of the last child, 975 | # and an array of the wait processes: 976 | # 977 | # Open3.pipeline_rw('sort', 'cat -n') do |first_stdin, last_stdout, wait_threads| 978 | # first_stdin.puts "foo\nbar\nbaz" 979 | # first_stdin.close # send EOF to sort. 980 | # puts last_stdout.read 981 | # wait_threads.each do |wait_thread| 982 | # wait_thread.join 983 | # end 984 | # end 985 | # 986 | # Output: 987 | # 988 | # 1 bar 989 | # 2 baz 990 | # 3 foo 991 | # 992 | # Like Process.spawn, this method has potential security vulnerabilities 993 | # if called with untrusted input; 994 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 995 | # 996 | # If the first argument is a hash, it becomes leading argument +env+ 997 | # in each call to Process.spawn; 998 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 999 | # 1000 | # If the last argument is a hash, it becomes trailing argument +options+ 1001 | # in each call to Process.spawn; 1002 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 1003 | # 1004 | # Each remaining argument in +cmds+ is one of: 1005 | # 1006 | # - A +command_line+: a string that begins with a shell reserved word 1007 | # or special built-in, or contains one or more metacharacters. 1008 | # - An +exe_path+: the string path to an executable to be called. 1009 | # - An array containing a +command_line+ or an +exe_path+, 1010 | # along with zero or more string arguments for the command. 1011 | # 1012 | # See {Argument command_line or exe_path}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Argument+command_line+or+exe_path]. 1013 | # 1014 | def pipeline_rw(*cmds, &block) 1015 | if Hash === cmds.last 1016 | opts = cmds.pop.dup 1017 | else 1018 | opts = {} 1019 | end 1020 | 1021 | in_r, in_w = IO.pipe 1022 | opts[:in] = in_r 1023 | in_w.sync = true 1024 | 1025 | out_r, out_w = IO.pipe 1026 | opts[:out] = out_w 1027 | 1028 | pipeline_run(cmds, opts, [in_r, out_w], [in_w, out_r], &block) 1029 | end 1030 | module_function :pipeline_rw 1031 | 1032 | # :call-seq: 1033 | # Open3.pipeline_r([env, ] *cmds, options = {}) -> [last_stdout, wait_threads] 1034 | # 1035 | # Basically a wrapper for 1036 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 1037 | # that: 1038 | # 1039 | # - Creates a child process for each of the given +cmds+ 1040 | # by calling Process.spawn. 1041 | # - Pipes the +stdout+ from each child to the +stdin+ of the next child, 1042 | # or, for the last child, to the caller's +stdout+. 1043 | # 1044 | # The method does not wait for child processes to exit, 1045 | # so the caller must do so. 1046 | # 1047 | # With no block given, returns a 2-element array containing: 1048 | # 1049 | # - The +stdout+ stream of the last child process. 1050 | # - An array of the wait threads for all of the child processes. 1051 | # 1052 | # Example: 1053 | # 1054 | # last_stdout, wait_threads = Open3.pipeline_r('ls', 'grep R') 1055 | # # => [#, [#, #]] 1056 | # puts last_stdout.read 1057 | # wait_threads.each do |wait_thread| 1058 | # wait_thread.join 1059 | # end 1060 | # 1061 | # Output: 1062 | # 1063 | # Rakefile 1064 | # README.md 1065 | # 1066 | # With a block given, calls the block with the +stdout+ stream 1067 | # of the last child process, 1068 | # and an array of the wait processes: 1069 | # 1070 | # Open3.pipeline_r('ls', 'grep R') do |last_stdout, wait_threads| 1071 | # puts last_stdout.read 1072 | # wait_threads.each do |wait_thread| 1073 | # wait_thread.join 1074 | # end 1075 | # end 1076 | # 1077 | # Output: 1078 | # 1079 | # Rakefile 1080 | # README.md 1081 | # 1082 | # Like Process.spawn, this method has potential security vulnerabilities 1083 | # if called with untrusted input; 1084 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 1085 | # 1086 | # If the first argument is a hash, it becomes leading argument +env+ 1087 | # in each call to Process.spawn; 1088 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 1089 | # 1090 | # If the last argument is a hash, it becomes trailing argument +options+ 1091 | # in each call to Process.spawn; 1092 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 1093 | # 1094 | # Each remaining argument in +cmds+ is one of: 1095 | # 1096 | # - A +command_line+: a string that begins with a shell reserved word 1097 | # or special built-in, or contains one or more metacharacters. 1098 | # - An +exe_path+: the string path to an executable to be called. 1099 | # - An array containing a +command_line+ or an +exe_path+, 1100 | # along with zero or more string arguments for the command. 1101 | # 1102 | # See {Argument command_line or exe_path}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Argument+command_line+or+exe_path]. 1103 | # 1104 | def pipeline_r(*cmds, &block) 1105 | if Hash === cmds.last 1106 | opts = cmds.pop.dup 1107 | else 1108 | opts = {} 1109 | end 1110 | 1111 | out_r, out_w = IO.pipe 1112 | opts[:out] = out_w 1113 | 1114 | pipeline_run(cmds, opts, [out_w], [out_r], &block) 1115 | end 1116 | module_function :pipeline_r 1117 | 1118 | 1119 | # :call-seq: 1120 | # Open3.pipeline_w([env, ] *cmds, options = {}) -> [first_stdin, wait_threads] 1121 | # 1122 | # Basically a wrapper for 1123 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 1124 | # that: 1125 | # 1126 | # - Creates a child process for each of the given +cmds+ 1127 | # by calling Process.spawn. 1128 | # - Pipes the +stdout+ from each child to the +stdin+ of the next child, 1129 | # or, for the first child, pipes the caller's +stdout+ to the child's +stdin+. 1130 | # 1131 | # The method does not wait for child processes to exit, 1132 | # so the caller must do so. 1133 | # 1134 | # With no block given, returns a 2-element array containing: 1135 | # 1136 | # - The +stdin+ stream of the first child process. 1137 | # - An array of the wait threads for all of the child processes. 1138 | # 1139 | # Example: 1140 | # 1141 | # first_stdin, wait_threads = Open3.pipeline_w('sort', 'cat -n') 1142 | # # => [#, [#, #]] 1143 | # first_stdin.puts("foo\nbar\nbaz") 1144 | # first_stdin.close # Send EOF to sort. 1145 | # wait_threads.each do |wait_thread| 1146 | # wait_thread.join 1147 | # end 1148 | # 1149 | # Output: 1150 | # 1151 | # 1 bar 1152 | # 2 baz 1153 | # 3 foo 1154 | # 1155 | # With a block given, calls the block with the +stdin+ stream 1156 | # of the first child process, 1157 | # and an array of the wait processes: 1158 | # 1159 | # Open3.pipeline_w('sort', 'cat -n') do |first_stdin, wait_threads| 1160 | # first_stdin.puts("foo\nbar\nbaz") 1161 | # first_stdin.close # Send EOF to sort. 1162 | # wait_threads.each do |wait_thread| 1163 | # wait_thread.join 1164 | # end 1165 | # end 1166 | # 1167 | # Output: 1168 | # 1169 | # 1 bar 1170 | # 2 baz 1171 | # 3 foo 1172 | # 1173 | # Like Process.spawn, this method has potential security vulnerabilities 1174 | # if called with untrusted input; 1175 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 1176 | # 1177 | # If the first argument is a hash, it becomes leading argument +env+ 1178 | # in each call to Process.spawn; 1179 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 1180 | # 1181 | # If the last argument is a hash, it becomes trailing argument +options+ 1182 | # in each call to Process.spawn; 1183 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 1184 | # 1185 | # Each remaining argument in +cmds+ is one of: 1186 | # 1187 | # - A +command_line+: a string that begins with a shell reserved word 1188 | # or special built-in, or contains one or more metacharacters. 1189 | # - An +exe_path+: the string path to an executable to be called. 1190 | # - An array containing a +command_line+ or an +exe_path+, 1191 | # along with zero or more string arguments for the command. 1192 | # 1193 | # See {Argument command_line or exe_path}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Argument+command_line+or+exe_path]. 1194 | # 1195 | def pipeline_w(*cmds, &block) 1196 | if Hash === cmds.last 1197 | opts = cmds.pop.dup 1198 | else 1199 | opts = {} 1200 | end 1201 | 1202 | in_r, in_w = IO.pipe 1203 | opts[:in] = in_r 1204 | in_w.sync = true 1205 | 1206 | pipeline_run(cmds, opts, [in_r], [in_w], &block) 1207 | end 1208 | module_function :pipeline_w 1209 | 1210 | # :call-seq: 1211 | # Open3.pipeline_start([env, ] *cmds, options = {}) -> [wait_threads] 1212 | # 1213 | # Basically a wrapper for 1214 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 1215 | # that: 1216 | # 1217 | # - Creates a child process for each of the given +cmds+ 1218 | # by calling Process.spawn. 1219 | # - Does not wait for child processes to exit. 1220 | # 1221 | # With no block given, returns an array of the wait threads 1222 | # for all of the child processes. 1223 | # 1224 | # Example: 1225 | # 1226 | # wait_threads = Open3.pipeline_start('ls', 'grep R') 1227 | # # => [#, #] 1228 | # wait_threads.each do |wait_thread| 1229 | # wait_thread.join 1230 | # end 1231 | # 1232 | # Output: 1233 | # 1234 | # Rakefile 1235 | # README.md 1236 | # 1237 | # With a block given, calls the block with an array of the wait processes: 1238 | # 1239 | # Open3.pipeline_start('ls', 'grep R') do |wait_threads| 1240 | # wait_threads.each do |wait_thread| 1241 | # wait_thread.join 1242 | # end 1243 | # end 1244 | # 1245 | # Output: 1246 | # 1247 | # Rakefile 1248 | # README.md 1249 | # 1250 | # Like Process.spawn, this method has potential security vulnerabilities 1251 | # if called with untrusted input; 1252 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 1253 | # 1254 | # If the first argument is a hash, it becomes leading argument +env+ 1255 | # in each call to Process.spawn; 1256 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 1257 | # 1258 | # If the last argument is a hash, it becomes trailing argument +options+ 1259 | # in each call to Process.spawn; 1260 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 1261 | # 1262 | # Each remaining argument in +cmds+ is one of: 1263 | # 1264 | # - A +command_line+: a string that begins with a shell reserved word 1265 | # or special built-in, or contains one or more metacharacters. 1266 | # - An +exe_path+: the string path to an executable to be called. 1267 | # - An array containing a +command_line+ or an +exe_path+, 1268 | # along with zero or more string arguments for the command. 1269 | # 1270 | # See {Argument command_line or exe_path}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Argument+command_line+or+exe_path]. 1271 | # 1272 | def pipeline_start(*cmds, &block) 1273 | if Hash === cmds.last 1274 | opts = cmds.pop.dup 1275 | else 1276 | opts = {} 1277 | end 1278 | 1279 | if block 1280 | pipeline_run(cmds, opts, [], [], &block) 1281 | else 1282 | ts, = pipeline_run(cmds, opts, [], []) 1283 | ts 1284 | end 1285 | end 1286 | module_function :pipeline_start 1287 | 1288 | # :call-seq: 1289 | # Open3.pipeline([env, ] *cmds, options = {}) -> array_of_statuses 1290 | # 1291 | # Basically a wrapper for 1292 | # {Process.spawn}[https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn] 1293 | # that: 1294 | # 1295 | # - Creates a child process for each of the given +cmds+ 1296 | # by calling Process.spawn. 1297 | # - Pipes the +stdout+ from each child to the +stdin+ of the next child, 1298 | # or, for the last child, to the caller's +stdout+. 1299 | # - Waits for the child processes to exit. 1300 | # - Returns an array of Process::Status objects (one for each child). 1301 | # 1302 | # Example: 1303 | # 1304 | # wait_threads = Open3.pipeline('ls', 'grep R') 1305 | # # => [#, #] 1306 | # 1307 | # Output: 1308 | # 1309 | # Rakefile 1310 | # README.md 1311 | # 1312 | # Like Process.spawn, this method has potential security vulnerabilities 1313 | # if called with untrusted input; 1314 | # see {Command Injection}[https://docs.ruby-lang.org/en/master/command_injection_rdoc.html#label-Command+Injection]. 1315 | # 1316 | # If the first argument is a hash, it becomes leading argument +env+ 1317 | # in each call to Process.spawn; 1318 | # see {Execution Environment}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Environment]. 1319 | # 1320 | # If the last argument is a hash, it becomes trailing argument +options+ 1321 | # in each call to Process.spawn' 1322 | # see {Execution Options}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options]. 1323 | # 1324 | # Each remaining argument in +cmds+ is one of: 1325 | # 1326 | # - A +command_line+: a string that begins with a shell reserved word 1327 | # or special built-in, or contains one or more metacharacters. 1328 | # - An +exe_path+: the string path to an executable to be called. 1329 | # - An array containing a +command_line+ or an +exe_path+, 1330 | # along with zero or more string arguments for the command. 1331 | # 1332 | # See {Argument command_line or exe_path}[https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Argument+command_line+or+exe_path]. 1333 | # 1334 | def pipeline(*cmds) 1335 | if Hash === cmds.last 1336 | opts = cmds.pop.dup 1337 | else 1338 | opts = {} 1339 | end 1340 | 1341 | pipeline_run(cmds, opts, [], []) {|ts| 1342 | ts.map(&:value) 1343 | } 1344 | end 1345 | module_function :pipeline 1346 | 1347 | def pipeline_run(cmds, pipeline_opts, child_io, parent_io) # :nodoc: 1348 | if cmds.empty? 1349 | raise ArgumentError, "no commands" 1350 | end 1351 | 1352 | opts_base = pipeline_opts.dup 1353 | opts_base.delete :in 1354 | opts_base.delete :out 1355 | 1356 | wait_thrs = [] 1357 | r = nil 1358 | cmds.each_with_index {|cmd, i| 1359 | cmd_opts = opts_base.dup 1360 | if String === cmd 1361 | cmd = [cmd] 1362 | else 1363 | cmd_opts.update cmd.pop if Hash === cmd.last 1364 | end 1365 | if i == 0 1366 | if !cmd_opts.include?(:in) 1367 | if pipeline_opts.include?(:in) 1368 | cmd_opts[:in] = pipeline_opts[:in] 1369 | end 1370 | end 1371 | else 1372 | cmd_opts[:in] = r 1373 | end 1374 | if i != cmds.length - 1 1375 | r2, w2 = IO.pipe 1376 | cmd_opts[:out] = w2 1377 | else 1378 | if !cmd_opts.include?(:out) 1379 | if pipeline_opts.include?(:out) 1380 | cmd_opts[:out] = pipeline_opts[:out] 1381 | end 1382 | end 1383 | end 1384 | pid = spawn(*cmd, cmd_opts) 1385 | wait_thrs << Process.detach(pid) 1386 | r&.close 1387 | w2&.close 1388 | r = r2 1389 | } 1390 | result = parent_io + [wait_thrs] 1391 | child_io.each(&:close) 1392 | if defined? yield 1393 | begin 1394 | return yield(*result) 1395 | ensure 1396 | parent_io.each(&:close) 1397 | wait_thrs.each(&:join) 1398 | end 1399 | end 1400 | result 1401 | end 1402 | module_function :pipeline_run 1403 | class << self 1404 | private :pipeline_run 1405 | end 1406 | 1407 | end 1408 | 1409 | # JRuby uses different popen logic on Windows, require it here to reuse wrapper methods above. 1410 | require 'open3/jruby_windows' if RUBY_ENGINE == 'jruby' && JRuby::Util::ON_WINDOWS 1411 | -------------------------------------------------------------------------------- /lib/open3/jruby_windows.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Custom implementation of Open3.popen{3,2,2e} that uses java.lang.ProcessBuilder rather than pipes and spawns. 3 | # 4 | 5 | require 'jruby' # need access to runtime for RubyStatus construction 6 | 7 | module Open3 8 | 9 | java_import java.lang.ProcessBuilder 10 | java_import org.jruby.RubyProcess 11 | java_import org.jruby.util.ShellLauncher 12 | 13 | def popen3(*cmd, &block) 14 | if cmd.size > 0 && Hash === cmd[-1] 15 | opts = cmd.pop 16 | else 17 | opts = {} 18 | end 19 | processbuilder_run(cmd, opts, io: IO_3, &block) 20 | end 21 | module_function :popen3 22 | 23 | IO_3 = proc do |process| 24 | [process.getOutputStream.to_io, process.getInputStream.to_io, process.getErrorStream.to_io] 25 | end 26 | 27 | BUILD_2 = proc do |builder| 28 | builder.redirectError(ProcessBuilder::Redirect::INHERIT) 29 | end 30 | 31 | IO_2 = proc do |process| 32 | [process.getOutputStream.to_io, process.getInputStream.to_io] 33 | end 34 | 35 | def popen2(*cmd, &block) 36 | if cmd.size > 0 && Hash === cmd[-1] 37 | opts = cmd.pop 38 | else 39 | opts = {} 40 | end 41 | processbuilder_run(cmd, opts, build: BUILD_2, io: IO_2, &block) 42 | end 43 | module_function :popen2 44 | 45 | BUILD_2E = proc do |builder| 46 | builder.redirectErrorStream(true) 47 | end 48 | 49 | def popen2e(*cmd, &block) 50 | if cmd.size > 0 && Hash === cmd[-1] 51 | opts = cmd.pop 52 | else 53 | opts = {} 54 | end 55 | processbuilder_run(cmd, opts, build: BUILD_2E, io: IO_2, &block) 56 | end 57 | module_function :popen2e 58 | 59 | def processbuilder_run(cmd, opts, build: nil, io:) 60 | opts.each do |k, v| 61 | if Integer === k 62 | if IO == v || !(String === v || v.respond_to?(:to_path)) 63 | # target is an open IO or a non-pathable object, bail out 64 | raise NotImplementedError.new("redirect to an open IO is not implemented on this platform") 65 | end 66 | end 67 | end 68 | 69 | if Hash === cmd[0] 70 | env = cmd.shift; 71 | else 72 | env = {} 73 | end 74 | 75 | if cmd.size == 1 && (cmd[0] =~ / / || ShellLauncher.shouldUseShell(cmd[0])) 76 | cmd = [RbConfig::CONFIG['SHELL'], JRuby::Util::ON_WINDOWS ? '/c' : '-c', cmd[0]] 77 | end 78 | 79 | builder = ProcessBuilder.new(cmd.to_java(:string)) 80 | 81 | builder.directory(java.io.File.new(opts[:chdir] || Dir.pwd)) 82 | 83 | environment = builder.environment 84 | env.each { |k, v| v.nil? ? environment.remove(k) : environment.put(k, v) } 85 | 86 | build.call(builder) if build 87 | 88 | process = builder.start 89 | 90 | pid = org.jruby.util.ShellLauncher.getPidFromProcess(process) 91 | 92 | parent_io = io.call(process) 93 | 94 | parent_io.each {|i| i.sync = true} 95 | 96 | wait_thr = DetachThread.new(pid) { RubyProcess::RubyStatus.newProcessStatus(JRuby.runtime, process.waitFor << 8, pid) } 97 | 98 | result = [*parent_io, wait_thr] 99 | 100 | if defined? yield 101 | begin 102 | return yield(*result) 103 | ensure 104 | parent_io.each(&:close) 105 | wait_thr.join 106 | end 107 | end 108 | 109 | result 110 | end 111 | module_function :processbuilder_run 112 | class << self 113 | private :processbuilder_run 114 | end 115 | 116 | class DetachThread < Thread 117 | attr_reader :pid 118 | 119 | def initialize(pid) 120 | super 121 | 122 | @pid = pid 123 | self[:pid] = pid 124 | end 125 | end 126 | 127 | end 128 | -------------------------------------------------------------------------------- /lib/open3/version.rb: -------------------------------------------------------------------------------- 1 | module Open3 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /open3.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}/version.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Popen, but with stderr, too} 17 | spec.description = spec.summary 18 | spec.homepage = "https://github.com/ruby/open3" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | spec.required_ruby_version = ">= 2.6.0" 21 | 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 28 | `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 29 | end 30 | spec.bindir = "exe" 31 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 32 | spec.require_paths = ["lib"] 33 | end 34 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require 'envutil' 3 | -------------------------------------------------------------------------------- /test/test_open3.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test/unit' 4 | require 'open3' 5 | 6 | class TestOpen3 < Test::Unit::TestCase 7 | RUBY = EnvUtil.rubybin 8 | 9 | def test_exit_status 10 | Open3.popen3(RUBY, '-e', 'exit true') {|i,o,e,t| 11 | assert_equal(true, t.value.success?) 12 | } 13 | Open3.popen3(RUBY, '-e', 'exit false') {|i,o,e,t| 14 | assert_equal(false, t.value.success?) 15 | } 16 | end 17 | 18 | def test_stdin 19 | Open3.popen3(RUBY, '-e', 'exit STDIN.gets.chomp == "t"') {|i,o,e,t| 20 | i.puts 't' 21 | assert_equal(true, t.value.success?) 22 | } 23 | Open3.popen3(RUBY, '-e', 'exit STDIN.gets.chomp == "t"') {|i,o,e,t| 24 | i.puts 'f' 25 | assert_equal(false, t.value.success?) 26 | } 27 | end 28 | 29 | def test_stdout 30 | Open3.popen3(RUBY, '-e', 'STDOUT.print "foo"') {|i,o,e,t| 31 | assert_equal("foo", o.read) 32 | } 33 | end 34 | 35 | def test_stderr 36 | Open3.popen3(RUBY, '-e', 'STDERR.print "bar"') {|i,o,e,t| 37 | assert_equal("bar", e.read) 38 | } 39 | end 40 | 41 | def test_block 42 | r = Open3.popen3(RUBY, '-e', 'STDOUT.print STDIN.read') {|i,o,e,t| 43 | i.print "baz" 44 | i.close 45 | assert_equal("baz", o.read) 46 | "qux" 47 | } 48 | assert_equal("qux", r) 49 | end 50 | 51 | def test_noblock 52 | i,o,e,t = Open3.popen3(RUBY, '-e', 'STDOUT.print STDIN.read') 53 | i.print "baz" 54 | i.close 55 | assert_equal("baz", o.read) 56 | ensure 57 | i.close 58 | o.close 59 | e.close 60 | t.join 61 | end 62 | 63 | def test_commandline 64 | commandline = "echo quux\n" 65 | Open3.popen3(commandline) {|i,o,e,t| 66 | assert_equal("quux\n", o.read) 67 | } 68 | end 69 | 70 | def test_pid 71 | Open3.popen3(RUBY, '-e', 'print $$') {|i,o,e,t| 72 | pid = o.read.to_i 73 | assert_equal(pid, t[:pid]) 74 | assert_equal(pid, t.pid) 75 | } 76 | end 77 | 78 | def test_env 79 | Open3.popen3({'A' => 'B', 'C' => 'D'}, RUBY, '-e' 'p ENV["A"]') do |i, out, err, thr| 80 | output = out.read 81 | assert_equal("\"B\"\n", output) 82 | end 83 | end 84 | 85 | def test_numeric_file_descriptor2 86 | with_pipe {|r, w| 87 | Open3.popen2(RUBY, '-e', 'STDERR.puts "foo"', 2 => w) {|i,o,t| 88 | assert_equal("foo\n", r.gets) 89 | } 90 | } 91 | end 92 | 93 | def test_numeric_file_descriptor3 94 | omit "passing FDs bigger than 2 is not supported on Windows" if /mswin|mingw/ =~ RbConfig::CONFIG['host_os'] 95 | with_pipe {|r, w| 96 | Open3.popen3(RUBY, '-e', 'IO.open(3).puts "foo"', 3 => w) {|i,o,e,t| 97 | assert_equal("foo\n", r.gets, "[GH-808] [ruby-core:67347] [Bug #10699]") 98 | } 99 | } 100 | end 101 | 102 | def with_pipe 103 | r, w = IO.pipe 104 | yield r, w 105 | ensure 106 | r.close 107 | w.close 108 | end 109 | 110 | def with_reopen(io, arg) 111 | old = io.dup 112 | io.reopen(arg) 113 | yield old 114 | ensure 115 | io.reopen(old) 116 | old.close 117 | end 118 | 119 | def test_popen2 120 | with_pipe {|r, w| 121 | with_reopen(STDERR, w) {|old| 122 | w.close 123 | Open3.popen2(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDERR.print s+"e"') {|i,o,t| 124 | assert_kind_of(Thread, t) 125 | i.print "z" 126 | i.close 127 | STDERR.reopen(old) 128 | assert_equal("zo", o.read) 129 | assert_equal("ze", r.read) 130 | } 131 | } 132 | } 133 | end 134 | 135 | def test_popen2e 136 | with_pipe {|r, w| 137 | with_reopen(STDERR, w) {|old| 138 | w.close 139 | Open3.popen2e(RUBY, '-e', 's=STDIN.read; STDOUT.print s+"o"; STDOUT.flush; STDERR.print s+"e"') {|i,o,t| 140 | assert_kind_of(Thread, t) 141 | i.print "y" 142 | i.close 143 | STDERR.reopen(old) 144 | assert_equal("yoye", o.read) 145 | assert_equal("", r.read) 146 | } 147 | } 148 | } 149 | end 150 | 151 | def test_popen2e_noblock 152 | i, o, t = Open3.popen2e(RUBY, '-e', 'STDOUT.print STDIN.read') 153 | i.print "baz" 154 | i.close 155 | assert_equal("baz", o.read) 156 | ensure 157 | i.close 158 | o.close 159 | t.join 160 | end 161 | 162 | def test_capture3 163 | o, e, s = Open3.capture3(RUBY, '-e', 'i=STDIN.read; print i+"o"; STDOUT.flush; STDERR.print i+"e"', :stdin_data=>"i") 164 | assert_equal("io", o) 165 | assert_equal("ie", e) 166 | assert(s.success?) 167 | end 168 | 169 | def test_capture3_stdin_data_io 170 | IO.pipe {|r, w| 171 | w.write "i" 172 | w.close 173 | o, e, s = Open3.capture3(RUBY, '-e', 'i=STDIN.read; print i+"o"; STDOUT.flush; STDERR.print i+"e"', :stdin_data=>r) 174 | assert_equal("io", o) 175 | assert_equal("ie", e) 176 | assert(s.success?) 177 | } 178 | end 179 | 180 | def test_capture3_flip 181 | o, e, s = Open3.capture3(RUBY, '-e', 'STDOUT.sync=true; 1000.times { print "o"*1000; STDERR.print "e"*1000 }') 182 | assert_equal("o"*1000000, o) 183 | assert_equal("e"*1000000, e) 184 | assert(s.success?) 185 | end 186 | 187 | def test_capture2 188 | o, s = Open3.capture2(RUBY, '-e', 'i=STDIN.read; print i+"o"', :stdin_data=>"i") 189 | assert_equal("io", o) 190 | assert(s.success?) 191 | end 192 | 193 | def test_capture2_stdin_data_io 194 | IO.pipe {|r, w| 195 | w.write "i" 196 | w.close 197 | o, s = Open3.capture2(RUBY, '-e', 'i=STDIN.read; print i+"o"', :stdin_data=>r) 198 | assert_equal("io", o) 199 | assert(s.success?) 200 | } 201 | end 202 | 203 | def test_capture2e 204 | oe, s = Open3.capture2e(RUBY, '-e', 'i=STDIN.read; print i+"o"; STDOUT.flush; STDERR.print i+"e"', :stdin_data=>"i") 205 | assert_equal("ioie", oe) 206 | assert(s.success?) 207 | end 208 | 209 | def test_capture2e_stdin_data_io 210 | IO.pipe {|r, w| 211 | w.write "i" 212 | w.close 213 | oe, s = Open3.capture2e(RUBY, '-e', 'i=STDIN.read; print i+"o"; STDOUT.flush; STDERR.print i+"e"', :stdin_data=>r) 214 | assert_equal("ioie", oe) 215 | assert(s.success?) 216 | } 217 | end 218 | 219 | def test_capture3_stdin_data 220 | o, e, s = Open3.capture3(RUBY, '-e', '', :stdin_data=>"z"*(1024*1024)) 221 | assert_equal("", o) 222 | assert_equal("", e) 223 | assert(s.success?) 224 | end 225 | 226 | def test_capture2_stdin_data 227 | o, s = Open3.capture2(RUBY, '-e', '', :stdin_data=>"z"*(1024*1024)) 228 | assert_equal("", o) 229 | assert(s.success?) 230 | end 231 | 232 | def test_capture2e_stdin_data 233 | oe, s = Open3.capture2e(RUBY, '-e', '', :stdin_data=>"z"*(1024*1024)) 234 | assert_equal("", oe) 235 | assert(s.success?) 236 | end 237 | 238 | def test_pipeline_rw 239 | Open3.pipeline_rw([RUBY, '-e', 'print STDIN.read + "1"'], 240 | [RUBY, '-e', 'print STDIN.read + "2"']) {|i,o,ts| 241 | assert_kind_of(IO, i) 242 | assert_kind_of(IO, o) 243 | assert_kind_of(Array, ts) 244 | assert_equal(2, ts.length) 245 | ts.each {|t| assert_kind_of(Thread, t) } 246 | i.print "0" 247 | i.close 248 | assert_equal("012", o.read) 249 | ts.each {|t| 250 | assert(t.value.success?) 251 | } 252 | } 253 | end 254 | 255 | def test_pipeline_r 256 | Open3.pipeline_r([RUBY, '-e', 'print "1"'], 257 | [RUBY, '-e', 'print STDIN.read + "2"']) {|o,ts| 258 | assert_kind_of(IO, o) 259 | assert_kind_of(Array, ts) 260 | assert_equal(2, ts.length) 261 | ts.each {|t| assert_kind_of(Thread, t) } 262 | assert_equal("12", o.read) 263 | ts.each {|t| 264 | assert(t.value.success?) 265 | } 266 | } 267 | end 268 | 269 | def test_pipeline_w 270 | command = [RUBY, '-e', 's=STDIN.read; print s[1..-1]; exit s[0] == ?t'] 271 | str = 'ttftff' 272 | Open3.pipeline_w(*[command]*str.length) {|i,ts| 273 | assert_kind_of(IO, i) 274 | assert_kind_of(Array, ts) 275 | assert_equal(str.length, ts.length) 276 | ts.each {|t| assert_kind_of(Thread, t) } 277 | i.print str 278 | i.close 279 | ts.each_with_index {|t, ii| 280 | assert_equal(str[ii] == ?t, t.value.success?) 281 | } 282 | } 283 | end 284 | 285 | def test_pipeline_start 286 | command = [RUBY, '-e', 's=STDIN.read; print s[1..-1]; exit s[0] == ?t'] 287 | str = 'ttftff' 288 | Open3.pipeline_start([RUBY, '-e', 'print ARGV[0]', str], 289 | *([command]*str.length)) {|ts| 290 | assert_kind_of(Array, ts) 291 | assert_equal(str.length+1, ts.length) 292 | ts.each {|t| assert_kind_of(Thread, t) } 293 | ts.each_with_index {|t, i| 294 | if i == 0 295 | assert(t.value.success?) 296 | else 297 | assert_equal(str[i-1] == ?t, t.value.success?) 298 | end 299 | } 300 | } 301 | end 302 | 303 | def test_pipeline_start_noblock 304 | ts = Open3.pipeline_start([RUBY, '-e', '']) 305 | assert_kind_of(Array, ts) 306 | assert_equal(1, ts.length) 307 | ts.each {|t| assert_kind_of(Thread, t) } 308 | t = ts[0] 309 | assert(t.value.success?) 310 | end 311 | 312 | def test_pipeline 313 | command = [RUBY, '-e', 's=STDIN.read; print s[1..-1]; exit s[0] == ?t'] 314 | str = 'ttftff' 315 | ss = Open3.pipeline([RUBY, '-e', 'print ARGV[0]', str], 316 | *([command]*str.length)) 317 | assert_kind_of(Array, ss) 318 | assert_equal(str.length+1, ss.length) 319 | ss.each {|s| assert_kind_of(Process::Status, s) } 320 | ss.each_with_index {|s, i| 321 | if i == 0 322 | assert(s.success?) 323 | else 324 | assert_equal(str[i-1] == ?t, s.success?) 325 | end 326 | } 327 | end 328 | 329 | def test_integer_and_symbol_key 330 | command = [RUBY, '-e', 'puts "test_integer_and_symbol_key"'] 331 | out, status = Open3.capture2(*command, :chdir => '.', 2 => IO::NULL) 332 | assert_equal("test_integer_and_symbol_key\n", out) 333 | assert_predicate(status, :success?) 334 | end 335 | end 336 | --------------------------------------------------------------------------------