Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkgs/args/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 2.8.0

* Allow designating a top-level command or a subcommand as a default one by
passing `isDefault: true` to `addCommand` or `addSubcommand`.
Default command will be selected by argument parser if no sibling command
matches. This allows creating command line interfaces where both
`program command` and `program command subcommand` are runnable
(Fixes #103).

## 2.7.0

* Remove sorting of the `allowedHelp` argument in usage output. Ordering will
Expand Down
89 changes: 71 additions & 18 deletions pkgs/args/lib/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,19 @@ class CommandRunner<T> {

/// A single-line template for how to invoke this executable.
///
/// Defaults to `"$executableName <command> arguments`". Subclasses can
/// override this for a more specific template.
String get invocation => '$executableName <command> [arguments]';
/// Defaults to `"$executableName <command> arguments"` (if there is no
/// default command) or `"$executableName [<command>] arguments"` (otherwise).
///
/// Subclasses can override this for a more specific template.
String get invocation {
var command = '<command>';

if (argParser.defaultCommand != null) {
command = '[$command]';
}

return '$executableName $command [arguments]';
}

/// Generates a string displaying usage information for the executable.
///
Expand All @@ -56,9 +66,10 @@ class CommandRunner<T> {
);
buffer.writeln(_wrap('Global options:'));
buffer.writeln('${argParser.usage}\n');
buffer.writeln(
'${_getCommandUsage(_commands, lineLength: argParser.usageLineLength)}\n',
);
buffer.writeln(_getCommandUsage(_commands,
lineLength: argParser.usageLineLength,
defaultCommand: argParser.defaultCommand));
buffer.writeln();
buffer.write(_wrap(
'Run "$executableName help <command>" for more information about a '
'command.'));
Expand Down Expand Up @@ -105,12 +116,25 @@ class CommandRunner<T> {
throw UsageException(message, _usageWithoutDescription);

/// Adds [Command] as a top-level command to this runner.
void addCommand(Command<T> command) {
///
/// If [isDefault] is `true` then added command will be designated as a
/// default one. Default command is selected if no other sibling command
/// matches. Only a single leaf-command can be designated as a default.
void addCommand(Command<T> command, {bool isDefault = false}) {
if (isDefault && command.subcommands.isNotEmpty) {
throw ArgumentError('default command must be a leaf command');
}
if (isDefault && argParser.defaultCommand != null) {
throw StateError('default command already defined');
}
var names = [command.name, ...command.aliases];
for (var name in names) {
_commands[name] = command;
argParser.addCommand(name, command.argParser);
}
if (isDefault) {
argParser.defaultCommand = command.name;
}
command._runner = this;
}

Expand Down Expand Up @@ -288,9 +312,13 @@ abstract class Command<T> {
parents.add(runner!.executableName);

var invocation = parents.reversed.join(' ');
return _subcommands.isNotEmpty
? '$invocation <subcommand> [arguments]'
: '$invocation [arguments]';
if (argParser.defaultCommand != null) {
return '$invocation [<subcommand>] [arguments]';
} else if (_subcommands.isNotEmpty) {
return '$invocation <subcommand> [arguments]';
} else {
return '$invocation [arguments]';
}
}

/// The command's parent command, if this is a subcommand.
Expand Down Expand Up @@ -363,11 +391,10 @@ abstract class Command<T> {

if (_subcommands.isNotEmpty) {
buffer.writeln();
buffer.writeln(_getCommandUsage(
_subcommands,
isSubcommand: true,
lineLength: length,
));
buffer.writeln(_getCommandUsage(_subcommands,
isSubcommand: true,
lineLength: length,
defaultCommand: argParser.defaultCommand));
}

buffer.writeln();
Expand Down Expand Up @@ -446,12 +473,26 @@ abstract class Command<T> {
}

/// Adds [Command] as a subcommand of this.
void addSubcommand(Command<T> command) {
///
/// If [isDefault] is `true` then added command will be designated as a
/// default one. Default subcommand is selected if no other sibling subcommand
/// matches. Only a single leaf-command can be designated as a default.
void addSubcommand(Command<T> command, {bool isDefault = false}) {
if (isDefault && command.subcommands.isNotEmpty) {
throw ArgumentError('default command must be a leaf command');
}
if (isDefault && argParser.defaultCommand != null) {
throw StateError('default command already defined');
}

var names = [command.name, ...command.aliases];
for (var name in names) {
_subcommands[name] = command;
argParser.addCommand(name, command.argParser);
}
if (isDefault) {
argParser.defaultCommand = command.name;
}
command._parent = this;
}

Expand All @@ -470,8 +511,10 @@ abstract class Command<T> {
///
/// [isSubcommand] indicates whether the commands should be called "commands" or
/// "subcommands".
///
/// [defaultCommand] indicate which command (if any) is designated as default.
String _getCommandUsage(Map<String, Command> commands,
{bool isSubcommand = false, int? lineLength}) {
{bool isSubcommand = false, int? lineLength, String? defaultCommand}) {
// Don't include aliases.
var names =
commands.keys.where((name) => !commands[name]!.aliases.contains(name));
Expand Down Expand Up @@ -502,7 +545,8 @@ String _getCommandUsage(Map<String, Command> commands,
buffer.write(category);
}
for (var command in commandsByCategory[category]!) {
var lines = wrapTextAsLines(command.summary,
var defaultMarker = defaultCommand == command.name ? '(default) ' : '';
var lines = wrapTextAsLines(defaultMarker + command.summary,
start: columnStart, length: lineLength);
buffer.writeln();
buffer.write(' ${padRight(command.name, length)} ${lines.first}');
Expand All @@ -515,6 +559,15 @@ String _getCommandUsage(Map<String, Command> commands,
}
}

if (defaultCommand != null) {
buffer.writeln();
buffer.writeln();
buffer.write(wrapText(
'Default command ($defaultCommand) will be selected if no command'
' is explicitly specified.',
length: lineLength));
}

return buffer.toString();
}

Expand Down
7 changes: 7 additions & 0 deletions pkgs/args/lib/src/allow_anything_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,11 @@ class AllowAnythingParser implements ArgParser {

@override
Option? findByNameOrAlias(String name) => null;

@override
String? get defaultCommand => null;

@override
set defaultCommand(String? value) => throw UnsupportedError(
"ArgParser.allowAnything().defaultCommand= isn't supported.");
}
5 changes: 5 additions & 0 deletions pkgs/args/lib/src/arg_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ class ArgParser {
/// The commands that have been defined for this parser.
final Map<String, ArgParser> commands;

/// Command which will be executed by default if no command is specified.
///
/// When `null` it is a usage error to omit the command name.
String? defaultCommand;

/// A list of the [Option]s in [options] intermingled with [String]
/// separators.
final _optionsAndSeparators = <Object>[];
Expand Down
67 changes: 46 additions & 21 deletions pkgs/args/lib/src/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Parser {
_grammar, const {}, _commandName, null, arguments, arguments);
}

ArgResults? commandResults;
({String name, ArgParser parser})? command;

// Parse the args.
while (_args.isNotEmpty) {
Expand All @@ -61,26 +61,19 @@ class Parser {

// Try to parse the current argument as a command. This happens before
// options so that commands can have option-like names.
var command = _grammar.commands[_current];
if (command != null) {
_validate(_rest.isEmpty, 'Cannot specify arguments before a command.',
_current);
var commandName = _args.removeFirst();
var commandParser = Parser(commandName, command, _args, this, _rest);

try {
commandResults = commandParser.parse();
} on ArgParserException catch (error) {
throw ArgParserException(
error.message,
[commandName, ...error.commands],
error.argumentName,
error.source,
error.offset);
}

// All remaining arguments were passed to command so clear them here.
_rest.clear();
//
// Otherwise, if there is a default command then select it before parsing
// any arguments. We make exception for situations when help flag is
// passed because we want `program command -h` to display help for
// `command` rather than display help for the default subcommand of the
// `command`.
if (_grammar.commands[_current] case final parser?) {
command = (name: _args.removeFirst(), parser: parser);
break;
} else if (_grammar.defaultCommand case final defaultCommand?
when !(_current == '-h' || _current == '--help')) {
command =
(name: defaultCommand, parser: _grammar.commands[defaultCommand]!);
break;
}

Expand All @@ -96,6 +89,38 @@ class Parser {
_rest.add(_args.removeFirst());
}

// If there is a default command and we did not select any other commands
// and we don't have any trailing arguments then select the default
// command unless user requested help.
if (command == null && _rest.isEmpty && !_results.containsKey('help')) {
if (_grammar.defaultCommand case final defaultCommand?) {
command =
(name: defaultCommand, parser: _grammar.commands[defaultCommand]!);
}
}

ArgResults? commandResults;
if (command != null) {
_validate(_rest.isEmpty, 'Cannot specify arguments before a command.',
command.name);
var commandParser =
Parser(command.name, command.parser, _args, this, _rest);

try {
commandResults = commandParser.parse();
} on ArgParserException catch (error) {
throw ArgParserException(
error.message,
[command.name, ...error.commands],
error.argumentName,
error.source,
error.offset);
}

// All remaining arguments were passed to command so clear them here.
_rest.clear();
}

// Check if mandatory and invoke existing callbacks.
_grammar.options.forEach((name, option) {
var parsedOption = _results[name];
Expand Down
2 changes: 1 addition & 1 deletion pkgs/args/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: args
version: 2.7.0
version: 2.8.0
description: >-
Library for defining parsers for parsing raw command-line arguments into a set
of options and values using GNU and POSIX style options.
Expand Down
Loading