Print the proper usage in CommandRunner. (#53)
This now prints the usage for the command that failed to parse, rather
than always printing the root usage.
Closes #52
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0861e19..108d60e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.13.6
+
+* `ArgParser.parse()` now throws an `ArgParserException`, which implements
+ `FormatException` and has a field that lists the commands that were parsed.
+
+* If `CommandRunner.run()` encounters a parse error for a subcommand, it now
+ prints the subcommand's usage rather than the global usage.
+
## 0.13.5
* Allow `CommandRunner.argParser` and `Command.argParser` to be overridden in
diff --git a/README.md b/README.md
index 461ebc6..36e931c 100644
--- a/README.md
+++ b/README.md
@@ -51,9 +51,11 @@
be a `bool`.
To validate a non-flag option, you can use the `allowed` parameter to provide an
-allowed set of values. When you do, the parser throws a [FormatException] if the
-value for an option is not in the allowed set. Here's an example of specifying
-allowed values:
+allowed set of values. When you do, the parser throws an
+[`ArgParserException`][ArgParserException] if the value for an option is not in
+the allowed set. Here's an example of specifying allowed values:
+
+[ArgParserException]: https://www.dartdocs.org/documentation/args/latest/args/ArgParserException-class.html
```dart
parser.addOption('mode', allowed: ['debug', 'release']);
diff --git a/lib/args.dart b/lib/args.dart
index 2534082..97b47b5 100644
--- a/lib/args.dart
+++ b/lib/args.dart
@@ -3,5 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
export 'src/arg_parser.dart';
+export 'src/arg_parser_exception.dart';
export 'src/arg_results.dart' hide newArgResults;
export 'src/option.dart' hide newOption;
diff --git a/lib/command_runner.dart b/lib/command_runner.dart
index 21dfa32..5780fae 100644
--- a/lib/command_runner.dart
+++ b/lib/command_runner.dart
@@ -7,6 +7,7 @@
import 'dart:math' as math;
import 'src/arg_parser.dart';
+import 'src/arg_parser_exception.dart';
import 'src/arg_results.dart';
import 'src/help_command.dart';
import 'src/usage_exception.dart';
@@ -103,18 +104,23 @@
Future run(Iterable<String> args) =>
new Future.sync(() => runCommand(parse(args)));
- /// Parses [args] and returns the result, converting a [FormatException] to a
- /// [UsageException].
+ /// Parses [args] and returns the result, converting an [ArgParserException]
+ /// to a [UsageException].
///
/// This is notionally a protected method. It may be overridden or called from
/// subclasses, but it shouldn't be called externally.
ArgResults parse(Iterable<String> args) {
try {
- // TODO(nweiz): if arg parsing fails for a command, print that command's
- // usage, not the global usage.
return argParser.parse(args);
- } on FormatException catch (error) {
- usageException(error.message);
+ } on ArgParserException catch (error) {
+ if (error.commands.isEmpty) usageException(error.message);
+
+ var command = commands[error.commands.first];
+ for (var commandName in error.commands.skip(1)) {
+ command = command.subcommands[commandName];
+ }
+
+ command.usageException(error.message);
}
}
diff --git a/lib/src/arg_parser_exception.dart b/lib/src/arg_parser_exception.dart
new file mode 100644
index 0000000..20eddb9
--- /dev/null
+++ b/lib/src/arg_parser_exception.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2016, 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.
+
+/// An exception thrown by [ArgParser].
+class ArgParserException extends FormatException {
+ /// The command(s) that were parsed before discovering the error.
+ ///
+ /// This will be empty if the error was on the root parser.
+ final List<String> commands;
+
+ ArgParserException(String message, [Iterable<String> commands])
+ : commands = commands == null
+ ? const []
+ : new List.unmodifiable(commands),
+ super(message);
+}
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index 22579fb..64374fc 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'arg_parser.dart';
+import 'arg_parser_exception.dart';
import 'arg_results.dart';
import 'option.dart';
@@ -63,7 +64,15 @@
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();
+
+ try {
+ commandResults = commandParser.parse();
+ } on ArgParserException catch (error) {
+ if (commandName == null) rethrow;
+ throw new ArgParserException(
+ error.message,
+ [commandName]..addAll(error.commands));
+ }
// All remaining arguments were passed to command so clear them here.
rest.clear();
@@ -242,9 +251,9 @@
/// Called during parsing to validate the arguments.
///
- /// Throws a [FormatException] if [condition] is `false`.
+ /// Throws an [ArgParserException] if [condition] is `false`.
void validate(bool condition, String message) {
- if (!condition) throw new FormatException(message);
+ if (!condition) throw new ArgParserException(message);
}
/// Validates and stores [value] as the value for [option], which must not be
diff --git a/pubspec.yaml b/pubspec.yaml
index e52bf29..ab921a5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: args
-version: 0.13.5
+version: 0.13.6
author: "Dart Team <misc@dartlang.org>"
homepage: https://github.com/dart-lang/args
description: >
diff --git a/test/command_runner_test.dart b/test/command_runner_test.dart
index d6605a4..6b4e30e 100644
--- a/test/command_runner_test.dart
+++ b/test/command_runner_test.dart
@@ -246,6 +246,27 @@
"""));
});
});
+
+ group("with an invalid argument", () {
+ test("at the root throws the root usage", () {
+ expect(runner.run(["--asdf"]), throwsUsageException(
+ 'Could not find an option named "asdf".',
+ '$_DEFAULT_USAGE'));
+ });
+
+ test("for a command throws the command usage", () {
+ var command = new FooCommand();
+ runner.addCommand(command);
+
+ expect(runner.run(["foo", "--asdf"]), throwsUsageException(
+ 'Could not find an option named "asdf".',
+ """
+Usage: test foo [arguments]
+-h, --help Print this usage information.
+
+Run "test help" to see global options."""));
+ });
+ });
});
group("with a footer", () {