toys-core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -0
- data/LICENSE.md +16 -24
- data/README.md +307 -59
- data/docs/guide.md +44 -4
- data/lib/toys-core.rb +58 -49
- data/lib/toys/acceptor.rb +672 -0
- data/lib/toys/alias.rb +106 -0
- data/lib/toys/arg_parser.rb +624 -0
- data/lib/toys/cli.rb +422 -181
- data/lib/toys/compat.rb +83 -0
- data/lib/toys/completion.rb +442 -0
- data/lib/toys/context.rb +354 -0
- data/lib/toys/core_version.rb +18 -26
- data/lib/toys/dsl/flag.rb +213 -56
- data/lib/toys/dsl/flag_group.rb +237 -51
- data/lib/toys/dsl/positional_arg.rb +210 -0
- data/lib/toys/dsl/tool.rb +968 -317
- data/lib/toys/errors.rb +46 -28
- data/lib/toys/flag.rb +821 -0
- data/lib/toys/flag_group.rb +282 -0
- data/lib/toys/input_file.rb +18 -26
- data/lib/toys/loader.rb +110 -100
- data/lib/toys/middleware.rb +24 -31
- data/lib/toys/mixin.rb +90 -59
- data/lib/toys/module_lookup.rb +125 -0
- data/lib/toys/positional_arg.rb +184 -0
- data/lib/toys/source_info.rb +192 -0
- data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
- data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
- data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
- data/lib/toys/standard_middleware/show_help.rb +130 -113
- data/lib/toys/standard_middleware/show_root_version.rb +29 -35
- data/lib/toys/standard_mixins/exec.rb +116 -78
- data/lib/toys/standard_mixins/fileutils.rb +16 -24
- data/lib/toys/standard_mixins/gems.rb +29 -30
- data/lib/toys/standard_mixins/highline.rb +34 -41
- data/lib/toys/standard_mixins/terminal.rb +72 -26
- data/lib/toys/template.rb +51 -35
- data/lib/toys/tool.rb +1161 -206
- data/lib/toys/utils/completion_engine.rb +171 -0
- data/lib/toys/utils/exec.rb +279 -182
- data/lib/toys/utils/gems.rb +58 -49
- data/lib/toys/utils/help_text.rb +117 -111
- data/lib/toys/utils/terminal.rb +69 -62
- data/lib/toys/wrappable_string.rb +162 -0
- metadata +24 -22
- data/lib/toys/definition/acceptor.rb +0 -191
- data/lib/toys/definition/alias.rb +0 -112
- data/lib/toys/definition/arg.rb +0 -140
- data/lib/toys/definition/flag.rb +0 -370
- data/lib/toys/definition/flag_group.rb +0 -205
- data/lib/toys/definition/source_info.rb +0 -190
- data/lib/toys/definition/tool.rb +0 -842
- data/lib/toys/dsl/arg.rb +0 -132
- data/lib/toys/runner.rb +0 -188
- data/lib/toys/standard_middleware.rb +0 -47
- data/lib/toys/utils/module_lookup.rb +0 -135
- data/lib/toys/utils/wrappable_string.rb +0 -165
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2019 Daniel Azuma
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
21
|
+
# IN THE SOFTWARE.
|
22
|
+
;
|
23
|
+
|
24
|
+
require "shellwords"
|
25
|
+
|
26
|
+
module Toys
|
27
|
+
module Utils
|
28
|
+
##
|
29
|
+
# Implementations of tab completion.
|
30
|
+
#
|
31
|
+
# This module is not loaded by default. Before using it directly, you must
|
32
|
+
# `require "toys/utils/completion_engine"`
|
33
|
+
#
|
34
|
+
module CompletionEngine
|
35
|
+
##
|
36
|
+
# A completion engine for bash.
|
37
|
+
#
|
38
|
+
class Bash
|
39
|
+
##
|
40
|
+
# Create a bash completion engine.
|
41
|
+
#
|
42
|
+
# @param cli [Toys::CLI] The CLI.
|
43
|
+
#
|
44
|
+
def initialize(cli)
|
45
|
+
@cli = cli
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Perform completion in the current shell environment, which must
|
50
|
+
# include settings for the `COMP_LINE` and `COMP_POINT` environment
|
51
|
+
# variables. Prints out completion candidates, one per line, and
|
52
|
+
# returns a status code indicating the result.
|
53
|
+
#
|
54
|
+
# * **0** for success.
|
55
|
+
# * **1** if completion failed.
|
56
|
+
# * **2** if the environment is incorrect (e.g. expected environment
|
57
|
+
# variables not found)
|
58
|
+
#
|
59
|
+
# @return [Integer] status code
|
60
|
+
#
|
61
|
+
def run
|
62
|
+
return 2 if !::ENV.key?("COMP_LINE") || !::ENV.key?("COMP_POINT")
|
63
|
+
line = ::ENV["COMP_LINE"].to_s
|
64
|
+
point = ::ENV["COMP_POINT"].to_i
|
65
|
+
point = line.length if point.negative?
|
66
|
+
line = line[0, point]
|
67
|
+
completions = run_internal(line)
|
68
|
+
if completions
|
69
|
+
completions.each { |completion| puts completion }
|
70
|
+
0
|
71
|
+
else
|
72
|
+
1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Internal completion method designed for testing.
|
78
|
+
# @private
|
79
|
+
#
|
80
|
+
def run_internal(line)
|
81
|
+
words = CompletionEngine.split(line)
|
82
|
+
quote_type, last = words.pop
|
83
|
+
return nil unless words.shift
|
84
|
+
words.map! { |_type, word| word }
|
85
|
+
prefix = ""
|
86
|
+
if (match = /\A(.*[=:])(.*)\z/.match(last))
|
87
|
+
prefix = match[1]
|
88
|
+
last = match[2]
|
89
|
+
end
|
90
|
+
context = Completion::Context.new(
|
91
|
+
cli: @cli, previous_words: words, fragment_prefix: prefix, fragment: last,
|
92
|
+
params: {shell: :bash, quote_type: quote_type}
|
93
|
+
)
|
94
|
+
candidates = @cli.completion.call(context)
|
95
|
+
candidates.uniq.sort.map do |candidate|
|
96
|
+
CompletionEngine.format_candidate(candidate, quote_type)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class << self
|
102
|
+
## @private
|
103
|
+
def split(line)
|
104
|
+
words = []
|
105
|
+
field = ::String.new
|
106
|
+
quote_type = nil
|
107
|
+
line.scan(split_regex) do |word, sqw, dqw, esc, garbage, sep|
|
108
|
+
raise ArgumentError, "Didn't expect garbage: #{line.inspect}" if garbage
|
109
|
+
field << field_str(word, sqw, dqw, esc)
|
110
|
+
quote_type = update_quote_type(quote_type, sqw, dqw)
|
111
|
+
if sep
|
112
|
+
words << [quote_type, field]
|
113
|
+
quote_type = nil
|
114
|
+
field = sep.empty? ? nil : ::String.new
|
115
|
+
end
|
116
|
+
end
|
117
|
+
words << [quote_type, field] if field
|
118
|
+
words
|
119
|
+
end
|
120
|
+
|
121
|
+
## @private
|
122
|
+
def format_candidate(candidate, quote_type)
|
123
|
+
str = candidate.to_s
|
124
|
+
partial = candidate.is_a?(Completion::Candidate) ? candidate.partial? : false
|
125
|
+
quote_type = nil if candidate.string.include?("'") && quote_type == :single
|
126
|
+
case quote_type
|
127
|
+
when :single
|
128
|
+
partial ? "'#{str}" : "'#{str}' "
|
129
|
+
when :double
|
130
|
+
str = str.gsub(/[$`"\\\n]/, '\\\\\\1')
|
131
|
+
partial ? "\"#{str}" : "\"#{str}\" "
|
132
|
+
else
|
133
|
+
str = ::Shellwords.escape(str)
|
134
|
+
partial ? str : "#{str} "
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def split_regex
|
141
|
+
word_re = "([^\\s\\\\\\'\\\"]+)"
|
142
|
+
sq_re = "'([^\\']*)(?:'|\\z)"
|
143
|
+
dq_re = "\"((?:[^\\\"\\\\]|\\\\.)*)(?:\"|\\z)"
|
144
|
+
esc_re = "(\\\\.?)"
|
145
|
+
sep_re = "(\\s|\\z)"
|
146
|
+
/\G\s*(?>#{word_re}|#{sq_re}|#{dq_re}|#{esc_re}|(\S))#{sep_re}?/m
|
147
|
+
end
|
148
|
+
|
149
|
+
def field_str(word, sqw, dqw, esc)
|
150
|
+
word ||
|
151
|
+
sqw ||
|
152
|
+
dqw&.gsub(/\\([$`"\\\n])/, '\\1') ||
|
153
|
+
esc&.gsub(/\\(.)/, '\\1') ||
|
154
|
+
""
|
155
|
+
end
|
156
|
+
|
157
|
+
def update_quote_type(quote_type, sqw, dqw)
|
158
|
+
if quote_type
|
159
|
+
:multi
|
160
|
+
elsif sqw
|
161
|
+
:single
|
162
|
+
elsif dqw
|
163
|
+
:double
|
164
|
+
else
|
165
|
+
:bare
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/lib/toys/utils/exec.rb
CHANGED
@@ -1,32 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Copyright
|
3
|
+
# Copyright 2019 Daniel Azuma
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
6
11
|
#
|
7
|
-
#
|
8
|
-
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
9
14
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# derived from this software without specific prior written permission.
|
18
|
-
#
|
19
|
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
|
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21
|
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
22
|
-
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
23
|
-
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
24
|
-
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
25
|
-
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
26
|
-
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
27
|
-
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
28
|
-
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
29
|
-
# POSSIBILITY OF SUCH DAMAGE.
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
21
|
+
# IN THE SOFTWARE.
|
30
22
|
;
|
31
23
|
|
32
24
|
require "logger"
|
@@ -41,36 +33,43 @@ module Toys
|
|
41
33
|
# processes and their streams. It also provides shortcuts for common cases
|
42
34
|
# such as invoking Ruby in a subprocess or capturing output in a string.
|
43
35
|
#
|
36
|
+
# This class is not loaded by default. Before using it directly, you should
|
37
|
+
# `require "toys/utils/exec"`
|
38
|
+
#
|
44
39
|
# ## Configuration options
|
45
40
|
#
|
46
41
|
# A variety of options can be used to control subprocesses. These include:
|
47
42
|
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
43
|
+
# * `:name` (Object) An optional object that can be used to identify this
|
44
|
+
# subprocess. It is available in the controller and result objects.
|
45
|
+
# * `:env` (Hash) Environment variables to pass to the subprocess
|
46
|
+
# * `:logger` (Logger) Logger to use for logging the actual command. If
|
47
|
+
# not present, the command is not logged.
|
48
|
+
# * `:log_level` (Integer,false) Level for logging the actual command.
|
49
|
+
# Defaults to Logger::INFO if not present. You may also pass `false` to
|
50
|
+
# disable logging of the command.
|
51
|
+
# * `:log_cmd` (String) The string logged for the actual command.
|
52
|
+
# Defaults to the `inspect` representation of the command.
|
53
|
+
# * `:background` (Boolean) Runs the process in the background, returning
|
54
|
+
# a controller object instead of a result object.
|
55
|
+
# * `:result_callback` (Proc) Called and passed the result object when a
|
56
|
+
# subprocess exits.
|
57
|
+
# * `:in` Connects the input stream of the subprocess. See the section on
|
58
|
+
# stream handling.
|
59
|
+
# * `:out` Connects the standard output stream of the subprocess. See the
|
60
|
+
# section on stream handling.
|
61
|
+
# * `:err` Connects the standard error stream of the subprocess. See the
|
62
|
+
# section on stream handling.
|
64
63
|
#
|
65
64
|
# In addition, the following options recognized by `Process#spawn` are
|
66
65
|
# supported.
|
67
66
|
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
67
|
+
# * `:chdir`
|
68
|
+
# * `:close_others`
|
69
|
+
# * `:new_pgroup`
|
70
|
+
# * `:pgroup`
|
71
|
+
# * `:umask`
|
72
|
+
# * `:unsetenv_others`
|
74
73
|
#
|
75
74
|
# Any other options are ignored.
|
76
75
|
#
|
@@ -95,48 +94,53 @@ module Toys
|
|
95
94
|
# Following is a full list of the stream handling options, along with how
|
96
95
|
# to specify them using the `:in`, `:out`, and `:err` options.
|
97
96
|
#
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
97
|
+
# * **Close the stream:** You may close the stream by passing `:close` as
|
98
|
+
# the option value. This is the same as passing `:close` to
|
99
|
+
# `Process#spawn`.
|
100
|
+
# * **Redirect to null:** You may redirect to a null stream by passing
|
101
|
+
# `:null` as the option value. This connects to a stream that is not
|
102
|
+
# closed but contains no data, i.e. `/dev/null` on unix systems. This
|
103
|
+
# is the default if the subprocess is run in the background.
|
104
|
+
# * **Inherit parent stream:** You may inherit the corresponding stream
|
105
|
+
# in the parent process by passing `:inherit` as the option value. This
|
106
|
+
# is the default if the subprocess is *not* run in the background.
|
107
|
+
# * **Redirect to a file:** You may redirect to a file. This reads from
|
108
|
+
# an existing file when connected to `:in`, and creates or appends to a
|
109
|
+
# file when connected to `:out` or `:err`. To specify a file, use the
|
110
|
+
# setting `[:file, "/path/to/file"]`. You may also, when writing a
|
111
|
+
# file, append an optional mode and permission code to the array. For
|
112
|
+
# example, `[:file, "/path/to/file", "a", 0644]`.
|
113
|
+
# * **Redirect to an IO object:** You may redirect to an IO object in the
|
114
|
+
# parent process, by passing the IO object as the option value. You may
|
115
|
+
# use any IO object. For example, you could connect the child's output
|
116
|
+
# to the parent's error using `out: $stderr`, or you could connect to
|
117
|
+
# an existing File stream. Unlike `Process#spawn`, this works for IO
|
118
|
+
# objects that do not have a corresponding file descriptor (such as
|
119
|
+
# StringIO objects). In such a case, a thread will be spawned to pipe
|
120
|
+
# the IO data through to the child process.
|
121
|
+
# * **Combine with another child stream:** You may redirect one child
|
122
|
+
# output stream to another, to combine them. To merge the child's error
|
123
|
+
# stream into its output stream, use `err: [:child, :out]`.
|
124
|
+
# * **Read from a string:** You may pass a string to the input stream by
|
125
|
+
# setting `[:string, "the string"]`. This works only for `:in`.
|
126
|
+
# * **Capture output stream:** You may capture a stream and make it
|
127
|
+
# available on the {Toys::Utils::Exec::Result} object, using the
|
128
|
+
# setting `:capture`. This works only for the `:out` and `:err`
|
129
|
+
# streams.
|
130
|
+
# * **Use the controller:** You may hook a stream to the controller using
|
131
|
+
# the setting `:controller`. You can then manipulate the stream via the
|
132
|
+
# controller. If you pass a block to {Toys::Utils::Exec#exec}, it
|
133
|
+
# yields the {Toys::Utils::Exec::Controller}, giving you access to
|
134
|
+
# streams.
|
134
135
|
#
|
135
136
|
class Exec
|
136
137
|
##
|
137
138
|
# Create an exec service.
|
138
139
|
#
|
139
|
-
# @param [
|
140
|
+
# @param block [Proc] A block that is called if a key is not found. It is
|
141
|
+
# passed the unknown key, and expected to return a default value
|
142
|
+
# (which can be nil).
|
143
|
+
# @param opts [Hash] Initial default options.
|
140
144
|
#
|
141
145
|
def initialize(opts = {}, &block)
|
142
146
|
@default_opts = Opts.new(&block).add(opts)
|
@@ -145,7 +149,8 @@ module Toys
|
|
145
149
|
##
|
146
150
|
# Set default options
|
147
151
|
#
|
148
|
-
# @param [Hash]
|
152
|
+
# @param opts [Hash] New default options to set
|
153
|
+
# @return [self]
|
149
154
|
#
|
150
155
|
def configure_defaults(opts = {})
|
151
156
|
@default_opts.add(opts)
|
@@ -159,15 +164,16 @@ module Toys
|
|
159
164
|
# If the process is not set to run in the background, and a block is
|
160
165
|
# provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
|
161
166
|
#
|
162
|
-
# @param [String,Array<String>]
|
163
|
-
# @param [Hash]
|
167
|
+
# @param cmd [String,Array<String>] The command to execute.
|
168
|
+
# @param opts [Hash] The command options. See the section on
|
164
169
|
# configuration options in the {Toys::Utils::Exec} module docs.
|
165
170
|
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
166
171
|
# for the subprocess streams.
|
167
172
|
#
|
168
|
-
# @return [Toys::Utils::Exec::Controller
|
169
|
-
#
|
170
|
-
#
|
173
|
+
# @return [Toys::Utils::Exec::Controller] The subprocess controller, if
|
174
|
+
# the process is running in the background.
|
175
|
+
# @return [Toys::Utils::Exec::Result] The result, if the process ran in
|
176
|
+
# the foreground.
|
171
177
|
#
|
172
178
|
def exec(cmd, opts = {}, &block)
|
173
179
|
exec_opts = Opts.new(@default_opts).add(opts)
|
@@ -191,15 +197,16 @@ module Toys
|
|
191
197
|
# If the process is not set to run in the background, and a block is
|
192
198
|
# provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
|
193
199
|
#
|
194
|
-
# @param [String,Array<String>]
|
195
|
-
# @param [Hash]
|
200
|
+
# @param args [String,Array<String>] The arguments to ruby.
|
201
|
+
# @param opts [Hash] The command options. See the section on
|
196
202
|
# configuration options in the {Toys::Utils::Exec} module docs.
|
197
203
|
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
198
204
|
# for the subprocess streams.
|
199
205
|
#
|
200
|
-
# @return [Toys::Utils::Exec::Controller
|
201
|
-
#
|
202
|
-
#
|
206
|
+
# @return [Toys::Utils::Exec::Controller] The subprocess controller, if
|
207
|
+
# the process is running in the background.
|
208
|
+
# @return [Toys::Utils::Exec::Result] The result, if the process ran in
|
209
|
+
# the foreground.
|
203
210
|
#
|
204
211
|
def exec_ruby(args, opts = {}, &block)
|
205
212
|
cmd = args.is_a?(::Array) ? [::RbConfig.ruby] + args : "#{::RbConfig.ruby} #{args}"
|
@@ -214,15 +221,16 @@ module Toys
|
|
214
221
|
# If the process is not set to run in the background, and a block is
|
215
222
|
# provided, a {Toys::Utils::Exec::Controller} will be yielded to it.
|
216
223
|
#
|
217
|
-
# @param [Proc]
|
218
|
-
# @param [Hash]
|
224
|
+
# @param func [Proc] The proc to call.
|
225
|
+
# @param opts [Hash] The command options. See the section on
|
219
226
|
# configuration options in the {Toys::Utils::Exec} module docs.
|
220
227
|
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
221
228
|
# for the subprocess streams.
|
222
229
|
#
|
223
|
-
# @return [Toys::Utils::Exec::Controller
|
224
|
-
#
|
225
|
-
#
|
230
|
+
# @return [Toys::Utils::Exec::Controller] The subprocess controller, if
|
231
|
+
# the process is running in the background.
|
232
|
+
# @return [Toys::Utils::Exec::Result] The result, if the process ran in
|
233
|
+
# the foreground.
|
226
234
|
#
|
227
235
|
def exec_proc(func, opts = {}, &block)
|
228
236
|
exec_opts = Opts.new(@default_opts).add(opts)
|
@@ -240,8 +248,8 @@ module Toys
|
|
240
248
|
# If a block is provided, a {Toys::Utils::Exec::Controller} will be
|
241
249
|
# yielded to it.
|
242
250
|
#
|
243
|
-
# @param [String,Array<String>]
|
244
|
-
# @param [Hash]
|
251
|
+
# @param cmd [String,Array<String>] The command to execute.
|
252
|
+
# @param opts [Hash] The command options. See the section on
|
245
253
|
# configuration options in the {Toys::Utils::Exec} module docs.
|
246
254
|
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
247
255
|
# for the subprocess streams.
|
@@ -261,8 +269,8 @@ module Toys
|
|
261
269
|
# If a block is provided, a {Toys::Utils::Exec::Controller} will be
|
262
270
|
# yielded to it.
|
263
271
|
#
|
264
|
-
# @param [String,Array<String>]
|
265
|
-
# @param [Hash]
|
272
|
+
# @param args [String,Array<String>] The arguments to ruby.
|
273
|
+
# @param opts [Hash] The command options. See the section on
|
266
274
|
# configuration options in the {Toys::Utils::Exec} module docs.
|
267
275
|
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
268
276
|
# for the subprocess streams.
|
@@ -282,8 +290,8 @@ module Toys
|
|
282
290
|
# If a block is provided, a {Toys::Utils::Exec::Controller} will be
|
283
291
|
# yielded to it.
|
284
292
|
#
|
285
|
-
# @param [Proc]
|
286
|
-
# @param [Hash]
|
293
|
+
# @param func [Proc] The proc to call.
|
294
|
+
# @param opts [Hash] The command options. See the section on
|
287
295
|
# configuration options in the {Toys::Utils::Exec} module docs.
|
288
296
|
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
289
297
|
# for the subprocess streams.
|
@@ -298,8 +306,11 @@ module Toys
|
|
298
306
|
# Execute the given string in a shell. Returns the exit code.
|
299
307
|
# Cannot be run in the background.
|
300
308
|
#
|
301
|
-
#
|
302
|
-
#
|
309
|
+
# If a block is provided, a {Toys::Utils::Exec::Controller} will be
|
310
|
+
# yielded to it.
|
311
|
+
#
|
312
|
+
# @param cmd [String] The shell command to execute.
|
313
|
+
# @param opts [Hash] The command options. See the section on
|
303
314
|
# configuration options in the {Toys::Utils::Exec} module docs.
|
304
315
|
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
305
316
|
# for the subprocess streams.
|
@@ -319,33 +330,35 @@ module Toys
|
|
319
330
|
# Option keys that belong to exec configuration
|
320
331
|
# @private
|
321
332
|
#
|
322
|
-
CONFIG_KEYS =
|
323
|
-
argv0
|
324
|
-
background
|
325
|
-
cli
|
326
|
-
env
|
327
|
-
err
|
328
|
-
in
|
329
|
-
logger
|
330
|
-
log_cmd
|
331
|
-
log_level
|
332
|
-
|
333
|
-
out
|
333
|
+
CONFIG_KEYS = [
|
334
|
+
:argv0,
|
335
|
+
:background,
|
336
|
+
:cli,
|
337
|
+
:env,
|
338
|
+
:err,
|
339
|
+
:in,
|
340
|
+
:logger,
|
341
|
+
:log_cmd,
|
342
|
+
:log_level,
|
343
|
+
:name,
|
344
|
+
:out,
|
345
|
+
:result_callback,
|
334
346
|
].freeze
|
335
347
|
|
336
348
|
##
|
337
349
|
# Option keys that belong to spawn configuration
|
338
350
|
# @private
|
339
351
|
#
|
340
|
-
SPAWN_KEYS =
|
341
|
-
chdir
|
342
|
-
close_others
|
343
|
-
new_pgroup
|
344
|
-
pgroup
|
345
|
-
umask
|
346
|
-
unsetenv_others
|
352
|
+
SPAWN_KEYS = [
|
353
|
+
:chdir,
|
354
|
+
:close_others,
|
355
|
+
:new_pgroup,
|
356
|
+
:pgroup,
|
357
|
+
:umask,
|
358
|
+
:unsetenv_others,
|
347
359
|
].freeze
|
348
360
|
|
361
|
+
## @private
|
349
362
|
def initialize(parent = nil)
|
350
363
|
if parent
|
351
364
|
@config_opts = ::Hash.new { |_h, k| parent.config_opts[k] }
|
@@ -359,6 +372,7 @@ module Toys
|
|
359
372
|
end
|
360
373
|
end
|
361
374
|
|
375
|
+
## @private
|
362
376
|
def add(config)
|
363
377
|
config.each do |k, v|
|
364
378
|
if CONFIG_KEYS.include?(k)
|
@@ -372,6 +386,7 @@ module Toys
|
|
372
386
|
self
|
373
387
|
end
|
374
388
|
|
389
|
+
## @private
|
375
390
|
def delete(*keys)
|
376
391
|
keys.each do |k|
|
377
392
|
if CONFIG_KEYS.include?(k)
|
@@ -385,7 +400,10 @@ module Toys
|
|
385
400
|
self
|
386
401
|
end
|
387
402
|
|
403
|
+
## @private
|
388
404
|
attr_reader :config_opts
|
405
|
+
|
406
|
+
## @private
|
389
407
|
attr_reader :spawn_opts
|
390
408
|
end
|
391
409
|
|
@@ -398,53 +416,84 @@ module Toys
|
|
398
416
|
#
|
399
417
|
class Controller
|
400
418
|
## @private
|
401
|
-
def initialize(controller_streams, captures, pid, join_threads,
|
419
|
+
def initialize(name, controller_streams, captures, pid, join_threads, result_callback)
|
420
|
+
@name = name
|
402
421
|
@in = controller_streams[:in]
|
403
422
|
@out = controller_streams[:out]
|
404
423
|
@err = controller_streams[:err]
|
405
424
|
@captures = captures
|
406
|
-
@pid =
|
425
|
+
@pid = @exception = @wait_thread = nil
|
426
|
+
case pid
|
427
|
+
when ::Integer
|
428
|
+
@pid = pid
|
429
|
+
@wait_thread = ::Process.detach(pid)
|
430
|
+
when ::Exception
|
431
|
+
@exception = pid
|
432
|
+
end
|
407
433
|
@join_threads = join_threads
|
408
|
-
@
|
409
|
-
@wait_thread = ::Process.detach(pid)
|
434
|
+
@result_callback = result_callback
|
410
435
|
@result = nil
|
411
436
|
end
|
412
437
|
|
413
438
|
##
|
414
|
-
#
|
415
|
-
#
|
416
|
-
#
|
417
|
-
|
439
|
+
# The subcommand's name.
|
440
|
+
# @return [Object]
|
441
|
+
#
|
442
|
+
attr_reader :name
|
443
|
+
|
444
|
+
##
|
445
|
+
# The subcommand's standard input stream (which can be written to).
|
446
|
+
#
|
447
|
+
# @return [IO] if the command was configured with `in: :controller`
|
448
|
+
# @return [nil] if the command was not configured with
|
449
|
+
# `in: :controller`
|
418
450
|
#
|
419
451
|
attr_reader :in
|
420
452
|
|
421
453
|
##
|
422
|
-
#
|
423
|
-
#
|
424
|
-
#
|
425
|
-
# @return [
|
454
|
+
# The subcommand's standard output stream (which can be read from).
|
455
|
+
#
|
456
|
+
# @return [IO] if the command was configured with `out: :controller`
|
457
|
+
# @return [nil] if the command was not configured with
|
458
|
+
# `out: :controller`
|
426
459
|
#
|
427
460
|
attr_reader :out
|
428
461
|
|
429
462
|
##
|
430
|
-
#
|
431
|
-
#
|
432
|
-
#
|
433
|
-
# @return [
|
463
|
+
# The subcommand's standard error stream (which can be read from).
|
464
|
+
#
|
465
|
+
# @return [IO] if the command was configured with `err: :controller`
|
466
|
+
# @return [nil] if the command was not configured with
|
467
|
+
# `err: :controller`
|
434
468
|
#
|
435
469
|
attr_reader :err
|
436
470
|
|
437
471
|
##
|
438
|
-
#
|
439
|
-
#
|
472
|
+
# The process ID.
|
473
|
+
#
|
474
|
+
# Exactly one of `exception` and `pid` will be non-nil.
|
475
|
+
#
|
476
|
+
# @return [Integer] if the process start was successful
|
477
|
+
# @return [nil] if the process could not be started.
|
440
478
|
#
|
441
479
|
attr_reader :pid
|
442
480
|
|
481
|
+
##
|
482
|
+
# The exception raised when the process failed to start.
|
483
|
+
#
|
484
|
+
# Exactly one of `exception` and `pid` will be non-nil.
|
485
|
+
#
|
486
|
+
# @return [Exception] if the process failed to start.
|
487
|
+
# @return [nil] if the process start was successful.
|
488
|
+
#
|
489
|
+
attr_reader :exception
|
490
|
+
|
443
491
|
##
|
444
492
|
# Captures the remaining data in the given stream.
|
445
493
|
# After calling this, do not read directly from the stream.
|
446
494
|
#
|
447
|
-
# @param [:out,:err]
|
495
|
+
# @param which [:out,:err] Which stream to capture
|
496
|
+
# @return [self]
|
448
497
|
#
|
449
498
|
def capture(which)
|
450
499
|
stream = stream_for(which)
|
@@ -462,6 +511,8 @@ module Toys
|
|
462
511
|
# Captures the remaining data in the standard output stream.
|
463
512
|
# After calling this, do not read directly from the stream.
|
464
513
|
#
|
514
|
+
# @return [self]
|
515
|
+
#
|
465
516
|
def capture_out
|
466
517
|
capture(:out)
|
467
518
|
end
|
@@ -470,6 +521,8 @@ module Toys
|
|
470
521
|
# Captures the remaining data in the standard error stream.
|
471
522
|
# After calling this, do not read directly from the stream.
|
472
523
|
#
|
524
|
+
# @return [self]
|
525
|
+
#
|
473
526
|
def capture_err
|
474
527
|
capture(:err)
|
475
528
|
end
|
@@ -484,10 +537,11 @@ module Toys
|
|
484
537
|
#
|
485
538
|
# After calling this, do not interact directly with the stream.
|
486
539
|
#
|
487
|
-
# @param [:in,:out,:err]
|
488
|
-
# @param [IO,StringIO,String,:null]
|
489
|
-
# @param [Object...]
|
540
|
+
# @param which [:in,:out,:err] Which stream to redirect
|
541
|
+
# @param io [IO,StringIO,String,:null] Where to redirect the stream
|
542
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
490
543
|
# file, if redirecting to/from a file.
|
544
|
+
# @return [self]
|
491
545
|
#
|
492
546
|
def redirect(which, io, *io_args)
|
493
547
|
io = ::File::NULL if io == :null
|
@@ -508,6 +562,7 @@ module Toys
|
|
508
562
|
io.close
|
509
563
|
end
|
510
564
|
end
|
565
|
+
self
|
511
566
|
end
|
512
567
|
|
513
568
|
##
|
@@ -520,9 +575,10 @@ module Toys
|
|
520
575
|
#
|
521
576
|
# After calling this, do not interact directly with the stream.
|
522
577
|
#
|
523
|
-
# @param [IO,StringIO,String,:null]
|
524
|
-
# @param [Object...]
|
578
|
+
# @param io [IO,StringIO,String,:null] Where to redirect the stream
|
579
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
525
580
|
# file, if redirecting from a file.
|
581
|
+
# @return [self]
|
526
582
|
#
|
527
583
|
def redirect_in(io, *io_args)
|
528
584
|
redirect(:in, io, *io_args)
|
@@ -538,9 +594,10 @@ module Toys
|
|
538
594
|
#
|
539
595
|
# After calling this, do not interact directly with the stream.
|
540
596
|
#
|
541
|
-
# @param [IO,StringIO,String,:null]
|
542
|
-
# @param [Object...]
|
597
|
+
# @param io [IO,StringIO,String,:null] Where to redirect the stream
|
598
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
543
599
|
# file, if redirecting to a file.
|
600
|
+
# @return [self]
|
544
601
|
#
|
545
602
|
def redirect_out(io, *io_args)
|
546
603
|
redirect(:out, io, *io_args)
|
@@ -555,9 +612,10 @@ module Toys
|
|
555
612
|
#
|
556
613
|
# After calling this, do not interact directly with the stream.
|
557
614
|
#
|
558
|
-
# @param [IO,StringIO,String]
|
559
|
-
# @param [Object...]
|
615
|
+
# @param io [IO,StringIO,String] Where to redirect the stream
|
616
|
+
# @param io_args [Object...] The mode and permissions for opening the
|
560
617
|
# file, if redirecting to a file.
|
618
|
+
# @return [self]
|
561
619
|
#
|
562
620
|
def redirect_err(io, *io_args)
|
563
621
|
redirect(:err, io, *io_args)
|
@@ -567,10 +625,12 @@ module Toys
|
|
567
625
|
# Send the given signal to the process. The signal may be specified
|
568
626
|
# by name or number.
|
569
627
|
#
|
570
|
-
# @param [Integer,String]
|
628
|
+
# @param sig [Integer,String] The signal to send.
|
629
|
+
# @return [self]
|
571
630
|
#
|
572
631
|
def kill(sig)
|
573
|
-
::Process.kill(sig, pid)
|
632
|
+
::Process.kill(sig, pid) if pid
|
633
|
+
self
|
574
634
|
end
|
575
635
|
alias signal kill
|
576
636
|
|
@@ -580,27 +640,24 @@ module Toys
|
|
580
640
|
# @return [Boolean]
|
581
641
|
#
|
582
642
|
def executing?
|
583
|
-
@wait_thread
|
643
|
+
@wait_thread&.status ? true : false
|
584
644
|
end
|
585
645
|
|
586
646
|
##
|
587
647
|
# Wait for the subcommand to complete, and return a result object.
|
588
648
|
#
|
589
|
-
# @param [Numeric,nil]
|
649
|
+
# @param timeout [Numeric,nil] The timeout in seconds, or `nil` to
|
590
650
|
# wait indefinitely.
|
591
|
-
# @return [Toys::Utils::Exec::Result
|
592
|
-
#
|
651
|
+
# @return [Toys::Utils::Exec::Result] The result object
|
652
|
+
# @return [nil] if a timeout occurred.
|
593
653
|
#
|
594
654
|
def result(timeout: nil)
|
595
|
-
return nil
|
655
|
+
return nil if @wait_thread && !@wait_thread.join(timeout)
|
596
656
|
@result ||= begin
|
597
657
|
close_streams
|
598
658
|
@join_threads.each(&:join)
|
599
|
-
|
600
|
-
|
601
|
-
@nonzero_status_handler.call(status)
|
602
|
-
end
|
603
|
-
Result.new(@captures[:out], @captures[:err], status)
|
659
|
+
Result.new(name, @captures[:out], @captures[:err], @wait_thread&.value, @exception)
|
660
|
+
.tap { |result| @result_callback&.call(result) }
|
604
661
|
end
|
605
662
|
end
|
606
663
|
|
@@ -640,46 +697,76 @@ module Toys
|
|
640
697
|
end
|
641
698
|
|
642
699
|
##
|
643
|
-
# The
|
700
|
+
# The result returned from a subcommand execution.
|
644
701
|
#
|
645
702
|
class Result
|
646
703
|
## @private
|
647
|
-
def initialize(out, err, status)
|
704
|
+
def initialize(name, out, err, status, exception)
|
705
|
+
@name = name
|
648
706
|
@captured_out = out
|
649
707
|
@captured_err = err
|
650
708
|
@status = status
|
709
|
+
@exception = exception
|
651
710
|
end
|
652
711
|
|
653
712
|
##
|
654
|
-
#
|
655
|
-
#
|
656
|
-
# @return [
|
713
|
+
# The subcommand's name.
|
714
|
+
#
|
715
|
+
# @return [Object]
|
716
|
+
#
|
717
|
+
attr_reader :name
|
718
|
+
|
719
|
+
##
|
720
|
+
# The captured output string.
|
721
|
+
#
|
722
|
+
# @return [String] The string captured from stdout.
|
723
|
+
# @return [nil] if the command was not configured to capture stdout.
|
657
724
|
#
|
658
725
|
attr_reader :captured_out
|
659
726
|
|
660
727
|
##
|
661
|
-
#
|
662
|
-
#
|
663
|
-
# @return [String
|
728
|
+
# The captured error string.
|
729
|
+
#
|
730
|
+
# @return [String] The string captured from stderr.
|
731
|
+
# @return [nil] if the command was not configured to capture stderr.
|
664
732
|
#
|
665
733
|
attr_reader :captured_err
|
666
734
|
|
667
735
|
##
|
668
|
-
#
|
669
|
-
#
|
736
|
+
# The status code object.
|
737
|
+
#
|
738
|
+
# Exactly one of `exception` and `status` will be non-nil.
|
739
|
+
#
|
740
|
+
# @return [Process::Status] The status code.
|
741
|
+
# @return [nil] if the process could not be started.
|
670
742
|
#
|
671
743
|
attr_reader :status
|
672
744
|
|
673
745
|
##
|
674
|
-
#
|
746
|
+
# The exception raised if a process couldn't be started.
|
747
|
+
#
|
748
|
+
# Exactly one of `exception` and `status` will be non-nil.
|
749
|
+
#
|
750
|
+
# @return [Exception] The exception raised from process start.
|
751
|
+
# @return [nil] if the process started successfully.
|
752
|
+
#
|
753
|
+
attr_reader :exception
|
754
|
+
|
755
|
+
##
|
756
|
+
# The numeric status code.
|
757
|
+
#
|
758
|
+
# This will be a nonzero integer if the process failed to start. That
|
759
|
+
# is, `exit_code` will never be `nil`, even if `status` is `nil`.
|
760
|
+
#
|
675
761
|
# @return [Integer]
|
676
762
|
#
|
677
763
|
def exit_code
|
678
|
-
status.exitstatus
|
764
|
+
status ? status.exitstatus : 127
|
679
765
|
end
|
680
766
|
|
681
767
|
##
|
682
768
|
# Returns true if the subprocess terminated with a zero status.
|
769
|
+
#
|
683
770
|
# @return [Boolean]
|
684
771
|
#
|
685
772
|
def success?
|
@@ -688,6 +775,7 @@ module Toys
|
|
688
775
|
|
689
776
|
##
|
690
777
|
# Returns true if the subprocess terminated with a nonzero status.
|
778
|
+
#
|
691
779
|
# @return [Boolean]
|
692
780
|
#
|
693
781
|
def error?
|
@@ -719,10 +807,7 @@ module Toys
|
|
719
807
|
setup_out_stream(:out)
|
720
808
|
setup_out_stream(:err)
|
721
809
|
log_command
|
722
|
-
|
723
|
-
@child_streams.each(&:close)
|
724
|
-
controller = Controller.new(@controller_streams, @captures, pid, @join_threads,
|
725
|
-
@config_opts[:nonzero_status_handler])
|
810
|
+
controller = start_with_controller
|
726
811
|
return controller if @config_opts[:background]
|
727
812
|
begin
|
728
813
|
@block&.call(controller)
|
@@ -743,6 +828,18 @@ module Toys
|
|
743
828
|
end
|
744
829
|
end
|
745
830
|
|
831
|
+
def start_with_controller
|
832
|
+
pid =
|
833
|
+
begin
|
834
|
+
@fork_func ? start_fork : start_process
|
835
|
+
rescue ::StandardError => e
|
836
|
+
e
|
837
|
+
end
|
838
|
+
@child_streams.each(&:close)
|
839
|
+
Controller.new(@config_opts[:name], @controller_streams, @captures, pid,
|
840
|
+
@join_threads, @config_opts[:result_callback])
|
841
|
+
end
|
842
|
+
|
746
843
|
def start_process
|
747
844
|
args = []
|
748
845
|
args << @config_opts[:env] if @config_opts[:env]
|