toys-core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE.md +16 -24
  4. data/README.md +307 -59
  5. data/docs/guide.md +44 -4
  6. data/lib/toys-core.rb +58 -49
  7. data/lib/toys/acceptor.rb +672 -0
  8. data/lib/toys/alias.rb +106 -0
  9. data/lib/toys/arg_parser.rb +624 -0
  10. data/lib/toys/cli.rb +422 -181
  11. data/lib/toys/compat.rb +83 -0
  12. data/lib/toys/completion.rb +442 -0
  13. data/lib/toys/context.rb +354 -0
  14. data/lib/toys/core_version.rb +18 -26
  15. data/lib/toys/dsl/flag.rb +213 -56
  16. data/lib/toys/dsl/flag_group.rb +237 -51
  17. data/lib/toys/dsl/positional_arg.rb +210 -0
  18. data/lib/toys/dsl/tool.rb +968 -317
  19. data/lib/toys/errors.rb +46 -28
  20. data/lib/toys/flag.rb +821 -0
  21. data/lib/toys/flag_group.rb +282 -0
  22. data/lib/toys/input_file.rb +18 -26
  23. data/lib/toys/loader.rb +110 -100
  24. data/lib/toys/middleware.rb +24 -31
  25. data/lib/toys/mixin.rb +90 -59
  26. data/lib/toys/module_lookup.rb +125 -0
  27. data/lib/toys/positional_arg.rb +184 -0
  28. data/lib/toys/source_info.rb +192 -0
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
  32. data/lib/toys/standard_middleware/show_help.rb +130 -113
  33. data/lib/toys/standard_middleware/show_root_version.rb +29 -35
  34. data/lib/toys/standard_mixins/exec.rb +116 -78
  35. data/lib/toys/standard_mixins/fileutils.rb +16 -24
  36. data/lib/toys/standard_mixins/gems.rb +29 -30
  37. data/lib/toys/standard_mixins/highline.rb +34 -41
  38. data/lib/toys/standard_mixins/terminal.rb +72 -26
  39. data/lib/toys/template.rb +51 -35
  40. data/lib/toys/tool.rb +1161 -206
  41. data/lib/toys/utils/completion_engine.rb +171 -0
  42. data/lib/toys/utils/exec.rb +279 -182
  43. data/lib/toys/utils/gems.rb +58 -49
  44. data/lib/toys/utils/help_text.rb +117 -111
  45. data/lib/toys/utils/terminal.rb +69 -62
  46. data/lib/toys/wrappable_string.rb +162 -0
  47. metadata +24 -22
  48. data/lib/toys/definition/acceptor.rb +0 -191
  49. data/lib/toys/definition/alias.rb +0 -112
  50. data/lib/toys/definition/arg.rb +0 -140
  51. data/lib/toys/definition/flag.rb +0 -370
  52. data/lib/toys/definition/flag_group.rb +0 -205
  53. data/lib/toys/definition/source_info.rb +0 -190
  54. data/lib/toys/definition/tool.rb +0 -842
  55. data/lib/toys/dsl/arg.rb +0 -132
  56. data/lib/toys/runner.rb +0 -188
  57. data/lib/toys/standard_middleware.rb +0 -47
  58. data/lib/toys/utils/module_lookup.rb +0 -135
  59. data/lib/toys/utils/wrappable_string.rb +0 -165
@@ -1,322 +1,1277 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright 2018 Daniel Azuma
3
+ # Copyright 2019 Daniel Azuma
4
4
  #
5
- # All rights reserved.
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:
6
11
  #
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
9
14
  #
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.
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.
30
22
  ;
31
23
 
32
- require "logger"
24
+ require "set"
33
25
 
34
26
  module Toys
35
27
  ##
36
- # This class manages the object context in effect during the execution of a
37
- # tool. The context is a hash of key-value pairs.
38
- #
39
- # Flags and arguments defined by your tool normally report their values in
40
- # the context, using keys that are strings or symbols.
41
- #
42
- # Keys that are neither strings nor symbols are by convention used for other
43
- # context information, including:
44
- #
45
- # * Common information such as the {Toys::Definition::Tool} object being
46
- # executed, the arguments originally passed to it, or the usage error
47
- # string. These well-known keys can be accessed via constants in the
48
- # {Toys::Tool::Keys} module.
49
- # * Common settings such as the verbosity level, and whether to exit
50
- # immediately if a subprocess exits with a nonzero result. These keys are
51
- # also present as {Toys::Context} constants.
52
- # * Private information used internally by middleware and mixins.
53
- #
54
- # This class provides convenience accessors for common keys and settings, and
55
- # you can retrieve argument-set keys using the {#options} hash.
28
+ # A Tool describes a single command that can be invoked using Toys.
29
+ # It has a name, a series of one or more words that you use to identify
30
+ # the tool on the command line. It also has a set of formal flags and
31
+ # command line arguments supported, and a block that gets run when the
32
+ # tool is executed.
56
33
  #
57
34
  class Tool
58
35
  ##
59
- # Well-known context keys.
36
+ # Create a new tool.
37
+ # Should be created only from the DSL via the Loader.
38
+ # @private
60
39
  #
61
- module Keys
62
- ##
63
- # Context key for the currently running CLI.
64
- # @return [Object]
65
- #
66
- CLI = ::Object.new.freeze
40
+ def initialize(loader, parent, full_name, priority, middleware_stack)
41
+ @parent = parent
42
+ @full_name = full_name.dup.freeze
43
+ @priority = priority
44
+ @middleware_stack = middleware_stack
67
45
 
68
- ##
69
- # Context key for the verbosity value. Verbosity is an integer defaulting
70
- # to 0, with higher values meaning more verbose and lower meaning quieter.
71
- # @return [Object]
72
- #
73
- VERBOSITY = ::Object.new.freeze
46
+ @acceptors = {}
47
+ @mixins = {}
48
+ @templates = {}
49
+ @completions = {}
74
50
 
75
- ##
76
- # Context key for the `Toys::Definition::Tool` object being executed.
77
- # @return [Object]
78
- #
79
- TOOL_DEFINITION = ::Object.new.freeze
51
+ reset_definition(loader)
52
+ end
80
53
 
81
- ##
82
- # Context key for the `Toys::Definition::SourceInfo` describing the
83
- # source of this tool.
84
- # @return [Object]
85
- #
86
- TOOL_SOURCE = ::Object.new.freeze
54
+ ##
55
+ # Reset the definition of this tool, deleting all definition data but
56
+ # leaving named acceptors, mixins, and templates intact.
57
+ # Should be called only from the DSL.
58
+ # @private
59
+ #
60
+ def reset_definition(loader)
61
+ @tool_class = DSL::Tool.new_class(@full_name, @priority, loader)
87
62
 
88
- ##
89
- # Context key for the full name of the tool being executed. Value is an
90
- # array of strings.
91
- # @return [Object]
92
- #
93
- TOOL_NAME = ::Object.new.freeze
63
+ @source_info = nil
64
+ @definition_finished = false
94
65
 
95
- ##
96
- # Context key for the active `Toys::Loader` object.
97
- # @return [Object]
98
- #
99
- LOADER = ::Object.new.freeze
66
+ @desc = WrappableString.new("")
67
+ @long_desc = []
100
68
 
