toys-core 0.3.2
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 +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE.md +29 -0
- data/README.md +30 -0
- data/lib/toys-core.rb +54 -0
- data/lib/toys/alias.rb +94 -0
- data/lib/toys/cli.rb +268 -0
- data/lib/toys/config_dsl.rb +356 -0
- data/lib/toys/context.rb +278 -0
- data/lib/toys/core_version.rb +36 -0
- data/lib/toys/errors.rb +42 -0
- data/lib/toys/helpers.rb +52 -0
- data/lib/toys/helpers/exec.rb +469 -0
- data/lib/toys/helpers/file_utils.rb +39 -0
- data/lib/toys/loader.rb +381 -0
- data/lib/toys/middleware.rb +124 -0
- data/lib/toys/middleware/add_verbosity_switches.rb +99 -0
- data/lib/toys/middleware/base.rb +51 -0
- data/lib/toys/middleware/handle_usage_errors.rb +67 -0
- data/lib/toys/middleware/set_default_descriptions.rb +131 -0
- data/lib/toys/middleware/show_usage.rb +170 -0
- data/lib/toys/middleware/show_version.rb +99 -0
- data/lib/toys/template.rb +123 -0
- data/lib/toys/templates.rb +55 -0
- data/lib/toys/templates/clean.rb +82 -0
- data/lib/toys/templates/gem_build.rb +121 -0
- data/lib/toys/templates/minitest.rb +126 -0
- data/lib/toys/templates/rubocop.rb +86 -0
- data/lib/toys/templates/yardoc.rb +101 -0
- data/lib/toys/tool.rb +749 -0
- data/lib/toys/utils/module_lookup.rb +101 -0
- data/lib/toys/utils/usage.rb +196 -0
- metadata +146 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright 2018 Daniel Azuma
|
2
|
+
#
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
12
|
+
# and/or other materials provided with the distribution.
|
13
|
+
# * Neither the name of the copyright holder, nor the names of any other
|
14
|
+
# contributors to this software, may be used to endorse or promote products
|
15
|
+
# derived from this software without specific prior written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
21
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
22
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
23
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
25
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
26
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
28
|
+
;
|
29
|
+
|
30
|
+
module Toys
|
31
|
+
##
|
32
|
+
# Current version of Toys core
|
33
|
+
# @return [String]
|
34
|
+
#
|
35
|
+
CORE_VERSION = "0.3.2".freeze
|
36
|
+
end
|
data/lib/toys/errors.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Copyright 2018 Daniel Azuma
|
2
|
+
#
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
12
|
+
# and/or other materials provided with the distribution.
|
13
|
+
# * Neither the name of the copyright holder, nor the names of any other
|
14
|
+
# contributors to this software, may be used to endorse or promote products
|
15
|
+
# derived from this software without specific prior written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
21
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
22
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
23
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
25
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
26
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
28
|
+
;
|
29
|
+
|
30
|
+
module Toys
|
31
|
+
##
|
32
|
+
# An exception indicating an error in a tool definition
|
33
|
+
#
|
34
|
+
class ToolDefinitionError < ::StandardError
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# An exception indicating a problem during tool lookup
|
39
|
+
#
|
40
|
+
class LoaderError < ::StandardError
|
41
|
+
end
|
42
|
+
end
|
data/lib/toys/helpers.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Copyright 2018 Daniel Azuma
|
2
|
+
#
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
12
|
+
# and/or other materials provided with the distribution.
|
13
|
+
# * Neither the name of the copyright holder, nor the names of any other
|
14
|
+
# contributors to this software, may be used to endorse or promote products
|
15
|
+
# derived from this software without specific prior written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
21
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
22
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
23
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
25
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
26
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
28
|
+
;
|
29
|
+
|
30
|
+
require "toys/utils/module_lookup"
|
31
|
+
|
32
|
+
module Toys
|
33
|
+
##
|
34
|
+
# Namespace for common helper modules
|
35
|
+
#
|
36
|
+
module Helpers
|
37
|
+
##
|
38
|
+
# Return a helper module by name.
|
39
|
+
#
|
40
|
+
# Currently recognized module names are:
|
41
|
+
#
|
42
|
+
# * `:exec` : Methods to help execute subcommands.
|
43
|
+
# * `:file_utils` : The FileUtils standard library methods.
|
44
|
+
#
|
45
|
+
# @param [String,Symbol] name Name of the helper module to return
|
46
|
+
# @return [Module,nil] The module, or `nil` if not found
|
47
|
+
#
|
48
|
+
def self.lookup(name)
|
49
|
+
Utils::ModuleLookup.lookup(:helpers, name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,469 @@
|
|
1
|
+
# Copyright 2018 Daniel Azuma
|
2
|
+
#
|
3
|
+
# All rights reserved.
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# * Redistributions of source code must retain the above copyright notice,
|
9
|
+
# this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
12
|
+
# and/or other materials provided with the distribution.
|
13
|
+
# * Neither the name of the copyright holder, nor the names of any other
|
14
|
+
# contributors to this software, may be used to endorse or promote products
|
15
|
+
# derived from this software without specific prior written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
21
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
22
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
23
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
25
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
26
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
27
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
28
|
+
;
|
29
|
+
|
30
|
+
require "logger"
|
31
|
+
|
32
|
+
module Toys
|
33
|
+
module Helpers
|
34
|
+
##
|
35
|
+
# A set of helper methods for invoking subcommands. Provides shortcuts for
|
36
|
+
# common cases such as invoking Ruby in a subprocess or capturing output
|
37
|
+
# in a string. Also provides an interface for controlling a spawned
|
38
|
+
# process's streams.
|
39
|
+
#
|
40
|
+
# ## Configuration options
|
41
|
+
#
|
42
|
+
# A variety of options can be used to control subprocesses. These include:
|
43
|
+
#
|
44
|
+
# * **:env** (Hash) Environment variables to pass to the subprocess
|
45
|
+
# * **:log_level** (Integer) If set, the actual command will be logged
|
46
|
+
# at the given level.
|
47
|
+
# * **:in_from** (`:controller`,String) Connects the input stream of the
|
48
|
+
# subprocess. If set to `:controller`, the controller will control the
|
49
|
+
# input stream. If set to a string, that string will be written to the
|
50
|
+
# input stream. If not set, the input stream will be connected to the
|
51
|
+
# STDIN for the Toys process itself.
|
52
|
+
# * **:out_to** (`:controller`,`:capture`) Connects the standard output
|
53
|
+
# stream of the subprocess. If set to `:controller`, the controller
|
54
|
+
# will control the output stream. If set to `:capture`, the output will
|
55
|
+
# be captured in a string that is available in the
|
56
|
+
# {Toys::Helpers::Exec::Result} object. If not set, the subprocess
|
57
|
+
# standard out is connected to STDOUT of the Toys process.
|
58
|
+
# * **:err_to** (`:controller`,`:capture`) Connects the standard error
|
59
|
+
# stream of the subprocess. See `:out_to` for more details.
|
60
|
+
# * **:out_err_to** (`:controller`,`:capture`) Combines the standard out
|
61
|
+
# and error streams of the subprocess and connects them. See `:out_to`
|
62
|
+
# for more details.
|
63
|
+
# * **:exit_on_nonzero_status** (Boolean) If true, a nonzero status code
|
64
|
+
# will cause the entire tool to terminate. Default is false.
|
65
|
+
#
|
66
|
+
# In addition, any options recognized by `Process#spawn` are supported.
|
67
|
+
# These include `:umask`, `:pgroup`, `:chdir`, and many others.
|
68
|
+
#
|
69
|
+
# Configuration options may be provided to any method that starts a
|
70
|
+
# subprocess. You may also set default values for this tool by calling
|
71
|
+
# {Toys::Helpers::Exec#configure_exec}.
|
72
|
+
#
|
73
|
+
module Exec
|
74
|
+
##
|
75
|
+
# Set default configuration keys.
|
76
|
+
#
|
77
|
+
# @param [Hash] opts The default options. See the section on
|
78
|
+
# configuration options in the {Toys::Helpers::Exec} module docs.
|
79
|
+
#
|
80
|
+
def configure_exec(opts = {})
|
81
|
+
@exec_config ||= {}
|
82
|
+
@exec_config.merge!(opts)
|
83
|
+
end
|
84
|
+
|
85
|
+
##
|
86
|
+
# Execute a command. The command may be given as a single string to pass
|
87
|
+
# to a shell, or an array of strings indicating a posix command.
|
88
|
+
#
|
89
|
+
# If you provide a block, a {Toys::Helpers::Exec::Controller} will be
|
90
|
+
# yielded to it, allowing you to interact with the subprocess streams.
|
91
|
+
#
|
92
|
+
# @param [String,Array<String>] cmd The command to execute.
|
93
|
+
# @param [Hash] opts The command options. See the section on
|
94
|
+
# configuration options in the {Toys::Helpers::Exec} module docs.
|
95
|
+
# @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
|
96
|
+
# for the subprocess streams.
|
97
|
+
#
|
98
|
+
# @return [Toys::Helpers::Result] The subprocess result, including
|
99
|
+
# exit code and any captured output.
|
100
|
+
#
|
101
|
+
def exec(cmd, opts = {}, &block)
|
102
|
+
exec_opts = ExecOpts.new(self)
|
103
|
+
exec_opts.add(@exec_config) if defined? @exec_config
|
104
|
+
exec_opts.add(opts)
|
105
|
+
executor = Executor.new(exec_opts, cmd)
|
106
|
+
executor.execute(&block)
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Spawn a ruby process and pass the given arguments to it.
|
111
|
+
#
|
112
|
+
# If you provide a block, a {Toys::Helpers::Exec::Controller} will be
|
113
|
+
# yielded to it, allowing you to interact with the subprocess streams.
|
114
|
+
#
|
115
|
+
# @param [String,Array<String>] args The arguments to ruby.
|
116
|
+
# @param [Hash] opts The command options. See the section on
|
117
|
+
# configuration options in the {Toys::Helpers::Exec} module docs.
|
118
|
+
# @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
|
119
|
+
# for the subprocess streams.
|
120
|
+
#
|
121
|
+
# @return [Toys::Helpers::Result] The subprocess result, including
|
122
|
+
# exit code and any captured output.
|
123
|
+
#
|
124
|
+
def ruby(args, opts = {}, &block)
|
125
|
+
cmd =
|
126
|
+
if args.is_a?(Array)
|
127
|
+
[[Exec.ruby_binary, "ruby"]] + args
|
128
|
+
else
|
129
|
+
"#{Exec.ruby_binary} #{args}"
|
130
|
+
end
|
131
|
+
exec(cmd, opts, &block)
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Execute the given string in a shell. Returns the exit code.
|
136
|
+
#
|
137
|
+
# @param [String] cmd The shell command to execute.
|
138
|
+
# @param [Hash] opts The command options. See the section on
|
139
|
+
# configuration options in the {Toys::Helpers::Exec} module docs.
|
140
|
+
# @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
|
141
|
+
# for the subprocess streams.
|
142
|
+
#
|
143
|
+
# @return [Integer] The exit code
|
144
|
+
#
|
145
|
+
def sh(cmd, opts = {})
|
146
|
+
exec(cmd, opts).exit_code
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# Execute a command. The command may be given as a single string to pass
|
151
|
+
# to a shell, or an array of strings indicating a posix command.
|
152
|
+
#
|
153
|
+
# Captures standard out and returns it as a string.
|
154
|
+
#
|
155
|
+
# @param [String,Array<String>] cmd The command to execute.
|
156
|
+
# @param [Hash] opts The command options. See the section on
|
157
|
+
# configuration options in the {Toys::Helpers::Exec} module docs.
|
158
|
+
# @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
|
159
|
+
# for the subprocess streams.
|
160
|
+
#
|
161
|
+
# @return [String] What was written to standard out.
|
162
|
+
#
|
163
|
+
def capture(cmd, opts = {})
|
164
|
+
exec(cmd, opts.merge(out_to: :capture)).captured_out
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Returns the paty to the Ruby binary
|
169
|
+
# @return [String] Path to the Ruby binary
|
170
|
+
#
|
171
|
+
def self.ruby_binary
|
172
|
+
::File.join(::RbConfig::CONFIG["bindir"], ::RbConfig::CONFIG["ruby_install_name"])
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# An object of this type is passed to a subcommand control block.
|
177
|
+
# You may use this object to interact with the subcommand's streams,
|
178
|
+
# and/or send signals to the process.
|
179
|
+
#
|
180
|
+
class Controller
|
181
|
+
## @private
|
182
|
+
def initialize(ins, out, err, out_err, pid)
|
183
|
+
@in = ins
|
184
|
+
@out = out
|
185
|
+
@err = err
|
186
|
+
@out_err = out_err
|
187
|
+
@pid = pid
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Return the subcommand's standard input stream (which can be written
|
192
|
+
# to), if the command was configured with `in_from: :controller`.
|
193
|
+
# Returns `nil` otherwise.
|
194
|
+
# @return [IO,nil]
|
195
|
+
#
|
196
|
+
attr_reader :in
|
197
|
+
|
198
|
+
##
|
199
|
+
# Return the subcommand's standard output stream (which can be read
|
200
|
+
# from), if the command was configured with `out_to: :controller`.
|
201
|
+
# Returns `nil` otherwise.
|
202
|
+
# @return [IO,nil]
|
203
|
+
#
|
204
|
+
attr_reader :out
|
205
|
+
|
206
|
+
##
|
207
|
+
# Return the subcommand's standard error stream (which can be read
|
208
|
+
# from), if the command was configured with `err_to: :controller`.
|
209
|
+
# Returns `nil` otherwise.
|
210
|
+
# @return [IO,nil]
|
211
|
+
#
|
212
|
+
attr_reader :err
|
213
|
+
|
214
|
+
##
|
215
|
+
# Return the subcommand's combined standard output and error stream
|
216
|
+
# (which can be read from), if the command was configured with
|
217
|
+
# `out_err_to: :controller`. Returns `nil` otherwise.
|
218
|
+
# @return [IO,nil]
|
219
|
+
#
|
220
|
+
attr_reader :out_err
|
221
|
+
|
222
|
+
##
|
223
|
+
# Returns the process ID.
|
224
|
+
# @return [Integer]
|
225
|
+
#
|
226
|
+
attr_reader :pid
|
227
|
+
|
228
|
+
##
|
229
|
+
# Send the given signal to the process. The signal may be specified
|
230
|
+
# by name or number.
|
231
|
+
#
|
232
|
+
# @param [Integer,String] signal The signal to send.
|
233
|
+
#
|
234
|
+
def kill(signal)
|
235
|
+
::Process.kill(signal, pid)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# The return result from a subcommand
|
241
|
+
#
|
242
|
+
class Result
|
243
|
+
## @private
|
244
|
+
def initialize(out, err, out_err, status)
|
245
|
+
@captured_out = out
|
246
|
+
@captured_err = err
|
247
|
+
@captured_out_err = out_err
|
248
|
+
@status = status
|
249
|
+
end
|
250
|
+
|
251
|
+
##
|
252
|
+
# Returns the captured output string, if the command was configured
|
253
|
+
# with `out_to: :capture`. Returns `nil` otherwise.
|
254
|
+
# @return [String,nil]
|
255
|
+
#
|
256
|
+
attr_reader :captured_out
|
257
|
+
|
258
|
+
##
|
259
|
+
# Returns the captured error string, if the command was configured
|
260
|
+
# with `err_to: :capture`. Returns `nil` otherwise.
|
261
|
+
# @return [String,nil]
|
262
|
+
#
|
263
|
+
attr_reader :captured_err
|
264
|
+
|
265
|
+
##
|
266
|
+
# Returns the captured combined output and error string, if the command
|
267
|
+
# was configured with `out_err_to: :capture`. Returns `nil` otherwise.
|
268
|
+
# @return [String,nil]
|
269
|
+
#
|
270
|
+
attr_reader :captured_out_err
|
271
|
+
|
272
|
+
##
|
273
|
+
# Returns the status code object.
|
274
|
+
# @return [Process::Status]
|
275
|
+
#
|
276
|
+
attr_reader :status
|
277
|
+
|
278
|
+
##
|
279
|
+
# Returns the numeric status code.
|
280
|
+
# @return [Integer]
|
281
|
+
#
|
282
|
+
def exit_code
|
283
|
+
status.exitstatus
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
# Returns true if the subprocess terminated with a zero status.
|
288
|
+
# @return [Boolean]
|
289
|
+
#
|
290
|
+
def success?
|
291
|
+
exit_code.zero?
|
292
|
+
end
|
293
|
+
|
294
|
+
##
|
295
|
+
# Returns true if the subprocess terminated with a nonzero status.
|
296
|
+
# @return [Boolean]
|
297
|
+
#
|
298
|
+
def error?
|
299
|
+
!exit_code.zero?
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
##
|
304
|
+
# An internal helper class storing the configuration of a subcommand invocation
|
305
|
+
# @private
|
306
|
+
#
|
307
|
+
class ExecOpts
|
308
|
+
##
|
309
|
+
# Option keys that belong to exec configuration rather than spawn
|
310
|
+
# @private
|
311
|
+
#
|
312
|
+
CONFIG_KEYS = %i[
|
313
|
+
exit_on_nonzero_status
|
314
|
+
env
|
315
|
+
log_level
|
316
|
+
in_from
|
317
|
+
out_to
|
318
|
+
err_to
|
319
|
+
out_err_to
|
320
|
+
].freeze
|
321
|
+
|
322
|
+
def initialize(context)
|
323
|
+
@context = context
|
324
|
+
@config = {exit_on_nonzero_status: @context.get(Context::EXIT_ON_NONZERO_STATUS)}
|
325
|
+
@spawn_opts = {}
|
326
|
+
end
|
327
|
+
|
328
|
+
def add(config)
|
329
|
+
config.each do |k, v|
|
330
|
+
if CONFIG_KEYS.include?(k)
|
331
|
+
@config[k] = v
|
332
|
+
else
|
333
|
+
@spawn_opts[k] = v
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
attr_reader :config
|
339
|
+
attr_reader :spawn_opts
|
340
|
+
attr_reader :context
|
341
|
+
end
|
342
|
+
|
343
|
+
##
|
344
|
+
# An object that manages the execution of a subcommand
|
345
|
+
# @private
|
346
|
+
#
|
347
|
+
class Executor
|
348
|
+
def initialize(exec_opts, cmd)
|
349
|
+
@cmd = Array(cmd)
|
350
|
+
@config = exec_opts.config
|
351
|
+
@context = exec_opts.context
|
352
|
+
@spawn_opts = exec_opts.spawn_opts.dup
|
353
|
+
@captures = {}
|
354
|
+
@controller_streams = {}
|
355
|
+
@join_threads = []
|
356
|
+
@child_streams = []
|
357
|
+
end
|
358
|
+
|
359
|
+
def execute(&block)
|
360
|
+
setup_in_stream
|
361
|
+
setup_out_stream(:out, :out_to, :out)
|
362
|
+
setup_out_stream(:err, :err_to, :err)
|
363
|
+
setup_out_stream(:out_err, :out_err_to, [:out, :err])
|
364
|
+
log_command
|
365
|
+
wait_thread = start_process
|
366
|
+
status = control_process(wait_thread, &block)
|
367
|
+
create_result(status)
|
368
|
+
end
|
369
|
+
|
370
|
+
private
|
371
|
+
|
372
|
+
def log_command
|
373
|
+
unless @config[:log_level] == false
|
374
|
+
cmd_str = @cmd.size == 1 ? @cmd.first : @cmd.inspect
|
375
|
+
@context.logger.add(@config[:log_level] || ::Logger::INFO, cmd_str)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def start_process
|
380
|
+
args = []
|
381
|
+
args << @config[:env] if @config[:env]
|
382
|
+
args.concat(@cmd)
|
383
|
+
pid = ::Process.spawn(*args, @spawn_opts)
|
384
|
+
@child_streams.each(&:close)
|
385
|
+
::Process.detach(pid)
|
386
|
+
end
|
387
|
+
|
388
|
+
def control_process(wait_thread)
|
389
|
+
begin
|
390
|
+
if block_given?
|
391
|
+
controller = Controller.new(
|
392
|
+
@controller_streams[:in], @controller_streams[:out], @controller_streams[:err],
|
393
|
+
@controller_streams[:out_err], wait_thread.pid
|
394
|
+
)
|
395
|
+
yield controller
|
396
|
+
end
|
397
|
+
ensure
|
398
|
+
@controller_streams.each_value(&:close)
|
399
|
+
end
|
400
|
+
@join_threads.each(&:join)
|
401
|
+
wait_thread.value
|
402
|
+
end
|
403
|
+
|
404
|
+
def create_result(status)
|
405
|
+
if @config[:exit_on_nonzero_status]
|
406
|
+
exit_status = status.exitstatus
|
407
|
+
@context.exit(exit_status) if exit_status != 0
|
408
|
+
end
|
409
|
+
Result.new(@captures[:out], @captures[:err], @captures[:out_err], status)
|
410
|
+
end
|
411
|
+
|
412
|
+
def setup_in_stream
|
413
|
+
setting = @config[:in_from]
|
414
|
+
if setting
|
415
|
+
r, w = ::IO.pipe
|
416
|
+
@spawn_opts[:in] = r
|
417
|
+
w.sync = true
|
418
|
+
@child_streams << r
|
419
|
+
case setting
|
420
|
+
when :controller
|
421
|
+
@controller_streams[:in] = w
|
422
|
+
when String
|
423
|
+
write_string_thread(w, setting)
|
424
|
+
else
|
425
|
+
raise "Unknown type for in_from"
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def setup_out_stream(stream_name, config_key, spawn_key)
|
431
|
+
setting = @config[config_key]
|
432
|
+
if setting
|
433
|
+
r, w = ::IO.pipe
|
434
|
+
@spawn_opts[spawn_key] = w
|
435
|
+
@child_streams << w
|
436
|
+
case setting
|
437
|
+
when :controller
|
438
|
+
@controller_streams[stream_name] = r
|
439
|
+
when :capture
|
440
|
+
@join_threads << capture_stream_thread(r, stream_name)
|
441
|
+
else
|
442
|
+
raise "Unknown type for #{config_key}"
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def write_string_thread(stream, string)
|
448
|
+
::Thread.new do
|
449
|
+
begin
|
450
|
+
stream.write string
|
451
|
+
ensure
|
452
|
+
stream.close
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def capture_stream_thread(stream, name)
|
458
|
+
::Thread.new do
|
459
|
+
begin
|
460
|
+
@captures[name] = stream.read
|
461
|
+
ensure
|
462
|
+
stream.close
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|