Add ability to group commands by category in usage (#202) To enable https://github.com/flutter/flutter/issues/83706 Formatting is loosely based on what `Brew` does, and open to suggestions. Existing tests are unchanged. Example (all displayed commands categorized): <img width="669" alt="Screenshot 2021-07-23 at 16 22 40" src="https://user-images.githubusercontent.com/6655696/126796093-e8652385-c3d7-4600-83b1-76852ab46ea4.png">
diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f7a33..3c2b591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,3 +1,7 @@ +## 2.3.0 + +* Add the ability to group commands by category in usage text. + ## 2.2.0 * Suggest similar commands if an unknown command is encountered, when using the
diff --git a/lib/command_runner.dart b/lib/command_runner.dart index ff6da4a..79fa282 100644 --- a/lib/command_runner.dart +++ b/lib/command_runner.dart
@@ -262,6 +262,12 @@ /// This defaults to the first line of [description]. String get summary => description.split('\n').first; + /// The command's category. + /// + /// Displayed in [parent]'s [CommandRunner.usage]. Commands with categories + /// will be grouped together, and displayed after commands without a category. + String get category => ''; + /// A single-line template for how to invoke this command (e.g. `"pub get /// `package`"`). String get invocation { @@ -455,20 +461,36 @@ // Show the commands alphabetically. names = names.toList()..sort(); + + // Group the commands by category. + var commandsByCategory = SplayTreeMap<String, List<Command>>(); + for (var name in names) { + var category = commands[name]!.category; + commandsByCategory.putIfAbsent(category, () => []).add(commands[name]!); + } + final categories = commandsByCategory.keys.toList(); + var length = names.map((name) => name.length).reduce(math.max); var buffer = StringBuffer('Available ${isSubcommand ? "sub" : ""}commands:'); var columnStart = length + 5; - for (var name in names) { - var lines = wrapTextAsLines(commands[name]!.summary, - start: columnStart, length: lineLength); - buffer.writeln(); - buffer.write(' ${padRight(name, length)} ${lines.first}'); - - for (var line in lines.skip(1)) { + for (var category in categories) { + if (category != '') { buffer.writeln(); - buffer.write(' ' * columnStart); - buffer.write(line); + buffer.writeln(); + buffer.write('$category'); + } + for (var command in commandsByCategory[category]!) { + var lines = wrapTextAsLines(command.summary, + start: columnStart, length: lineLength); + buffer.writeln(); + buffer.write(' ${padRight(command.name, length)} ${lines.first}'); + + for (var line in lines.skip(1)) { + buffer.writeln(); + buffer.write(' ' * columnStart); + buffer.write(line); + } } }
diff --git a/pubspec.yaml b/pubspec.yaml index 332a9bd..b2994c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml
@@ -1,5 +1,5 @@ name: args -version: 2.2.0 +version: 2.3.0 homepage: https://github.com/dart-lang/args description: >- Library for defining parsers for parsing raw command-line arguments into a set
diff --git a/test/command_runner_test.dart b/test/command_runner_test.dart index 4cc6066..3f152b0 100644 --- a/test/command_runner_test.dart +++ b/test/command_runner_test.dart
@@ -53,6 +53,101 @@ Run "test help <command>" for more information about a command.''')); }); + group('displays categories', () { + test('when some commands are categorized', () { + runner.addCommand(Category1Command()); + runner.addCommand(Category2Command()); + runner.addCommand(FooCommand()); + + expect(runner.usage, equals(''' +A test command runner. + +Usage: test <command> [arguments] + +Global options: +-h, --help Print this usage information. + +Available commands: + foo Set a value. + +Displayers + baz Display a value. + +Printers + bar Print a value. + +Run "test help <command>" for more information about a command.''')); + }); + + test('except when all commands in a category are hidden', () { + runner.addCommand(Category1Command()); + runner.addCommand(HiddenCategorizedCommand()); + + expect(runner.usage, equals(''' +A test command runner. + +Usage: test <command> [arguments] + +Global options: +-h, --help Print this usage information. + +Available commands: + +Printers + bar Print a value. + +Run "test help <command>" for more information about a command.''')); + }); + + test('when all commands are categorized', () { + runner.addCommand(Category1Command()); + runner.addCommand(Category2Command()); + + expect(runner.usage, equals(''' +A test command runner. + +Usage: test <command> [arguments] + +Global options: +-h, --help Print this usage information. + +Available commands: + +Displayers + baz Display a value. + +Printers + bar Print a value. + +Run "test help <command>" for more information about a command.''')); + }); + + test('when multiple commands are in a category', () { + runner.addCommand(Category1Command()); + runner.addCommand(Category2Command()); + runner.addCommand(Category2Command2()); + + expect(runner.usage, equals(''' +A test command runner. + +Usage: test <command> [arguments] + +Global options: +-h, --help Print this usage information. + +Available commands: + +Displayers + baz Display a value. + baz2 Display another value. + +Printers + bar Print a value. + +Run "test help <command>" for more information about a command.''')); + }); + }); + test('truncates newlines in command descriptions by default', () { runner.addCommand(MultilineCommand()); @@ -109,6 +204,7 @@ test("doesn't print hidden commands", () { runner ..addCommand(HiddenCommand()) + ..addCommand(HiddenCategorizedCommand()) ..addCommand(FooCommand()); expect(runner.usage, equals('''
diff --git a/test/test_utils.dart b/test/test_utils.dart index 9f2d3df..c580d71 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart
@@ -75,6 +75,69 @@ Future<String> run() async => 'hi'; } +class Category1Command extends Command { + var hasRun = false; + + @override + final name = 'bar'; + + @override + final description = 'Print a value.'; + + @override + final category = 'Printers'; + + @override + final takesArguments = false; + + @override + void run() { + hasRun = true; + } +} + +class Category2Command extends Command { + var hasRun = false; + + @override + final name = 'baz'; + + @override + final description = 'Display a value.'; + + @override + final category = 'Displayers'; + + @override + final takesArguments = false; + + @override + void run() { + hasRun = true; + } +} + +class Category2Command2 extends Command { + var hasRun = false; + + @override + final name = 'baz2'; + + @override + final description = 'Display another value.'; + + @override + final category = 'Displayers'; + + @override + final takesArguments = false; + + @override + void run() { + hasRun = true; + } +} + class MultilineCommand extends Command { var hasRun = false; @@ -168,6 +231,30 @@ } } +class HiddenCategorizedCommand extends Command { + var hasRun = false; + + @override + final name = 'hiddencategorized'; + + @override + final description = 'Set a value.'; + + @override + final category = 'Some category'; + + @override + final hidden = true; + + @override + final takesArguments = false; + + @override + void run() { + hasRun = true; + } +} + class AliasedCommand extends Command { var hasRun = false;