toys-core 0.3.8 → 0.3.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbe7cd88d7c1eba9d74c0006bfe81372963949d969671ad3d13597b941525a3f
4
- data.tar.gz: 5c567a9a61f747fd292339681d24d7bc56fa89859d9f60f6f0f6796665b93148
3
+ metadata.gz: 5dae1937beb353c94635ea47760fec27761e258b31c1e09770384ea22d768938
4
+ data.tar.gz: de39a399a009af7d284d21115cec946349227b99fefaca6cd511696ad6b83550
5
5
  SHA512:
6
- metadata.gz: fe5308be03b7d7348390caf05d6c6ba76503746133b45931b1b7c75c7b5095f03ca4ee88cefde3e0a46583901a66fa56bd5fb628c242c64d3abb854d3fb548c8
7
- data.tar.gz: 9766792fef7ba0e77aa8e0254f222ea6f64238eaa3bdc1ed758faf8429105662be235014de8c375803a2da38fe2b0c3da1ae110e77a96d127e318f452f29453b
6
+ metadata.gz: 29ab72f03cd03a7efca54b0b9c9f5a9ac479f6cb0df74668b966d8162686f43d311c63758ba919084cac420cb9b35546b144e7afe779435a256014f9009745e4
7
+ data.tar.gz: 05f7d64072c690d2b9686455e85d7a499cc1bb67f3b3cf98c5a27e83eb2a101c57c3b93f3889f091a5c30eb1ce024876b45f5fca1359ba555eb166e005533505
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Release History
2
2
 
3
+ ### 0.3.9 / 2018-06-24
4
+
5
+ * CHANGED: Cli#add_search_path_hierarchy changed the behavior of the base/terminate param
6
+ * CHANGED: Removed alias_as directive since it's incompatible with selective loading.
7
+ * ADDED: Ability to define named templates in Toys files
8
+ * ADDED: Ability to disable argument parsing
9
+ * ADDED: Exec#exec_proc and Exec#exec_tool that supports all the stream redirects
10
+ * IMPROVED: Acceptors can be looked up recursively in the same way as mixins and templates
11
+
3
12
  ### 0.3.8 / 2018-06-10
4
13
 
5
14
  * CHANGED: Renamed helpers to mixins.
data/lib/toys/cli.rb CHANGED
@@ -183,16 +183,17 @@ module Toys
183
183
  #
184
184
  # @param [String] start The first directory to add. Defaults to the current
185
185
  # working directory.
186
- # @param [String] base The last directory to add. Defaults to `"/"`.
186
+ # @param [Array<String>] terminate Optional list of directories that should
187
+ # terminate the search.
187
188
  # @param [Boolean] high_priority Add the configs at the head of the
188
189
  # priority list rather than the tail.
189
190
  #
190
- def add_search_path_hierarchy(start: nil, base: "/", high_priority: false)
191
+ def add_search_path_hierarchy(start: nil, terminate: [], high_priority: false)
191
192
  path = start || ::Dir.pwd
192
193
  paths = []
193
194
  loop do
195
+ break if terminate.include?(path)
194
196
  paths << path
195
- break if path == base
196
197
  next_path = ::File.dirname(path)
197
198
  break if next_path == path
198
199
  path = next_path
@@ -232,11 +233,12 @@ module Toys
232
233
  # Make a clone with the same settings but no paths in the loader.
233
234
  # This is sometimes useful for running sub-tools.
234
235
  #
236
+ # @param [Hash] _opts Unused options that can be used by subclasses.
235
237
  # @return [Toys::CLI]
236
238
  # @yieldparam cli [Toys::CLI] If you pass a block, the new CLI is yielded
237
239
  # to it so you can add paths and make other modifications.
238
240
  #
