Index: sdk/lib/_internal/pub/lib/src/command.dart |
diff --git a/sdk/lib/_internal/pub/lib/src/command.dart b/sdk/lib/_internal/pub/lib/src/command.dart |
index e3dcab5ef072fac8cfb9c72cf00217044dcaeee4..3f6f35e13f63143fea65a945e9d83edd6bed7dcc 100644 |
--- a/sdk/lib/_internal/pub/lib/src/command.dart |
+++ b/sdk/lib/_internal/pub/lib/src/command.dart |
@@ -5,12 +5,10 @@ |
library pub.command; |
import 'dart:async'; |
-import 'dart:io'; |
import 'dart:math' as math; |
import 'package:args/args.dart'; |
import 'package:path/path.dart' as path; |
-import 'package:stack_trace/stack_trace.dart'; |
import 'command/build.dart'; |
import 'command/cache.dart'; |
@@ -23,17 +21,18 @@ import 'command/upgrade.dart'; |
import 'command/uploader.dart'; |
import 'command/version.dart'; |
import 'entrypoint.dart'; |
-import 'exit_codes.dart' as exit_codes; |
-import 'http.dart'; |
-import 'io.dart'; |
import 'log.dart' as log; |
import 'system_cache.dart'; |
import 'utils.dart'; |
/// The base class for commands for the pub executable. |
+/// |
+/// A command may either be a "leaf" command or it may be a parent for a set |
+/// of subcommands. Only leaf commands are ever actually invoked. If a command |
+/// has subcommands, then one of those must always be chosen. |
abstract class PubCommand { |
/// The commands that pub understands. |
- static final Map<String, PubCommand> commands = _initCommands(); |
+ static final Map<String, PubCommand> mainCommands = _initCommands(); |
/// The top-level [ArgParser] used to parse the pub command line. |
static final pubArgParser = _initArgParser(); |
@@ -44,35 +43,59 @@ abstract class PubCommand { |
var buffer = new StringBuffer(); |
buffer.writeln('Pub is a package manager for Dart.'); |
buffer.writeln(); |
- buffer.writeln('Usage: pub command [arguments]'); |
+ buffer.writeln('Usage: pub <command> [arguments]'); |
buffer.writeln(); |
buffer.writeln('Global options:'); |
buffer.writeln(pubArgParser.getUsage()); |
+ buffer.write(_listCommands(mainCommands)); |
buffer.writeln(); |
+ buffer.writeln( |
+ 'Use "pub help [command]" for more information about a command.'); |
+ |
+ log.message(buffer); |
+ } |
+ |
+ /// Fails with a usage error [message] when trying to select from one of |
+ /// [commands]. |
+ static void usageErrorWithCommands(Map<String, PubCommand> commands, |
+ String message) { |
+ throw new UsageException("$message\n${_listCommands(commands)}"); |
+ } |
- // Show the public commands alphabetically. |
- var names = ordered(commands.keys.where((name) => |
- !commands[name].aliases.contains(name) && |
- !commands[name].hidden)); |
+ /// Writes [commands] in a nicely formatted list to [buffer]. |
+ static String _listCommands(Map<String, PubCommand> commands) { |
+ // If there are no subcommands, do nothing. |
+ if (commands.isEmpty) return ""; |
+ // 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 = ordered(names); |
var length = names.map((name) => name.length).reduce(math.max); |
+ var isSubcommand = commands != mainCommands; |
- buffer.writeln('Available commands:'); |
+ var buffer = new StringBuffer(); |
+ buffer.writeln(); |
+ buffer.writeln('Available ${isSubcommand ? "sub" : ""}commands:'); |
for (var name in names) { |
buffer.writeln(' ${padRight(name, length)} ' |
'${commands[name].description}'); |
} |
- buffer.writeln(); |
- buffer.write( |
- 'Use "pub help [command]" for more information about a command.'); |
- log.message(buffer.toString()); |
+ return buffer.toString(); |
} |
SystemCache cache; |
/// The parsed options for this command. |
- ArgResults commandOptions; |
+ ArgResults get commandOptions => _commandOptions; |
+ ArgResults _commandOptions; |
Entrypoint entrypoint; |
@@ -81,18 +104,27 @@ abstract class PubCommand { |
/// If the command is undocumented and should not appear in command listings, |
/// this will be `true`. |
- bool get hidden => false; |
+ 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); |
+ } |
/// How to invoke this command (e.g. `"pub get [package]"`). |
String get usage; |
- /// Whether or not this command requires [entrypoint] to be defined. If false, |
- /// pub won't look for a pubspec and [entrypoint] will be null when the |
- /// command runs. |
+ /// Whether or not this command requires [entrypoint] to be defined. |
+ /// |
+ /// If false, pub won't look for a pubspec and [entrypoint] will be null when |
+ /// the command runs. This only needs to be set in leaf commands. |
bool get requiresEntrypoint => true; |
- /// Whether or not this command takes arguments in addition to options. If |
- /// false, pub will exit with an error if arguments are provided. |
+ /// Whether or not this command takes arguments in addition to options. |
+ /// |
+ /// If false, pub will exit with an error if arguments are provided. This |
+ /// only needs to be set in leaf commands. |
bool get takesArguments => false; |
/// Alternate names for this command. These names won't be used in the |
@@ -102,9 +134,17 @@ abstract class PubCommand { |
/// The [ArgParser] for this command. |
final commandParser = new ArgParser(); |
+ /// Subcommands exposed by this command. |
+ /// |
+ /// If empty, then this command has no subcommands. Otherwise, a subcommand |
+ /// must be specified by the user. In that case, this command's [onRun] will |
+ /// not be called and the subcommand's will. |
+ final subcommands = <String, PubCommand>{}; |
+ |
/// Override this to use offline-only sources instead of hitting the network. |
+ /// |
/// This will only be called before the [SystemCache] is created. After that, |
- /// it has no effect. |
+ /// it has no effect. This only needs to be set in leaf commands. |
bool get isOffline => false; |
PubCommand() { |
@@ -113,94 +153,50 @@ abstract class PubCommand { |
help: 'Print usage information for this command.'); |
} |
- void run(String cacheDir, ArgResults options, List<String> arguments) { |
- commandOptions = options.command; |
- |
- if (commandOptions['help']) { |
- this.printUsage(); |
- return; |
- } |
+ /// Runs this command using a system cache at [cacheDir] with [options]. |
+ Future run(String cacheDir, ArgResults options) { |
+ _commandOptions = options; |
cache = new SystemCache.withSources(cacheDir, isOffline: isOffline); |
- handleError(error, Chain chain) { |
- // This is basically the top-level exception handler so that we don't |
- // spew a stack trace on our users. |
- var message; |
- |
- log.error(getErrorMessage(error)); |
- log.fine("Exception type: ${error.runtimeType}"); |
- |
- if (options['trace'] || !isUserFacingException(error)) { |
- log.error(chain.terse); |
- } else { |
- log.fine(chain.terse); |
- } |
- |
- if (error is ApplicationException && error.innerError != null) { |
- var message = "Wrapped exception: ${error.innerError}"; |
- if (error.innerTrace != null) message = "$message\n${error.innerTrace}"; |
- log.fine(message); |
- } |
- |
- if (options['trace']) { |
- log.dumpTranscript(); |
- } else if (!isUserFacingException(error)) { |
- log.error(""" |
-This is an unexpected error. Please run |
- |
- pub --trace ${arguments.map((arg) => "'$arg'").join(' ')} |
- |
-and include the results in a bug report on http://dartbug.com/new. |
-"""); |
- } |
- |
- return flushThenExit(_chooseExitCode(error)); |
+ if (requiresEntrypoint) { |
+ // TODO(rnystrom): Will eventually need better logic to walk up |
+ // subdirectories until we hit one that looks package-like. For now, |
+ // just assume the cwd is it. |
+ entrypoint = new Entrypoint(path.current, cache); |
} |
- var captureStackChains = |
- options['trace'] || options['verbose'] || options['verbosity'] == 'all'; |
- captureErrors(() { |
- return syncFuture(() { |
- // Make sure there aren't unexpected arguments. |
- if (!takesArguments && commandOptions.rest.isNotEmpty) { |
- log.error('Command "${commandOptions.name}" does not take any ' |
- 'arguments.'); |
- this.printUsage(); |
- return flushThenExit(exit_codes.USAGE); |
- } |
- |
- if (requiresEntrypoint) { |
- // TODO(rnystrom): Will eventually need better logic to walk up |
- // subdirectories until we hit one that looks package-like. For now, |
- // just assume the cwd is it. |
- entrypoint = new Entrypoint(path.current, cache); |
- } |
- |
- var commandFuture = onRun(); |
- if (commandFuture == null) return true; |
- |
- return commandFuture; |
- }).whenComplete(() => cache.deleteTempDir()); |
- }, captureStackChains: captureStackChains).catchError(handleError) |
- .then((_) { |
- // Explicitly exit on success to ensure that any dangling dart:io handles |
- // don't cause the process to never terminate. |
- return flushThenExit(exit_codes.SUCCESS); |
- }); |
+ return syncFuture(onRun); |
} |
- /// Override this to perform the specific command. Return a future that |
- /// completes when the command is done or fails if the command fails. If the |
- /// command is synchronous, it may return `null`. |
- Future onRun(); |
+ /// Override this to perform the specific command. |
+ /// |
+ /// Return a future that completes when the command is done or fails if the |
+ /// command fails. If the command is synchronous, it may return `null`. Only |
+ /// leaf command should override this. |
+ Future onRun() { |
+ // Leaf commands should override this and non-leaf commands should never |
+ // call it. |
+ assert(false); |
+ } |
/// Displays usage information for this command. |
void printUsage([String description]) { |
if (description == null) description = this.description; |
+ log.message('$description\n\n${_getUsage()}'); |
+ } |
+ // TODO(rnystrom): Use this in other places handle usage failures. |
+ /// Throw an [ApplicationException] for a usage error of this command with |
+ /// [message]. |
+ void usageError(String message) { |
+ throw new UsageException("$message\n\n${_getUsage()}"); |
+ } |
+ |
+ /// Generates a string of usage information for this command. |
+ String _getUsage() { |
var buffer = new StringBuffer(); |
- buffer.write('$description\n\nUsage: $usage'); |
+ buffer.write('Usage: $usage'); |
var commandUsage = commandParser.getUsage(); |
if (!commandUsage.isEmpty) { |
@@ -208,20 +204,12 @@ and include the results in a bug report on http://dartbug.com/new. |
buffer.write(commandUsage); |
} |
- log.message(buffer.toString()); |
- } |
- |
- /// Returns the appropriate exit code for [exception], falling back on 1 if no |
- /// appropriate exit code could be found. |
- int _chooseExitCode(exception) { |
- if (exception is HttpException || exception is HttpException || |
- exception is SocketException || exception is PubHttpException) { |
- return exit_codes.UNAVAILABLE; |
- } else if (exception is FormatException) { |
- return exit_codes.DATA; |
- } else { |
- return 1; |
+ if (subcommands.isNotEmpty) { |
+ buffer.writeln(); |
+ buffer.write(_listCommands(subcommands)); |
} |
+ |
+ return buffer.toString(); |
} |
} |
@@ -272,9 +260,19 @@ ArgParser _initArgParser() { |
help: 'Shortcut for "--verbosity=all".'); |
// Register the commands. |
- PubCommand.commands.forEach((name, command) { |
- argParser.addCommand(name, command.commandParser); |
+ PubCommand.mainCommands.forEach((name, command) { |
+ _registerCommand(name, command, argParser); |
}); |
return argParser; |
} |
+ |
+/// Registers a [command] with [name] on [parser]. |
+void _registerCommand(String name, PubCommand command, ArgParser parser) { |
+ parser.addCommand(name, command.commandParser); |
+ |
+ // Recursively wire up any subcommands. |
+ command.subcommands.forEach((name, subcommand) { |
+ _registerCommand(name, subcommand, command.commandParser); |
+ }); |
+} |