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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -0
- data/LICENSE.md +16 -24
- data/README.md +307 -59
- data/docs/guide.md +44 -4
- data/lib/toys-core.rb +58 -49
- data/lib/toys/acceptor.rb +672 -0
- data/lib/toys/alias.rb +106 -0
- data/lib/toys/arg_parser.rb +624 -0
- data/lib/toys/cli.rb +422 -181
- data/lib/toys/compat.rb +83 -0
- data/lib/toys/completion.rb +442 -0
- data/lib/toys/context.rb +354 -0
- data/lib/toys/core_version.rb +18 -26
- data/lib/toys/dsl/flag.rb +213 -56
- data/lib/toys/dsl/flag_group.rb +237 -51
- data/lib/toys/dsl/positional_arg.rb +210 -0
- data/lib/toys/dsl/tool.rb +968 -317
- data/lib/toys/errors.rb +46 -28
- data/lib/toys/flag.rb +821 -0
- data/lib/toys/flag_group.rb +282 -0
- data/lib/toys/input_file.rb +18 -26
- data/lib/toys/loader.rb +110 -100
- data/lib/toys/middleware.rb +24 -31
- data/lib/toys/mixin.rb +90 -59
- data/lib/toys/module_lookup.rb +125 -0
- data/lib/toys/positional_arg.rb +184 -0
- data/lib/toys/source_info.rb +192 -0
- data/lib/toys/standard_middleware/add_verbosity_flags.rb +38 -43
- data/lib/toys/standard_middleware/handle_usage_errors.rb +39 -40
- data/lib/toys/standard_middleware/set_default_descriptions.rb +111 -89
- data/lib/toys/standard_middleware/show_help.rb +130 -113
- data/lib/toys/standard_middleware/show_root_version.rb +29 -35
- data/lib/toys/standard_mixins/exec.rb +116 -78
- data/lib/toys/standard_mixins/fileutils.rb +16 -24
- data/lib/toys/standard_mixins/gems.rb +29 -30
- data/lib/toys/standard_mixins/highline.rb +34 -41
- data/lib/toys/standard_mixins/terminal.rb +72 -26
- data/lib/toys/template.rb +51 -35
- data/lib/toys/tool.rb +1161 -206
- data/lib/toys/utils/completion_engine.rb +171 -0
- data/lib/toys/utils/exec.rb +279 -182
- data/lib/toys/utils/gems.rb +58 -49
- data/lib/toys/utils/help_text.rb +117 -111
- data/lib/toys/utils/terminal.rb +69 -62
- data/lib/toys/wrappable_string.rb +162 -0
- metadata +24 -22
- data/lib/toys/definition/acceptor.rb +0 -191
- data/lib/toys/definition/alias.rb +0 -112
- data/lib/toys/definition/arg.rb +0 -140
- data/lib/toys/definition/flag.rb +0 -370
- data/lib/toys/definition/flag_group.rb +0 -205
- data/lib/toys/definition/source_info.rb +0 -190
- data/lib/toys/definition/tool.rb +0 -842
- data/lib/toys/dsl/arg.rb +0 -132
- data/lib/toys/runner.rb +0 -188
- data/lib/toys/standard_middleware.rb +0 -47
- data/lib/toys/utils/module_lookup.rb +0 -135
- data/lib/toys/utils/wrappable_string.rb +0 -165
data/lib/toys/alias.rb
ADDED
@@ -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
|