101
- ##
102
- # Context key for the active `Logger` object.
103
- # @return [Object]
104
- #
105
- LOGGER = ::Object.new.freeze
69
+ @default_data = {}
70
+ @used_flags = []
71
+ @initializers = []
106
72
 
107
- ##
108
- # Context key for the name of the toys binary. Value is a string.
109
- # @return [Object]
110
- #
111
- BINARY_NAME = ::Object.new.freeze
73
+ default_flag_group = FlagGroup::Base.new(nil, nil, nil)
74
+ @flag_groups = [default_flag_group]
75
+ @flag_group_names = {nil => default_flag_group}
112
76
 
113
- ##
114
- # Context key for the argument list passed to the current tool. Value is
115
- # an array of strings.
116
- # @return [Object]
117
- #
118
- ARGS = ::Object.new.freeze
77
+ @flags = []
78
+ @required_args = []
79
+ @optional_args = []
80
+ @remaining_arg = nil
119
81
 
120
- ##
121
- # Context key for the usage error raised. Value is a string if there was
122
- # an error, or nil if there was no error.
123
- # @return [Object]
124
- #
125
- USAGE_ERROR = ::Object.new.freeze
82
+ @disable_argument_parsing = false
83
+ @enforce_flags_before_args = false
84
+ @require_exact_flag_match = false
85
+ @includes_modules = false
86
+ @custom_context_directory = nil
87
+
88
+ @interrupt_handler = nil
89
+ @usage_error_handler = nil
90
+
91
+ @completion = DefaultCompletion.new
126
92
  end
127
93
 
128
94
  ##
129
- # Create a Context object. Applications generally will not need to create
130
- # these objects directly; they are created by the tool when it is preparing
131
- # for execution.
132
- # @private
95
+ # The name of the tool as an array of strings.
96
+ # This array may not be modified.
97
+ #
98
+ # @return [Array<String>]
99
+ #
100
+ attr_reader :full_name
101
+
102
+ ##
103
+ # The priority of this tool definition.
104
+ #
105
+ # @return [Integer]
106
+ #
107
+ attr_reader :priority
108
+
109
+ ##
110
+ # The tool class.
111
+ #
112
+ # @return [Class]
113
+ #
114
+ attr_reader :tool_class
115
+
116
+ ##
117
+ # The short description string.
118
+ #
119
+ # When reading, this is always returned as a {Toys::WrappableString}.
120
+ #
121
+ # When setting, the description may be provided as any of the following:
122
+ # * A {Toys::WrappableString}.
123
+ # * A normal String, which will be transformed into a
124
+ # {Toys::WrappableString} using spaces as word delimiters.
125
+ # * An Array of String, which will be transformed into a
126
+ # {Toys::WrappableString} where each array element represents an
127
+ # individual word for wrapping.
128
+ #
129
+ # @return [Toys::WrappableString]
130
+ #
131
+ attr_reader :desc
132
+
133
+ ##
134
+ # The long description strings.
135
+ #
136
+ # When reading, this is returned as an Array of {Toys::WrappableString}
137
+ # representing the lines in the description.
138
+ #
139
+ # When setting, the description must be provided as an Array where *each
140
+ # element* may be any of the following:
141
+ # * A {Toys::WrappableString} representing one line.
142
+ # * A normal String representing a line. This will be transformed into a
143
+ # {Toys::WrappableString} using spaces as word delimiters.
144
+ # * An Array of String representing a line. This will be transformed into
145
+ # a {Toys::WrappableString} where each array element represents an
146
+ # individual word for wrapping.
147
+ #
148
+ # @return [Array<Toys::WrappableString>]
149
+ #
150
+ attr_reader :long_desc
151
+
152
+ ##
153
+ # A list of all defined flag groups, in order.
154
+ #
155
+ # @return [Array<Toys::FlagGroup>]
156
+ #
157
+ attr_reader :flag_groups
158
+
159
+ ##
160
+ # A list of all defined flags.
161
+ #
162
+ # @return [Array<Toys::Flag>]
163
+ #
164
+ attr_reader :flags
165
+
166
+ ##
167
+ # A list of all defined required positional arguments.
168
+ #
169
+ # @return [Array<Toys::PositionalArg>]
170
+ #
171
+ attr_reader :required_args
172
+
173
+ ##
174
+ # A list of all defined optional positional arguments.
175
+ #
176
+ # @return [Array<Toys::PositionalArg>]
177
+ #
178
+ attr_reader :optional_args
179
+
180
+ ##
181
+ # The remaining arguments specification.
182
+ #
183
+ # @return [Toys::PositionalArg] The argument definition
184
+ # @return [nil] if remaining arguments are not supported by this tool.
185
+ #
186
+ attr_reader :remaining_arg
187
+
188
+ ##
189
+ # A list of flags that have been used in the flag definitions.
190
+ #
191
+ # @return [Array<String>]
192
+ #
193
+ attr_reader :used_flags
194
+
195
+ ##
196
+ # The default context data set by arguments.
197
+ #
198
+ # @return [Hash]
199
+ #
200
+ attr_reader :default_data
201
+
202
+ ##
203
+ # The middleware stack active for this tool.
204
+ #
205
+ # @return [Array<Toys::Middleware>]
206
+ #
207
+ attr_reader :middleware_stack
208
+
209
+ ##
210
+ # Info on the source of this tool.
211
+ #
212
+ # @return [Toys::SourceInfo] The source info
213
+ # @return [nil] if the source is not defined.
214
+ #
215
+ attr_reader :source_info
216
+
217
+ ##
218
+ # The custom context directory set for this tool.
219
+ #
220
+ # @return [String] The directory path
221
+ # @return [nil] if no custom context directory is set.
222
+ #
223
+ attr_reader :custom_context_directory
224
+
225
+ ##
226
+ # The completion strategy for this tool.
227
+ #
228
+ # When reading, this may return an instance of one of the subclasses of
229
+ # {Toys::Completion::Base}, or a Proc that duck-types it. Generally, this
230
+ # defaults to a {Toys::Tool::DefaultCompletion}, providing a standard
231
+ # algorithm that finds appropriate completions from flags, positional
232
+ # arguments, and subtools.
233
+ #
234
+ # When setting, you may pass any of the following:
235
+ # * `nil` or `:default` which sets the value to a default instance.
236
+ # * A Hash of options to pass to the {Toys::Tool::DefaultCompletion}
237
+ # constructor.
238
+ # * Any other form recognized by {Toys::Completion.create}.
239
+ #
240
+ # @return [Toys::Completion::Base,Proc]
241
+ #
242
+ attr_reader :completion
243
+
244
+ ##
245
+ # The interrupt handler.
246
+ #
247
+ # @return [Proc] The interrupt handler proc
248
+ # @return [Symbol] The name of a method to call
249
+ # @return [nil] if there is no interrupt handler
133
250
  #
134
- # @param [Toys::CLI] cli
135
- # @param [Hash] data
251
+ attr_reader :interrupt_handler
252
+
253
+ ##
254
+ # The usage error handler.
255
+ #
256
+ # @return [Proc] The usage error handler proc
257
+ # @return [Symbol] The name of a method to call
258
+ # @return [nil] if there is no usage error handler
259
+ #
260
+ attr_reader :usage_error_handler
261
+
262
+ ##
263
+ # The local name of this tool, i.e. the last element of the full name.
136
264
  #
