Support command return value forwarding. (#59)
This was unintentionally supported prior to 0.13.6+1. This re-adds
support, documents and tests it, and adds type annotations to make it
type-safe.
Closes #57
Closes #58
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9127332..5f5c8c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.13.7
+
+* Add explicit support for forwarding the value returned by `Command.run()` to
+ `CommandRunner.run()`. This worked unintentionally prior to 0.13.6+1.
+
+* Add type arguments to `CommandRunner` and `Command` to indicate the return
+ values of the `run()` functions.
+
## 0.13.6+1
* When a `CommandRunner` is passed `--help` before any commands, it now prints
diff --git a/lib/command_runner.dart b/lib/command_runner.dart
index 338ad94..6a6d282 100644
--- a/lib/command_runner.dart
+++ b/lib/command_runner.dart
@@ -16,7 +16,11 @@
export 'src/usage_exception.dart';
/// A class for invoking [Commands] based on raw command-line arguments.
-class CommandRunner {
+///
+/// The type argument `T` represents the type returned by [Command.run] and
+/// [CommandRunner.run]; it can be ommitted if you're not using the return
+/// values.
+class CommandRunner<T> {
/// The name of the executable being run.
///
/// Used for error reporting and [usage].
@@ -60,8 +64,8 @@
}
/// An unmodifiable view of all top-level commands defined for this runner.
- Map<String, Command> get commands => new UnmodifiableMapView(_commands);
- final _commands = <String, Command>{};
+ Map<String, Command<T>> get commands => new UnmodifiableMapView(_commands);
+ final _commands = <String, Command<T>>{};
/// The top-level argument parser.
///
@@ -74,7 +78,7 @@
CommandRunner(this.executableName, this.description) {
argParser.addFlag('help',
abbr: 'h', negatable: false, help: 'Print this usage information.');
- addCommand(new HelpCommand());
+ addCommand(new HelpCommand<T>());
}
/// Prints the usage information for this runner.
@@ -88,7 +92,7 @@
throw new UsageException(message, _usageWithoutDescription);
/// Adds [Command] as a top-level command to this runner.
- void addCommand(Command command) {
+ void addCommand(Command<T> command) {
var names = [command.name]..addAll(command.aliases);
for (var name in names) {
_commands[name] = command;
@@ -101,7 +105,7 @@
///
/// This always returns a [Future] in case the command is asynchronous. The
/// [Future] will throw a [UsageException] if [args] was invalid.
- Future run(Iterable<String> args) =>
+ Future<T> run(Iterable<String> args) =>
new Future.sync(() => runCommand(parse(args)));
/// Parses [args] and returns the result, converting an [ArgParserException]
@@ -133,7 +137,9 @@
/// It's useful to override this to handle global flags and/or wrap the entire
/// command in a block. For example, you might handle the `--verbose` flag
/// here to enable verbose logging before running the command.
- Future runCommand(ArgResults topLevelResults) async {
+ ///
+ /// This returns the return value of [Command.run].
+ Future<T> runCommand(ArgResults topLevelResults) async {
var argResults = topLevelResults;
var commands = _commands;
Command command;
@@ -145,7 +151,7 @@
if (command == null) {
// No top-level command was chosen.
printUsage();
- return;
+ return null;
}
command.usageException('Missing subcommand for "$commandString".');
@@ -170,13 +176,13 @@
if (argResults['help']) {
command.printUsage();
- return;
+ return null;
}
}
if (topLevelResults['help']) {
command.printUsage();
- return;
+ return null;
}
// Make sure there aren't unexpected arguments.
@@ -185,7 +191,7 @@
'Command "${argResults.name}" does not take any arguments.');
}
- await command.run();
+ return (await command.run()) as T;
}
}
@@ -197,7 +203,7 @@
/// A command with subcommands is known as a "branch command" and cannot be run
/// itself. It should call [addSubcommand] (often from the constructor) to
/// register subcommands.
-abstract class Command {
+abstract class Command<T> {
/// The name of this command.
String get name;
@@ -229,18 +235,18 @@
///
/// This will be `null` until [Command.addSubcommmand] has been called with
/// this command.
- Command get parent => _parent;
- Command _parent;
+ Command<T> get parent => _parent;
+ Command<T> _parent;
/// The command runner for this command.
///
/// This will be `null` until [CommandRunner.addCommand] has been called with
/// this command or one of its parents.
- CommandRunner get runner {
+ CommandRunner<T> get runner {
if (parent == null) return _runner;
return parent.runner;
}
- CommandRunner _runner;
+ CommandRunner<T> _runner;
/// The parsed global argument results.
///
@@ -298,8 +304,9 @@
}
/// An unmodifiable view of all sublevel commands of this command.
- Map<String, Command> get subcommands => new UnmodifiableMapView(_subcommands);
- final _subcommands = <String, Command>{};
+ Map<String, Command<T>> get subcommands =>
+ new UnmodifiableMapView(_subcommands);
+ final _subcommands = <String, Command<T>>{};
/// Whether or not this command should be hidden from help listings.
///
@@ -341,14 +348,15 @@
/// Runs this command.
///
- /// If this returns a [Future], [CommandRunner.run] won't complete until the
- /// returned [Future] does. Otherwise, the return value is ignored.
+ /// This must return a `T`, a `Future<T>`, or `null`. The value is returned by
+ /// [CommandRunner.runCommand]. Subclasses must explicitly declare a return
+ /// type for `run()`, and may not use `void` if `T` is defined.
run() {
throw new UnimplementedError("Leaf command $this must implement run().");
}
/// Adds [Command] as a subcommand of this.
- void addSubcommand(Command command) {
+ void addSubcommand(Command<T> command) {
var names = [command.name]..addAll(command.aliases);
for (var name in names) {
_subcommands[name] = command;
diff --git a/lib/src/help_command.dart b/lib/src/help_command.dart
index 085e444..104327c 100644
--- a/lib/src/help_command.dart
+++ b/lib/src/help_command.dart
@@ -7,17 +7,17 @@
/// The built-in help command that's added to every [CommandRunner].
///
/// This command displays help information for the various subcommands.
-class HelpCommand extends Command {
+class HelpCommand<T> extends Command<T> {
final name = "help";
String get description =>
"Display help information for ${runner.executableName}.";
String get invocation => "${runner.executableName} help [command]";
- void run() {
+ T run() {
// Show the default help if no command was specified.
if (argResults.rest.isEmpty) {
runner.printUsage();
- return;
+ return null;
}
// Walk the command tree to show help for the selected command or
@@ -47,5 +47,6 @@
}
command.printUsage();
+ return null;
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 4156049..e1a6b05 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: args
-version: 0.13.6+1
+version: 0.13.7
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 8875d8c..9b0713a 100644
--- a/test/command_runner_test.dart
+++ b/test/command_runner_test.dart
@@ -161,6 +161,22 @@
}), completes);
});
+ test("runs a command with a return value", () {
+ var runner = new CommandRunner<int>("test", "");
+ var command = new ValueCommand();
+ runner.addCommand(command);
+
+ expect(runner.run(["foo"]), completion(equals(12)));
+ });
+
+ test("runs a command with an asynchronous return value", () {
+ var runner = new CommandRunner<String>("test", "");
+ var command = new AsyncValueCommand();
+ runner.addCommand(command);
+
+ expect(runner.run(["foo"]), completion(equals("hi")));
+ });
+
test("runs a hidden comand", () {
var command = new HiddenCommand();
runner.addCommand(command);
diff --git a/test/utils.dart b/test/utils.dart
index e07f8f3..a874eee 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -27,6 +27,22 @@
}
}
+class ValueCommand extends Command<int> {
+ final name = "foo";
+ final description = "Return a value.";
+ final takesArguments = false;
+
+ int run() => 12;
+}
+
+class AsyncValueCommand extends Command<String> {
+ final name = "foo";
+ final description = "Return a future.";
+ final takesArguments = false;
+
+ Future<String> run() async => "hi";
+}
+
class MultilineCommand extends Command {
var hasRun = false;