f4300b84a2
Review URL: https://codereview.chromium.org//12401002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@19391 260f80e4-7a28-3924-810f-c04153c831b5
282 lines
8.9 KiB
Dart
282 lines
8.9 KiB
Dart
// 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;
|
|
}
|
|
}
|
|
}
|