toys-core 0.4.5 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e238ba243a2e72d0c04df73414040baf052dae6179f6f81030e5bf96b1bc06a
4
- data.tar.gz: 4b711336fca9d960390e523741ddf800674e7657da1677a884679e28fbe9c6f1
3
+ metadata.gz: bd284ba44663511c6004cf0d742c4e1b5a703cbe596fd6612350be3b316ec60f
4
+ data.tar.gz: 9fdc655c88053d669727399c4c4c7321d4d2ececd579c9420bcddb1f0a40d65d
5
5
  SHA512:
6
- metadata.gz: 470b8f9c78917c60db54df07c54f43a14b7626cf31f22ab7810756103af41482f34a66a92b09d82587e5ac09f3774f7446275a0f2f131fab5f4a7a0eec6903dc
7
- data.tar.gz: 0eb481163a3476ad7fd94146f09ee0b8acc4a02b1a0aadd95d559100756e6793e913091588e35523073e41152b464a5dbb48aba37c86613a34a483afbb50cbe3
6
+ metadata.gz: ccdfadc2660d171d39776f63e708839edfaef4b63e21a2ddc39feb6456763a0d54fa27cc611cd2c9180210c291fb1f2510fa925b9f9ebea98f7e3794d42ca00e
7
+ data.tar.gz: e8d68670c340ac2a2b51a83a1201334ccbbda1a69464a25b9a376badfaf891895ffcc189ad54afbd1a2901b4673ded5fcb61296065ed205e98bfd05dbf4dc810
data/.yardopts CHANGED
@@ -1,6 +1,7 @@
1
1
  --no-private
2
2
  --title=Toys Core
3
3
  --markup=markdown
4
+ --markup-provider redcarpet
4
5
  --main=README.md
5
6
  ./lib/**/*.rb
6
7
  -
@@ -1,5 +1,15 @@
1
1
  # Release History
2
2
 
3
+ ### 0.5.0 / 2018-10-07
4
+
5
+ * FIXED: Template instantiation was failing if the hosting tool was priority-masked.
6
+ * ADDED: Several additional characters can optionally be used as tool path delimiters.
7
+ * ADDED: Support for preloaded files and directories
8
+ * ADDED: Support for data directories
9
+ * ADDED: Ability to display just the list of subtools of a tool
10
+ * IMPROVED: The tool directive can now take an array as the tool name.
11
+ * IMPROVED: The tool directive can now take an `if_defined` argument.
12
+
3
13
  ### 0.4.5 / 2018-08-05
4
14
 
5
15
  * CHANGED: Dropped preload file feature
@@ -68,6 +68,7 @@ require "toys/core_version"
68
68
  require "toys/definition/acceptor"
69
69
  require "toys/definition/alias"
70
70
  require "toys/definition/arg"
71
+ require "toys/definition/data_finder"
71
72
  require "toys/definition/flag"
72
73
  require "toys/definition/tool"
73
74
  require "toys/dsl/arg"
@@ -58,9 +58,25 @@ module Toys
58
58
  # loaded first as a standalone configuration file. If not provided,
59
59
  # standalone configuration files are disabled.
60
60
  # The default toys CLI sets this to `".toys.rb"`.
61
+ # @param [String,nil] preload_file_name A file with this name that appears
62
+ # in any configuration directory is preloaded before any tools in that
63
+ # configuration directory are defined.
64
+ # The default toys CLI sets this to `".preload.rb"`.
65
+ # @param [String,nil] preload_directory_name A directory with this name
66
+ # that appears in any configuration directory is searched for Ruby
67
+ # files, which are preloaded before any tools in that configuration
68
+ # directory are defined.
69
+ # The default toys CLI sets this to `".preload"`.
70
+ # @param [String,nil] data_directory_name A directory with this name that
71
+ # appears in any configuration directory is added to the data directory
72
+ # search path for any tool file in that directory.
73
+ # The default toys CLI sets this to `".data"`.
61
74
  # @param [Array] middleware_stack An array of middleware that will be used
62
75
  # by default for all tools loaded by this CLI. If not provided, uses
63
76
  # {Toys::CLI.default_middleware_stack}.
77
+ # @param [String] extra_delimiters A string containing characters that can
78
+ # function as delimiters in a tool name. Defaults to empty. Allowed
79
+ # characters are period, colon, and slash.
64
80
  # @param [Toys::Utils::ModuleLookup] mixin_lookup A lookup for well-known
65
81
  # mixin modules. If not provided, uses
66
82
  # {Toys::CLI.default_mixin_lookup}.
@@ -81,23 +97,30 @@ module Toys
81
97
  # Default is a {Toys::CLI::DefaultErrorHandler} writing to the logger.
82
98
  #
83
99
  def initialize(
84
- binary_name: nil, middleware_stack: nil,
100
+ binary_name: nil, middleware_stack: nil, extra_delimiters: "",
85
101
  config_dir_name: nil, config_file_name: nil, index_file_name: nil,
102
+ preload_file_name: nil, preload_directory_name: nil, data_directory_name: nil,
86
103
  mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil,
87
104
  logger: nil, base_level: nil, error_handler: nil
88
105
  )
89
106
  @logger = logger || self.class.default_logger
90
107
  @base_level = base_level || @logger.level
91
108
  @middleware_stack = middleware_stack || self.class.default_middleware_stack
109
+ @extra_delimiters = extra_delimiters
92
110
  @binary_name = binary_name || ::File.basename($PROGRAM_NAME)
93
111
  @config_dir_name = config_dir_name
94
112
  @config_file_name = config_file_name
95
113
  @index_file_name = index_file_name
114
+ @preload_file_name = preload_file_name
115
+ @preload_directory_name = preload_directory_name
116
+ @data_directory_name = data_directory_name
96
117
  @mixin_lookup = mixin_lookup || self.class.default_mixin_lookup
97
118
  @middleware_lookup = middleware_lookup || self.class.default_middleware_lookup
98
119
  @template_lookup = template_lookup || self.class.default_template_lookup
99
120
  @loader = Loader.new(
100
- index_file_name: index_file_name,
121
+ index_file_name: index_file_name, extra_delimiters: @extra_delimiters,
122
+ preload_directory_name: @preload_directory_name, preload_file_name: @preload_file_name,
123
+ data_directory_name: @data_directory_name,
101
124
  mixin_lookup: @mixin_lookup, template_lookup: @template_lookup,
102
125
  middleware_lookup: @middleware_lookup, middleware_stack: @middleware_stack
103
126
  )