137
- def initialize(cli, data)
138
- @__data = data
139
- @__data[Keys::CLI] = cli
140
- @__data[Keys::LOADER] = cli.loader
141
- @__data[Keys::BINARY_NAME] = cli.binary_name
142
- @__data[Keys::LOGGER] = cli.logger
265
+ # @return [String]
266
+ #
267
+ def simple_name
268
+ full_name.last
143
269
  end
144
270
 
145
271
  ##
146
- # Return the currently running CLI.
147
- # @return [Toys::CLI]
272
+ # A displayable name of this tool, generally the full name delimited by
273
+ # spaces.
274
+ #
275
+ # @return [String]
148
276
  #
149
- def cli
150
- @__data[Keys::CLI]
277
+ def display_name
278
+ full_name.join(" ")
151
279
  end
152
280
 
153
281
  ##
154
- # Return the current verbosity setting as an integer.
155
- # @return [Integer]
282
+ # Returns true if this tool is a root tool.
283
+ # @return [Boolean]
156
284
  #
157
- def verbosity
158
- @__data[Keys::VERBOSITY]
285
+ def root?
286
+ full_name.empty?
159
287
  end
160
288
 
161
289
  ##
162
- # Return the tool being executed.
163
- # @return [Toys::Definition::Tool]
290
+ # Returns true if this tool is marked as runnable.
291
+ # @return [Boolean]
164
292
  #
165
- def tool_definition
166
- @__data[Keys::TOOL_DEFINITION]
293
+ def runnable?
294
+ tool_class.public_instance_methods(false).include?(:run)
167
295
  end
168
296
 
169
297
  ##
170
- # Return the source of the tool being executed.
171
- # @return [Toys::Definition::SourceInfo]
298
+ # Returns true if this tool handles interrupts.
299
+ # @return [Boolean]
172
300
  #
173
- def tool_source
174
- @__data[Keys::TOOL_SOURCE]
301
+ def handles_interrupts?
302
+ !interrupt_handler.nil?
175
303
  end
176
304
 
177
305
  ##
178
- # Return the name of the tool being executed, as an array of strings.
179
- # @return [Array[String]]
306
+ # Returns true if this tool handles usage errors.
307
+ # @return [Boolean]
180
308
  #
181
- def tool_name
182
- @__data[Keys::TOOL_NAME]
309
+ def handles_usage_errors?
310
+ !usage_error_handler.nil?
183
311
  end
184
312
 
185
313
  ##
186
- # Return the raw arguments passed to the tool, as an array of strings.
187
- # This does not include the tool name itself.
188
- # @return [Array[String]]
314
+ # Returns true if this tool has at least one included module.
315
+ # @return [Boolean]
189
316
  #
190
- def args
191
- @__data[Keys::ARGS]
317
+ def includes_modules?
318
+ @includes_modules
192
319
  end
193
320
 
194
321
  ##
195
- # Return any usage error detected during argument parsing, or `nil` if
196
- # no error was detected.
197
- # @return [String,nil]
322
+ # Returns true if there is a specific description set for this tool.
323
+ # @return [Boolean]
198
324
  #
199
- def usage_error
200
- @__data[Keys::USAGE_ERROR]
325
+ def includes_description?
326
+ !long_desc.empty? || !desc.empty?
201
327
  end
202
328
 
203
329
  ##
204
- # Return the logger for this execution.
205
- # @return [Logger]
330
+ # Returns true if at least one flag or positional argument is defined
331
+ # for this tool.
332
+ # @return [Boolean]
206
333
  #
207
- def logger
208
- @__data[Keys::LOGGER]
334
+ def includes_arguments?
335
+ !default_data.empty? || !flags.empty? ||
336
+ !required_args.empty? || !optional_args.empty? ||
337
+ !remaining_arg.nil? || flags_before_args_enforced?
209
338
  end
210
339
 
211
340
  ##
212
- # Return the active loader that can be used to get other tools.
213
- # @return [Toys::Loader]
341
+ # Returns true if this tool has any definition information.
342
+ # @return [Boolean]
214
343
  #
215
- def loader
216
- @__data[Keys::LOADER]
344
+ def includes_definition?
345
+ includes_arguments? || runnable? || argument_parsing_disabled? ||
346
+ includes_modules? || includes_description?
217
347
  end
218
348
 
219
349
  ##
220
- # Return the name of the binary that was executed.
221
- # @return [String]
350
+ # Returns true if this tool's definition has been finished and is locked.
351
+ # @return [Boolean]
352
+ #
353
+ def definition_finished?
354
+ @definition_finished
355
+ end
356
+
357
+ ##
358
+ # Returns true if this tool has disabled argument parsing.
359
+ # @return [Boolean]
360
+ #
361
+ def argument_parsing_disabled?
362
+ @disable_argument_parsing
363
+ end
364
+
365
+ ##
366
+ # Returns true if this tool enforces flags before args.
367
+ # @return [Boolean]
368
+ #
369
+ def flags_before_args_enforced?
370
+ @enforce_flags_before_args
371
+ end
372
+
373
+ ##
374
+ # Returns true if this tool requires exact flag matches.
375
+ # @return [Boolean]
376
+ #
377
+ def exact_flag_match_required?
378
+ @require_exact_flag_match
379
+ end
380
+
381
+ ##
382
+ # All arg definitions in order: required, optional, remaining.
383
+ #
384
+ # @return [Array<Toys::PositionalArg>]
385
+ #
386
+ def positional_args
387
+ result = required_args + optional_args
388
+ result << remaining_arg if remaining_arg
389
+ result
390
+ end
391
+
392
+ ##
393
+ # Resolve the given flag given the flag string. Returns an object that
394
+ # describes the resolution result, including whether the resolution
395
+ # matched a unique flag, the specific flag syntax that was matched, and
396
+ # additional information.
397
+ #
398
+ # @param str [String] Flag string
399
+ # @return [Toys::Flag::Resolution]
400
+ #
401
+ def resolve_flag(str)
402
+ result = Flag::Resolution.new(str)
403
+ flags.each do |flag_def|
404
+ result.merge!(flag_def.resolve(str))
405
+ end
406
+ result
407
+ end
408
+
409
+ ##
410
+ # Get the named acceptor from this tool or its ancestors.
411
+ #
412
+ # @param name [String] The acceptor name.
413
+ # @return [Tool::Acceptor::Base] The acceptor.
414
+ # @return [nil] if no acceptor of the given name is found.
415
+ #
416
+ def lookup_acceptor(name)
417
+ @acceptors.fetch(name.to_s) { |k| @parent ? @parent.lookup_acceptor(k) : nil }
418
+ end
419
+
420
+ ##
421
+ # Get the named template from this tool or its ancestors.
422
+ #
423
+ # @param name [String] The template name.
424
+ # @return [Class,nil] The template class.
425
+ # @return [nil] if no template of the given name is found.
426
+ #
427
+ def lookup_template(name)
428
+ @templates.fetch(name.to_s) { |k| @parent ? @parent.lookup_template(k) : nil }
429
+ end
430
+
431
+ ##
432
+ # Get the named mixin from this tool or its ancestors.
433
+ #
434
+ # @param name [String] The mixin name.
435
+ # @return [Module] The mixin module.
436
+ # @return [nil] if no mixin of the given name is found.
437
+ #
438
+ def lookup_mixin(name)
439
+ @mixins.fetch(name.to_s) { |k| @parent ? @parent.lookup_mixin(k) : nil }
440
+ end
441
+
442
+ ##
443
+ # Get the named completion from this tool or its ancestors.
444
+ #
445
+ # @param name [String] The completion name
446
+ # @return [Tool::Completion::Base,Proc] The completion proc.
447
+ # @return [nil] if no completion of the given name is found.
222
448
  #
