Skip to content

Commit 031f319

Browse files
authored
Merge pull request #2699 from ksss/more-open3
Add type of `Open3.popen3`
2 parents 468c20d + 9aa9051 commit 031f319

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

stdlib/open3/0/open3.rbs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,138 @@ module Open3
245245
#
246246
def self?.capture2e: (*String, ?stdin_data: String, ?binmode: boolish) -> [String, Process::Status]
247247
| (env, *String, ?stdin_data: String, ?binmode: boolish) -> [String, Process::Status]
248+
249+
# <!--
250+
# rdoc-file=lib/open3.rb
251+
# - Open3.popen3([env, ] command_line, options = {}) -> [stdin, stdout, stderr, wait_thread]
252+
# - Open3.popen3([env, ] exe_path, *args, options = {}) -> [stdin, stdout, stderr, wait_thread]
253+
# - Open3.popen3([env, ] command_line, options = {}) {|stdin, stdout, stderr, wait_thread| ... } -> object
254+
# - Open3.popen3([env, ] exe_path, *args, options = {}) {|stdin, stdout, stderr, wait_thread| ... } -> object
255+
# -->
256+
# Basically a wrapper for [Process.spawn](rdoc-ref:Process.spawn) that:
257+
#
258+
# * Creates a child process, by calling Process.spawn with the given
259+
# arguments.
260+
# * Creates streams `stdin`, `stdout`, and `stderr`, which are the standard
261+
# input, standard output, and standard error streams in the child process.
262+
# * Creates thread `wait_thread` that waits for the child process to exit; the
263+
# thread has method `pid`, which returns the process ID of the child
264+
# process.
265+
#
266+
# With no block given, returns the array `[stdin, stdout, stderr, wait_thread]`.
267+
# The caller should close each of the three returned streams.
268+
#
269+
# stdin, stdout, stderr, wait_thread = Open3.popen3('echo')
270+
# # => [#<IO:fd 8>, #<IO:fd 10>, #<IO:fd 12>, #<Process::Waiter:0x00007f58d5428f58 run>]
271+
# stdin.close
272+
# stdout.close
273+
# stderr.close
274+
# wait_thread.pid # => 2210481
275+
# wait_thread.value # => #<Process::Status: pid 2210481 exit 0>
276+
#
277+
# With a block given, calls the block with the four variables (three streams and
278+
# the wait thread) and returns the block's return value. The caller need not
279+
# close the streams:
280+
#
281+
# Open3.popen3('echo') do |stdin, stdout, stderr, wait_thread|
282+
# p stdin
283+
# p stdout
284+
# p stderr
285+
# p wait_thread
286+
# p wait_thread.pid
287+
# p wait_thread.value
288+
# end
289+
#
290+
# Output:
291+
#
292+
# #<IO:fd 6>
293+
# #<IO:fd 7>
294+
# #<IO:fd 9>
295+
# #<Process::Waiter:0x00007f58d53606e8 sleep>
296+
# 2211047
297+
# #<Process::Status: pid 2211047 exit 0>
298+
#
299+
# Like Process.spawn, this method has potential security vulnerabilities if
300+
# called with untrusted input; see [Command
301+
# Injection](rdoc-ref:command_injection.rdoc@Command+Injection).
302+
#
303+
# Unlike Process.spawn, this method waits for the child process to exit before
304+
# returning, so the caller need not do so.
305+
#
306+
# If the first argument is a hash, it becomes leading argument `env` in the call
307+
# to Process.spawn; see [Execution
308+
# Environment](rdoc-ref:Process@Execution+Environment).
309+
#
310+
# If the last argument is a hash, it becomes trailing argument `options` in the
311+
# call to Process.spawn; see [Execution
312+
# Options](rdoc-ref:Process@Execution+Options).
313+
#
314+
# The single required argument is one of the following:
315+
#
316+
# * `command_line` if it is a string, and if it begins with a shell reserved
317+
# word or special built-in, or if it contains one or more metacharacters.
318+
# * `exe_path` otherwise.
319+
#
320+
# **Argument `command_line`**
321+
#
322+
# String argument `command_line` is a command line to be passed to a shell; it
323+
# must begin with a shell reserved word, begin with a special built-in, or
324+
# contain meta characters:
325+
#
326+
# Open3.popen3('if true; then echo "Foo"; fi') {|*args| p args } # Shell reserved word.
327+
# Open3.popen3('echo') {|*args| p args } # Built-in.
328+
# Open3.popen3('date > date.tmp') {|*args| p args } # Contains meta character.
329+
#
330+
# Output (similar for each call above):
331+
#
332+
# [#<IO:(closed)>, #<IO:(closed)>, #<IO:(closed)>, #<Process::Waiter:0x00007f58d52f28c8 dead>]
333+
#
334+
# The command line may also contain arguments and options for the command:
335+
#
336+
# Open3.popen3('echo "Foo"') { |i, o, e, t| o.gets }
337+
# "Foo\n"
338+
#
339+
# **Argument `exe_path`**
340+
#
341+
# Argument `exe_path` is one of the following:
342+
#
343+
# * The string path to an executable to be called.
344+
# * A 2-element array containing the path to an executable and the string to
345+
# be used as the name of the executing process.
346+
#
347+
# Example:
348+
#
349+
# Open3.popen3('/usr/bin/date') { |i, o, e, t| o.gets }
350+
# # => "Wed Sep 27 02:56:44 PM CDT 2023\n"
351+
#
352+
# Ruby invokes the executable directly, with no shell and no shell expansion:
353+
#
354+
# Open3.popen3('doesnt_exist') { |i, o, e, t| o.gets } # Raises Errno::ENOENT
355+
#
356+
# If one or more `args` is given, each is an argument or option to be passed to
357+
# the executable:
358+
#
359+
# Open3.popen3('echo', 'C #') { |i, o, e, t| o.gets }
360+
# # => "C #\n"
361+
# Open3.popen3('echo', 'hello', 'world') { |i, o, e, t| o.gets }
362+
# # => "hello world\n"
363+
#
364+
# Take care to avoid deadlocks. Output streams `stdout` and `stderr` have
365+
# fixed-size buffers, so reading extensively from one but not the other can
366+
# cause a deadlock when the unread buffer fills. To avoid that, `stdout` and
367+
# `stderr` should be read simultaneously (using threads or IO.select).
368+
#
369+
# Related:
370+
#
371+
# * Open3.popen2: Makes the standard input and standard output streams of the
372+
# child process available as separate streams, with no access to the
373+
# standard error stream.
374+
# * Open3.popen2e: Makes the standard input and the merge of the standard
375+
# output and standard error streams of the child process available as
376+
# separate streams.
377+
#
378+
def self?.popen3: (*String, ?unsetenv_others: bool, ?pgroup: true | Integer, ?umask: Integer, ?close_others: bool, ?chdir: String) -> [IO, IO, IO, Process::Waiter]
379+
| (env, *String, ?unsetenv_others: bool, ?pgroup: true | Integer, ?umask: Integer, ?close_others: bool, ?chdir: String) -> [IO, IO, IO, Process::Waiter]
380+
| [T] (*String, ?unsetenv_others: bool, ?pgroup: true | Integer, ?umask: Integer, ?close_others: bool, ?chdir: String) { (IO, IO, IO, Process::Waiter) -> T } -> T
381+
| [T] (env, *String, ?unsetenv_others: bool, ?pgroup: true | Integer, ?umask: Integer, ?close_others: bool, ?chdir: String) { (IO, IO, IO, Process::Waiter) -> T } -> T
248382
end