239
- def child
241
+ def child(_opts = {})
240
242
  cli = CLI.new(binary_name: @binary_name,
241
243
  config_dir_name: @config_dir_name,
242
244
  config_file_name: @config_file_name,
@@ -32,5 +32,5 @@ module Toys
32
32
  # Current version of Toys core
33
33
  # @return [String]
34
34
  #
35
- CORE_VERSION = "0.3.8".freeze
35
+ CORE_VERSION = "0.3.9".freeze
36
36
  end
@@ -66,7 +66,7 @@ module Toys
66
66
  # block.
67
67
  #
68
68
  def initialize(name, converter = nil, &block)
69
- @name = name
69
+ @name = name.to_s
70
70
  @converter = converter || block
71
71
  end
72
72
 
@@ -75,14 +75,7 @@ module Toys
75
75
  # @return [String]
76
76
  #
77
77
  attr_reader :name
78
-
79
- ##
80
- # Name of the acceptor as a string
81
- # @return [String]
82
- #
83
- def to_s
84
- name.to_s
85
- end
78
+ alias to_s name
86
79
 
87
80
  ##
88
81
  # Validate the given input.
@@ -43,13 +43,13 @@ module Toys
43
43
  #
44
44
  def initialize(loader, full_name, target, priority)
45
45
  @target_name =
46
- if target.is_a?(::String)
47
- full_name[0..-2] + [target]
46
+ if target.is_a?(::Array)
47
+ target.map(&:to_s)
48
48
  else
49
- target.dup
49
+ full_name[0..-2] + [target.to_s]
50
50
  end
51
51
  @target_name.freeze
52
- @full_name = full_name.dup.freeze
52
+ @full_name = full_name.map(&:to_s).freeze
53
53
  @priority = priority
54
54
  @tool_class = DSL::Tool.new_class(@full_name, priority, loader)
55
55
  end
@@ -28,6 +28,7 @@
28
28
  ;
29
29
 
30
30
  require "optparse"
31
+ require "set"
31
32
 
32
33
  module Toys
33
34
  module Definition
@@ -44,21 +45,23 @@ module Toys
44
45
  # You can reference these acceptors directly. Otherwise, you have to add
45
46
  # one explicitly to the tool using {Tool#add_acceptor}.
46
47
  #
47
- OPTPARSER_ACCEPTORS = [
48
- ::Object,
49
- ::NilClass,
50
- ::String,
51
- ::Integer,
52
- ::Float,
53
- ::Numeric,
54
- ::TrueClass,
55
- ::FalseClass,
56
- ::Array,
57
- ::Regexp,
58
- ::OptionParser::DecimalInteger,
59
- ::OptionParser::OctalInteger,
60
- ::OptionParser::DecimalNumeric
61
- ].freeze
48
+ OPTPARSER_ACCEPTORS = ::Set.new(
49
+ [
50
+ ::Object,
51
+ ::NilClass,
52
+ ::String,
53
+ ::Integer,
54
+ ::Float,
55
+ ::Numeric,
56
+ ::TrueClass,
57
+ ::FalseClass,
58
+ ::Array,
59
+ ::Regexp,
60
+ ::OptionParser::DecimalInteger,
61
+ ::OptionParser::OctalInteger,
62
+ ::OptionParser::DecimalNumeric
63
+ ]
64
+ ).freeze
62
65
 
63
66
  ##
64
67
  # Create a new tool.
@@ -78,16 +81,18 @@ module Toys
78
81
  @long_desc = []
79
82
 
80
83
  @default_data = {}
81
- @acceptors = {}
82
- OPTPARSER_ACCEPTORS.each { |a| @acceptors[a] = a }
83
84
  @used_flags = []
84
85
 
86
+ @acceptors = {}
85
87
  @mixins = {}
88
+ @templates = {}
86
89
 
87
90
  @flag_definitions = []
88
91
  @required_arg_definitions = []
89
92
  @optional_arg_definitions = []
90
93
  @remaining_args_definition = nil
94
+
95
+ @disable_argument_parsing = false
91
96
  @runnable = false
92
97
  end
93
98
 
@@ -239,6 +244,14 @@ module Toys
239
244
  @definition_finished
240
245
  end
241
246
 
247
+ ##
248
+ # Returns true if this tool has disabled argument parsing.
249
+ # @return [Boolean]
250
+ #
251
+ def argument_parsing_disabled?
252
+ @disable_argument_parsing
253
+ end
254
+
242
255
  ##
243
256
  # Returns all arg definitions in order: required, optional, remaining.
244
257
  # @return [Array<Toys::Definition::Arg>]
@@ -264,6 +277,46 @@ module Toys
264
277
  result.uniq
265
278
  end
266
279
 
280
+ ##
281
+ # Resolve the given acceptor. You may pass in a
282
+ # {Toys::Definition::Acceptor}, an acceptor name, a well-known acceptor
283
+ # understood by OptionParser, or `nil`.
284
+ #
285
+ # Returns either `nil` or an acceptor that is usable by OptionParser.
286
+ #
287
+ # If an acceptor name is given, it may be resolved by this tool or any of
288
+ # its ancestors. Raises {Toys::ToolDefinitionError} if the name is not
289
+ # recognized.
290
+ #
291
+ # @param [Object] accept An acceptor input.
292
+ # @return [Object] The resolved acceptor.
293
+ #
294
+ def resolve_acceptor(accept)
295
+ return accept if accept.nil? || accept.is_a?(Acceptor)
296
+ name = accept
297
+ accept = @acceptors.fetch(name) do |k|
298
+ if @parent
299
+ @parent.resolve_acceptor(k)
300
+ elsif OPTPARSER_ACCEPTORS.include?(k)
301
+ k
302
+ end
303
+ end
304
+ if accept.nil?
305
+ raise ToolDefinitionError, "Unknown acceptor: #{name.inspect}"
306
+ end
307
+ accept
308
+ end
309
+
310
+ ##
311
+ # Get the named template from this tool or its ancestors.
312
+ #
313
+ # @param [String] name The template name
314
+ # @return [Class,nil] The template class, or `nil` if not found.
315
+ #
316
+ def resolve_template(name)
317
+ @templates.fetch(name.to_s) { |k| @parent ? @parent.resolve_template(k) : nil }
318
+ end
319
+
267
320
  ##
268
321
  # Get the named mixin from this tool or its ancestors.
269
322
  #
@@ -335,6 +388,11 @@ module Toys
335
388
  # @param [Toys::Definition::Acceptor] acceptor The acceptor to add.
336
389
  #
337
390
  def add_acceptor(acceptor)
391
+ if @acceptors.key?(acceptor.name)
392
+ raise ToolDefinitionError,
393
+ "An acceptor named #{acceptor.name.inspect} has already been" \
394
+ " defined in tool #{display_name.inspect}."
395
+ end
338
396
  @acceptors[acceptor.name] = acceptor
339
397
  self
340
398
  end
@@ -346,7 +404,44 @@ module Toys
346
404
  # @param [Module] mixin_module The mixin module.
347
405
  #
348
406
  def add_mixin(name, mixin_module)
349
- @mixins[name.to_s] = mixin_module
407
+ name = name.to_s
408
+ if @mixins.key?(name)
409
+ raise ToolDefinitionError,
410
+ "A mixin named #{name.inspect} has already been defined in tool" \
411
+ " #{display_name.inspect}."
412
+ end
413
+ @mixins[name] = mixin_module
414
+ self
415
+ end
416
+
417
+ ##
418
+ # Add a named template class to this tool.
419
+ #
420
+ # @param [String] name The name of the template.
421
+ # @param [Class] template_class The template class.
422
+ #
423
+ def add_template(name, template_class)
424
+ name = name.to_s
425
+ if @templates.key?(name)
426
+ raise ToolDefinitionError,
427
+ "A template named #{name.inspect} has already been defined in tool" \
428
+ " #{display_name.inspect}."
429
+ end
430
+ @templates[name] = template_class
431
+ self
432
+ end
433
+
434
+ ##
435
+ # Disable argument parsing for this tool
436
+ #
437
+ def disable_argument_parsing
438
+ check_definition_state
439
+ if includes_arguments?
440
+ raise ToolDefinitionError,
441
+ "Cannot disable argument parsing for tool #{display_name.inspect}" \
442
+ " because arguments have already been defined."
443
+ end
444
+ @disable_argument_parsing = true
350
445
  self
351
446
  end
352
447
 
@@ -385,7 +480,7 @@ module Toys
385
480
  accept: nil, default: nil, handler: nil,
386
481
  report_collisions: true,
387
482
  desc: nil, long_desc: nil)
