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
@@ -0,0 +1,106 @@
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
+ # An alias is a name that refers to another name.
27
+ #
28
+ class Alias
29
+ ##
30
+ # Create a new alias.
31
+ # Should be created only from the DSL via the Loader.
32
+ # @private
33
+ #
34
+ def initialize(loader, full_name, target, priority)
35
+ @target_name =
36
+ if target.is_a?(::Array)
37
+ target.map(&:to_s)
38
+ else
39
+ full_name[0..-2] + [target.to_s]
40
+ end
41
+ @target_name.freeze
42
+ @full_name = full_name.map(&:to_s).freeze
43
+ @priority = priority
44
+ @tool_class = DSL::Tool.new_class(@full_name, priority, loader)
45
+ end
46
+
47
+ ##
48
+ # The tool class.
49
+ #
50
+ # @return [Class]
51
+ #
52
+ attr_reader :tool_class
53
+
54
+ ##
55
+ # The name of the tool as an array of strings.
56
+ # This array may not be modified.
57
+ #
58
+ # @return [Array<String>]
59
+ #
60
+ attr_reader :full_name
61
+
62
+ ##
63
+ # The priority of this alias.
64
+ #
65
+ # @return [Integer]
66
+ #
67
+ attr_reader :priority
68
+
69
+ ##
70
+ # The name of the target as an array of strings.
71
+ # This array may not be modified.
72
+ #
73
+ # @return [Array<String>]
74
+ #
75
+ attr_reader :target_name
76
+
77
+ ##
78
+ # The local name of this alias, i.e. the last element of the full name.
79
+ #
80
+ # @return [String]
81
+ #
82
+ def simple_name
83
+ full_name.last
84
+ end
85
+
86
+ ##
87
+ # A displayable name of this alias, generally the full name delimited by
88
+ # spaces.
89
+ #
90
+ # @return [String]
91
+ #
92
+ def display_name
93
+ full_name.join(" ")
94
+ end
95
+
96
+ ##
97
+ # A displayable name of the target, generally the full name delimited by
98
+ # spaces.
99
+ #
100
+ # @return [String]
101
+ #
102
+ def display_target
103
+ target_name.join(" ")
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,624 @@
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
+ # An internal class that parses command line arguments for a tool.
27
+ #
28
+ # Generally, you should not need to use this class directly. It is called
29
+ # from {Toys::CLI}.
30
+ #
31
+ class ArgParser
32
+ ##
33
+ # Base representation of a usage error reported by the ArgParser.
34
+ #
35
+ # This functions similarly to an exception, but is not raised. Rather, it
36
+ # is returned in the {Toys::ArgParser#errors} array.
37
+ #
38
+ class UsageError
39
+ ##
40
+ # Create a UsageError given a message and common data
41
+ #
42
+ # @param message [String] The basic error message.
43
+ # @param name [String,nil] The name of the element (normally flag or
44
+ # positional argument) that reported the error, or nil if there is
45
+ # no definite element.
46
+ # @param value [String,nil] The value that was rejected, or nil if not
47
+ # applicable.
48
+ # @param suggestions [Array<String>,nil] An array of suggestions from
49
+ # DidYouMean, or nil if not applicable.
50
+ #
51
+ def initialize(message, name: nil, value: nil, suggestions: nil)
52
+ @message = message
53
+ @name = name
54
+ @value = value
55
+ @suggestions = suggestions
56
+ end
57
+
58
+ ##
59
+ # The basic error message. Does not include suggestions, if any.
60
+ #
61
+ # @return [String]
62
+ #
63
+ attr_reader :message
64
+
65
+ ##
66
+ # The name of the element (normally a flag or positional argument) that
67
+ # reported the error.
68
+ #
69
+ # @return [String] The element name.
70
+ # @return [nil] if there is no definite element source.
71
+ #
72
+ attr_reader :name
73
+
74
+ ##
75
+ # The value that was rejected.
76
+ #
77
+ # @return [String] the value string
78
+ # @return [nil] if a value is not applicable to this error.
79
+ #
80
+ attr_reader :value
81
+
82
+ ##
83
+ # An array of suggestions from DidYouMean.
84
+ #
85
+ # @return [Array<String>] array of suggestions.
86
+ # @return [nil] if suggestions are not applicable to this error.
87
+ #
88
+ attr_reader :suggestions
89
+
90
+ ##
91
+ # A fully formatted error message including suggestions.
92
+ #
93
+ # @return [String]
94
+ #
95
+ def full_message
96
+ if suggestions && !suggestions.empty?
97
+ alts_str = suggestions.join("\n ")
98
+ "#{message}\nDid you mean... #{alts_str}"
99
+ else
100
+ message
101
+ end
102
+ end
103
+ alias to_s full_message
104
+ end
105
+
106
+ ##
107
+ # A UsageError indicating a value was provided for a flag that does not
108
+ # take a value.
109
+ #
110
+ class FlagValueNotAllowedError < UsageError
111
+ ##
112
+ # Create a FlagValueNotAllowedError.
113
+ #
114
+ # @param message [String,nil] A custom message. Normally omitted, in
115
+ # which case an appropriate default is supplied.
116
+ # @param name [String] The name of the flag. Normally required.
117
+ #
118
+ def initialize(message = nil, name: nil)
119
+ super(message || "Flag \"#{name}\" should not take an argument.", name: name)
120
+ end
121
+ end
122
+
123
+ ##
124
+ # A UsageError indicating a value was not provided for a flag that requires
125
+ # a value.
126
+ #
127
+ class FlagValueMissingError < UsageError
128
+ ##
129
+ # Create a FlagValueMissingError.
130
+ #
131
+ # @param message [String,nil] A custom message. Normally omitted, in
132
+ # which case an appropriate default is supplied.
133
+ # @param name [String] The name of the flag. Normally required.
134
+ #
135
+ def initialize(message = nil, name: nil)
136
+ super(message || "Flag \"#{name}\" is missing a value.", name: name)
137
+ end
138
+ end
139
+
140
+ ##
141
+ # A UsageError indicating a flag name was not recognized.
142
+ #
143
+ class FlagUnrecognizedError < UsageError
144
+ ##
145
+ # Create a FlagUnrecognizedError.
146
+ #
147
+ # @param message [String,nil] A custom message. Normally omitted, in
148
+ # which case an appropriate default is supplied.
149
+ # @param value [String] The requested flag name. Normally required.
150
+ # @param suggestions [Array<String>] An array of suggestions to present
151
+ # to the user. Optional.
152
+ #
153
+ def initialize(message = nil, value: nil, suggestions: nil)
154
+ super(message || "Flag \"#{value}\" is not recognized.",
155
+ value: value, suggestions: suggestions)
156
+ end
157
+ end
158
+
159
+ ##
160
+ # A UsageError indicating a flag name prefix was given that matched
161
+ # multiple flags.
162
+ #
163
+ class FlagAmbiguousError < UsageError
164
+ ##
165
+ # Create a FlagAmbiguousError.
166
+ #
167
+ # @param message [String,nil] A custom message. Normally omitted, in
168
+ # which case an appropriate default is supplied.
169
+ # @param value [String] The requested flag name. Normally required.
170
+ # @param suggestions [Array<String>] An array of suggestions to present
171
+ # to the user. Optional.
172
+ #
173
+ def initialize(message = nil, value: nil, suggestions: nil)
174
+ super(message || "Flag prefix \"#{value}\" is ambiguous.",
175
+ value: value, suggestions: suggestions)
176
+ end
177
+ end
178
+
179
+ ##
180
+ # A UsageError indicating a flag did not accept the value given it.
181
+ #
182
+ class FlagValueUnacceptableError < UsageError
183
+ ##
184
+ # Create a FlagValueUnacceptableError.
185
+ #
186
+ # @param message [String,nil] A custom message. Normally omitted, in
187
+ # which case an appropriate default is supplied.
188
+ # @param name [String] The name of the flag. Normally required.
189
+ # @param value [String] The value given. Normally required.
190
+ # @param suggestions [Array<String>] An array of suggestions to present
191
+ # to the user. Optional.
192
+ #
193
+ def initialize(message = nil, name: nil, value: nil, suggestions: nil)
194
+ super(message || "Unacceptable value \"#{value}\" for flag \"#{name}\".",
195
+ name: name, suggestions: suggestions)
196
+ end
197
+ end
198
+
199
+ ##
200
+ # A UsageError indicating a positional argument did not accept the value
201
+ # given it.
202
+ #
203
+ class ArgValueUnacceptableError < UsageError
204
+ ##
205
+ # Create an ArgValueUnacceptableError.
206
+ #
207
+ # @param message [String,nil] A custom message. Normally omitted, in
208
+ # which case an appropriate default is supplied.
209
+ # @param name [String] The name of the argument. Normally required.
210
+ # @param value [String] The value given. Normally required.
211
+ # @param suggestions [Array<String>] An array of suggestions to present
212
+ # to the user. Optional.
213
+ #
214
+ def initialize(message = nil, name: nil, value: nil, suggestions: nil)
215
+ super(message || "Unacceptable value \"#{value}\" for positional argument \"#{name}\".",
216
+ name: name, suggestions: suggestions)
217
+ end
218
+ end
219
+
220
+ ##
221
+ # A UsageError indicating a required positional argument was not fulfilled.
222
+ #
223
+ class ArgMissingError < UsageError
224
+ ##
225
+ # Create an ArgMissingError.
226
+ #
227
+ # @param message [String,nil] A custom message. Normally omitted, in
228
+ # which case an appropriate default is supplied.
229
+ # @param name [String] The name of the argument. Normally required.
230
+ #
231
+ def initialize(message = nil, name: nil)
232
+ super(message || "Required positional argument \"#{name}\" is missing.", name: name)
233
+ end
234
+ end
235
+
236
+ ##
237
+ # A UsageError indicating extra arguments were supplied.
238
+ #
239
+ class ExtraArgumentsError < UsageError
240
+ ##
241
+ # Create an ExtraArgumentsError.
242
+ #
243
+ # @param message [String,nil] A custom message. Normally omitted, in
244
+ # which case an appropriate default is supplied.
245
+ # @param value [String] The first extra argument. Normally required.
246
+ # @param values [Array<String>] All extra arguments. Normally required.
247
+ #
248
+ def initialize(message = nil, value: nil, values: nil)
249
+ super(message || "Extra arguments: \"#{Array(values).join(' ')}\".", value: value)
250
+ end
251
+ end
252
+
253
+ ##
254
+ # A UsageError indicating the given subtool name does not exist.
255
+ #
256
+ class ToolUnrecognizedError < UsageError
257
+ ##
258
+ # Create a ToolUnrecognizedError.
259
+ #
260
+ # @param message [String,nil] A custom message. Normally omitted, in
261
+ # which case an appropriate default is supplied.
262
+ # @param value [String] The requested subtool. Normally required.
263
+ # @param values [Array<String>] The full path of the requested tool.
264
+ # Normally required.
265
+ # @param suggestions [Array<String>] An array of suggestions to present
266
+ # to the user. Optional.
267
+ #
268
+ def initialize(message = nil, value: nil, values: nil, suggestions: nil)
269
+ super(message || "Tool not found: \"#{Array(values).join(' ')}\".",
270
+ value: value, suggestions: suggestions)
271
+ @name = name
272
+ end
273
+ end
274
+
275
+ ##
276
+ # A UsageError indicating a flag group constraint was not fulfilled.
277
+ #
278
+ class FlagGroupConstraintError < UsageError
279
+ ##
280
+ # Create a FlagGroupConstraintError.
281
+ #
282
+ # @param message [String] The message. Required.
283
+ #
284
+ def initialize(message)
285
+ super(message)
286
+ end
287
+ end
288
+
289
+ ##
290
+ # Create an argument parser for a particular tool.
291
+ #
292
+ # @param cli [Toys::CLI] The CLI in effect.
293
+ # @param tool [Toys::Tool] The tool defining the argument format.
294
+ # @param verbosity [Integer] The initial verbosity level (default is 0).
295
+ # @param require_exact_flag_match [Boolean] Whether to require flag matches
296
+ # be exact (not partial). Default is false.
297
+ #
298
+ def initialize(cli, tool, verbosity: 0, require_exact_flag_match: false)
299
+ @require_exact_flag_match = require_exact_flag_match
300
+ @loader = cli.loader
301
+ @data = initial_data(cli, tool, verbosity)
302
+ @tool = tool
303
+ @seen_flag_keys = []
304
+ @errors = []
305
+ @unmatched_args = []
306
+ @unmatched_positional = []
307
+ @unmatched_flags = []
308
+ @parsed_args = []
309
+ @active_flag_def = nil
310
+ @active_flag_arg = nil
311
+ @arg_defs = tool.positional_args
312
+ @arg_def_index = 0
313
+ @flags_allowed = true
314
+ @finished = false
315
+ end
316
+
317
+ ##
318
+ # The tool definition governing this parser.
319
+ # @return [Toys::Tool]
320
+ #
321
+ attr_reader :tool
322
+
323
+ ##
324
+ # All command line arguments that have been parsed.
325
+ # @return [Array<String>]
326
+ #
327
+ attr_reader :parsed_args
328
+
329
+ ##
330
+ # Extra positional args that were not matched.
331
+ # @return [Array<String>]
332
+ #
333
+ attr_reader :unmatched_positional
334
+
335
+ ##
336
+ # Flags that were not matched.
337
+ # @return [Array<String>]
338
+ #
339
+ attr_reader :unmatched_flags
340
+
341
+ ##
342
+ # All args that were not matched.
343
+ # @return [Array<String>]
344
+ #
345
+ attr_reader :unmatched_args
346
+
347
+ ##
348
+ # The collected tool data from parsed arguments.
349
+ # @return [Hash]
350
+ #
351
+ attr_reader :data
352
+
353
+ ##
354
+ # An array of parse error messages.
355
+ # @return [Array<Toys::ArgParser::UsageError>]
356
+ #
357
+ attr_reader :errors
358
+
359
+ ##
360
+ # The current flag definition whose value is still pending
361
+ #
362
+ # @return [Toys::Flag] The pending flag definition
363
+ # @return [nil] if there is no pending flag
364
+ #
365
+ attr_reader :active_flag_def
366
+
367
+ ##
368
+ # Whether flags are currently allowed. Returns false after `--` is received.
369
+ # @return [Boolean]
370
+ #
371
+ def flags_allowed?
372
+ @flags_allowed
373
+ end
374
+
375
+ ##
376
+ # Determine if this parser is finished
377
+ # @return [Boolean]
378
+ #
379
+ def finished?
380
+ @finished
381
+ end
382
+
383
+ ##
384
+ # The argument definition that will be applied to the next argument.
385
+ #
386
+ # @return [Toys::PositionalArg] The next argument definition.
387
+ # @return [nil] if all arguments have been filled.
388
+ #
389
+ def next_arg_def
390
+ @arg_defs[@arg_def_index]
391
+ end
392
+
393
+ ##
394
+ # Incrementally parse a single string or an array of strings
395
+ #
396
+ # @param args [String,Array<String>]
397
+ # @return [self]
398
+ #
399
+ def parse(args)
400
+ raise "Parser has finished" if @finished
401
+ Array(args).each do |arg|
402
+ @parsed_args << arg
403
+ unless @tool.argument_parsing_disabled?
404
+ check_flag_value(arg) || check_flag(arg) || handle_positional(arg)
405
+ end
406
+ end
407
+ self
408
+ end
409
+
410
+ ##
411
+ # Complete parsing. This should be called after all arguments have been
412
+ # processed. It does a final check for any errors, including:
413
+ #
414
+ # * The arguments ended with a flag that was expecting a value but wasn't
415
+ # provided.
416
+ # * One or more required arguments were never given a value.
417
+ # * One or more extra arguments were provided.
418
+ # * Restrictions defined in one or more flag groups were not fulfilled.
419
+ #
420
+ # Any errors are added to the errors array. It also fills in final values
421
+ # for `Context::Key::USAGE_ERRORS` and `Context::Key::ARGS`.
422
+ #
423
+ # After this method is called, this object is locked down, and no
424
+ # additional arguments may be parsed.
425
+ #
426
+ # @return [self]
427
+ #
428
+ def finish
429
+ finish_active_flag
430
+ finish_arg_defs
431
+ finish_flag_groups
432
+ finish_special_data
433
+ @finished = true
434
+ self
435
+ end
436
+
437
+ private
438
+
439
+ REMAINING_HANDLER = ->(val, prev) { prev.is_a?(::Array) ? prev << val : [val] }
440
+ ARG_HANDLER = ->(val, _prev) { val }
441
+
442
+ def initial_data(cli, tool, verbosity)
443
+ data = {
444
+ Context::Key::ARGS => nil,
445
+ Context::Key::CLI => cli,
446
+ Context::Key::CONTEXT_DIRECTORY => tool.context_directory,
447
+ Context::Key::LOGGER => cli.logger,
448
+ Context::Key::TOOL => tool,
449
+ Context::Key::TOOL_SOURCE => tool.source_info,
450
+ Context::Key::TOOL_NAME => tool.full_name,
451
+ Context::Key::USAGE_ERRORS => [],
452
+ }
453
+ Compat.merge_clones(data, tool.default_data)
454
+ data[Context::Key::VERBOSITY] ||= verbosity
455
+ data
456
+ end
457
+
458
+ def check_flag_value(arg)
459
+ return false unless @active_flag_def
460
+ result = @active_flag_def.value_type == :required || !arg.start_with?("-")
461
+ add_data(@active_flag_def.key, @active_flag_def.handler, @active_flag_def.acceptor,
462
+ result ? arg : nil, :flag, @active_flag_arg)
463
+ @seen_flag_keys << @active_flag_def.key
464
+ @active_flag_def = nil
465
+ @active_flag_arg = nil
466
+ result
467
+ end
468
+
469
+ def check_flag(arg)
470
+ return false unless @flags_allowed
471
+ case arg
472
+ when "--"
473
+ @flags_allowed = false
474
+ when /\A(--\w[\?\w-]*)=(.*)\z/
475
+ handle_valued_flag(::Regexp.last_match(1), ::Regexp.last_match(2))
476
+ when /\A--.+\z/
477
+ handle_plain_flag(arg)
478
+ when /\A-(.+)\z/
479
+ handle_single_flags(::Regexp.last_match(1))
480
+ else
481
+ return false
482
+ end
483
+ true
484
+ end
485
+
486
+ def handle_single_flags(str)
487
+ until str.empty?
488
+ str = handle_plain_flag("-#{str[0]}", str[1..-1])
489
+ end
490
+ end
491
+
492
+ def handle_plain_flag(name, following = "")
493
+ flag_result = find_flag(name)
494
+ flag_def = flag_result&.unique_flag
495
+ return "" unless flag_def
496
+ @seen_flag_keys << flag_def.key
497
+ if flag_def.flag_type == :boolean
498
+ add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?,
499
+ :flag, name)
500
+ elsif following.empty?
501
+ if flag_def.value_type == :required || flag_result.unique_flag_syntax.value_delim == " "
502
+ @active_flag_def = flag_def
503
+ @active_flag_arg = name
504
+ else
505
+ add_data(flag_def.key, flag_def.handler, flag_def.acceptor, nil, :flag, name)
506
+ end
507
+ else
508
+ add_data(flag_def.key, flag_def.handler, flag_def.acceptor, following, :flag, name)
509
+ following = ""
510
+ end
511
+ following
512
+ end
513
+
514
+ def handle_valued_flag(name, value)
515
+ flag_result = find_flag(name)
516
+ flag_def = flag_result&.unique_flag
517
+ return unless flag_def
518
+ @seen_flag_keys << flag_def.key
519
+ if flag_def.flag_type == :value
520
+ add_data(flag_def.key, flag_def.handler, flag_def.acceptor, value, :flag, name)
521
+ else
522
+ add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?,
523
+ :flag, name)
524
+ @errors << FlagValueNotAllowedError.new(name: name)
525
+ end
526
+ end
527
+
528
+ def handle_positional(arg)
529
+ if @tool.flags_before_args_enforced?
530
+ @flags_allowed = false
531
+ end
532
+ arg_def = next_arg_def
533
+ unless arg_def
534
+ @unmatched_positional << arg
535
+ @unmatched_args << arg
536
+ return
537
+ end
538
+ @arg_def_index += 1 unless arg_def.type == :remaining
539
+ handler = arg_def.type == :remaining ? REMAINING_HANDLER : ARG_HANDLER
540
+ add_data(arg_def.key, handler, arg_def.acceptor, arg, :arg, arg_def.display_name)
541
+ end
542
+
543
+ def find_flag(name)
544
+ flag_result = @tool.resolve_flag(name)
545
+ if flag_result.not_found? || @require_exact_flag_match && !flag_result.found_exact?
546
+ @errors << FlagUnrecognizedError.new(
547
+ value: name, suggestions: Compat.suggestions(name, @tool.used_flags)
548
+ )
549
+ @unmatched_flags << name
550
+ @unmatched_args << name
551
+ flag_result = nil
552
+ elsif flag_result.found_multiple?
553
+ @errors << FlagAmbiguousError.new(
554
+ value: name, suggestions: flag_result.matching_flag_strings
555
+ )
556
+ @unmatched_flags << name
557
+ @unmatched_args << name
558
+ flag_result = nil
559
+ end
560
+ flag_result
561
+ end
562
+
563
+ def add_data(key, handler, accept, value, type_name, display_name)
564
+ if accept
565
+ match = accept.match(value)
566
+ unless match
567
+ error_class = type_name == :flag ? FlagValueUnacceptableError : ArgValueUnacceptableError
568
+ suggestions = accept.respond_to?(:suggestions) ? accept.suggestions(value) : nil
569
+ @errors << error_class.new(value: value, name: display_name, suggestions: suggestions)
570
+ return
571
+ end
572
+ value = accept.convert(*Array(match))
573
+ end
574
+ if handler
575
+ value = handler.call(value, @data[key])
576
+ end
577
+ @data[key] = value
578
+ end
579
+
580
+ def finish_active_flag
581
+ if @active_flag_def
582
+ if @active_flag_def.value_type == :required
583
+ @errors << FlagValueMissingError.new(name: @active_flag_arg)
584
+ else
585
+ add_data(@active_flag_def.key, @active_flag_def.handler, @active_flag_def.acceptor,
586
+ nil, :flag, @active_flag_arg)
587
+ end
588
+ end
589
+ end
590
+
591
+ def finish_arg_defs
592
+ arg_def = @arg_defs[@arg_def_index]
593
+ if arg_def && arg_def.type == :required
594
+ @errors << ArgMissingError.new(name: arg_def.display_name)
595
+ end
596
+ unless @unmatched_positional.empty?
597
+ first_arg = @unmatched_positional.first
598
+ @errors <<
599
+ if @tool.runnable? || !@seen_flag_keys.empty?
600
+ ExtraArgumentsError.new(values: @unmatched_positional, value: first_arg)
601
+ else
602
+ dictionary = @loader.list_subtools(@tool.full_name).map(&:simple_name)
603
+ ToolUnrecognizedError.new(values: @tool.full_name + [first_arg],
604
+ value: first_arg,
605
+ suggestions: Compat.suggestions(first_arg, dictionary))
606
+ end
607
+ end
608
+ end
609
+
610
+ def finish_flag_groups
611
+ @tool.flag_groups.each do |group|
612
+ @errors += Array(group.validation_errors(@seen_flag_keys))
613
+ end
614
+ end
615
+
616
+ def finish_special_data
617
+ @data[Context::Key::USAGE_ERRORS] = @errors
618
+ @data[Context::Key::ARGS] = @parsed_args
619
+ @data[Context::Key::UNMATCHED_ARGS] = @unmatched_args
620
+ @data[Context::Key::UNMATCHED_POSITIONAL] = @unmatched_positional
621
+ @data[Context::Key::UNMATCHED_FLAGS] = @unmatched_flags
622
+ end
623
+ end
624
+ end