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

Unified Diff: pkg/args/lib/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: Code review changes 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
« no previous file with comments | « pkg/args/README.md ('k') | pkg/args/lib/src/help_command.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: pkg/args/lib/command_runner.dart
diff --git a/pkg/args/lib/command_runner.dart b/pkg/args/lib/command_runner.dart
new file mode 100644
index 0000000000000000000000000000000000000000..ccf1b7c11ab79f23f6fb5fc8cfe541a48fb0da26
--- /dev/null
+++ b/pkg/args/lib/command_runner.dart
@@ -0,0 +1,377 @@
+// 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 'src/arg_parser.dart';
+import 'src/arg_results.dart';
+import 'src/help_command.dart';
+import 'src/usage_exception.dart';
+import 'src/utils.dart';
+
+export 'src/usage_exception.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.
Sean Eagan 2014/12/12 17:50:32 also used for usage text.
nweiz 2014/12/16 02:07:44 Done.
+ 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>";
Sean Eagan 2014/12/12 17:50:32 invocationTemplate? invocationHelp? Since `usage`
nweiz 2014/12/16 02:07:44 Yeah, I wasn't a big fan of this name. Pub had it
+
+ /// 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:
+${argParser.usage}
+
+${_getCommandUsage(_commands)}
+
+Run "$executableName help [command]" for more information about a command.''';
+ }
+
+ /// Returns [usage] with [description] removed from the beginning.
+ String get _usageWithoutDescription {
Sean Eagan 2014/12/12 17:50:32 Why is this necessary in the first place? Why not
nweiz 2014/12/16 02:07:44 This was following pub's behavior, but I think the
Sean Eagan 2014/12/16 17:41:05 Makes sense.
nweiz 2014/12/17 00:04:59 But the vastly-more-common use case becomes more d
+ // 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 top-level commands defined for this runner.
+ Map<String, Command> get commands =>
+ new UnmodifiableMapView(_commands);
+ final _commands = 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 argParser = new ArgParser();
+
+ CommandRunner(this.executableName, this.description) {
Sean Eagan 2014/12/12 17:50:32 executableName is almost always derivable from dar
+ argParser.addFlag('help', abbr: 'h', negatable: false,
+ help: 'Print this usage information.');
+ addCommand(new HelpCommand());
Sean Eagan 2014/12/12 17:50:32 This should only be added when the first sub-comma
nweiz 2014/12/16 02:07:44 What's the use of a command runner with no command
Sean Eagan 2014/12/16 17:41:05 The problem is, [CommandRunner] as currently defin
nweiz 2014/12/17 00:04:59 I'm skeptical that integrating with a separate lib
+ }
+
+ /// Prints the usage information for this runner.
+ ///
+ /// This is called internally by [run] and can be overridden by subclasses to
+ /// control how output is displayed or integrate with a logging system.
+ 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) {
+ _commands[name] = command;
+ argParser.addCommand(name, command.argParser);
+ }
+ 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 argParser.parse(args);
+ } on FormatException catch (error) {
+ 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.
+ ///
+ /// 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 topLevelOptions) {
+ return new Future.sync(() {
+ var options = topLevelOptions;
+ var commands = _commands;
+ 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];
+ command._globalOptions = topLevelOptions;
+ command._options = options;
+ 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>";
+ }
+
+ /// 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;
Sean Eagan 2014/12/12 17:50:32 It's weird to define [options] and [globalOptions]
nweiz 2014/12/16 02:07:44 It's more that it's useful for writing methods wit
Sean Eagan 2014/12/16 17:41:05 Once the command implementation gets sufficiently
nweiz 2014/12/17 00:04:59 You're describing a pretty complex architecture th
+ 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.
Sean Eagan 2014/12/12 17:50:32 appear in -> be hidden from
nweiz 2014/12/16 02:07:44 Done.
+ ///
+ /// 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.
Sean Eagan 2014/12/12 17:50:32 true -> false or "leaf commands are always visibl
nweiz 2014/12/16 02:07:44 Done.
+ bool get hidden {
Sean Eagan 2014/12/12 17:50:32 Why not add this directly to ArgParser?
nweiz 2014/12/16 02:07:44 See other discussions.
+ // 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:32 This is too coarse. Why should individual positio
+
+ /// 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:32 How about an AliasCommand for this: command.addSu
nweiz 2014/12/16 02:07:44 Part of the point of this architecture is to keep
Sean Eagan 2014/12/16 17:41:05 Agreed if there is some use of that metadata, and
nweiz 2014/12/17 00:04:59 The fact that the metadata is used by the dispatch
+
+ 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 to
+ /// control how output is displayed or integrate with a logging system.
+ void printUsage() => print(usage);
Sean Eagan 2014/12/12 17:50:32 I would think "to control how output is displayed
nweiz 2014/12/16 02:07:44 The usage is the only output that needs to be auto
Sean Eagan 2014/12/16 17:41:05 Unscripted also automatically outputs usage errors
nweiz 2014/12/17 00:04:59 This is another situation where I'm not sure the b
+
+ /// Throws a [UsageException] with [message].
+ void usageError(String message) =>
Sean Eagan 2014/12/12 17:50:32 usageException?
nweiz 2014/12/16 02:07:44 Done.
+ throw new UsageException(message, _usageWithoutDescription);
+}
+
+/// 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.
+ 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();
+}
« no previous file with comments | « pkg/args/README.md ('k') | pkg/args/lib/src/help_command.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698