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

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

Issue 11819068: Add command support to args. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Fix a couple of type annotations. Created 7 years, 11 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
« no previous file with comments | « pkg/args/lib/args.dart ('k') | pkg/args/lib/src/usage.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 library args.src.parser;
6
7 import '../args.dart';
8
9 final _SOLO_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]+)(=(.*))?$');
12
13 /**
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
16 * state required during a parse.
17 */
18 class Parser {
19 /**
20 * If parser is parsing a command's options, this will be the name of the
21 * command. For top-level results, this returns `null`.
22 */
23 final String commandName;
24
25 /**
26 * The parser for the supercommand of this command parser, or `null` if this
27 * is the top-level parser.
28 */
29 final Parser parent;
30
31 /** The grammar being parsed. */
32 final ArgParser grammar;
33
34 /** The arguments being parsed. */
35 final List<String> args;
36
37 /** The accumulated parsed options. */
38 final Map results = {};
39
40 Parser(this.commandName, this.grammar, this.args, [this.parent]);
41
42 /** The current argument being parsed. */
43 String get current => args[0];
44
45 /** Parses the arguments. This can only be called once. */
46 ArgResults parse() {
47 var commandResults = null;
48
49 // Initialize flags to their defaults.
50 grammar.options.forEach((name, option) {
51 if (option.allowMultiple) {
52 results[name] = [];
53 } else {
54 results[name] = option.defaultValue;
55 }
56 });
57
58 // Parse the args.
59 while (args.length > 0) {
60 if (current == '--') {
61 // Reached the argument terminator, so stop here.
62 args.removeAt(0);
63 break;
64 }
65
66 // Try to parse the current argument as a command. This happens before
67 // options so that commands can have option-like names.
68 var command = grammar.commands[current];
69 if (command != null) {
70 var commandName = args.removeAt(0);
71 var commandParser = new Parser(commandName, command, args, this);
72 commandResults = commandParser.parse();
73 continue;
74 }
75
76 // Try to parse the current argument as an option. Note that the order
77 // here matters.
78 if (parseSoloOption()) continue;
79 if (parseAbbreviation(this)) continue;
80 if (parseLongOption()) continue;
81
82 // If we got here, the argument doesn't look like an option, so stop.
83 break;
84 }
85
86 // Set unspecified multivalued arguments to their default value,
87 // if any, and invoke the callbacks.
88 grammar.options.forEach((name, option) {
89 if (option.allowMultiple &&
90 results[name].length == 0 &&
91 option.defaultValue != null) {
92 results[name].add(option.defaultValue);
93 }
94 if (option.callback != null) option.callback(results[name]);
95 });
96
97 // Add in the leftover arguments we didn't parse to the innermost command.
98 var rest = args.toList();
99 args.clear();
100 return new ArgResults(results, commandName, commandResults, rest);
101 }
102
103 /**
104 * Pulls the value for [option] from the second argument in [args]. Validates
105 * that there is a valid value there.
106 */
107 void readNextArgAsValue(Option option) {
108 // Take the option argument from the next command line arg.
109 validate(args.length > 0,
110 'Missing argument for "${option.name}".');
111
112 // Make sure it isn't an option itself.
113 validate(!_ABBR_OPT.hasMatch(current) && !_LONG_OPT.hasMatch(current),
114 'Missing argument for "${option.name}".');
115
116 setOption(results, option, current);
117 args.removeAt(0);
118 }
119
120 /**
121 * 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
123 * collapsed abbreviations (like "-abc") to handle the possible value that
124 * may follow it.
125 */
126 bool parseSoloOption() {
127 var soloOpt = _SOLO_OPT.firstMatch(current);
128 if (soloOpt == null) return false;
129
130 var option = grammar.findByAbbreviation(soloOpt[1]);
131 if (option == null) {
132 // Walk up to the parent command if possible.
133 validate(parent != null,
134 'Could not find an option or flag "-${soloOpt[1]}".');
135 return parent.parseSoloOption();
136 }
137
138 args.removeAt(0);
139
140 if (option.isFlag) {
141 setOption(results, option, true);
142 } else {
143 readNextArgAsValue(option);
144 }
145
146 return true;
147 }
148
149 /**
150 * Tries to parse the current argument as a series of collapsed abbreviations
151 * (like "-abc") or a single abbreviation with the value directly attached
152 * to it (like "-mrelease").
153 */
154 bool parseAbbreviation(Parser innermostCommand) {
155 var abbrOpt = _ABBR_OPT.firstMatch(current);
156 if (abbrOpt == null) return false;
157
158 // If the first character is the abbreviation for a non-flag option, then
159 // the rest is the value.
160 var c = abbrOpt[1].substring(0, 1);
161 var first = grammar.findByAbbreviation(c);
162 if (first == null) {
163 // Walk up to the parent command if possible.
164 validate(parent != null,
165 'Could not find an option with short name "-$c".');
166 return parent.parseAbbreviation(innermostCommand);
167 } else if (!first.isFlag) {
168 // The first character is a non-flag option, so the rest must be the
169 // value.
170 var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}';
171 setOption(results, first, value);
172 } else {
173 // 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.
175 validate(abbrOpt[2] == '',
176 'Option "-$c" is a flag and cannot handle value '
177 '"${abbrOpt[1].substring(1)}${abbrOpt[2]}".');
178
179 // Not an option, so all characters should be flags.
180 // We use "innermostCommand" here so that if a parent command parses the
181 // *first* letter, subcommands can still be found to parse the other
182 // letters.
183 for (var i = 0; i < abbrOpt[1].length; i++) {
184 var c = abbrOpt[1].substring(i, i + 1);
185 innermostCommand.parseShortFlag(c);
186 }
187 }
188
189 args.removeAt(0);
190 return true;
191 }
192
193 void parseShortFlag(String c) {
194 var option = grammar.findByAbbreviation(c);
195 if (option == null) {
196 // Walk up to the parent command if possible.
197 validate(parent != null,
198 'Could not find an option with short name "-$c".');
199 parent.parseShortFlag(c);
200 return;
201 }
202
203 // In a list of short options, only the first can be a non-flag. If
204 // we get here we've checked that already.
205 validate(option.isFlag,
206 'Option "-$c" must be a flag to be in a collapsed "-".');
207
208 setOption(results, option, true);
209 }
210
211 /**
212 * Tries to parse the current argument as a long-form named option, which
213 * may include a value like "--mode=release" or "--mode release".
214 */
215 bool parseLongOption() {
216 var longOpt = _LONG_OPT.firstMatch(current);
217 if (longOpt == null) return false;
218
219 var name = longOpt[1];
220 var option = grammar.options[name];
221 if (option != null) {
222 args.removeAt(0);
223 if (option.isFlag) {
224 validate(longOpt[3] == null,
225 'Flag option "$name" should not be given a value.');
226
227 setOption(results, option, true);
228 } else if (longOpt[3] != null) {
229 // We have a value like --foo=bar.
230 setOption(results, option, longOpt[3]);
231 } else {
232 // Option like --foo, so look for the value as the next arg.
233 readNextArgAsValue(option);
234 }
235 } else if (name.startsWith('no-')) {
236 // See if it's a negated flag.
237 name = name.substring('no-'.length);
238 option = grammar.options[name];
239 if (option == null) {
240 // Walk up to the parent command if possible.
241 validate(parent != null, 'Could not find an option named "$name".');
242 return parent.parseLongOption();
243 }
244
245 args.removeAt(0);
246 validate(option.isFlag, 'Cannot negate non-flag option "$name".');
247 validate(option.negatable, 'Cannot negate option "$name".');
248
249 setOption(results, option, false);
250 } else {
251 // Walk up to the parent command if possible.
252 validate(parent != null, 'Could not find an option named "$name".');
253 return parent.parseLongOption();
254 }
255
256 return true;
257 }
258
259 /**
260 * Called during parsing to validate the arguments. Throws a
261 * [FormatException] if [condition] is `false`.
262 */
263 validate(bool condition, String message) {
264 if (!condition) throw new FormatException(message);
265 }
266
267 /** Validates and stores [value] as the value for [option]. */
268 setOption(Map results, Option option, value) {
269 // See if it's one of the allowed values.
270 if (option.allowed != null) {
271 validate(option.allowed.any((allow) => allow == value),
272 '"$value" is not an allowed value for option "${option.name}".');
273 }
274
275 if (option.allowMultiple) {
276 results[option.name].add(value);
277 } else {
278 results[option.name] = value;
279 }
280 }
281 }
OLDNEW
« no previous file with comments | « pkg/args/lib/args.dart ('k') | pkg/args/lib/src/usage.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698