@@ -251,7 +274,11 @@ module Toys
251
274
  config_dir_name: @config_dir_name,
252
275
  config_file_name: @config_file_name,
253
276
  index_file_name: @index_file_name,
277
+ preload_directory_name: @preload_directory_name,
278
+ preload_file_name: @preload_file_name,
279
+ data_directory_name: @data_directory_name,
254
280
  middleware_stack: @middleware_stack,
281
+ extra_delimiters: @extra_delimiters,
255
282
  mixin_lookup: @mixin_lookup,
256
283
  middleware_lookup: @middleware_lookup,
257
284
  template_lookup: @template_lookup,
@@ -34,5 +34,5 @@ module Toys
34
34
  # Current version of Toys core
35
35
  # @return [String]
36
36
  #
37
- CORE_VERSION = "0.4.5"
37
+ CORE_VERSION = "0.5.0"
38
38
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2018 Daniel Azuma
4
+ #
5
+ # All rights reserved.
6
+ #
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright notice,
11
+ # this list of conditions and the following disclaimer.
12
+ # * Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ # * Neither the name of the copyright holder, nor the names of any other
16
+ # contributors to this software, may be used to endorse or promote products
17
+ # derived from this software without specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
+ # POSSIBILITY OF SUCH DAMAGE.
30
+ ;
31
+
32
+ module Toys
33
+ module Definition
34
+ ##
35
+ # Finds data files.
36
+ #
37
+ class DataFinder
38
+ ##
39
+ # Create a new finder.
40
+ #
41
+ # @param [String,nil] data_name Name of the data directory, or nil if
42
+ # data directories are disabled.
43
+ # @param [Toys::Definition::DataFinder,nil] parent The parent, or nil if
44
+ # this is the root.
45
+ # @param [String,nil] directory The data directory, or nil for none.
46
+ # @private
47
+ #
48
+ def initialize(data_name, parent, directory)
49
+ @data_name = data_name
50
+ @parent = parent
51
+ @directory = directory
52
+ end
53
+
54
+ ##
55
+ # Create a new finder for the given directory.
56
+ #
57
+ # @param [String] directory Toys directory path
58
+ # @return [Toys::Definition::DataFinder] The finder
59
+ #
60
+ def finder_for(directory)
61
+ return self if @data_name.nil?
62
+ directory = ::File.join(directory, @data_name)
63
+ return self unless ::File.directory?(directory)
64
+ DataFinder.new(@data_name, self, directory)
65
+ end
66
+
67
+ ##
68
+ # Return the absolute path to the given data file or directory.
69
+ #
70
+ # @param [String] path The relative path to find
71
+ # @param [nil,:file,:directory] type Type of file system object to find,
72
+ # or nil to return any type.
73
+ # @return [String,nil] Absolute path of the result, or nil if not found.
74
+ #
75
+ def find_data(path, type: nil)
76
+ return nil if @directory.nil?
77
+ full_path = ::File.join(@directory, path)
78
+ case type
79
+ when :file
80
+ return full_path if ::File.file?(full_path)
81
+ when :directory
82
+ return full_path if ::File.directory?(full_path)
83
+ else
84
+ return full_path if ::File.readable?(full_path)
85
+ end
86
+ @parent.find_data(path, type: type)
87
+ end
88
+
89
+ ##
90
+ # Create an empty finder.
91
+ #
92
+ # @param [String,nil] data_name Name of the data directory, or nil if
93
+ # data directories are disabled.
94
+ # @return [Toys::Definition::DataFinder]
95
+ #
96
+ def self.create_empty(data_name)
97
+ new(data_name, nil, nil)
98
+ end
99
+
100
+ ##
101
+ # A default empty finder.
102
+ #
103
+ # @return [Toys::Definition::DataFinder]
104
+ #
105
+ EMPTY = create_empty(nil)
106
+ end
107
+ end
108
+ end
@@ -72,11 +72,27 @@ module Toys
72
72
  def initialize(loader, parent, full_name, priority, middleware_stack)
73
73
  @parent = parent
74
74
  @full_name = full_name.dup.freeze
75
- @tool_class = DSL::Tool.new_class(@full_name, priority, loader)
76
75
  @priority = priority
77
76
  @middleware_stack = middleware_stack
78
77
 
78
+ @acceptors = {}
79
+ @mixins = {}
80
+ @templates = {}
81
+
82
+ reset_definition(loader)
83
+ end
84
+
85
+ ##
86
+ # Reset the definition of this tool, deleting all definition data but
87
+ # leaving named acceptors, mixins, and templates intact.
88
+ # Should be called only from the DSL.
89
+ # @private
90
+ #
91
+ def reset_definition(loader)
92
+ @tool_class = DSL::Tool.new_class(@full_name, @priority, loader)
93
+
79
94
  @source_path = nil
95
+ @data_finder = nil
80
96
  @definition_finished = false
81
97
 
82
98
  @desc = Utils::WrappableString.new("")
@@ -86,17 +102,13 @@ module Toys
86
102
  @used_flags = []
87
103
  @initializers = []
88
104
 
89
- @acceptors = {}
90
- @mixins = {}
91
- @templates = {}
92
-
93
105
  @flag_definitions = []
94
106
  @required_arg_definitions = []
95
107
  @optional_arg_definitions = []
96
108
  @remaining_args_definition = nil
97
109
 
98
110
  @disable_argument_parsing = false
99
- @runnable = false
111
+ @includes_modules = false
100
112
  end
101
113
 
102
114
  ##
@@ -209,7 +221,15 @@ module Toys
209
221
  # @return [Boolean]
210
222
  #
211
223
  def runnable?
212
- @runnable
224
+ tool_class.public_instance_methods(false).include?(:run)
225
+ end
226
+
227
+ ##
228
+ # Returns true if this tool has at least one included module.
229
+ # @return [Boolean]
230
+ #
231
+ def includes_modules?
232
+ @includes_modules
213
233
  end
214
234
 
215
235
  ##
@@ -236,7 +256,8 @@ module Toys
236
256
  # @return [Boolean]
237
257
  #
238
258
  def includes_definition?
239
- includes_arguments? || runnable?
259
+ includes_arguments? || runnable? || argument_parsing_disabled? ||
260
+ includes_modules? || includes_description?
240
261
  end
