 Chromium Code Reviews
 Chromium Code Reviews Issue 12545013:
  Added the continueParsing option to ArgParser.  (Closed) 
  Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
    
  
    Issue 12545013:
  Added the continueParsing option to ArgParser.  (Closed) 
  Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart| OLD | NEW | 
|---|---|
| 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 Loading... | |
| 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 Loading... | |
| 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 } | 
| OLD | NEW |