| 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();
|
|
|