Index: pkg/args/lib/src/parser.dart |
diff --git a/pkg/args/lib/src/parser.dart b/pkg/args/lib/src/parser.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..d35f9ea4aff1bb67a1c20034b17574168d0c6502 |
--- /dev/null |
+++ b/pkg/args/lib/src/parser.dart |
@@ -0,0 +1,281 @@ |
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library args.src.parser; |
+ |
+import '../args.dart'; |
+ |
+final _SOLO_OPT = new RegExp(r'^-([a-zA-Z0-9])$'); |
+final _ABBR_OPT = new RegExp(r'^-([a-zA-Z0-9]+)(.*)$'); |
+final _LONG_OPT = new RegExp(r'^--([a-zA-Z\-_0-9]+)(=(.*))?$'); |
+ |
+/** |
+ * The actual parsing class. Unlike [ArgParser] which is really more an "arg |
+ * grammar", this is the class that does the parsing and holds the mutable |
+ * state required during a parse. |
+ */ |
+class Parser { |
+ /** |
+ * If parser is parsing a command's options, this will be the name of the |
+ * command. For top-level results, this returns `null`. |
+ */ |
+ final String commandName; |
+ |
+ /** |
+ * The parser for the supercommand of this command parser, or `null` if this |
+ * is the top-level parser. |
+ */ |
+ final Parser parent; |
+ |
+ /** The grammar being parsed. */ |
+ final ArgParser grammar; |
+ |
+ /** The arguments being parsed. */ |
+ final List<String> args; |
+ |
+ /** The accumulated parsed options. */ |
+ final Map results = {}; |
+ |
+ Parser(this.commandName, this.grammar, this.args, [this.parent]); |
+ |
+ /** The current argument being parsed. */ |
+ String get current => args[0]; |
+ |
+ /** Parses the arguments. This can only be called once. */ |
+ ArgResults parse() { |
+ var commandResults = null; |
+ |
+ // Initialize flags to their defaults. |
+ grammar.options.forEach((name, option) { |
+ if (option.allowMultiple) { |
+ results[name] = []; |
+ } else { |
+ results[name] = option.defaultValue; |
+ } |
+ }); |
+ |
+ // Parse the args. |
+ while (args.length > 0) { |
+ if (current == '--') { |
+ // Reached the argument terminator, so stop here. |
+ args.removeAt(0); |
+ break; |
+ } |
+ |
+ // Try to parse the current argument as a command. This happens before |
+ // options so that commands can have option-like names. |
+ var command = grammar.commands[current]; |
+ if (command != null) { |
+ var commandName = args.removeAt(0); |
+ var commandParser = new Parser(commandName, command, args, this); |
+ commandResults = commandParser.parse(); |
+ continue; |
+ } |
+ |
+ // Try to parse the current argument as an option. Note that the order |
+ // here matters. |
+ if (parseSoloOption()) continue; |
+ if (parseAbbreviation(this)) continue; |
+ if (parseLongOption()) continue; |
+ |
+ // If we got here, the argument doesn't look like an option, so stop. |
+ break; |
+ } |
+ |
+ // Set unspecified multivalued arguments to their default value, |
+ // if any, and invoke the callbacks. |
+ grammar.options.forEach((name, option) { |
+ if (option.allowMultiple && |
+ results[name].length == 0 && |
+ option.defaultValue != null) { |
+ results[name].add(option.defaultValue); |
+ } |
+ if (option.callback != null) option.callback(results[name]); |
+ }); |
+ |
+ // Add in the leftover arguments we didn't parse to the innermost command. |
+ var rest = args.toList(); |
+ args.clear(); |
+ return new ArgResults(results, commandName, commandResults, rest); |
+ } |
+ |
+ /** |
+ * Pulls the value for [option] from the second argument in [args]. Validates |
+ * that there is a valid value there. |
+ */ |
+ void readNextArgAsValue(Option option) { |
+ // Take the option argument from the next command line arg. |
+ validate(args.length > 0, |
+ 'Missing argument for "${option.name}".'); |
+ |
+ // Make sure it isn't an option itself. |
+ validate(!_ABBR_OPT.hasMatch(current) && !_LONG_OPT.hasMatch(current), |
+ 'Missing argument for "${option.name}".'); |
+ |
+ setOption(results, option, current); |
+ args.removeAt(0); |
+ } |
+ |
+ /** |
+ * Tries to parse the current argument as a "solo" option, which is a single |
+ * hyphen followed by a single letter. We treat this differently than |
+ * collapsed abbreviations (like "-abc") to handle the possible value that |
+ * may follow it. |
+ */ |
+ bool parseSoloOption() { |
+ var soloOpt = _SOLO_OPT.firstMatch(current); |
+ if (soloOpt == null) return false; |
+ |
+ var option = grammar.findByAbbreviation(soloOpt[1]); |
+ if (option == null) { |
+ // Walk up to the parent command if possible. |
+ validate(parent != null, |
+ 'Could not find an option or flag "-${soloOpt[1]}".'); |
+ return parent.parseSoloOption(); |
+ } |
+ |
+ args.removeAt(0); |
+ |
+ if (option.isFlag) { |
+ setOption(results, option, true); |
+ } else { |
+ readNextArgAsValue(option); |
+ } |
+ |
+ return true; |
+ } |
+ |
+ /** |
+ * Tries to parse the current argument as a series of collapsed abbreviations |
+ * (like "-abc") or a single abbreviation with the value directly attached |
+ * to it (like "-mrelease"). |
+ */ |
+ bool parseAbbreviation(Parser innermostCommand) { |
+ var abbrOpt = _ABBR_OPT.firstMatch(current); |
+ if (abbrOpt == null) return false; |
+ |
+ // If the first character is the abbreviation for a non-flag option, then |
+ // the rest is the value. |
+ var c = abbrOpt[1].substring(0, 1); |
+ var first = grammar.findByAbbreviation(c); |
+ if (first == null) { |
+ // Walk up to the parent command if possible. |
+ validate(parent != null, |
+ 'Could not find an option with short name "-$c".'); |
+ return parent.parseAbbreviation(innermostCommand); |
+ } else if (!first.isFlag) { |
+ // The first character is a non-flag option, so the rest must be the |
+ // value. |
+ var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}'; |
+ setOption(results, first, value); |
+ } else { |
+ // If we got some non-flag characters, then it must be a value, but |
+ // if we got here, it's a flag, which is wrong. |
+ validate(abbrOpt[2] == '', |
+ 'Option "-$c" is a flag and cannot handle value ' |
+ '"${abbrOpt[1].substring(1)}${abbrOpt[2]}".'); |
+ |
+ // Not an option, so all characters should be flags. |
+ // We use "innermostCommand" here so that if a parent command parses the |
+ // *first* letter, subcommands can still be found to parse the other |
+ // letters. |
+ for (var i = 0; i < abbrOpt[1].length; i++) { |
+ var c = abbrOpt[1].substring(i, i + 1); |
+ innermostCommand.parseShortFlag(c); |
+ } |
+ } |
+ |
+ args.removeAt(0); |
+ return true; |
+ } |
+ |
+ void parseShortFlag(String c) { |
+ var option = grammar.findByAbbreviation(c); |
+ if (option == null) { |
+ // Walk up to the parent command if possible. |
+ validate(parent != null, |
+ 'Could not find an option with short name "-$c".'); |
+ parent.parseShortFlag(c); |
+ return; |
+ } |
+ |
+ // In a list of short options, only the first can be a non-flag. If |
+ // we get here we've checked that already. |
+ validate(option.isFlag, |
+ 'Option "-$c" must be a flag to be in a collapsed "-".'); |
+ |
+ setOption(results, option, true); |
+ } |
+ |
+ /** |
+ * Tries to parse the current argument as a long-form named option, which |
+ * may include a value like "--mode=release" or "--mode release". |
+ */ |
+ bool parseLongOption() { |
+ var longOpt = _LONG_OPT.firstMatch(current); |
+ if (longOpt == null) return false; |
+ |
+ var name = longOpt[1]; |
+ var option = grammar.options[name]; |
+ if (option != null) { |
+ args.removeAt(0); |
+ if (option.isFlag) { |
+ validate(longOpt[3] == null, |
+ 'Flag option "$name" should not be given a value.'); |
+ |
+ setOption(results, option, true); |
+ } else if (longOpt[3] != null) { |
+ // We have a value like --foo=bar. |
+ setOption(results, option, longOpt[3]); |
+ } else { |
+ // Option like --foo, so look for the value as the next arg. |
+ readNextArgAsValue(option); |
+ } |
+ } else if (name.startsWith('no-')) { |
+ // See if it's a negated flag. |
+ name = name.substring('no-'.length); |
+ option = grammar.options[name]; |
+ if (option == null) { |
+ // Walk up to the parent command if possible. |
+ validate(parent != null, 'Could not find an option named "$name".'); |
+ return parent.parseLongOption(); |
+ } |
+ |
+ args.removeAt(0); |
+ validate(option.isFlag, 'Cannot negate non-flag option "$name".'); |
+ validate(option.negatable, 'Cannot negate option "$name".'); |
+ |
+ setOption(results, option, false); |
+ } else { |
+ // Walk up to the parent command if possible. |
+ validate(parent != null, 'Could not find an option named "$name".'); |
+ return parent.parseLongOption(); |
+ } |
+ |
+ return true; |
+ } |
+ |
+ /** |
+ * Called during parsing to validate the arguments. Throws a |
+ * [FormatException] if [condition] is `false`. |
+ */ |
+ validate(bool condition, String message) { |
+ if (!condition) throw new FormatException(message); |
+ } |
+ |
+ /** Validates and stores [value] as the value for [option]. */ |
+ setOption(Map results, Option option, value) { |
+ // See if it's one of the allowed values. |
+ if (option.allowed != null) { |
+ validate(option.allowed.any((allow) => allow == value), |
+ '"$value" is not an allowed value for option "${option.name}".'); |
+ } |
+ |
+ if (option.allowMultiple) { |
+ results[option.name].add(value); |
+ } else { |
+ results[option.name] = value; |
+ } |
+ } |
+} |