388
- check_definition_state
483
+ check_definition_state(is_arg: true)
389
484
  accept = resolve_acceptor(accept)
390
485
  flag_def = Definition::Flag.new(key, flags, @used_flags, report_collisions,
391
486
  accept, handler, default)
@@ -404,6 +499,7 @@ module Toys
404
499
  # @param [String...] flags The flags to disable
405
500
  #
406
501
  def disable_flag(*flags)
502
+ check_definition_state(is_arg: true)
407
503
  flags = flags.uniq
408
504
  intersection = @used_flags & flags
409
505
  unless intersection.empty?
@@ -435,7 +531,7 @@ module Toys
435
531
  # formats. Defaults to the empty array.
436
532
  #
437
533
  def add_required_arg(key, accept: nil, display_name: nil, desc: nil, long_desc: nil)
438
- check_definition_state
534
+ check_definition_state(is_arg: true)
439
535
  accept = resolve_acceptor(accept)
440
536
  arg_def = Definition::Arg.new(key, :required, accept, nil, desc, long_desc, display_name)
441
537
  @required_arg_definitions << arg_def
@@ -469,7 +565,7 @@ module Toys
469
565
  #
470
566
  def add_optional_arg(key, default: nil, accept: nil, display_name: nil,
471
567
  desc: nil, long_desc: nil)
472
- check_definition_state
568
+ check_definition_state(is_arg: true)
473
569
  accept = resolve_acceptor(accept)
474
570
  arg_def = Definition::Arg.new(key, :optional, accept, default,
475
571
  desc, long_desc, display_name)
@@ -504,7 +600,7 @@ module Toys
504
600
  #
505
601
  def set_remaining_args(key, default: [], accept: nil, display_name: nil,
506
602
  desc: nil, long_desc: nil)
507
- check_definition_state
603
+ check_definition_state(is_arg: true)
508
604
  accept = resolve_acceptor(accept)
509
605
  arg_def = Definition::Arg.new(key, :remaining, accept, default,
510
606
  desc, long_desc, display_name)
@@ -557,19 +653,16 @@ module Toys
557
653
  proc { middleware.config(self, loader, &next_config) }
558
654
  end
559
655
 
560
- def check_definition_state
656
+ def check_definition_state(is_arg: false)
561
657
  if @definition_finished
562
658
  raise ToolDefinitionError,
563
659
  "Defintion of tool #{display_name.inspect} is already finished"
564
660
  end
565
- end
566
-
567
- def resolve_acceptor(accept)
568
- return accept if accept.nil? || accept.is_a?(Acceptor)
569
- unless @acceptors.key?(accept)
570
- raise ToolDefinitionError, "Unknown acceptor: #{accept.inspect}"
661
+ if is_arg && argument_parsing_disabled?
662
+ raise ToolDefinitionError,
663
+ "Tool #{display_name.inspect} has disabled argument parsing"
571
664
  end
572
- @acceptors[accept]
665
+ self
573
666
  end
574
667
  end
575
668
  end
data/lib/toys/dsl/tool.rb CHANGED
@@ -139,6 +139,30 @@ module Toys
139
139
  self
