toys 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/toys/errors.rb DELETED
@@ -1,42 +0,0 @@
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 LookupError < ::StandardError
41
- end
42
- end
data/lib/toys/helpers.rb DELETED
@@ -1,52 +0,0 @@
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
@@ -1,469 +0,0 @@
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 = {}
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