blob: 1e297b0e68b35069f57527e85ac3f6d8a63e781f [file] [log] [blame]
// 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 'arg_parser.dart';
import 'arg_results.dart';
import 'option.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 argument 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 remaining non-option, non-command arguments.
final rest = <String>[];
/// The accumulated parsed options.
final Map<String, dynamic> results = <String, dynamic>{};
Parser(this.commandName, this.grammar, this.args, this.parent, rest) {
if (rest != null) this.rest.addAll(rest);
}
/// The current argument being parsed.
String get current => args[0];
/// Parses the arguments. This can only be called once.
ArgResults parse() {
var arguments = args.toList();
var commandResults = null;
// 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) {
validate(rest.isEmpty, 'Cannot specify arguments before a command.');
var commandName = args.removeAt(0);
var commandParser = new Parser(commandName, command, args, this, rest);
commandResults = commandParser.parse();
// All remaining arguments were passed to command so clear them here.
rest.clear();
break;
}
// 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;
// This argument is neither option nor command, so stop parsing unless
// the [allowTrailingOptions] option is set.
if (!grammar.allowTrailingOptions) break;
rest.add(args.removeAt(0));
}
// Invoke the callbacks.
grammar.options.forEach((name, option) {
if (option.callback == null) return;
option.callback(option.getOrDefault(results[name]));
});
// Add in the leftover arguments we didn't parse to the innermost command.
rest.addAll(args);
args.clear();
return newArgResults(
grammar, results, commandName, commandResults, rest, arguments);
}
/// 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, 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.isMultiple) {
var list = results.putIfAbsent(option.name, () => []);
list.add(value);
} else {
results[option.name] = value;
}
}
}