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

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: 12 changes. 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]+)(.*)$');
(...skipping 10 matching lines...) Expand all
21 * command. For top-level results, this returns `null`. 21 * command. For top-level results, this returns `null`.
22 */ 22 */
23 final String commandName; 23 final String commandName;
24 24
25 /** 25 /**
26 * The parser for the supercommand of this command parser, or `null` if this 26 * The parser for the supercommand of this command parser, or `null` if this
27 * is the top-level parser. 27 * is the top-level parser.
28 */ 28 */
29 final Parser parent; 29 final Parser parent;
30 30
31 /** If `true`, parser will continue after it sees a non-option argument. */
Bob Nystrom 2013/06/24 15:53:20 "parser" -> "parsing" "it sees" -> ""
Andrei Mouravski 2013/06/24 20:41:04 Done.
32 final bool allowTrailingOptions;
33
31 /** The grammar being parsed. */ 34 /** The grammar being parsed. */
32 final ArgParser grammar; 35 final ArgParser grammar;
33 36
34 /** The arguments being parsed. */ 37 /** The arguments being parsed. */
35 final List<String> args; 38 final List<String> args;
36 39
40 /** The remaining non-option, non-command arguments. */
41 List<String> rest = <String>[];
42
37 /** The accumulated parsed options. */ 43 /** The accumulated parsed options. */
38 final Map results = {}; 44 final Map results = {};
39 45
40 Parser(this.commandName, this.grammar, this.args, [this.parent]); 46 Parser(this.commandName, this.grammar, this.args,
47 [this.allowTrailingOptions = false, this.parent, rest]) {
Bob Nystrom 2013/06/24 15:53:20 Ugh. Boolean parameters should be named. Since you
Andrei Mouravski 2013/06/24 20:41:04 Done.
48 if (rest != null) this.rest.addAll(rest);
49 }
50
41 51
42 /** The current argument being parsed. */ 52 /** The current argument being parsed. */
43 String get current => args[0]; 53 String get current => args[0];
44 54
45 /** Parses the arguments. This can only be called once. */ 55 /** Parses the arguments. This can only be called once. */
46 ArgResults parse() { 56 ArgResults parse() {
47 var commandResults = null; 57 var commandResults = null;
48 58
49 // Initialize flags to their defaults. 59 // Initialize flags to their defaults.
50 grammar.options.forEach((name, option) { 60 grammar.options.forEach((name, option) {
51 if (option.allowMultiple) { 61 if (option.allowMultiple) {
52 results[name] = []; 62 results[name] = [];
53 } else { 63 } else {
54 results[name] = option.defaultValue; 64 results[name] = option.defaultValue;
55 } 65 }
56 }); 66 });
57 67
58 // Parse the args. 68 // Parse the args.
59 while (args.length > 0) { 69 while (args.length > 0) {
60 if (current == '--') { 70 if (current == '--') {
61 // Reached the argument terminator, so stop here. 71 // Reached the argument terminator, so stop here.
62 args.removeAt(0); 72 args.removeAt(0);
63 break; 73 break;
64 } 74 }
65 75
66 // Try to parse the current argument as a command. This happens before 76 // Try to parse the current argument as a command. This happens before
67 // options so that commands can have option-like names. 77 // options so that commands can have option-like names.
68 var command = grammar.commands[current]; 78 var command = grammar.commands[current];
69 if (command != null) { 79 if (command != null) {
80 validate(rest.isEmpty, 'Cannot specify arguments before a command.');
70 var commandName = args.removeAt(0); 81 var commandName = args.removeAt(0);
71 var commandParser = new Parser(commandName, command, args, this); 82 var commandParser = new Parser(commandName, command, args,
83 allowTrailingOptions, this, rest.toList());
Bob Nystrom 2013/06/24 15:53:20 .toList() isn't needed.
Andrei Mouravski 2013/06/24 20:41:04 Done.
72 commandResults = commandParser.parse(); 84 commandResults = commandParser.parse();
73 continue; 85
86 // All remaining arguments were passed to command so clear them here.
87 rest.clear();
88 break;
74 } 89 }
75 90
76 // Try to parse the current argument as an option. Note that the order 91 // Try to parse the current argument as an option. Note that the order
77 // here matters. 92 // here matters.
78 if (parseSoloOption()) continue; 93 if (isCurrentArgAnOption) {
79 if (parseAbbreviation(this)) continue; 94 if (parseSoloOption()) continue;
80 if (parseLongOption()) continue; 95 if (parseAbbreviation(this)) continue;
96 if (parseLongOption()) continue;
97 throw new FormatException(
98 'Could not find an option or flag "${args[0]}".');
99 }
81 100
82 // If we got here, the argument doesn't look like an option, so stop. 101 // This argument is neither option nor command, so stop parsing unless
83 break; 102 // the [allowTrailingOptions] option is set.
103 if (!allowTrailingOptions) break;
104 rest.add(args.removeAt(0));
84 } 105 }
85 106
86 // Set unspecified multivalued arguments to their default value, 107 // Set unspecified multivalued arguments to their default value,
87 // if any, and invoke the callbacks. 108 // if any, and invoke the callbacks.
88 grammar.options.forEach((name, option) { 109 grammar.options.forEach((name, option) {
89 if (option.allowMultiple && 110 if (option.allowMultiple &&
90 results[name].length == 0 && 111 results[name].length == 0 &&
91 option.defaultValue != null) { 112 option.defaultValue != null) {
92 results[name].add(option.defaultValue); 113 results[name].add(option.defaultValue);
93 } 114 }
94 if (option.callback != null) option.callback(results[name]); 115 if (option.callback != null) option.callback(results[name]);
95 }); 116 });
96 117
97 // Add in the leftover arguments we didn't parse to the innermost command. 118 // Add in the leftover arguments we didn't parse to the innermost command.
98 var rest = args.toList(); 119 rest.addAll(args.toList());
Bob Nystrom 2013/06/24 15:53:20 Don't need .toList() here.
Andrei Mouravski 2013/06/24 20:41:04 Done.
99 args.clear(); 120 args.clear();
100 return new ArgResults(results, commandName, commandResults, rest); 121 return new ArgResults(results, commandName, commandResults, rest);
101 } 122 }
102 123
103 /** 124 /**
104 * Pulls the value for [option] from the second argument in [args]. Validates 125 * Pulls the value for [option] from the second argument in [args]. Validates
105 * that there is a valid value there. 126 * that there is a valid value there.
106 */ 127 */
107 void readNextArgAsValue(Option option) { 128 void readNextArgAsValue(Option option) {
108 // Take the option argument from the next command line arg. 129 // Take the option argument from the next command line arg.
109 validate(args.length > 0, 130 validate(args.length > 0,
110 'Missing argument for "${option.name}".'); 131 'Missing argument for "${option.name}".');
111 132
112 // Make sure it isn't an option itself. 133 // Make sure it isn't an option itself.
113 validate(!_ABBR_OPT.hasMatch(current) && !_LONG_OPT.hasMatch(current), 134 validate(!_ABBR_OPT.hasMatch(current) && !_LONG_OPT.hasMatch(current),
114 'Missing argument for "${option.name}".'); 135 'Missing argument for "${option.name}".');
115 136
116 setOption(results, option, current); 137 setOption(results, option, current);
117 args.removeAt(0); 138 args.removeAt(0);
118 } 139 }
119 140
141 /** Returns `true` if the current argument looks like an option. */
142 bool get isCurrentArgAnOption => [_SOLO_OPT, _ABBR_OPT, _LONG_OPT].any(
143 (re) => re.firstMatch(current) != null);
144
120 /** 145 /**
121 * Tries to parse the current argument as a "solo" option, which is a single 146 * 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 147 * hyphen followed by a single letter. We treat this differently than
123 * collapsed abbreviations (like "-abc") to handle the possible value that 148 * collapsed abbreviations (like "-abc") to handle the possible value that
124 * may follow it. 149 * may follow it.
125 */ 150 */
126 bool parseSoloOption() { 151 bool parseSoloOption() {
127 var soloOpt = _SOLO_OPT.firstMatch(current); 152 var soloOpt = _SOLO_OPT.firstMatch(current);
128 if (soloOpt == null) return false; 153 if (soloOpt == null) return false;
129 154
130 var option = grammar.findByAbbreviation(soloOpt[1]); 155 var option = grammar.findByAbbreviation(soloOpt[1]);
131 if (option == null) { 156 if (option == null) {
132 // Walk up to the parent command if possible. 157 // Walk up to the parent command if possible.
133 validate(parent != null, 158 return tryParseOnParent((p) => p.parseSoloOption(),
134 'Could not find an option or flag "-${soloOpt[1]}".'); 159 'Could not find an option or flag "-${soloOpt[1]}".');
135 return parent.parseSoloOption();
136 } 160 }
137 161
138 args.removeAt(0); 162 args.removeAt(0);
139 163
140 if (option.isFlag) { 164 if (option.isFlag) {
141 setOption(results, option, true); 165 setOption(results, option, true);
142 } else { 166 } else {
143 readNextArgAsValue(option); 167 readNextArgAsValue(option);
144 } 168 }
145 169
146 return true; 170 return true;
147 } 171 }
148 172
149 /** 173 /**
150 * Tries to parse the current argument as a series of collapsed abbreviations 174 * Tries to parse the current argument as a series of collapsed abbreviations
151 * (like "-abc") or a single abbreviation with the value directly attached 175 * (like "-abc") or a single abbreviation with the value directly attached
152 * to it (like "-mrelease"). 176 * to it (like "-mrelease").
153 */ 177 */
154 bool parseAbbreviation(Parser innermostCommand) { 178 bool parseAbbreviation(Parser innermostCommand) {
155 var abbrOpt = _ABBR_OPT.firstMatch(current); 179 var abbrOpt = _ABBR_OPT.firstMatch(current);
156 if (abbrOpt == null) return false; 180 if (abbrOpt == null) return false;
157 181
158 // If the first character is the abbreviation for a non-flag option, then 182 // If the first character is the abbreviation for a non-flag option, then
159 // the rest is the value. 183 // the rest is the value.
160 var c = abbrOpt[1].substring(0, 1); 184 var c = abbrOpt[1].substring(0, 1);
161 var first = grammar.findByAbbreviation(c); 185 var first = grammar.findByAbbreviation(c);
162 if (first == null) { 186 if (first == null) {
163 // Walk up to the parent command if possible. 187 // Walk up to the parent command if possible.
164 validate(parent != null, 188 return tryParseOnParent((p) => p.parseAbbreviation(innermostCommand),
165 'Could not find an option with short name "-$c".'); 189 'Could not find an option with short name "-$c".');
166 return parent.parseAbbreviation(innermostCommand);
167 } else if (!first.isFlag) { 190 } else if (!first.isFlag) {
168 // The first character is a non-flag option, so the rest must be the 191 // The first character is a non-flag option, so the rest must be the
169 // value. 192 // value.
170 var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}'; 193 var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}';
171 setOption(results, first, value); 194 setOption(results, first, value);
172 } else { 195 } else {
173 // If we got some non-flag characters, then it must be a value, but 196 // 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. 197 // if we got here, it's a flag, which is wrong.
175 validate(abbrOpt[2] == '', 198 validate(abbrOpt[2] == '',
176 'Option "-$c" is a flag and cannot handle value ' 199 'Option "-$c" is a flag and cannot handle value '
(...skipping 10 matching lines...) Expand all
187 } 210 }
188 211
189 args.removeAt(0); 212 args.removeAt(0);
190 return true; 213 return true;
191 } 214 }
192 215
193 void parseShortFlag(String c) { 216 void parseShortFlag(String c) {
194 var option = grammar.findByAbbreviation(c); 217 var option = grammar.findByAbbreviation(c);
195 if (option == null) { 218 if (option == null) {
196 // Walk up to the parent command if possible. 219 // Walk up to the parent command if possible.
197 validate(parent != null, 220 tryParseOnParent((p) => p.parseShortFlag(c),
198 'Could not find an option with short name "-$c".'); 221 'Could not find an option with short name "-$c".');
199 parent.parseShortFlag(c);
200 return; 222 return;
201 } 223 }
202 224
203 // In a list of short options, only the first can be a non-flag. If 225 // In a list of short options, only the first can be a non-flag. If
204 // we get here we've checked that already. 226 // we get here we've checked that already.
205 validate(option.isFlag, 227 validate(option.isFlag,
206 'Option "-$c" must be a flag to be in a collapsed "-".'); 228 'Option "-$c" must be a flag to be in a collapsed "-".');
207 229
208 setOption(results, option, true); 230 setOption(results, option, true);
209 } 231 }
(...skipping 21 matching lines...) Expand all
231 } else { 253 } else {
232 // Option like --foo, so look for the value as the next arg. 254 // Option like --foo, so look for the value as the next arg.
233 readNextArgAsValue(option); 255 readNextArgAsValue(option);
234 } 256 }
235 } else if (name.startsWith('no-')) { 257 } else if (name.startsWith('no-')) {
236 // See if it's a negated flag. 258 // See if it's a negated flag.
237 name = name.substring('no-'.length); 259 name = name.substring('no-'.length);
238 option = grammar.options[name]; 260 option = grammar.options[name];
239 if (option == null) { 261 if (option == null) {
240 // Walk up to the parent command if possible. 262 // Walk up to the parent command if possible.
241 validate(parent != null, 'Could not find an option named "$name".'); 263 return tryParseOnParent((p) => p.parseLongOption(),
242 return parent.parseLongOption(); 264 'Could not find an option named "$name".');
243 } 265 }
244 266
245 args.removeAt(0); 267 args.removeAt(0);
246 validate(option.isFlag, 'Cannot negate non-flag option "$name".'); 268 validate(option.isFlag, 'Cannot negate non-flag option "$name".');
247 validate(option.negatable, 'Cannot negate option "$name".'); 269 validate(option.negatable, 'Cannot negate option "$name".');
248 270
249 setOption(results, option, false); 271 setOption(results, option, false);
250 } else { 272 } else {
251 // Walk up to the parent command if possible. 273 // Walk up to the parent command if possible.
252 validate(parent != null, 'Could not find an option named "$name".'); 274 return tryParseOnParent((p) => p.parseLongOption(),
253 return parent.parseLongOption(); 275 'Could not find an option named "$name".');
254 } 276 }
255 277
256 return true; 278 return true;
257 } 279 }
258 280
259 /** 281 /**
260 * Called during parsing to validate the arguments. Throws a 282 * Called during parsing to validate the arguments. Throws a
261 * [FormatException] if [condition] is `false`. 283 * [FormatException] if [condition] is `false`.
262 */ 284 */
263 validate(bool condition, String message) { 285 validate(bool condition, String message) {
264 if (!condition) throw new FormatException(message); 286 if (!condition) throw new FormatException(message);
265 } 287 }
266 288
289 /**
290 * Tries to run parseFunc recursively on this parser's parent parser.
291 *
292 * Returns `true` if the parse succeeded on any ancestor of this parser and
293 * means that the current argument was accepted.
294 * Returns `false` if no [Parser] accepted the [parseFunc] and
295 * [allowTrailingOptions] is true, which allows the current argument to be
296 * accepted elsewhere.
297 * Throws a [FormatException] exception otherwise because the current argument
298 * is not accepted by anything.
299 */
300 bool tryParseOnParent(bool parseFunc(Parser p), String message) {
301 if (parent != null) {
302 return parseFunc(parent);
303 } else if (allowTrailingOptions) {
304 return false;
305 } else {
306 throw new FormatException(message);
307 }
308 }
309
267 /** Validates and stores [value] as the value for [option]. */ 310 /** Validates and stores [value] as the value for [option]. */
268 setOption(Map results, Option option, value) { 311 setOption(Map results, Option option, value) {
269 // See if it's one of the allowed values. 312 // See if it's one of the allowed values.
270 if (option.allowed != null) { 313 if (option.allowed != null) {
271 validate(option.allowed.any((allow) => allow == value), 314 validate(option.allowed.any((allow) => allow == value),
272 '"$value" is not an allowed value for option "${option.name}".'); 315 '"$value" is not an allowed value for option "${option.name}".');
273 } 316 }
274 317
275 if (option.allowMultiple) { 318 if (option.allowMultiple) {
276 results[option.name].add(value); 319 results[option.name].add(value);
277 } else { 320 } else {
278 results[option.name] = value; 321 results[option.name] = value;
279 } 322 }
280 } 323 }
281 } 324 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698