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.
@@ -32,5 +32,5 @@ module Toys
32
32
  # Current version of Toys core
33
33
  # @return [String]
34
34
  #
35
- CORE_VERSION = "0.3.4".freeze
35
+ CORE_VERSION = "0.3.5".freeze
36
36
  end
@@ -27,7 +27,7 @@
27
27
  # POSSIBILITY OF SUCH DAMAGE.
28
28
  ;
29
29
 
30
- require "logger"
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
- # ## 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}.
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::Helpers::Exec} module docs.
62
+ # configuration options in the {Toys::Utils::Exec} docs.
79
63
  #
80
64
  def configure_exec(opts = {})
81
- @exec_config ||= {}
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::Helpers::Exec::Controller} will be
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::Helpers::Exec} module docs.
95
- # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
96
- # for the subprocess streams.
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::Helpers::Result] The subprocess result, including
99
- # exit code and any captured output.
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
- 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)
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::Helpers::Exec::Controller} will be
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::Helpers::Exec} module docs.
118
- # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
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::Helpers::Result] The subprocess result, including
122
- # exit code and any captured output.
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
- 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)
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::Helpers::Exec} module docs.
140
- # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
141
- # for the subprocess streams.
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
- exec(cmd, opts).exit_code
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::Helpers::Exec} module docs.
158
- # @yieldparam controller [Toys::Helpers::Exec::Controller] A controller
159
- # for the subprocess streams.
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
- 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
137
+ self[Exec].capture(cmd, Exec._setup_exec_opts(opts, self))
301
138
  end
302
139
 
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
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
- 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
147
+ opts.merge(nonzero_status_handler: nonzero_status_handler)
466
148
  end
467
149
  end
468
150
  end