140
140
  end
141
141
 
142
+ ##
143
+ # Create a named template class.
144
+ # This template may be expanded by name in this tool or any subtool.
145
+ #
146
+ # You should pass a block and define the template in that block. You do
147
+ # not need to include `Toys::Template` in the block. Otherwise, see
148
+ # {Toys::Template} for information on defining a template. In general,
149
+ # the block should define an initialize method, and call `to_expand` to
150
+ # define how to expand the template.
151
+ #
152
+ # @param [String] name Name of the template
153
+ #
154
+ def template(name, &block)
155
+ cur_tool = DSL::Tool.activate_tool(self)
156
+ if cur_tool
157
+ template_class = ::Class.new do
158
+ include ::Toys::Template
159
+ end
160
+ template_class.class_eval(&block)
161
+ cur_tool.add_template(name, template_class)
162
+ end
163
+ self
164
+ end
165
+
142
166
  ##
143
167
  # Create a subtool. You must provide a block defining the subtool.
144
168
  #
@@ -170,19 +194,6 @@ module Toys
170
194
  self
171
195
  end
172
196
 
173
- ##
174
- # Create an alias of the current tool.
175
- #
176
- # @param [String] word The name of the alias
177
- #
178
- def alias_as(word)
179
- if @__words.empty?
180
- raise ToolDefinitionError, "Cannot make an alias of the root."
181
- end
182
- @__loader.make_alias(@__words[0..-2] + [word.to_s], @__words, @__priority)
183
- self
184
- end
185
-
186
197
  ##
187
198
  # Include another config file or directory at the current location.
188
199
  #
@@ -204,12 +215,14 @@ module Toys
204
215
  # @param [Object...] args Template arguments
205
216
  #
206
217
  def expand(template_class, *args)
207
- unless template_class.is_a?(::Class)
208
- name = template_class.to_s
218
+ name = template_class.to_s
219
+ if template_class.is_a?(::String)
220
+ template_class = cur_tool.resolve_template(template_class)
221
+ elsif template_class.is_a?(::Symbol)
209
222
  template_class = @__loader.resolve_standard_template(name)
210
- if template_class.nil?
211
- raise ToolDefinitionError, "Template name not found: #{name.inspect}"
212
- end
223
+ end
224
+ if template_class.nil?
225
+ raise ToolDefinitionError, "Template not found: #{name.inspect}"
213
226
  end
214
227
  template = template_class.new(*args)
215
228
  yield template if block_given?
@@ -446,6 +459,47 @@ module Toys
446
459
  end
447
460
  alias remaining remaining_args
448
461
 
462
+ ##
463
+ # Set an option value statically.
464
+ #
465
+ # @param [Symbol] key The key to use to retrieve the value from the
466
+ # execution context.
467
+ # @param [Object] value The value to set.
468
+ #
469
+ def set(key, value)
470
+ cur_tool = DSL::Tool.activate_tool(self)
471
+ return self if cur_tool.nil?
472
+ cur_tool.default_data[key] = value
473
+ self
474
+ end
475
+
476
+ ##
477
+ # Disable argument parsing for this tool. Arguments will not be parsed
478
+ # and the options will not be populated. Instead, tools can retrieve the
479
+ # full unparsed argument list by calling {Toys::Tool#args}.
480
+ #
481
+ # This directive is mutually exclusive with any of the directives that
482
+ # declare arguments or flags.
483
+ #
484
+ def disable_argument_parsing
485
+ cur_tool = DSL::Tool.activate_tool(self)
486
+ cur_tool.disable_argument_parsing unless cur_tool.nil?
487
+ self
488
+ end
489
+
490
+ ##
491
+ # Mark one or more flags as disabled, preventing their use by any
492
+ # subsequent flag definition. This may be used to prevent middleware from
493
+ # defining a particular flag.
494
+ #
495
+ # @param [String...] flags The flags to disable
496
+ #
497
+ def disable_flag(*flags)
498
+ cur_tool = DSL::Tool.activate_tool(self)
499
+ cur_tool.disable_flag(*flags) unless cur_tool.nil?
500
+ self
501
+ end
502
+
449
503
  ##
450
504
  # Specify how to run this tool. You may do this by providing a block to
451
505
  # this directive, or by defining the `run` method in the tool.
@@ -480,6 +534,17 @@ module Toys
480
534
  super(mod)
481
535
  end
482
536
 
537
+ ##
538
+ # Activate the given gem. If it is not present, attempt to install it (or
539
+ # inform the user to update the bundle).
540
+ #
541
+ # @param [String] name Name of the gem
542
+ # @param [String...] requirements Version requirements
543
+ #
544
+ def gem(name, *requirements)
545
+ (@__gems ||= Utils::Gems.new).activate(name, *requirements)
546
+ end
547
+
483
548
  ## @private
484
549
  def self.new_class(words, priority, loader)
485
550
  tool_class = ::Class.new(::Toys::Tool)
@@ -44,12 +44,15 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
44
44
  include ::Toys::Tool::Keys
45
45
  @tool_class = tool_class
46
46
  end
