toys-core 0.11.5 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +1 -1
  4. data/lib/toys-core.rb +4 -1
  5. data/lib/toys/acceptor.rb +3 -3
  6. data/lib/toys/arg_parser.rb +6 -7
  7. data/lib/toys/cli.rb +44 -14
  8. data/lib/toys/compat.rb +19 -22
  9. data/lib/toys/completion.rb +3 -1
  10. data/lib/toys/context.rb +2 -2
  11. data/lib/toys/core.rb +1 -1
  12. data/lib/toys/dsl/base.rb +85 -0
  13. data/lib/toys/dsl/flag.rb +3 -3
  14. data/lib/toys/dsl/flag_group.rb +7 -7
  15. data/lib/toys/dsl/internal.rb +206 -0
  16. data/lib/toys/dsl/positional_arg.rb +3 -3
  17. data/lib/toys/dsl/tool.rb +174 -216
  18. data/lib/toys/errors.rb +1 -0
  19. data/lib/toys/flag.rb +15 -18
  20. data/lib/toys/flag_group.rb +5 -4
  21. data/lib/toys/input_file.rb +4 -4
  22. data/lib/toys/loader.rb +189 -50
  23. data/lib/toys/middleware.rb +1 -1
  24. data/lib/toys/mixin.rb +2 -2
  25. data/lib/toys/positional_arg.rb +3 -3
  26. data/lib/toys/settings.rb +900 -0
  27. data/lib/toys/source_info.rb +121 -18
  28. data/lib/toys/standard_middleware/apply_config.rb +5 -4
  29. data/lib/toys/standard_middleware/set_default_descriptions.rb +18 -18
  30. data/lib/toys/standard_middleware/show_help.rb +17 -5
  31. data/lib/toys/standard_mixins/exec.rb +12 -14
  32. data/lib/toys/standard_mixins/git_cache.rb +48 -0
  33. data/lib/toys/standard_mixins/xdg.rb +56 -0
  34. data/lib/toys/template.rb +2 -2
  35. data/lib/toys/{tool.rb → tool_definition.rb} +100 -41
  36. data/lib/toys/utils/exec.rb +4 -5
  37. data/lib/toys/utils/gems.rb +8 -7
  38. data/lib/toys/utils/git_cache.rb +184 -0
  39. data/lib/toys/utils/help_text.rb +90 -34
  40. data/lib/toys/utils/terminal.rb +1 -1
  41. data/lib/toys/utils/xdg.rb +293 -0
  42. metadata +14 -7
data/lib/toys/errors.rb CHANGED
@@ -54,6 +54,7 @@ module Toys
54
54
  @config_line = config_line
55
55
  @tool_name = tool_name
56
56
  @tool_args = tool_args
57
+ set_backtrace(cause.backtrace)
57
58
  end
58
59
 
59
60
  attr_reader :cause
data/lib/toys/flag.rb CHANGED
@@ -85,11 +85,11 @@ module Toys
85
85
  # true.
86
86
  # @param group [Toys::FlagGroup] Group containing this flag.
87
87
  # @param desc [String,Array<String>,Toys::WrappableString] Short
88
- # description for the flag. See {Toys::Tool#desc=} for a description of
89
- # allowed formats. Defaults to the empty string.
88
+ # description for the flag. See {Toys::ToolDefinition#desc} for a
89
+ # description of allowed formats. Defaults to the empty string.
90
90
  # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
91
- # Long description for the flag. See {Toys::Tool#long_desc=} for a
92
- # description of allowed formats. Defaults to the empty array.
91
+ # Long description for the flag. See {Toys::ToolDefinition#long_desc}
92
+ # for a description of allowed formats. Defaults to the empty array.
93
93
  # @param display_name [String] A display name for this flag, used in help
94
94
  # text and error messages.
95
95
  # @param used_flags [Array<String>] An array of flags already in use.
@@ -368,7 +368,7 @@ module Toys
368
368
  key_str = key.to_s
369
369
  flag_str =
370
370
  if key_str.length == 1
371
- "-#{key_str}" if key_str =~ /[a-zA-Z0-9\?]/
371
+ "-#{key_str}" if key_str =~ /[a-zA-Z0-9?]/
372
372
  elsif key_str.length > 1
