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

Side by Side Diff: pkg/args/lib/src/command_runner.dart

Issue 797473002: Add a CommandRunner class for dispatching commands to args. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years 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
(Empty)
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
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.
4
5 library args.command_runner;
6
7 import 'dart:async';
8 import 'dart:collection';
9 import 'dart:math' as math;
10
11 import 'arg_parser.dart';
12 import 'arg_results.dart';
13 import 'help_command.dart';
14 import 'usage_exception.dart';
15 import 'utils.dart';
16
17 /// A class for invoking [Commands] based on raw command-line arguments.
18 class CommandRunner {
19 /// The name of the executable being run.
20 ///
21 /// Used for error reporting.
22 final String executableName;
23
24 /// A short description of this executable.
25 final String description;
26
27 /// A single-line template for how to invoke this executable.
28 ///
29 /// Defaults to "$executableName [command] <arguments>". Subclasses can
30 /// override this for a more specific template.
31 String get usageTemplate => "$executableName [command] <arguments>";
32
33 /// Generates a string displaying usage information for the executable.
34 ///
35 /// This includes usage for the global arguments as well as a list of
36 /// top-level commands.
37 String get usage {
38 return '''
39 $description
40
41 Usage: $usageTemplate
42
43 Global options:
44 ${topLevelArgParser.usage}
45
46 ${_getCommandUsage(_topLevelCommands)}
47
48 Run "$executableName help [command]" for more information about a command.''';
49 }
50
51 /// Returns [usage] with [description] removed from the beginning.
52 String get _usageWithoutDescription {
53 // Base this on the return value of [usage] so that subclasses can override
54 // usage and have their changes reflected here.
55 return usage.replaceFirst("$description\n\n", "");
Bob Nystrom 2014/12/11 20:25:30 This feels super sketchy to me. What's the use cas
nweiz 2014/12/11 23:55:24 Usually extra bottom-matter. Pub does it to add li
Bob Nystrom 2014/12/12 18:13:22 That was my guess. How about just making a "footer
nweiz 2014/12/16 02:07:44 Done.
56 }
57
58 /// An unmodifiable view of all top-level commands defined for this runner.
59 Map<String, Command> get topLevelCommands =>
Bob Nystrom 2014/12/11 20:25:30 I think `commands` is clear enough here. I don't t
nweiz 2014/12/11 23:55:24 Done.
60 new UnmodifiableMapView(_topLevelCommands);
61 final _topLevelCommands = new Map<String, Command>();
62
63 /// The top-level argument parser.
64 ///
65 /// Global options should be registered with this parser; they'll end up
66 /// available via [Command.globalOptions]. Commands should be registered with
67 /// [addCommand] rather than directly on the parser.
68 final topLevelArgParser = new ArgParser();
Bob Nystrom 2014/12/11 20:25:30 Can this just be `argParser`? The fact that it's o
nweiz 2014/12/11 23:55:24 Done.
69
70 CommandRunner(this.executableName, this.description) {
Sean Eagan 2014/12/12 17:50:31 The executable name should rarely if ever need to
nweiz 2014/12/16 02:07:44 args doesn't currently use dart:io. Adding an impo
71 topLevelArgParser.addFlag('help', abbr: 'h', negatable: false,
72 help: 'Print this usage information.');
73 addCommand(new HelpCommand());
74 }
75
76 /// Prints the usage information for this runner.
77 ///
78 /// This is called internally by [run] and can be overridden by subclasses.
Bob Nystrom 2014/12/11 20:25:30 "...to control how output is displayed" ?
nweiz 2014/12/11 23:55:24 Done.
79 void printUsage() => print(usage);
80
81 /// Throws a [UsageException] with [message].
82 void usageError(String message) =>
83 throw new UsageException(message, _usageWithoutDescription);
84
85 /// Adds [Command] as a top-level command to this runner.
86 void addCommand(Command command) {
87 var names = [command.name]..addAll(command.aliases);
88 for (var name in names) {
89 _topLevelCommands[name] = command;
90 topLevelArgParser.addCommand(name, command.argParser);
Bob Nystrom 2014/12/11 20:25:30 Can we unify the parser's concept of commands with
nweiz 2014/12/11 23:55:24 Moved to a separate library.
91 }
92 command._runner = this;
93 }
94
95 /// Parses [args] and invokes [Command.run] on the chosen command.
96 ///
97 /// This always returns a [Future] in case the command is asynchronous. The
98 /// [Future] will throw a [UsageError] if [args] was invalid.
99 Future run(Iterable<String> args) =>
100 new Future.sync(() => runCommand(parse(args)));
101
102 /// Parses [args] and returns the result, converting a [FormatException] to a
103 /// [UsageException].
104 ///
105 /// This is notionally a protected method. It may be overridden or called from
106 /// subclasses, but it shouldn't be called externally.
107 ArgResults parse(Iterable<String> args) {
108 try {
109 // TODO(nweiz): if arg parsing fails for a command, print that command's
110 // usage, not the global usage.
111 return topLevelArgParser.parse(args);
112 } on FormatException catch (error) {
Sean Eagan 2014/12/12 17:50:31 Why not just fix ArgParser.parse to throw UsageErr
nweiz 2014/12/16 02:07:44 I'm not a fan of that for a few reasons: * It wou
113 usageError(error.message);
114 }
115 }
116
117 /// Runs the command specified by [topLevelOptions].
118 ///
119 /// This is notionally a protected method. It may be overridden or called from
120 /// subclasses, but it shouldn't be called externally.
Bob Nystrom 2014/12/11 20:25:30 Can you give some details about why a user may wis
nweiz 2014/12/11 23:55:24 Done.
121 Future runCommand(ArgResults topLevelOptions) {
122 return new Future.sync(() {
123 var options = topLevelOptions;
124 var commands = _topLevelCommands;
125 var command;
126 var commandString = executableName;
127
128 while (commands.isNotEmpty) {
129 if (options.command == null) {
130 if (options.rest.isEmpty) {
131 if (command == null) {
132 // No top-level command was chosen.
133 printUsage();
134 return new Future.value();
135 }
136
137 command.usageError('Missing subcommand for "$commandString".');
138 } else {
139 if (command == null) {
140 usageError(
141 'Could not find a command named "${options.rest[0]}".');
142 }
143
144 command.usageError('Could not find a subcommand named '
145 '"${options.rest[0]}" for "$commandString".');
146 }
147 }
148
149 // Step into the command.
150 var parent = command;
151 options = options.command;
152 command = commands[options.name]
153 .._globalOptions = topLevelOptions
154 .._options = options;
Bob Nystrom 2014/12/11 20:25:30 Oh man, cascaded setters make my skin crawl.
nweiz 2014/12/11 23:55:24 Done.
155 commands = command._subcommands;
156 commandString += " ${options.name}";
157
158 if (options['help']) {
159 command.printUsage();
160 return new Future.value();
161 }
162 }
163
164 // Make sure there aren't unexpected arguments.
165 if (!command.takesArguments && options.rest.isNotEmpty) {
166 command.usageError(
167 'Command "${options.name}" does not take any arguments.');
168 }
169
170 return command.run();
171 });
172 }
173 }
174
175 /// A single command.
176 ///
177 /// A command is known as a "leaf command" if it has no subcommands and is meant
178 /// to be run. Leaf commands must override [run].
179 ///
180 /// A command with subcommands is known as a "branch command" and cannot be run
181 /// itself. It should call [addSubcommand] (often from the constructor) to
182 /// register subcommands.
183 abstract class Command {
184 /// The name of this command.
185 String get name;
186
187 /// A short description of this command.
188 String get description;
189
190 /// A single-line template for how to invoke this command (e.g. `"pub get
191 /// [package]"`).
192 String get usageTemplate {
193 var parents = [name];
194 for (var command = parent; command != null; command = command.parent) {
195 parents.add(command.name);
196 }
197 parents.add(runner.executableName);
198
199 var invocation = parents.reversed.join(" ");
200 return _subcommands.isNotEmpty
201 ? "$invocation [subcommand] <arguments>"
202 : "$invocation <arguments>";
Bob Nystrom 2014/12/11 20:25:30 Nit, but operators should be trailing not leading.
nweiz 2014/12/11 23:55:24 Done.
203 }
204
205 /// The command's parent command, if this is a subcommand.
206 ///
207 /// This will be `null` until [Command.addSubcommmand] has been called with
208 /// this command.
209 Command get parent => _parent;
210 Command _parent;
211
212 /// The command runner for this command.
213 ///
214 /// This will be `null` until [CommandRunner.addCommand] has been called with
215 /// this command or one of its parents.
216 CommandRunner get runner {
217 if (parent == null) return _runner;
218 return parent.runner;
219 }
220 CommandRunner _runner;
221
222 /// The parsed global options.
223 ///
224 /// This will be `null` until just before [Command.run] is called.
225 ArgResults get globalOptions => _globalOptions;
226 ArgResults _globalOptions;
227
228 /// The parsed options for this command.
229 ///
230 /// This will be `null` until just before [Command.run] is called.
231 ArgResults get options => _options;
232 ArgResults _options;
233
234 /// The argument parser for this command.
235 ///
236 /// Options for this command should be registered with this parser (often in
237 /// the constructor); they'll end up available via [options]. Subcommands
238 /// should be registered with [addSubcommand] rather than directly on the
239 /// parser.
240 final argParser = new ArgParser();
241
242 /// Generates a string displaying usage information for this command.
243 ///
244 /// This includes usage for the command's arguments as well as a list of
245 /// subcommands, if there are any.
246 String get usage {
247 var buffer = new StringBuffer()
248 ..writeln(description)
249 ..writeln()
250 ..writeln('Usage: $usageTemplate')
251 ..writeln(argParser.usage);
252
253 if (_subcommands.isNotEmpty) {
254 buffer.writeln();
255 buffer.writeln(_getCommandUsage(_subcommands, isSubcommand: true));
256 }
257
258 buffer.writeln();
259 buffer.write('Run "${runner.executableName} help" to see global options.');
260
261 return buffer.toString();
262 }
263
264 /// Returns [usage] with [description] removed from the beginning.
265 String get _usageWithoutDescription {
266 // Base this on the return value of [usage] so that subclasses can override
267 // usage and have their changes reflected here.
268 return usage.replaceFirst("$description\n\n", "");
269 }
270
271 /// An unmodifiable view of all sublevel commands of this command.
272 Map<String, Command> get subcommands =>
273 new UnmodifiableMapView(_subcommands);
274 final _subcommands = new Map<String, Command>();
275
276 /// Whether or not this command should appear in help listings.
277 ///
278 /// This is intended to be overridden by commands that want to mark themselves
279 /// hidden.
280 ///
281 /// By default, this is always true for leaf commands. It's true for branch
282 /// commands as long as any of their leaf commands are visible.
283 bool get hidden {
284 // Leaf commands are visible by default.
285 if (_subcommands.isEmpty) return false;
286
287 // Otherwise, a command is hidden if all of its subcommands are.
288 return _subcommands.values.every((subcommand) => subcommand.hidden);
289 }
290
291 /// Whether or not this command takes positional arguments in addition to
292 /// options.
293 ///
294 /// If false, [CommandRunner.run] will throw a [UsageException] if arguments
295 /// are provided. Defaults to true.
296 ///
297 /// This is intended to be overridden by commands that don't want to receive
298 /// arguments. It has no effect for branch commands.
299 final takesArguments = true;
Sean Eagan 2014/12/12 17:50:31 This is too coarse grained. Unscripted allows add
nweiz 2014/12/16 02:07:44 That would be a much broader change largely in Arg
300
301 /// Alternate names for this command.
302 ///
303 /// These names won't be used in the documentation, but they will work when
304 /// invoked on the command line.
305 ///
306 /// This is intended to be overridden.
307 final aliases = const <String>[];
Sean Eagan 2014/12/12 17:50:31 If adding this, it ought to be exposed on ArgParse
nweiz 2014/12/16 02:07:44 I'm open to adding this to ArgParser.addCommand, b
308
309 Command() {
310 argParser.addFlag('help', abbr: 'h', negatable: false,
311 help: 'Print this usage information.');
312 }
313
314 /// Runs this command.
315 ///
316 /// If this returns a [Future], [CommandRunner.run] won't complete until the
317 /// returned [Future] does. Otherwise, the return value is ignored.
318 run() {
319 throw new UnimplementedError("Leaf command $this must implement run().");
320 }
321
322 /// Adds [Command] as a subcommand of this.
323 void addSubcommand(Command command) {
324 var names = [command.name]..addAll(command.aliases);
325 for (var name in names) {
326 _subcommands[name] = command;
327 argParser.addCommand(name, command.argParser);
328 }
329 command._parent = this;
330 }
331
332 /// Prints the usage information for this command.
333 ///
334 /// This is called internally by [run] and can be overridden by subclasses.
335 void printUsage() => print(usage);
336
337 /// Throws a [UsageException] with [message].
338 void usageError(String message) =>
339 throw new UsageException(message, _usageWithoutDescription);
Bob Nystrom 2014/12/11 20:25:30 These methods are pretty similar to ones in Comman
nweiz 2014/12/11 23:55:24 I think that muddies the semantic distinction too
Bob Nystrom 2014/12/12 18:13:22 You think so? It has some extra functionality, but
nweiz 2014/12/16 02:07:44 I don't think behavioral overlap alone is enough t
Bob Nystrom 2014/12/16 16:50:41 I do feel there is an is-a relationship here (I th
340 }
341
342 /// Returns a string representation of [commands] fit for use in a usage string.
343 ///
344 /// [isSubcommand] indicates whether the commands should be called "commands" or
345 /// "subcommands".
346 String _getCommandUsage(Map<String, Command> commands,
347 {bool isSubcommand: false}) {
348 // Don't include aliases.
349 var names = commands.keys
350 .where((name) => !commands[name].aliases.contains(name));
351
352 // Filter out hidden ones, unless they are all hidden.
353 var visible = names.where((name) => !commands[name].hidden);
354 if (visible.isNotEmpty) names = visible;
355
356 // Show the commands alphabetically.
Bob Nystrom 2014/12/11 20:25:30 Do you think users will want to be able to control
nweiz 2014/12/11 23:55:24 I think it's more likely that the user will want c
Bob Nystrom 2014/12/12 18:13:22 SGTM.
357 names = names.toList()..sort();
358 var length = names.map((name) => name.length).reduce(math.max);
359
360 var buffer = new StringBuffer(
361 'Available ${isSubcommand ? "sub" : ""}commands:');
362 for (var name in names) {
363 buffer.writeln();
364 buffer.write(' ${padRight(name, length)} '
365 '${commands[name].description.split("\n").first}');
366 }
367
368 return buffer.toString();
369 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698