47
- name = "M#{namespace.object_id}"
47
+ basename = ::File.basename(path).tr(".-", "_").gsub(/\W/, "")
48
+ name = "M#{namespace.object_id}_#{basename}"
48
49
  const_set(name, namespace)
49
50
  str = <<-STR
50
- module #{name}; @tool_class.class_eval do
51
- #{::IO.read(path)}
52
- end end
51
+ module #{name}
52
+ @tool_class.class_eval do
53
+ #{::IO.read(path)}
54
+ end
55
+ end
53
56
  STR
54
57
  ::Toys::DSL::Tool.prepare(tool_class, remaining_words, path) do
55
58
  ::Toys::ContextualError.capture_path("Error while loading Toys config!", path) do
data/lib/toys/runner.rb CHANGED
@@ -59,7 +59,7 @@ module Toys
59
59
  #
60
60
  def run(args, verbosity: 0)
61
61
  data = create_data(args, verbosity)
62
- parse_args(args, data)
62
+ parse_args(args, data) unless @tool_definition.argument_parsing_disabled?
63
63
  tool = @tool_definition.tool_class.new(@cli, data)
64
64
 
65
65
  original_level = @cli.logger.level
@@ -89,21 +89,49 @@ module Toys
89
89
  # @return [Toys::Utils::Exec::Result] The subprocess result, including
90
90
  # the exit code and any captured output.
91
91
  #
92
- def ruby(args, opts = {}, &block)
93
- Exec._exec(self).ruby(args, Exec._setup_exec_opts(opts, self), &block)
92
+ def exec_ruby(args, opts = {}, &block)
93
+ Exec._exec(self).exec_ruby(args, Exec._setup_exec_opts(opts, self), &block)
94
94
  end
95
+ alias ruby exec_ruby
95
96
 
96
97
  ##
97
- # Execute the given string in a shell. Returns the exit code.
98
+ # Execute a proc in a subprocess.
98
99
  #
99
- # @param [String] cmd The shell command to execute.
100
+ # If you provide a block, a {Toys::Utils::Exec::Controller} will be
101
+ # yielded to it, allowing you to interact with the subprocess streams.
102
+ #
103
+ # @param [Proc] func The proc to call.
100
104
  # @param [Hash] opts The command options. See the section on
101
105
  # configuration options in the {Toys::Utils::Exec} module docs.
106
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
107
+ # for the subprocess streams.
102
108
  #
103
- # @return [Integer] The exit code
109
+ # @return [Toys::Utils::Exec::Result] The subprocess result, including
110
+ # exit code and any captured output.
104
111
  #
105
- def sh(cmd, opts = {})
106
- Exec._exec(self).sh(cmd, Exec._setup_exec_opts(opts, self))
112
+ def exec_proc(func, opts = {}, &block)
113
+ Exec._exec(self).exec_proc(func, Exec._setup_exec_opts(opts, self), &block)
114
+ end
115
+
116
+ ##
117
+ # Execute a tool. The command may be given as a single string or an array
118
+ # of strings, representing the tool to run and the arguments to pass.
119
+ #
120
+ # If you provide a block, a {Toys::Utils::Exec::Controller} will be
121
+ # yielded to it, allowing you to interact with the subprocess streams.
122
+ #
123
+ # @param [String,Array<String>] cmd The tool to execute.
124
+ # @param [Hash] opts The command options. See the section on
125
+ # configuration options in the {Toys::Utils::Exec} module docs.
126
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
127
+ # for the subprocess streams.
128
+ #
129
+ # @return [Toys::Utils::Exec::Result] The subprocess result, including
130
+ # exit code and any captured output.
131
+ #
132
+ def exec_tool(cmd, opts = {}, &block)
133
+ func = Exec._make_tool_caller(cmd)
134
+ Exec._exec(self).exec_proc(func, Exec._setup_exec_opts(opts, self), &block)
107
135
  end
108
136
 
109
137
  ##
@@ -122,6 +150,66 @@ module Toys
122
150
  Exec._exec(self).capture(cmd, Exec._setup_exec_opts(opts, self))
123
151
  end
124
152
 
