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

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: Fix a couple of type annotations. 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/lib/src/parser.dart » ('j') | no next file with comments »
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..29da1e262b840885b6d87c41474eec6aa6f0d418 100644
--- a/pkg/args/lib/args.dart
+++ b/pkg/args/lib/args.dart
@@ -1,4 +1,4 @@
-// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// 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.
@@ -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
@@ -190,39 +212,42 @@
*/
library args;
-import 'dart:math';
-
-// TODO(rnystrom): Use "package:" URL here when test.dart can handle pub.
-import 'src/utils.dart';
+import 'src/parser.dart';
+import 'src/usage.dart';
/**
* 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;
+ /**
+ * The options that have been defined for this parser.
+ */
+ final Map<String, Option> options = <String, Option>{};
/**
- * The names of the options, in the order that they were added. This way we
- * can generate usage information in the same order.
+ * The commands that have been defined for this parser.
*/
- // TODO(rnystrom): Use an ordered map type, if one appears.
- final List<String> _optionNames;
+ final Map<String, ArgParser> commands = <String, ArgParser>{};
- /** 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.
+ */
+ 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:
@@ -254,7 +279,7 @@ class ArgParser {
void callback(value), {bool isFlag, bool negatable: false,
bool allowMultiple: false}) {
// Make sure the name isn't in use.
- if (_options.containsKey(name)) {
+ if (options.containsKey(name)) {
throw new ArgumentError('Duplicate option "$name".');
}
@@ -265,289 +290,56 @@ class ArgParser {
'Abbreviation "$abbr" is longer than one character.');
}
- var existing = _findByAbbr(abbr);
+ var existing = findByAbbreviation(abbr);
if (existing != null) {
throw new ArgumentError(
'Abbreviation "$abbr" is already used by "${existing.name}".');
}
}
- _options[name] = new _Option(name, abbr, help, allowed, allowedHelp,
+ options[name] = new Option(name, abbr, help, allowed, allowedHelp,
defaultsTo, callback, isFlag: isFlag, negatable: negatable,
allowMultiple: allowMultiple);
- _optionNames.add(name);
}
/**
* Parses [args], a list of command-line arguments, matches them against the
* flags and options defined by this parser, and returns the result.
*/
- ArgResults parse(List<String> args) {
- _args = args;
- _current = 0;
- var results = {};
-
- // Initialize flags to their defaults.
- _options.forEach((name, option) {
- if (option.allowMultiple) {
- results[name] = [];
- } else {
- results[name] = option.defaultValue;
- }
- });
-
- // Parse the args.
- for (_current = 0; _current < args.length; _current++) {
- var arg = args[_current];
-
- if (arg == '--') {
- // Reached the argument terminator, so stop here.
- _current++;
- break;
- }
-
- // 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 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.
- for (var name in _optionNames) {
- var option = _options[name];
- 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.
- return new ArgResults(results,
- _args.getRange(_current, _args.length - _current));
- }
+ ArgResults parse(List<String> args) =>
+ new Parser(null, this, args.toList()).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();
- }
-
- /**
- * 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;
- }
- }
-
- /**
- * 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,
- 'Missing argument for "${option.name}".');
-
- // Make sure it isn't an option itself.
- _validate(!_ABBR_OPT.hasMatch(_args[_current]) &&
- !_LONG_OPT.hasMatch(_args[_current]),
- 'Missing argument for "${option.name}".');
-
- _setOption(results, option, _args[_current]);
- }
-
- /**
- * 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(Map results) {
- var soloOpt = _SOLO_OPT.firstMatch(_args[_current]);
- if (soloOpt == null) return false;
-
- var option = _findByAbbr(soloOpt[1]);
- _validate(option != null,
- 'Could not find an option or flag "-${soloOpt[1]}".');
-
- if (option.isFlag) {
- _setOption(results, option, true);
- } else {
- _readNextArgAsValue(results, 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(Map results) {
- var abbrOpt = _ABBR_OPT.firstMatch(_args[_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);
- if (first == null) {
- _validate(false, 'Could not find an option with short name "-$c".');
- } 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.
- 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);
- }
- }
-
- return 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]);
- if (longOpt == null) return false;
-
- var name = longOpt[1];
- var option = _options[name];
- if (option != null) {
- 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(results, 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".');
-
- _setOption(results, option, false);
- } else {
- _validate(option != null, 'Could not find an option named "$name".');
- }
-
- return true;
- }
-
- /**
- * 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;
- }
+ String getUsage() => 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)) {
+ if (!options.containsKey(option)) {
throw new ArgumentError('No option named $option');
}
- return _options[option].defaultValue;
+ return options[option].defaultValue;
}
-}
-
-/**
- * The results of parsing a series of command line arguments using
- * [ArgParser.parse()]. Includes the parsed options and any remaining unparsed
- * command line arguments.
- */
-class ArgResults {
- final Map _options;
/**
- * 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.
+ * Finds the option whose abbreviation is [abbr], or `null` if no option has
+ * that abbreviation.
*/
- final List<String> rest;
-
- /** Creates a new [ArgResults]. */
- ArgResults(this._options, this.rest);
-
- /** Gets the parsed command-line option named [name]. */
- operator [](String name) {
- if (!_options.containsKey(name)) {
- throw new ArgumentError(
- 'Could not find an option named "$name".');
- }
-
- return _options[name];
+ Option findByAbbreviation(String abbr) {
+ return options.values.firstMatching((option) => option.abbreviation == abbr,
+ orElse: () => null);
}
-
- /** Get the names of the options as a [Collection]. */
- Collection<String> get options => _options.keys.toList();
}
-class _Option {
+/**
+ * A command-line option. Includes both flags and options which take a value.
+ */
+class Option {
final String name;
final String abbreviation;
final List allowed;
@@ -559,227 +351,52 @@ class _Option {
final bool negatable;
final bool allowMultiple;
- _Option(this.name, this.abbreviation, this.help, this.allowed,
+ Option(this.name, this.abbreviation, this.help, this.allowed,
this.allowedHelp, this.defaultValue, this.callback, {this.isFlag,
this.negatable, this.allowMultiple: false});
}
/**
- * Takes an [ArgParser] and generates a string of usage (i.e. help) text for its
- * defined options. Internally, it works like a tabular printer. The output is
- * divided into three horizontal columns, like so:
- *
- * -h, --help Prints the usage information
- * | | | |
- *
- * It builds the usage text up one column at a time and handles padding with
- * spaces and wrapping to the next line to keep the cells correctly lined up.
+ * The results of parsing a series of command line arguments using
+ * [ArgParser.parse()]. Includes the parsed options and any remaining unparsed
+ * command line arguments.
*/
-class _Usage {
- static const NUM_COLUMNS = 3; // Abbreviation, long name, help.
-
- /** The parser this is generating usage for. */
- final ArgParser args;
-
- /** The working buffer for the generated usage text. */
- StringBuffer buffer;
-
- /**
- * The column that the "cursor" is currently on. If the next call to
- * [write()] is not for this column, it will correctly handle advancing to
- * the next column (and possibly the next row).
- */
- int currentColumn = 0;
-
- /** The width in characters of each column. */
- List<int> columnWidths;
+class ArgResults {
+ final Map _options;
/**
- * The number of sequential lines of text that have been written to the last
- * column (which shows help info). We track this so that help text that spans
- * multiple lines can be padded with a blank line after it for separation.
- * Meanwhile, sequential options with single-line help will be compacted next
- * to each other.
+ * 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`.
*/
- int numHelpLines = 0;
+ final String name;
/**
- * How many newlines need to be rendered before the next bit of text can be
- * written. We do this lazily so that the last bit of usage doesn't have
- * dangling newlines. We only write newlines right *before* we write some
- * real content.
+ * The command that was selected, or `null` if none was. This will contain
+ * the options that were selected for that command.
*/
- int newlinesNeeded = 0;
-
- _Usage(this.args);
+ final ArgResults command;
/**
- * Generates a string displaying usage information for the defined options.
- * This is basically the help text shown on the command line.
+ * 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.
*/
- String generate() {
- buffer = new StringBuffer();
-
- calculateColumnWidths();
-
- for (var name in args._optionNames) {
- var option = args._options[name];
- write(0, getAbbreviation(option));
- write(1, getLongOption(option));
-
- if (option.help != null) write(2, option.help);
-
- if (option.allowedHelp != null) {
- var allowedNames = option.allowedHelp.keys.toList();
- allowedNames.sort();
- newline();
- for (var name in allowedNames) {
- write(1, getAllowedTitle(name));
- write(2, option.allowedHelp[name]);
- }
- newline();
- } else if (option.allowed != null) {
- write(2, buildAllowedList(option));
- } else if (option.defaultValue != null) {
- if (option.isFlag && option.defaultValue == true) {
- write(2, '(defaults to on)');
- } else if (!option.isFlag) {
- write(2, '(defaults to "${option.defaultValue}")');
- }
- }
-
- // If any given option displays more than one line of text on the right
- // column (i.e. help, default value, allowed options, etc.) then put a
- // blank line after it. This gives space where it's useful while still
- // keeping simple one-line options clumped together.
- if (numHelpLines > 1) newline();
- }
-
- return buffer.toString();
- }
-
- String getAbbreviation(_Option option) {
- if (option.abbreviation != null) {
- return '-${option.abbreviation}, ';
- } else {
- return '';
- }
- }
-
- String getLongOption(_Option option) {
- if (option.negatable) {
- return '--[no-]${option.name}';
- } else {
- return '--${option.name}';
- }
- }
-
- String getAllowedTitle(String allowed) {
- return ' [$allowed]';
- }
-
- void calculateColumnWidths() {
- int abbr = 0;
- int title = 0;
- for (var name in args._optionNames) {
- var option = args._options[name];
-
- // Make room in the first column if there are abbreviations.
- abbr = max(abbr, getAbbreviation(option).length);
-
- // Make room for the option.
- title = max(title, getLongOption(option).length);
-
- // Make room for the allowed help.
- if (option.allowedHelp != null) {
- for (var allowed in option.allowedHelp.keys) {
- title = max(title, getAllowedTitle(allowed).length);
- }
- }
- }
-
- // Leave a gutter between the columns.
- title += 4;
- columnWidths = [abbr, title];
- }
-
- newline() {
- newlinesNeeded++;
- currentColumn = 0;
- numHelpLines = 0;
- }
-
- write(int column, String text) {
- var lines = text.split('\n');
-
- // Strip leading and trailing empty lines.
- while (lines.length > 0 && lines[0].trim() == '') {
- lines.removeRange(0, 1);
- }
-
- while (lines.length > 0 && lines[lines.length - 1].trim() == '') {
- lines.removeLast();
- }
-
- for (var line in lines) {
- writeLine(column, line);
- }
- }
-
- writeLine(int column, String text) {
- // Write any pending newlines.
- while (newlinesNeeded > 0) {
- buffer.add('\n');
- newlinesNeeded--;
- }
+ final List<String> rest;
- // Advance until we are at the right column (which may mean wrapping around
- // to the next line.
- while (currentColumn != column) {
- if (currentColumn < NUM_COLUMNS - 1) {
- buffer.add(padRight('', columnWidths[currentColumn]));
- } else {
- buffer.add('\n');
- }
- currentColumn = (currentColumn + 1) % NUM_COLUMNS;
- }
+ /** Creates a new [ArgResults]. */
+ ArgResults(this._options, this.name, this.command, this.rest);
- if (column < columnWidths.length) {
- // Fixed-size column, so pad it.
- buffer.add(padRight(text, columnWidths[column]));
- } else {
- // The last column, so just write it.
- buffer.add(text);
+ /** Gets the parsed command-line option named [name]. */
+ operator [](String name) {
+ if (!_options.containsKey(name)) {
+ throw new ArgumentError(
+ 'Could not find an option named "$name".');
}
- // Advance to the next column.
- currentColumn = (currentColumn + 1) % NUM_COLUMNS;
-
- // If we reached the last column, we need to wrap to the next line.
- if (column == NUM_COLUMNS - 1) newlinesNeeded++;
-
- // Keep track of how many consecutive lines we've written in the last
- // column.
- if (column == NUM_COLUMNS - 1) {
- numHelpLines++;
- } else {
- numHelpLines = 0;
- }
+ return _options[name];
}
- buildAllowedList(_Option option) {
- var allowedBuffer = new StringBuffer();
- allowedBuffer.add('[');
- bool first = true;
- for (var allowed in option.allowed) {
- if (!first) allowedBuffer.add(', ');
- allowedBuffer.add(allowed);
- if (allowed == option.defaultValue) {
- allowedBuffer.add(' (default)');
- }
- first = false;
- }
- allowedBuffer.add(']');
- return allowedBuffer.toString();
- }
+ /** Get the names of the options as a [Collection]. */
+ Collection<String> get options => _options.keys.toList();
}
+
« no previous file with comments | « no previous file | pkg/args/lib/src/parser.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698