test/stdlib/Open3_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ def test_capture2e
2727
assert_send_type "(::Hash[::String, ::String], *::String) -> [ ::String, ::Process::Status ]",
2828
Open3, :capture2e, { 'FOO' => 'BAR' }, "echo $FOO"
2929
end
30+
31+
def test_popen3
32+
assert_send_type "(::String) -> [ ::IO, ::IO, ::IO, ::Process::Waiter ]",
33+
Open3, :popen3, 'echo "Foo"'
34+
assert_send_type "(::String, unsetenv_others: bool) -> [ ::IO, ::IO, ::IO, ::Process::Waiter ]",
35+
Open3, :popen3, 'env', unsetenv_others: true
36+
assert_send_type "(::String, close_others: bool) -> [ ::IO, ::IO, ::IO, ::Process::Waiter ]",
37+
Open3, :popen3, 'env', close_others: true
38+
assert_send_type "(::String, chdir: ::String) -> [ ::IO, ::IO, ::IO, ::Process::Waiter ]",
39+
Open3, :popen3, 'echo "Foo"', chdir: '.'
40+
assert_send_type "(::Hash[::String, ::String], ::String) -> [ ::IO, ::IO, ::IO, ::Process::Waiter ]",
41+
Open3, :popen3, { 'FOO' => 'BAR' }, "echo $FOO"
42+
43+
assert_send_type "(::String) { (::IO, ::IO, ::IO, ::Process::Waiter) -> [::IO, ::IO, ::IO, ::Process::Waiter] } -> [::IO, ::IO, ::IO, ::Process::Waiter]",
44+
Open3, :popen3, 'echo "Foo"' do |*args| args end
45+
assert_send_type "(::Hash[::String, ::String], ::String) { (::IO, ::IO, ::IO, ::Process::Waiter) -> [::IO, ::IO, ::IO, ::Process::Waiter] } -> [::IO, ::IO, ::IO, ::Process::Waiter]",
46+
Open3, :popen3, { 'FOO' => 'BAR' }, 'echo $FOO' do |*args| args end
47+
end
3048
end
3149

3250
class Open3InstanceTest < Test::Unit::TestCase
@@ -48,4 +66,9 @@ def test_capture2e
4866
assert_send_type "(*::String) -> [ ::String, ::Process::Status ]",
4967
CustomOpen3.new, :capture2e, 'echo "Foo"'
5068
end
69+
70+
def test_popen3
71+
assert_send_type "(::String) -> [ ::IO, ::IO, ::IO, ::Process::Waiter ]",
72+
CustomOpen3.new, :popen3, 'echo "Foo"'
73+
end
5174
end

0 commit comments

Comments
 (0)