153
+ ##
154
+ # Spawn a ruby process and pass the given arguments to it.
155
+ #
156
+ # Captures standard out and returns it as a string.
157
+ #
158
+ # @param [String,Array<String>] args The arguments to ruby.
159
+ # @param [Hash] opts The command options. See the section on
160
+ # configuration options in the {Toys::Utils::Exec} module docs.
161
+ #
162
+ # @return [String] What was written to standard out.
163
+ #
164
+ def capture_ruby(args, opts = {})
165
+ Exec._exec(self).capture_ruby(args, Exec._setup_exec_opts(opts, self))
166
+ end
167
+
168
+ ##
169
+ # Execute a proc in a subprocess.
170
+ #
171
+ # Captures standard out and returns it as a string.
172
+ #
173
+ # @param [Proc] func The proc to call.
174
+ # @param [Hash] opts The command options. See the section on
175
+ # configuration options in the {Toys::Utils::Exec} module docs.
176
+ #
177
+ # @return [String] What was written to standard out.
178
+ #
179
+ def capture_proc(func, opts = {})
180
+ Exec._exec(self).capture_proc(func, Exec._setup_exec_opts(opts, self))
181
+ end
182
+
183
+ ##
184
+ # Execute a tool. The command may be given as a single string or an array
185
+ # of strings, representing the tool to run and the arguments to pass.
186
+ #
187
+ # Captures standard out and returns it as a string.
188
+ #
189
+ # @param [String,Array<String>] cmd The tool to execute.
190
+ # @param [Hash] opts The command options. See the section on
191
+ # configuration options in the {Toys::Utils::Exec} module docs.
192
+ #
193
+ # @return [String] What was written to standard out.
194
+ #
195
+ def capture_tool(cmd, opts = {})
196
+ func = Exec._make_tool_caller(cmd)
197
+ Exec._exec(self).capture_proc(func, Exec._setup_exec_opts(opts, self))
198
+ end
199
+
200
+ ##
201
+ # Execute the given string in a shell. Returns the exit code.
202
+ #
203
+ # @param [String] cmd The shell command to execute.
204
+ # @param [Hash] opts The command options. See the section on
205
+ # configuration options in the {Toys::Utils::Exec} module docs.
206
+ #
207
+ # @return [Integer] The exit code
208
+ #
209
+ def sh(cmd, opts = {})
210
+ Exec._exec(self).sh(cmd, Exec._setup_exec_opts(opts, self))
211
+ end
212
+
125
213
  ##
126
214
  # Exit if the given status code is nonzero. Otherwise, returns 0.
127
215
  #
@@ -137,10 +225,21 @@ module Toys
137
225
  ## @private
138
226
  def self._exec(tool)
139
227
  tool[Exec] ||= Utils::Exec.new do |k|
140
- k == :logger ? tool[Tool::Keys::LOGGER] : nil
228
+ case k
229
+ when :logger
230
+ tool[Tool::Keys::LOGGER]
231
+ when :cli
232
+ tool[Tool::Keys::CLI]
233
+ end
141
234
  end
142
235
  end
143
236
 
237
+ ## @private
238
+ def self._make_tool_caller(cmd)
239
+ cmd = ::Shellwords.split(cmd) if cmd.is_a?(::String)
240
+ proc { |config| ::Kernel.exit(config[:cli].run(*cmd)) }
241
+ end
242
+
144
243
  ## @private
145
244
  def self._setup_exec_opts(opts, tool)
146
245
  return opts unless opts.key?(:exit_on_nonzero_status)
data/lib/toys/template.rb CHANGED
@@ -99,6 +99,7 @@ module Toys
99
99
  module Template
100
100
  ## @private
101
101
  def self.included(mod)
102
+ return if mod.respond_to?(:to_expand)
102
103
  mod.extend(ClassMethods)
103
104
  mod.include(Tool::Keys)
104
105
  end
data/lib/toys/tool.rb CHANGED
@@ -269,6 +269,17 @@ module Toys
269
269
  key.is_a?(::Symbol) || key.is_a?(::String) ? @__data[key] : nil
270
270
  end
271
271
 
272
+ ##
273
+ # Activate the given gem. If it is not present, attempt to install it (or
274
+ # inform the user to update the bundle).
275
+ #
276
+ # @param [String] name Name of the gem
277
+ # @param [String...] requirements Version requirements
278
+ #
279
+ def gem(name, *requirements)
280
+ (@__data[Utils::Gems] ||= Utils::Gems.new).activate(name, *requirements)
281
+ end
282
+
272
283
  ##
273
284
  # Exit immediately with the given status code
274
285
  #
@@ -28,6 +28,7 @@
28
28
  ;
29
29
 
30
30
  require "logger"
31
+ require "shellwords"
31
32
 
32
33
  module Toys
33
34
  module Utils
@@ -187,22 +188,32 @@ module Toys
187
188
  # @return [Toys::Utils::Exec::Result] The subprocess result, including
188
189
  # exit code and any captured output.
189
190
  #
190
- def ruby(args, opts = {}, &block)
191
+ def exec_ruby(args, opts = {}, &block)
191
192
  cmd = args.is_a?(::Array) ? [::RbConfig.ruby] + args : "#{::RbConfig.ruby} #{args}"
192
- exec(cmd, {argv0: "ruby"}.merge(opts), &block)
193
+ log_cmd = args.is_a?(::Array) ? ["ruby"] + args : "ruby #{args}"
194
+ exec(cmd, {argv0: "ruby", log_cmd: log_cmd}.merge(opts), &block)
193
195
  end
196
+ alias ruby exec_ruby
194
197
 
195
198
  ##
196
- # Execute the given string in a shell. Returns the exit code.
199
+ # Execute a proc in a fork.
197
200
  #
198
- # @param [String] cmd The shell command to execute.
201
+ # If you provide a block, a {Toys::Utils::Exec::Controller} will be
202
+ # yielded to it, allowing you to interact with the subprocess streams.
203
+ #
204
+ # @param [Proc] func The proc to call.
199
205
  # @param [Hash] opts The command options. See the section on
200
206
  # configuration options in the {Toys::Utils::Exec} module docs.
207
+ # @yieldparam controller [Toys::Utils::Exec::Controller] A controller
208
+ # for the subprocess streams.
201
209
  #
