toys-core 0.9.4 → 0.10.0

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -1
  3. data/CHANGELOG.md +30 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +3 -3
  6. data/lib/toys-core.rb +11 -21
  7. data/lib/toys/acceptor.rb +0 -21
  8. data/lib/toys/arg_parser.rb +1 -22
  9. data/lib/toys/cli.rb +102 -70
  10. data/lib/toys/compat.rb +49 -41
  11. data/lib/toys/completion.rb +0 -21
  12. data/lib/toys/context.rb +0 -23
  13. data/lib/toys/core.rb +1 -22
  14. data/lib/toys/dsl/flag.rb +0 -21
  15. data/lib/toys/dsl/flag_group.rb +0 -21
  16. data/lib/toys/dsl/positional_arg.rb +0 -21
  17. data/lib/toys/dsl/tool.rb +135 -51
  18. data/lib/toys/errors.rb +0 -21
  19. data/lib/toys/flag.rb +0 -21
  20. data/lib/toys/flag_group.rb +0 -21
  21. data/lib/toys/input_file.rb +0 -21
  22. data/lib/toys/loader.rb +41 -78
  23. data/lib/toys/middleware.rb +146 -77
  24. data/lib/toys/mixin.rb +0 -21
  25. data/lib/toys/module_lookup.rb +3 -26
  26. data/lib/toys/positional_arg.rb +0 -21
  27. data/lib/toys/source_info.rb +49 -38
  28. data/lib/toys/standard_middleware/add_verbosity_flags.rb +0 -23
  29. data/lib/toys/standard_middleware/apply_config.rb +42 -0
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +7 -28
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +0 -23
  32. data/lib/toys/standard_middleware/show_help.rb +0 -23
  33. data/lib/toys/standard_middleware/show_root_version.rb +0 -23
  34. data/lib/toys/standard_mixins/bundler.rb +89 -0
  35. data/lib/toys/standard_mixins/exec.rb +124 -35
  36. data/lib/toys/standard_mixins/fileutils.rb +0 -21
  37. data/lib/toys/standard_mixins/gems.rb +2 -24
  38. data/lib/toys/standard_mixins/highline.rb +0 -21
  39. data/lib/toys/standard_mixins/terminal.rb +0 -21
  40. data/lib/toys/template.rb +0 -21
  41. data/lib/toys/tool.rb +22 -34
  42. data/lib/toys/utils/completion_engine.rb +0 -21
  43. data/lib/toys/utils/exec.rb +1 -21
  44. data/lib/toys/utils/gems.rb +174 -63
  45. data/lib/toys/utils/help_text.rb +0 -21
  46. data/lib/toys/utils/terminal.rb +46 -37
  47. data/lib/toys/wrappable_string.rb +0 -21
  48. metadata +25 -9
@@ -1,25 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
3
+ require "rbconfig"
23
4
 
24
5
  module Toys
25
6
  ##
@@ -27,44 +8,51 @@ module Toys
27
8
  # @private
28
9
  #
29
10
  module Compat
30
- ## @private
31
- CURRENT_VERSION = ::Gem::Version.new(::RUBY_VERSION)
11
+ parts = ::RUBY_VERSION.split(".")
12
+ ruby_version = parts[0].to_i * 10000 + parts[1].to_i * 100 + parts[2].to_i
32
13
 
33
- ## @private
14
+ # @private
34
15
  def self.jruby?
35
16
  ::RUBY_PLATFORM == "java"
36
17
  end
37
18
 
38
- ## @private
19
+ # @private
39
20
  def self.allow_fork?
40
- !jruby? && RbConfig::CONFIG["host_os"] !~ /mswin/
21
+ !jruby? && ::RbConfig::CONFIG["host_os"] !~ /mswin/
41
22
  end
42
23
 
