From a5b5d220d152040b2ce08f6fd4f8efe26230dd1d Mon Sep 17 00:00:00 2001 From: "rnystrom@google.com" <rnystrom@google.com> Date: Thu, 6 Feb 2014 18:52:00 +0000 Subject: [PATCH] Support subcommands in pub and pub help. BUG= R=nweiz@google.com Review URL: https://codereview.chromium.org//138723005 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@32376 260f80e4-7a28-3924-810f-c04153c831b5 --- bin/pub.dart | 135 +++++++++-- lib/src/command.dart | 226 +++++++++--------- lib/src/command/build.dart | 3 +- lib/src/command/cache.dart | 45 +--- lib/src/command/cache_list.dart | 33 +++ lib/src/command/help.dart | 40 +++- lib/src/command/lish.dart | 11 +- lib/src/log.dart | 7 + lib/src/solver/solve_report.dart | 65 ++--- lib/src/utils.dart | 6 + ..._cannot_be_combined_with_dry_run_test.dart | 11 +- ...ckage_creation_provides_an_error_test.dart | 1 - test/pub_cache_test.dart | 24 -- test/pub_test.dart | 117 ++++++++- 14 files changed, 454 insertions(+), 270 deletions(-) create mode 100644 lib/src/command/cache_list.dart diff --git a/bin/pub.dart b/bin/pub.dart index a589dcc8..ecc8318a 100644 --- a/bin/pub.dart +++ b/bin/pub.dart @@ -7,9 +7,11 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:path/path.dart' as path; +import 'package:stack_trace/stack_trace.dart'; import '../lib/src/command.dart'; import '../lib/src/exit_codes.dart' as exit_codes; +import '../lib/src/http.dart'; import '../lib/src/io.dart'; import '../lib/src/log.dart' as log; import '../lib/src/sdk.dart' as sdk; @@ -38,18 +40,6 @@ void main(List<String> arguments) { return; } - if (options.command == null) { - if (options.rest.isEmpty) { - // No command was chosen. - PubCommand.printGlobalUsage(); - } else { - log.error('Could not find a command named "${options.rest[0]}".'); - log.error('Run "pub help" to see available commands.'); - flushThenExit(exit_codes.USAGE); - } - return; - } - if (options['trace']) { log.recordTranscript(); } @@ -81,8 +71,125 @@ void main(List<String> arguments) { cacheDir = '${Platform.environment['HOME']}/.pub-cache'; } - validatePlatform().then((_) { - PubCommand.commands[options.command.name].run(cacheDir, options, arguments); + validatePlatform().then((_) => runPub(cacheDir, options, arguments)); +} + +/// Runs the appropriate pub command whose [arguments] have been parsed to +/// [options] using the system cache in [cacheDir]. +/// +/// Handles and correctly reports any errors that occur while running. +void runPub(String cacheDir, ArgResults options, List<String> arguments) { + var captureStackChains = + options['trace'] || + options['verbose'] || + options['verbosity'] == 'all'; + + captureErrors(() => invokeCommand(cacheDir, options), + captureStackChains: captureStackChains).catchError((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)); + }).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); + }); +} + +/// 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 if (exception is UsageException) { + return exit_codes.USAGE; + } else { + return 1; + } +} + +/// Walks the command tree and runs the selected pub command. +Future invokeCommand(String cacheDir, ArgResults mainOptions) { + var commands = PubCommand.mainCommands; + var command; + var commandString = "pub"; + var options = mainOptions; + + while (commands.isNotEmpty) { + if (options.command == null) { + if (options.rest.isEmpty) { + if (command == null) { + // No top-level command was chosen. + PubCommand.printGlobalUsage(); + return new Future.value(); + } + + command.usageError('Missing subcommand for "$commandString".'); + } else { + if (command == null) { + PubCommand.usageErrorWithCommands(commands, + '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. + options = options.command; + command = commands[options.name]; + 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 syncFuture(() { + return command.run(cacheDir, options); + }).whenComplete(() { + command.cache.deleteTempDir(); }); } diff --git a/lib/src/command.dart b/lib/src/command.dart index e3dcab5e..3f6f35e1 100644 --- a/lib/src/command.dart +++ b/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); + }); +} diff --git a/lib/src/command/build.dart b/lib/src/command/build.dart index b470f1bf..5131ea24 100644 --- a/lib/src/command/build.dart +++ b/lib/src/command/build.dart @@ -27,8 +27,7 @@ final _allowedBuildDirectories = new Set<String>.from([ /// Handles the `build` pub command. class BuildCommand extends PubCommand { - String get description => - "Copy and compile all Dart entrypoints in the 'web' directory."; + String get description => "Apply transformers to build a package."; String get usage => "pub build [options]"; List<String> get aliases => const ["deploy", "settle-up"]; bool get takesArguments => true; diff --git a/lib/src/command/cache.dart b/lib/src/command/cache.dart index 97e5dbe7..dbef3153 100644 --- a/lib/src/command/cache.dart +++ b/lib/src/command/cache.dart @@ -4,48 +4,15 @@ library pub.command.cache; -import 'dart:async'; -import 'dart:convert'; - import '../command.dart'; -import '../exit_codes.dart' as exit_codes; -import '../io.dart'; -import '../log.dart' as log; +import 'cache_list.dart'; /// Handles the `cache` pub command. class CacheCommand extends PubCommand { - String get description => "Inspect the system cache."; - String get usage => "pub cache list"; - bool get hidden => true; - bool get requiresEntrypoint => false; - bool get takesArguments => true; - - Future onRun() { - // TODO(rnystrom): Use subcommand for "list". - if (commandOptions.rest.length != 1) { - log.error('The cache command expects one argument.'); - this.printUsage(); - return flushThenExit(exit_codes.USAGE); - } - - if ((commandOptions.rest[0] != 'list')) { - log.error('Unknown cache command "${commandOptions.rest[0]}".'); - this.printUsage(); - return flushThenExit(exit_codes.USAGE); - } - - // TODO(keertip): Add flag to list packages from non default sources - var packagesObj = <String, Map>{}; + String get description => "Work with the system cache."; + String get usage => "pub cache <subcommand>"; - for (var package in cache.sources.defaultSource.getCachedPackages()) { - - var packageInfo = packagesObj.putIfAbsent(package.name, () => {}); - packageInfo[package.version.toString()] = {'location': package.dir}; - } - - // TODO(keertip): Add support for non-JSON format - // and check for --format flag - log.message(JSON.encode({'packages': packagesObj})); - } + final subcommands = { + "list": new CacheListCommand() + }; } - diff --git a/lib/src/command/cache_list.dart b/lib/src/command/cache_list.dart new file mode 100644 index 00000000..d774a6ba --- /dev/null +++ b/lib/src/command/cache_list.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2013, 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 pub.command.cache_list; + +import 'dart:async'; +import 'dart:convert'; + +import '../command.dart'; +import '../log.dart' as log; + +/// Handles the `cache list` pub command. +class CacheListCommand extends PubCommand { + String get description => "List packages in the system cache."; + String get usage => "pub cache list"; + bool get hidden => true; + bool get requiresEntrypoint => false; + + Future onRun() { + // TODO(keertip): Add flag to list packages from non default sources. + var packagesObj = <String, Map>{}; + + for (var package in cache.sources.defaultSource.getCachedPackages()) { + var packageInfo = packagesObj.putIfAbsent(package.name, () => {}); + packageInfo[package.version.toString()] = {'location': package.dir}; + } + + // TODO(keertip): Add support for non-JSON format and check for --format + // flag. + log.message(JSON.encode({'packages': packagesObj})); + } +} diff --git a/lib/src/command/help.dart b/lib/src/command/help.dart index 91181754..c1889ff2 100644 --- a/lib/src/command/help.dart +++ b/lib/src/command/help.dart @@ -7,9 +7,6 @@ library pub.command.help; import 'dart:async'; import '../command.dart'; -import '../exit_codes.dart' as exit_codes; -import '../io.dart'; -import '../log.dart' as log; /// Handles the `help` pub command. class HelpCommand extends PubCommand { @@ -19,18 +16,39 @@ class HelpCommand extends PubCommand { bool get takesArguments => true; Future onRun() { + // Show the default help if no command was specified. if (commandOptions.rest.isEmpty) { PubCommand.printGlobalUsage(); - } else { - var name = commandOptions.rest[0]; - var command = PubCommand.commands[name]; - if (command == null) { - log.error('Could not find a command named "$name".'); - log.error('Run "pub help" to see available commands.'); - return flushThenExit(exit_codes.USAGE); + return null; + } + + // Walk the command tree to show help for the selected command or + // subcommand. + var commands = PubCommand.mainCommands; + var command = null; + var commandString = "pub"; + + for (var name in commandOptions.rest) { + if (commands.isEmpty) { + command.usageError( + 'Command "$commandString" does not expect a subcommand.'); } - command.printUsage(); + if (commands[name] == null) { + if (command == null) { + PubCommand.usageErrorWithCommands(commands, + 'Could not find a command named "$name".'); + } + + command.usageError( + 'Could not find a subcommand named "$name" for "$commandString".'); + } + + command = commands[name]; + commands = command.subcommands; + commandString += " $name"; } + + command.printUsage(); } } diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart index bd9c445e..d5e46763 100644 --- a/lib/src/command/lish.dart +++ b/lib/src/command/lish.dart @@ -10,7 +10,6 @@ import 'package:http/http.dart' as http; import '../command.dart'; import '../directory_tree.dart'; -import '../exit_codes.dart' as exit_codes; import '../http.dart'; import '../io.dart'; import '../log.dart' as log; @@ -94,9 +93,7 @@ class LishCommand extends PubCommand { Future onRun() { if (force && dryRun) { - log.error('Cannot use both --force and --dry-run.'); - this.printUsage(); - return flushThenExit(exit_codes.USAGE); + usageError('Cannot use both --force and --dry-run.'); } var packageBytesFuture = entrypoint.packageFiles().then((files) { @@ -144,15 +141,15 @@ class LishCommand extends PubCommand { if (dryRun) { var s = warnings.length == 1 ? '' : 's'; - log.warning("Package has ${warnings.length} warning$s."); + log.warning("\nPackage has ${warnings.length} warning$s."); return false; } - var message = 'Looks great! Are you ready to upload your package'; + var message = '\nLooks great! Are you ready to upload your package'; if (!warnings.isEmpty) { var s = warnings.length == 1 ? '' : 's'; - message = "Package has ${warnings.length} warning$s. Upload anyway"; + message = "\nPackage has ${warnings.length} warning$s. Upload anyway"; } return confirm(message).then((confirmed) { diff --git a/lib/src/log.dart b/lib/src/log.dart index f1726bba..0dcf088d 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -119,6 +119,13 @@ void write(Level level, message) { if (_loggers.isEmpty) showNormal(); var lines = splitLines(message.toString()); + + // Discard a trailing newline. This is useful since StringBuffers often end + // up with an extra newline at the end from using [writeln]. + if (lines.isNotEmpty && lines.last == "") { + lines.removeLast(); + } + var entry = new Entry(level, lines); var logFn = _loggers[level]; diff --git a/lib/src/solver/solve_report.dart b/lib/src/solver/solve_report.dart index 68568782..53507de2 100644 --- a/lib/src/solver/solve_report.dart +++ b/lib/src/solver/solve_report.dart @@ -39,12 +39,6 @@ class _SolveReport { final _output = new StringBuffer(); - /// To avoid emitting trailing newlines, we track if any are needed and only - /// emit then when text on the next line is about to be written. - // TODO(rnystrom): Move this into a separate class that wraps any StringSink - // with this logic. - int _pendingLines = 0; - _SolveReport(this._sources, this._root, this._previousLockFile, this._result) { // Fill the map so we can use it later. @@ -99,13 +93,13 @@ class _SolveReport { var removed = _previousLockFile.packages.keys.toSet(); removed.removeAll(names); if (removed.isNotEmpty) { - _writeln("These packages are no longer being depended on:"); + _output.writeln("These packages are no longer being depended on:"); removed = removed.toList(); removed.sort(); removed.forEach(_reportPackage); } - log.message(_output.toString()); + log.message(_output); } /// Displays a warning about the overrides currently in effect. @@ -113,14 +107,14 @@ class _SolveReport { _output.clear(); if (_result.overrides.isNotEmpty) { - _writeln("Warning: You are using these overridden dependencies:"); + _output.writeln("Warning: You are using these overridden dependencies:"); var overrides = _result.overrides.map((dep) => dep.name).toList(); overrides.sort((a, b) => a.compareTo(b)); overrides.forEach( (name) => _reportPackage(name, highlightOverride: false)); - log.warning(_output.toString()); + log.warning(_output); } } @@ -149,39 +143,39 @@ class _SolveReport { // < The package was downgraded from a higher version. // * Any other change between the old and new package. if (isOverridden) { - _write(log.magenta("! ")); + _output.write(log.magenta("! ")); } else if (newId == null) { - _write(log.red("- ")); + _output.write(log.red("- ")); } else if (oldId == null) { - _write(log.green("+ ")); + _output.write(log.green("+ ")); } else if (!_descriptionsEqual(oldId, newId)) { - _write(log.cyan("* ")); + _output.write(log.cyan("* ")); changed = true; } else if (oldId.version < newId.version) { - _write(log.green("> ")); + _output.write(log.green("> ")); changed = true; } else if (oldId.version > newId.version) { - _write(log.cyan("< ")); + _output.write(log.cyan("< ")); changed = true; } else { // Unchanged. - _write(" "); + _output.write(" "); } - _write(log.bold(id.name)); - _write(" "); + _output.write(log.bold(id.name)); + _output.write(" "); _writeId(id); // If the package was upgraded, show what it was upgraded from. if (changed) { - _write(" (was "); + _output.write(" (was "); _writeId(oldId); - _write(")"); + _output.write(")"); } // Highlight overridden packages. if (isOverridden && highlightOverride) { - _write(" ${log.magenta('(overridden)')}"); + _output.write(" ${log.magenta('(overridden)')}"); } // See if there are any newer versions of the package that we were @@ -211,10 +205,10 @@ class _SolveReport { "${pluralize('version', newerUnstable)} available)"; } - if (message != null) _write(" ${log.cyan(message)}"); + if (message != null) _output.write(" ${log.cyan(message)}"); } - _writeln(); + _output.writeln(); } /// Returns `true` if [a] and [b] are from the same source and have the same @@ -233,7 +227,7 @@ class _SolveReport { /// Writes a terse description of [id] (not including its name) to the output. void _writeId(PackageId id) { - _write(id.version); + _output.write(id.version); var source = null; if (_sources.contains(id.source)) { @@ -242,26 +236,7 @@ class _SolveReport { if (source != null && source != _sources.defaultSource) { var description = source.formatDescription(_root.dir, id.description); - _write(" from ${id.source} $description"); - } - } - - /// Writes [obj] to the output. - void _write(Object obj) { - while (_pendingLines > 0) { - _output.writeln(); - _pendingLines--; + _output.write(" from ${id.source} $description"); } - _output.write(obj); - } - - /// Writes [obj] (if not null) followed by a newline to the output. - /// - /// Doesn't actually immediately write a newline. Instead, it waits until - /// output is written on the next line. That way, trailing newlines aren't - /// displayed. - void _writeln([Object obj]) { - if (obj != null) _write(obj); - _pendingLines++; } } \ No newline at end of file diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 4a63ca3b..c5f81c25 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -853,6 +853,12 @@ class ApplicationException implements Exception { String toString() => message; } +/// A class for command usage exceptions. +class UsageException extends ApplicationException { + UsageException(String message) + : super(message); +} + /// An class for exceptions where a package could not be found in a [Source]. /// /// The source is responsible for wrapping its internal exceptions in this so diff --git a/test/lish/force_cannot_be_combined_with_dry_run_test.dart b/test/lish/force_cannot_be_combined_with_dry_run_test.dart index 26231e3e..c436ff24 100644 --- a/test/lish/force_cannot_be_combined_with_dry_run_test.dart +++ b/test/lish/force_cannot_be_combined_with_dry_run_test.dart @@ -14,7 +14,16 @@ main() { integration('--force cannot be combined with --dry-run', () { schedulePub(args: ['lish', '--force', '--dry-run'], - error: "Cannot use both --force and --dry-run.", + error: """ + Cannot use both --force and --dry-run. + + Usage: pub publish [options] + -h, --help Print usage information for this command. + -n, --dry-run Validate but do not publish the package. + -f, --force Publish without confirmation if there are no errors. + --server The package server to which to upload this package. + (defaults to "https://pub.dartlang.org") + """, exitCode: exit_codes.USAGE); }); } diff --git a/test/lish/package_creation_provides_an_error_test.dart b/test/lish/package_creation_provides_an_error_test.dart index d747b3be..a1c134d1 100644 --- a/test/lish/package_creation_provides_an_error_test.dart +++ b/test/lish/package_creation_provides_an_error_test.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'package:scheduled_test/scheduled_test.dart'; import 'package:scheduled_test/scheduled_server.dart'; -import '../../lib/src/exit_codes.dart' as exit_codes; import '../descriptor.dart' as d; import '../test_pub.dart'; import 'utils.dart'; diff --git a/test/pub_cache_test.dart b/test/pub_cache_test.dart index 91bc4a0c..44831661 100644 --- a/test/pub_cache_test.dart +++ b/test/pub_cache_test.dart @@ -17,30 +17,6 @@ main() { "pub.dartlang.org", package); } - integration('running pub cache displays error message', () { - schedulePub(args: ['cache'], - output: ''' - Inspect the system cache. - - Usage: pub cache list - -h, --help Print usage information for this command. - ''', - error: 'The cache command expects one argument.', - exitCode: 64); - }); - - integration('running pub cache foo displays error message', () { - schedulePub(args: ['cache' ,'foo'], - output: ''' - Inspect the system cache. - - Usage: pub cache list - -h, --help Print usage information for this command. - ''', - error: 'Unknown cache command "foo".', - exitCode: 64); - }); - integration('running pub cache list when there is no cache', () { schedulePub(args: ['cache', 'list'], output: '{"packages":{}}'); }); diff --git a/test/pub_test.dart b/test/pub_test.dart index 336e125d..8c9e6525 100644 --- a/test/pub_test.dart +++ b/test/pub_test.dart @@ -6,12 +6,13 @@ library pub_tests; import 'package:scheduled_test/scheduled_test.dart'; +import '../lib/src/exit_codes.dart' as exit_codes; import 'test_pub.dart'; final USAGE_STRING = """ Pub is a package manager for Dart. - Usage: pub command [arguments] + Usage: pub <command> [arguments] Global options: -h, --help Print this usage information. @@ -27,7 +28,7 @@ final USAGE_STRING = """ -v, --verbose Shortcut for "--verbosity=all". Available commands: - build Copy and compile all Dart entrypoints in the 'web' directory. + build Apply transformers to build a package. get Get the current package's dependencies. help Display help information for Pub. publish Publish the current package to pub.dartlang.org. @@ -80,6 +81,21 @@ main() { '''); }); + integration('running pub with --help after a command with subcommands shows ' + 'command usage', () { + schedulePub(args: ['cache', '--help'], + output: ''' + Work with the system cache. + + Usage: pub cache <subcommand> + -h, --help Print usage information for this command. + + Available subcommands: + list List packages in the system cache. + '''); + }); + + integration('running pub with just --version displays version', () { schedulePub(args: ['--version'], output: VERSION_STRING); }); @@ -88,9 +104,32 @@ main() { schedulePub(args: ['quylthulg'], error: ''' Could not find a command named "quylthulg". - Run "pub help" to see available commands. + + Available commands: + build Apply transformers to build a package. + get Get the current package's dependencies. + help Display help information for Pub. + publish Publish the current package to pub.dartlang.org. + serve Run a local web development server. + upgrade Upgrade the current package's dependencies to latest versions. + uploader Manage uploaders for a package on pub.dartlang.org. + version Print pub version. + ''', + exitCode: exit_codes.USAGE); + }); + + integration('an unknown subcommand displays an error message', () { + schedulePub(args: ['cache', 'quylthulg'], + error: ''' + Could not find a subcommand named "quylthulg" for "pub cache". + + Usage: pub cache <subcommand> + -h, --help Print usage information for this command. + + Available subcommands: + list List packages in the system cache. ''', - exitCode: 64); + exitCode: exit_codes.USAGE); }); integration('an unknown option displays an error message', () { @@ -99,7 +138,7 @@ main() { Could not find an option named "blorf". Run "pub help" to see available options. ''', - exitCode: 64); + exitCode: exit_codes.USAGE); }); integration('an unknown command option displays an error message', () { @@ -110,21 +149,32 @@ main() { Could not find an option named "blorf". Run "pub help" to see available options. ''', - exitCode: 64); + exitCode: exit_codes.USAGE); }); integration('an unexpected argument displays an error message', () { schedulePub(args: ['version', 'unexpected'], - output: ''' - Print pub version. + error: ''' + Command "version" does not take any arguments. Usage: pub version -h, --help Print usage information for this command. ''', + exitCode: exit_codes.USAGE); + }); + + integration('a missing subcommand displays an error message', () { + schedulePub(args: ['cache'], error: ''' - Command "version" does not take any arguments. + Missing subcommand for "pub cache". + + Usage: pub cache <subcommand> + -h, --help Print usage information for this command. + + Available subcommands: + list List packages in the system cache. ''', - exitCode: 64); + exitCode: exit_codes.USAGE); }); group('help', () { @@ -157,15 +207,58 @@ main() { '''); }); + integration('shows help for a subcommand', () { + schedulePub(args: ['help', 'cache', 'list'], + output: ''' + List packages in the system cache. + + Usage: pub cache list + -h, --help Print usage information for this command. + '''); + }); + integration('an unknown help command displays an error message', () { schedulePub(args: ['help', 'quylthulg'], error: ''' Could not find a command named "quylthulg". - Run "pub help" to see available commands. + + Available commands: + build Apply transformers to build a package. + get Get the current package's dependencies. + help Display help information for Pub. + publish Publish the current package to pub.dartlang.org. + serve Run a local web development server. + upgrade Upgrade the current package's dependencies to latest versions. + uploader Manage uploaders for a package on pub.dartlang.org. + version Print pub version. + ''', + exitCode: exit_codes.USAGE); + }); + + integration('an unknown help subcommand displays an error message', () { + schedulePub(args: ['help', 'cache', 'quylthulg'], + error: ''' + Could not find a subcommand named "quylthulg" for "pub cache". + + Usage: pub cache <subcommand> + -h, --help Print usage information for this command. + + Available subcommands: + list List packages in the system cache. ''', - exitCode: 64); + exitCode: exit_codes.USAGE); }); + integration('an unexpected help subcommand displays an error message', () { + schedulePub(args: ['help', 'version', 'badsubcommand'], + error: ''' + Command "pub version" does not expect a subcommand. + + Usage: pub version + -h, --help Print usage information for this command. + ''', + exitCode: exit_codes.USAGE); + }); }); group('version', () { -- GitLab