| 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 |