43
- ## @private
44
- def self.check_minimum_version(version)
45
- CURRENT_VERSION >= ::Gem::Version.new(version)
24
+ # @private
25
+ def self.supports_suggestions?
26
+ unless defined?(@supports_suggestions)
27
+ require "rubygems"
28
+ begin
29
+ require "did_you_mean"
30
+ @supports_suggestions = defined?(::DidYouMean::SpellChecker)
31
+ rescue ::LoadError
32
+ @supports_suggestions = false
33
+ end
34
+ end
35
+ @supports_suggestions
46
36
  end
47
37
 
48
- if check_minimum_version("2.4.0")
49
- ## @private
50
- def self.suggestions(word, list)
38
+ # @private
39
+ def self.suggestions(word, list)
40
+ if supports_suggestions?
51
41
  ::DidYouMean::SpellChecker.new(dictionary: list).correct(word)
52
- end
53
- else
54
- ## @private
55
- def self.suggestions(_word, _list)
42
+ else
56
43
  []
57
44
  end
58
45
  end
59
46
 
60
- if check_minimum_version("2.4.0")
61
- ## @private
47
+ # In Ruby < 2.4, some objects such as nil cannot be cloned.
48
+ if ruby_version >= 20400
49
+ # @private
62
50
  def self.merge_clones(hash, orig)
63
51
  orig.each { |k, v| hash[k] = v.clone }
64
52
  hash
65
53
  end
66
54
  else
67
- ## @private
55
+ # @private
68
56
  def self.merge_clones(hash, orig)
69
57
  orig.each do |k, v|
70
58
  hash[k] =
@@ -78,16 +66,36 @@ module Toys
78
66
  end
79
67
  end
80
68
 
81
- if check_minimum_version("2.5.0")
82
- ## @private
69
+ # The :base argument to Dir.glob requires Ruby 2.5 or later.
70
+ if ruby_version >= 20500
71
+ # @private
83
72
  def self.glob_in_dir(glob, dir)
84
73
  ::Dir.glob(glob, base: dir)
85
74
  end
86
75
  else
87
- ## @private
76
+ # @private
88
77
  def self.glob_in_dir(glob, dir)
89
78
  ::Dir.chdir(dir) { ::Dir.glob(glob) }
90
79
  end
91
80
  end
81
+
82
+ # Due to a bug in Ruby < 2.7, passing an empty **kwargs splat to
83
+ # initialize will fail if there are no formal keyword args.
84
+ if ruby_version >= 20700
85
+ # @private
86
+ def self.instantiate(klass, args, kwargs, block)
87
+ klass.new(*args, **kwargs, &block)
88
+ end
89
+ else
90
+ # @private
91
+ def self.instantiate(klass, args, kwargs, block)
92
+ formals = klass.instance_method(:initialize).parameters
93
+ if kwargs.empty? && formals.all? { |arg| arg.first != :key && arg.first != :keyrest }
94
+ klass.new(*args, &block)
95
+ else
96
+ klass.new(*args, **kwargs, &block)
97
+ end
98
+ end
99
+ end
92
100
  end
93
101
  end
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  ##
26
5
  # A Completion is a callable Proc that determines candidates for shell tab
@@ -1,28 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
- require "logger"
25
-
26
3
  module Toys
27
4
  ##
28
5
  # This is the base class for tool execution. It represents `self` when your
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  ##
26
5
  # The core Toys classes.
@@ -30,7 +9,7 @@ module Toys
30
9
  # Current version of Toys core.
31
10
  # @return [String]
32
11
  #
33
- VERSION = "0.9.4"
12
+ VERSION = "0.10.0"
34
13
  end
35
14
 
36
15
  ## @private deprecated
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  module DSL
26
5
  ##
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  module DSL
26
5
  ##
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  module DSL
26
5
  ##
@@ -1,26 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2019 Daniel Azuma
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
- # IN THE SOFTWARE.
22
- ;
23
-
24
3
  module Toys
25
4
  module DSL
26
5
  ##
@@ -226,7 +205,8 @@ module Toys
226
205
  #
