-
Notifications
You must be signed in to change notification settings - Fork 42
Expand file tree
/
Copy pathparser.dart
More file actions
411 lines (352 loc) · 14.1 KB
/
parser.dart
File metadata and controls
411 lines (352 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
import 'arg_parser.dart';
import 'arg_parser_exception.dart';
import 'arg_results.dart';
import 'option.dart';
/// The actual argument parsing class.
///
/// Unlike [ArgParser] which is really more an "arg grammar", this is the class
/// that does the parsing and holds the mutable state required during a parse.
class Parser {
/// If parser is parsing a command's options, this will be the name of the
/// command. For top-level results, this returns `null`.
final String? _commandName;
/// The parser for the supercommand of this command parser, or `null` if this
/// is the top-level parser.
final Parser? _parent;
/// The grammar being parsed.
final ArgParser _grammar;
/// The arguments being parsed.
final Queue<String> _args;
/// The remaining non-option, non-command arguments.
final List<String> _rest;
/// The accumulated parsed options.
final Map<String, dynamic> _results = <String, dynamic>{};
Parser(this._commandName, this._grammar, this._args,
[this._parent, List<String>? rest])
: _rest = [...?rest];
/// The current argument being parsed.
String get _current => _args.first;
/// Parses the arguments. This can only be called once.
ArgResults parse() {
var arguments = _args.toList();
if (_grammar.allowsAnything) {
return newArgResults(
_grammar, const {}, _commandName, null, arguments, arguments);
}
({String name, ArgParser parser})? command;
// Parse the args.
while (_args.isNotEmpty) {
if (_current == '--') {
// Reached the argument terminator, so stop here.
_args.removeFirst();
break;
}
// Try to parse the current argument as a command. This happens before
// options so that commands can have option-like names.
//
// Otherwise, if there is a default command then select it before parsing
// any arguments. We make exception for situations when help flag is
// passed because we want `program command -h` to display help for
// `command` rather than display help for the default subcommand of the
// `command`.
if (_grammar.commands[_current] case final parser?) {
command = (name: _args.removeFirst(), parser: parser);
break;
} else if (_grammar.defaultCommand case final defaultCommand?
when !(_current == '-h' || _current == '--help')) {
command =
(name: defaultCommand, parser: _grammar.commands[defaultCommand]!);
break;
}
// Try to parse the current argument as an option. Note that the order
// here matters.
if (_parseSoloOption()) continue;
if (_parseAbbreviation(this)) continue;
if (_parseLongOption()) continue;
// This argument is neither option nor command, so stop parsing unless
// the [allowTrailingOptions] option is set.
if (!_grammar.allowTrailingOptions) break;
_rest.add(_args.removeFirst());
}
// If there is a default command and we did not select any other commands
// and we don't have any trailing arguments then select the default
// command unless user requested help.
if (command == null && _rest.isEmpty && !_results.containsKey('help')) {
if (_grammar.defaultCommand case final defaultCommand?) {
command =
(name: defaultCommand, parser: _grammar.commands[defaultCommand]!);
}
}
ArgResults? commandResults;
if (command != null) {
_validate(_rest.isEmpty, 'Cannot specify arguments before a command.',
command.name);
var commandParser =
Parser(command.name, command.parser, _args, this, _rest);
try {
commandResults = commandParser.parse();
} on ArgParserException catch (error) {
throw ArgParserException(
error.message,
[command.name, ...error.commands],
error.argumentName,
error.source,
error.offset);
}
// All remaining arguments were passed to command so clear them here.
_rest.clear();
}
// Check if mandatory and invoke existing callbacks.
_grammar.options.forEach((name, option) {
var parsedOption = _results[name];
if (option.isFlag && parsedOption is int) parsedOption = parsedOption > 0;
var callback = option.callback;
if (callback == null) return;
// Check if an option is mandatory and was passed; if not, throw an
// exception.
if (option.mandatory && parsedOption == null) {
throw ArgParserException('Option $name is mandatory.', null, name);
}
// ignore: avoid_dynamic_calls
callback(option.valueOrDefault(parsedOption));
});
// Add in the leftover arguments we didn't parse to the innermost command.
_rest.addAll(_args);
_args.clear();
return newArgResults(
_grammar, _results, _commandName, commandResults, _rest, arguments);
}
/// Pulls the value for [option] from the second argument in [_args].
///
/// Validates that there is a valid value there.
void _readNextArgAsValue(Option option, String arg) {
// Take the option argument from the next command line arg.
_validate(_args.isNotEmpty, 'Missing argument for "$arg".', arg);
_setOption(_results, option, _current, arg);
_args.removeFirst();
}
/// Tries to parse the current argument as a "solo" option, which is a single
/// hyphen followed by a single letter.
///
/// We treat this differently than collapsed abbreviations (like "-abc") to
/// handle the possible value that may follow it.
bool _parseSoloOption() {
// Hand coded regexp: r'^-([a-zA-Z0-9])$'
// Length must be two, hyphen followed by any letter/digit.
if (_current.length != 2) return false;
if (!_current.startsWith('-')) return false;
var opt = _current[1];
if (!_isLetterOrDigit(opt.codeUnitAt(0))) return false;
return _handleSoloOption(opt);
}
bool _handleSoloOption(String opt) {
var option = _grammar.findByAbbreviation(opt);
if (option == null) {
// Walk up to the parent command if possible.
_validate(_parent != null, 'Could not find an option or flag "-$opt".',
'-$opt');
return _parent!._handleSoloOption(opt);
}
_args.removeFirst();
if (option.isFlag) {
_setFlag(_results, option, true);
} else {
_readNextArgAsValue(option, '-$opt');
}
return true;
}
/// Tries to parse the current argument as a series of collapsed abbreviations
/// (like "-abc") or a single abbreviation with the value directly attached
/// to it (like "-mrelease").
bool _parseAbbreviation(Parser innermostCommand) {
// Hand coded regexp: r'^-([a-zA-Z0-9]+)(.*)$'
// Hyphen then at least one letter/digit then zero or more
// anything-but-newlines.
if (_current.length < 2) return false;
if (!_current.startsWith('-')) return false;
// Find where we go from letters/digits to rest.
var index = 1;
while (index < _current.length &&
_isLetterOrDigit(_current.codeUnitAt(index))) {
++index;
}
// Must be at least one letter/digit.
if (index == 1) return false;
// If the first character is the abbreviation for a non-flag option, then
// the rest is the value.
var lettersAndDigits = _current.substring(1, index);
var rest = _current.substring(index);
if (rest.contains('\n') || rest.contains('\r')) return false;
return _handleAbbreviation(lettersAndDigits, rest, innermostCommand);
}
bool _handleAbbreviation(
String lettersAndDigits, String rest, Parser innermostCommand) {
var c = lettersAndDigits.substring(0, 1);
var first = _grammar.findByAbbreviation(c);
if (first == null) {
// Walk up to the parent command if possible.
_validate(_parent != null,
'Could not find an option with short name "-$c".', '-$c');
return _parent!
._handleAbbreviation(lettersAndDigits, rest, innermostCommand);
} else if (!first.isFlag) {
// The first character is a non-flag option, so the rest must be the
// value.
var value = '${lettersAndDigits.substring(1)}$rest';
_setOption(_results, first, value, '-$c');
} else {
// If we got some non-flag characters, then it must be a value, but
// if we got here, it's a flag, which is wrong.
_validate(
rest == '',
'Option "-$c" is a flag and cannot handle value '
'"${lettersAndDigits.substring(1)}$rest".',
'-$c');
// Not an option, so all characters should be flags.
// We use "innermostCommand" here so that if a parent command parses the
// *first* letter, subcommands can still be found to parse the other
// letters.
for (var i = 0; i < lettersAndDigits.length; i++) {
var c = lettersAndDigits.substring(i, i + 1);
innermostCommand._parseShortFlag(c);
}
}
_args.removeFirst();
return true;
}
void _parseShortFlag(String c) {
var option = _grammar.findByAbbreviation(c);
if (option == null) {
// Walk up to the parent command if possible.
_validate(_parent != null,
'Could not find an option with short name "-$c".', '-$c');
_parent!._parseShortFlag(c);
return;
}
// In a list of short options, only the first can be a non-flag. If
// we get here we've checked that already.
_validate(option.isFlag,
'Option "-$c" must be a flag to be in a collapsed "-".', '-$c');
_setFlag(_results, option, true);
}
/// Tries to parse the current argument as a long-form named option, which
/// may include a value like "--mode=release" or "--mode release".
bool _parseLongOption() {
// Hand coded regexp: r'^--([a-zA-Z\-_0-9]+)(=(.*))?$'
// Two hyphens then at least one letter/digit/hyphen, optionally an equal
// sign followed by zero or more anything-but-newlines.
if (!_current.startsWith('--')) return false;
var index = _current.indexOf('=');
var name =
index == -1 ? _current.substring(2) : _current.substring(2, index);
for (var i = 0; i != name.length; ++i) {
if (!_isLetterDigitHyphenOrUnderscore(name.codeUnitAt(i))) return false;
}
var value = index == -1 ? null : _current.substring(index + 1);
if (value != null && (value.contains('\n') || value.contains('\r'))) {
return false;
}
return _handleLongOption(name, value);
}
bool _handleLongOption(String name, String? value) {
var option = _grammar.findByNameOrAlias(name);
if (option != null) {
_args.removeFirst();
if (option.isFlag) {
_validate(value == null,
'Flag option "--$name" should not be given a value.', '--$name');
_setFlag(_results, option, true);
} else if (value != null) {
// We have a value like --foo=bar.
_setOption(_results, option, value, '--$name');
} else {
// Option like --foo, so look for the value as the next arg.
_readNextArgAsValue(option, '--$name');
}
} else if (name.startsWith('no-')) {
// See if it's a negated flag.
var positiveName = name.substring('no-'.length);
option = _grammar.findByNameOrAlias(positiveName);
if (option == null) {
// Walk up to the parent command if possible.
_validate(_parent != null, 'Could not find an option named "--$name".',
'--$name');
return _parent!._handleLongOption(name, value);
}
_args.removeFirst();
_validate(
option.isFlag, 'Cannot negate non-flag option "--$name".', '--$name');
_validate(
option.negatable!, 'Cannot negate option "--$name".', '--$name');
_setFlag(_results, option, false);
} else {
// Walk up to the parent command if possible.
_validate(_parent != null, 'Could not find an option named "--$name".',
'--$name');
return _parent!._handleLongOption(name, value);
}
return true;
}
/// Called during parsing to validate the arguments.
///
/// Throws an [ArgParserException] if [condition] is `false`.
void _validate(bool condition, String message,
[String? args, List<String>? source, int? offset]) {
if (!condition) {
throw ArgParserException(message, null, args, source, offset);
}
}
/// Validates and stores [value] as the value for [option], which must not be
/// a flag.
void _setOption(Map results, Option option, String value, String arg) {
assert(!option.isFlag);
if (!option.isMultiple) {
_validateAllowed(option, value, arg);
results[option.name] = value;
return;
}
var list = results.putIfAbsent(option.name, () => <String>[]) as List;
if (option.splitCommas) {
for (var element in value.split(',')) {
_validateAllowed(option, element, arg);
list.add(element);
}
} else {
_validateAllowed(option, value, arg);
list.add(value);
}
}
/// Validates and increases or resets the count for [option].
///
/// If [value] is `false`, resets the option's value to zero.
/// If `true`, increases the count of occurrences of the [option],
/// which must be a flag.
void _setFlag(Map results, Option option, bool value) {
assert(option.isFlag);
results[option.name] =
value ? ((results[option.name] as int?) ?? 0) + 1 : 0;
}
/// Validates that [value] is allowed as a value of [option].
void _validateAllowed(Option option, String value, String arg) {
if (option.allowed == null) return;
_validate(option.allowed!.contains(value),
'"$value" is not an allowed value for option "$arg".', arg);
}
}
bool _isLetterOrDigit(int codeUnit) =>
// Uppercase letters.
(codeUnit >= 65 && codeUnit <= 90) ||
// Lowercase letters.
(codeUnit >= 97 && codeUnit <= 122) ||
// Digits.
(codeUnit >= 48 && codeUnit <= 57);
bool _isLetterDigitHyphenOrUnderscore(int codeUnit) =>
_isLetterOrDigit(codeUnit) ||
// Hyphen.
codeUnit == 45 ||
// Underscore.
codeUnit == 95;