Skip to content

Commit a3000c1

Browse files
committed
Make it possible to add default subcommand
Make it possible to add default subcommand (designated by an empty name) for a branch command: default subcommand will be run when no other subcommand is selected. This allows creating command line interfaces where both `program command` and `program command subcommand` are runnable. Fixes #103
1 parent f2efaaf commit a3000c1

File tree

7 files changed

+285
-41
lines changed

7 files changed

+285
-41
lines changed

pkgs/args/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 2.8.0-dev
2+
3+
* Make it possible to add default subcommand (designated by an empty name)
4+
for a branch command: default subcommand will be run when no other
5+
subcommand is selected. This allows creating command line interfaces where
6+
both `program command` and `program command subcommand` are runnable.
7+
(Fixes #103).
8+
19
## 2.7.0
210

311
* Remove sorting of the `allowedHelp` argument in usage output. Ordering will

pkgs/args/lib/command_runner.dart

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
262294
abstract 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".
473541
String _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);

pkgs/args/lib/src/parser.dart

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:collection';
66

7+
import '../command_runner.dart';
78
import 'arg_parser.dart';
89
import 'arg_parser_exception.dart';
910
import 'arg_results.dart';
@@ -50,6 +51,7 @@ class Parser {
5051
}
5152

5253
ArgResults? commandResults;
54+
(String, ArgParser)? command;
5355

5456
// Parse the args.
5557
while (_args.isNotEmpty) {
@@ -61,26 +63,12 @@ class Parser {
6163

6264
// Try to parse the current argument as a command. This happens before
6365
// options so that commands can have option-like names.
64-
var command = _grammar.commands[_current];
65-
if (command != null) {
66-
_validate(_rest.isEmpty, 'Cannot specify arguments before a command.',
67-
_current);
68-
var commandName = _args.removeFirst();
69-
var commandParser = Parser(commandName, command, _args, this, _rest);
70-
71-
try {
72-
commandResults = commandParser.parse();
73-
} on ArgParserException catch (error) {
74-
throw ArgParserException(
75-
error.message,
76-
[commandName, ...error.commands],
77-
error.argumentName,
78-
error.source,
79-
error.offset);
80-
}
81-
82-
// All remaining arguments were passed to command so clear them here.
83-
_rest.clear();
66+
if (_grammar.commands[_current] case final parser?) {
67+
command = (_args.removeFirst(), parser);
68+
break;
69+
} else if (_grammar.commands[Command.defaultSubcommand]
70+
case final parser?) {
71+
command = (Command.defaultSubcommand, parser);
8472
break;
8573
}
8674

@@ -96,6 +84,32 @@ class Parser {
9684
_rest.add(_args.removeFirst());
9785
}
9886

87+
if (command == null && _rest.isEmpty) {
88+
if (_grammar.commands[Command.defaultSubcommand] case final parser?) {
89+
command = (Command.defaultSubcommand, parser);
90+
}
91+
}
92+
93+
if (command case (final commandName, final parser)?) {
94+
_validate(_rest.isEmpty, 'Cannot specify arguments before a command.',
95+
commandName);
96+
var commandParser = Parser(commandName, parser, _args, this, _rest);
97+
98+
try {
99+
commandResults = commandParser.parse();
100+
} on ArgParserException catch (error) {
101+
throw ArgParserException(
102+
error.message,
103+
[commandName, ...error.commands],
104+
error.argumentName,
105+
error.source,
106+
error.offset);
107+
}
108+
109+
// All remaining arguments were passed to command so clear them here.
110+
_rest.clear();
111+
}
112+
99113
// Check if mandatory and invoke existing callbacks.
100114
_grammar.options.forEach((name, option) {
101115
var parsedOption = _results[name];

pkgs/args/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: args
2-
version: 2.7.0
2+
version: 2.8.0-dev
33
description: >-
44
Library for defining parsers for parsing raw command-line arguments into a set
55
of options and values using GNU and POSIX style options.

0 commit comments

Comments
 (0)