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

Unified Diff: pkg/args/lib/src/command_runner.dart

Issue 797473002: Add a CommandRunner class for dispatching commands to args. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years 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
Index: pkg/args/lib/src/command_runner.dart
diff --git a/pkg/args/lib/src/command_runner.dart b/pkg/args/lib/src/command_runner.dart
new file mode 100644
index 0000000000000000000000000000000000000000..cfce5e8b124314690fe2c2d65d79b052a26ed259
--- /dev/null
+++ b/pkg/args/lib/src/command_runner.dart
@@ -0,0 +1,369 @@
+// Copyright (c) 2014, 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.
+
+library args.command_runner;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:math' as math;
+
+import 'arg_parser.dart';
+import 'arg_results.dart';
+import 'help_command.dart';
+import 'usage_exception.dart';
+import 'utils.dart';
+
+/// A class for invoking [Commands] based on raw command-line arguments.
+class CommandRunner {
+ /// The name of the executable being run.
+ ///
+ /// Used for error reporting.
+ final String executableName;
+
+ /// A short description of this executable.
+ final String description;
+
+ /// A single-line template for how to invoke this executable.
+ ///
+ /// Defaults to "$executableName [command] <arguments>". Subclasses can
+ /// override this for a more specific template.
+ String get usageTemplate => "$executableName [command] <arguments>";
+
+ /// Generates a string displaying usage information for the executable.
+ ///
+ /// This includes usage for the global arguments as well as a list of
+ /// top-level commands.
+ String get usage {
+ return '''
+$description
+
+Usage: $usageTemplate
+
+Global options:
+${topLevelArgParser.usage}
+
+${_getCommandUsage(_topLevelCommands)}
+
+Run "$executableName help [command]" for more information about a command.''';
+ }
+
+ /// Returns [usage] with [description] removed from the beginning.
+ String get _usageWithoutDescription {
+ // Base this on the return value of [usage] so that subclasses can override
+ // usage and have their changes reflected here.
+ return usage.replaceFirst("$description\n\n", "");
Bob Nystrom 2014/12/11 20:25:30 This feels super sketchy to me. What's the use cas
nweiz 2014/12/11 23:55:24 Usually extra bottom-matter. Pub does it to add li
Bob Nystrom 2014/12/12 18:13:22 That was my guess. How about just making a "footer
nweiz 2014/12/16 02:07:44 Done.
+ }
+
+ /// An unmodifiable view of all top-level commands defined for this runner.
+ Map<String, Command> get topLevelCommands =>
Bob Nystrom 2014/12/11 20:25:30 I think `commands` is clear enough here. I don't t
nweiz 2014/12/11 23:55:24 Done.
+ new UnmodifiableMapView(_topLevelCommands);
+ final _topLevelCommands = new Map<String, Command>();
+
+ /// The top-level argument parser.
+ ///
+ /// Global options should be registered with this parser; they'll end up
+ /// available via [Command.globalOptions]. Commands should be registered with
+ /// [addCommand] rather than directly on the parser.
+ final topLevelArgParser = new ArgParser();
Bob Nystrom 2014/12/11 20:25:30 Can this just be `argParser`? The fact that it's o
nweiz 2014/12/11 23:55:24 Done.
+
+ CommandRunner(this.executableName, this.description) {
Sean Eagan 2014/12/12 17:50:31 The executable name should rarely if ever need to
nweiz 2014/12/16 02:07:44 args doesn't currently use dart:io. Adding an impo
+ topLevelArgParser.addFlag('help', abbr: 'h', negatable: false,
+ help: 'Print this usage information.');
+ addCommand(new HelpCommand());
+ }
+
+ /// Prints the usage information for this runner.
+ ///
+ /// This is called internally by [run] and can be overridden by subclasses.
Bob Nystrom 2014/12/11 20:25:30 "...to control how output is displayed" ?
nweiz 2014/12/11 23:55:24 Done.
+ void printUsage() => print(usage);
+
+ /// Throws a [UsageException] with [message].
+ void usageError(String message) =>
+ throw new UsageException(message, _usageWithoutDescription);
+
+ /// Adds [Command] as a top-level command to this runner.
+ void addCommand(Command command) {
+ var names = [command.name]..addAll(command.aliases);
+ for (var name in names) {
+ _topLevelCommands[name] = command;
+ topLevelArgParser.addCommand(name, command.argParser);
Bob Nystrom 2014/12/11 20:25:30 Can we unify the parser's concept of commands with
nweiz 2014/12/11 23:55:24 Moved to a separate library.
+ }
+ command._runner = this;
+ }
+
+ /// 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) =>
+ new Future.sync(() => runCommand(parse(args)));
+
+ /// Parses [args] and returns the result, converting a [FormatException] 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 topLevelArgParser.parse(args);
+ } on FormatException catch (error) {
Sean Eagan 2014/12/12 17:50:31 Why not just fix ArgParser.parse to throw UsageErr
nweiz 2014/12/16 02:07:44 I'm not a fan of that for a few reasons: * It wou
+ usageError(error.message);
+ }
+ }
+
+ /// Runs the command specified by [topLevelOptions].
+ ///
+ /// This is notionally a protected method. It may be overridden or called from
+ /// subclasses, but it shouldn't be called externally.
Bob Nystrom 2014/12/11 20:25:30 Can you give some details about why a user may wis
nweiz 2014/12/11 23:55:24 Done.
+ Future runCommand(ArgResults topLevelOptions) {
+ return new Future.sync(() {
+ var options = topLevelOptions;
+ var commands = _topLevelCommands;
+ var command;
+ var commandString = executableName;
+
+ while (commands.isNotEmpty) {
+ if (options.command == null) {
+ if (options.rest.isEmpty) {
+ if (command == null) {
+ // No top-level command was chosen.
+ printUsage();
+ return new Future.value();
+ }
+
+ command.usageError('Missing subcommand for "$commandString".');
+ } else {
+ if (command == null) {
+ usageError(
+ 'Could not find a command named "${options.rest[0]}".');
+ }
+
+ command.usageError('Could not find a subcommand named '
+ '"${options.rest[0]}" for "$commandString".');
+ }
+ }
+
+ // Step into the command.
+ var parent = command;
+ options = options.command;
+ command = commands[options.name]
+ .._globalOptions = topLevelOptions
+ .._options = options;
Bob Nystrom 2014/12/11 20:25:30 Oh man, cascaded setters make my skin crawl.
nweiz 2014/12/11 23:55:24 Done.
+ commands = command._subcommands;
+ commandString += " ${options.name}";
+
+ if (options['help']) {
+ command.printUsage();
+ return new Future.value();
+ }
+ }
+
+ // Make sure there aren't unexpected arguments.
+ if (!command.takesArguments && options.rest.isNotEmpty) {
+ command.usageError(
+ 'Command "${options.name}" does not take any arguments.');
+ }
+
+ return command.run();
+ });
+ }
+}
+
+/// A single command.
+///
+/// A command is known as a "leaf command" if it has no subcommands and is meant
+/// to be run. Leaf commands must override [run].
+///
+/// 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 {
+ /// The name of this command.
+ String get name;
+
+ /// A short description of this command.
+ String get description;
+
+ /// A single-line template for how to invoke this command (e.g. `"pub get
+ /// [package]"`).
+ String get usageTemplate {
+ var parents = [name];
+ for (var command = parent; command != null; command = command.parent) {
+ parents.add(command.name);
+ }
+ parents.add(runner.executableName);
+
+ var invocation = parents.reversed.join(" ");
+ return _subcommands.isNotEmpty
+ ? "$invocation [subcommand] <arguments>"
+ : "$invocation <arguments>";
Bob Nystrom 2014/12/11 20:25:30 Nit, but operators should be trailing not leading.
nweiz 2014/12/11 23:55:24 Done.
+ }
+
+ /// The command's parent command, if this is a subcommand.
+ ///
+ /// This will be `null` until [Command.addSubcommmand] has been called with
+ /// this command.
+ Command get parent => _parent;
+ Command _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 {
+ if (parent == null) return _runner;
+ return parent.runner;
+ }
+ CommandRunner _runner;
+
+ /// The parsed global options.
+ ///
+ /// This will be `null` until just before [Command.run] is called.
+ ArgResults get globalOptions => _globalOptions;
+ ArgResults _globalOptions;
+
+ /// The parsed options for this command.
+ ///
+ /// This will be `null` until just before [Command.run] is called.
+ ArgResults get options => _options;
+ ArgResults _options;
+
+ /// The argument parser for this command.
+ ///
+ /// Options for this command should be registered with this parser (often in
+ /// the constructor); they'll end up available via [options]. Subcommands
+ /// should be registered with [addSubcommand] rather than directly on the
+ /// parser.
+ final argParser = new ArgParser();
+
+ /// Generates a string displaying usage information for this command.
+ ///
+ /// This includes usage for the command's arguments as well as a list of
+ /// subcommands, if there are any.
+ String get usage {
+ var buffer = new StringBuffer()
+ ..writeln(description)
+ ..writeln()
+ ..writeln('Usage: $usageTemplate')
+ ..writeln(argParser.usage);
+
+ if (_subcommands.isNotEmpty) {
+ buffer.writeln();
+ buffer.writeln(_getCommandUsage(_subcommands, isSubcommand: true));
+ }
+
+ buffer.writeln();
+ buffer.write('Run "${runner.executableName} help" to see global options.');
+
+ return buffer.toString();
+ }
+
+ /// Returns [usage] with [description] removed from the beginning.
+ String get _usageWithoutDescription {
+ // Base this on the return value of [usage] so that subclasses can override
+ // usage and have their changes reflected here.
+ return usage.replaceFirst("$description\n\n", "");
+ }
+
+ /// An unmodifiable view of all sublevel commands of this command.
+ Map<String, Command> get subcommands =>
+ new UnmodifiableMapView(_subcommands);
+ final _subcommands = new Map<String, Command>();
+
+ /// Whether or not this command should appear in help listings.
+ ///
+ /// This is intended to be overridden by commands that want to mark themselves
+ /// hidden.
+ ///
+ /// By default, this is always true for leaf commands. It's true for branch
+ /// commands as long as any of their leaf commands are visible.
+ bool get hidden {
+ // Leaf commands are visible by default.
+ if (_subcommands.isEmpty) return false;
+
+ // Otherwise, a command is hidden if all of its subcommands are.
+ return _subcommands.values.every((subcommand) => subcommand.hidden);
+ }
+
+ /// Whether or not this command takes positional arguments in addition to
+ /// options.
+ ///
+ /// If false, [CommandRunner.run] will throw a [UsageException] if arguments
+ /// are provided. Defaults to true.
+ ///
+ /// 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;
Sean Eagan 2014/12/12 17:50:31 This is too coarse grained. Unscripted allows add
nweiz 2014/12/16 02:07:44 That would be a much broader change largely in Arg
+
+ /// Alternate names for this command.
+ ///
+ /// These names won't be used in the documentation, but they will work when
+ /// invoked on the command line.
+ ///
+ /// This is intended to be overridden.
+ final aliases = const <String>[];
Sean Eagan 2014/12/12 17:50:31 If adding this, it ought to be exposed on ArgParse
nweiz 2014/12/16 02:07:44 I'm open to adding this to ArgParser.addCommand, b
+
+ Command() {
+ argParser.addFlag('help', abbr: 'h', negatable: false,
+ help: 'Print this usage information.');
+ }
+
+ /// Runs this command.
+ ///
+ /// If this returns a [Future], [CommandRunner.run] won't complete until the
+ /// returned [Future] does. Otherwise, the return value is ignored.
+ run() {
+ throw new UnimplementedError("Leaf command $this must implement run().");
+ }
+
+ /// Adds [Command] as a subcommand of this.
+ void addSubcommand(Command command) {
+ var names = [command.name]..addAll(command.aliases);
+ for (var name in names) {
+ _subcommands[name] = command;
+ argParser.addCommand(name, command.argParser);
+ }
+ command._parent = this;
+ }
+
+ /// Prints the usage information for this command.
+ ///
+ /// This is called internally by [run] and can be overridden by subclasses.
+ void printUsage() => print(usage);
+
+ /// Throws a [UsageException] with [message].
+ void usageError(String message) =>
+ throw new UsageException(message, _usageWithoutDescription);
Bob Nystrom 2014/12/11 20:25:30 These methods are pretty similar to ones in Comman
nweiz 2014/12/11 23:55:24 I think that muddies the semantic distinction too
Bob Nystrom 2014/12/12 18:13:22 You think so? It has some extra functionality, but
nweiz 2014/12/16 02:07:44 I don't think behavioral overlap alone is enough t
Bob Nystrom 2014/12/16 16:50:41 I do feel there is an is-a relationship here (I th
+}
+
+/// Returns a string representation of [commands] fit for use in a usage string.
+///
+/// [isSubcommand] indicates whether the commands should be called "commands" or
+/// "subcommands".
+String _getCommandUsage(Map<String, Command> commands,
+ {bool isSubcommand: false}) {
+ // Don't include aliases.
+ var names = 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);
+ if (visible.isNotEmpty) names = visible;
+
+ // Show the commands alphabetically.
Bob Nystrom 2014/12/11 20:25:30 Do you think users will want to be able to control
nweiz 2014/12/11 23:55:24 I think it's more likely that the user will want c
Bob Nystrom 2014/12/12 18:13:22 SGTM.
+ names = names.toList()..sort();
+ var length = names.map((name) => name.length).reduce(math.max);
+
+ var buffer = new StringBuffer(
+ 'Available ${isSubcommand ? "sub" : ""}commands:');
+ for (var name in names) {
+ buffer.writeln();
+ buffer.write(' ${padRight(name, length)} '
+ '${commands[name].description.split("\n").first}');
+ }
+
+ return buffer.toString();
+}

Powered by Google App Engine
This is Rietveld 408576698