// 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; import 'dart:async'; import 'dart:math' as math; import 'package:args/args.dart'; import 'package:path/path.dart' as path; import 'command/build.dart'; import 'command/cache.dart'; import 'command/deps.dart'; import 'command/downgrade.dart'; import 'command/get.dart'; import 'command/global.dart'; import 'command/help.dart'; import 'command/lish.dart'; import 'command/list_package_dirs.dart'; import 'command/run.dart'; import 'command/serve.dart'; import 'command/upgrade.dart'; import 'command/uploader.dart'; import 'command/version.dart'; import 'entrypoint.dart'; import 'exceptions.dart'; import 'log.dart' as log; import 'global_packages.dart'; 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> mainCommands = _initCommands(); /// The top-level [ArgParser] used to parse the pub command line. static final pubArgParser = _initArgParser(); /// Displays usage information for the app. static void printGlobalUsage() { // Build up a buffer so it shows up as a single log entry. var buffer = new StringBuffer(); buffer.writeln('Pub is a package manager for Dart.'); buffer.writeln(); buffer.writeln('Usage: pub <command> [arguments]'); buffer.writeln(); buffer.writeln('Global options:'); buffer.writeln(pubArgParser.getUsage()); buffer.writeln(); buffer.write(_listCommands(mainCommands)); buffer.writeln(); buffer.writeln( 'Run "pub help [command]" for more information about a command.'); buffer.writeln( 'See http://dartlang.org/tools/pub for detailed documentation.'); 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, _listCommands(commands)); } /// 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; var buffer = new StringBuffer(); buffer.writeln('Available ${isSubcommand ? "sub" : ""}commands:'); for (var name in names) { buffer.writeln(' ${padRight(name, length)} ' '${commands[name].description.split("\n").first}'); } return buffer.toString(); } SystemCache get cache => _cache; SystemCache _cache; GlobalPackages get globals => _globals; GlobalPackages _globals; /// The parsed options for the pub executable. ArgResults get globalOptions => _globalOptions; ArgResults _globalOptions; /// The parsed options for this command. ArgResults get commandOptions => _commandOptions; ArgResults _commandOptions; /// Gets the [Entrypoint] package for the current working directory. /// /// This will load the pubspec and fail with an error if the current directory /// is not a package. Entrypoint get entrypoint { // Lazy load it. if (_entrypoint == null) { _entrypoint = new Entrypoint(path.current, _cache, packageSymlinks: globalOptions['package-symlinks']); } return _entrypoint; } Entrypoint _entrypoint; /// A one-line description of this command. String get description; /// If the command is undocumented and should not appear in command listings, /// this will be `true`. 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; /// The URL for web documentation for this command. String get docUrl => null; /// 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; /// Override this and return `false` to disallow trailing options from being /// parsed after a non-option argument is parsed. bool get allowTrailingOptions => true; /// Alternate names for this command. /// /// These names won't be used in the documentation, but they will work when /// invoked on the command line. final aliases = const <String>[]; /// The [ArgParser] for this command. ArgParser get commandParser => _commandParser; ArgParser _commandParser; /// 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. This only needs to be set in leaf commands. bool get isOffline => false; PubCommand() { _commandParser = new ArgParser(allowTrailingOptions: allowTrailingOptions); // Allow "--help" after a command to get command help. commandParser.addFlag('help', abbr: 'h', negatable: false, help: 'Print usage information for this command.'); } /// Runs this command using a system cache at [cacheDir] with [globalOptions] /// and [options]. Future run(String cacheDir, ArgResults globalOptions, ArgResults options) { _globalOptions = globalOptions; _commandOptions = options; _cache = new SystemCache.withSources(cacheDir, isOffline: isOffline); _globals = new GlobalPackages(_cache); return new Future.sync(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); return null; } /// Displays usage information for this command. /// /// If [description] is omitted, defaults to the command's description. void printUsage([String description]) { if (description == null) description = this.description; log.message('$description\n\n${_getUsage()}'); } /// Throw a [UsageException] for a usage error of this command with /// [message]. void usageError(String message) { throw new UsageException(message, _getUsage()); } /// Parses a user-supplied integer [intString] named [name]. /// /// If the parsing fails, prints a usage message and exits. int parseInt(String intString, String name) { try { return int.parse(intString); } on FormatException catch (_) { usageError('Could not parse $name "$intString".'); } } /// Generates a string of usage information for this command. String _getUsage() { var buffer = new StringBuffer(); buffer.write('Usage: $usage'); var commandUsage = commandParser.getUsage(); if (!commandUsage.isEmpty) { buffer.writeln(); buffer.writeln(commandUsage); } if (subcommands.isNotEmpty) { buffer.writeln(); buffer.write(_listCommands(subcommands)); } buffer.writeln(); buffer.writeln('Run "pub help" to see global options.'); if (docUrl != null) { buffer.writeln("See $docUrl for detailed documentation."); } return buffer.toString(); } } _initCommands() { var commands = { 'build': new BuildCommand(), 'cache': new CacheCommand(), 'deps': new DepsCommand(), 'downgrade': new DowngradeCommand(), 'global': new GlobalCommand(), 'get': new GetCommand(), 'help': new HelpCommand(), 'list-package-dirs': new ListPackageDirsCommand(), 'publish': new LishCommand(), 'run': new RunCommand(), 'serve': new ServeCommand(), 'upgrade': new UpgradeCommand(), 'uploader': new UploaderCommand(), 'version': new VersionCommand() }; for (var command in commands.values.toList()) { for (var alias in command.aliases) { commands[alias] = command; } } return commands; } /// Creates the top-level [ArgParser] used to parse the pub command line. ArgParser _initArgParser() { var argParser = new ArgParser(allowTrailingOptions: true); // Add the global options. argParser.addFlag('help', abbr: 'h', negatable: false, help: 'Print this usage information.'); argParser.addFlag('version', negatable: false, help: 'Print pub version.'); argParser.addFlag('trace', help: 'Print debugging information when an error occurs.'); argParser.addOption('verbosity', help: 'Control output verbosity.', allowed: ['normal', 'io', 'solver', 'all'], allowedHelp: { 'normal': 'Show errors, warnings, and user messages.', 'io': 'Also show IO operations.', 'solver': 'Show steps during version resolution.', 'all': 'Show all output including internal tracing messages.' }); argParser.addFlag('verbose', abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".'); argParser.addFlag('with-prejudice', hide: !isAprilFools, negatable: false, help: 'Execute commands with prejudice.'); argParser.addFlag('package-symlinks', hide: true, negatable: true, defaultsTo: true); // Register the commands. 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); }); }