223
- def binary_name
224
- @__data[Keys::BINARY_NAME]
449
+ def lookup_completion(name)
450
+ @completions.fetch(name.to_s) { |k| @parent ? @parent.lookup_completion(k) : nil }
225
451
  end
226
452
 
227
453
  ##
228
- # Return an option or other piece of data by key.
454
+ # Include the given mixin in the tool class.
229
455
  #
230
- # @param [Symbol] key
231
- # @return [Object]
456
+ # @param name [String,Symbol,Module] The mixin name or module
457
+ # @return [self]
232
458
  #
233
- def [](key)
234
- @__data[key]
459
+ def include_mixin(name)
460
+ tool_class.include(name)
461
+ self
235
462
  end
236
- alias get []
237
463
 
238
464
  ##
239
- # Set an option or other piece of context data by key.
465
+ # Sets the path to the file that defines this tool.
466
+ # A tool may be defined from at most one path. If a different path is
467
+ # already set, raises {Toys::ToolDefinitionError}
240
468
  #
241
- # @param [Symbol] key
242
- # @param [Object] value
469
+ # @param source [Toys::SourceInfo] Source info
470
+ # @return [self]
243
471
  #
244
- def []=(key, value)
245
- @__data[key] = value
472
+ def lock_source(source)
473
+ if source_info && source_info.source != source.source
474
+ raise ToolDefinitionError,
475
+ "Cannot redefine tool #{display_name.inspect} in #{source.source_name}" \
476
+ " (already defined in #{source_info.source_name})"
477
+ end
478
+ @source_info = source
479
+ self
246
480
  end
247
481
 
248
482
  ##
249
- # Set an option or other piece of context data by key.
483
+ # Set the short description string.
484
+ #
485
+ # See {#desc} for details.
250
486
  #
251
- # @param [Symbol] key
252
- # @param [Object] value
487
+ # @param desc [Toys::WrappableString,String,Array<String>]
253
488
  #
254
- def set(key, value = nil)
255
- if key.is_a?(::Hash)
256
- @__data.merge!(key)
489
+ def desc=(desc)
490
+ check_definition_state
491
+ @desc = WrappableString.make(desc)
492
+ end
493
+
494
+ ##
495
+ # Set the long description strings.
496
+ #
497
+ # See {#long_desc} for details.
498
+ #
499
+ # @param long_desc [Array<Toys::WrappableString,String,Array<String>>]
500
+ #
501
+ def long_desc=(long_desc)
502
+ check_definition_state
503
+ @long_desc = WrappableString.make_array(long_desc)
504
+ end
505
+
506
+ ##
507
+ # Append long description strings.
508
+ #
509
+ # You must pass an array of lines in the long description. See {#long_desc}
510
+ # for details on how each line may be represented.
511
+ #
512
+ # @param long_desc [Array<Toys::WrappableString,String,Array<String>>]
513
+ # @return [self]
514
+ #
515
+ def append_long_desc(long_desc)
516
+ check_definition_state
517
+ @long_desc.concat(WrappableString.make_array(long_desc))
518
+ self
519
+ end
520
+
521
+ ##
522
+ # Add a named acceptor to the tool. This acceptor may be refereneced by
523
+ # name when adding a flag or an arg. See {Toys::Acceptor.create} for
524
+ # detailed information on how to specify an acceptor.
525
+ #
526
+ # @param name [String] The name of the acceptor.
527
+ # @param acceptor [Toys::Acceptor::Base,Object] The acceptor to add. You
528
+ # can provide either an acceptor object, or a spec understood by
529
+ # {Toys::Acceptor.create}.
530
+ # @param type_desc [String] Type description string, shown in help.
531
+ # Defaults to the acceptor name.
532
+ # @param block [Proc] Optional block used to create an acceptor. See
533
+ # {Toys::Acceptor.create}.
534
+ # @return [self]
535
+ #
536
+ def add_acceptor(name, acceptor = nil, type_desc: nil, &block)
537
+ name = name.to_s
538
+ if @acceptors.key?(name)
539
+ raise ToolDefinitionError,
540
+ "An acceptor named #{name.inspect} has already been defined in tool" \
541
+ " #{display_name.inspect}."
542
+ end
543
+ @acceptors[name] = Toys::Acceptor.create(acceptor, type_desc: type_desc, &block)
544
+ self
545
+ end
546
+
547
+ ##
548
+ # Add a named mixin module to this tool.
549
+ # You may provide a mixin module or a block that configures one.
550
+ #
551
+ # @param name [String] The name of the mixin.
552
+ # @param mixin_module [Module] The mixin module.
553
+ # @param block [Proc] Define the mixin module here if a `mixin_module` is
554
+ # not provided directly.
555
+ # @return [self]
556
+ #
557
+ def add_mixin(name, mixin_module = nil, &block)
558
+ name = name.to_s
559
+ if @mixins.key?(name)
560
+ raise ToolDefinitionError,
561
+ "A mixin named #{name.inspect} has already been defined in tool" \
562
+ " #{display_name.inspect}."
563
+ end
564
+ @mixins[name] = mixin_module || Mixin.create(&block)
565
+ self
566
+ end
567
+
568
+ ##
569
+ # Add a named completion proc to this tool. The completion may be
570
+ # referenced by name when adding a flag or an arg. See
571
+ # {Toys::Completion.create} for detailed information on how to specify a
572
+ # completion.
573
+ #
574
+ # @param name [String] The name of the completion.
575
+ # @param completion [Proc,Tool::Completion::Base,Object] The completion to
576
+ # add. You can provide either a completion object, or a spec understood
577
+ # by {Toys::Completion.create}.
578
+ # @param options [Hash] Additional options to pass to the completion.
579
+ # @param block [Proc] Optional block used to create a completion. See
580
+ # {Toys::Completion.create}.
581
+ # @return [self]
582
+ #
583
+ def add_completion(name, completion = nil, **options, &block)
584
+ name = name.to_s
585
+ if @completions.key?(name)
586
+ raise ToolDefinitionError,
587
+ "A completion named #{name.inspect} has already been defined in tool" \
588
+ " #{display_name.inspect}."
589
+ end
590
+ @completions[name] = Toys::Completion.create(completion, options, &block)
591
+ self
592
+ end
593
+
594
+ ##
595
+ # Add a named template class to this tool.
596
+ # You may provide a template class or a block that configures one.
597
+ #
598
+ # @param name [String] The name of the template.
599
+ # @param template_class [Class] The template class.
600
+ # @param block [Proc] Define the template class here if a `template_class`
601
+ # is not provided directly.
602
+ # @return [self]
603
+ #
604
+ def add_template(name, template_class = nil, &block)
605
+ name = name.to_s
606
+ if @templates.key?(name)
607
+ raise ToolDefinitionError,
608
+ "A template named #{name.inspect} has already been defined in tool" \
609
+ " #{display_name.inspect}."
610
+ end
611
+ @templates[name] = template_class || Template.create(&block)
612
+ self
613
+ end
614
+
615
+ ##
616
+ # Disable argument parsing for this tool.
617
+ #
618
+ # @return [self]
619
+ #
620
+ def disable_argument_parsing
621
+ check_definition_state
622
+ if includes_arguments?
623
+ raise ToolDefinitionError,
624
+ "Cannot disable argument parsing for tool #{display_name.inspect}" \
625
+ " because arguments have already been defined."
626
+ end
627
+ @disable_argument_parsing = true
628
+ self
629
+ end
630
+
631
+ ##
632
+ # Enforce that flags must come before args for this tool.
633
+ # You may disable enforcement by passoing `false` for the state.
634
+ #
635
+ # @param state [Boolean]
636
+ # @return [self]
637
+ #
638
+ def enforce_flags_before_args(state = true)
639
+ check_definition_state
640
+ if argument_parsing_disabled?
641
+ raise ToolDefinitionError,
642
+ "Cannot enforce flags before args for tool #{display_name.inspect}" \
643
+ " because parsing is disabled."
644
+ end
645
+ @enforce_flags_before_args = state
646
+ self
647
+ end
648
+
649
+ ##
650
+ # Require that flags must match exactly. (If false, flags can match an
651
+ # unambiguous substring.)
652
+ #
653
+ # @param state [Boolean]
654
+ # @return [self]
655
+ #
656
+ def require_exact_flag_match(state = true)
657
+ check_definition_state
658
+ if argument_parsing_disabled?
659
+ raise ToolDefinitionError,
660
+ "Cannot require exact flag match for tool" \
661
+ " #{display_name.inspect} because parsing is disabled."
662
+ end
663
+ @require_exact_flag_match = state
664
+ self
665
+ end
666
+
667
+ ##
668
+ # Add a flag group to the group list.
669
+ #
670
+ # The type should be one of the following symbols:
671
+ # * `:optional` All flags in the group are optional
672
+ # * `:required` All flags in the group are required
673
+ # * `:exactly_one` Exactly one flag in the group must be provided
674
+ # * `:at_least_one` At least one flag in the group must be provided
675
+ # * `:at_most_one` At most one flag in the group must be provided
676
+ #
677
+ # @param type [Symbol] The type of group. Default is `:optional`.
678
+ # @param desc [String,Array<String>,Toys::WrappableString] Short
679
+ # description for the group. See {Toys::Tool#desc=} for a description
680
+ # of allowed formats. Defaults to `"Flags"`.
681
+ # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
682
+ # Long description for the flag group. See {Toys::Tool#long_desc=} for
683
+ # a description of allowed formats. Defaults to the empty array.
684
+ # @param name [String,Symbol,nil] The name of the group, or nil for no
685
+ # name.
686
+ # @param report_collisions [Boolean] If `true`, raise an exception if a
687
+ # the given name is already taken. If `false`, ignore. Default is
688
+ # `true`.
689
+ # @param prepend [Boolean] If `true`, prepend rather than append the
690
+ # group to the list. Default is `false`.
691
+ # @return [self]
692
+ #
693
+ def add_flag_group(type: :optional, desc: nil, long_desc: nil,
694
+ name: nil, report_collisions: true, prepend: false)
695
+ if !name.nil? && @flag_group_names.key?(name)
696
+ return self unless report_collisions
697
+ raise ToolDefinitionError, "Flag group #{name} already exists"
698
+ end
699
+ group = FlagGroup.create(type: type, name: name, desc: desc, long_desc: long_desc)
700
+ @flag_group_names[name] = group unless name.nil?
701
+ if prepend
702
+ @flag_groups.unshift(group)
257
703
  else
