toys-core 0.9.4 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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