Index: packages/args/lib/command_runner.dart |
diff --git a/packages/args/lib/command_runner.dart b/packages/args/lib/command_runner.dart |
index d728cb0269402fd7c160e06c3aa9595f2495e0e1..6a6d2826a241777900272dc3f59ab604d41a96c1 100644 |
--- a/packages/args/lib/command_runner.dart |
+++ b/packages/args/lib/command_runner.dart |
@@ -2,13 +2,12 @@ |
// 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. |
-library args.command_runner; |
- |
import 'dart:async'; |
import 'dart:collection'; |
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'; |
@@ -17,7 +16,11 @@ import 'src/utils.dart'; |
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]. |
@@ -42,7 +45,7 @@ class CommandRunner { |
/// |
/// If a subclass overrides this to return a string, it will automatically be |
/// added to the end of [usage]. |
- final String usageFooter = null; |
+ String get usageFooter => null; |
/// Returns [usage] with [description] removed from the beginning. |
String get _usageWithoutDescription { |
@@ -61,20 +64,21 @@ Run "$executableName help <command>" for more information about a command.'''; |
} |
/// An unmodifiable view of all top-level commands defined for this runner. |
- Map<String, Command> get commands => new UnmodifiableMapView(_commands); |
- final _commands = new Map<String, Command>(); |
+ Map<String, Command<T>> get commands => new UnmodifiableMapView(_commands); |
+ final _commands = <String, Command<T>>{}; |
/// The top-level argument parser. |
/// |
/// Global options should be registered with this parser; they'll end up |
/// available via [Command.globalResults]. Commands should be registered with |
/// [addCommand] rather than directly on the parser. |
- final argParser = new ArgParser(); |
+ ArgParser get argParser => _argParser; |
+ final _argParser = new ArgParser(); |
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 @@ Run "$executableName help <command>" for more information about a command.'''; |
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; |
@@ -100,22 +104,28 @@ Run "$executableName help <command>" for more information about a command.'''; |
/// Parses [args] and invokes [Command.run] on the chosen command. |
/// |
/// This always returns a [Future] in case the command is asynchronous. The |
- /// [Future] will throw a [UsageError] if [args] was invalid. |
- Future run(Iterable<String> args) => |
+ /// [Future] will throw a [UsageException] if [args] was invalid. |
+ Future<T> 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); |
+ return null; |
} |
} |
@@ -127,56 +137,61 @@ Run "$executableName help <command>" for more information about a command.'''; |
/// 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) { |
- return new Future.sync(() { |
- var argResults = topLevelResults; |
- var commands = _commands; |
- var command; |
- var commandString = executableName; |
- |
- while (commands.isNotEmpty) { |
- if (argResults.command == null) { |
- if (argResults.rest.isEmpty) { |
- if (command == null) { |
- // No top-level command was chosen. |
- printUsage(); |
- return new Future.value(); |
- } |
- |
- command.usageException('Missing subcommand for "$commandString".'); |
- } else { |
- if (command == null) { |
- usageException( |
- 'Could not find a command named "${argResults.rest[0]}".'); |
- } |
- |
- command.usageException('Could not find a subcommand named ' |
- '"${argResults.rest[0]}" for "$commandString".'); |
+ /// |
+ /// This returns the return value of [Command.run]. |
+ Future<T> runCommand(ArgResults topLevelResults) async { |
+ var argResults = topLevelResults; |
+ var commands = _commands; |
+ Command command; |
+ var commandString = executableName; |
+ |
+ while (commands.isNotEmpty) { |
+ if (argResults.command == null) { |
+ if (argResults.rest.isEmpty) { |
+ if (command == null) { |
+ // No top-level command was chosen. |
+ printUsage(); |
+ return null; |
} |
- } |
- // Step into the command. |
- argResults = argResults.command; |
- command = commands[argResults.name]; |
- command._globalResults = topLevelResults; |
- command._argResults = argResults; |
- commands = command._subcommands; |
- commandString += " ${argResults.name}"; |
- |
- if (argResults['help']) { |
- command.printUsage(); |
- return new Future.value(); |
+ command.usageException('Missing subcommand for "$commandString".'); |
+ } else { |
+ if (command == null) { |
+ usageException( |
+ 'Could not find a command named "${argResults.rest[0]}".'); |
+ } |
+ |
+ command.usageException('Could not find a subcommand named ' |
+ '"${argResults.rest[0]}" for "$commandString".'); |
} |
} |
- // Make sure there aren't unexpected arguments. |
- if (!command.takesArguments && argResults.rest.isNotEmpty) { |
- command.usageException( |
- 'Command "${argResults.name}" does not take any arguments.'); |
+ // Step into the command. |
+ argResults = argResults.command; |
+ command = commands[argResults.name]; |
+ command._globalResults = topLevelResults; |
+ command._argResults = argResults; |
+ commands = command._subcommands; |
+ commandString += " ${argResults.name}"; |
+ |
+ if (argResults['help']) { |
+ command.printUsage(); |
+ return null; |
} |
+ } |
- return command.run(); |
- }); |
+ if (topLevelResults['help']) { |
+ command.printUsage(); |
+ return null; |
+ } |
+ |
+ // Make sure there aren't unexpected arguments. |
+ if (!command.takesArguments && argResults.rest.isNotEmpty) { |
+ command.usageException( |
+ 'Command "${argResults.name}" does not take any arguments.'); |
+ } |
+ |
+ return (await command.run()) as T; |
} |
} |
@@ -188,13 +203,19 @@ Run "$executableName help <command>" for more information about a command.'''; |
/// 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; |
- /// A short description of this command. |
+ /// A description of this command, included in [usage]. |
String get description; |
+ /// A short description of this command, included in [parent]'s |
+ /// [CommandRunner.usage]. |
+ /// |
+ /// This defaults to the first line of [description]. |
+ String get summary => description.split("\n").first; |
+ |
/// A single-line template for how to invoke this command (e.g. `"pub get |
/// [package]"`). |
String get invocation { |
@@ -214,18 +235,18 @@ abstract class Command { |
/// |
/// 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. |
/// |
@@ -245,7 +266,8 @@ abstract class Command { |
/// the constructor); they'll end up available via [argResults]. Subcommands |
/// should be registered with [addSubcommand] rather than directly on the |
/// parser. |
- final argParser = new ArgParser(); |
+ ArgParser get argParser => _argParser; |
+ final _argParser = new ArgParser(); |
/// Generates a string displaying usage information for this command. |
/// |
@@ -257,13 +279,13 @@ abstract class Command { |
/// |
/// If a subclass overrides this to return a string, it will automatically be |
/// added to the end of [usage]. |
- final String usageFooter = null; |
+ String get usageFooter => null; |
/// Returns [usage] with [description] removed from the beginning. |
String get _usageWithoutDescription { |
var buffer = new StringBuffer() |
- ..writeln('Usage: $invocation') |
- ..writeln(argParser.usage); |
+ ..writeln('Usage: $invocation') |
+ ..writeln(argParser.usage); |
if (_subcommands.isNotEmpty) { |
buffer.writeln(); |
@@ -282,8 +304,9 @@ abstract class Command { |
} |
/// An unmodifiable view of all sublevel commands of this command. |
- Map<String, Command> get subcommands => new UnmodifiableMapView(_subcommands); |
- final _subcommands = new Map<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. |
/// |
@@ -308,7 +331,7 @@ abstract class Command { |
/// |
/// This is intended to be overridden by commands that don't want to receive |
/// arguments. It has no effect for branch commands. |
- final takesArguments = true; |
+ bool get takesArguments => true; |
/// Alternate names for this command. |
/// |
@@ -316,7 +339,7 @@ abstract class Command { |
/// invoked on the command line. |
/// |
/// This is intended to be overridden. |
- final aliases = const <String>[]; |
+ List<String> get aliases => const []; |
Command() { |
argParser.addFlag('help', |
@@ -325,14 +348,15 @@ abstract class Command { |
/// 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; |
@@ -349,7 +373,7 @@ abstract class Command { |
/// Throws a [UsageException] with [message]. |
void usageException(String message) => |
- throw new UsageException(message, _usageWithoutDescription); |
+ throw new UsageException(message, _usageWithoutDescription); |
} |
/// Returns a string representation of [commands] fit for use in a usage string. |
@@ -360,7 +384,7 @@ String _getCommandUsage(Map<String, Command> commands, |
{bool isSubcommand: false}) { |
// Don't include aliases. |
var names = |
- commands.keys.where((name) => !commands[name].aliases.contains(name)); |
+ commands.keys.where((name) => !commands[name].aliases.contains(name)); |
// Filter out hidden ones, unless they are all hidden. |
var visible = names.where((name) => !commands[name].hidden); |
@@ -371,11 +395,17 @@ String _getCommandUsage(Map<String, Command> commands, |
var length = names.map((name) => name.length).reduce(math.max); |
var buffer = |
- new StringBuffer('Available ${isSubcommand ? "sub" : ""}commands:'); |
+ new StringBuffer('Available ${isSubcommand ? "sub" : ""}commands:'); |
for (var name in names) { |
+ var lines = commands[name].summary.split("\n"); |
buffer.writeln(); |
- buffer.write(' ${padRight(name, length)} ' |
- '${commands[name].description.split("\n").first}'); |
+ buffer.write(' ${padRight(name, length)} ${lines.first}'); |
+ |
+ for (var line in lines.skip(1)) { |
+ buffer.writeln(); |
+ buffer.write(' ' * (length + 5)); |
+ buffer.write(line); |
+ } |
} |
return buffer.toString(); |