227
206
  def template(name, template_class = nil, &block)
228
207
  cur_tool = DSL::Tool.current_tool(self, false)
229
- cur_tool&.add_template(name, template_class, &block)
208
+ return self if cur_tool.nil?
209
+ cur_tool.add_template(name, template_class, &block)
230
210
  self
231
211
  end
232
212
 
@@ -273,7 +253,8 @@ module Toys
273
253
  #
274
254
  def completion(name, spec = nil, **options, &block)
275
255
  cur_tool = DSL::Tool.current_tool(self, false)
276
- cur_tool&.add_completion(name, spec, **options, &block)
256
+ return self if cur_tool.nil?
257
+ cur_tool.add_completion(name, spec, **options, &block)
277
258
  self
278
259
  end
279
260
 
@@ -322,11 +303,11 @@ module Toys
322
303
  # @return [self]
323
304
  #
324
305
  def tool(words, if_defined: :combine, delegate_to: nil, &block)
325
- subtool_words = @__words
306
+ subtool_words = @__words.dup
326
307
  next_remaining = @__remaining_words
327
- Array(words).each do |word|
308
+ @__loader.split_path(words).each do |word|
328
309
  word = word.to_s
329
- subtool_words += [word]
310
+ subtool_words << word
330
311
  next_remaining = Loader.next_remaining_words(next_remaining, word)
331
312
  end
332
313
  subtool = @__loader.get_tool(subtool_words, @__priority)
@@ -338,10 +319,12 @@ module Toys
338
319
  subtool.reset_definition(@__loader)
339
320
  end
340
321
  end
341
- subtool_class = subtool.tool_class
342
- DSL::Tool.prepare(subtool_class, next_remaining, source_info) do
343
- subtool_class.delegate_to(delegate_to) if delegate_to
344
- subtool_class.class_eval(&block) if block
322
+ if delegate_to
323
+ delegator = proc { self.delegate_to(delegate_to) }
324
+ @__loader.load_block(source_info, delegator, subtool_words, next_remaining, @__priority)
325
+ end
326
+ if block
327
+ @__loader.load_block(source_info, block, subtool_words, next_remaining, @__priority)
345
328
  end
346
329
  self
347
330
  end
@@ -455,6 +438,7 @@ module Toys
455
438
  #
456
439
  def expand(template_class, *args, **kwargs)
457
440
  cur_tool = DSL::Tool.current_tool(self, false)
441
+ return self if cur_tool.nil?
458
442
  name = template_class.to_s
459
443
  if template_class.is_a?(::String)
460
444
  template_class = cur_tool.lookup_template(template_class)
@@ -464,15 +448,7 @@ module Toys
464
448
  if template_class.nil?
465
449
  raise ToolDefinitionError, "Template not found: #{name.inspect}"
466
450
  end
467
- # Due to a bug in Ruby < 2.7, passing an empty **kwargs splat to
468
- # initialize will fail if there are no formal keyword args.
469
- formals = template_class.instance_method(:initialize).parameters
470
- template =
471
- if kwargs.empty? && formals.all? { |(type, _name)| type != :key && type != :keyrest }
472
- template_class.new(*args)
473
- else
474
- template_class.new(*args, **kwargs)
475
- end
451
+ template = Compat.instantiate(template_class, args, kwargs, nil)
476
452
  yield template if block_given?
477
453
  class_exec(template, &template_class.expansion)
478
454
  self
@@ -513,7 +489,8 @@ module Toys
513
489
  #
514
490
  def desc(str)
515
491
  cur_tool = DSL::Tool.current_tool(self, true)
516
- cur_tool.desc = str if cur_tool
492
+ return self if cur_tool.nil?
493
+ cur_tool.desc = str
517
494
  self
518
495
  end
519
496
  alias short_desc desc
@@ -539,10 +516,27 @@ module Toys
539
516
  # long_desc "This line is appended to the description."
540
517
  #
