OLD | NEW |
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'; | |
8 import 'dart:math' as math; | |
9 | |
10 import 'package:args/args.dart'; | 7 import 'package:args/args.dart'; |
| 8 import 'package:args/command_runner.dart'; |
11 import 'package:path/path.dart' as path; | 9 import 'package:path/path.dart' as path; |
12 | 10 |
13 import 'command/build.dart'; | |
14 import 'command/cache.dart'; | |
15 import 'command/deps.dart'; | |
16 import 'command/downgrade.dart'; | |
17 import 'command/get.dart'; | |
18 import 'command/global.dart'; | |
19 import 'command/help.dart'; | |
20 import 'command/lish.dart'; | |
21 import 'command/list_package_dirs.dart'; | |
22 import 'command/run.dart'; | |
23 import 'command/serve.dart'; | |
24 import 'command/upgrade.dart'; | |
25 import 'command/uploader.dart'; | |
26 import 'command/version.dart'; | |
27 import 'entrypoint.dart'; | 11 import 'entrypoint.dart'; |
28 import 'exceptions.dart'; | |
29 import 'log.dart' as log; | 12 import 'log.dart' as log; |
30 import 'global_packages.dart'; | 13 import 'global_packages.dart'; |
31 import 'system_cache.dart'; | 14 import 'system_cache.dart'; |
32 import 'utils.dart'; | |
33 | 15 |
34 /// The base class for commands for the pub executable. | 16 /// The base class for commands for the pub executable. |
35 /// | 17 /// |
36 /// A command may either be a "leaf" command or it may be a parent for a set | 18 /// A command may either be a "leaf" command or it may be a parent for a set |
37 /// of subcommands. Only leaf commands are ever actually invoked. If a command | 19 /// of subcommands. Only leaf commands are ever actually invoked. If a command |
38 /// has subcommands, then one of those must always be chosen. | 20 /// has subcommands, then one of those must always be chosen. |
39 abstract class PubCommand { | 21 abstract class PubCommand extends Command { |
40 /// The commands that pub understands. | 22 SystemCache get cache { |
41 static final Map<String, PubCommand> mainCommands = _initCommands(); | 23 if (_cache == null) { |
42 | 24 _cache = new SystemCache.withSources(isOffline: isOffline); |
43 /// The top-level [ArgParser] used to parse the pub command line. | 25 } |
44 static final pubArgParser = _initArgParser(); | 26 return _cache; |
45 | |
46 /// Displays usage information for the app. | |
47 static void printGlobalUsage() { | |
48 // Build up a buffer so it shows up as a single log entry. | |
49 var buffer = new StringBuffer(); | |
50 buffer.writeln('Pub is a package manager for Dart.'); | |
51 buffer.writeln(); | |
52 buffer.writeln('Usage: pub <command> [arguments]'); | |
53 buffer.writeln(); | |
54 buffer.writeln('Global options:'); | |
55 buffer.writeln(pubArgParser.getUsage()); | |
56 buffer.writeln(); | |
57 buffer.write(_listCommands(mainCommands)); | |
58 buffer.writeln(); | |
59 buffer.writeln( | |
60 'Run "pub help [command]" for more information about a command.'); | |
61 buffer.writeln( | |
62 'See http://dartlang.org/tools/pub for detailed documentation.'); | |
63 | |
64 log.message(buffer); | |
65 } | 27 } |
66 | |
67 /// Fails with a usage error [message] when trying to select from one of | |
68 /// [commands]. | |
69 static void usageErrorWithCommands(Map<String, PubCommand> commands, | |
70 String message) { | |
71 throw new UsageException(message, _listCommands(commands)); | |
72 } | |
73 | |
74 /// Writes [commands] in a nicely formatted list to [buffer]. | |
75 static String _listCommands(Map<String, PubCommand> commands) { | |
76 // If there are no subcommands, do nothing. | |
77 if (commands.isEmpty) return ""; | |
78 | |
79 // Don't include aliases. | |
80 var names = commands.keys | |
81 .where((name) => !commands[name].aliases.contains(name)); | |
82 | |
83 // Filter out hidden ones, unless they are all hidden. | |
84 var visible = names.where((name) => !commands[name].hidden); | |
85 if (visible.isNotEmpty) names = visible; | |
86 | |
87 // Show the commands alphabetically. | |
88 names = ordered(names); | |
89 var length = names.map((name) => name.length).reduce(math.max); | |
90 var isSubcommand = commands != mainCommands; | |
91 | |
92 var buffer = new StringBuffer(); | |
93 buffer.writeln('Available ${isSubcommand ? "sub" : ""}commands:'); | |
94 for (var name in names) { | |
95 buffer.writeln(' ${padRight(name, length)} ' | |
96 '${commands[name].description.split("\n").first}'); | |
97 } | |
98 | |
99 return buffer.toString(); | |
100 } | |
101 | |
102 SystemCache get cache => _cache; | |
103 SystemCache _cache; | 28 SystemCache _cache; |
104 | 29 |
105 GlobalPackages get globals => _globals; | 30 GlobalPackages get globals { |
| 31 if (_globals == null) { |
| 32 _globals = new GlobalPackages(cache); |
| 33 } |
| 34 return _globals; |
| 35 } |
106 GlobalPackages _globals; | 36 GlobalPackages _globals; |
107 | 37 |
108 /// The parsed options for the pub executable. | |
109 ArgResults get globalOptions => _globalOptions; | |
110 ArgResults _globalOptions; | |
111 | |
112 /// The parsed options for this command. | |
113 ArgResults get commandOptions => _commandOptions; | |
114 ArgResults _commandOptions; | |
115 | |
116 /// Gets the [Entrypoint] package for the current working directory. | 38 /// Gets the [Entrypoint] package for the current working directory. |
117 /// | 39 /// |
118 /// This will load the pubspec and fail with an error if the current directory | 40 /// This will load the pubspec and fail with an error if the current directory |
119 /// is not a package. | 41 /// is not a package. |
120 Entrypoint get entrypoint { | 42 Entrypoint get entrypoint { |
121 // Lazy load it. | 43 // Lazy load it. |
122 if (_entrypoint == null) { | 44 if (_entrypoint == null) { |
123 _entrypoint = new Entrypoint(path.current, _cache, | 45 _entrypoint = new Entrypoint(path.current, cache, |
124 packageSymlinks: globalOptions['package-symlinks']); | 46 packageSymlinks: globalResults['package-symlinks']); |
125 } | 47 } |
126 return _entrypoint; | 48 return _entrypoint; |
127 } | 49 } |
128 | |
129 Entrypoint _entrypoint; | 50 Entrypoint _entrypoint; |
130 | 51 |
131 /// A one-line description of this command. | |
132 String get description; | |
133 | |
134 /// If the command is undocumented and should not appear in command listings, | |
135 /// this will be `true`. | |
136 bool get hidden { | |
137 // Leaf commands are visible by default. | |
138 if (subcommands.isEmpty) return false; | |
139 | |
140 // Otherwise, a command is hidden if all of its subcommands are. | |
141 return subcommands.values.every((subcommand) => subcommand.hidden); | |
142 } | |
143 | |
144 /// How to invoke this command (e.g. `"pub get [package]"`). | |
145 String get usage; | |
146 | |
147 /// The URL for web documentation for this command. | 52 /// The URL for web documentation for this command. |
148 String get docUrl => null; | 53 String get docUrl => null; |
149 | 54 |
150 /// Whether or not this command takes arguments in addition to options. | |
151 /// | |
152 /// If false, pub will exit with an error if arguments are provided. This | |
153 /// only needs to be set in leaf commands. | |
154 bool get takesArguments => false; | |
155 | |
156 /// Override this and return `false` to disallow trailing options from being | 55 /// Override this and return `false` to disallow trailing options from being |
157 /// parsed after a non-option argument is parsed. | 56 /// parsed after a non-option argument is parsed. |
158 bool get allowTrailingOptions => true; | 57 bool get allowTrailingOptions => true; |
159 | 58 |
160 /// Alternate names for this command. | 59 ArgParser get argParser { |
161 /// | 60 // Lazily initialize the parser because the superclass constructor requires |
162 /// These names won't be used in the documentation, but they will work when | 61 // it but we want to initialize it based on [allowTrailingOptions]. |
163 /// invoked on the command line. | 62 if (_argParser == null) { |
164 final aliases = const <String>[]; | 63 _argParser = new ArgParser(allowTrailingOptions: allowTrailingOptions); |
165 | 64 } |
166 /// The [ArgParser] for this command. | 65 return _argParser; |
167 ArgParser get commandParser => _commandParser; | 66 } |
168 ArgParser _commandParser; | 67 ArgParser _argParser; |
169 | |
170 /// Subcommands exposed by this command. | |
171 /// | |
172 /// If empty, then this command has no subcommands. Otherwise, a subcommand | |
173 /// must be specified by the user. In that case, this command's [onRun] will | |
174 /// not be called and the subcommand's will. | |
175 final subcommands = <String, PubCommand>{}; | |
176 | 68 |
177 /// Override this to use offline-only sources instead of hitting the network. | 69 /// Override this to use offline-only sources instead of hitting the network. |
178 /// | 70 /// |
179 /// This will only be called before the [SystemCache] is created. After that, | 71 /// This will only be called before the [SystemCache] is created. After that, |
180 /// it has no effect. This only needs to be set in leaf commands. | 72 /// it has no effect. This only needs to be set in leaf commands. |
181 bool get isOffline => false; | 73 bool get isOffline => false; |
182 | 74 |
183 PubCommand() { | 75 String get usageFooter { |
184 _commandParser = new ArgParser(allowTrailingOptions: allowTrailingOptions); | 76 if (docUrl == null) return null; |
185 | 77 return "See $docUrl for detailed documentation."; |
186 // Allow "--help" after a command to get command help. | |
187 commandParser.addFlag('help', abbr: 'h', negatable: false, | |
188 help: 'Print usage information for this command.'); | |
189 } | 78 } |
190 | 79 |
191 /// Runs this command using a system cache at [cacheDir] with [globalOptions] | 80 void printUsage() { |
192 /// and [options]. | 81 log.message(usage); |
193 Future run(String cacheDir, ArgResults globalOptions, ArgResults options) { | |
194 _globalOptions = globalOptions; | |
195 _commandOptions = options; | |
196 | |
197 _cache = new SystemCache.withSources(cacheDir, isOffline: isOffline); | |
198 _globals = new GlobalPackages(_cache); | |
199 | |
200 return new Future.sync(onRun); | |
201 } | |
202 | |
203 /// Override this to perform the specific command. | |
204 /// | |
205 /// Return a future that completes when the command is done or fails if the | |
206 /// command fails. If the command is synchronous, it may return `null`. Only | |
207 /// leaf command should override this. | |
208 Future onRun() { | |
209 // Leaf commands should override this and non-leaf commands should never | |
210 // call it. | |
211 assert(false); | |
212 return null; | |
213 } | |
214 | |
215 /// Displays usage information for this command. | |
216 /// | |
217 /// If [description] is omitted, defaults to the command's description. | |
218 void printUsage([String description]) { | |
219 if (description == null) description = this.description; | |
220 log.message('$description\n\n${_getUsage()}'); | |
221 } | |
222 | |
223 /// Throw a [UsageException] for a usage error of this command with | |
224 /// [message]. | |
225 void usageError(String message) { | |
226 throw new UsageException(message, _getUsage()); | |
227 } | 82 } |
228 | 83 |
229 /// Parses a user-supplied integer [intString] named [name]. | 84 /// Parses a user-supplied integer [intString] named [name]. |
230 /// | 85 /// |
231 /// If the parsing fails, prints a usage message and exits. | 86 /// If the parsing fails, prints a usage message and exits. |
232 int parseInt(String intString, String name) { | 87 int parseInt(String intString, String name) { |
233 try { | 88 try { |
234 return int.parse(intString); | 89 return int.parse(intString); |
235 } on FormatException catch (_) { | 90 } on FormatException catch (_) { |
236 usageError('Could not parse $name "$intString".'); | 91 usageException('Could not parse $name "$intString".'); |
237 } | 92 } |
238 } | 93 } |
239 | |
240 /// Generates a string of usage information for this command. | |
241 String _getUsage() { | |
242 var buffer = new StringBuffer(); | |
243 buffer.write('Usage: $usage'); | |
244 | |
245 var commandUsage = commandParser.getUsage(); | |
246 if (!commandUsage.isEmpty) { | |
247 buffer.writeln(); | |
248 buffer.writeln(commandUsage); | |
249 } | |
250 | |
251 if (subcommands.isNotEmpty) { | |
252 buffer.writeln(); | |
253 buffer.write(_listCommands(subcommands)); | |
254 } | |
255 | |
256 buffer.writeln(); | |
257 buffer.writeln('Run "pub help" to see global options.'); | |
258 if (docUrl != null) { | |
259 buffer.writeln("See $docUrl for detailed documentation."); | |
260 } | |
261 | |
262 return buffer.toString(); | |
263 } | |
264 } | 94 } |
265 | |
266 _initCommands() { | |
267 var commands = { | |
268 'build': new BuildCommand(), | |
269 'cache': new CacheCommand(), | |
270 'deps': new DepsCommand(), | |
271 'downgrade': new DowngradeCommand(), | |
272 'global': new GlobalCommand(), | |
273 'get': new GetCommand(), | |
274 'help': new HelpCommand(), | |
275 'list-package-dirs': new ListPackageDirsCommand(), | |
276 'publish': new LishCommand(), | |
277 'run': new RunCommand(), | |
278 'serve': new ServeCommand(), | |
279 'upgrade': new UpgradeCommand(), | |
280 'uploader': new UploaderCommand(), | |
281 'version': new VersionCommand() | |
282 }; | |
283 | |
284 for (var command in commands.values.toList()) { | |
285 for (var alias in command.aliases) { | |
286 commands[alias] = command; | |
287 } | |
288 } | |
289 | |
290 return commands; | |
291 } | |
292 | |
293 /// Creates the top-level [ArgParser] used to parse the pub command line. | |
294 ArgParser _initArgParser() { | |
295 var argParser = new ArgParser(allowTrailingOptions: true); | |
296 | |
297 // Add the global options. | |
298 argParser.addFlag('help', abbr: 'h', negatable: false, | |
299 help: 'Print this usage information.'); | |
300 argParser.addFlag('version', negatable: false, | |
301 help: 'Print pub version.'); | |
302 argParser.addFlag('trace', | |
303 help: 'Print debugging information when an error occurs.'); | |
304 argParser.addOption('verbosity', | |
305 help: 'Control output verbosity.', | |
306 allowed: ['normal', 'io', 'solver', 'all'], | |
307 allowedHelp: { | |
308 'normal': 'Show errors, warnings, and user messages.', | |
309 'io': 'Also show IO operations.', | |
310 'solver': 'Show steps during version resolution.', | |
311 'all': 'Show all output including internal tracing messages.' | |
312 }); | |
313 argParser.addFlag('verbose', abbr: 'v', negatable: false, | |
314 help: 'Shortcut for "--verbosity=all".'); | |
315 argParser.addFlag('with-prejudice', hide: !isAprilFools, negatable: false, | |
316 help: 'Execute commands with prejudice.'); | |
317 argParser.addFlag('package-symlinks', hide: true, negatable: true, | |
318 defaultsTo: true); | |
319 | |
320 // Register the commands. | |
321 PubCommand.mainCommands.forEach((name, command) { | |
322 _registerCommand(name, command, argParser); | |
323 }); | |
324 | |
325 return argParser; | |
326 } | |
327 | |
328 /// Registers a [command] with [name] on [parser]. | |
329 void _registerCommand(String name, PubCommand command, ArgParser parser) { | |
330 parser.addCommand(name, command.commandParser); | |
331 | |
332 // Recursively wire up any subcommands. | |
333 command.subcommands.forEach((name, subcommand) { | |
334 _registerCommand(name, subcommand, command.commandParser); | |
335 }); | |
336 } | |
OLD | NEW |