Chromium Code Reviews| 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]+)(.*)$'); |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 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 Loading... | |
| 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 Loading... | |
| 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 } |
| OLD | NEW |