258
- @__data[key] = value
704
+ @flag_groups.push(group)
259
705
  end
260
706
  self
261
707
  end
262
708
 
263
709
  ##
264
- # Returns the subset of the context that uses string or symbol keys. By
265
- # convention, this includes keys that are set by tool flags and arguments,
266
- # but does not include well-known context values such as verbosity or
267
- # private context values used by middleware or mixins.
710
+ # Add a flag to the current tool. Each flag must specify a key which
711
+ # the script may use to obtain the flag value from the context.
712
+ # You may then provide the flags themselves in `OptionParser` form.
268
713
  #
269
- # @return [Hash]
714
+ # @param key [String,Symbol] The key to use to retrieve the value from
715
+ # the execution context.
716
+ # @param flags [Array<String>] The flags in OptionParser format. If empty,
717
+ # a flag will be inferred from the key.
718
+ # @param accept [Object] An acceptor that validates and/or converts the
719
+ # value. You may provide either the name of an acceptor you have
720
+ # defined, or one of the default acceptors provided by OptionParser.
721
+ # Optional. If not specified, accepts any value as a string.
722
+ # @param default [Object] The default value. This is the value that will
723
+ # be set in the context if this flag is not provided on the command
724
+ # line. Defaults to `nil`.
725
+ # @param handler [Proc,nil,:set,:push] An optional handler for
726
+ # setting/updating the value. A handler is a proc taking two
727
+ # arguments, the given value and the previous value, returning the
728
+ # new value that should be set. You may also specify a predefined
729
+ # named handler. The `:set` handler (the default) replaces the
730
+ # previous value (effectively `-> (val, _prev) { val }`). The
731
+ # `:push` handler expects the previous value to be an array and
732
+ # pushes the given value onto it; it should be combined with setting
733
+ # `default: []` and is intended for "multi-valued" flags.
734
+ # @param complete_flags [Object] A specifier for shell tab completion
735
+ # for flag names associated with this flag. By default, a
736
+ # {Toys::Flag::DefaultCompletion} is used, which provides the flag's
737
+ # names as completion candidates. To customize completion, set this to
738
+ # a hash of options to pass to the constructor for
739
+ # {Toys::Flag::DefaultCompletion}, or pass any other spec recognized
740
+ # by {Toys::Completion.create}.
741
+ # @param complete_values [Object] A specifier for shell tab completion
742
+ # for flag values associated with this flag. Pass any spec
743
+ # recognized by {Toys::Completion.create}.
744
+ # @param report_collisions [Boolean] Raise an exception if a flag is
745
+ # requested that is already in use or marked as disabled. Default is
746
+ # true.
747
+ # @param group [Toys::FlagGroup,String,Symbol,nil] Group for
748
+ # this flag. You may provide a group name, a FlagGroup object, or
749
+ # `nil` which denotes the default group.
750
+ # @param desc [String,Array<String>,Toys::WrappableString] Short
751
+ # description for the flag. See {Toys::Tool#desc=} for a description of
752
+ # allowed formats. Defaults to the empty string.
753
+ # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
754
+ # Long description for the flag. See {Toys::Tool#long_desc=} for a
755
+ # description of allowed formats. Defaults to the empty array.
756
+ # @param display_name [String] A display name for this flag, used in help
757
+ # text and error messages.
758
+ # @return [self]
270
759
  #
