├── .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 |
--------------------------------------------------------------------------------