Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(845)

Unified Diff: pkg/args/lib/args.dart

Issue 11819068: Add command support to args. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Remove TODO. Created 7 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | pkg/args/test/args_test.dart » ('j') | pkg/args/test/command_test.dart » ('J')
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: pkg/args/lib/args.dart
diff --git a/pkg/args/lib/args.dart b/pkg/args/lib/args.dart
index 0cb81961a3ec2c1743f09a9138c389eb3e3013c1..f308cb313778c30bb6f8cfe36333b64ad04a2b41 100644
--- a/pkg/args/lib/args.dart
+++ b/pkg/args/lib/args.dart
@@ -145,7 +145,29 @@
* var results = parser.parse(['--mode', 'on', '--mode', 'off']);
* print(results['mode']); // prints '[on, off]'
*
- * ## Usage ##
+ * ## Defining commands ##
+ *
+ * In addition to *options*, you can also define *commands*. A command is a
+ * named argument that has its own set of options. For example, when you run:
+ *
+ * $ git commit -a
+ *
+ * The executable is `git`, the command is `commit`, and the `-a` option is an
+ * option passed to the command. You can add a command like so:
+ *
+ * var parser = new ArgParser();
+ * var command = parser.addCommand("commit");
+ * command.addFlag('all', abbr: 'a');
+ *
+ * It returns another [ArgParser] which you can use to define options and
+ * subcommands on that command. When an argument list is parsed, you can then
+ * determine which command was entered and what options were provided for it.
+ *
+ * var results = parser.parse(['commit', '-a']);
+ * print(results.command.name); // "commit"
+ * print(results.command['a']); // true
+ *
+ * ## Displaying usage ##
*
* This library can also be used to automatically generate nice usage help
* text like you get when you run a program with `--help`. To use this, you
@@ -179,7 +201,7 @@
*
* [arm] ARM Holding 32-bit chip
* [ia32] Intel x86
- *
+ *
* To assist the formatting of the usage help, single line help text will
* be followed by a single new line. Options with multi-line help text
* will be followed by two new lines. This provides spatial diversity between
@@ -192,37 +214,45 @@ library args;
import 'dart:math';
-// TODO(rnystrom): Use "package:" URL here when test.dart can handle pub.
import 'src/utils.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]+)(=(.*))?$');
+
/**
* A class for taking a list of raw command line arguments and parsing out
* options and flags from them.
*/
class ArgParser {
- static final _SOLO_OPT = new RegExp(r'^-([a-zA-Z0-9])$');
- static final _ABBR_OPT = new RegExp(r'^-([a-zA-Z0-9]+)(.*)$');
- static final _LONG_OPT = new RegExp(r'^--([a-zA-Z\-_0-9]+)(=(.*))?$');
-
- final Map<String, _Option> _options;
+ final Map<String, _Option> _options = <String, _Option>{};
+ final Map<String, ArgParser> _commands = <String, ArgParser>{};
/**
* The names of the options, in the order that they were added. This way we
* can generate usage information in the same order.
*/
// TODO(rnystrom): Use an ordered map type, if one appears.
- final List<String> _optionNames;
+ final List<String> _optionNames = <String>[];
- /** The current argument list being parsed. Set by [parse()]. */
- List<String> _args;
+ /** Creates a new ArgParser. */
+ ArgParser();
- /** Index of the current argument being parsed in [_args]. */
- int _current;
+ /**
+ * Defines a command. A command is a named argument which may in turn
+ * define its own options and subcommands. Returns an [ArgParser] that can
+ * be used to define the command's options.
+ */
nweiz 2013/01/11 00:06:06 When you have a sec, can you run your comment-fixi
Bob Nystrom 2013/01/11 17:59:49 I thought about that, but I'd like to do that in a
nweiz 2013/01/11 21:43:09 sgtm
+ ArgParser addCommand(String name) {
+ // Make sure the name isn't in use.
+ if (_commands.containsKey(name)) {
+ throw new ArgumentError('Duplicate command "$name".');
+ }
- /** Creates a new ArgParser. */
- ArgParser()
- : _options = <String, _Option>{},
- _optionNames = <String>[];
+ var command = new ArgParser();
+ _commands[name] = command;
+ return command;
+ }
/**
* Defines a flag. Throws an [ArgumentError] if:
@@ -283,12 +313,79 @@ class ArgParser {
* flags and options defined by this parser, and returns the result.
*/
ArgResults parse(List<String> args) {
- _args = args;
- _current = 0;
- var results = {};
+ return new _ArgParser(null, this, args).parse();
+ }
+
+ /**
+ * Generates a string displaying usage information for the defined options.
+ * This is basically the help text shown on the command line.
+ */
+ String getUsage() {
+ return new _Usage(this).generate();
+ }
+
+ /**
+ * Get the default value for an option. Useful after parsing to test
+ * if the user specified something other than the default.
+ */
+ getDefault(String option) {
+ if (!_options.containsKey(option)) {
+ throw new ArgumentError('No option named $option');
+ }
+ return _options[option].defaultValue;
+ }
+
+ /**
+ * Finds the option whose abbreviation is [abbr], or `null` if no option has
+ * that abbreviation.
+ */
+ _Option _findByAbbr(String abbr) {
+ for (var option in _options.values) {
+ if (option.abbreviation == abbr) return option;
+ }
+
+ return null;
+ }
+}
+
+/**
+ * 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 _ArgParser {
nweiz 2013/01/11 00:06:06 There are getting to be a lot of classes in this f
Bob Nystrom 2013/01/11 17:59:49 Great idea. Done.
+ /**
+ * 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 _ArgParser parent;
+
+ /** The grammar being parsed. */
+ final ArgParser parser;
nweiz 2013/01/11 00:06:06 "grammar"? It's kind of confusing to read "parser"
Bob Nystrom 2013/01/11 17:59:49 Done.
+
+ /** The arguments being parsed. */
+ final List<String> args;
+
+ /** The accumulated parsed options. */
+ final Map results = {};
+
+ _ArgParser(this.commandName, this.parser, 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.
- _options.forEach((name, option) {
+ parser._options.forEach((name, option) {
if (option.allowMultiple) {
results[name] = [];
} else {
@@ -297,20 +394,28 @@ class ArgParser {
});
// Parse the args.
- for (_current = 0; _current < args.length; _current++) {
- var arg = args[_current];
-
- if (arg == '--') {
+ while (args.length > 0) {
+ if (current == '--') {
// Reached the argument terminator, so stop here.
- _current++;
+ 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 = parser._commands[current];
+ if (command != null) {
+ var commandName = args.removeAt(0);
+ var commandParser = new _ArgParser(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(results)) continue;
- if (_parseAbbreviation(results)) continue;
- if (_parseLongOption(results)) continue;
+ 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;
@@ -318,8 +423,8 @@ class ArgParser {
// Set unspecified multivalued arguments to their default value,
// if any, and invoke the callbacks.
- for (var name in _optionNames) {
- var option = _options[name];
+ for (var name in parser._optionNames) {
+ var option = parser._options[name];
if (option.allowMultiple &&
results[name].length == 0 &&
option.defaultValue != null) {
@@ -329,58 +434,27 @@ class ArgParser {
}
// Add in the leftover arguments we didn't parse.
- return new ArgResults(results,
- _args.getRange(_current, _args.length - _current));
+ // TODO(bob): How should "rest" be handled with commands? Which command on
+ // the stack gets them?
nweiz 2013/01/11 00:06:06 The deepest command should get the rest of the arg
Bob Nystrom 2013/01/11 17:59:49 Done.
+ return new ArgResults(results, commandName, commandResults, args);
}
/**
- * Generates a string displaying usage information for the defined options.
- * This is basically the help text shown on the command line.
+ * Pulls the value for [option] from the next argument in [args] (where the
+ * current option is at [index]. Validates that there is a valid value there.
nweiz 2013/01/11 00:06:06 "[index]" -> "[index])".
Bob Nystrom 2013/01/11 17:59:49 Fixed. That doc was out of date. The old parser di
*/
- String getUsage() {
- return new _Usage(this).generate();
- }
-
- /**
- * 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);
- }
+ void readNextArgAsValue(_Option option) {
+ args.removeAt(0);
- /** 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;
- }
- }
-
- /**
- * Pulls the value for [option] from the next argument in [_args] (where the
- * current option is at index [_current]. Validates that there is a valid
- * value there.
- */
- void _readNextArgAsValue(Map results, _Option option) {
- _current++;
// Take the option argument from the next command line arg.
- _validate(_current < _args.length,
+ validate(args.length > 0,
'Missing argument for "${option.name}".');
// Make sure it isn't an option itself.
- _validate(!_ABBR_OPT.hasMatch(_args[_current]) &&
- !_LONG_OPT.hasMatch(_args[_current]),
+ validate(!_ABBR_OPT.hasMatch(current) && !_LONG_OPT.hasMatch(current),
'Missing argument for "${option.name}".');
nweiz 2013/01/11 00:06:06 You should run these validations before you remove
Bob Nystrom 2013/01/11 17:59:49 It's a little confusing. The first element in the
- _setOption(results, option, _args[_current]);
+ setOption(results, option, current);
}
/**
@@ -389,20 +463,25 @@ class ArgParser {
* collapsed abbreviations (like "-abc") to handle the possible value that
* may follow it.
*/
- bool _parseSoloOption(Map results) {
- var soloOpt = _SOLO_OPT.firstMatch(_args[_current]);
+ bool parseSoloOption() {
+ var soloOpt = _SOLO_OPT.firstMatch(current);
if (soloOpt == null) return false;
- var option = _findByAbbr(soloOpt[1]);
- _validate(option != null,
- 'Could not find an option or flag "-${soloOpt[1]}".');
+ var option = parser._findByAbbr(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();
+ }
if (option.isFlag) {
- _setOption(results, option, true);
+ setOption(results, option, true);
} else {
- _readNextArgAsValue(results, option);
+ readNextArgAsValue(option);
}
+ args.removeAt(0);
return true;
}
@@ -411,107 +490,131 @@ class ArgParser {
* (like "-abc") or a single abbreviation with the value directly attached
* to it (like "-mrelease").
*/
- bool _parseAbbreviation(Map results) {
- var abbrOpt = _ABBR_OPT.firstMatch(_args[_current]);
+ bool parseAbbreviation(_ArgParser 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 = _findByAbbr(c);
+ var first = parser._findByAbbr(c);
if (first == null) {
- _validate(false, 'Could not find an option with short name "-$c".');
+ // 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);
+ 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] == '',
+ 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);
- var option = _findByAbbr(c);
- _validate(option != null,
- 'Could not find an option with short name "-$c".');
-
- // 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);
+ innermostCommand.parseShortFlag(c);
}
}
+ args.removeAt(0);
return true;
}
+ void parseShortFlag(String c) {
+ var option = parser._findByAbbr(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(Map results) {
- var longOpt = _LONG_OPT.firstMatch(_args[_current]);
+ bool parseLongOption() {
+ var longOpt = _LONG_OPT.firstMatch(current);
if (longOpt == null) return false;
var name = longOpt[1];
- var option = _options[name];
+ var option = parser._options[name];
if (option != null) {
if (option.isFlag) {
- _validate(longOpt[3] == null,
+ validate(longOpt[3] == null,
'Flag option "$name" should not be given a value.');
- _setOption(results, option, true);
+ setOption(results, option, true);
} else if (longOpt[3] != null) {
// We have a value like --foo=bar.
- _setOption(results, option, longOpt[3]);
+ setOption(results, option, longOpt[3]);
} else {
// Option like --foo, so look for the value as the next arg.
- _readNextArgAsValue(results, option);
+ readNextArgAsValue(option);
}
} else if (name.startsWith('no-')) {
// See if it's a negated flag.
name = name.substring('no-'.length);
- option = _options[name];
- _validate(option != null, 'Could not find an option named "$name".');
- _validate(option.isFlag, 'Cannot negate non-flag option "$name".');
- _validate(option.negatable, 'Cannot negate option "$name".');
+ option = parser._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();
+ }
- _setOption(results, option, false);
+ validate(option.isFlag, 'Cannot negate non-flag option "$name".');
+ validate(option.negatable, 'Cannot negate option "$name".');
+
+ setOption(results, option, false);
} else {
- _validate(option != null, 'Could not find an option named "$name".');
+ // Walk up to the parent command if possible.
+ validate(parent != null, 'Could not find an option named "$name".');
+ return parent.parseLongOption();
}
+ args.removeAt(0);
return true;
}
/**
- * Finds the option whose abbreviation is [abbr], or `null` if no option has
- * that abbreviation.
+ * Called during parsing to validate the arguments. Throws a
+ * [FormatException] if [condition] is `false`.
*/
- _Option _findByAbbr(String abbr) {
- for (var option in _options.values) {
- if (option.abbreviation == abbr) return option;
- }
-
- return null;
+ validate(bool condition, String message) {
+ if (!condition) throw new FormatException(message);
}
- /**
- * Get the default value for an option. Useful after parsing to test
- * if the user specified something other than the default.
- */
- getDefault(String option) {
- if (!_options.containsKey(option)) {
- throw new ArgumentError('No option named $option');
+ /** 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;
}
- return _options[option].defaultValue;
}
}
@@ -524,6 +627,18 @@ class ArgResults {
final Map _options;
/**
+ * If these are the results for parsing a command's options, this will be
+ * the name of the command. For top-level results, this returns `null`.
+ */
+ final String name;
+
+ /**
+ * The command that was selected, or `null` if none was. This will contain
+ * the options that were selected for that command.
+ */
+ final ArgResults command;
+
+ /**
* The remaining command-line arguments that were not parsed as options or
* flags. If `--` was used to separate the options from the remaining
* arguments, it will not be included in this list.
@@ -531,7 +646,7 @@ class ArgResults {
final List<String> rest;
/** Creates a new [ArgResults]. */
- ArgResults(this._options, this.rest);
+ ArgResults(this._options, this.name, this.command, this.rest);
/** Gets the parsed command-line option named [name]. */
operator [](String name) {
« no previous file with comments | « no previous file | pkg/args/test/args_test.dart » ('j') | pkg/args/test/command_test.dart » ('J')

Powered by Google App Engine
This is Rietveld 408576698