| // 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<String, dynamic> 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() { | |
| ArgResults commandResults; | |
| // 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`. | |
| */ | |
| void validate(bool condition, String message) { | |
| if (!condition) throw new FormatException(message); | |
| } | |
| /** Validates and stores [value] as the value for [option]. */ | |
| void setOption(Map results, Option option, dynamic 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; | |
| } | |
| } | |
| } |