373
373
  key_str = key_str.downcase.tr("_", "-").gsub(/[^a-z0-9-]/, "").sub(/^-+/, "")
374
374
  "--#{key_str}" unless key_str.empty?
@@ -455,23 +455,19 @@ module Toys
455
455
  #
456
456
  def initialize(str)
457
457
  case str
458
- when /\A(-([\?\w]))\z/
458
+ when /\A(-([?\w]))\z/
459
459
  setup(str, $1, nil, $1, $2, :short, nil, nil, nil, nil)
460
- when /\A(-([\?\w]))( ?)\[(\w+)\]\z/
461
- setup(str, $1, nil, $1, $2, :short, :value, :optional, $3, $4)
462
- when /\A(-([\?\w]))\[( )(\w+)\]\z/
463
- setup(str, $1, nil, $1, $2, :short, :value, :optional, $3, $4)
464
- when /\A(-([\?\w]))( ?)(\w+)\z/
460
+ when /\A(-([?\w]))(?:( ?)\[|\[( ))(\w+)\]\z/
461
+ setup(str, $1, nil, $1, $2, :short, :value, :optional, $3 || $4, $5)
462
+ when /\A(-([?\w]))( ?)(\w+)\z/
465
463
  setup(str, $1, nil, $1, $2, :short, :value, :required, $3, $4)
466
- when /\A--\[no-\](\w[\?\w-]*)\z/
464
+ when /\A--\[no-\](\w[?\w-]*)\z/
467
465
  setup(str, "--#{$1}", "--no-#{$1}", str, $1, :long, :boolean, nil, nil, nil)
468
- when /\A(--(\w[\?\w-]*))\z/
466
+ when /\A(--(\w[?\w-]*))\z/
469
467
  setup(str, $1, nil, $1, $2, :long, nil, nil, nil, nil)
470
- when /\A(--(\w[\?\w-]*))([= ])\[(\w+)\]\z/
471
- setup(str, $1, nil, $1, $2, :long, :value, :optional, $3, $4)
472
- when /\A(--(\w[\?\w-]*))\[([= ])(\w+)\]\z/
473
- setup(str, $1, nil, $1, $2, :long, :value, :optional, $3, $4)
474
- when /\A(--(\w[\?\w-]*))([= ])(\w+)\z/
468
+ when /\A(--(\w[?\w-]*))(?:([= ])\[|\[([= ]))(\w+)\]\z/
469
+ setup(str, $1, nil, $1, $2, :long, :value, :optional, $3 || $4, $5)
470
+ when /\A(--(\w[?\w-]*))([= ])(\w+)\z/
475
471
  setup(str, $1, nil, $1, $2, :long, :value, :required, $3, $4)
476
472
  else
477
473
  raise ToolDefinitionError, "Illegal flag: #{str.inspect}"
@@ -731,6 +727,7 @@ module Toys
731
727
  # @param include_negative [Boolean] Whether to include `--no-*` forms.
732
728
  #
733
729
  def initialize(flag:, include_short: true, include_long: true, include_negative: true)
730
+ super()
734
731
  @flag = flag
735
732
  @include_short = include_short
736
733
  @include_long = include_long
@@ -17,11 +17,12 @@ module Toys
17
17
  #
18
18
  # @param type [Symbol] The type of group. Default is `:optional`.
19
19
  # @param desc [String,Array<String>,Toys::WrappableString] Short
20
- # description for the group. See {Toys::Tool#desc=} for a description
21
- # of allowed formats. Defaults to `"Flags"`.
20
+ # description for the group. See {Toys::ToolDefinition#desc} for a
21
+ # description of allowed formats. Defaults to `"Flags"`.
22
22
  # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
23
- # Long description for the flag group. See {Toys::Tool#long_desc=} for
24
- # a description of allowed formats. Defaults to the empty array.
23
+ # Long description for the flag group. See
24
+ # {Toys::ToolDefinition#long_desc} for a description of allowed
25
+ # formats. Defaults to the empty array.
25
26
  # @param name [String,Symbol,nil] The name of the group, or nil for no
26
27
  # name.
27
28
  # @return [Toys::FlagGroup::Base] A flag group of the correct subclass.
@@ -11,11 +11,11 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
11
11
  end
12
12
 
13
13
  ## @private
14
- def self.evaluate(tool_class, remaining_words, source)
14
+ def self.evaluate(tool_class, words, priority, remaining_words, source, loader)
15
15
  namespace = ::Module.new
16
16
  namespace.module_eval do
17
17
  include ::Toys::Context::Key
18
- @tool_class = tool_class
18
+ @__tool_class = tool_class
19
19
  end
20
20
  path = source.source_path
21
21
  basename = ::File.basename(path).tr(".-", "_").gsub(/\W/, "")
@@ -23,7 +23,7 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
23
23
  str = build_eval_string(name, ::IO.read(path))
24
24
  if str
25
25
  const_set(name, namespace)
26
- ::Toys::DSL::Tool.prepare(tool_class, remaining_words, source) do
26
+ ::Toys::DSL::Internal.prepare(tool_class, words, priority, remaining_words, source, loader) do
27
27
  ::Toys::ContextualError.capture_path("Error while loading Toys config!", path) do
28
28
  # rubocop:disable Security/Eval
29
29
  eval(str, __binding, path, -2)
@@ -39,7 +39,7 @@ module Toys::InputFile # rubocop:disable Style/ClassAndModuleChildren
39
39
  return nil if index.nil?
40
40
  "#{string[0, index]}\n" \
41
41
  "module #{module_name}\n" \
42
- "@tool_class.class_eval do\n" \
42
+ "@__tool_class.class_eval do\n" \
43
43
  "#{string[index..-1]}\n" \
44
44
  "end\n" \
45
45
  "end\n"
data/lib/toys/loader.rb CHANGED
@@ -54,7 +54,8 @@ module Toys
54
54
  extra_delimiters: "",
55
55
  mixin_lookup: nil,
56
56
  middleware_lookup: nil,
57
- template_lookup: nil
57
+ template_lookup: nil,
58
+ git_cache: nil
58
59
  )
59
60
  if index_file_name && ::File.extname(index_file_name) != ".rb"
60
61
  raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
@@ -71,30 +72,82 @@ module Toys
71
72
  @loading_started = false
72
73
  @worklist = []
73
74
  @tool_data = {}
75
+ @roots_by_priority = {}
74
76
  @max_priority = @min_priority = 0
75
77
  @stop_priority = BASE_PRIORITY
76
78
  @min_loaded_priority = 999_999
77
79
  @middleware_stack = Middleware.stack(middleware_stack)
78
80
  @delimiter_handler = DelimiterHandler.new(extra_delimiters)
81
+ @git_cache = git_cache
79
82
  get_tool([], BASE_PRIORITY)
80
83
  end
81
84
 
82
85
  ##
83
86
  # Add a configuration file/directory to the loader.
84
87
  #
85
- # @param paths [String,Array<String>] One or more paths to add.
88
+ # @param path [String] A single path to add.
86
89
  # @param high_priority [Boolean] If true, add this path at the top of the
87
90
  # priority list. Defaults to false, indicating the new path should be
88
91
  # at the bottom of the priority list.
92
+ # @param source_name [String] A custom name for the root source. Optional.
93
+ # @param context_directory [String,nil,:path,:parent] The context directory
94
+ # for tools loaded from this path. You can pass a directory path as a
95
+ # string, `:path` to denote the given path, `:parent` to denote the
96
+ # given path's parent directory, or `nil` to denote no context.
97
+ # Defaults to `:parent`.
89
98
  # @return [self]
90
99
  #
91
- def add_path(paths, high_priority: false)
92
- paths = Array(paths)
100
+ def add_path(path,
101
+ high_priority: false,
102
+ source_name: nil,
103
+ context_directory: :parent)
93
104
  @mutex.synchronize do
94
105
  raise "Cannot add a path after tool loading has started" if @loading_started
95
106
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
96
- paths.each do |path|
97
- source = SourceInfo.create_path_root(path, @data_dir_name, @lib_dir_name)
107
+ source = SourceInfo.create_path_root(path, priority,
108
+ context_directory: context_directory,
109
+ data_dir_name: @data_dir_name,
110
+ lib_dir_name: @lib_dir_name,
111
+ source_name: source_name)
112
+ @roots_by_priority[priority] = source
113
+ @worklist << [source, [], priority]
114
+ end
115
+ self
116
+ end
117
+
118
+ ##
119
+ # Add a set of configuration files/directories from a common directory to
120
+ # the loader. The set of paths will be added at the same priority level and
121
+ # will share a root.
122
+ #
123
+ # @param root_path [String] A root path to be seen as the root source. This
124
+ # should generally be a directory containing the paths to add.
125
+ # @param relative_paths [String,Array<String>] One or more paths to add, as
126
+ # relative paths from the common root.
127
+ # @param high_priority [Boolean] If true, add the paths at the top of the
128
+ # priority list. Defaults to false, indicating the new paths should be
129
+ # at the bottom of the priority list.
130
+ # @param context_directory [String,nil,:path,:parent] The context directory
131
+ # for tools loaded from this path. You can pass a directory path as a
132
+ # string, `:path` to denote the given root path, `:parent` to denote
133
+ # the given root path's parent directory, or `nil` to denote no context.
134
+ # Defaults to `:path`.
135
+ # @return [self]
136
+ #
137
+ def add_path_set(root_path, relative_paths,
138
+ high_priority: false,
139
+ context_directory: :path)
140
+ relative_paths = Array(relative_paths)
141
+ @mutex.synchronize do
142
+ raise "Cannot add a path after tool loading has started" if @loading_started
143
+ priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
144
+ root_source = SourceInfo.create_path_root(root_path, priority,
145
+ context_directory: context_directory,
146
+ data_dir_name: @data_dir_name,
147
+ lib_dir_name: @lib_dir_name)
148
+ @roots_by_priority[priority] = root_source
149
+ relative_paths.each do |path, individual_name|
150
+ source = root_source.relative_child(path, source_name: individual_name)
98
151
  @worklist << [source, [], priority]
99
152
  end
100
153
  end
@@ -107,19 +160,65 @@ module Toys
107
160
  # @param high_priority [Boolean] If true, add this block at the top of the
108
161
  # priority list. Defaults to false, indicating the block should be at
109
162
  # the bottom of the priority list.
110
- # @param name [String] The source name that will be shown in documentation
111
- # for tools defined in this block. If omitted, a default unique string
112
- # will be generated.
163
+ # @param source_name [String] The source name that will be shown in
164
+ # documentation for tools defined in this block. If omitted, a default
165
+ # unique string will be generated.
113
166
  # @param block [Proc] The block of configuration, executed in the context
114
167
  # of the tool DSL {Toys::DSL::Tool}.
168
+ # @param context_directory [String,nil] The context directory for tools
169
+ # loaded from this block. You can pass a directory path as a string, or
170
+ # `nil` to denote no context. Defaults to `nil`.
115
171
  # @return [self]
116
172
  #
117
- def add_block(high_priority: false, name: nil, &block)
118
- name ||= "(Code block #{block.object_id})"
173
+ def add_block(high_priority: false,
174
+ source_name: nil,
175
+ context_directory: nil,
176
+ &block)
119
177
  @mutex.synchronize do
120
178
  raise "Cannot add a block after tool loading has started" if @loading_started
121
179
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
122
- source = SourceInfo.create_proc_root(block, name, @data_dir_name, @lib_dir_name)
180
+ source = SourceInfo.create_proc_root(block, priority,
181
+ context_directory: context_directory,
182
+ source_name: source_name,
183
+ data_dir_name: @data_dir_name,
184
+ lib_dir_name: @lib_dir_name)
185
+ @roots_by_priority[priority] = source
186
+ @worklist << [source, [], priority]
187
+ end
188
+ self
189
+ end
190
+
191
+ ##
192
+ # Add a configuration git source to the loader.
193
+ #
194
+ # @param git_remote [String] The git repo URL
195
+ # @param git_path [String] The path to the relevant file or directory in
196
+ # the repo. Specify the empty string to use the entire repo.
197
+ # @param git_commit [String] The git ref (i.e. SHA, tag, or branch name)
198
+ # @param high_priority [Boolean] If true, add this path at the top of the
199
+ # priority list. Defaults to false, indicating the new path should be
200
+ # at the bottom of the priority list.
201
+ # @param update [Boolean] If the commit is not a SHA, pulls any updates
202
+ # from the remote. Defaults to false, which uses a local cache and does
203
+ # not update if the commit has been fetched previously.
204
+ # @param context_directory [String,nil] The context directory for tools
205
+ # loaded from this source. You can pass a directory path as a string,
206
+ # or `nil` to denote no context. Defaults to `nil`.
207
+ # @return [self]
208
+ #
209
+ def add_git(git_remote, git_path, git_commit,
210
+ high_priority: false,
211
+ update: false,
212
+ context_directory: nil)
213
+ @mutex.synchronize do
214
+ raise "Cannot add a git source after tool loading has started" if @loading_started
215
+ priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
216
+ path = git_cache.find(git_remote, path: git_path, commit: git_commit, update: update)
217
+ source = SourceInfo.create_git_root(git_remote, git_path, git_commit, path, priority,
218
+ context_directory: context_directory,
219
+ data_dir_name: @data_dir_name,
220
+ lib_dir_name: @lib_dir_name)
221
+ @roots_by_priority[priority] = source
123
222
  @worklist << [source, [], priority]
124
223
  end
125
224
  self
@@ -136,7 +235,7 @@ module Toys
136
235
  # that are not part of the tool name and should be passed as tool args.
137
236
  #
138
237
  # @param args [Array<String>] Command line arguments
139
- # @return [Array(Toys::Tool,Array<String>)]
238
+ # @return [Array(Toys::ToolDefinition,Array<String>)]
140
239
  #
141
240
  def lookup(args)
142
241
  orig_prefix, args = @delimiter_handler.find_orig_prefix(args)
@@ -157,13 +256,13 @@ module Toys
157
256
  # the given name, returns `nil`.
158
257
  #
159
258
  # @param words [Array<String>] The tool name
160
- # @return [Toys::Tool] if the tool was found
259
+ # @return [Toys::ToolDefinition] if the tool was found
161
260
  # @return [nil] if no such tool exists
162
261
  #
163
262
  def lookup_specific(words)
164
263
  words = @delimiter_handler.split_path(words.first) if words.size == 1
165
264
  load_for_prefix(words)
166
- tool = get_tool_data(words).cur_definition
265
+ tool = get_tool_data(words, false)&.cur_definition
167
266
  finish_definitions_in_tree(words) if tool
168
267
  tool
169
268
  end
@@ -177,7 +276,7 @@ module Toys
177
276
  # rather than just the immediate children (the default)
178
277
  # @param include_hidden [Boolean] If true, include hidden subtools,
179
278
  # e.g. names beginning with underscores.
180
- # @return [Array<Toys::Tool>] An array of subtools.
279
+ # @return [Array<Toys::ToolDefinition>] An array of subtools.
181
280
  #
182
281
  def list_subtools(words, recursive: false, include_hidden: false)
183
282
  load_for_prefix(words)
@@ -234,8 +333,8 @@ module Toys
234
333
  #
235
334
  # @private
236
335
  #
237
- def get_tool(words, priority)
238
- get_tool_data(words).get_tool(priority, self)
336
+ def get_tool(words, priority, tool_class = nil)
337
+ get_tool_data(words, true).get_tool(priority, self, tool_class)
239
338
  end
240
339
 
241
340
  ##
@@ -248,7 +347,7 @@ module Toys
248
347
  # @private
249
348
  #
250
349
  def activate_tool(words, priority)
251
- get_tool_data(words).activate_tool(priority, self)
350
+ get_tool_data(words, true).activate_tool(priority, self)
252
351
  end
253
352
 
254
353
  ##
@@ -267,10 +366,11 @@ module Toys
267
366
  #
268
367
  # @private
269
368
  #
270
- def build_tool(words, priority)
369
+ def build_tool(words, priority, tool_class = nil)
271
370
  parent = words.empty? ? nil : get_tool(words.slice(0..-2), priority)
272
371
  middleware_stack = parent ? parent.subtool_middleware_stack : @middleware_stack
273
- Tool.new(self, parent, words, priority, middleware_stack, @middleware_lookup)
372
+ ToolDefinition.new(parent, words, priority, @roots_by_priority[priority],
373
+ middleware_stack, @middleware_lookup, tool_class)
274
374
  end
275
375
 
276
376
  ##
@@ -335,12 +435,31 @@ module Toys
335
435
  # @private
336
436
  #
337
437
  def load_path(parent_source, path, words, remaining_words, priority)
438
+ if parent_source.git_remote
439
+ raise LoaderError,
440
+ "Git source #{parent_source.source_name} tried to load from the local file system"
441
+ end
338
442
  source = parent_source.absolute_child(path)
339
443
  @mutex.synchronize do
340
444
  load_validated_path(source, words, remaining_words, priority)
341
445
  end
342
446
  end
343
447
 
448
+ ##
449
+ # Load configuration from the given git remote. This is called from the
450
+ # `load_git` directive in the DSL.
451
+ #
452
+ # @private
453
+ #
454
+ def load_git(parent_source, git_remote, git_path, git_commit, words, remaining_words, priority,
455
+ update: false)
456
+ path = git_cache.find(git_remote, path: git_path, commit: git_commit, update: update)
457
+ source = parent_source.git_child(git_remote, git_path, git_commit, path)
458
+ @mutex.synchronize do
459
+ load_validated_path(source, words, remaining_words, priority)
460
+ end
461
+ end
462
+
344
463
  ##
345
464
  # Load a subtool block. Called from the `tool` directive in the DSL.
346
465
  #
@@ -353,6 +472,18 @@ module Toys
353
472
  end
354
473
  end
355
474
 
475
+ ##
476
+ # Get a GitCache.
477
+ #
478
+ # @private
479
+ #
480
+ def git_cache
481
+ @git_cache ||= begin
482
+ require "toys/utils/git_cache"
483
+ Utils::GitCache.new
484
+ end
485
+ end
486
+
356
487
  ##
357
488
  # Determine the next setting for remaining_words, given a word.
358
489
  #
@@ -381,8 +512,10 @@ module Toys
381
512
  result
382
513
  end
383
514
 
384
- def get_tool_data(words)
385
- @mutex.synchronize { @tool_data[words] ||= ToolData.new(words) }
515
+ def get_tool_data(words, create)
516
+ @mutex.synchronize do
517
+ create ? (@tool_data[words] ||= ToolData.new(words)) : @tool_data[words]
518
+ end
386
519
  end
387
520
 
388
521
  ##
@@ -403,7 +536,7 @@ module Toys
403
536
  if remaining_words
404
537
  update_min_loaded_priority(priority)
405
538
  tool_class = get_tool(words, priority).tool_class
406
- DSL::Tool.prepare(tool_class, remaining_words, source) do
539
+ DSL::Internal.prepare(tool_class, words, priority, remaining_words, source, self) do
407
540
  ContextualError.capture("Error while loading Toys config!") do
408
541
  tool_class.class_eval(&source.source_proc)
409
542
  end
@@ -425,7 +558,7 @@ module Toys
425
558
  if source.source_type == :file
426
559
  update_min_loaded_priority(priority)
427
560
  tool_class = get_tool(words, priority).tool_class
428
- InputFile.evaluate(tool_class, remaining_words, source)
561
+ InputFile.evaluate(tool_class, words, priority, remaining_words, source, self)
429
562
  else
430
563
  do_preload(source.source_path)
431
564
  load_index_in(source, words, remaining_words, priority)
@@ -467,16 +600,20 @@ module Toys
467
600
  if @preload_dir_name
468
601
  preload_dir = ::File.join(path, @preload_dir_name)
469
602
  if ::File.directory?(preload_dir) && ::File.readable?(preload_dir)
470
- ::Dir.entries(preload_dir).each do |child|
471
- next unless ::File.extname(child) == ".rb"
472
- preload_file = ::File.join(preload_dir, child)
473
- next if !::File.file?(preload_file) || !::File.readable?(preload_file)
474
- require preload_file
475
- end
603
+ preload_dir_contents(preload_dir)
476
604
  end
477
605
  end
478
606
  end
479
607
 
608
+ def preload_dir_contents(preload_dir)
609
+ ::Dir.entries(preload_dir).each do |child|
610
+ next unless ::File.extname(child) == ".rb"
611
+ preload_file = ::File.join(preload_dir, child)
612
+ next if !::File.file?(preload_file) || !::File.readable?(preload_file)
613
+ require preload_file
614
+ end
615
+ end
616
+
480
617
  def sort_tools_by_name(tools)
481
618
  tools.sort! do |a, b|
482
619
  a = a.full_name
@@ -520,7 +657,7 @@ module Toys
520
657
  class ToolData
521
658
  # @private
522
659
  def initialize(words)
523
- @words = words
660
+ @words = validate_words(words)
524
661
  @definitions = {}
525
662
  @top_priority = @active_priority = nil
526
663
  @mutex = ::Monitor.new
@@ -537,12 +674,15 @@ module Toys
537
674
  end
538
675
 
539
676
  # @private
540
- def get_tool(priority, loader)
677
+ def get_tool(priority, loader, tool_class = nil)
541
678
  @mutex.synchronize do
542
679
  if @top_priority.nil? || @top_priority < priority
543
680
  @top_priority = priority
544
681
  end
545
- @definitions[priority] ||= loader.build_tool(@words, priority)
682
+ if tool_class && @definitions.include?(priority)
683
+ raise ToolDefinitionError, "Tool already defined for #{@words.inspect}"
684
+ end
685
+ @definitions[priority] ||= loader.build_tool(@words, priority, tool_class)
546
686
  end
547
687
  end
548
688
 
@@ -558,6 +698,14 @@ module Toys
558
698
 
559
699
  private
560
700
 
701
+ def validate_words(words)
702
+ words.each do |word|
703
+ if /[[:cntrl:] #"$&'()*;<>\[\\\]\^`{|}]/.match(word)
704
+ raise ToolDefinitionError, "Illegal characters in name #{word.inspect}"
705
+ end
706
+ end
707
+ end
708
+
561
709
  def top_definition
562
710
  @top_priority ? @definitions[@top_priority] : nil
563
711
  end
@@ -573,32 +721,23 @@ module Toys
573
721
  # @private
574
722
  #
575
723
  class DelimiterHandler
576
- ## @private
577
- ALLOWED_DELIMITERS = %r{^[\./:]*$}.freeze
578
- private_constant :ALLOWED_DELIMITERS
579
-
580
- ## @private
581
724
  def initialize(extra_delimiters)
582
- unless ALLOWED_DELIMITERS =~ extra_delimiters
725
+ unless %r{^[[:space:]./:]*$}.match?(extra_delimiters)
583
726
  raise ::ArgumentError, "Illegal delimiters in #{extra_delimiters.inspect}"
584
727
  end
585
728
  chars = ::Regexp.escape(extra_delimiters.chars.uniq.join)
586
- @extra_delimiters = chars.empty? ? nil : ::Regexp.new("[#{chars}]")
729
+ @delimiters = ::Regexp.new("[[:space:]#{chars}]")
587
730
  end
588
731
 
589
- ## @private
590
732
  def split_path(str)
591
- @extra_delimiters ? str.split(@extra_delimiters) : [str]
733
+ str.split(@delimiters)
592
734
  end
593
735
 
594
- ## @private
595
736
  def find_orig_prefix(args)
596
- if @extra_delimiters
597
- first_split = (args.first || "").split(@extra_delimiters)
598
- if first_split.size > 1
599
- args = first_split + args.slice(1..-1)
600
- return [first_split, args]
601
- end
737
+ first_split = (args.first || "").split(@delimiters)
738
+ if first_split.size > 1
739
+ args = first_split + args.slice(1..-1)
740
+ return [first_split, args]
602
741
  end
603
742
  orig_prefix = args.take_while { |arg| !arg.start_with?("-") }
604
743
  [orig_prefix, args]