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

Side by Side Diff: sdk/lib/_internal/pub/lib/src/command.dart

Issue 138723005: Support subcommands in pub and pub help. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Revise. Created 6 years, 10 months 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library pub.command; 5 library pub.command;
6 6
7 import 'dart:async'; 7 import 'dart:async';
8 import 'dart:io';
9 import 'dart:math' as math; 8 import 'dart:math' as math;
10 9
11 import 'package:args/args.dart'; 10 import 'package:args/args.dart';
12 import 'package:path/path.dart' as path; 11 import 'package:path/path.dart' as path;
13 import 'package:stack_trace/stack_trace.dart';
14 12
15 import 'command/build.dart'; 13 import 'command/build.dart';
16 import 'command/cache.dart'; 14 import 'command/cache.dart';
17 import 'command/get.dart'; 15 import 'command/get.dart';
18 import 'command/help.dart'; 16 import 'command/help.dart';
19 import 'command/lish.dart'; 17 import 'command/lish.dart';
20 import 'command/list_package_dirs.dart'; 18 import 'command/list_package_dirs.dart';
21 import 'command/serve.dart'; 19 import 'command/serve.dart';
22 import 'command/upgrade.dart'; 20 import 'command/upgrade.dart';
23 import 'command/uploader.dart'; 21 import 'command/uploader.dart';
24 import 'command/version.dart'; 22 import 'command/version.dart';
25 import 'entrypoint.dart'; 23 import 'entrypoint.dart';
26 import 'exit_codes.dart' as exit_codes;
27 import 'http.dart';
28 import 'io.dart';
29 import 'log.dart' as log; 24 import 'log.dart' as log;
30 import 'system_cache.dart'; 25 import 'system_cache.dart';
31 import 'utils.dart'; 26 import 'utils.dart';
32 27
33 /// The base class for commands for the pub executable. 28 /// The base class for commands for the pub executable.
29 ///
30 /// A command may either be a "leaf" command or it may be a parent for a set
31 /// of subcommands. Only leaf commands are ever actually invoked. If a command
32 /// has subcommands, then one of those must always be chosen.
34 abstract class PubCommand { 33 abstract class PubCommand {
35 /// The commands that pub understands. 34 /// The commands that pub understands.
36 static final Map<String, PubCommand> commands = _initCommands(); 35 static final Map<String, PubCommand> mainCommands = _initCommands();
37 36
38 /// The top-level [ArgParser] used to parse the pub command line. 37 /// The top-level [ArgParser] used to parse the pub command line.
39 static final pubArgParser = _initArgParser(); 38 static final pubArgParser = _initArgParser();
40 39
41 /// Displays usage information for the app. 40 /// Displays usage information for the app.
42 static void printGlobalUsage() { 41 static void printGlobalUsage() {
43 // Build up a buffer so it shows up as a single log entry. 42 // Build up a buffer so it shows up as a single log entry.
44 var buffer = new StringBuffer(); 43 var buffer = new StringBuffer();
45 buffer.writeln('Pub is a package manager for Dart.'); 44 buffer.writeln('Pub is a package manager for Dart.');
46 buffer.writeln(); 45 buffer.writeln();
47 buffer.writeln('Usage: pub command [arguments]'); 46 buffer.writeln('Usage: pub <command> [arguments]');
48 buffer.writeln(); 47 buffer.writeln();
49 buffer.writeln('Global options:'); 48 buffer.writeln('Global options:');
50 buffer.writeln(pubArgParser.getUsage()); 49 buffer.writeln(pubArgParser.getUsage());
50 buffer.write(_listCommands(mainCommands));
51 buffer.writeln(); 51 buffer.writeln();
52 buffer.writeln(
53 'Use "pub help [command]" for more information about a command.');
52 54
55 log.message(buffer);
56 }
57
58 /// Fails with a usage error [message] when trying to select from one of
59 /// [commands].
60 static void commandUsageError(Map<String, PubCommand> commands,
nweiz 2014/02/04 01:24:29 "PubCommand.commandUsageError" reads weird, both b
Bob Nystrom 2014/02/06 00:06:31 Done.
61 String message) {
62 throw new UsageException("$message\n${_listCommands(commands)}");
63 }
64
65 /// Writes [commands] in a nicely formatted list to [buffer].
66 ///
67 /// If [isSubcommand] is `true`, then the list will be labelled as
68 /// "subcommands", otherwise they are labelled "commands".
nweiz 2014/02/04 01:24:29 [isSubcommand] isn't passed anymore.
Bob Nystrom 2014/02/06 00:06:31 Done.
69 static String _listCommands(Map<String, PubCommand> commands) {
53 // Show the public commands alphabetically. 70 // Show the public commands alphabetically.
54 var names = ordered(commands.keys.where((name) => 71 var names = ordered(commands.keys.where((name) =>
55 !commands[name].aliases.contains(name) && 72 !commands[name].aliases.contains(name) &&
56 !commands[name].hidden)); 73 !commands[name].hidden));
57 74
75 // If all of the subcommands are hidden, do nothing.
76 // TODO(rnystrom): Remove this once cache has a visible command.
77 if (names.isEmpty) return "";
78
58 var length = names.map((name) => name.length).reduce(math.max); 79 var length = names.map((name) => name.length).reduce(math.max);
59 80
60 buffer.writeln('Available commands:'); 81 var isSubcommand = commands != mainCommands;
82
83 var buffer = new StringBuffer();
84 buffer.writeln();
85 buffer.writeln('Available ${isSubcommand ? "sub" : ""}commands:');
61 for (var name in names) { 86 for (var name in names) {
62 buffer.writeln(' ${padRight(name, length)} ' 87 buffer.writeln(' ${padRight(name, length)} '
63 '${commands[name].description}'); 88 '${commands[name].description}');
64 } 89 }
65 90
66 buffer.writeln(); 91 return buffer.toString();
67 buffer.write(
68 'Use "pub help [command]" for more information about a command.');
69 log.message(buffer.toString());
70 } 92 }
71 93
72 SystemCache cache; 94 SystemCache cache;
73 95
74 /// The parsed options for this command. 96 /// The parsed options for this command.
75 ArgResults commandOptions; 97 ArgResults get commandOptions => _commandOptions;
98 ArgResults _commandOptions;
76 99
77 Entrypoint entrypoint; 100 Entrypoint entrypoint;
78 101
79 /// A one-line description of this command. 102 /// A one-line description of this command.
80 String get description; 103 String get description;
81 104
82 /// If the command is undocumented and should not appear in command listings, 105 /// If the command is undocumented and should not appear in command listings,
83 /// this will be `true`. 106 /// this will be `true`.
84 bool get hidden => false; 107 bool get hidden => false;
85 108
86 /// How to invoke this command (e.g. `"pub get [package]"`). 109 /// How to invoke this command (e.g. `"pub get [package]"`).
87 String get usage; 110 String get usage;
88 111
89 /// Whether or not this command requires [entrypoint] to be defined. If false, 112 /// Whether or not this command requires [entrypoint] to be defined.
90 /// pub won't look for a pubspec and [entrypoint] will be null when the 113 ///
91 /// command runs. 114 /// If false, pub won't look for a pubspec and [entrypoint] will be null when
115 /// the command runs. This only needs to be set in leaf commands.
92 bool get requiresEntrypoint => true; 116 bool get requiresEntrypoint => true;
93 117
94 /// Whether or not this command takes arguments in addition to options. If 118 /// Whether or not this command takes arguments in addition to options.
95 /// false, pub will exit with an error if arguments are provided. 119 ///
120 /// If false, pub will exit with an error if arguments are provided. This
121 /// only needs to be set in leaf commands.
96 bool get takesArguments => false; 122 bool get takesArguments => false;
97 123
98 /// Alternate names for this command. These names won't be used in the 124 /// Alternate names for this command. These names won't be used in the
99 /// documentation, but they will work when invoked on the command line. 125 /// documentation, but they will work when invoked on the command line.
100 final aliases = const <String>[]; 126 final aliases = const <String>[];
101 127
102 /// The [ArgParser] for this command. 128 /// The [ArgParser] for this command.
103 final commandParser = new ArgParser(); 129 final commandParser = new ArgParser();
104 130
131 /// Subcommands exposed by this command.
132 ///
133 /// If empty, then this command has no subcommands. Otherwise, a subcommand
134 /// must be specified by the user. In that case, this command's [onRun] will
135 /// not be called and the subcommand's will.
136 final subcommands = <String, PubCommand>{};
137
105 /// Override this to use offline-only sources instead of hitting the network. 138 /// Override this to use offline-only sources instead of hitting the network.
139 ///
106 /// This will only be called before the [SystemCache] is created. After that, 140 /// This will only be called before the [SystemCache] is created. After that,
107 /// it has no effect. 141 /// it has no effect. This only needs to be set in leaf commands.
108 bool get isOffline => false; 142 bool get isOffline => false;
109 143
110 PubCommand() { 144 PubCommand() {
111 // Allow "--help" after a command to get command help. 145 // Allow "--help" after a command to get command help.
112 commandParser.addFlag('help', abbr: 'h', negatable: false, 146 commandParser.addFlag('help', abbr: 'h', negatable: false,
113 help: 'Print usage information for this command.'); 147 help: 'Print usage information for this command.');
114 } 148 }
115 149
116 void run(String cacheDir, ArgResults options, List<String> arguments) { 150 /// Runs this command using a system cache at [cacheDir] with [options].
117 commandOptions = options.command; 151 Future run(String cacheDir, ArgResults options) {
nweiz 2014/02/04 01:24:29 What happened to mainOptions/rootOptions? Subcomma
Bob Nystrom 2014/02/06 00:06:31 I took it out because it wasn't being used. We pro
118 152 _commandOptions = options;
119 if (commandOptions['help']) {
120 this.printUsage();
121 return;
122 }
123 153
124 cache = new SystemCache.withSources(cacheDir, isOffline: isOffline); 154 cache = new SystemCache.withSources(cacheDir, isOffline: isOffline);
125 155
126 handleError(error, Chain chain) { 156 if (requiresEntrypoint) {
127 // This is basically the top-level exception handler so that we don't 157 // TODO(rnystrom): Will eventually need better logic to walk up
128 // spew a stack trace on our users. 158 // subdirectories until we hit one that looks package-like. For now,
129 var message; 159 // just assume the cwd is it.
130 160 entrypoint = new Entrypoint(path.current, cache);
131 log.error(getErrorMessage(error));
132 log.fine("Exception type: ${error.runtimeType}");
133
134 if (options['trace'] || !isUserFacingException(error)) {
135 log.error(chain.terse);
136 } else {
137 log.fine(chain.terse);
138 }
139
140 if (error is ApplicationException && error.innerError != null) {
141 var message = "Wrapped exception: ${error.innerError}";
142 if (error.innerTrace != null) message = "$message\n${error.innerTrace}";
143 log.fine(message);
144 }
145
146 if (options['trace']) {
147 log.dumpTranscript();
148 } else if (!isUserFacingException(error)) {
149 log.error("""
150 This is an unexpected error. Please run
151
152 pub --trace ${arguments.map((arg) => "'$arg'").join(' ')}
153
154 and include the results in a bug report on http://dartbug.com/new.
155 """);
156 }
157
158 return flushThenExit(_chooseExitCode(error));
159 } 161 }
160 162
161 var captureStackChains = 163 var commandFuture = onRun();
162 options['trace'] || options['verbose'] || options['verbosity'] == 'all'; 164 if (commandFuture == null) return null;
nweiz 2014/02/04 01:24:29 Why not just "return syncFuture(onRun)"?
Bob Nystrom 2014/02/06 00:06:31 Done, though that behavior is a bit different. But
163 captureErrors(() {
164 return syncFuture(() {
165 // Make sure there aren't unexpected arguments.
166 if (!takesArguments && commandOptions.rest.isNotEmpty) {
167 log.error('Command "${commandOptions.name}" does not take any '
168 'arguments.');
169 this.printUsage();
170 return flushThenExit(exit_codes.USAGE);
171 }
172 165
173 if (requiresEntrypoint) { 166 return commandFuture;
174 // TODO(rnystrom): Will eventually need better logic to walk up
175 // subdirectories until we hit one that looks package-like. For now,
176 // just assume the cwd is it.
177 entrypoint = new Entrypoint(path.current, cache);
178 }
179
180 var commandFuture = onRun();
181 if (commandFuture == null) return true;
182
183 return commandFuture;
184 }).whenComplete(() => cache.deleteTempDir());
185 }, captureStackChains: captureStackChains).catchError(handleError)
186 .then((_) {
187 // Explicitly exit on success to ensure that any dangling dart:io handles
188 // don't cause the process to never terminate.
189 return flushThenExit(exit_codes.SUCCESS);
190 });
191 } 167 }
192 168
193 /// Override this to perform the specific command. Return a future that 169 /// Override this to perform the specific command.
194 /// completes when the command is done or fails if the command fails. If the 170 ///
195 /// command is synchronous, it may return `null`. 171 /// Return a future that completes when the command is done or fails if the
196 Future onRun(); 172 /// command fails. If the command is synchronous, it may return `null`. Only
173 /// lead command should override this.
nweiz 2014/02/04 01:24:29 "lead" -> "leaf".
Bob Nystrom 2014/02/06 00:06:31 Done.
174 Future onRun() {
175 // Leaf commands should override this and non-leaf commands should never
176 // call it.
177 assert(false);
178 }
197 179
198 /// Displays usage information for this command. 180 /// Displays usage information for this command.
199 void printUsage([String description]) { 181 void printUsage([String description]) {
200 if (description == null) description = this.description; 182 if (description == null) description = this.description;
183 log.message('$description\n\n${_getUsage()}');
184 }
201 185
186 /// Generates a string of usage information for this command.
187 String _getUsage() {
202 var buffer = new StringBuffer(); 188 var buffer = new StringBuffer();
203 buffer.write('$description\n\nUsage: $usage'); 189 buffer.write('Usage: $usage');
204 190
205 var commandUsage = commandParser.getUsage(); 191 var commandUsage = commandParser.getUsage();
206 if (!commandUsage.isEmpty) { 192 if (!commandUsage.isEmpty) {
207 buffer.write('\n'); 193 buffer.write('\n');
208 buffer.write(commandUsage); 194 buffer.write(commandUsage);
209 } 195 }
210 196
211 log.message(buffer.toString()); 197 if (subcommands.isNotEmpty) {
198 buffer.writeln();
199 buffer.write(_listCommands(subcommands));
200 }
201
202 return buffer.toString();
212 } 203 }
nweiz 2014/02/04 01:24:29 Nit: it reads weird to me to have this defined bet
Bob Nystrom 2014/02/06 00:06:31 Done.
213 204
214 /// Returns the appropriate exit code for [exception], falling back on 1 if no 205 // TODO(rnystrom): Use this in other places handle usage failures.
215 /// appropriate exit code could be found. 206 /// Throw an [ApplicationException] for a usage error of this command with
216 int _chooseExitCode(exception) { 207 /// [message].
217 if (exception is HttpException || exception is HttpException || 208 void usageError(String message) {
218 exception is SocketException || exception is PubHttpException) { 209 throw new UsageException("$message\n\n${_getUsage()}");
219 return exit_codes.UNAVAILABLE;
220 } else if (exception is FormatException) {
221 return exit_codes.DATA;
222 } else {
223 return 1;
224 }
225 } 210 }
226 } 211 }
227 212
228 _initCommands() { 213 _initCommands() {
229 var commands = { 214 var commands = {
230 'build': new BuildCommand(), 215 'build': new BuildCommand(),
231 'cache': new CacheCommand(), 216 'cache': new CacheCommand(),
232 'get': new GetCommand(), 217 'get': new GetCommand(),
233 'help': new HelpCommand(), 218 'help': new HelpCommand(),
234 'list-package-dirs': new ListPackageDirsCommand(), 219 'list-package-dirs': new ListPackageDirsCommand(),
(...skipping 30 matching lines...) Expand all
265 allowedHelp: { 250 allowedHelp: {
266 'normal': 'Show errors, warnings, and user messages.', 251 'normal': 'Show errors, warnings, and user messages.',
267 'io': 'Also show IO operations.', 252 'io': 'Also show IO operations.',
268 'solver': 'Show steps during version resolution.', 253 'solver': 'Show steps during version resolution.',
269 'all': 'Show all output including internal tracing messages.' 254 'all': 'Show all output including internal tracing messages.'
270 }); 255 });
271 argParser.addFlag('verbose', abbr: 'v', negatable: false, 256 argParser.addFlag('verbose', abbr: 'v', negatable: false,
272 help: 'Shortcut for "--verbosity=all".'); 257 help: 'Shortcut for "--verbosity=all".');
273 258
274 // Register the commands. 259 // Register the commands.
275 PubCommand.commands.forEach((name, command) { 260 PubCommand.mainCommands.forEach((name, command) {
276 argParser.addCommand(name, command.commandParser); 261 _registerCommand(name, command, argParser);
277 }); 262 });
278 263
279 return argParser; 264 return argParser;
280 } 265 }
266
267 /// Registers a [command] with [name] on [parser].
268 void _registerCommand(String name, PubCommand command, ArgParser parser) {
269 parser.addCommand(name, command.commandParser);
270
271 // Recursively wire up any subcommands.
272 command.subcommands.forEach((name, subcommand) {
273 _registerCommand(name, subcommand, command.commandParser);
274 });
275 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698