241
262
 
242
263
  ##
@@ -346,14 +367,16 @@ module Toys
346
367
  # already set, raises {Toys::ToolDefinitionError}
347
368
  #
348
369
  # @param [String] path The path to the file defining this tool
370
+ # @param [Toys::Definition::DataFinder] data_finder Data finder
349
371
  #
350
- def lock_source_path(path)
372
+ def lock_source_path(path, data_finder)
351
373
  if source_path && source_path != path
352
374
  raise ToolDefinitionError,
353
375
  "Cannot redefine tool #{display_name.inspect} in #{path}" \
354
376
  " (already defined in #{source_path})"
355
377
  end
356
378
  @source_path = path
379
+ @data_finder = data_finder
357
380
  end
358
381
 
359
382
  ##
@@ -648,12 +671,24 @@ module Toys
648
671
  end
649
672
 
650
673
  ##
651
- # Mark this tool as runnable. Should be called from the DSL only.
674
+ # Find the given data file or directory in this tool's search path.
675
+ #
676
+ # @param [String] path The path to find
677
+ # @param [nil,:file,:directory] type Type of file system object to find,
678
+ # or nil to return any type.
679
+ # @return [String,nil] Absolute path of the result, or nil if not found.
680
+ #
681
+ def find_data(path, type: nil)
682
+ @data_finder ? @data_finder.find_data(path, type: type) : nil
683
+ end
684
+
685
+ ##
686
+ # Mark this tool as having at least one module included
652
687
  # @private
653
688
  #
654
- def mark_runnable
689
+ def mark_includes_modules
655
690
  check_definition_state
656
- @runnable = true
691
+ @includes_modules = true
657
692
  self
658
693
  end
659
694
 
@@ -687,12 +722,11 @@ module Toys
687
722
  end
688
723
  end
689
724
 
690
- private
691
-
692
- def make_config_proc(middleware, loader, next_config)
693
- proc { middleware.config(self, loader, &next_config) }
694
- end
695
-
725
+ ##
726
+ # Check that the tool can still be defined. Should be called internally
727
+ # or from the DSL only.
728
+ # @private
729
+ #
696
730
  def check_definition_state(is_arg: false)
697
731
  if @definition_finished
698
732
  raise ToolDefinitionError,
@@ -704,6 +738,12 @@ module Toys
704
738
  end
705
739
  self
706
740
  end
741
+
742
+ private
743
+
744
+ def make_config_proc(middleware, loader, next_config)
745
+ proc { middleware.config(self, loader, &next_config) }
746
+ end
707
747
  end
708
748
  end
709
749
  end
@@ -64,9 +64,8 @@ module Toys
64
64
  #
65
65
  module Tool
66
66
  ## @private
67
- def method_added(meth)
68
- cur_tool = DSL::Tool.current_tool(self, true)
69
- cur_tool.mark_runnable if cur_tool && meth == :run
67
+ def method_added(_meth)
68
+ DSL::Tool.current_tool(self, true)&.check_definition_state
70
69
  end
71
70
 
72
71
  ##
@@ -177,18 +176,34 @@ module Toys
177
176
  ##
178
177
  # Create a subtool. You must provide a block defining the subtool.
179
178
  #
180
- # If the subtool is already defined (either as a tool or a namespace), the
181
- # old definition is discarded and replaced with the new definition.
182
- #
183
- # @param [String] word The name of the subtool
179
+ # @param [String,Array<String>] words The name of the subtool
180
+ # @param [:combine,:reset,:ignore] if_defined What to do if a definition
181
+ # already exists for this tool. Possible values are `:combine` (the
182
+ # default) indicating the definition should be combined with the
183
+ # existing definition, `:reset` indicating the earlier definition
184
+ # should be reset and the new definition applied instead, or
185
+ # `:ignore` indicating the new definition should be ignored.
184
186
  # @return [Toys::DSL::Tool] self, for chaining.
185
187
  #
186
- def tool(word, &block)
187
- word = word.to_s
188
- subtool_words = @__words + [word]
189
- next_remaining = Loader.next_remaining_words(@__remaining_words, word)
190
- subtool_class = @__loader.get_tool_definition(subtool_words, @__priority).tool_class
191
- DSL::Tool.prepare(subtool_class, next_remaining, @__path) do
188
+ def tool(words, if_defined: :combine, &block)
189
+ subtool_words = @__words
190
+ next_remaining = @__remaining_words
191
+ Array(words).each do |word|
192
+ word = word.to_s
193
+ subtool_words += [word]
194
+ next_remaining = Loader.next_remaining_words(next_remaining, word)
195
+ end
196
+ subtool = @__loader.get_tool_definition(subtool_words, @__priority)
197
+ if subtool.includes_definition?
198
+ case if_defined
199
+ when :ignore
200
+ return self
201
+ when :reset
202
+ subtool.reset_definition(@__loader)
203
+ end
204
+ end
205
+ subtool_class = subtool.tool_class
206
+ DSL::Tool.prepare(subtool_class, next_remaining, @__path, @__data_finder) do
192
207
  subtool_class.class_eval(&block)
193
208
  end
194
209
  self
@@ -231,8 +246,7 @@ module Toys
231
246
  # @return [Toys::DSL::Tool] self, for chaining.
232
247
  #
233
248
  def expand(template_class, *args)
234
- cur_tool = DSL::Tool.current_tool(self, true)
235
- return self if cur_tool.nil?
249
+ cur_tool = DSL::Tool.current_tool(self, false)
236
250
  name = template_class.to_s
237
251
  if template_class.is_a?(::String)
238
252
  template_class = cur_tool.resolve_template(template_class)
@@ -607,11 +621,14 @@ module Toys
607
621
  if included_modules.include?(mod)
608
622
  raise ToolDefinitionError, "Mixin already included: #{mod.name}"
609
623
  end
610
- if mod.respond_to?(:initialization_callback) && mod.initialization_callback
611
- cur_tool.add_initializer(mod.initialization_callback, *args)
624
+ cur_tool.mark_includes_modules
625
+ if mod.respond_to?(:initialization_callback)
626
+ callback = mod.initialization_callback
627
+ cur_tool.add_initializer(callback, *args) if callback
612
628
  end
