Chromium Code Reviews| 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) { |