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

Side by Side Diff: pkg/args/lib/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: Code review changes 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
« no previous file with comments | « pkg/args/README.md ('k') | pkg/args/lib/src/help_command.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 'src/arg_parser.dart';
12 import 'src/arg_results.dart';
13 import 'src/help_command.dart';
14 import 'src/usage_exception.dart';
15 import 'src/utils.dart';
16
17 export 'src/usage_exception.dart';
18
19 /// A class for invoking [Commands] based on raw command-line arguments.
20 class CommandRunner {
21 /// The name of the executable being run.
22 ///
23 /// Used for error reporting.
Sean Eagan 2014/12/12 17:50:32 also used for usage text.
nweiz 2014/12/16 02:07:44 Done.
24 final String executableName;
25
26 /// A short description of this executable.
27 final String description;
28
29 /// A single-line template for how to invoke this executable.
30 ///
31 /// Defaults to "$executableName [command] <arguments>". Subclasses can
32 /// override this for a more specific template.
33 String get usageTemplate => "$executableName [command] <arguments>";
Sean Eagan 2014/12/12 17:50:32 invocationTemplate? invocationHelp? Since `usage`
nweiz 2014/12/16 02:07:44 Yeah, I wasn't a big fan of this name. Pub had it
34
35 /// Generates a string displaying usage information for the executable.
36 ///
37 /// This includes usage for the global arguments as well as a list of
38 /// top-level commands.
39 String get usage {
40 return '''
41 $description
42
43 Usage: $usageTemplate
44
45 Global options:
46 ${argParser.usage}
47
48 ${_getCommandUsage(_commands)}
49
50 Run "$executableName help [command]" for more information about a command.''';
51 }
52
53 /// Returns [usage] with [description] removed from the beginning.
54 String get _usageWithoutDescription {
Sean Eagan 2014/12/12 17:50:32 Why is this necessary in the first place? Why not
nweiz 2014/12/16 02:07:44 This was following pub's behavior, but I think the
Sean Eagan 2014/12/16 17:41:05 Makes sense.
nweiz 2014/12/17 00:04:59 But the vastly-more-common use case becomes more d
55 // Base this on the return value of [usage] so that subclasses can override
56 // usage and have their changes reflected here.
57 return usage.replaceFirst("$description\n\n", "");
58 }
59
60 /// An unmodifiable view of all top-level commands defined for this runner.
61 Map<String, Command> get commands =>
62 new UnmodifiableMapView(_commands);
63 final _commands = new Map<String, Command>();
64
65 /// The top-level argument parser.
66 ///
67 /// Global options should be registered with this parser; they'll end up
68 /// available via [Command.globalOptions]. Commands should be registered with
69 /// [addCommand] rather than directly on the parser.
70 final argParser = new ArgParser();
71
72 CommandRunner(this.executableName, this.description) {
Sean Eagan 2014/12/12 17:50:32 executableName is almost always derivable from dar
73 argParser.addFlag('help', abbr: 'h', negatable: false,
74 help: 'Print this usage information.');
75 addCommand(new HelpCommand());
Sean Eagan 2014/12/12 17:50:32 This should only be added when the first sub-comma
nweiz 2014/12/16 02:07:44 What's the use of a command runner with no command
Sean Eagan 2014/12/16 17:41:05 The problem is, [CommandRunner] as currently defin
nweiz 2014/12/17 00:04:59 I'm skeptical that integrating with a separate lib
76 }
77
78 /// Prints the usage information for this runner.
79 ///
80 /// This is called internally by [run] and can be overridden by subclasses to
81 /// control how output is displayed or integrate with a logging system.
82 void printUsage() => print(usage);
83
84 /// Throws a [UsageException] with [message].
85 void usageError(String message) =>
86 throw new UsageException(message, _usageWithoutDescription);
87
88 /// Adds [Command] as a top-level command to this runner.
89 void addCommand(Command command) {
90 var names = [command.name]..addAll(command.aliases);
91 for (var name in names) {
92 _commands[name] = command;
93 argParser.addCommand(name, command.argParser);
94 }
95 command._runner = this;
96 }
97
98 /// Parses [args] and invokes [Command.run] on the chosen command.
99 ///
100 /// This always returns a [Future] in case the command is asynchronous. The
101 /// [Future] will throw a [UsageError] if [args] was invalid.
102 Future run(Iterable<String> args) =>
103 new Future.sync(() => runCommand(parse(args)));
104
105 /// Parses [args] and returns the result, converting a [FormatException] to a
106 /// [UsageException].
107 ///
108 /// This is notionally a protected method. It may be overridden or called from
109 /// subclasses, but it shouldn't be called externally.
110 ArgResults parse(Iterable<String> args) {
111 try {
112 // TODO(nweiz): if arg parsing fails for a command, print that command's
113 // usage, not the global usage.
114 return argParser.parse(args);
115 } on FormatException catch (error) {
116 usageError(error.message);
117 }
118 }
119
120 /// Runs the command specified by [topLevelOptions].
121 ///
122 /// This is notionally a protected method. It may be overridden or called from
123 /// subclasses, but it shouldn't be called externally.
124 ///
125 /// It's useful to override this to handle global flags and/or wrap the entire
126 /// command in a block. For example, you might handle the `--verbose` flag
127 /// here to enable verbose logging before running the command.
128 Future runCommand(ArgResults topLevelOptions) {
129 return new Future.sync(() {
130 var options = topLevelOptions;
131 var commands = _commands;
132 var command;
133 var commandString = executableName;
134
135 while (commands.isNotEmpty) {
136 if (options.command == null) {
137 if (options.rest.isEmpty) {
138 if (command == null) {
139 // No top-level command was chosen.
140 printUsage();
141 return new Future.value();
142 }
143
144 command.usageError('Missing subcommand for "$commandString".');
145 } else {
146 if (command == null) {
147 usageError(
148 'Could not find a command named "${options.rest[0]}".');
149 }
150
151 command.usageError('Could not find a subcommand named '
152 '"${options.rest[0]}" for "$commandString".');
153 }
154 }
155
156 // Step into the command.
157 var parent = command;
158 options = options.command;
159 command = commands[options.name];
160 command._globalOptions = topLevelOptions;
161 command._options = options;
162 commands = command._subcommands;
163 commandString += " ${options.name}";
164
165 if (options['help']) {
166 command.printUsage();
167 return new Future.value();
168 }
169 }
170
171 // Make sure there aren't unexpected arguments.
172 if (!command.takesArguments && options.rest.isNotEmpty) {
173 command.usageError(
174 'Command "${options.name}" does not take any arguments.');
175 }
176
177 return command.run();
178 });
179 }
180 }
181
182 /// A single command.
183 ///
184 /// A command is known as a "leaf command" if it has no subcommands and is meant
185 /// to be run. Leaf commands must override [run].
186 ///
187 /// A command with subcommands is known as a "branch command" and cannot be run
188 /// itself. It should call [addSubcommand] (often from the constructor) to
189 /// register subcommands.
190 abstract class Command {
191 /// The name of this command.
192 String get name;
193
194 /// A short description of this command.
195 String get description;
196
197 /// A single-line template for how to invoke this command (e.g. `"pub get
198 /// [package]"`).
199 String get usageTemplate {
200 var parents = [name];
201 for (var command = parent; command != null; command = command.parent) {
202 parents.add(command.name);
203 }
204 parents.add(runner.executableName);
205
206 var invocation = parents.reversed.join(" ");
207 return _subcommands.isNotEmpty ?
208 "$invocation [subcommand] <arguments>" :
209 "$invocation <arguments>";
210 }
211
212 /// The command's parent command, if this is a subcommand.
213 ///
214 /// This will be `null` until [Command.addSubcommmand] has been called with
215 /// this command.
216 Command get parent => _parent;
217 Command _parent;
218
219 /// The command runner for this command.
220 ///
221 /// This will be `null` until [CommandRunner.addCommand] has been called with
222 /// this command or one of its parents.
223 CommandRunner get runner {
224 if (parent == null) return _runner;
225 return parent.runner;
226 }
227 CommandRunner _runner;
228
229 /// The parsed global options.
230 ///
231 /// This will be `null` until just before [Command.run] is called.
232 ArgResults get globalOptions => _globalOptions;
Sean Eagan 2014/12/12 17:50:32 It's weird to define [options] and [globalOptions]
nweiz 2014/12/16 02:07:44 It's more that it's useful for writing methods wit
Sean Eagan 2014/12/16 17:41:05 Once the command implementation gets sufficiently
nweiz 2014/12/17 00:04:59 You're describing a pretty complex architecture th
233 ArgResults _globalOptions;
234
235 /// The parsed options for this command.
236 ///
237 /// This will be `null` until just before [Command.run] is called.
238 ArgResults get options => _options;
239 ArgResults _options;
240
241 /// The argument parser for this command.
242 ///
243 /// Options for this command should be registered with this parser (often in
244 /// the constructor); they'll end up available via [options]. Subcommands
245 /// should be registered with [addSubcommand] rather than directly on the
246 /// parser.
247 final argParser = new ArgParser();
248
249 /// Generates a string displaying usage information for this command.
250 ///
251 /// This includes usage for the command's arguments as well as a list of
252 /// subcommands, if there are any.
253 String get usage {
254 var buffer = new StringBuffer()
255 ..writeln(description)
256 ..writeln()
257 ..writeln('Usage: $usageTemplate')
258 ..writeln(argParser.usage);
259
260 if (_subcommands.isNotEmpty) {
261 buffer.writeln();
262 buffer.writeln(_getCommandUsage(_subcommands, isSubcommand: true));
263 }
264
265 buffer.writeln();
266 buffer.write('Run "${runner.executableName} help" to see global options.');
267
268 return buffer.toString();
269 }
270
271 /// Returns [usage] with [description] removed from the beginning.
272 String get _usageWithoutDescription {
273 // Base this on the return value of [usage] so that subclasses can override
274 // usage and have their changes reflected here.
275 return usage.replaceFirst("$description\n\n", "");
276 }
277
278 /// An unmodifiable view of all sublevel commands of this command.
279 Map<String, Command> get subcommands =>
280 new UnmodifiableMapView(_subcommands);
281 final _subcommands = new Map<String, Command>();
282
283 /// Whether or not this command should appear in help listings.
Sean Eagan 2014/12/12 17:50:32 appear in -> be hidden from
nweiz 2014/12/16 02:07:44 Done.
284 ///
285 /// This is intended to be overridden by commands that want to mark themselves
286 /// hidden.
287 ///
288 /// By default, this is always true for leaf commands. It's true for branch
289 /// commands as long as any of their leaf commands are visible.
Sean Eagan 2014/12/12 17:50:32 true -> false or "leaf commands are always visibl
nweiz 2014/12/16 02:07:44 Done.
290 bool get hidden {
Sean Eagan 2014/12/12 17:50:32 Why not add this directly to ArgParser?
nweiz 2014/12/16 02:07:44 See other discussions.
291 // Leaf commands are visible by default.
292 if (_subcommands.isEmpty) return false;
293
294 // Otherwise, a command is hidden if all of its subcommands are.
295 return _subcommands.values.every((subcommand) => subcommand.hidden);
296 }
297
298 /// Whether or not this command takes positional arguments in addition to
299 /// options.
300 ///
301 /// If false, [CommandRunner.run] will throw a [UsageException] if arguments
302 /// are provided. Defaults to true.
303 ///
304 /// This is intended to be overridden by commands that don't want to receive
305 /// arguments. It has no effect for branch commands.
306 final takesArguments = true;
Sean Eagan 2014/12/12 17:50:32 This is too coarse. Why should individual positio
307
308 /// Alternate names for this command.
309 ///
310 /// These names won't be used in the documentation, but they will work when
311 /// invoked on the command line.
312 ///
313 /// This is intended to be overridden.
314 final aliases = const <String>[];
Sean Eagan 2014/12/12 17:50:32 How about an AliasCommand for this: command.addSu
nweiz 2014/12/16 02:07:44 Part of the point of this architecture is to keep
Sean Eagan 2014/12/16 17:41:05 Agreed if there is some use of that metadata, and
nweiz 2014/12/17 00:04:59 The fact that the metadata is used by the dispatch
315
316 Command() {
317 argParser.addFlag('help', abbr: 'h', negatable: false,
318 help: 'Print this usage information.');
319 }
320
321 /// Runs this command.
322 ///
323 /// If this returns a [Future], [CommandRunner.run] won't complete until the
324 /// returned [Future] does. Otherwise, the return value is ignored.
325 run() {
326 throw new UnimplementedError("Leaf command $this must implement run().");
327 }
328
329 /// Adds [Command] as a subcommand of this.
330 void addSubcommand(Command command) {
331 var names = [command.name]..addAll(command.aliases);
332 for (var name in names) {
333 _subcommands[name] = command;
334 argParser.addCommand(name, command.argParser);
335 }
336 command._parent = this;
337 }
338
339 /// Prints the usage information for this command.
340 ///
341 /// This is called internally by [run] and can be overridden by subclasses to
342 /// control how output is displayed or integrate with a logging system.
343 void printUsage() => print(usage);
Sean Eagan 2014/12/12 17:50:32 I would think "to control how output is displayed
nweiz 2014/12/16 02:07:44 The usage is the only output that needs to be auto
Sean Eagan 2014/12/16 17:41:05 Unscripted also automatically outputs usage errors
nweiz 2014/12/17 00:04:59 This is another situation where I'm not sure the b
344
345 /// Throws a [UsageException] with [message].
346 void usageError(String message) =>
Sean Eagan 2014/12/12 17:50:32 usageException?
nweiz 2014/12/16 02:07:44 Done.
347 throw new UsageException(message, _usageWithoutDescription);
348 }
349
350 /// Returns a string representation of [commands] fit for use in a usage string.
351 ///
352 /// [isSubcommand] indicates whether the commands should be called "commands" or
353 /// "subcommands".
354 String _getCommandUsage(Map<String, Command> commands,
355 {bool isSubcommand: false}) {
356 // Don't include aliases.
357 var names = commands.keys
358 .where((name) => !commands[name].aliases.contains(name));
359
360 // Filter out hidden ones, unless they are all hidden.
361 var visible = names.where((name) => !commands[name].hidden);
362 if (visible.isNotEmpty) names = visible;
363
364 // Show the commands alphabetically.
365 names = names.toList()..sort();
366 var length = names.map((name) => name.length).reduce(math.max);
367
368 var buffer = new StringBuffer(
369 'Available ${isSubcommand ? "sub" : ""}commands:');
370 for (var name in names) {
371 buffer.writeln();
372 buffer.write(' ${padRight(name, length)} '
373 '${commands[name].description.split("\n").first}');
374 }
375
376 return buffer.toString();
377 }
OLDNEW
« no previous file with comments | « pkg/args/README.md ('k') | pkg/args/lib/src/help_command.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698