271
- def options
272
- @__data.select do |k, _v|
273
- k.is_a?(::Symbol) || k.is_a?(::String)
760
+ def add_flag(key, flags = [],
761
+ accept: nil, default: nil, handler: nil, complete_flags: nil,
762
+ complete_values: nil, report_collisions: true, group: nil, desc: nil,
763
+ long_desc: nil, display_name: nil)
764
+ unless group.is_a?(FlagGroup::Base)
765
+ group_name = group
766
+ group = @flag_group_names[group_name]
767
+ raise ToolDefinitionError, "No such flag group: #{group_name.inspect}" if group.nil?
274
768
  end
769
+ check_definition_state(is_arg: true)
770
+ accept = resolve_acceptor_name(accept)
771
+ complete_flags = resolve_completion_name(complete_flags)
772
+ complete_values = resolve_completion_name(complete_values)
773
+ flag_def = Flag.new(key, flags, @used_flags, report_collisions, accept, handler, default,
774
+ complete_flags, complete_values, desc, long_desc, display_name, group)
775
+ if flag_def.active?
776
+ @flags << flag_def
777
+ group << flag_def
778
+ end
779
+ @default_data[key] = default
780
+ self
781
+ end
782
+
783
+ ##
784
+ # Mark one or more flags as disabled, preventing their use by any
785
+ # subsequent flag definition. This may be used to prevent middleware from
786
+ # defining a particular flag.
787
+ #
788
+ # @param flags [String...] The flags to disable
789
+ # @return [self]
790
+ #
791
+ def disable_flag(*flags)
792
+ check_definition_state(is_arg: true)
793
+ flags = flags.uniq
794
+ intersection = @used_flags & flags
795
+ unless intersection.empty?
796
+ raise ToolDefinitionError, "Cannot disable flags already used: #{intersection.inspect}"
797
+ end
798
+ @used_flags.concat(flags)
799
+ self
800
+ end
801
+
802
+ ##
803
+ # Add a required positional argument to the current tool. You must specify
804
+ # a key which the script may use to obtain the argument value from the
805
+ # context.
806
+ #
807
+ # @param key [String,Symbol] The key to use to retrieve the value from
808
+ # the execution context.
809
+ # @param accept [Object] An acceptor that validates and/or converts the
810
+ # value. You may provide either the name of an acceptor you have
811
+ # defined, or one of the default acceptors provided by OptionParser.
812
+ # Optional. If not specified, accepts any value as a string.
813
+ # @param complete [Object] A specifier for shell tab completion. See
814
+ # {Toys::Completion.create} for recognized formats.
815
+ # @param display_name [String] A name to use for display (in help text and
816
+ # error reports). Defaults to the key in upper case.
817
+ # @param desc [String,Array<String>,Toys::WrappableString] Short
818
+ # description for the arg. See {Toys::Tool#desc=} for a description of
819
+ # allowed formats. Defaults to the empty string.
820
+ # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
821
+ # Long description for the arg. See {Toys::Tool#long_desc=} for a
822
+ # description of allowed formats. Defaults to the empty array.
823
+ # @return [self]
824
+ #
825
+ def add_required_arg(key, accept: nil, complete: nil, display_name: nil,
826
+ desc: nil, long_desc: nil)
827
+ check_definition_state(is_arg: true)
828
+ accept = resolve_acceptor_name(accept)
829
+ complete = resolve_completion_name(complete)
830
+ arg_def = PositionalArg.new(key, :required, accept, nil, complete,
831
+ desc, long_desc, display_name)
832
+ @required_args << arg_def
833
+ self
834
+ end
835
+
836
+ ##
837
+ # Add an optional positional argument to the current tool. You must specify
838
+ # a key which the script may use to obtain the argument value from the
839
+ # context. If an optional argument is not given on the command line, the
840
+ # value is set to the given default.
841
+ #
842
+ # @param key [String,Symbol] The key to use to retrieve the value from
843
+ # the execution context.
844
+ # @param default [Object] The default value. This is the value that will
845
+ # be set in the context if this argument is not provided on the command
846
+ # line. Defaults to `nil`.
847
+ # @param accept [Object] An acceptor that validates and/or converts the
848
+ # value. You may provide either the name of an acceptor you have
849
+ # defined, or one of the default acceptors provided by OptionParser.
850
+ # Optional. If not specified, accepts any value as a string.
851
+ # @param complete [Object] A specifier for shell tab completion. See
852
+ # {Toys::Completion.create} for recognized formats.
853
+ # @param display_name [String] A name to use for display (in help text and
854
+ # error reports). Defaults to the key in upper case.
855
+ # @param desc [String,Array<String>,Toys::WrappableString] Short
856
+ # description for the arg. See {Toys::Tool#desc=} for a description of
857
+ # allowed formats. Defaults to the empty string.
858
+ # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
859
+ # Long description for the arg. See {Toys::Tool#long_desc=} for a
860
+ # description of allowed formats. Defaults to the empty array.
861
+ # @return [self]
862
+ #
863
+ def add_optional_arg(key, default: nil, accept: nil, complete: nil,
864
+ display_name: nil, desc: nil, long_desc: nil)
865
+ check_definition_state(is_arg: true)
866
+ accept = resolve_acceptor_name(accept)
867
+ complete = resolve_completion_name(complete)
868
+ arg_def = PositionalArg.new(key, :optional, accept, default, complete,
869
+ desc, long_desc, display_name)
870
+ @optional_args << arg_def
871
+ @default_data[key] = default
872
+ self
275
873
  end
276
874
 
277
875
  ##
278
- # Find the given data file or directory in this tool's search path.
876
+ # Specify what should be done with unmatched positional arguments. You must
877
+ # specify a key which the script may use to obtain the remaining args
878
+ # from the context.
279
879
  #
280
- # @param [String] path The path to find
281
- # @param [nil,:file,:directory] type Type of file system object to find,
282
- # or nil to return any type.
283
- # @return [String,nil] Absolute path of the result, or nil if not found.
880
+ # @param key [String,Symbol] The key to use to retrieve the value from
881
+ # the execution context.
882
+ # @param default [Object] The default value. This is the value that will
883
+ # be set in the context if no unmatched arguments are provided on the
884
+ # command line. Defaults to the empty array `[]`.
885
+ # @param accept [Object] An acceptor that validates and/or converts the
886
+ # value. You may provide either the name of an acceptor you have
887
+ # defined, or one of the default acceptors provided by OptionParser.
888
+ # Optional. If not specified, accepts any value as a string.
889
+ # @param complete [Object] A specifier for shell tab completion. See
890
+ # {Toys::Completion.create} for recognized formats.
891
+ # @param display_name [String] A name to use for display (in help text and
892
+ # error reports). Defaults to the key in upper case.
893
+ # @param desc [String,Array<String>,Toys::WrappableString] Short
894
+ # description for the arg. See {Toys::Tool#desc=} for a description of
895
+ # allowed formats. Defaults to the empty string.
896
+ # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
897
+ # Long description for the arg. See {Toys::Tool#long_desc=} for a
898
+ # description of allowed formats. Defaults to the empty array.
899
+ # @return [self]
284
900
  #
285
- def find_data(path, type: nil)
286
- @__data[Keys::TOOL_SOURCE].find_data(path, type: type)
901
+ def set_remaining_args(key, default: [], accept: nil, complete: nil,
902
+ display_name: nil, desc: nil, long_desc: nil)
903
+ check_definition_state(is_arg: true)
904
+ accept = resolve_acceptor_name(accept)
905
+ complete = resolve_completion_name(complete)
906
+ arg_def = PositionalArg.new(key, :remaining, accept, default, complete,
907
+ desc, long_desc, display_name)
908
+ @remaining_arg = arg_def
909
+ @default_data[key] = default
910
+ self
287
911
  end
