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,41 +1,59 @@
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
24
  module Toys
33
25
  ##
34
- # An exception indicating an error in a tool definition
26
+ # An exception indicating an error in a tool definition.
35
27
  #
36
28
  class ToolDefinitionError < ::StandardError
37
29
  end
38
30
 
31
+ ##
32
+ # An exception indicating that a tool has no run method.
33
+ #
34
+ class NotRunnableError < ::StandardError
35
+ end
36
+
37
+ ##
38
+ # An exception indicating problems parsing arguments.
39
+ #
40
+ class ArgParsingError < ::StandardError
41
+ ##
42
+ # Create an ArgParsingError given a set of error messages
43
+ # @param errors [Array<Toys::ArgParser::UsageError>]
44
+ #
45
+ def initialize(errors)
46
+ @usage_errors = errors
47
+ super(errors.join("\n"))
48
+ end
49
+
50
+ ##
51
+ # The individual usage error messages.
52
+ # @return [Array<Toys::ArgParser::UsageError>]
53
+ #
54
+ attr_reader :usage_errors
55
+ end
56
+
39
57
  ##
40
58
  # An exception indicating a problem during tool lookup
41
59
  #
@@ -76,8 +94,8 @@ module Toys
76
94
  add_config_path_if_missing(e, path)
77
95
  raise e
78
96
  rescue ::SyntaxError => e
