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