288
912
 
289
913
  ##
290
- # Return the context directory for this tool. Generally, this defaults
291
- # to the directory containing the toys config directory structure being
292
- # read, but it may be changed by setting a different context directory
293
- # for the tool.
294
- # May return nil if there is no context.
914
+ # Set the run handler block
295
915
  #
296
- # @return [String,nil] Context directory
916
+ # @param proc [Proc] The runnable block
917
+ #
918
+ def run_handler=(proc)
919
+ check_definition_state
920
+ @tool_class.to_run(&proc)
921
+ end
922
+
923
+ ##
924
+ # Set the interrupt handler.
925
+ #
926
+ # @param handler [Proc,Symbol] The interrupt handler
927
+ #
928
+ def interrupt_handler=(handler)
929
+ check_definition_state
930
+ if !handler.is_a?(::Proc) && !handler.is_a?(::Symbol) && !handler.nil?
931
+ raise ToolDefinitionError, "Interrupt handler must be a proc or symbol"
932
+ end
933
+ @interrupt_handler = handler
934
+ end
935
+
936
+ ##
937
+ # Set the usage error handler.
938
+ #
939
+ # @param handler [Proc,Symbol] The usage error handler
940
+ #
941
+ def usage_error_handler=(handler)
942
+ check_definition_state
943
+ if !handler.is_a?(::Proc) && !handler.is_a?(::Symbol) && !handler.nil?
944
+ raise ToolDefinitionError, "Usage error handler must be a proc or symbol"
945
+ end
946
+ @usage_error_handler = handler
947
+ end
948
+
949
+ ##
950
+ # Add an initializer.
951
+ #
952
+ # @param proc [Proc] The initializer block
953
+ # @param args [Object...] Arguments to pass to the initializer
954
+ # @return [self]
955
+ #
956
+ def add_initializer(proc, *args)
957
+ check_definition_state
958
+ @initializers << [proc, args]
959
+ self
960
+ end
961
+
962
+ ##
963
+ # Set the custom context directory.
964
+ #
965
+ # See {#custom_context_directory} for details.
966
+ #
967
+ # @param dir [String]
968
+ #
969
+ def custom_context_directory=(dir)
970
+ check_definition_state
971
+ @custom_context_directory = dir
972
+ end
973
+
974
+ ##
975
+ # Set the completion strategy for this Tool.
976
+ #
977
+ # See {#completion} for details.
978
+ #
979
+ # @param spec [Object]
980
+ #
981
+ def completion=(spec)
982
+ @completion =
983
+ case spec
984
+ when nil, :default
985
+ DefaultCompletion.new
986
+ when ::Hash
987
+ DefaultCompletion.new(spec)
988
+ else
989
+ Completion.create(spec)
990
+ end
991
+ end
992
+
993
+ ##
994
+ # Return the effective context directory.
995
+ # If there is a custom context directory, uses that. Otherwise, looks for
996
+ # a custom context directory up the tool ancestor chain. If none is
997
+ # found, uses the default context directory from the source info. It is
998
+ # possible for there to be no context directory at all, in which case,
999
+ # returns nil.
1000
+ #
1001
+ # @return [String] The effective context directory path.
1002
+ # @return [nil] if there is no effective context directory.
297
1003
  #
298
1004
  def context_directory
299
- @__data[Keys::TOOL_DEFINITION].context_directory
1005
+ lookup_custom_context_directory || source_info&.context_directory
300
1006
  end
301
1007
 
302
1008
  ##
303
- # Exit immediately with the given status code
1009
+ # Lookup the custom context directory in this tool and its ancestors.
1010
+ # @private
304
1011
  #
305
- # @param [Integer] code The status code, which should be 0 for no error,
306
- # or nonzero for an error condition. Default is 0.
1012
+ def lookup_custom_context_directory
1013
+ custom_context_directory || @parent&.lookup_custom_context_directory
1014
+ end
1015
+
1016
+ ## @private
1017
+ def scalar_acceptor(spec = nil, type_desc: nil, &block)
1018
+ Acceptor.create(resolve_acceptor_name(spec), type_desc: type_desc, &block)
1019
+ end
1020
+
1021
+ ## @private
1022
+ def scalar_completion(spec = nil, **options, &block)
1023
+ if spec.nil? && block.nil? || spec == :default
1024
+ options
1025
+ else
1026
+ Completion.create(resolve_completion_name(spec), options, &block)
1027
+ end
1028
+ end
1029
+
1030
+ ##
1031
+ # Mark this tool as having at least one module included.
1032
+ # @private
307
1033
  #
308
- def exit(code = 0)
309
- throw :result, code
1034
+ def mark_includes_modules
1035
+ check_definition_state
1036
+ @includes_modules = true
1037
+ self
310
1038
  end
311
1039
 
312
1040
  ##
313
- # Exit immediately with the given status code
1041
+ # Complete definition and run middleware configs. Should be called from
1042
+ # the Loader only.
1043
+ # @private
314
1044
  #
315
- # @param [Integer] code The status code, which should be 0 for no error,
316
- # or nonzero for an error condition. Default is 0.
1045
+ def finish_definition(loader)
1046
+ unless @definition_finished
1047
+ ContextualError.capture("Error installing tool middleware!", tool_name: full_name) do
1048
+ config_proc = proc {}
1049
+ middleware_stack.reverse_each do |middleware|
1050
+ config_proc = make_config_proc(middleware, loader, config_proc)
1051
+ end
1052
+ config_proc.call
1053
+ end
1054
+ flag_groups.each do |flag_group|
1055
+ flag_group.flags.sort_by!(&:sort_str)
1056
+ end
1057
+ @definition_finished = true
1058
+ end
1059
+ self
1060
+ end
1061
+
1062
+ ##
1063
+ # Run all initializers against a context. Called from the Runner.
1064
+ # @private
1065
+ #
1066
+ def run_initializers(context)
1067
+ @initializers.each do |func, args|
1068
+ context.instance_exec(*args, &func)
1069
+ end
1070
+ end
1071
+
1072
+ ##
1073
+ # Check that the tool can still be defined. Should be called internally
1074
+ # or from the DSL only.
1075
+ # @private
317
1076
  #