79
- if e.message =~ /#{::Regexp.escape(path)}:(\d+)/
80
- opts = opts.merge(config_path: path, config_line: $1.to_i)
97
+ if (match = /#{::Regexp.escape(path)}:(\d+)/.match(e.message))
98
+ opts = opts.merge(config_path: path, config_line: match[1].to_i)
81
99
  e = ContextualError.new(e, banner, opts)
82
100
  end
83
101
  raise e
@@ -108,7 +126,7 @@ module Toys
108
126
 
109
127
  def add_config_path_if_missing(error, path)
110
128
  if error.config_path.nil? && error.config_line.nil?
111
- l = error.cause.backtrace_locations.find { |b| b.absolute_path == path }
129
+ l = (error.cause.backtrace_locations || []).find { |b| b.absolute_path == path }
112
130
  if l
113
131
  error.config_path = path
114
132
  error.config_line = l.lineno
@@ -0,0 +1,821 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2019 Daniel Azuma
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21
+ # IN THE SOFTWARE.
22
+ ;
23
+
24
+ module Toys
25
+ ##
26
+ # Representation of a formal set of flags that set a particular context
27
+ # key. The flags within a single Flag definition are synonyms.
28
+ #
29
+ class Flag
30
+ ##
31
+ # The set handler replaces the previous value.
32
+ # @return [Proc]
33
+ #
34
+ SET_HANDLER = ->(val, _prev) { val }
35
+
36
+ ##
37
+ # The push handler pushes the given value using the `<<` operator.
38
+ # @return [Proc]
39
+ #
40
+ PUSH_HANDLER = ->(val, prev) { prev.nil? ? [val] : prev << val }
41
+
42
+ ##
43
+ # The default handler is the set handler, replacing the previous value.
44
+ # @return [Proc]
45
+ #
46
+ DEFAULT_HANDLER = SET_HANDLER
47
+
48
+ ##
49
+ # Create a Flag definition.
50
+ # This argument list is subject to change. Use {Toys::Flag.create} instead
51
+ # for a more stable interface.
52
+ # @private
53
+ #
54
+ def initialize(key, flags, used_flags, report_collisions, acceptor, handler, default,
55
+ flag_completion, value_completion, desc, long_desc, display_name, group)
56
+ @group = group
57
+ @key = key
58
+ @flag_syntax = Array(flags).map { |s| Syntax.new(s) }
59
+ @acceptor = Acceptor.create(acceptor)
60
+ @handler = resolve_handler(handler)
61
+ @desc = WrappableString.make(desc)
62
+ @long_desc = WrappableString.make_array(long_desc)
63
+ @default = default
64
+ @flag_completion = create_flag_completion(flag_completion)
65
+ @value_completion = Completion.create(value_completion)
66
+ create_default_flag if @flag_syntax.empty?
67
+ remove_used_flags(used_flags, report_collisions)
68
+ canonicalize
69
+ summarize(display_name)
70
+ end
71
+
72
+ ##
73
+ # Create a flag definition.
74
+ #
75
+ # @param key [String,Symbol] The key to use to retrieve the value from
76
+ # the execution context.
77
+ # @param flags [Array<String>] The flags in OptionParser format. If empty,
78
+ # a flag will be inferred from the key.
79
+ # @param accept [Object] An acceptor that validates and/or converts the
80
+ # value. See {Toys::Acceptor.create} for recognized formats. Optional.
81
+ # If not specified, defaults to {Toys::Acceptor::DEFAULT}.
82
+ # @param default [Object] The default value. This is the value that will
83
+ # be set in the context if this flag is not provided on the command
84
+ # line. Defaults to `nil`.
85
+ # @param handler [Proc,nil,:set,:push] An optional handler for
86
+ # setting/updating the value. A handler is a proc taking two
87
+ # arguments, the given value and the previous value, returning the
88
+ # new value that should be set. You may also specify a predefined
89
+ # named handler. The `:set` handler (the default) replaces the
90
+ # previous value (effectively `-> (val, _prev) { val }`). The
91
+ # `:push` handler expects the previous value to be an array and
92
+ # pushes the given value onto it; it should be combined with setting
93
+ # `default: []` and is intended for "multi-valued" flags.
94
+ # @param complete_flags [Object] A specifier for shell tab completion for
95
+ # flag names associated with this flag. By default, a
96
+ # {Toys::Flag::DefaultCompletion} is used, which provides the flag's
97
+ # names as completion candidates. To customize completion, set this to
98
+ # a hash of options to pass to the constructor for
99
+ # {Toys::Flag::DefaultCompletion}, or pass any other spec recognized
100
+ # by {Toys::Completion.create}.
101
+ # @param complete_values [Object] A specifier for shell tab completion for
102
+ # flag values associated with this flag. Pass any spec recognized by
103
+ # {Toys::Completion.create}.
104
+ # @param report_collisions [Boolean] Raise an exception if a flag is
105
+ # requested that is already in use or marked as disabled. Default is
106
+ # true.
107
+ # @param group [Toys::FlagGroup] Group containing this flag.
108
+ # @param desc [String,Array<String>,Toys::WrappableString] Short
109
+ # description for the flag. See {Toys::Tool#desc=} for a description of
110
+ # allowed formats. Defaults to the empty string.
111
+ # @param long_desc [Array<String,Array<String>,Toys::WrappableString>]
112
+ # Long description for the flag. See {Toys::Tool#long_desc=} for a
113
+ # description of allowed formats. Defaults to the empty array.
114
+ # @param display_name [String] A display name for this flag, used in help
115
+ # text and error messages.
116
+ # @param used_flags [Array<String>] An array of flags already in use.
117
+ #
118
+ def self.create(key, flags = [],
119
+ used_flags: nil, report_collisions: true, accept: nil, handler: nil,
120
+ default: nil, complete_flags: nil, complete_values: nil, display_name: nil,
121
+ desc: nil, long_desc: nil, group: nil)
122
+ new(key, flags, used_flags, report_collisions, accept, handler, default, complete_flags,
123
+ complete_values, desc, long_desc, display_name, group)
124
+ end
125
+
126
+ ##
127
+ # Returns the flag group containing this flag
128
+ # @return [Toys::FlagGroup]
129
+ #
130
+ attr_reader :group
131
+
132
+ ##
133
+ # Returns the key.
134
+ # @return [Symbol]
135
+ #
136
+ attr_reader :key
137
+
138
+ ##
139
+ # Returns an array of Flag::Syntax for the flags.
140
+ # @return [Array<Toys::Flag::Syntax>]
141
+ #
142
+ attr_reader :flag_syntax
143
+
144
+ ##
145
+ # Returns the effective acceptor.
146
+ # @return [Toys::Acceptor::Base]
147
+ #
148
+ attr_reader :acceptor
149
+
150
+ ##
151
+ # Returns the default value, which may be `nil`.
152
+ # @return [Object]
153
+ #
154
+ attr_reader :default
155
+
156
+ ##
157
+ # The short description string.
158
+ #
159
+ # When reading, this is always returned as a {Toys::WrappableString}.
160
+ #
161
+ # When setting, the description may be provided as any of the following:
162
+ # * A {Toys::WrappableString}.
163
+ # * A normal String, which will be transformed into a
164
+ # {Toys::WrappableString} using spaces as word delimiters.
165
+ # * An Array of String, which will be transformed into a
166
+ # {Toys::WrappableString} where each array element represents an
167
+ # individual word for wrapping.
168
+ #
169
+ # @return [Toys::WrappableString]
170
+ #
171
+ attr_reader :desc
172
+
173
+ ##
174
+ # The long description strings.
175
+ #
176
+ # When reading, this is returned as an Array of {Toys::WrappableString}
177
+ # representing the lines in the description.
178
+ #
179
+ # When setting, the description must be provided as an Array where *each
180
+ # element* may be any of the following:
181
+ # * A {Toys::WrappableString} representing one line.
182
+ # * A normal String representing a line. This will be transformed into a
183
+ # {Toys::WrappableString} using spaces as word delimiters.
184
+ # * An Array of String representing a line. This will be transformed into
185
+ # a {Toys::WrappableString} where each array element represents an
186
+ # individual word for wrapping.
187
+ #
188
+ # @return [Array<Toys::WrappableString>]
189
+ #
190
+ attr_reader :long_desc
191
+
192
+ ##
193
+ # The handler for setting/updating the value.
194
+ # @return [Proc]
195
+ #
196
+ attr_reader :handler
197
+
198
+ ##
199
+ # The proc that determines shell completions for the flag.
200
+ # @return [Proc,Toys::Completion::Base]
201
+ #
202
+ attr_reader :flag_completion
203
+
204
+ ##
205
+ # The proc that determines shell completions for the value.
206
+ # @return [Proc,Toys::Completion::Base]
207
+ #
208
+ attr_reader :value_completion
209
+
210
+ ##
211
+ # The type of flag.
212
+ #
213
+ # @return [:boolean] if the flag is a simple boolean switch
214
+ # @return [:value] if the flag sets a value
215
+ #
216
+ attr_reader :flag_type
217
+
218
+ ##
219
+ # The type of value.
220
+ #
221
+ # @return [:required] if the flag type is `:value` and the value is
222
+ # required.
223
+ # @return [:optional] if the flag type is `:value` and the value is
224
+ # optional.
225
+ # @return [nil] if the flag type is not `:value`.
226
+ #
227
+ attr_reader :value_type
228
+
229
+ ##
230
+ # The string label for the value as it should display in help.
231
+ # @return [String] The label
232
+ # @return [nil] if the flag type is not `:value`.
233
+ #
234
+ attr_reader :value_label
235
+
236
+ ##
237
+ # The value delimiter, which may be `""`, `" "`, or `"="`.
238
+ #
239
+ # @return [String] The delimiter
240
+ # @return [nil] if the flag type is not `:value`.
241
+ #
242
+ attr_reader :value_delim
243
+
244
+ ##
245
+ # The display name of this flag.
246
+ # @return [String]
247
+ #
248
+ attr_reader :display_name
249
+
250
+ ##
251
+ # A string that can be used to sort this flag
252
+ # @return [String]
253
+ #
254
+ attr_reader :sort_str
255
+
256
+ ##
257
+ # An array of Flag::Syntax including only short (single dash) flags.
258
+ # @return [Array<Flag::Syntax>]
259
+ #
260
+ def short_flag_syntax
261
+ @short_flag_syntax ||= flag_syntax.find_all { |ss| ss.flag_style == :short }
262
+ end
263
+
264
+ ##
265
+ # An array of Flag::Syntax including only long (double-dash) flags.
266
+ # @return [Array<Flag::Syntax>]
267
+ #
268
+ def long_flag_syntax
269
+ @long_flag_syntax ||= flag_syntax.find_all { |ss| ss.flag_style == :long }
270
+ end
271
+
272
+ ##
273
+ # The list of all effective flags used.
274
+ # @return [Array<String>]
275
+ #
276
+ def effective_flags
277
+ @effective_flags ||= flag_syntax.flat_map(&:flags)
278
+ end
279
+
280
+ ##
281
+ # Look up the flag by string. Returns an object that indicates whether
282
+ # the given string matched this flag, whether the match was unique, and
283
+ # other pertinent information.
284
+ #
285
+ # @param str [String] Flag string to look up
286
+ # @return [Toys::Flag::Resolution] Information about the match.
287
+ #
288
+ def resolve(str)
289
+ resolution = Resolution.new(str)
290
+ flag_syntax.each do |fs|
291
+ if fs.positive_flag == str
292
+ resolution.add!(self, fs, false, true)
293
+ elsif fs.negative_flag == str
294
+ resolution.add!(self, fs, true, true)
295
+ elsif fs.positive_flag.start_with?(str)
296
+ resolution.add!(self, fs, false, false)
297
+ elsif fs.negative_flag.to_s.start_with?(str)
298
+ resolution.add!(self, fs, true, false)
299
+ end
300
+ end
301
+ resolution
302
+ end
303
+
304
+ ##
305
+ # A list of canonical flag syntax strings.
306
+ #
307
+ # @return [Array<String>]
308
+ #
309
+ def canonical_syntax_strings
310
+ @canonical_syntax_strings ||= flag_syntax.map(&:canonical_str)
311
+ end
312
+
313
+ ##
314
+ # Whether this flag is active--that is, it has a nonempty flags list.
315
+ #
316
+ # @return [Boolean]
317
+ #
318
+ def active?
319
+ !effective_flags.empty?
320
+ end
321
+
322
+ ##
323
+ # Set the short description string.
324
+ #
325
+ # See {#desc} for details.
326
+ #
327
+ # @param desc [Toys::WrappableString,String,Array<String>]
328
+ #
329
+ def desc=(desc)
330
+ @desc = WrappableString.make(desc)
331
+ end
332
+
333
+ ##
334
+ # Set the long description strings.
335
+ #
336
+ # See {#long_desc} for details.
337
+ #
338
+ # @param long_desc [Array<Toys::WrappableString,String,Array<String>>]
339
+ #
340
+ def long_desc=(long_desc)
341
+ @long_desc = WrappableString.make_array(long_desc)
342
+ end
343
+
344
+ ##
345
+ # Append long description strings.
346
+ #
347
+ # You must pass an array of lines in the long description. See {#long_desc}
348
+ # for details on how each line may be represented.
349
+ #
350
+ # @param long_desc [Array<Toys::WrappableString,String,Array<String>>]
351
+ # @return [self]
352
+ #
353
+ def append_long_desc(long_desc)
354
+ @long_desc.concat(WrappableString.make_array(long_desc))
355
+ self
356
+ end
357
+
358
+ private
359
+
360
+ def resolve_handler(handler)
361
+ case handler
362
+ when ::Proc
363
+ handler
364
+ when nil, :default
365
+ DEFAULT_HANDLER
366
+ when :set
367
+ SET_HANDLER
368
+ when :push, :append
369
+ PUSH_HANDLER
370
+ else
371
+ raise ToolDefinitionError, "Unknown handler: #{handler.inspect}"
372
+ end
373
+ end
374
+
375
+ def create_flag_completion(spec)
376
+ case spec
377
+ when nil, :default
378
+ DefaultCompletion.new(self)
379
+ when ::Hash
380
+ DefaultCompletion.new(self, spec)
381
+ else
382
+ Completion.create(spec)
383
+ end
384
+ end
385
+
386
+ def create_default_flag
387
+ key_str = key.to_s
388
+ flag_str =
389
+ if key_str.length == 1
390
+ "-#{key_str}" if key_str =~ /[a-zA-Z0-9\?]/
391
+ elsif key_str.length > 1
392
+ key_str = key_str.downcase.tr("_", "-").gsub(/[^a-z0-9-]/, "").sub(/^-+/, "")
393
+ "--#{key_str}" unless key_str.empty?
394
+ end
395
+ if flag_str
396
+ needs_val = @value_completion != Completion::EMPTY ||
397
+ ![::Object, ::TrueClass, ::FalseClass].include?(@acceptor.well_known_spec) ||
398
+ ![nil, true, false].include?(@default)
399
+ flag_str = "#{flag_str} VALUE" if needs_val
400
+ @flag_syntax << Syntax.new(flag_str)
401
+ end
402
+ end
403
+
404
+ def remove_used_flags(used_flags, report_collisions)
405
+ return if !used_flags && !report_collisions
406
+ @flag_syntax.select! do |fs|
407
+ fs.flags.all? do |f|
408
+ collision = used_flags&.include?(f)
409
+ if collision && report_collisions
410
+ raise ToolDefinitionError,
411
+ "Cannot use flag #{f.inspect} because it is already assigned or reserved."
412
+ end
413
+ !collision
414
+ end
415
+ end
416
+ used_flags&.concat(effective_flags.uniq)
417
+ end
418
+
419
+ def canonicalize
420
+ @flag_type = nil
421
+ @value_type = nil
422
+ @value_label = nil
423
+ @value_delim = " "
424
+ short_flag_syntax.reverse_each do |flag|
425
+ analyze_flag_syntax(flag)
426
+ end
427
+ long_flag_syntax.reverse_each do |flag|
428
+ analyze_flag_syntax(flag)
429
+ end
430
+ @flag_type ||= :boolean
431
+ @value_type ||= :required if @flag_type == :value
432
+ flag_syntax.each do |flag|
433
+ flag.configure_canonical(@flag_type, @value_type, @value_label, @value_delim)
434
+ end
435
+ end
436
+
437
+ def analyze_flag_syntax(flag)
438
+ return if flag.flag_type.nil?
439
+ if !@flag_type.nil? && @flag_type != flag.flag_type
440
+ raise ToolDefinitionError, "Cannot have both value and boolean flags for #{key.inspect}"
441
+ end
442
+ @flag_type = flag.flag_type
443
+ return unless @flag_type == :value
444
+ if !@value_type.nil? && @value_type != flag.value_type
445
+ raise ToolDefinitionError,
446
+ "Cannot have both required and optional values for flag #{key.inspect}"
447
+ end
448
+ @value_type = flag.value_type
449
+ @value_label = flag.value_label
450
+ @value_delim = flag.value_delim
451
+ end
452
+
453
+ def summarize(name)
454
+ @display_name =
455
+ name ||
456
+ long_flag_syntax.first&.canonical_str ||
457
+ short_flag_syntax.first&.canonical_str ||
458
+ key.to_s
459
+ @sort_str =
460
+ long_flag_syntax.first&.sort_str ||
461
+ short_flag_syntax.first&.sort_str ||
462
+ ""
463
+ end
464
+
465
+ ##
466
+ # Representation of a single flag.
467
+ #
468
+ class Syntax
469
+ # rubocop:disable Style/PerlBackrefs
470
+
471
+ ##
472
+ # Parse flag syntax
473
+ # @param str [String] syntax.
474
+ #
475
+ def initialize(str)
476
+ case str
477
+ when /\A(-([\?\w]))\z/
478
+ setup(str, $1, nil, $1, $2, :short, nil, nil, nil, nil)
479
+ when /\A(-([\?\w]))( ?)\[(\w+)\]\z/
480
+ setup(str, $1, nil, $1, $2, :short, :value, :optional, $3, $4)
481
+ when /\A(-([\?\w]))\[( )(\w+)\]\z/
482
+ setup(str, $1, nil, $1, $2, :short, :value, :optional, $3, $4)
483
+ when /\A(-([\?\w]))( ?)(\w+)\z/
484
+ setup(str, $1, nil, $1, $2, :short, :value, :required, $3, $4)
485
+ when /\A--\[no-\](\w[\?\w-]*)\z/
486
+ setup(str, "--#{$1}", "--no-#{$1}", str, $1, :long, :boolean, nil, nil, nil)
487
+ when /\A(--(\w[\?\w-]*))\z/
488
+ setup(str, $1, nil, $1, $2, :long, nil, nil, nil, nil)
489
+ when /\A(--(\w[\?\w-]*))([= ])\[(\w+)\]\z/
490
+ setup(str, $1, nil, $1, $2, :long, :value, :optional, $3, $4)
491
+ when /\A(--(\w[\?\w-]*))\[([= ])(\w+)\]\z/
492
+ setup(str, $1, nil, $1, $2, :long, :value, :optional, $3, $4)
493
+ when /\A(--(\w[\?\w-]*))([= ])(\w+)\z/
494
+ setup(str, $1, nil, $1, $2, :long, :value, :required, $3, $4)
495
+ else
496
+ raise ToolDefinitionError, "Illegal flag: #{str.inspect}"
497
+ end
498
+ end
499
+
500
+ # rubocop:enable Style/PerlBackrefs
501
+
502
+ ##
503
+ # The original string that was parsed to produce this syntax.
504
+ # @return [String]
505
+ #
506
+ attr_reader :original_str
507
+
508
+ ##
509
+ # The flags (without values) corresponding to this syntax.
510
+ # @return [Array<String>]
511
+ #
512
+ attr_reader :flags
513
+
514
+ ##
515
+ # The flag (without values) corresponding to the normal "positive" form
516
+ # of this flag.
517
+ # @return [String]
518
+ #
519
+ attr_reader :positive_flag
520
+
521
+ ##
522
+ # The flag (without values) corresponding to the "negative" form of this
523
+ # flag, if any. i.e. if the original string was `"--[no-]abc"`, the
524
+ # negative flag is `"--no-abc"`.
525
+ # @return [String] The negative form.
526
+ # @return [nil] if the flag has no negative form.
527
+ #
528
+ attr_reader :negative_flag
529
+
530
+ ##
531
+ # The original string with the value (if any) stripped, but retaining
532
+ # the `[no-]` prefix if present.
533
+ # @return [String]
534
+ #
535
+ attr_reader :str_without_value
536
+
537
+ ##
538
+ # A string used to sort this flag compared with others.
539
+ # @return [String]
540
+ #
541
+ attr_reader :sort_str
542
+
543
+ ##
544
+ # The style of flag (`:long` or `:short`).
545
+ # @return [:long] if this is a long flag (i.e. double hyphen)
546
+ # @return [:short] if this is a short flag (i.e. single hyphen with one
547
+ # character).
548
+ #
549
+ attr_reader :flag_style
550
+
551
+ ##
552
+ # The type of flag (`:boolean` or `:value`)
553
+ # @return [:boolean] if this is a boolean flag (i.e. no value)
554
+ # @return [:value] if this flag takes a value (even if optional)
555
+ #
556
+ attr_reader :flag_type
557
+
558
+ ##
559
+ # The type of value (`:required` or `:optional`)
560
+ # @return [:required] if this flag takes a required value
561
+ # @return [:optional] if this flag takes an optional value
562
+ # @return [nil] if this flag is a boolean flag
563
+ #
564
+ attr_reader :value_type
565
+
566
+ ##
567
+ # The default delimiter used for the value of this flag. This could be
568
+ # `""` or `" "` for a short flag, or `" "` or `"="` for a long flag.
569
+ # @return [String] delimiter
570
+ # @return [nil] if this flag is a boolean flag
571
+ #
572
+ attr_reader :value_delim
573
+
574
+ ##
575
+ # The default "label" for the value. e.g. in `--abc=VAL` the label is
576
+ # `"VAL"`.
577
+ # @return [String] the label
578
+ # @return [nil] if this flag is a boolean flag
579
+ #
580
+ attr_reader :value_label
581
+
582
+ ##
583
+ # A canonical string representing this flag's syntax, normalized to match
584
+ # the type, delimiters, etc. settings of other flag syntaxes. This is
585
+ # generally used in help strings to represent this flag.
586
+ # @return [String]
587
+ #
588
+ attr_reader :canonical_str
589
+
590
+ ## @private
591
+ def configure_canonical(canonical_flag_type, canonical_value_type,
592
+ canonical_value_label, canonical_value_delim)
593
+ return unless flag_type.nil?
594
+ @flag_type = canonical_flag_type
595
+ return unless canonical_flag_type == :value
596
+ @value_type = canonical_value_type
597
+ canonical_value_delim = "" if canonical_value_delim == "=" && flag_style == :short
598
+ canonical_value_delim = "=" if canonical_value_delim == "" && flag_style == :long
599
+ @value_delim = canonical_value_delim
600
+ @value_label = canonical_value_label
601
+ label = @value_type == :optional ? "[#{@value_label}]" : @value_label
602
+ @canonical_str = "#{str_without_value}#{@value_delim}#{label}"
603
+ end
604
+
605
+ private
606
+
607
+ def setup(original_str, positive_flag, negative_flag, str_without_value, sort_str,
608
+ flag_style, flag_type, value_type, value_delim, value_label)
609
+ @original_str = original_str
610
+ @positive_flag = positive_flag
611
+ @negative_flag = negative_flag
612
+ @flags = [positive_flag]
613
+ @flags << negative_flag if negative_flag
614
+ @str_without_value = str_without_value
615
+ @sort_str = sort_str
616
+ @flag_style = flag_style
617
+ @flag_type = flag_type
618
+ @value_type = value_type
619
+ @value_delim = value_delim
620
+ @value_label = value_label ? value_label.upcase : value_label
621
+ @canonical_str = original_str
622
+ end
623
+ end
624
+
625
+ ##
626
+ # The result of looking up a flag by name.
627
+ #
628
+ class Resolution
629
+ ## @private
630
+ def initialize(str)
631
+ @string = str
632
+ @flags = []
633
+ @found_exact = false
634
+ end
635
+
636
+ ##
637
+ # The flag string that was looked up
638
+ # @return [String]
639
+ #
640
+ attr_reader :string
641
+
642
+ ##
643
+ # Whether an exact match of the string was found
644
+ # @return [Boolean]
645
+ #
646
+ def found_exact?
647
+ @found_exact
648
+ end
649
+
650
+ ##
651
+ # The number of matches that were found.
652
+ # @return [Integer]
653
+ #
654
+ def count
655
+ @flags.size
656
+ end
657
+
658
+ ##
659
+ # Whether a single unique match was found.
660
+ # @return [Boolean]
661
+ #
662
+ def found_unique?
663
+ @flags.size == 1
664
+ end
665
+
666
+ ##
667
+ # Whether no matches were found.
668
+ # @return [Boolean]
669
+ #
670
+ def not_found?
671
+ @flags.empty?
672
+ end
673
+
674
+ ##
675
+ # Whether multiple matches were found (i.e. ambiguous input).
676
+ # @return [Boolean]
677
+ #
678
+ def found_multiple?
679
+ @flags.size > 1
680
+ end
681
+
682
+ ##
683
+ # Return the unique {Toys::Flag}, or `nil` if not found or
684
+ # not unique.
685
+ # @return [Toys::Flag,nil]
686
+ #
687
+ def unique_flag
688
+ found_unique? ? @flags.first[0] : nil
689
+ end
690
+
691
+ ##
692
+ # Return the unique {Toys::Flag::Syntax}, or `nil` if not found
693
+ # or not unique.
694
+ # @return [Toys::Flag::Syntax,nil]
695
+ #
696
+ def unique_flag_syntax
697
+ found_unique? ? @flags.first[1] : nil
698
+ end
699
+
700
+ ##
701
+ # Return whether the unique match was a hit on the negative (`--no-*`)
702
+ # case, or `nil` if not found or not unique.
703
+ # @return [Boolean,nil]
704
+ #
705
+ def unique_flag_negative?
706
+ found_unique? ? @flags.first[2] : nil
707
+ end
708
+
709
+ ##
710
+ # Returns an array of the matching full flag strings.
711
+ # @return [Array<String>]
712
+ #
713
+ def matching_flag_strings
714
+ @flags.map do |_flag, flag_syntax, negative|
715
+ negative ? flag_syntax.negative_flag : flag_syntax.positive_flag
716
+ end
717
+ end
718
+
719
+ ## @private
720
+ def add!(flag, flag_syntax, negative, exact)
721
+ @flags = [] if exact && !found_exact?
722
+ if exact || !found_exact?
723
+ @flags << [flag, flag_syntax, negative]
724
+ @found_exact = exact
725
+ end
726
+ self
727
+ end
728
+
729
+ ## @private
730
+ def merge!(other)
731
+ raise "String mismatch" unless string == other.string
732
+ other.instance_variable_get(:@flags).each do |flag, flag_syntax, negative|
733
+ add!(flag, flag_syntax, negative, other.found_exact?)
734
+ end
735
+ self
736
+ end
737
+ end
738
+
739
+ ##
740
+ # A Completion that returns all possible flags associated with a
741
+ # {Toys::Flag}.
742
+ #
743
+ class DefaultCompletion < Completion::Base
744
+ ##
745
+ # Create a completion given configuration options.
746
+ #
747
+ # @param flag [Toys::Flag] The flag definition.
748
+ # @param include_short [Boolean] Whether to include short flags.
749
+ # @param include_long [Boolean] Whether to include long flags.
750
+ # @param include_negative [Boolean] Whether to include `--no-*` forms.
751
+ #
752
+ def initialize(flag, include_short: true, include_long: true, include_negative: true)
753
+ @flag = flag
754
+ @include_short = include_short
755
+ @include_long = include_long
756
+ @include_negative = include_negative
757
+ end
758
+
759
+ ##
760
+ # Whether to include short flags
761
+ # @return [Boolean]
762
+ #
763
+ def include_short?
764
+ @include_short
765
+ end
766
+
767
+ ##
768
+ # Whether to include long flags
769
+ # @return [Boolean]
770
+ #
771
+ def include_long?
772
+ @include_long
773
+ end
774
+
775
+ ##
776
+ # Whether to include negative long flags
777
+ # @return [Boolean]
778
+ #
779
+ def include_negative?
780
+ @include_negative
781
+ end
782
+
783
+ ##
784
+ # Returns candidates for the current completion.
785
+ #
786
+ # @param context [Toys::Completion::Context] the current completion
787
+ # context including the string fragment.
788
+ # @return [Array<Toys::Completion::Candidate>] an array of candidates
789
+ #
790
+ def call(context)
791
+ results =
792
+ if @include_short && @include_long && @include_negative
793
+ @flag.effective_flags
794
+ else
795
+ collect_results
796
+ end
797
+ fragment = context.fragment
798
+ results.find_all { |val| val.start_with?(fragment) }
799
+ .map { |str| Completion::Candidate.new(str) }
800
+ end
801
+
802
+ private
803
+
804
+ def collect_results
805
+ results = []
806
+ if @include_short
807
+ results += @flag.short_flag_syntax.map(&:positive_flag)
808
+ end
809
+ if @include_long
810
+ results +=
811
+ if @include_negative
812
+ @flag.long_flag_syntax.flat_map(&:flags)
813
+ else
814
+ @flag.long_flag_syntax.map(&:positive_flag)
815
+ end
816
+ end
817
+ results
818
+ end
819
+ end
820
+ end
821
+ end