202
- # @return [Integer] The exit code
210
+ # @return [Toys::Utils::Exec::Result] The subprocess result, including
211
+ # exit code and any captured output.
203
212
  #
204
- def sh(cmd, opts = {})
205
- exec(cmd, opts).exit_code
213
+ def exec_proc(func, opts = {}, &block)
214
+ exec_opts = Opts.new(@default_opts).add(opts)
215
+ executor = Executor.new(exec_opts, func)
216
+ executor.execute(&block)
206
217
  end
207
218
 
208
219
  ##
@@ -221,6 +232,49 @@ module Toys
221
232
  exec(cmd, opts.merge(out: :capture)).captured_out
222
233
  end
223
234
 
235
+ ##
236
+ # Spawn a ruby process and pass the given arguments to it.
237
+ #
238
+ # Captures standard out and returns it as a string.
239
+ #
240
+ # @param [String,Array<String>] args The arguments to ruby.
241
+ # @param [Hash] opts The command options. See the section on
242
+ # configuration options in the {Toys::Utils::Exec} module docs.
243
+ #
244
+ # @return [String] What was written to standard out.
245
+ #
246
+ def capture_ruby(args, opts = {})
247
+ ruby(args, opts.merge(out: :capture)).captured_out
248
+ end
249
+
250
+ ##
251
+ # Execute a proc in a fork.
252
+ #
253
+ # Captures standard out and returns it as a string.
254
+ #
255
+ # @param [Proc] func The proc to call.
256
+ # @param [Hash] opts The command options. See the section on
257
+ # configuration options in the {Toys::Utils::Exec} module docs.
258
+ #
259
+ # @return [String] What was written to standard out.
260
+ #
261
+ def capture_proc(func, opts = {})
262
+ exec_proc(func, opts.merge(out: :capture)).captured_out
263
+ end
264
+
265
+ ##
266
+ # Execute the given string in a shell. Returns the exit code.
267
+ #
268
+ # @param [String] cmd The shell command to execute.
269
+ # @param [Hash] opts The command options. See the section on
270
+ # configuration options in the {Toys::Utils::Exec} module docs.
271
+ #
272
+ # @return [Integer] The exit code
273
+ #
274
+ def sh(cmd, opts = {})
275
+ exec(cmd, opts).exit_code
276
+ end
277
+
224
278
  ##
225
279
  # An internal helper class storing the configuration of a subprocess invocation
226
280
  # @private
@@ -232,10 +286,12 @@ module Toys
232
286
  #
233
287
  CONFIG_KEYS = %i[
234
288
  argv0
289
+ cli
235
290
  env
236
291
  err
237
292
  in
238
293
  logger
294
+ log_cmd
239
295
  log_level
240
296
  nonzero_status_handler
241
297
  out
@@ -414,21 +470,25 @@ module Toys
414
470
  #
415
471
  class Executor
416
472
  def initialize(exec_opts, spawn_cmd)
417
- @spawn_cmd = spawn_cmd
473
+ @fork_func = spawn_cmd.respond_to?(:call) ? spawn_cmd : nil
474
+ @spawn_cmd = spawn_cmd.respond_to?(:call) ? nil : spawn_cmd
418
475
  @config_opts = exec_opts.config_opts
419
476
  @spawn_opts = exec_opts.spawn_opts
420
477
  @captures = {}
421
478
  @controller_streams = {}
422
479
  @join_threads = []
423
480
  @child_streams = []
481
+ @parent_streams = []
424
482
  end
425
483
 
426
484
  def execute(&block)
427
485
  setup_in_stream
428
- setup_out_stream(:out)
429
- setup_out_stream(:err)
486
+ setup_out_stream(:out, $stdout, 1)
487
+ setup_out_stream(:err, $stderr, 2)
430
488
  log_command
431
- wait_thread = start_process
489
+ pid = @fork_func ? start_fork : start_process
490
+ @child_streams.each(&:close)
491
+ wait_thread = ::Process.detach(pid)
432
492
  status = control_process(wait_thread, &block)
433
493
  create_result(status)
434
494
  end
@@ -438,8 +498,9 @@ module Toys
438
498
  def log_command
439
499
  logger = @config_opts[:logger]
440
500
  if logger && @config_opts[:log_level] != false
441
- cmd_str = @spawn_cmd.size == 1 ? @spawn_cmd.first : @spawn_cmd.inspect
442
- logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str)
501
+ cmd_str = @config_opts[:log_cmd]
502
+ cmd_str ||= @spawn_cmd.size == 1 ? @spawn_cmd.first : @spawn_cmd.inspect if @spawn_cmd
503
+ logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str) if cmd_str
443
504
  end
444
505
  end
445
506
 
@@ -447,9 +508,107 @@ module Toys
447
508
  args = []
448
509
  args << @config_opts[:env] if @config_opts[:env]
449
510
  args.concat(@spawn_cmd)
