OLD | NEW |
| (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 'arg_parser.dart'; | |
8 import 'arg_results.dart'; | |
9 import 'option.dart'; | |
10 | |
11 final _SOLO_OPT = new RegExp(r'^-([a-zA-Z0-9])$'); | |
12 final _ABBR_OPT = new RegExp(r'^-([a-zA-Z0-9]+)(.*)$'); | |
13 final _LONG_OPT = new RegExp(r'^--([a-zA-Z\-_0-9]+)(=(.*))?$'); | |
14 | |
15 /// The actual argument parsing class. | |
16 /// | |
17 /// Unlike [ArgParser] which is really more an "arg grammar", this is the class | |
18 /// that does the parsing and holds the mutable state required during a parse. | |
19 class Parser { | |
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 final String commandName; | |
23 | |
24 /// The parser for the supercommand of this command parser, or `null` if this | |
25 /// is the top-level parser. | |
26 final Parser parent; | |
27 | |
28 /// The grammar being parsed. | |
29 final ArgParser grammar; | |
30 | |
31 /// The arguments being parsed. | |
32 final List<String> args; | |
33 | |
34 /// The remaining non-option, non-command arguments. | |
35 final rest = <String>[]; | |
36 | |
37 /// The accumulated parsed options. | |
38 final Map<String, dynamic> results = <String, dynamic>{}; | |
39 | |
40 Parser(this.commandName, this.grammar, this.args, this.parent, rest) { | |
41 if (rest != null) this.rest.addAll(rest); | |
42 } | |
43 | |
44 /// The current argument being parsed. | |
45 String get current => args[0]; | |
46 | |
47 /// Parses the arguments. This can only be called once. | |
48 ArgResults parse() { | |
49 var arguments = args.toList(); | |
50 var commandResults = null; | |
51 | |
52 // Parse the args. | |
53 while (args.length > 0) { | |
54 if (current == '--') { | |
55 // Reached the argument terminator, so stop here. | |
56 args.removeAt(0); | |
57 break; | |
58 } | |
59 | |
60 // Try to parse the current argument as a command. This happens before | |
61 // options so that commands can have option-like names. | |
62 var command = grammar.commands[current]; | |
63 if (command != null) { | |
64 validate(rest.isEmpty, 'Cannot specify arguments before a command.'); | |
65 var commandName = args.removeAt(0); | |
66 var commandParser = new Parser(commandName, command, args, this, rest); | |
67 commandResults = commandParser.parse(); | |
68 | |
69 // All remaining arguments were passed to command so clear them here. | |
70 rest.clear(); | |
71 break; | |
72 } | |
73 | |
74 // Try to parse the current argument as an option. Note that the order | |
75 // here matters. | |
76 if (parseSoloOption()) continue; | |
77 if (parseAbbreviation(this)) continue; | |
78 if (parseLongOption()) continue; | |
79 | |
80 // This argument is neither option nor command, so stop parsing unless | |
81 // the [allowTrailingOptions] option is set. | |
82 if (!grammar.allowTrailingOptions) break; | |
83 rest.add(args.removeAt(0)); | |
84 } | |
85 | |
86 // Invoke the callbacks. | |
87 grammar.options.forEach((name, option) { | |
88 if (option.callback == null) return; | |
89 option.callback(option.getOrDefault(results[name])); | |
90 }); | |
91 | |
92 // Add in the leftover arguments we didn't parse to the innermost command. | |
93 rest.addAll(args); | |
94 args.clear(); | |
95 return newArgResults( | |
96 grammar, results, commandName, commandResults, rest, arguments); | |
97 } | |
98 | |
99 /// Pulls the value for [option] from the second argument in [args]. | |
100 /// | |
101 /// Validates that there is a valid value there. | |
102 void readNextArgAsValue(Option option) { | |
103 // Take the option argument from the next command line arg. | |
104 validate(args.length > 0, 'Missing argument for "${option.name}".'); | |
105 | |
106 setOption(results, option, current); | |
107 args.removeAt(0); | |
108 } | |
109 | |
110 /// Tries to parse the current argument as a "solo" option, which is a single | |
111 /// hyphen followed by a single letter. | |
112 /// | |
113 /// We treat this differently than collapsed abbreviations (like "-abc") to | |
114 /// handle the possible value that may follow it. | |
115 bool parseSoloOption() { | |
116 var soloOpt = _SOLO_OPT.firstMatch(current); | |
117 if (soloOpt == null) return false; | |
118 | |
119 var option = grammar.findByAbbreviation(soloOpt[1]); | |
120 if (option == null) { | |
121 // Walk up to the parent command if possible. | |
122 validate( | |
123 parent != null, 'Could not find an option or flag "-${soloOpt[1]}".'); | |
124 return parent.parseSoloOption(); | |
125 } | |
126 | |
127 args.removeAt(0); | |
128 | |
129 if (option.isFlag) { | |
130 setFlag(results, option, true); | |
131 } else { | |
132 readNextArgAsValue(option); | |
133 } | |
134 | |
135 return true; | |
136 } | |
137 | |
138 /// Tries to parse the current argument as a series of collapsed abbreviations | |
139 /// (like "-abc") or a single abbreviation with the value directly attached | |
140 /// to it (like "-mrelease"). | |
141 bool parseAbbreviation(Parser innermostCommand) { | |
142 var abbrOpt = _ABBR_OPT.firstMatch(current); | |
143 if (abbrOpt == null) return false; | |
144 | |
145 // If the first character is the abbreviation for a non-flag option, then | |
146 // the rest is the value. | |
147 var c = abbrOpt[1].substring(0, 1); | |
148 var first = grammar.findByAbbreviation(c); | |
149 if (first == null) { | |
150 // Walk up to the parent command if possible. | |
151 validate( | |
152 parent != null, 'Could not find an option with short name "-$c".'); | |
153 return parent.parseAbbreviation(innermostCommand); | |
154 } else if (!first.isFlag) { | |
155 // The first character is a non-flag option, so the rest must be the | |
156 // value. | |
157 var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}'; | |
158 setOption(results, first, value); | |
159 } else { | |
160 // If we got some non-flag characters, then it must be a value, but | |
161 // if we got here, it's a flag, which is wrong. | |
162 validate(abbrOpt[2] == '', | |
163 'Option "-$c" is a flag and cannot handle value ' | |
164 '"${abbrOpt[1].substring(1)}${abbrOpt[2]}".'); | |
165 | |
166 // Not an option, so all characters should be flags. | |
167 // We use "innermostCommand" here so that if a parent command parses the | |
168 // *first* letter, subcommands can still be found to parse the other | |
169 // letters. | |
170 for (var i = 0; i < abbrOpt[1].length; i++) { | |
171 var c = abbrOpt[1].substring(i, i + 1); | |
172 innermostCommand.parseShortFlag(c); | |
173 } | |
174 } | |
175 | |
176 args.removeAt(0); | |
177 return true; | |
178 } | |
179 | |
180 void parseShortFlag(String c) { | |
181 var option = grammar.findByAbbreviation(c); | |
182 if (option == null) { | |
183 // Walk up to the parent command if possible. | |
184 validate( | |
185 parent != null, 'Could not find an option with short name "-$c".'); | |
186 parent.parseShortFlag(c); | |
187 return; | |
188 } | |
189 | |
190 // In a list of short options, only the first can be a non-flag. If | |
191 // we get here we've checked that already. | |
192 validate( | |
193 option.isFlag, 'Option "-$c" must be a flag to be in a collapsed "-".'); | |
194 | |
195 setFlag(results, option, true); | |
196 } | |
197 | |
198 /// Tries to parse the current argument as a long-form named option, which | |
199 /// may include a value like "--mode=release" or "--mode release". | |
200 bool parseLongOption() { | |
201 var longOpt = _LONG_OPT.firstMatch(current); | |
202 if (longOpt == null) return false; | |
203 | |
204 var name = longOpt[1]; | |
205 var option = grammar.options[name]; | |
206 if (option != null) { | |
207 args.removeAt(0); | |
208 if (option.isFlag) { | |
209 validate(longOpt[3] == null, | |
210 'Flag option "$name" should not be given a value.'); | |
211 | |
212 setFlag(results, option, true); | |
213 } else if (longOpt[3] != null) { | |
214 // We have a value like --foo=bar. | |
215 setOption(results, option, longOpt[3]); | |
216 } else { | |
217 // Option like --foo, so look for the value as the next arg. | |
218 readNextArgAsValue(option); | |
219 } | |
220 } else if (name.startsWith('no-')) { | |
221 // See if it's a negated flag. | |
222 name = name.substring('no-'.length); | |
223 option = grammar.options[name]; | |
224 if (option == null) { | |
225 // Walk up to the parent command if possible. | |
226 validate(parent != null, 'Could not find an option named "$name".'); | |
227 return parent.parseLongOption(); | |
228 } | |
229 | |
230 args.removeAt(0); | |
231 validate(option.isFlag, 'Cannot negate non-flag option "$name".'); | |
232 validate(option.negatable, 'Cannot negate option "$name".'); | |
233 | |
234 setFlag(results, option, false); | |
235 } else { | |
236 // Walk up to the parent command if possible. | |
237 validate(parent != null, 'Could not find an option named "$name".'); | |
238 return parent.parseLongOption(); | |
239 } | |
240 | |
241 return true; | |
242 } | |
243 | |
244 /// Called during parsing to validate the arguments. | |
245 /// | |
246 /// Throws a [FormatException] if [condition] is `false`. | |
247 void validate(bool condition, String message) { | |
248 if (!condition) throw new FormatException(message); | |
249 } | |
250 | |
251 /// Validates and stores [value] as the value for [option], which must not be | |
252 /// a flag. | |
253 void setOption(Map results, Option option, String value) { | |
254 assert(!option.isFlag); | |
255 | |
256 if (!option.isMultiple) { | |
257 _validateAllowed(option, value); | |
258 results[option.name] = value; | |
259 return; | |
260 } | |
261 | |
262 var list = results.putIfAbsent(option.name, () => []); | |
263 | |
264 if (option.splitCommas) { | |
265 for (var element in value.split(",")) { | |
266 _validateAllowed(option, element); | |
267 list.add(element); | |
268 } | |
269 } else { | |
270 _validateAllowed(option, value); | |
271 list.add(value); | |
272 } | |
273 } | |
274 | |
275 /// Validates and stores [value] as the value for [option], which must be a | |
276 /// flag. | |
277 void setFlag(Map results, Option option, bool value) { | |
278 assert(option.isFlag); | |
279 results[option.name] = value; | |
280 } | |
281 | |
282 /// Validates that [value] is allowed as a value of [option]. | |
283 void _validateAllowed(Option option, String value) { | |
284 if (option.allowed == null) return; | |
285 | |
286 validate(option.allowed.contains(value), | |
287 '"$value" is not an allowed value for option "${option.name}".'); | |
288 } | |
289 } | |
OLD | NEW |