@@ -33,7 +33,15 @@ class CommandRunner<T> {
3333 ///
3434 /// Defaults to `"$executableName <command> arguments` ". Subclasses can
3535 /// override this for a more specific template.
36- String get invocation => '$executableName <command> [arguments]' ;
36+ String get invocation {
37+ var command = '<command>' ;
38+
39+ if (commands.containsKey (Command .defaultSubcommand)) {
40+ command = '[$command ]' ;
41+ }
42+
43+ return '$executableName $command [arguments]' ;
44+ }
3745
3846 /// Generates a string displaying usage information for the executable.
3947 ///
@@ -51,11 +59,24 @@ class CommandRunner<T> {
5159 String get _usageWithoutDescription {
5260 var usagePrefix = 'Usage:' ;
5361 var buffer = StringBuffer ();
62+
63+ if (commands[Command .defaultSubcommand] case final defaultCommand? ) {
64+ final cmdLine = '$executableName [arguments]' ;
65+ buffer.writeln (
66+ '$usagePrefix ${_wrap (cmdLine , hangingIndent : usagePrefix .length )}\n ' ,
67+ );
68+ buffer.writeln (defaultCommand.description);
69+ buffer.writeln ();
70+ buffer.writeln ('${defaultCommand .argParser .usage }\n ' );
71+ }
72+
73+ final cmdLine = '$executableName <command> [arguments]' ;
5474 buffer.writeln (
55- '$usagePrefix ${_wrap (invocation , hangingIndent : usagePrefix .length )}\n ' ,
75+ '$usagePrefix ${_wrap (cmdLine , hangingIndent : usagePrefix .length )}\n ' ,
5676 );
5777 buffer.writeln (_wrap ('Global options:' ));
5878 buffer.writeln ('${argParser .usage }\n ' );
79+
5980 buffer.writeln (
6081 '${_getCommandUsage (_commands , lineLength : argParser .usageLineLength )}\n ' ,
6182 );
@@ -205,8 +226,17 @@ class CommandRunner<T> {
205226
206227 // Make sure there aren't unexpected arguments.
207228 if (! command! .takesArguments && argResults.rest.isNotEmpty) {
208- command.usageException (
209- 'Command "${argResults .name }" does not take any arguments.' );
229+ if (argResults.name == Command .defaultSubcommand) {
230+ if (command.parent case final parent? ) {
231+ parent.usageException (
232+ 'Command "${parent .name }" does not take any arguments.' );
233+ } else {
234+ command.usageException ('Command does not take any arguments.' );
235+ }
236+ } else {
237+ command.usageException (
238+ 'Command "${argResults .name }" does not take any arguments.' );
239+ }
210240 }
211241
212242 return (await command.run ()) as T ? ;
@@ -258,8 +288,17 @@ class CommandRunner<T> {
258288///
259289/// A command with subcommands is known as a "branch command" and cannot be run
260290/// itself. It should call [addSubcommand] (often from the constructor) to
261- /// register subcommands.
291+ /// register subcommands. If a registered subcommand has an empty (`''` ) name
292+ /// or an alias then it will be used as a default subcommand and will be
293+ /// selected when no other subcommand matches.
262294abstract class Command <T > {
295+ /// Marker name used to denote default subcommands.
296+ ///
297+ /// Default subcommand triggers when no other subcommand matches then can
298+ /// be used to define command line interfaces where both `program command`
299+ /// and `program command subcommand` perform meaningful actions.
300+ static const String defaultSubcommand = '' ;
301+
263302 /// The name of this command.
264303 String get name;
265304
@@ -281,16 +320,24 @@ abstract class Command<T> {
281320 /// A single-line template for how to invoke this command (e.g. `"pub get
282321 /// `package` "`).
283322 String get invocation {
284- var parents = [name];
323+ var invocation = _invocationChain;
324+ if (_subcommands.containsKey (defaultSubcommand)) {
325+ return '$invocation [<subcommand>] [arguments]' ;
326+ } else if (_subcommands.isNotEmpty) {
327+ return '$invocation <subcommand> [arguments]' ;
328+ } else {
329+ return '$invocation [arguments]' ;
330+ }
331+ }
332+
333+ String get _invocationChain {
334+ var parents = [if (name != defaultSubcommand) name];
285335 for (var command = parent; command != null ; command = command.parent) {
286336 parents.add (command.name);
287337 }
288338 parents.add (runner! .executableName);
289339
290- var invocation = parents.reversed.join (' ' );
291- return _subcommands.isNotEmpty
292- ? '$invocation <subcommand> [arguments]'
293- : '$invocation [arguments]' ;
340+ return parents.reversed.join (' ' );
294341 }
295342
296343 /// The command's parent command, if this is a subcommand.
@@ -356,18 +403,35 @@ abstract class Command<T> {
356403 String get _usageWithoutDescription {
357404 var length = argParser.usageLineLength;
358405 var usagePrefix = 'Usage: ' ;
359- var buffer = StringBuffer ()
360- ..writeln (
361- usagePrefix + _wrap (invocation, hangingIndent: usagePrefix.length))
362- ..writeln (argParser.usage);
406+ var buffer = StringBuffer ();
363407
364- if (_subcommands.isNotEmpty) {
408+ if (_subcommands[Command .defaultSubcommand] case final defaultSubcommand? ) {
409+ final invocationChain = _invocationChain;
410+ buffer.writeln (usagePrefix +
411+ _wrap ('$invocationChain [arguments]' ,
412+ hangingIndent: usagePrefix.length));
413+ buffer.writeln (defaultSubcommand.argParser.usage);
365414 buffer.writeln ();
366- buffer.writeln (_getCommandUsage (
415+ buffer.writeln (usagePrefix +
416+ _wrap ('$invocationChain <subcommand> [arguments]' ,
417+ hangingIndent: usagePrefix.length));
418+ } else {
419+ buffer.writeln (
420+ usagePrefix + _wrap (invocation, hangingIndent: usagePrefix.length));
421+ buffer.writeln (argParser.usage);
422+ }
423+
424+ if (_subcommands.isNotEmpty) {
425+ final commandUsage = _getCommandUsage (
367426 _subcommands,
368427 isSubcommand: true ,
369428 lineLength: length,
370- ));
429+ );
430+
431+ if (commandUsage != '' ) {
432+ buffer.writeln ();
433+ buffer.writeln (commandUsage);
434+ }
371435 }
372436
373437 buffer.writeln ();
@@ -447,6 +511,10 @@ abstract class Command<T> {
447511
448512 /// Adds [Command] as a subcommand of this.
449513 void addSubcommand (Command <T > command) {
514+ if (name == defaultSubcommand) {
515+ throw StateError ('default subcommand must be a leaf command' );
516+ }
517+
450518 var names = [command.name, ...command.aliases];
451519 for (var name in names) {
452520 _subcommands[name] = command;
@@ -472,9 +540,15 @@ abstract class Command<T> {
472540/// "subcommands".
473541String _getCommandUsage (Map <String , Command > commands,
474542 {bool isSubcommand = false , int ? lineLength}) {
475- // Don't include aliases.
476- var names =
477- commands.keys.where ((name) => ! commands[name]! .aliases.contains (name));
543+ // Don't include aliases and the default subcommand name (which is just an
544+ // empty string).
545+ var names = commands.keys.where ((name) =>
546+ name != Command .defaultSubcommand &&
547+ ! commands[name]! .aliases.contains (name));
548+
549+ if (names.isEmpty) {
550+ return '' ;
551+ }
478552
479553 // Filter out hidden ones, unless they are all hidden.
480554 var visible = names.where ((name) => ! commands[name]! .hidden);
0 commit comments