318
- def self.exit(code = 0)
319
- throw :result, code
1077
+ def check_definition_state(is_arg: false)
1078
+ if @definition_finished
1079
+ raise ToolDefinitionError,
1080
+ "Defintion of tool #{display_name.inspect} is already finished"
1081
+ end
1082
+ if is_arg && argument_parsing_disabled?
1083
+ raise ToolDefinitionError,
1084
+ "Tool #{display_name.inspect} has disabled argument parsing"
1085
+ end
1086
+ self
1087
+ end
1088
+
1089
+ ##
1090
+ # A Completion that implements the default algorithm for a tool.
1091
+ #
1092
+ class DefaultCompletion < Completion::Base
1093
+ ##
1094
+ # Create a completion given configuration options.
1095
+ #
1096
+ # @param complete_subtools [Boolean] Whether to complete subtool names
1097
+ # @param include_hidden_subtools [Boolean] Whether to include hidden
1098
+ # subtools (i.e. those beginning with an underscore)
1099
+ # @param complete_args [Boolean] Whether to complete positional args
1100
+ # @param complete_flags [Boolean] Whether to complete flag names
1101
+ # @param complete_flag_values [Boolean] Whether to complete flag values
1102
+ #
1103
+ def initialize(complete_subtools: true, include_hidden_subtools: false,
1104
+ complete_args: true, complete_flags: true, complete_flag_values: true)
1105
+ @complete_subtools = complete_subtools
1106
+ @include_hidden_subtools = include_hidden_subtools
1107
+ @complete_flags = complete_flags
1108
+ @complete_args = complete_args
1109
+ @complete_flag_values = complete_flag_values
1110
+ end
1111
+
1112
+ ##
1113
+ # Whether to complete subtool names
1114
+ # @return [Boolean]
1115
+ #
1116
+ def complete_subtools?
1117
+ @complete_subtools
1118
+ end
1119
+
1120
+ ##
1121
+ # Whether to include hidden subtools
1122
+ # @return [Boolean]
1123
+ #
1124
+ def include_hidden_subtools?
1125
+ @include_hidden_subtools
1126
+ end
1127
+
1128
+ ##
1129
+ # Whether to complete flags
1130
+ # @return [Boolean]
1131
+ #
1132
+ def complete_flags?
1133
+ @complete_flags
1134
+ end
1135
+
1136
+ ##
1137
+ # Whether to complete positional args
1138
+ # @return [Boolean]
1139
+ #
1140
+ def complete_args?
1141
+ @complete_args
1142
+ end
1143
+
1144
+ ##
1145
+ # Whether to complete flag values
1146
+ # @return [Boolean]
1147
+ #
1148
+ def complete_flag_values?
1149
+ @complete_flag_values
1150
+ end
1151
+
1152
+ ##
1153
+ # Returns candidates for the current completion.
1154
+ #
1155
+ # @param context [Toys::Completion::Context] the current completion
1156
+ # context including the string fragment.
1157
+ # @return [Array<Toys::Completion::Candidate>] an array of candidates
1158
+ #
1159
+ def call(context)
1160
+ candidates = valued_flag_candidates(context)
1161
+ return candidates if candidates
1162
+ candidates = subtool_or_arg_candidates(context)
1163
+ candidates += plain_flag_candidates(context)
1164
+ candidates += flag_value_candidates(context)
1165
+ candidates
1166
+ end
1167
+
1168
+ private
1169
+
1170
+ def valued_flag_candidates(context)
1171
+ return unless @complete_flag_values
1172
+ arg_parser = context.arg_parser
1173
+ return unless arg_parser.flags_allowed?
1174
+ active_flag_def = arg_parser.active_flag_def
1175
+ return if active_flag_def && active_flag_def.value_type == :required
1176
+ match = /\A(--\w[\?\w-]*)=(.*)\z/.match(context.fragment_prefix)
1177
+ return unless match
1178
+ flag_value_context = context.with(fragment_prefix: match[2])
1179
+ flag_def = flag_value_context.tool.resolve_flag(match[1]).unique_flag
1180
+ return [] unless flag_def
1181
+ flag_def.value_completion.call(flag_value_context)
1182
+ end
1183
+
1184
+ def subtool_or_arg_candidates(context)
1185
+ return [] if context.arg_parser.active_flag_def
1186
+ return [] if context.arg_parser.flags_allowed? && context.fragment.start_with?("-")
1187
+ subtool_candidates(context) || arg_candidates(context)
1188
+ end
1189
+
1190
+ def subtool_candidates(context)
1191
+ return if !@complete_subtools || !context.args.empty?
1192
+ tool_name, prefix, fragment = analyze_subtool_fragment(context)
1193
+ return unless tool_name
1194
+ subtools = context.cli.loader.list_subtools(tool_name,
1195
+ include_hidden: @include_hidden_subtools)
1196
+ return if subtools.empty?
1197
+ candidates = []
1198
+ subtools.each do |subtool|
1199
+ name = subtool.simple_name
1200
+ candidates << Completion::Candidate.new("#{prefix}#{name}") if name.start_with?(fragment)
1201
+ end
1202
+ candidates
1203
+ end
1204
+
1205
+ def analyze_subtool_fragment(context)
1206
+ tool_name = context.tool.full_name
1207
+ prefix = ""
1208
+ fragment = context.fragment
1209
+ delims = context.cli.extra_delimiters
1210
+ unless context.fragment_prefix.empty?
1211
+ if !context.fragment_prefix.end_with?(":") || !delims.include?(":")
1212
+ return [nil, nil, nil]
1213
+ end
1214
+ tool_name += context.fragment_prefix.split(":")
1215
+ end
1216
+ unless delims.empty?
1217
+ delims_regex = ::Regexp.escape(delims)
1218
+ if (match = /\A((.+)[#{delims_regex}])(.*)\z/.match(fragment))
1219
+ fragment = match[3]
1220
+ tool_name += match[2].split(/[#{delims_regex}]/)
1221
+ prefix = match[1]
1222
+ end
1223
+ end
1224
+ [tool_name, prefix, fragment]
1225
+ end
1226
+
1227
+ def arg_candidates(context)
1228
+ return unless @complete_args
1229
+ arg_def = context.arg_parser.next_arg_def
1230
+ return [] unless arg_def
1231
+ arg_def.completion.call(context)
1232
+ end
1233
+
1234
+ def plain_flag_candidates(context)
1235
+ return [] if !@complete_flags || context[:disable_flags]
1236
+ arg_parser = context.arg_parser
1237
+ return [] unless arg_parser.flags_allowed?
1238
+ flag_def = arg_parser.active_flag_def
1239
+ return [] if flag_def && flag_def.value_type == :required
1240
+ return [] if context.fragment =~ /\A[^-]/ || !context.fragment_prefix.empty?
1241
+ context.tool.flags.flat_map do |flag|
1242
+ flag.flag_completion.call(context)
1243
+ end
1244
+ end
1245
+
1246
+ def flag_value_candidates(context)
1247
+ return unless @complete_flag_values
1248
+ arg_parser = context.arg_parser
1249
+ flag_def = arg_parser.active_flag_def
1250
+ return [] unless flag_def
1251
+ return [] if @complete_flags && arg_parser.flags_allowed? &&
1252
+ flag_def.value_type == :optional && context.fragment.start_with?("-")
1253
+ flag_def.value_completion.call(context)
1254
+ end
1255
+ end
1256
+
1257
+ private
1258
+
1259
+ def make_config_proc(middleware, loader, next_config)
1260
+ proc { middleware.config(self, loader, &next_config) }
1261
+ end
1262
+
1263
+ def resolve_acceptor_name(name)
1264
+ return name unless name.is_a?(::String)
1265
+ accept = lookup_acceptor(name)
1266
+ raise ToolDefinitionError, "Unknown acceptor: #{name.inspect}" if accept.nil?
1267
+ accept
1268
+ end
1269
+
1270
+ def resolve_completion_name(name)
1271
+ return name unless name.is_a?(::String)
1272
+ completion = lookup_completion(name)
1273
+ raise ToolDefinitionError, "Unknown completion: #{name.inspect}" if completion.nil?
1274
+ completion
320
1275
  end
321
1276
  end
322
1277
  end