613
- if mod.respond_to?(:inclusion_callback) && mod.inclusion_callback
614
- class_exec(*args, &mod.inclusion_callback)
629
+ if mod.respond_to?(:inclusion_callback)
630
+ callback = mod.inclusion_callback
631
+ class_exec(*args, &callback) if callback
615
632
  end
616
633
  super(mod)
617
634
  end
@@ -633,6 +650,18 @@ module Toys
633
650
  super(DSL::Tool.resolve_mixin(mod, cur_tool, @__loader))
634
651
  end
635
652
 
653
+ ##
654
+ # Find the given data path (file or directory)
655
+ #
656
+ # @param [String] path The path to find
657
+ # @param [nil,:file,:directory] type Type of file system object to find,
658
+ # or nil to return any type.
659
+ # @return [String,nil] Absolute path of the result, or nil if not found.
660
+ #
661
+ def find_data(path, type: nil)
662
+ @__data_finder ? @__data_finder.find_data(path, type: type) : nil
663
+ end
664
+
636
665
  ## @private
637
666
  def self.new_class(words, priority, loader)
638
667
  tool_class = ::Class.new(::Toys::Tool)
@@ -642,13 +671,13 @@ module Toys
642
671
  tool_class.instance_variable_set(:@__loader, loader)
643
672
  tool_class.instance_variable_set(:@__remaining_words, nil)
644
673
  tool_class.instance_variable_set(:@__path, nil)
674
+ tool_class.instance_variable_set(:@__data_finder, nil)
645
675
  tool_class
646
676
  end
647
677
 
648
678
  ## @private
649
679
  def self.current_tool(tool_class, activate)
650
680
  memoize_var = activate ? :@__active_tool : :@__cur_tool
651
- path = tool_class.instance_variable_get(:@__path)
652
681
  if tool_class.instance_variable_defined?(memoize_var)
653
682
  cur_tool = tool_class.instance_variable_get(memoize_var)
654
683
  else
@@ -667,18 +696,24 @@ module Toys
667
696
  end
668
697
  tool_class.instance_variable_set(memoize_var, cur_tool)
669
698
  end
670
- cur_tool.lock_source_path(path) if cur_tool && activate
699
+ if cur_tool && activate
700
+ path = tool_class.instance_variable_get(:@__path)
701
+ data_finder = tool_class.instance_variable_get(:@__data_finder)
702
+ cur_tool.lock_source_path(path, data_finder)
703
+ end
671
704
  cur_tool
672
705
  end
673
706
 
674
707
  ## @private
675
- def self.prepare(tool_class, remaining_words, path)
708
+ def self.prepare(tool_class, remaining_words, path, data_finder)
676
709
  tool_class.instance_variable_set(:@__remaining_words, remaining_words)
677
710
  tool_class.instance_variable_set(:@__path, path)
711
+ tool_class.instance_variable_set(:@__data_finder, data_finder)
678
712
  yield
679
713
  ensure
680
714
  tool_class.instance_variable_set(:@__remaining_words, nil)
681
715
  tool_class.instance_variable_set(:@__path, nil)
716
+ tool_class.instance_variable_set(:@__data_finder, nil)
682
717
  end
683
718
 
684
719
  ## @private
@@ -40,7 +40,7 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
40
40
  end
41
41
 
42
42
  ## @private
43
- def self.evaluate(tool_class, remaining_words, path)
43
+ def self.evaluate(tool_class, remaining_words, path, data_finder)
44
44
  namespace = ::Module.new
45
45
  namespace.module_eval do
46
46
  include ::Toys::Tool::Keys
@@ -51,7 +51,7 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
51
51
  str = build_eval_string(name, ::IO.read(path))
52
52
  if str
53
53
  const_set(name, namespace)
54
- ::Toys::DSL::Tool.prepare(tool_class, remaining_words, path) do
54
+ ::Toys::DSL::Tool.prepare(tool_class, remaining_words, path, data_finder) do
55
55
  ::Toys::ContextualError.capture_path("Error while loading Toys config!", path) do
56
56
  # rubocop:disable Security/Eval
57
57
  eval(str, __binding, path, 0)
@@ -34,6 +34,8 @@ module Toys
34
34
  # The Loader service loads tools from configuration files, and finds the
35
35
  # appropriate tool given a set of command line arguments.
36
36
  #
37
+ # This class is not thread-safe.
38
+ #
37
39
  class Loader
38
40
  ## @private
39
41
  ToolData = ::Struct.new(:definitions, :top_priority, :active_priority) do
@@ -53,8 +55,21 @@ module Toys
53
55
  # in any configuration directory (not just a toplevel directory) is
54
56
  # loaded first as a standalone configuration file. If not provided,
55
57
  # standalone configuration files are disabled.
58
+ # @param [String,nil] preload_file_name A file with this name that appears
59
+ # in any configuration directory is preloaded before any tools in that
60
+ # configuration directory are defined.
61
+ # @param [String,nil] preload_directory_name A directory with this name
62
+ # that appears in any configuration directory is searched for Ruby
63
+ # files, which are preloaded before any tools in that configuration
64
+ # directory are defined.
65
+ # @param [String,nil] data_directory_name A directory with this name that
66
+ # appears in any configuration directory is added to the data directory
67
+ # search path for any tool file in that directory.
56
68
  # @param [Array] middleware_stack An array of middleware that will be used
57
69
  # by default for all tools loaded by this loader.
70
+ # @param [String] extra_delimiters A string containing characters that can
71
+ # function as delimiters in a tool name. Defaults to empty. Allowed
72
+ # characters are period, colon, and slash.
58
73
  # @param [Toys::Utils::ModuleLookup] mixin_lookup A lookup for well-known
59
74
  # mixin modules. Defaults to an empty lookup.
60
75
  # @param [Toys::Utils::ModuleLookup] middleware_lookup A lookup for
@@ -62,7 +77,8 @@ module Toys
62
77
  # @param [Toys::Utils::ModuleLookup] template_lookup A lookup for
63
78
  # well-known template classes. Defaults to an empty lookup.
64
79
  #
