Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(264)

Side by Side Diff: pkg/args/lib/src/parser.dart

Issue 12545013: Added the continueParsing option to ArgParser. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Fixed up documentation and the like. Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library args.src.parser; 5 library args.src.parser;
6 6
7 import '../args.dart'; 7 import '../args.dart';
8 8
9 final _SOLO_OPT = new RegExp(r'^-([a-zA-Z0-9])$'); 9 final _SOLO_OPT = new RegExp(r'^-([a-zA-Z0-9])$');
10 final _ABBR_OPT = new RegExp(r'^-([a-zA-Z0-9]+)(.*)$'); 10 final _ABBR_OPT = new RegExp(r'^-([a-zA-Z0-9]+)(.*)$');
11 final _LONG_OPT = new RegExp(r'^--([a-zA-Z\-_0-9]+)(=(.*))?$'); 11 final _LONG_OPT = new RegExp(r'^--([a-zA-Z\-_0-9]+)(=(.*))?$');
12 12
13 /** 13 /**
14 * The actual parsing class. Unlike [ArgParser] which is really more an "arg 14 * The actual parsing class. Unlike [ArgParser] which is really more an "arg
15 * grammar", this is the class that does the parsing and holds the mutable 15 * grammar", this is the class that does the parsing and holds the mutable
16 * state required during a parse. 16 * state required during a parse.
17 *
18 * If [parseAllOptions] is set, the parser will continue parsing even after it
19 * finds an argument that is neither an option nor a command. This allows
20 * options to be specified after command parameters.
21 *
22 * [parseAllOptions] is false by default, so when a non-option, non-command
23 * argument is encountered, it and any remaining arguments are passed to the
24 * leafmost command.
25 *
26 * An argument that looks like an option, but does not match any option will
27 * throw a [FormatException], regardless of the value of [parseAllOptions].
Bob Nystrom 2013/06/20 00:33:36 Redundant with ArgParser docs. Remove, I think.
Andrei Mouravski 2013/06/22 00:54:02 Done.
17 */ 28 */
18 class Parser { 29 class Parser {
19 /** 30 /**
20 * If parser is parsing a command's options, this will be the name of the 31 * If parser is parsing a command's options, this will be the name of the
21 * command. For top-level results, this returns `null`. 32 * command. For top-level results, this returns `null`.
22 */ 33 */
23 final String commandName; 34 final String commandName;
24 35
25 /** 36 /**
26 * The parser for the supercommand of this command parser, or `null` if this 37 * The parser for the supercommand of this command parser, or `null` if this
27 * is the top-level parser. 38 * is the top-level parser.
28 */ 39 */
29 final Parser parent; 40 final Parser parent;
30 41
42 /** If `true`, parser will continue after it sees a non-option argument. */
43 final bool parseAllOptions;
44
31 /** The grammar being parsed. */ 45 /** The grammar being parsed. */
32 final ArgParser grammar; 46 final ArgParser grammar;
33 47
34 /** The arguments being parsed. */ 48 /** The arguments being parsed. */
35 final List<String> args; 49 final List<String> args;
36 50
51 /** The remaining non-option, non-command arguments. */
52 List<String> rest;
53
37 /** The accumulated parsed options. */ 54 /** The accumulated parsed options. */
38 final Map results = {}; 55 final Map results = {};
39 56
40 Parser(this.commandName, this.grammar, this.args, [this.parent]); 57 Parser(this.commandName, this.grammar, this.args,
58 [this.parseAllOptions = false, this.parent, rest])
59 : this.rest = rest == null ? [] : rest;
Bob Nystrom 2013/06/20 00:33:36 How about having the field be: final rest = <Stri
Andrei Mouravski 2013/06/22 00:54:02 Done.
41 60
42 /** The current argument being parsed. */ 61 /** The current argument being parsed. */
43 String get current => args[0]; 62 String get current => args[0];
44 63
45 /** Parses the arguments. This can only be called once. */ 64 /** Parses the arguments. This can only be called once. */
46 ArgResults parse() { 65 ArgResults parse() {
47 var commandResults = null; 66 var commandResults = null;
48 67
49 // Initialize flags to their defaults. 68 // Initialize flags to their defaults.
50 grammar.options.forEach((name, option) { 69 grammar.options.forEach((name, option) {
51 if (option.allowMultiple) { 70 if (option.allowMultiple) {
52 results[name] = []; 71 results[name] = [];
53 } else { 72 } else {
54 results[name] = option.defaultValue; 73 results[name] = option.defaultValue;
55 } 74 }
56 }); 75 });
57 76
58 // Parse the args. 77 // Parse the args.
59 while (args.length > 0) { 78 while (args.length > 0) {
60 if (current == '--') { 79 if (current == '--') {
61 // Reached the argument terminator, so stop here. 80 // Reached the argument terminator, so stop here.
62 args.removeAt(0); 81 args.removeAt(0);
63 break; 82 break;
64 } 83 }
65 84
66 // Try to parse the current argument as a command. This happens before 85 // Try to parse the current argument as a command. This happens before
67 // options so that commands can have option-like names. 86 // options so that commands can have option-like names.
68 var command = grammar.commands[current]; 87 var command = grammar.commands[current];
69 if (command != null) { 88 if (command != null) {
89 validate(rest.isEmpty, 'Cannot specify arguments before a command.');
70 var commandName = args.removeAt(0); 90 var commandName = args.removeAt(0);
71 var commandParser = new Parser(commandName, command, args, this); 91 var commandParser = new Parser(commandName, command, args,
92 parseAllOptions, this, rest.toList());
72 commandResults = commandParser.parse(); 93 commandResults = commandParser.parse();
73 continue; 94
95 // All remaining arguments were passed to command so clear them here.
96 rest.clear();
97 break;
74 } 98 }
75 99
76 // Try to parse the current argument as an option. Note that the order 100 // Try to parse the current argument as an option. Note that the order
77 // here matters. 101 // here matters.
78 if (parseSoloOption()) continue; 102 if (isCurrentArgAnOption) {
79 if (parseAbbreviation(this)) continue; 103 if (parseSoloOption()) continue;
80 if (parseLongOption()) continue; 104 if (parseAbbreviation(this)) continue;
105 if (parseLongOption()) continue;
106 throw new FormatException(
107 'Could not find an option or flag "${args[0]}".');
Bob Nystrom 2013/06/20 00:33:36 Why is this throw needed? Shouldn't it have alread
Andrei Mouravski 2013/06/22 00:54:02 Because I'm more lenient inside the parse function
Bob Nystrom 2013/06/24 15:53:20 *Why* are you being more lenient? Those functions
Andrei Mouravski 2013/06/24 20:41:03 Done.
108 }
81 109
82 // If we got here, the argument doesn't look like an option, so stop. 110 // This argument is neither option nor command, so stop parsing unless
83 break; 111 // the [parseAllOptions] option is set.
112 if (!parseAllOptions) break;
113 rest.add(args.removeAt(0));
84 } 114 }
85 115
86 // Set unspecified multivalued arguments to their default value, 116 // Set unspecified multivalued arguments to their default value,
87 // if any, and invoke the callbacks. 117 // if any, and invoke the callbacks.
88 grammar.options.forEach((name, option) { 118 grammar.options.forEach((name, option) {
89 if (option.allowMultiple && 119 if (option.allowMultiple &&
90 results[name].length == 0 && 120 results[name].length == 0 &&
91 option.defaultValue != null) { 121 option.defaultValue != null) {
92 results[name].add(option.defaultValue); 122 results[name].add(option.defaultValue);
93 } 123 }
94 if (option.callback != null) option.callback(results[name]); 124 if (option.callback != null) option.callback(results[name]);
95 }); 125 });
96 126
97 // Add in the leftover arguments we didn't parse to the innermost command. 127 // Add in the leftover arguments we didn't parse to the innermost command.
98 var rest = args.toList(); 128 rest.addAll(args.toList());
99 args.clear(); 129 args.clear();
100 return new ArgResults(results, commandName, commandResults, rest); 130 return new ArgResults(results, commandName, commandResults, rest);
101 } 131 }
102 132
103 /** 133 /**
104 * Pulls the value for [option] from the second argument in [args]. Validates 134 * Pulls the value for [option] from the second argument in [args]. Validates
105 * that there is a valid value there. 135 * that there is a valid value there.
106 */ 136 */
107 void readNextArgAsValue(Option option) { 137 void readNextArgAsValue(Option option) {
108 // Take the option argument from the next command line arg. 138 // Take the option argument from the next command line arg.
109 validate(args.length > 0, 139 validate(args.length > 0,
110 'Missing argument for "${option.name}".'); 140 'Missing argument for "${option.name}".');
111 141
112 // Make sure it isn't an option itself. 142 // Make sure it isn't an option itself.
113 validate(!_ABBR_OPT.hasMatch(current) && !_LONG_OPT.hasMatch(current), 143 validate(!_ABBR_OPT.hasMatch(current) && !_LONG_OPT.hasMatch(current),
114 'Missing argument for "${option.name}".'); 144 'Missing argument for "${option.name}".');
115 145
116 setOption(results, option, current); 146 setOption(results, option, current);
117 args.removeAt(0); 147 args.removeAt(0);
118 } 148 }
119 149
150 /** Returns `true` if the current argument looks like an option. */
151 bool get isCurrentArgAnOption => [_SOLO_OPT, _ABBR_OPT, _LONG_OPT].any(
152 (re) => re.firstMatch(current) != null);
153
120 /** 154 /**
121 * Tries to parse the current argument as a "solo" option, which is a single 155 * Tries to parse the current argument as a "solo" option, which is a single
122 * hyphen followed by a single letter. We treat this differently than 156 * hyphen followed by a single letter. We treat this differently than
123 * collapsed abbreviations (like "-abc") to handle the possible value that 157 * collapsed abbreviations (like "-abc") to handle the possible value that
124 * may follow it. 158 * may follow it.
125 */ 159 */
126 bool parseSoloOption() { 160 bool parseSoloOption() {
127 var soloOpt = _SOLO_OPT.firstMatch(current); 161 var soloOpt = _SOLO_OPT.firstMatch(current);
128 if (soloOpt == null) return false; 162 if (soloOpt == null) return false;
129 163
130 var option = grammar.findByAbbreviation(soloOpt[1]); 164 var option = grammar.findByAbbreviation(soloOpt[1]);
131 if (option == null) { 165 if (option == null) {
132 // Walk up to the parent command if possible. 166 // Walk up to the parent command if possible.
133 validate(parent != null, 167 return tryParseOnParent((p) => p.parseSoloOption(),
134 'Could not find an option or flag "-${soloOpt[1]}".'); 168 'Could not find an option or flag "-${soloOpt[1]}".');
135 return parent.parseSoloOption();
136 } 169 }
137 170
138 args.removeAt(0); 171 args.removeAt(0);
139 172
140 if (option.isFlag) { 173 if (option.isFlag) {
141 setOption(results, option, true); 174 setOption(results, option, true);
142 } else { 175 } else {
143 readNextArgAsValue(option); 176 readNextArgAsValue(option);
144 } 177 }
145 178
146 return true; 179 return true;
147 } 180 }
148 181
149 /** 182 /**
150 * Tries to parse the current argument as a series of collapsed abbreviations 183 * Tries to parse the current argument as a series of collapsed abbreviations
151 * (like "-abc") or a single abbreviation with the value directly attached 184 * (like "-abc") or a single abbreviation with the value directly attached
152 * to it (like "-mrelease"). 185 * to it (like "-mrelease").
153 */ 186 */
154 bool parseAbbreviation(Parser innermostCommand) { 187 bool parseAbbreviation(Parser innermostCommand) {
155 var abbrOpt = _ABBR_OPT.firstMatch(current); 188 var abbrOpt = _ABBR_OPT.firstMatch(current);
156 if (abbrOpt == null) return false; 189 if (abbrOpt == null) return false;
157 190
158 // If the first character is the abbreviation for a non-flag option, then 191 // If the first character is the abbreviation for a non-flag option, then
159 // the rest is the value. 192 // the rest is the value.
160 var c = abbrOpt[1].substring(0, 1); 193 var c = abbrOpt[1].substring(0, 1);
161 var first = grammar.findByAbbreviation(c); 194 var first = grammar.findByAbbreviation(c);
162 if (first == null) { 195 if (first == null) {
163 // Walk up to the parent command if possible. 196 // Walk up to the parent command if possible.
164 validate(parent != null, 197 return tryParseOnParent((p) => p.parseAbbreviation(innermostCommand),
165 'Could not find an option with short name "-$c".'); 198 'Could not find an option with short name "-$c".');
166 return parent.parseAbbreviation(innermostCommand);
167 } else if (!first.isFlag) { 199 } else if (!first.isFlag) {
168 // The first character is a non-flag option, so the rest must be the 200 // The first character is a non-flag option, so the rest must be the
169 // value. 201 // value.
170 var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}'; 202 var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}';
171 setOption(results, first, value); 203 setOption(results, first, value);
172 } else { 204 } else {
173 // If we got some non-flag characters, then it must be a value, but 205 // If we got some non-flag characters, then it must be a value, but
174 // if we got here, it's a flag, which is wrong. 206 // if we got here, it's a flag, which is wrong.
175 validate(abbrOpt[2] == '', 207 validate(abbrOpt[2] == '',
176 'Option "-$c" is a flag and cannot handle value ' 208 'Option "-$c" is a flag and cannot handle value '
(...skipping 10 matching lines...) Expand all
187 } 219 }
188 220
189 args.removeAt(0); 221 args.removeAt(0);
190 return true; 222 return true;
191 } 223 }
192 224
193 void parseShortFlag(String c) { 225 void parseShortFlag(String c) {
194 var option = grammar.findByAbbreviation(c); 226 var option = grammar.findByAbbreviation(c);
195 if (option == null) { 227 if (option == null) {
196 // Walk up to the parent command if possible. 228 // Walk up to the parent command if possible.
197 validate(parent != null, 229 tryParseOnParent((p) => p.parseShortFlag(c),
198 'Could not find an option with short name "-$c".'); 230 'Could not find an option with short name "-$c".');
199 parent.parseShortFlag(c);
200 return; 231 return;
201 } 232 }
202 233
203 // In a list of short options, only the first can be a non-flag. If 234 // In a list of short options, only the first can be a non-flag. If
204 // we get here we've checked that already. 235 // we get here we've checked that already.
205 validate(option.isFlag, 236 validate(option.isFlag,
206 'Option "-$c" must be a flag to be in a collapsed "-".'); 237 'Option "-$c" must be a flag to be in a collapsed "-".');
207 238
208 setOption(results, option, true); 239 setOption(results, option, true);
209 } 240 }
(...skipping 21 matching lines...) Expand all
231 } else { 262 } else {
232 // Option like --foo, so look for the value as the next arg. 263 // Option like --foo, so look for the value as the next arg.
233 readNextArgAsValue(option); 264 readNextArgAsValue(option);
234 } 265 }
235 } else if (name.startsWith('no-')) { 266 } else if (name.startsWith('no-')) {
236 // See if it's a negated flag. 267 // See if it's a negated flag.
237 name = name.substring('no-'.length); 268 name = name.substring('no-'.length);
238 option = grammar.options[name]; 269 option = grammar.options[name];
239 if (option == null) { 270 if (option == null) {
240 // Walk up to the parent command if possible. 271 // Walk up to the parent command if possible.
241 validate(parent != null, 'Could not find an option named "$name".'); 272 return tryParseOnParent((p) => p.parseLongOption(),
242 return parent.parseLongOption(); 273 'Could not find an option named "$name".');
243 } 274 }
244 275
245 args.removeAt(0); 276 args.removeAt(0);
246 validate(option.isFlag, 'Cannot negate non-flag option "$name".'); 277 validate(option.isFlag, 'Cannot negate non-flag option "$name".');
247 validate(option.negatable, 'Cannot negate option "$name".'); 278 validate(option.negatable, 'Cannot negate option "$name".');
248 279
249 setOption(results, option, false); 280 setOption(results, option, false);
250 } else { 281 } else {
251 // Walk up to the parent command if possible. 282 // Walk up to the parent command if possible.
252 validate(parent != null, 'Could not find an option named "$name".'); 283 return tryParseOnParent((p) => p.parseLongOption(),
253 return parent.parseLongOption(); 284 'Could not find an option named "$name".');
254 } 285 }
255 286
256 return true; 287 return true;
257 } 288 }
258 289
259 /** 290 /**
260 * Called during parsing to validate the arguments. Throws a 291 * Called during parsing to validate the arguments. Throws a
261 * [FormatException] if [condition] is `false`. 292 * [FormatException] if [condition] is `false`.
262 */ 293 */
263 validate(bool condition, String message) { 294 validate(bool condition, String message) {
264 if (!condition) throw new FormatException(message); 295 if (!condition) throw new FormatException(message);
265 } 296 }
266 297
298 /**
299 * Tries to run parseFunc recursively on this parser's parent parser.
300 *
301 * Returns `true` if the parse succeeded on any ancestor of this parser.
302 * Returns `false` if no [Parser] accepted the [parseFunc] and
303 * [parseAllOptions] is true.
304 * Throws a [FormatException] exception otherwise.
Bob Nystrom 2013/06/20 00:33:36 This documents *what* it does, but not *why* it do
Andrei Mouravski 2013/06/22 00:54:02 Done.
305 */
306 bool tryParseOnParent(bool parseFunc(Parser p), String message) {
307 if (parent != null) {
308 return parseFunc(parent);
309 } else if (parseAllOptions) {
310 return false;
Bob Nystrom 2013/06/20 00:33:36 What's this case for? If we can't parse it, should
Andrei Mouravski 2013/06/22 00:54:02 It's to fall through when parsing.
311 } else {
312 throw new FormatException(message);
313 }
314 }
315
267 /** Validates and stores [value] as the value for [option]. */ 316 /** Validates and stores [value] as the value for [option]. */
268 setOption(Map results, Option option, value) { 317 setOption(Map results, Option option, value) {
269 // See if it's one of the allowed values. 318 // See if it's one of the allowed values.
270 if (option.allowed != null) { 319 if (option.allowed != null) {
271 validate(option.allowed.any((allow) => allow == value), 320 validate(option.allowed.any((allow) => allow == value),
272 '"$value" is not an allowed value for option "${option.name}".'); 321 '"$value" is not an allowed value for option "${option.name}".');
273 } 322 }
274 323
275 if (option.allowMultiple) { 324 if (option.allowMultiple) {
276 results[option.name].add(value); 325 results[option.name].add(value);
277 } else { 326 } else {
278 results[option.name] = value; 327 results[option.name] = value;
279 } 328 }
280 } 329 }
281 } 330 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698