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
data/lib/toys/core_version.rb
CHANGED
data/lib/toys/helpers/exec.rb
CHANGED
@@ -27,7 +27,7 @@
|
|
27
27
|
# POSSIBILITY OF SUCH DAMAGE.
|
28
28
|
;
|
29
29
|
|
30
|
-
require "
|
30
|
+
require "toys/utils/exec"
|
31
31
|
|
32
32
|
module Toys
|
33
33
|
module Helpers
|
@@ -37,98 +37,71 @@ module Toys
|
|
37
37
|
# in a string. Also provides an interface for controlling a spawned
|
38
38
|
# process's streams.
|
39
39
|
#
|
40
|
-
#
|
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}.
|
40
|
+
# This is a frontend for {Toys::Utils::Exec}. More information is
|
41
|
+
# available in that class's documentation.
|
72
42
|
#
|
73
43
|
module Exec
|
44
|
+
## @private
|
45
|
+
def self.extended(context)
|
46
|
+
context[Exec] = Utils::Exec.new do |k|
|
47
|
+
case k
|
48
|
+
when :logger
|
49
|
+
context[Context::LOGGER]
|
50
|
+
when :nonzero_status_handler
|
51
|
+
if context[Context::EXIT_ON_NONZERO_STATUS]
|
52
|
+
proc { |s| context.exit(s.exitstatus) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
74
58
|
##
|
75
59
|
# Set default configuration keys.
|
76
60
|
#
|
77
61
|
# @param [Hash] opts The default options. See the section on
|
78
|
-
# configuration options in the {Toys::
|
62
|
+
# configuration options in the {Toys::Utils::Exec} docs.
|
79
63
|
#
|
80
64
|
def configure_exec(opts = {})
|
81
|
-
|
82
|
-
@exec_config.merge!(opts)
|
65
|
+
self[Exec].config_defaults(opts)
|
83
66
|
end
|
84
67
|
|
85
68
|
##
|
86
69
|
# Execute a command. The command may be given as a single string to pass
|
87
70
|
# to a shell, or an array of strings indicating a posix command.
|
88
71
|
#
|
89
|
-
# If you provide a block, a {Toys::
|
72
|
+
# If you provide a block, a {Toys::Utils::Exec::Controller} will be
|
90
73
|
# yielded to it, allowing you to interact with the subprocess streams.
|
91
74
|
#
|
92
75
|
# @param [String,Array<String>] cmd The command to execute.
|
93
76
|
# @param [Hash] opts The command options. See the section on
|
94
|
-
# configuration options in the {Toys::
|
95
|
-
# @yieldparam controller [Toys::
|
96
|
-
#
|
77
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
78
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
|
79
|
+
# the subprocess streams.
|
97
80
|
#
|
98
|
-
# @return [Toys::
|
99
|
-
#
|
81
|
+
# @return [Toys::Utils::Result] The subprocess result, including the exit
|
82
|
+
# code and any captured output.
|
100
83
|
#
|
101
84
|
def exec(cmd, opts = {}, &block)
|
102
|
-
|
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)
|
85
|
+
self[Exec].exec(cmd, Exec._setup_exec_opts(opts, self), &block)
|
107
86
|
end
|
108
87
|
|
109
88
|
##
|
110
89
|
# Spawn a ruby process and pass the given arguments to it.
|
111
90
|
#
|
112
|
-
# If you provide a block, a {Toys::
|
91
|
+
# If you provide a block, a {Toys::Utils::Exec::Controller} will be
|
113
92
|
# yielded to it, allowing you to interact with the subprocess streams.
|
114
93
|
#
|
115
94
|
# @param [String,Array<String>] args The arguments to ruby.
|
116
95
|
# @param [Hash] opts The command options. See the section on
|
117
|
-
# configuration options in the {Toys::
|
118
|
-
# @yieldparam controller [Toys::
|
96
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
97
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
|
119
98
|
# for the subprocess streams.
|
120
99
|
#
|
121
|
-
# @return [Toys::
|
122
|
-
#
|
100
|
+
# @return [Toys::Utils::Result] The subprocess result, including the exit
|
101
|
+
# code and any captured output.
|
123
102
|
#
|
124
103
|
def ruby(args, opts = {}, &block)
|
125
|
-
|
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)
|
104
|
+
self[Exec].ruby(args, Exec._setup_exec_opts(opts, self), &block)
|
132
105
|
end
|
133
106
|
|
134
107
|
##
|
@@ -136,14 +109,14 @@ module Toys
|
|
136
109
|
#
|
137
110
|
# @param [String] cmd The shell command to execute.
|
138
111
|
# @param [Hash] opts The command options. See the section on
|
139
|
-
# configuration options in the {Toys::
|
140
|
-
# @yieldparam controller [Toys::
|
141
|
-
#
|
112
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
113
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
|
114
|
+
# the subprocess streams.
|
142
115
|
#
|
143
116
|
# @return [Integer] The exit code
|
144
117
|
#
|
145
118
|
def sh(cmd, opts = {})
|
146
|
-
|
119
|
+
self[Exec].sh(cmd, Exec._setup_exec_opts(opts, self))
|
147
120
|
end
|
148
121
|
|
149
122
|
##
|
@@ -154,315 +127,24 @@ module Toys
|
|
154
127
|
#
|
155
128
|
# @param [String,Array<String>] cmd The command to execute.
|
156
129
|
# @param [Hash] opts The command options. See the section on
|
157
|
-
# configuration options in the {Toys::
|
158
|
-
# @yieldparam controller [Toys::
|
159
|
-
#
|
130
|
+
# configuration options in the {Toys::Utils::Exec} module docs.
|
131
|
+
# @yieldparam controller [Toys::Utils::Exec::Controller] A controller for
|
132
|
+
# the subprocess streams.
|
160
133
|
#
|
161
134
|
# @return [String] What was written to standard out.
|
162
135
|
#
|
163
136
|
def capture(cmd, opts = {})
|
164
|
-
|
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
|
137
|
+
self[Exec].capture(cmd, Exec._setup_exec_opts(opts, self))
|
301
138
|
end
|
302
139
|
|
303
|
-
##
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
140
|
+
## @private
|
141
|
+
def self._setup_exec_opts(opts, context)
|
142
|
+
return opts unless opts.key?(:exit_on_nonzero_status)
|
143
|
+
nonzero_status_handler =
|
144
|
+
if opts[:exit_on_nonzero_status]
|
145
|
+
proc { |s| context.exit(s.exitstatus) }
|
454
146
|
end
|
455
|
-
|
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
|
147
|
+
opts.merge(nonzero_status_handler: nonzero_status_handler)
|
466
148
|
end
|
467
149
|
end
|
468
150
|
end
|