541
518
  # @param strs [Toys::WrappableString,String,Array<String>...]
519
+ # @param file [String] Optional. Read the description from the given file
520
+ # provided relative to the current toys file. The file must be a
521
+ # plain text file whose suffix is `.txt`.
522
+ # @param data [String] Optional. Read the description from the given data
523
+ # file. The file must be a plain text file whose suffix is `.txt`.
542
524
  # @return [self]
543
525
  #
544
- def long_desc(*strs)
545
- DSL::Tool.current_tool(self, true)&.append_long_desc(strs)
526
+ def long_desc(*strs, file: nil, data: nil)
527
+ cur_tool = DSL::Tool.current_tool(self, true)
528
+ return self if cur_tool.nil?
529
+ if file
530
+ unless source_info.source_path
531
+ raise ::Toys::ToolDefinitionError,
532
+ "Cannot set long_desc from a file because the tool is not defined in a file"
533
+ end
534
+ file = ::File.join(::File.dirname(source_info.source_path), file)
535
+ elsif data
536
+ file = source_info.find_data(data, type: :file)
537
+ end
538
+ strs += DSL::Tool.load_long_desc_file(file) if file
539
+ cur_tool.append_long_desc(strs)
546
540
  self
547
541
  end
548
542
 
@@ -1405,7 +1399,8 @@ module Toys
1405
1399
  #
1406
1400
  def on_interrupt(handler = nil, &block)
1407
1401
  cur_tool = DSL::Tool.current_tool(self, true)
1408
- cur_tool.interrupt_handler = handler || block unless cur_tool.nil?
1402
+ return self if cur_tool.nil?
1403
+ cur_tool.interrupt_handler = handler || block
1409
1404
  self
1410
1405
  end
1411
1406
 
@@ -1436,7 +1431,8 @@ module Toys
1436
1431
  #
1437
1432
  def on_usage_error(handler = nil, &block)
1438
1433
  cur_tool = DSL::Tool.current_tool(self, true)
1439
- cur_tool.usage_error_handler = handler || block unless cur_tool.nil?
1434
+ return self if cur_tool.nil?
1435
+ cur_tool.usage_error_handler = handler || block
1440
1436
  self
1441
1437
  end
1442
1438
 
@@ -1578,11 +1574,84 @@ module Toys
1578
1574
  #
1579
1575
  def set_context_directory(dir) # rubocop:disable Naming/AccessorMethodName
1580
1576
  cur_tool = DSL::Tool.current_tool(self, false)
1581
- return if cur_tool.nil?
1577
+ return self if cur_tool.nil?
1582
1578
  cur_tool.custom_context_directory = dir
1583
1579
  self
1584
1580
  end
1585
1581
 
