toys-core 0.7.0 → 0.8.0

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