toys-core 0.3.4 → 0.3.5
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/.yardopts +1 -0
- data/CHANGELOG.md +7 -0
- data/README.md +8 -7
- data/docs/getting-started.md +8 -0
- data/lib/toys/cli.rb +2 -1
- data/lib/toys/config_dsl.rb +282 -74
- data/lib/toys/core_version.rb +1 -1
- data/lib/toys/helpers/exec.rb +47 -365
- data/lib/toys/loader.rb +1 -1
- data/lib/toys/middleware/add_verbosity_flags.rb +2 -2
- data/lib/toys/middleware/set_default_descriptions.rb +16 -16
- data/lib/toys/middleware/show_help.rb +35 -11
- data/lib/toys/middleware/show_version.rb +1 -1
- data/lib/toys/template.rb +1 -1
- data/lib/toys/templates/clean.rb +1 -1
- data/lib/toys/templates/gem_build.rb +1 -1
- data/lib/toys/templates/minitest.rb +1 -1
- data/lib/toys/templates/rubocop.rb +1 -1
- data/lib/toys/templates/yardoc.rb +1 -1
- data/lib/toys/tool.rb +110 -235
- data/lib/toys/utils/exec.rb +505 -0
- data/lib/toys/utils/help_text.rb +11 -11
- metadata +7 -4
@@ -0,0 +1,505 @@
|
|
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 Utils
|
34
|
+
##
|
35
|
+
# A service that executes subprocesses.
|
36
|
+
#
|
37
|
+
# This service provides a convenient interface for controlling spawned
|
38
|
+
# processes and their streams. It also provides shortcuts for common cases
|
39
|
+
# such as invoking Ruby in a subprocess or capturing output in a string.
|
40
|
+
#
|
41
|
+
# ## Stream handling
|
42
|
+
#
|
43
|
+
# By default, subprocess streams are connected to the corresponding streams
|
44
|
+
# in the parent process.
|
45
|
+
#
|
46
|
+
# Alternately, input streams may be read from a string you provide, and
|
47
|
+
# you may direct output streams to be captured and their contents exposed
|
48
|
+
# in the result object.
|
49
|
+
#
|
50
|
+
# You may also connect subprocess streams to a controller, which you can
|
51
|
+
# then manipulate by providing a block. Your block may read and write
|
52
|
+
# connected streams to interact with the process. For example, to redirect
|
53
|
+
# data into a subprocess you can connect its input stream to the controller
|
54
|
+
# using the `:in_from` option (see below). Then, in your block, you can
|
55
|
+
# write to that stream via the controller.
|
56
|
+
#
|
57
|
+
# ## Configuration options
|
58
|
+
#
|
59
|
+
# A variety of options can be used to control subprocesses. These include:
|
60
|
+
#
|
61
|
+
# * **:env** (Hash) Environment variables to pass to the subprocess
|
62
|
+
# * **:logger** (Logger) Logger to use for logging the actual command.
|
63
|
+
# If not present, the command is not logged.
|
64
|
+
# * **:log_level** (Integer) Log level for logging the actual command.
|
65
|
+
# Defaults to Logger::INFO if not present.
|
66
|
+
# * **:in_from** (`:controller`,String) Connects the input stream of the
|
67
|
+
# subprocess. If set to `:controller`, the controller will control the
|
68
|
+
# input stream. If set to a string, that string will be written to the
|
69
|
+
# input stream. If not set, the input stream will be connected to the
|
70
|
+
# STDIN for the Toys process itself.
|
71
|
+
# * **:out_to** (`:controller`,`:capture`) Connects the standard output
|
72
|
+
# stream of the subprocess. If set to `:controller`, the controller
|
73
|
+
# will control the output stream. If set to `:capture`, the output will
|
74
|
+
# be captured in a string that is available in the
|
75
|
+
# {Toys::Helpers::Exec::Result} object. If not set, the subprocess
|
76
|
+
# standard out is connected to STDOUT of the Toys process.
|
77
|
+
# * **:err_to** (`:controller`,`:capture`) Connects the standard error
|
78
|
+
# stream of the subprocess. If set to `:controller`, the controller
|
79
|
+
# will control the output stream. If set to `:capture`, the output will
|
80
|
+
# be captured in a string that is available in the
|
81
|
+
# {Toys::Helpers::Exec::Result} object. If not set, the subprocess
|
82
|
+
# standard out is connected to STDERR of the Toys process.
|
83
|
+
#
|
84
|
+
# In addition, the following options recognized by `Process#spawn` are
|
85
|
+
# supported.
|
86
|
+
#
|
87
|
+
# * `:chdir`
|
88
|
+
# * `:close_others`
|
89
|
+
# * `:new_pgroup`
|
90
|
+
# * `:pgroup`
|
91
|
+
# * `:umask`
|
92
|
+
# * `:unsetenv_others`
|
93
|
+
#
|
94
|
+
# Any other options are ignored.
|
95
|
+
#
|
96
|
+
# Configuration options may be provided to any method that starts a
|
97
|
+
# subprocess. You may also modify default values by calling
|
98
|
+
# {Toys::Utils::Exec#config_defaults}.
|
99
|
+
#
|
100
|
+
class Exec
|
101
|
+
##
|
102
|
+
# Create an exec service.
|
103
|
+
#
|
104
|
+
# @param [Hash] opts Initial default options.
|
105
|
+
#
|
106
|
+
def initialize(opts = {}, &block)
|
107
|
+
@default_opts = Opts.new(&block).add(opts)
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Set default options
|
112
|
+
#
|
113
|
+
# @param [Hash] opts New default options to set
|
114
|
+
#
|
115
|
+
def config_defaults(opts = {})
|
116
|
+
@default_opts.add(opts)
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Execute a command. The command may be given as a single string to pass
|
122
|
+
# to a shell, or an array of strings indicating a posix command.
|
123
|
+
#
|
124
|
+
# If you provide a block, a {Toys::Utils::Exec::Controller} will be
|
125
|
+
# yielded to it, allowing you to interact with the subprocess streams.
|
126
|
+
#
|
127
|
+
# @param [String,Array<String>] cmd The command to execute.
|
128
|
+
# @param [Hash] opts The command options. See the section on
|
129
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
130
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
131
|
+
# for the subprocess streams.
|
132
|
+
#
|
133
|
+
# @return [Toys::Utils::Exec::Result] The subprocess result, including
|
134
|
+
# exit code and any captured output.
|
135
|
+
#
|
136
|
+
def exec(cmd, opts = {}, &block)
|
137
|
+
exec_opts = Opts.new(@default_opts).add(opts)
|
138
|
+
executor = Executor.new(exec_opts, cmd)
|
139
|
+
executor.execute(&block)
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Spawn a ruby process and pass the given arguments to it.
|
144
|
+
#
|
145
|
+
# If you provide a block, a {Toys::Utils::Exec::Controller} will be
|
146
|
+
# yielded to it, allowing you to interact with the subprocess streams.
|
147
|
+
#
|
148
|
+
# @param [String,Array<String>] args The arguments to ruby.
|
149
|
+
# @param [Hash] opts The command options. See the section on
|
150
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
151
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
152
|
+
# for the subprocess streams.
|
153
|
+
#
|
154
|
+
# @return [Toys::Utils::Result] The subprocess result, including
|
155
|
+
# exit code and any captured output.
|
156
|
+
#
|
157
|
+
def ruby(args, opts = {}, &block)
|
158
|
+
cmd =
|
159
|
+
if args.is_a?(::Array)
|
160
|
+
[[::RbConfig.ruby, "ruby"]] + args
|
161
|
+
else
|
162
|
+
"#{::RbConfig.ruby} #{args}"
|
163
|
+
end
|
164
|
+
exec(cmd, opts, &block)
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Execute the given string in a shell. Returns the exit code.
|
169
|
+
#
|
170
|
+
# @param [String] cmd The shell command to execute.
|
171
|
+
# @param [Hash] opts The command options. See the section on
|
172
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
173
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
174
|
+
# for the subprocess streams.
|
175
|
+
#
|
176
|
+
# @return [Integer] The exit code
|
177
|
+
#
|
178
|
+
def sh(cmd, opts = {})
|
179
|
+
exec(cmd, opts).exit_code
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Execute a command. The command may be given as a single string to pass
|
184
|
+
# to a shell, or an array of strings indicating a posix command.
|
185
|
+
#
|
186
|
+
# Captures standard out and returns it as a string.
|
187
|
+
#
|
188
|
+
# @param [String,Array<String>] cmd The command to execute.
|
189
|
+
# @param [Hash] opts The command options. See the section on
|
190
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
191
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller
|
192
|
+
# for the subprocess streams.
|
193
|
+
#
|
194
|
+
# @return [String] What was written to standard out.
|
195
|
+
#
|
196
|
+
def capture(cmd, opts = {})
|
197
|
+
exec(cmd, opts.merge(out_to: :capture)).captured_out
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# An internal helper class storing the configuration of a subprocess invocation
|
202
|
+
# @private
|
203
|
+
#
|
204
|
+
class Opts
|
205
|
+
##
|
206
|
+
# Option keys that belong to exec configuration
|
207
|
+
# @private
|
208
|
+
#
|
209
|
+
CONFIG_KEYS = %i[
|
210
|
+
env
|
211
|
+
err_to
|
212
|
+
in_from
|
213
|
+
logger
|
214
|
+
log_level
|
215
|
+
nonzero_status_handler
|
216
|
+
out_to
|
217
|
+
].freeze
|
218
|
+
|
219
|
+
##
|
220
|
+
# Option keys that belong to spawn configuration
|
221
|
+
# @private
|
222
|
+
#
|
223
|
+
SPAWN_KEYS = %i[
|
224
|
+
chdir
|
225
|
+
close_others
|
226
|
+
new_pgroup
|
227
|
+
pgroup
|
228
|
+
umask
|
229
|
+
unsetenv_others
|
230
|
+
].freeze
|
231
|
+
|
232
|
+
def initialize(parent = nil)
|
233
|
+
if parent
|
234
|
+
@config_opts = ::Hash.new { |_h, k| parent.config_opts[k] }
|
235
|
+
@spawn_opts = ::Hash.new { |_h, k| parent.spawn_opts[k] }
|
236
|
+
elsif block_given?
|
237
|
+
@config_opts = ::Hash.new { |_h, k| yield k }
|
238
|
+
@spawn_opts = ::Hash.new { |_h, k| yield k }
|
239
|
+
else
|
240
|
+
@config_opts = {}
|
241
|
+
@spawn_opts = {}
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def add(config)
|
246
|
+
config.each do |k, v|
|
247
|
+
if CONFIG_KEYS.include?(k)
|
248
|
+
@config_opts[k] = v
|
249
|
+
elsif SPAWN_KEYS.include?(k)
|
250
|
+
@spawn_opts[k] = v
|
251
|
+
end
|
252
|
+
end
|
253
|
+
self
|
254
|
+
end
|
255
|
+
|
256
|
+
def delete(*keys)
|
257
|
+
keys.each do |k|
|
258
|
+
if CONFIG_KEYS.include?(k)
|
259
|
+
@config_opts.delete(k)
|
260
|
+
elsif SPAWN_KEYS.include?(k)
|
261
|
+
@spawn_opts.delete(k)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
self
|
265
|
+
end
|
266
|
+
|
267
|
+
attr_reader :config_opts
|
268
|
+
attr_reader :spawn_opts
|
269
|
+
end
|
270
|
+
|
271
|
+
##
|
272
|
+
# An object of this type is passed to a subcommand control block.
|
273
|
+
# You may use this object to interact with the subcommand's streams,
|
274
|
+
# and/or send signals to the process.
|
275
|
+
#
|
276
|
+
class Controller
|
277
|
+
## @private
|
278
|
+
def initialize(ins, out, err, pid)
|
279
|
+
@in = ins
|
280
|
+
@out = out
|
281
|
+
@err = err
|
282
|
+
@pid = pid
|
283
|
+
end
|
284
|
+
|
285
|
+
##
|
286
|
+
# Return the subcommand's standard input stream (which can be written
|
287
|
+
# to), if the command was configured with `in_from: :controller`.
|
288
|
+
# Returns `nil` otherwise.
|
289
|
+
# @return [IO,nil]
|
290
|
+
#
|
291
|
+
attr_reader :in
|
292
|
+
|
293
|
+
##
|
294
|
+
# Return the subcommand's standard output stream (which can be read
|
295
|
+
# from), if the command was configured with `out_to: :controller`.
|
296
|
+
# Returns `nil` otherwise.
|
297
|
+
# @return [IO,nil]
|
298
|
+
#
|
299
|
+
attr_reader :out
|
300
|
+
|
301
|
+
##
|
302
|
+
# Return the subcommand's standard error stream (which can be read
|
303
|
+
# from), if the command was configured with `err_to: :controller`.
|
304
|
+
# Returns `nil` otherwise.
|
305
|
+
# @return [IO,nil]
|
306
|
+
#
|
307
|
+
attr_reader :err
|
308
|
+
|
309
|
+
##
|
310
|
+
# Returns the process ID.
|
311
|
+
# @return [Integer]
|
312
|
+
#
|
313
|
+
attr_reader :pid
|
314
|
+
|
315
|
+
##
|
316
|
+
# Send the given signal to the process. The signal may be specified
|
317
|
+
# by name or number.
|
318
|
+
#
|
319
|
+
# @param [Integer,String] signal The signal to send.
|
320
|
+
#
|
321
|
+
def kill(signal)
|
322
|
+
::Process.kill(signal, pid)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
##
|
327
|
+
# The return result from a subcommand
|
328
|
+
#
|
329
|
+
class Result
|
330
|
+
## @private
|
331
|
+
def initialize(out, err, status)
|
332
|
+
@captured_out = out
|
333
|
+
@captured_err = err
|
334
|
+
@status = status
|
335
|
+
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Returns the captured output string, if the command was configured
|
339
|
+
# with `out_to: :capture`. Returns `nil` otherwise.
|
340
|
+
# @return [String,nil]
|
341
|
+
#
|
342
|
+
attr_reader :captured_out
|
343
|
+
|
344
|
+
##
|
345
|
+
# Returns the captured error string, if the command was configured
|
346
|
+
# with `err_to: :capture`. Returns `nil` otherwise.
|
347
|
+
# @return [String,nil]
|
348
|
+
#
|
349
|
+
attr_reader :captured_err
|
350
|
+
|
351
|
+
##
|
352
|
+
# Returns the status code object.
|
353
|
+
# @return [Process::Status]
|
354
|
+
#
|
355
|
+
attr_reader :status
|
356
|
+
|
357
|
+
##
|
358
|
+
# Returns the numeric status code.
|
359
|
+
# @return [Integer]
|
360
|
+
#
|
361
|
+
def exit_code
|
362
|
+
status.exitstatus
|
363
|
+
end
|
364
|
+
|
365
|
+
##
|
366
|
+
# Returns true if the subprocess terminated with a zero status.
|
367
|
+
# @return [Boolean]
|
368
|
+
#
|
369
|
+
def success?
|
370
|
+
exit_code.zero?
|
371
|
+
end
|
372
|
+
|
373
|
+
##
|
374
|
+
# Returns true if the subprocess terminated with a nonzero status.
|
375
|
+
# @return [Boolean]
|
376
|
+
#
|
377
|
+
def error?
|
378
|
+
!exit_code.zero?
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
##
|
383
|
+
# An object that manages the execution of a subcommand
|
384
|
+
# @private
|
385
|
+
#
|
386
|
+
class Executor
|
387
|
+
def initialize(exec_opts, cmd)
|
388
|
+
@cmd = Array(cmd)
|
389
|
+
@config_opts = exec_opts.config_opts
|
390
|
+
@spawn_opts = exec_opts.spawn_opts
|
391
|
+
@captures = {}
|
392
|
+
@controller_streams = {}
|
393
|
+
@join_threads = []
|
394
|
+
@child_streams = []
|
395
|
+
end
|
396
|
+
|
397
|
+
def execute(&block)
|
398
|
+
setup_in_stream
|
399
|
+
setup_out_stream(:out, :out_to, :out)
|
400
|
+
setup_out_stream(:err, :err_to, :err)
|
401
|
+
log_command
|
402
|
+
wait_thread = start_process
|
403
|
+
status = control_process(wait_thread, &block)
|
404
|
+
create_result(status)
|
405
|
+
end
|
406
|
+
|
407
|
+
private
|
408
|
+
|
409
|
+
def log_command
|
410
|
+
logger = @config_opts[:logger]
|
411
|
+
if logger && @config_opts[:log_level] != false
|
412
|
+
cmd_str = @cmd.size == 1 ? @cmd.first : @cmd.inspect
|
413
|
+
logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str)
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def start_process
|
418
|
+
args = []
|
419
|
+
args << @config_opts[:env] if @config_opts[:env]
|
420
|
+
args.concat(@cmd)
|
421
|
+
pid = ::Process.spawn(*args, @spawn_opts)
|
422
|
+
@child_streams.each(&:close)
|
423
|
+
::Process.detach(pid)
|
424
|
+
end
|
425
|
+
|
426
|
+
def control_process(wait_thread)
|
427
|
+
begin
|
428
|
+
if block_given?
|
429
|
+
controller = Controller.new(
|
430
|
+
@controller_streams[:in], @controller_streams[:out], @controller_streams[:err],
|
431
|
+
wait_thread.pid
|
432
|
+
)
|
433
|
+
yield controller
|
434
|
+
end
|
435
|
+
ensure
|
436
|
+
@controller_streams.each_value(&:close)
|
437
|
+
end
|
438
|
+
@join_threads.each(&:join)
|
439
|
+
wait_thread.value
|
440
|
+
end
|
441
|
+
|
442
|
+
def create_result(status)
|
443
|
+
nonzero_status_handler = @config_opts[:nonzero_status_handler]
|
444
|
+
nonzero_status_handler.call(status) if nonzero_status_handler && status.exitstatus != 0
|
445
|
+
Result.new(@captures[:out], @captures[:err], status)
|
446
|
+
end
|
447
|
+
|
448
|
+
def setup_in_stream
|
449
|
+
setting = @config_opts[:in_from]
|
450
|
+
if setting
|
451
|
+
r, w = ::IO.pipe
|
452
|
+
@spawn_opts[:in] = r
|
453
|
+
w.sync = true
|
454
|
+
@child_streams << r
|
455
|
+
case setting
|
456
|
+
when :controller
|
457
|
+
@controller_streams[:in] = w
|
458
|
+
when String
|
459
|
+
write_string_thread(w, setting)
|
460
|
+
else
|
461
|
+
raise "Unknown type for in_from"
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
def setup_out_stream(stream_name, config_key, spawn_key)
|
467
|
+
setting = @config_opts[config_key]
|
468
|
+
if setting
|
469
|
+
r, w = ::IO.pipe
|
470
|
+
@spawn_opts[spawn_key] = w
|
471
|
+
@child_streams << w
|
472
|
+
case setting
|
473
|
+
when :controller
|
474
|
+
@controller_streams[stream_name] = r
|
475
|
+
when :capture
|
476
|
+
@join_threads << capture_stream_thread(r, stream_name)
|
477
|
+
else
|
478
|
+
raise "Unknown type for #{config_key}"
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
def write_string_thread(stream, string)
|
484
|
+
::Thread.new do
|
485
|
+
begin
|
486
|
+
stream.write string
|
487
|
+
ensure
|
488
|
+
stream.close
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def capture_stream_thread(stream, name)
|
494
|
+
::Thread.new do
|
495
|
+
begin
|
496
|
+
@captures[name] = stream.read
|
497
|
+
ensure
|
498
|
+
stream.close
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|