1582
+ ##
1583
+ # Applies the given block to all subtools, recursively. Effectively, the
1584
+ # given block is run at the *end* of every tool block. This can be used,
1585
+ # for example, to provide some shared configuration for all tools.
1586
+ #
1587
+ # The block is applied only to subtools defined *after* the block
1588
+ # appears. Subtools defined before the block appears are not affected.
1589
+ #
1590
+ # ## Example
1591
+ #
1592
+ # It is common for tools to use the `:exec` mixin to invoke external
1593
+ # programs. This example automatically includes the exec mixin in all
1594
+ # subtools, recursively, so you do not have to repeat the `include`
1595
+ # directive in every tool.
1596
+ #
1597
+ # # .toys.rb
1598
+ #
1599
+ # subtool_apply do
1600
+ # # Include the mixin only if the tool hasn't already done so
1601
+ # unless include?(:exec)
1602
+ # include :exec, exit_on_nonzero_status: true
1603
+ # end
1604
+ # end
1605
+ #
1606
+ # tool "foo" do
1607
+ # def run
1608
+ # # This tool has access to methods defined by the :exec mixin
1609
+ # # because the above block is applied to the tool.
1610
+ # sh "echo hello"
1611
+ # end
1612
+ # end
1613
+ #
1614
+ def subtool_apply(&block)
1615
+ cur_tool = DSL::Tool.current_tool(self, false)
1616
+ return self if cur_tool.nil?
1617
+ cur_tool.subtool_middleware_stack.add(:apply_config,
1618
+ parent_source: source_info, &block)
1619
+ self
1620
+ end
1621
+
1622
+ ##
1623
+ # Determines whether the current Toys version satisfies the given
1624
+ # requirements.
1625
+ #
1626
+ # @return [Boolean] whether or not the requirements are satisfied
1627
+ #
1628
+ def toys_version?(*requirements)
1629
+ require "rubygems"
1630
+ version = ::Gem::Version.new(Core::VERSION)
1631
+ requirement = ::Gem::Requirement.new(*requirements)
1632
+ requirement.satisfied_by?(version)
1633
+ end
1634
+
1635
+ ##
1636
+ # Asserts that the current Toys version against the given requirements,
1637
+ # raising an exception if not.
1638
+ #
1639
+ # @return [self]
1640
+ #
1641
+ # @raise [Toys::ToolDefinitionError] if the current Toys version does not
1642
+ # satisfy the requirements.
1643
+ #
1644
+ def toys_version!(*requirements)
1645
+ require "rubygems"
1646
+ version = ::Gem::Version.new(Core::VERSION)
1647
+ requirement = ::Gem::Requirement.new(*requirements)
1648
+ unless requirement.satisfied_by?(version)
1649
+ raise Toys::ToolDefinitionError,
1650
+ "Toys version requirements #{requirement} not satisfied by {version}"
1651
+ end
1652
+ self
1653
+ end
1654
+
1586
1655
  ## @private
1587
1656
  def self.new_class(words, priority, loader)
1588
1657
  tool_class = ::Class.new(::Toys::Context)
@@ -1599,7 +1668,7 @@ module Toys
1599
1668
  def self.current_tool(tool_class, activate)
1600
1669
  memoize_var = activate ? :@__active_tool : :@__cur_tool
1601
1670
  if tool_class.instance_variable_defined?(memoize_var)
1602
- cur_tool = tool_class.instance_variable_get(memoize_var)
1671
+ tool_class.instance_variable_get(memoize_var)
1603
1672
  else
1604
1673
  loader = tool_class.instance_variable_get(:@__loader)
1605
1674
  words = tool_class.instance_variable_get(:@__words)
@@ -1610,13 +1679,12 @@ module Toys
1610
1679
  else
1611
1680
  loader.get_tool(words, priority)
1612
1681
  end
1682
+ if cur_tool && activate
1683
+ source = tool_class.instance_variable_get(:@__source).last
1684
+ cur_tool.lock_source(source)
1685
+ end
1613
1686
  tool_class.instance_variable_set(memoize_var, cur_tool)
1614
1687
  end
1615
- if cur_tool && activate
1616
- source = tool_class.instance_variable_get(:@__source).last
1617
- cur_tool.lock_source(source)
1618
- end
1619
- cur_tool
1620
1688
  end
1621
1689
 
1622
1690
  ## @private
@@ -1654,6 +1722,22 @@ module Toys
1654
1722
  end
1655
1723
  mod
1656
1724
  end
1725
+
1726
+ ## @private
1727
+ def self.load_long_desc_file(path)
1728
+ if ::File.extname(path) == ".txt"
1729
+ begin
1730
+ ::File.readlines(path).map do |line|
1731
+ line = line.chomp
1732
+ line =~ /^\s/ ? [line] : line
1733
+ end
1734
+ rescue ::SystemCallError => e
1735
+ raise Toys::ToolDefinitionError, e.to_s
1736
+ end
1737
+ else
1738
+ raise Toys::ToolDefinitionError, "Cannot load long desc from file type: #{path}"
1739
+ end
1740
+ end
1657
1741
  end
1658
1742
  end
1659
1743
  end