toys-core 0.3.8 → 0.3.9

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 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