450
- pid = ::Process.spawn(*args, @spawn_opts)
451
- @child_streams.each(&:close)
452
- ::Process.detach(pid)
511
+ ::Process.spawn(*args, @spawn_opts)
512
+ end
513
+
514
+ def start_fork
515
+ pid = ::Process.fork
516
+ return pid unless pid.nil?
517
+ exit_code = -1
518
+ begin
519
+ setup_env_within_fork
520
+ setup_streams_within_fork
521
+ exit_code = run_fork_func
522
+ rescue ::SystemExit => e
523
+ exit_code = e.status
524
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
525
+ warn(([e.inspect] + e.backtrace).join("\n"))
526
+ ensure
527
+ ::Kernel.exit!(exit_code)
528
+ end
529
+ end
530
+
531
+ def run_fork_func
532
+ catch(:result) do
533
+ if @spawn_opts[:chdir]
534
+ ::Dir.chdir(@spawn_opts[:chdir]) { @fork_func.call(@config_opts) }
535
+ else
536
+ @fork_func.call(@config_opts)
537
+ end
538
+ 0
539
+ end
540
+ end
541
+
542
+ def setup_env_within_fork
543
+ if @config_opts[:unsetenv_others]
544
+ ::ENV.each_key do |k|
545
+ ::ENV.delete(k) unless @config_opts.key?(k)
546
+ end
547
+ end
548
+ (@config_opts[:env] || {}).each { |k, v| ::ENV[k.to_s] = v.to_s }
549
+ end
550
+
551
+ def setup_streams_within_fork
552
+ @parent_streams.each(&:close)
553
+ setup_in_stream_within_fork(@spawn_opts[:in])
554
+ out_stream = interpret_out_stream_within_fork(@spawn_opts[:out])
555
+ err_stream = interpret_out_stream_within_fork(@spawn_opts[:err])
556
+ if out_stream == :close
557
+ $stdout.close
558
+ elsif out_stream
559
+ $stdout.reopen(out_stream)
560
+ $stdout.sync = true
561
+ end
562
+ if err_stream == :close
563
+ $stderr.close
564
+ elsif err_stream
565
+ $stderr.reopen(err_stream)
566
+ $stderr.sync = true
567
+ end
568
+ end
569
+
570
+ def setup_in_stream_within_fork(stream)
571
+ in_stream =
572
+ case stream
573
+ when ::Integer
574
+ ::IO.open(stream)
575
+ when ::Array
576
+ ::File.open(*stream)
577
+ when ::String
578
+ ::File.open(stream, "r")
579
+ when :close
580
+ :close
581
+ else
582
+ stream if stream.respond_to?(:write)
583
+ end
584
+ if in_stream == :close
585
+ $stdin.close
586
+ elsif in_stream
587
+ $stdin.reopen(in_stream)
588
+ end
589
+ end
590
+
591
+ def interpret_out_stream_within_fork(stream)
592
+ case stream
593
+ when ::Integer
594
+ ::IO.open(stream)
595
+ when ::Array
596
+ if stream.first == :child
597
+ if stream[1] == :err
598
+ $stderr
599
+ elsif stream[1] == :out
600
+ $stdout
601
+ end
602
+ else
603
+ ::File.open(*stream)
604
+ end
605
+ when ::String
606
+ ::File.open(stream, "w")
607
+ when :close
608
+ :close
609
+ else
610
+ stream if stream.respond_to?(:write)
611
+ end
453
612
  end
454
613
 
455
614
  def control_process(wait_thread)
@@ -476,6 +635,10 @@ module Toys
476
635
 
477
636
  def setup_in_stream
478
637
  setting = @config_opts[:in]
638
+ if setting.nil?
639
+ return if $stdin.respond_to?(:fileno) && $stdin.fileno.zero?
640
+ setting = $stdin
641
+ end
479
642
  return unless setting
480
643
  case setting
481
644
  when ::Symbol
@@ -540,9 +703,12 @@ module Toys
540
703
  @spawn_opts[:in] = args + [::File::RDONLY]
541
704
  end
542
705
 
543
- def setup_out_stream(key)
706
+ def setup_out_stream(key, stdstream, stdfileno)
544
707
  setting = @config_opts[key]
545
- return unless setting
708
+ if setting.nil?
709
+ return if setting.respond_to?(:fileno) && setting.fileno == stdfileno
710
+ setting = stdstream
711
+ end
546
712
  case setting
547
713
  when ::Symbol
548
714
  setup_out_stream_of_type(key, setting, [])
@@ -617,6 +783,7 @@ module Toys
617
783
  r, w = ::IO.pipe
618
784
  @spawn_opts[:in] = r
619
785
  @child_streams << r
786
+ @parent_streams << w
620
787
  w.sync = true
621
788
  w
622
789
  end
@@ -625,6 +792,7 @@ module Toys
625
792
  r, w = ::IO.pipe
626
793
  @spawn_opts[key] = w
627
794
  @child_streams << w
795
+ @parent_streams << r
628
796
  r
629
797
  end
630
798
 
@@ -74,7 +74,8 @@ module Toys
74
74
  end
75
75
 
76
76
  ##
77
- # Activate the given gem.
77
+ # Activate the given gem. If it is not present, attempt to install it (or
78
+ # inform the user to update the bundle).
78
79
  #
79
80
  # @param [String] name Name of the gem
80
81
  # @param [String...] requirements Version requirements
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toys-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-11 00:00:00.000000000 Z
11
+ date: 2018-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest