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,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