65
- def initialize(index_file_name: nil, middleware_stack: [],
80
+ def initialize(index_file_name: nil, preload_directory_name: nil, preload_file_name: nil,
81
+ data_directory_name: nil, middleware_stack: [], extra_delimiters: "",
66
82
  mixin_lookup: nil, middleware_lookup: nil, template_lookup: nil)
67
83
  if index_file_name && ::File.extname(index_file_name) != ".rb"
68
84
  raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
@@ -71,10 +87,14 @@ module Toys
71
87
  @middleware_lookup = middleware_lookup || Utils::ModuleLookup.new
72
88
  @template_lookup = template_lookup || Utils::ModuleLookup.new
73
89
  @index_file_name = index_file_name
90
+ @preload_file_name = preload_file_name
91
+ @preload_directory_name = preload_directory_name
92
+ @empty_data_finder = Definition::DataFinder.create_empty(data_directory_name)
74
93
  @middleware_stack = middleware_stack
75
94
  @worklist = []
76
95
  @tool_data = {}
77
96
  @max_priority = @min_priority = 0
97
+ @extra_delimiters = process_extra_delimiters(extra_delimiters)
78
98
  get_tool_definition([], -999_999)
79
99
  end
80
100
 
@@ -90,7 +110,7 @@ module Toys
90
110
  paths = Array(path)
91
111
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
92
112
  paths.each do |p|
93
- @worklist << [:file, check_path(p), [], priority]
113
+ @worklist << [:file, check_path(p), [], @empty_data_finder, priority]
94
114
  end
95
115
  self
96
116
  end
@@ -108,7 +128,7 @@ module Toys
108
128
  def add_block(high_priority: false, path: nil, &block)
109
129
  path ||= "(Block #{block.object_id})"
110
130
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
111
- @worklist << [block, path, [], priority]
131
+ @worklist << [block, path, [], @empty_data_finder, priority]
112
132
  self
113
133
  end
114
134
 
@@ -123,23 +143,23 @@ module Toys
123
143
  # Returns a tuple of the found tool, and the array of remaining arguments
124
144
  # that are not part of the tool name and should be passed as tool args.
125
145
  #
126
- # @param [String] args Command line arguments
146
+ # @param [Array<String>] args Command line arguments
127
147
  # @return [Array(Toys::Definition::Tool,Array<String>)]
128
148
  #
129
149
  def lookup(args)
130
- orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
150
+ orig_prefix, args = find_orig_prefix(args)
131
151
  cur_prefix = orig_prefix
132
152
  loop do
133
153
  load_for_prefix(cur_prefix)
134
- p = orig_prefix
154
+ prefix = orig_prefix
135
155
  loop do
136
- tool_definition = get_active_tool(p, [])
156
+ tool_definition = get_active_tool(prefix, [])
137
157
  if tool_definition
138
158
  finish_definitions_in_tree(tool_definition.full_name)
139
- return [tool_definition, args.slice(p.length..-1)]
159
+ return [tool_definition, args.slice(prefix.length..-1)]
140
160
  end
141
- break if p.empty? || p.length <= cur_prefix.length
142
- p = p.slice(0..-2)
161
+ break if prefix.empty? || prefix.length <= cur_prefix.length
162
+ prefix = prefix.slice(0..-2)
143
163
  end
144
164
  raise "Unexpected error" if cur_prefix.empty?
145
165
  cur_prefix = cur_prefix.slice(0..-2)
@@ -189,13 +209,16 @@ module Toys
189
209
  end
190
210
 
191
211
  ##
192
- # Returns a tool specified by the given words, with the given priority.
193
- # Does not do any loading. If the tool is not present, creates it.
212
+ # Returns the active tool specified by the given words, with the given
213
+ # priority, without doing any loading. If the given priority matches the
214
+ # currently active tool, returns it. If the given priority is lower than
215
+ # the active priority, returns `nil`. If the given priority is higher than
216
+ # the active priority, returns and activates a new tool.
194
217
  #
195
218
  # @param [Array<String>] words The name of the tool.
196
219
  # @param [Integer] priority The priority of the request.
197
220
  # @return [Toys::Definition::Tool,Toys::Definition::Alias,nil] The tool or
198
- # alias, or `nil` if the given priority is insufficient
221
+ # alias, or `nil` if the given priority is insufficient.
199
222
  #
200
223
  # @private
201
224
  #
@@ -259,9 +282,10 @@ module Toys
259
282
  if tool_data.top_priority.nil? || tool_data.top_priority < priority
260
283
  tool_data.top_priority = priority
261
284
  end
262
- middlewares = @middleware_stack.map { |m| resolve_middleware(m) }
263
- tool_data.definitions[priority] ||=
285
+ tool_data.definitions[priority] ||= begin
286
+ middlewares = @middleware_stack.map { |m| resolve_middleware(m) }
264
287
  Definition::Tool.new(self, parent, words, priority, middlewares)
288
+ end
265
289
  end
266
290
 
267
291
  ##
@@ -290,7 +314,7 @@ module Toys
290
314
  # @private
291
315
  #
292
316
  def load_path(path, words, remaining_words, priority)
293
- load_validated_path(check_path(path), words, remaining_words, priority)
317
+ load_validated_path(check_path(path), words, remaining_words, @empty_data_finder, priority)
294
318
  end
295
319
 
296
320
  ##
@@ -301,13 +325,13 @@ module Toys
301
325
  def load_proc(proc, words, remaining_words, priority, path)
302
326
  if remaining_words
303
327
  tool_class = get_tool_definition(words, priority).tool_class
304
- ::Toys::DSL::Tool.prepare(tool_class, remaining_words, path) do
328
+ ::Toys::DSL::Tool.prepare(tool_class, remaining_words, path, @empty_data_finder) do
305
329
  ::Toys::ContextualError.capture("Error while loading Toys config!") do
306
330
  tool_class.class_eval(&proc)
307
331
  end
308
332
  end
309
333
  else
310
- @worklist << [proc, path, words, priority]
334
+ @worklist << [proc, path, words, @empty_data_finder, priority]
311
335
  end
312
336
  end
313
337
 
@@ -328,6 +352,28 @@ module Toys
328
352
 
329
353
  private
330
354
 
355
+ ALLOWED_DELIMITERS = %r{^[\./:]*$}
356
+
357
+ def process_extra_delimiters(input)
358
+ unless ALLOWED_DELIMITERS =~ input
359
+ raise ::ArgumentError, "Illegal delimiters in #{input.inspect}"
360
+ end
361
+ chars = ::Regexp.escape(input.chars.uniq.join)
362
+ chars.empty? ? nil : ::Regexp.new("[#{chars}]")
363
+ end
364
+
365
+ def find_orig_prefix(args)
366
+ if @extra_delimiters
367
+ first_split = (args.first || "").split(@extra_delimiters)
368
+ if first_split.size > 1
369
+ args = first_split + args.slice(1..-1)
370
+ return [first_split, args]
371
+ end
372
+ end
373
+ orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
374
+ [orig_prefix, args]
375
+ end
376
+
331
377
  def get_tool_data(words)
332
378
  @tool_data[words] ||= ToolData.new({}, nil, nil)
333
379
  end
@@ -395,51 +441,73 @@ module Toys
395
441
  def load_for_prefix(prefix)
396
442
  cur_worklist = @worklist
397
443
  @worklist = []
398
- cur_worklist.each do |source, path, words, priority|
444
+ cur_worklist.each do |source, path, words, data_finder, priority|
399
445
  remaining_words = calc_remaining_words(prefix, words)
400
446
  if source.respond_to?(:call)
401
447
  load_proc(source, words, remaining_words, priority, path)
402
448
  elsif source == :file
403
- load_validated_path(path, words, remaining_words, priority)
449
+ load_validated_path(path, words, remaining_words, data_finder, priority)
404
450
  end
405
451
  end
406
452
  end
407
453
 
408
- def load_validated_path(path, words, remaining_words, priority)
454
+ def load_validated_path(path, words, remaining_words, data_finder, priority)
409
455
  if remaining_words
410
- load_relevant_path(path, words, remaining_words, priority)
456
+ load_relevant_path(path, words, remaining_words, data_finder, priority)
411
457
  else
412
- @worklist << [:file, path, words, priority]
458
+ @worklist << [:file, path, words, data_finder, priority]
413
459
  end
414
460
  end
415
461
 
416
- def load_relevant_path(path, words, remaining_words, priority)
462
+ def load_relevant_path(path, words, remaining_words, data_finder, priority)
417
463
  if ::File.extname(path) == ".rb"
418
464
  tool_class = get_tool_definition(words, priority).tool_class
419
- Toys::InputFile.evaluate(tool_class, remaining_words, path)
465
+ InputFile.evaluate(tool_class, remaining_words, path, data_finder)
420
466
  else
421
- load_index_in(path, words, remaining_words, priority)
467
+ do_preload(path)
468
+ data_finder = data_finder.finder_for(path)
469
+ load_index_in(path, words, remaining_words, data_finder, priority)
422
470
  ::Dir.entries(path).each do |child|
423
- load_child_in(path, child, words, remaining_words, priority)
471
+ load_child_in(path, child, words, remaining_words, data_finder, priority)
424
472
  end
425
473
  end
426
474
  end
427
475
 
428
- def load_index_in(path, words, remaining_words, priority)
476
+ def load_index_in(path, words, remaining_words, data_finder, priority)
429
477
  return unless @index_file_name
430
478
  index_path = ::File.join(path, @index_file_name)
431
479
  index_path = check_path(index_path, type: :file, lenient: true)
432
- load_relevant_path(index_path, words, remaining_words, priority) if index_path
480
+ load_relevant_path(index_path, words, remaining_words, data_finder, priority) if index_path
433
481
  end
434
482
 
435
- def load_child_in(path, child, words, remaining_words, priority)
483
+ def load_child_in(path, child, words, remaining_words, data_finder, priority)
436
484
  return if child.start_with?(".")
437
485
  return if child == @index_file_name
438
486
  child_path = check_path(::File.join(path, child))
439
487
  child_word = ::File.basename(child, ".rb")
440
488
  next_words = words + [child_word]
441
489
  next_remaining = Loader.next_remaining_words(remaining_words, child_word)
442
- load_validated_path(child_path, next_words, next_remaining, priority)
490
+ load_validated_path(child_path, next_words, next_remaining, data_finder, priority)
491
+ end
492
+
493
+ def do_preload(path)
494
+ if @preload_file_name
495
+ preload_file = ::File.join(path, @preload_file_name)
496
+ if !::File.directory?(preload_file) && ::File.readable?(preload_file)
497
+ require preload_file
498
+ end
499
+ end
500
+ if @preload_directory_name
501
+ preload_dir = ::File.join(path, @preload_directory_name)
502
+ if ::File.directory?(preload_dir) && ::File.readable?(preload_dir)
503
+ ::Dir.entries(preload_dir).each do |child|
504
+ preload_file = ::File.join(preload_dir, child)
505
+ if !::File.directory?(preload_file) && ::File.readable?(preload_file)
506
+ require preload_file
507
+ end
508
+ end
509
+ end
510
+ end
443
511
  end
444
512
 
445
513
  def check_path(path, lenient: false, type: nil)
@@ -56,6 +56,12 @@ module Toys
56
56
  #
57
57
  DEFAULT_USAGE_FLAGS = ["--usage"].freeze
58
58
 
59
+ ##
60
+ # Default list subtools flags
61
+ # @return [Array<String>]
62
+ #
63
+ DEFAULT_LIST_FLAGS = ["--tools"].freeze
64
+
59
65
  ##
60
66
  # Default recursive flags
61
67
  # @return [Array<String>]
@@ -80,6 +86,12 @@ module Toys
80
86
  #
81
87
  SHOW_USAGE_KEY = Object.new.freeze
82
88
 
89
+ ##
90
+ # Key set when the show subtool list flag is present
91
+ # @return [Object]
92
+ #
93
+ SHOW_LIST_KEY = Object.new.freeze
94
+
83
95
  ##
84
96
  # Key for the recursive setting
85
97
  # @return [Object]
@@ -117,6 +129,14 @@ module Toys
117
129
  # * The `false` value for no flags. (Default)
118
130
  # * A proc that takes a tool and returns any of the above.
119
131
  #
132
+ # @param [Boolean,Array<String>,Proc] list_flags Specify flags to
133
+ # display subtool list. The value may be any of the following:
134
+ #
135
+ # * An array of flags.
136
+ # * The `true` value to use {DEFAULT_LIST_FLAGS}.
137
+ # * The `false` value for no flags. (Default)
138
+ # * A proc that takes a tool and returns any of the above.
139
+ #
120
140
  # @param [Boolean,Array<String>,Proc] recursive_flags Specify flags
121
141
  # to control recursive subtool search. The value may be any of the
122
142
  # following:
@@ -156,6 +176,7 @@ module Toys
156
176
  #
157
177
  def initialize(help_flags: false,
158
178
  usage_flags: false,
179
+ list_flags: false,
159
180
  recursive_flags: false,
160
181
  search_flags: false,
161
182
  default_recursive: false,
@@ -167,6 +188,7 @@ module Toys
167
188
  styled_output: nil)
168
189
  @help_flags = help_flags
169
190
  @usage_flags = usage_flags
191
+ @list_flags = list_flags
170
192
  @recursive_flags = recursive_flags
171
193
  @search_flags = search_flags
172
194
  @default_recursive = default_recursive ? true : false
@@ -183,20 +205,17 @@ module Toys
183
205
  #
184
206
  def config(tool_definition, loader)
185
207
  unless tool_definition.argument_parsing_disabled?
208
+ has_subtools = loader.has_subtools?(tool_definition.full_name)
186
209
  help_flags = add_help_flags(tool_definition)
187
210
  usage_flags = add_usage_flags(tool_definition)
188
- if @allow_root_args && (!help_flags.empty? || !usage_flags.empty?)
189
- if tool_definition.root? && tool_definition.arg_definitions.empty?
190
- tool_definition.set_remaining_args(TOOL_NAME_KEY,
191
- display_name: "TOOL_NAME",
192
- desc: "The tool for which to display help")
193
- end
194
- end
195
- if (!help_flags.empty? || @fallback_execution) &&
196
- loader.has_subtools?(tool_definition.full_name)
211
+ list_flags = has_subtools ? add_list_flags(tool_definition) : []
212
+ if (!help_flags.empty? || !list_flags.empty? || @fallback_execution) && has_subtools
197
213
  add_recursive_flags(tool_definition)
198
214
  add_search_flags(tool_definition)
199
215
  end
216
+ if !help_flags.empty? || !usage_flags.empty? || !list_flags.empty?
217
+ add_root_args(tool_definition)
218
+ end
200
219
  end
201
220
  yield
202
221
  end
@@ -206,17 +225,16 @@ module Toys
206
225
  #
207
226
  def run(tool)
208
227
  if tool[SHOW_USAGE_KEY]
209
- help_text = get_help_text(tool)
210
- str = help_text.usage_string(wrap_width: terminal.width)
211
- terminal.puts(str)
212
- elsif @fallback_execution && !tool[Tool::Keys::TOOL_DEFINITION].runnable? ||
213
- tool[SHOW_HELP_KEY]
214
- help_text = get_help_text(tool)
215
- str = help_text.help_string(recursive: tool[RECURSIVE_SUBTOOLS_KEY],
216
- search: tool[SEARCH_STRING_KEY],
217
- show_source_path: @show_source_path,
218
- wrap_width: terminal.width)
219
- output_help(str)
228
+ terminal.puts(get_help_text(tool).usage_string(wrap_width: terminal.width))
229
+ elsif tool[SHOW_LIST_KEY]
230
+ terminal.puts(get_help_text(tool).list_string(recursive: tool[RECURSIVE_SUBTOOLS_KEY],
231
+ search: tool[SEARCH_STRING_KEY],
232
+ wrap_width: terminal.width))
233
+ elsif should_show_help(tool)
234
+ output_help(get_help_text(tool).help_string(recursive: tool[RECURSIVE_SUBTOOLS_KEY],
235
+ search: tool[SEARCH_STRING_KEY],
236
+ show_source_path: @show_source_path,
237
+ wrap_width: terminal.width))
220
238
  else
221
239
  yield
222
240
  end
@@ -228,6 +246,11 @@ module Toys
228
246
  @terminal ||= Utils::Terminal.new(output: @stream, styled: @styled_output)
229
247
  end
230
248
 
249
+ def should_show_help(tool)
250
+ @fallback_execution && !tool[Tool::Keys::TOOL_DEFINITION].runnable? ||
251
+ tool[SHOW_HELP_KEY]
252
+ end
253
+
231
254
  def output_help(str)
232
255
  if less_path
233
256
  Utils::Exec.new.exec([less_path, "-R"], in: [:string, str])
@@ -288,6 +311,18 @@ module Toys
288
311
  usage_flags
289
312
  end
290
313
 
314
+ def add_list_flags(tool_definition)
315
+ list_flags = resolve_flags_spec(@list_flags, tool_definition, DEFAULT_LIST_FLAGS)
316
+ unless list_flags.empty?
317
+ tool_definition.add_flag(
318
+ SHOW_LIST_KEY, list_flags,
319
+ report_collisions: false,
320
+ desc: "List the subtools under this tool"
321
+ )
322
+ end
323
+ list_flags
324
+ end
325
+
291
326
  def add_recursive_flags(tool_definition)
292
327
  recursive_flags = resolve_flags_spec(@recursive_flags, tool_definition,
293
328
  DEFAULT_RECURSIVE_FLAGS)
@@ -297,7 +332,8 @@ module Toys
297
332
  tool_definition.add_flag(
298
333
  RECURSIVE_SUBTOOLS_KEY, recursive_flags,
299
334
  report_collisions: false, default: @default_recursive,
300
- desc: "Show all subtools recursively (default is #{@default_recursive})"
335
+ desc: "List all subtools recursively when displaying help" \
336
+ " (default is #{@default_recursive})"
301
337
  )
302
338
  end
303
339
  recursive_flags
@@ -309,12 +345,20 @@ module Toys
309
345
  tool_definition.add_flag(
310
346
  SEARCH_STRING_KEY, search_flags,
311
347
  report_collisions: false,
312
- desc: "Search subtools for the given regular expression"
348
+ desc: "Search subtools for the given regular expression when displaying help"
313
349
  )
314
350
  end
315
351
  search_flags
316
352
  end
317
353
 
354
+ def add_root_args(tool_definition)
355
+ if @allow_root_args && tool_definition.root? && tool_definition.arg_definitions.empty?
356
+ tool_definition.set_remaining_args(TOOL_NAME_KEY,
357
+ display_name: "TOOL_NAME",
358
+ desc: "The tool for which to display help")
359
+ end
360
+ end
361
+
318
362
  def resolve_flags_spec(flags, tool_definition, defaults)
319
363
  flags = flags.call(tool_definition) if flags.respond_to?(:call)
320
364
  case flags
@@ -259,6 +259,18 @@ module Toys
259
259
  end
260
260
  end
261
261
 
262
+ ##
263
+ # Find the given data file or directory in this tool's search path.
264
+ #
265
+ # @param [String] path The path to find
266
+ # @param [nil,:file,:directory] type Type of file system object to find,
267
+ # or nil to return any type.
268
+ # @return [String,nil] Absolute path of the result, or nil if not found.
269
+ #
270
+ def find_data(path, type: nil)
271
+ @__data[Keys::TOOL_DEFINITION].find_data(path, type: type)
272
+ end
273
+
262
274
  ##
263
275
  # Exit immediately with the given status code
264
276
  #
@@ -129,6 +129,29 @@ module Toys
129
129
  assembler.result
130
130
  end
131
131
 
132
+ ##
133
+ # Generate a subtool list string.
134
+ #
135
+ # @param [Boolean] recursive If true, and the tool is a namespace,
136
+ # display all subcommands recursively. Defaults to false.
137
+ # @param [String,nil] search An optional string to search for when
138
+ # listing subcommands. Defaults to `nil` which finds all subcommands.
139
+ # @param [Integer] indent Indent width. Default is {DEFAULT_INDENT}.
140
+ # @param [Integer,nil] wrap_width Wrap width of the column, or `nil` to
141
+ # disable wrap. Default is `nil`.
142
+ # @param [Boolean] styled Output ansi styles. Default is `true`.
143
+ #
144
+ # @return [String] A usage string.
145
+ #
146
+ def list_string(recursive: false, search: nil,
147
+ indent: nil, wrap_width: nil, styled: true)
148
+ indent ||= DEFAULT_INDENT
149
+ subtools = find_subtools(recursive, search)
150
+ assembler = ListStringAssembler.new(@tool, subtools, recursive, search,
151
+ indent, wrap_width, styled)
152
+ assembler.result
153
+ end
154
+
132
155
  private
133
156
 
134
157
  def find_subtools(recursive, search)
@@ -493,6 +516,86 @@ module Toys
493
516
  "#{' ' * (@indent + @indent2)}#{str}"
494
517
  end
495
518
  end
519
+
520
+ ## @private
521
+ class ListStringAssembler
522
+ def initialize(tool, subtools, recursive, search_term, indent, wrap_width, styled)
523
+ @tool = tool
524
+ @subtools = subtools
525
+ @recursive = recursive
526
+ @search_term = search_term
527
+ @indent = indent
528
+ @wrap_width = wrap_width
529
+ assemble(styled)
530
+ end
531
+
532
+ attr_reader :result
533
+
534
+ private
535
+
536
+ def assemble(styled)
537
+ @lines = Utils::Terminal.new(output: ::StringIO.new, styled: styled)
538
+ add_header
539
+ add_list
540
+ @result = @lines.output.string
541
+ end
542
+
543
+ def add_header
544
+ top_line = @recursive ? "Recursive list of tools" : "List of tools"
545
+ @lines <<
546
+ if @tool.root?
547
+ "#{top_line}:"
548
+ else
549
+ "#{top_line} under #{bold(@tool.display_name)}:"
550
+ end
551
+ @lines << ""
552
+ if @search_term
553
+ @lines << "Showing search results for \"#{@search_term}\""
554
+ @lines << ""
555
+ end
556
+ end
557
+
558
+ def add_list
559
+ name_len = @tool.full_name.length
560
+ @subtools.each do |subtool|
561
+ tool_name = subtool.full_name.slice(name_len..-1).join(" ")
562
+ desc =
563
+ if subtool.is_a?(Definition::Alias)
564
+ "(Alias of #{subtool.display_target})"
565
+ else
566
+ subtool.desc
567
+ end
568
+ add_prefix_with_desc(bold(tool_name), desc)
569
+ end
570
+ end
571
+
572
+ def add_prefix_with_desc(prefix, desc)
573
+ if desc.empty?
574
+ @lines << prefix
575
+ elsif !desc.is_a?(Utils::WrappableString)
576
+ @lines << "#{prefix} - #{desc}"
577
+ else
578
+ desc = wrap_indent(Utils::WrappableString.new(["#{prefix} -"] + desc.fragments))
579
+ @lines << desc[0]
580
+ desc[1..-1].each do |line|
581
+ @lines << indent_str(line)
582
+ end
583
+ end
584
+ end
585
+
586
+ def wrap_indent(input)
587
+ return Utils::WrappableString.wrap_lines(input, nil) unless @wrap_width
588
+ Utils::WrappableString.wrap_lines(input, @wrap_width, @wrap_width - @indent)
589
+ end
590
+
591
+ def bold(str)
592
+ @lines.apply_styles(str, :bold)
593
+ end
594
+
595
+ def indent_str(str)
596
+ "#{' ' * @indent}#{str}"
597
+ end
598
+ end
496
599
  end
497
600
  end
498
601
  end
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.4.5
4
+ version: 0.5.0
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-08-06 00:00:00.000000000 Z
11
+ date: 2018-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -66,34 +66,48 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redcarpet
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rubocop
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 0.58.2
89
+ version: 0.59.1
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 0.58.2
96
+ version: 0.59.1
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: yard
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: 0.9.14
103
+ version: 0.9.16
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: 0.9.14
110
+ version: 0.9.16
97
111
  description: Toys-Core is the command line tool framework underlying Toys. It can
98
112
  be used to create command line binaries using the internal Toys APIs.
99
113
  email:
@@ -113,6 +127,7 @@ files:
113
127
  - lib/toys/definition/acceptor.rb
114
128
  - lib/toys/definition/alias.rb
115
129
  - lib/toys/definition/arg.rb
130
+ - lib/toys/definition/data_finder.rb
116
131
  - lib/toys/definition/flag.rb
117
132
  - lib/toys/definition/tool.rb
118
133
  - lib/toys/dsl/arg.rb
@@ -145,7 +160,11 @@ files:
145
160
  homepage: https://github.com/dazuma/toys
146
161
  licenses:
147
162
  - BSD-3-Clause
148
- metadata: {}
163
+ metadata:
164
+ changelog_uri: https://github.com/dazuma/toys/blob/master/toys-core/CHANGELOG.md
165
+ source_code_uri: https://github.com/dazuma/toys
166
+ bug_tracker_uri: https://github.com/dazuma/toys/issues
167
+ documentation_uri: https://www.rubydoc.info/gems/toys-core
149
168
  post_install_message:
150
169
  rdoc_options: []
151
170
  require_paths: