From 2b9381da3eaaf8cee96d9fe207115096834860ea Mon Sep 17 00:00:00 2001 From: Christer Date: Sun, 21 Sep 2025 12:16:00 +0200 Subject: [PATCH 01/12] refactor: Moved completion command to own folder --- .../lib/src/better_command_runner/better_command_runner.dart | 2 +- .../{ => completion}/completion_command.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename packages/cli_tools/lib/src/better_command_runner/{ => completion}/completion_command.dart (99%) diff --git a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart index 6606bfd..70ef694 100644 --- a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart +++ b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart @@ -5,7 +5,7 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:config/config.dart'; -import 'completion_command.dart'; +import 'completion/completion_command.dart'; /// A function type for executing code before running a command. typedef OnBeforeRunCommand = Future Function(BetterCommandRunner runner); diff --git a/packages/cli_tools/lib/src/better_command_runner/completion_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart similarity index 99% rename from packages/cli_tools/lib/src/better_command_runner/completion_command.dart rename to packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart index 2ce9c6c..fd6e9e8 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion_command.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart @@ -3,8 +3,8 @@ import 'dart:io' show IOSink, stdout; import 'package:args/command_runner.dart' show Command; import 'package:config/config.dart'; -import 'better_command.dart'; -import 'better_command_runner.dart'; +import '../better_command.dart'; +import '../better_command_runner.dart'; enum CompletionTarget { completely, From 0141510ca249c039dedff9ec51848c5aa375e84b Mon Sep 17 00:00:00 2001 From: Christer Date: Sun, 21 Sep 2025 12:58:08 +0200 Subject: [PATCH 02/12] refactor: Broke out completely generator into own file --- .../completion/completely_generator.dart | 156 +++++++++++++ .../completion/completion_command.dart | 207 +----------------- .../completion/usage_representation.dart | 88 ++++++++ 3 files changed, 246 insertions(+), 205 deletions(-) create mode 100644 packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart create mode 100644 packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart new file mode 100644 index 0000000..954db7d --- /dev/null +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart @@ -0,0 +1,156 @@ +import 'dart:io' show IOSink; + +import 'package:config/config.dart'; + +import 'usage_representation.dart'; + +/// Generates usage representation for a command in the YAML format +/// of the `completely` tool. +/// https://github.com/bashly-framework/completely +class CompletelyYamlGenerator implements UsageRepresentationGenerator { + @override + void generate( + final IOSink out, + final CommandUsage usage, + ) { + _innerGenerate(out, usage, const []); + } + + void _innerGenerate( + final IOSink out, + final CommandUsage usage, + final List inheritedOptions, + ) { + if (usage.subcommands.isEmpty && + usage.persistentOptions.isEmpty && + usage.options.isEmpty && + inheritedOptions.isEmpty) { + return; + } + + out.writeln('${usage.commandSequence.join(' ')}:'); + + for (final subcommand in usage.subcommands) { + out.writeln(' - ${subcommand.command}'); + } + + _generateCompletelyForOptions(out, usage, inheritedOptions); + out.writeln(); + + final propagatedOptions = [...inheritedOptions, ...usage.persistentOptions]; + for (final subcommand in usage.subcommands) { + _innerGenerate(out, subcommand, propagatedOptions); + } + } + + static void _generateCompletelyForOptions( + final IOSink out, + final CommandUsage usage, + final List inheritedOptions, + ) { + final allOptions = [ + ...inheritedOptions, + ...usage.persistentOptions, + ...usage.options + ]; + + // options + for (final option in allOptions) { + if (option.option.argName != null) { + out.writeln(' - --${option.option.argName}'); + + if (option.option case final FlagOption flagOption) { + if (flagOption.negatable && !flagOption.hideNegatedUsage) { + out.writeln(' - --no-${flagOption.argName}'); + } + } + } + + if (option.option.argAbbrev != null) { + out.writeln(' - -${option.option.argAbbrev}'); + } + + if (option.option.argPos == 0) { + final values = _getOptionValues(option.option); + if (values.isNotEmpty) { + _generateCompletelyForOptionValues(out, values); + } + } + // can't currently complete for positional options after the first one + } + + // value completions for each option + for (final option in allOptions) { + _generateCompletelyForOption(out, usage.commandSequence, option.option); + } + } + + static void _generateCompletelyForOption( + final IOSink out, + final List commandSequence, + final ConfigOptionBase option, + ) { + if (option is FlagOption) { + return; + } + + if (option.argName case final String argName) { + _generateCompletelyForArgNameOption( + out, + commandSequence, + option, + ' --$argName', + ); + } + if (option.argAbbrev case final String argAbbrev) { + _generateCompletelyForArgNameOption( + out, + commandSequence, + option, + ' -$argAbbrev', + ); + } + } + + static void _generateCompletelyForArgNameOption( + final IOSink out, + final List commandSequence, + final ConfigOptionBase option, + final String argName, + ) { + final values = _getOptionValues(option); + if (values.isNotEmpty) { + out.writeln('${commandSequence.join(' ')}*$argName:'); + _generateCompletelyForOptionValues(out, values); + } + } + + static List _getOptionValues( + final ConfigOptionBase option, + ) { + if (option.allowedValues case final List allowedValues) { + return allowedValues.map(option.valueParser.format).toList(); + } + + switch (option.option) { + case EnumOption(): + final enumParser = option.option.valueParser as EnumParser; + return enumParser.enumValues.map(enumParser.format).toList(); + case FileOption(): + return ['']; + case DirOption(): + return ['']; + default: + return []; + } + } + + static void _generateCompletelyForOptionValues( + final IOSink out, + final Iterable values, + ) { + for (final value in values) { + out.writeln(' - $value'); + } + } +} diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart index fd6e9e8..c23e1f8 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart @@ -1,10 +1,10 @@ import 'dart:io' show IOSink, stdout; -import 'package:args/command_runner.dart' show Command; import 'package:config/config.dart'; import '../better_command.dart'; -import '../better_command_runner.dart'; +import 'completely_generator.dart'; +import 'usage_representation.dart'; enum CompletionTarget { completely, @@ -75,206 +75,3 @@ class CompletionCommand extends BetterCommand { return null as T; } } - -class CommandUsage { - final List commandSequence; - final List options; - final List subcommands; - - CommandUsage({ - required this.commandSequence, - required this.options, - required this.subcommands, - }); - - String get command => commandSequence.last; -} - -abstract class UsageRepresentation { - /// Compiles a usage representation tree for all subcommands and options - /// for a command runner. - static CommandUsage compile( - final BetterCommandRunner runner, { - final String? execNameOverride, - }) { - return _generateForCommand( - [execNameOverride ?? runner.executableName], - _filterOptions(runner.globalOptions), - runner.commands.values, - const [], - ); - } - - static CommandUsage _generateForCommand( - final List commandSequence, - final List globalOptions, - final Iterable subcommands, - final List options, - ) { - final validOptions = [...globalOptions, ..._filterOptions(options)]; - final validSubcommands = - subcommands.whereType().where((final c) => !c.hidden); - - return CommandUsage( - commandSequence: commandSequence, - options: validOptions, - subcommands: validSubcommands - .map((final subcommand) => _generateForCommand( - [...commandSequence, subcommand.name], - globalOptions, - subcommand.subcommands.values, - subcommand.options, - )) - .toList(), - ); - } - - static List _filterOptions( - final List options, - ) { - return options - .where((final o) => !o.option.hide) - .where((final o) => o.option.argName != null || o.option.argPos != null) - .toList(); - } -} - -/// Interface for generating a usage representation for a command. -abstract interface class UsageRepresentationGenerator { - void generate( - final IOSink out, - final CommandUsage usage, - ); -} - -/// Generates usage representation for a command in the YAML format -/// of the `completely` tool. -/// https://github.com/bashly-framework/completely -class CompletelyYamlGenerator implements UsageRepresentationGenerator { - @override - void generate( - final IOSink out, - final CommandUsage usage, - ) { - if (usage.subcommands.isEmpty && usage.options.isEmpty) { - return; - } - - out.writeln('${usage.commandSequence.join(' ')}:'); - - for (final subcommand in usage.subcommands) { - out.writeln(' - ${subcommand.command}'); - } - - _generateCompletelyForOptions(out, usage); - out.writeln(); - - for (final subcommand in usage.subcommands) { - generate(out, subcommand); - } - } - - static void _generateCompletelyForOptions( - final IOSink out, - final CommandUsage usage, - ) { - // options - for (final option in usage.options) { - if (option.option.argName != null) { - out.writeln(' - --${option.option.argName}'); - - if (option.option case final FlagOption flagOption) { - if (flagOption.negatable && !flagOption.hideNegatedUsage) { - out.writeln(' - --no-${flagOption.argName}'); - } - } - } - - if (option.option.argAbbrev != null) { - out.writeln(' - -${option.option.argAbbrev}'); - } - - if (option.option.argPos == 0) { - final values = _getOptionValues(option.option); - if (values.isNotEmpty) { - _generateCompletelyForOptionValues(out, values); - } - } - // can't currently complete for positional options after the first one - } - - // value completions for each option - for (final option in usage.options) { - _generateCompletelyForOption(out, usage.commandSequence, option.option); - } - } - - static void _generateCompletelyForOption( - final IOSink out, - final List commandSequence, - final ConfigOptionBase option, - ) { - if (option is FlagOption) { - return; - } - - if (option.argName case final String argName) { - _generateCompletelyForArgNameOption( - out, - commandSequence, - option, - ' --$argName', - ); - } - if (option.argAbbrev case final String argAbbrev) { - _generateCompletelyForArgNameOption( - out, - commandSequence, - option, - ' -$argAbbrev', - ); - } - } - - static void _generateCompletelyForArgNameOption( - final IOSink out, - final List commandSequence, - final ConfigOptionBase option, - final String argName, - ) { - final values = _getOptionValues(option); - if (values.isNotEmpty) { - out.writeln('${commandSequence.join(' ')}*$argName:'); - _generateCompletelyForOptionValues(out, values); - } - } - - static List _getOptionValues( - final ConfigOptionBase option, - ) { - if (option.allowedValues case final List allowedValues) { - return allowedValues.map(option.valueParser.format).toList(); - } - - switch (option.option) { - case EnumOption(): - final enumParser = option.option.valueParser as EnumParser; - return enumParser.enumValues.map(enumParser.format).toList(); - case FileOption(): - return ['']; - case DirOption(): - return ['']; - default: - return []; - } - } - - static void _generateCompletelyForOptionValues( - final IOSink out, - final Iterable values, - ) { - for (final value in values) { - out.writeln(' - $value'); - } - } -} diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart b/packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart new file mode 100644 index 0000000..b833861 --- /dev/null +++ b/packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart @@ -0,0 +1,88 @@ +import 'dart:io' show IOSink; + +import 'package:args/command_runner.dart' show Command; +import 'package:config/config.dart'; + +import '../better_command.dart'; +import '../better_command_runner.dart'; + +/// Interface for generating a usage representation for a command. +abstract interface class UsageRepresentationGenerator { + void generate( + final IOSink out, + final CommandUsage usage, + ); +} + +class CommandUsage { + final List commandSequence; + final List subcommands; + + /// Persistent options - the options of the current command that continue to + /// be available for all subcommands. + /// (In Dart, this usually only applies to global options.) + final List persistentOptions; + + /// Regular options - the options of the current command that are not + /// persistent. + final List options; + + CommandUsage({ + required this.commandSequence, + required this.subcommands, + required this.persistentOptions, + required this.options, + }); + + String get command => commandSequence.last; +} + +abstract class UsageRepresentation { + /// Compiles a usage representation tree for all subcommands and options + /// for a command runner. + static CommandUsage compile( + final BetterCommandRunner runner, { + final String? execNameOverride, + }) { + return _generateForCommand( + [execNameOverride ?? runner.executableName], + runner.commands.values, + _filterOptions(runner.globalOptions), + const [], + ); + } + + static CommandUsage _generateForCommand( + final List commandSequence, + final Iterable subcommands, + final List persistentOptions, + final List options, + ) { + final validOptions = _filterOptions(options); + final validSubcommands = + subcommands.whereType().where((final c) => !c.hidden); + + return CommandUsage( + commandSequence: commandSequence, + persistentOptions: persistentOptions, + options: validOptions, + subcommands: validSubcommands + .map((final subcommand) => _generateForCommand( + [...commandSequence, subcommand.name], + subcommand.subcommands.values, + const [], + subcommand.options, + )) + .toList(), + ); + } + + static List _filterOptions( + final List options, + ) { + return options + .where((final o) => !o.option.hide) + .where((final o) => o.option.argName != null || o.option.argPos != null) + .toList(); + } +} From 8d78a858e2321a27e4f94ca10505e9c7416a957c Mon Sep 17 00:00:00 2001 From: Christer Date: Sun, 21 Sep 2025 16:15:15 +0200 Subject: [PATCH 03/12] feat(config): multiParser getter in MultiOption --- packages/config/lib/src/config/options.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 88be5a3..e4d4e15 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -615,10 +615,11 @@ class MultiParser extends ValueParser> { /// Multi-value configuration option. class MultiOption extends ConfigOptionBase> { + final MultiParser multiParser; final List? allowedElementValues; const MultiOption({ - required final MultiParser multiParser, + required this.multiParser, super.argName, super.argAliases, super.argAbbrev, @@ -640,6 +641,13 @@ class MultiOption extends ConfigOptionBase> { valueParser: multiParser, ); + /// [MultiOption] does not properly support [allowedValues], + /// use [allowedElementValues] instead. + @override + List>? get allowedValues { + return super.allowedValues; + } + @override void _addToArgParser(final ArgParser argParser) { final argName = this.argName; @@ -647,7 +655,6 @@ class MultiOption extends ConfigOptionBase> { throw StateError("Can't add option without arg name to arg parser."); } - final multiParser = valueParser as MultiParser; argParser.addMultiOption( argName, abbr: argAbbrev, From eda37c609608404c3321ab27abbbfcf01ce7d78c Mon Sep 17 00:00:00 2001 From: Christer Date: Sun, 21 Sep 2025 16:16:04 +0200 Subject: [PATCH 04/12] feat(cli_tools): Support the carapace CLI completions tool --- packages/cli_tools/README_completion.md | 85 +++++++- .../completion/carapace_completion.dart | 194 ++++++++++++++++++ .../completion/completion_command.dart | 6 +- 3 files changed, 273 insertions(+), 12 deletions(-) create mode 100644 packages/cli_tools/lib/src/better_command_runner/completion/carapace_completion.dart diff --git a/packages/cli_tools/README_completion.md b/packages/cli_tools/README_completion.md index 2f793ee..d31b7b1 100644 --- a/packages/cli_tools/README_completion.md +++ b/packages/cli_tools/README_completion.md @@ -3,12 +3,78 @@ There is now an experimental feature to generate and install command line completion in bash for commands using `BetterCommandRunner`. +### Enable experimental feature + +Enable this experimental feature by constructing `BetterCommandRunner` +with the flag `experimentalCompletionCommand` set to `true`. + +## Using the tool `carapace` + +Carapace supports a lot of shells, including Bash, ZSH, Fish, Elvish, +Powershell, Cmd, and more. + +https://carapace.sh/ + ### Prerequisites -This requires the tool `completely`, which also requires ruby to be installed. +This requires the tool `carapace`. + +```sh +brew install carapace +``` +For installing in other environments, see: +https://carapace-sh.github.io/carapace-bin/install.html + +### Install completion for the command + +When the `carapace` tool is installed, generate the completion for the command. + +Note that the YAML file must have the same name as the command executable +before the `.yaml` extension. + +```sh +my-command completion -f my-command.yaml -t carapace +cp example.yaml "${UserConfigDir}/carapace/specs/" +``` + +> Note: ${UserConfigDir} refers to the platform-specific user configuration +directory. On MacOS this is `~/Library/Application Support/` (even though many +other Bash commands use `~/.local/share/`), and on Windows this is `%APPDATA%`. +This can be overridden with the env var `XDG_CONFIG_HOME`, but be aware this +affects lots of applications. + +Run the following once for the current shell, +or add to your shell startup script: + +Bash: +```bash +source <(carapace my-command) +``` + +Zsh: +```zsh +zstyle ':completion:*' format $'\e[2;37mCompleting %d\e[m' +source <(carapace my-command) +``` + +For more information and installing in other shells, see: +https://carapace-sh.github.io/carapace-bin/setup.html + + +### Distribution + +End users will need to install `carapace` and copy the Yaml file to the proper +location, even if the Yaml file is distributed with the command. + + +## Using the tool `completely` https://github.com/bashly-framework/completely +### Prerequisites + +This requires the tool `completely`, which also requires ruby to be installed. + ```sh gem install completely ``` @@ -20,20 +86,15 @@ brew install brew-gem brew gem install completely ``` -### Activate - -Construct `BetterCommandRunner` with the flag `experimentalCompletionCommand` -set to `true`. - -### Install completion +### Install completion for the command When the `completely` tool is installed, run e.g: ```sh -my-command completion -f completely.yaml -completely generate completely.yaml completely.bash +my-command completion -f my-command.yaml -t completely +completely generate my-command.yaml my-command.bash mkdir -p ~/.local/share/bash-completion/completions -cp completely.bash ~/.local/share/bash-completion/completions/my-command.bash +cp my-command.bash ~/.local/share/bash-completion/completions/ ``` This will write the completions script to `~/.local/share/bash-completion/completions/`, @@ -45,5 +106,7 @@ In order to update the completions in the current bash shell, run: exec bash ``` +### Distribution + For end users, the generated bash script can be distributed as a file for them -to install directly. +to install directly in their `~/.local/share/bash-completion/completions/`. diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/carapace_completion.dart b/packages/cli_tools/lib/src/better_command_runner/completion/carapace_completion.dart new file mode 100644 index 0000000..db883d3 --- /dev/null +++ b/packages/cli_tools/lib/src/better_command_runner/completion/carapace_completion.dart @@ -0,0 +1,194 @@ +import 'dart:io' show IOSink; + +import 'package:config/config.dart'; + +import 'usage_representation.dart'; + +/// Generates usage representation for a command in the YAML format +/// of the `carapace` tool. +/// https://github.com/carapace-sh/carapace +class CarapaceYamlGenerator implements UsageRepresentationGenerator { + @override + void generate( + final IOSink out, + final CommandUsage usage, + ) { + out.writeln( + r'# yaml-language-server: $schema=https://carapace.sh/schemas/command.json'); + + _generateForCommand(out, usage, 0); + } + + void _generateForCommand( + final IOSink out, + final CommandUsage usage, + int indentLevel, + ) { + if (indentLevel == 0) { + _writeWithIndent(out, 'name: ${usage.commandSequence.last}', indentLevel); + } else { + _writeWithIndent( + out, '- name: ${usage.commandSequence.last}', indentLevel); + indentLevel += 1; + } + + if (usage.persistentOptions.isNotEmpty) { + _writeWithIndent(out, 'persistentFlags:', indentLevel); + _declareOptions(out, usage.persistentOptions, indentLevel + 1); + } + + if (usage.options.isNotEmpty) { + _writeWithIndent(out, 'flags:', indentLevel); + _declareOptions(out, usage.options, indentLevel + 1); + } + + final allOptions = [...usage.persistentOptions, ...usage.options]; + + final exclusiveGroups = _generateExclusiveOptionGroups(allOptions); + if (exclusiveGroups.isNotEmpty) { + _writeWithIndent(out, 'exclusiveFlags:', indentLevel); + for (final group in exclusiveGroups) { + _writeWithIndent(out, '- [${group.join(', ')}]', indentLevel + 1); + } + } + + final specs = _generateOptionCompletionSpecs(allOptions); + if (specs.isNotEmpty) { + _writeWithIndent(out, 'completion:', indentLevel); + _writeWithIndent(out, 'flag:', indentLevel + 1); + for (final spec in specs) { + _writeWithIndent(out, spec, indentLevel + 2); + } + } + + out.writeln(); + + if (usage.subcommands.isNotEmpty) { + _writeWithIndent(out, 'commands:', indentLevel); + for (final subcommand in usage.subcommands) { + _generateForCommand(out, subcommand, indentLevel + 1); + } + } + } + + static void _writeWithIndent( + final IOSink out, + final String text, + final int indentLevel, + ) { + out.writeln('${_getIndent(indentLevel)}$text'); + } + + static String _getIndent(final int indentLevel) => ' ' * indentLevel; + + static void _declareOptions( + final IOSink out, + final List options, + final int indentLevel, + ) { + // options + for (final option in options) { + _declareOption(out, option.option, indentLevel); + } + } + + static void _declareOption( + final IOSink out, + final ConfigOptionBase option, + final int indentLevel, + ) { + final names = [ + if (option.argAbbrev != null) '-${option.argAbbrev}', + if (option.argName != null) '--${option.argName}', + ]; + + String attributes = ''; + if (option is! FlagOption) { + attributes += '='; + } + if (option is MultiOption) { + attributes += '*'; + } + if (option.mandatory && option is! FlagOption) { + attributes += '!'; + } + + _writeWithIndent( + out, + '${names.join(', ')}$attributes: ${option.helpText ?? ''}', + indentLevel, + ); + + if (option case final FlagOption flagOption) { + if (flagOption.negatable && !flagOption.hideNegatedUsage) { + _writeWithIndent( + out, + '--no-${option.argName}$attributes: ${option.helpText ?? ''}', + indentLevel, + ); + } + } + } + + static List> _generateExclusiveOptionGroups( + final List options, + ) { + final groups = >[]; + for (final option in options) { + if (option.option case final FlagOption flagOption) { + final argName = flagOption.argName; + if (argName != null && + flagOption.negatable && + !flagOption.hideNegatedUsage) { + groups.add([argName, 'no-$argName']); + } + } + } + return groups; + } + + static List _generateOptionCompletionSpecs( + final List options, + ) { + final specs = []; + for (final option in options) { + final name = option.option.argName ?? option.option.argAbbrev; + if (name == null) { + continue; + } + + final values = _getOptionValues(option.option); + if (values.isNotEmpty) { + final valueSpec = values.map((final v) => '"$v"').join(', '); + specs.add('$name: [$valueSpec]'); + } + } + return specs; + } + + static List _getOptionValues( + final ConfigOptionBase option, + ) { + if (option case final MultiOption multiOption) { + if (multiOption.allowedElementValues case final List allowedValues) { + return allowedValues + .map(multiOption.multiParser.elementParser.format) + .toList(); + } + } else if (option.allowedValues case final List allowedValues) { + return allowedValues.map(option.valueParser.format).toList(); + } + + switch (option.option) { + case EnumOption(): + final enumParser = option.option.valueParser as EnumParser; + return enumParser.enumValues.map(enumParser.format).toList(); + case FileOption(): + return [r'$files']; + case DirOption(): + return [r'$directories']; + default: + return []; + } + } +} diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart index c23e1f8..c37a638 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart @@ -3,11 +3,13 @@ import 'dart:io' show IOSink, stdout; import 'package:config/config.dart'; import '../better_command.dart'; +import 'carapace_completion.dart'; import 'completely_generator.dart'; import 'usage_representation.dart'; enum CompletionTarget { completely, + carapace, } enum CompletionOption implements OptionDefinition { @@ -15,7 +17,6 @@ enum CompletionOption implements OptionDefinition { enumParser: EnumParser(CompletionTarget.values), argName: 'target', argAbbrev: 't', - defaultsTo: CompletionTarget.completely, )), execName(StringOption( argName: 'exec-name', @@ -66,6 +67,9 @@ class CompletionCommand extends BetterCommand { case CompletionTarget.completely: CompletelyYamlGenerator().generate(out, usage); break; + case CompletionTarget.carapace: + CarapaceYamlGenerator().generate(out, usage); + break; } if (file != null) { From 11a152f7dfc85acfbfb105531721949ddbaca12d Mon Sep 17 00:00:00 2001 From: Christer Date: Mon, 22 Sep 2025 15:06:56 +0200 Subject: [PATCH 05/12] fix: Made --target option mandatory --- .../better_command_runner/completion/completion_command.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart index c37a638..a0804eb 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart @@ -17,6 +17,8 @@ enum CompletionOption implements OptionDefinition { enumParser: EnumParser(CompletionTarget.values), argName: 'target', argAbbrev: 't', + helpText: 'The target tool format', + mandatory: true, )), execName(StringOption( argName: 'exec-name', From 1a1c539fbac542150f39340e0f623d9336928a8a Mon Sep 17 00:00:00 2001 From: Christer Date: Sat, 27 Sep 2025 12:03:17 +0200 Subject: [PATCH 06/12] refactor: Renamed carapace_generator.dart --- .../{carapace_completion.dart => carapace_generator.dart} | 0 .../better_command_runner/completion/completion_command.dart | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/cli_tools/lib/src/better_command_runner/completion/{carapace_completion.dart => carapace_generator.dart} (100%) diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/carapace_completion.dart b/packages/cli_tools/lib/src/better_command_runner/completion/carapace_generator.dart similarity index 100% rename from packages/cli_tools/lib/src/better_command_runner/completion/carapace_completion.dart rename to packages/cli_tools/lib/src/better_command_runner/completion/carapace_generator.dart diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart index a0804eb..36a28ed 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart @@ -3,7 +3,7 @@ import 'dart:io' show IOSink, stdout; import 'package:config/config.dart'; import '../better_command.dart'; -import 'carapace_completion.dart'; +import 'carapace_generator.dart'; import 'completely_generator.dart'; import 'usage_representation.dart'; From f4defafecd6d3f18314c4ebbac5e895516d4e520 Mon Sep 17 00:00:00 2001 From: Christer Date: Sat, 27 Sep 2025 12:04:04 +0200 Subject: [PATCH 07/12] fix(cli_tools): Fixed omission in completion representation --- .../completion/usage_representation.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart b/packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart index b833861..3db72fa 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/usage_representation.dart @@ -82,7 +82,10 @@ abstract class UsageRepresentation { ) { return options .where((final o) => !o.option.hide) - .where((final o) => o.option.argName != null || o.option.argPos != null) + .where((final o) => + o.option.argName != null || + o.option.argAbbrev != null || + o.option.argPos != null) .toList(); } } From e31570537044b6959e2e9e15f6bd90a825154e97 Mon Sep 17 00:00:00 2001 From: Christer Date: Sat, 27 Sep 2025 12:44:35 +0200 Subject: [PATCH 08/12] fix(cli_tools): Fixed pattern bug in completely generator --- .../completion/completely_generator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart index 954db7d..4b67c1c 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completely_generator.dart @@ -99,7 +99,7 @@ class CompletelyYamlGenerator implements UsageRepresentationGenerator { out, commandSequence, option, - ' --$argName', + '--$argName', ); } if (option.argAbbrev case final String argAbbrev) { @@ -107,7 +107,7 @@ class CompletelyYamlGenerator implements UsageRepresentationGenerator { out, commandSequence, option, - ' -$argAbbrev', + '-$argAbbrev', ); } } From 1c32c64fbe72168963c7ca6a9b8db22ca6ca43c6 Mon Sep 17 00:00:00 2001 From: Christer Date: Sat, 27 Sep 2025 13:37:30 +0200 Subject: [PATCH 09/12] test(cli_tools): Added completion subcommand tests --- .../completion_test.dart | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 packages/cli_tools/test/better_command_runner/completion_test.dart diff --git a/packages/cli_tools/test/better_command_runner/completion_test.dart b/packages/cli_tools/test/better_command_runner/completion_test.dart new file mode 100644 index 0000000..1c3296b --- /dev/null +++ b/packages/cli_tools/test/better_command_runner/completion_test.dart @@ -0,0 +1,210 @@ +import 'package:cli_tools/better_command_runner.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + test( + 'Given a BetterCommandRunner without enabling experimental completion feature' + ' when running base command with --help flag' + ' then global usage does not include "completion" command', () async { + final infos = []; + final messageOutput = MessageOutput( + usageLogger: (final u) => infos.add(u), + ); + final runner = BetterCommandRunner( + 'test', + 'test project', + messageOutput: messageOutput, + ); + + await runner.run(['--help']); + + expect(infos, hasLength(1)); + expect( + infos.single, + isNot(stringContainsInOrder([ + 'Generate a command line completion specification', + ])), + ); + }); + + group( + 'Given a BetterCommandRunner with experimental completion feature enabled', + () { + final infos = []; + final messageOutput = MessageOutput( + usageLogger: (final u) => infos.add(u), + ); + + final runner = BetterCommandRunner( + 'test', + 'test project', + messageOutput: messageOutput, + experimentalCompletionCommand: true, + ); + + setUp(() { + infos.clear(); + }); + + test( + 'when running base command with --help flag ' + 'then global usage includes "completion" command', () async { + await runner.run(['--help']); + + expect(infos, hasLength(1)); + expect( + infos.single, + stringContainsInOrder([ + 'Usage: test [arguments]', + 'Global options:', + '-h, --help', + 'Available commands:', + 'completion', + 'Generate a command line completion specification', + ]), + ); + }); + + test( + 'when running subcommand "completion -t completely -f " ' + 'then a proper completely specification is written to the file', + () async { + await d.dir('test-dir').create(); + final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); + + await runner.run(['completion', '-t', 'completely', '-f', filePath]); + + final spec = d.file( + filePath, + stringContainsInOrder([ + 'test:', + ' - completion', + ' - --quiet', + ' - -q', + ' - --verbose', + ' - -v', + 'test completion*--target:', + ' - completely', + ' - carapace', + 'test completion*-t:', + ' - completely', + ' - carapace', + 'test completion*--file:', + ' - ', + 'test completion*-f:', + ' - ', + ])); + await expectLater(spec.validate(), completes); + }); + + test( + 'when running subcommand "completion -t completely -e other-exec-name -f " ' + 'then the completely specification contains other-exec-name', () async { + await d.dir('test-dir').create(); + final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); + + await runner.run([ + 'completion', + '-t', + 'completely', + '-e', + 'other-exec-name', + '-f', + filePath, + ]); + + final spec = d.file( + filePath, + stringContainsInOrder([ + 'other-exec-name:', + ' - completion', + ' - --quiet', + ' - -q', + ' - --verbose', + ' - -v', + 'other-exec-name completion*--target:', + ' - completely', + ' - carapace', + 'other-exec-name completion*-t:', + ' - completely', + ' - carapace', + 'other-exec-name completion*--file:', + ' - ', + 'other-exec-name completion*-f:', + ' - ', + ])); + await expectLater(spec.validate(), completes); + }); + + test( + 'when running subcommand "completion -t carapace -f " ' + 'then a proper carapace specification is written to the file', + () async { + await d.dir('test-dir').create(); + final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); + + await runner.run(['completion', '-t', 'carapace', '-f', filePath]); + + final spec = d.file( + filePath, + stringContainsInOrder([ + r'# yaml-language-server: $schema=https://carapace.sh/schemas/command.json', + 'name: test', + 'persistentFlags:', + ' -q, --quiet: Suppress all cli output. Is overridden by -v, --verbose.', + ' -v, --verbose: Prints additional information useful for development. Overrides --q, --quiet.', + 'commands:', + ' - name: completion', + ' flags:', + ' -t, --target=!: The target tool format', + ' -e, --exec-name=: Override the name of the executable', + ' -f, --file=: Write the specification to a file instead of stdout', + ' completion:', + ' flag:', + ' target: ["completely", "carapace"]', + r' file: ["$files"]', + ])); + await expectLater(spec.validate(), completes); + }); + + test( + 'when running subcommand "completion -t carapace -e other-exec-name -f " ' + 'then the carapace specification contains other-exec-name', () async { + await d.dir('test-dir').create(); + final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); + + await runner.run([ + 'completion', + '-t', + 'carapace', + '-e', + 'other-exec-name', + '-f', + filePath, + ]); + + final spec = d.file( + filePath, + stringContainsInOrder([ + r'# yaml-language-server: $schema=https://carapace.sh/schemas/command.json', + 'name: other-exec-name', + 'persistentFlags:', + ' -q, --quiet: Suppress all cli output. Is overridden by -v, --verbose.', + ' -v, --verbose: Prints additional information useful for development. Overrides --q, --quiet.', + 'commands:', + ' - name: completion', + ' flags:', + ' -t, --target=!: The target tool format', + ' -e, --exec-name=: Override the name of the executable', + ' -f, --file=: Write the specification to a file instead of stdout', + ' completion:', + ' flag:', + ' target: ["completely", "carapace"]', + r' file: ["$files"]', + ])); + await expectLater(spec.validate(), completes); + }); + }); +} From baaf40aa323205b829518e68e9afe04b103fdb51 Mon Sep 17 00:00:00 2001 From: Christer Date: Sat, 27 Sep 2025 21:25:28 +0200 Subject: [PATCH 10/12] refactor(cli_tools): Organized completion as parent/subcommand --- .../completion/completion_command.dart | 74 ++++--------------- .../completion_generate_command.dart | 72 ++++++++++++++++++ .../completion/completion_target.dart | 4 + .../completion_test.dart | 46 ++++++++---- 4 files changed, 122 insertions(+), 74 deletions(-) create mode 100644 packages/cli_tools/lib/src/better_command_runner/completion/completion_generate_command.dart create mode 100644 packages/cli_tools/lib/src/better_command_runner/completion/completion_target.dart diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart index 36a28ed..b5592b5 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart @@ -1,83 +1,39 @@ -import 'dart:io' show IOSink, stdout; +import 'dart:async'; import 'package:config/config.dart'; import '../better_command.dart'; -import 'carapace_generator.dart'; -import 'completely_generator.dart'; -import 'usage_representation.dart'; +import 'completion_generate_command.dart'; +import 'completion_target.dart'; -enum CompletionTarget { - completely, - carapace, -} - -enum CompletionOption implements OptionDefinition { - target(EnumOption( +abstract final class CompletionOptions { + static const targetOption = EnumOption( enumParser: EnumParser(CompletionTarget.values), argName: 'target', argAbbrev: 't', helpText: 'The target tool format', mandatory: true, - )), - execName(StringOption( + ); + static const execNameOption = StringOption( argName: 'exec-name', argAbbrev: 'e', helpText: 'Override the name of the executable', - )), - file(FileOption( - argName: 'file', - argAbbrev: 'f', - helpText: 'Write the specification to a file instead of stdout', - )); - - const CompletionOption(this.option); - - @override - final ConfigOptionBase option; + ); } -class CompletionCommand extends BetterCommand { - CompletionCommand() : super(options: CompletionOption.values); +class CompletionCommand extends BetterCommand { + CompletionCommand() { + addSubcommand(CompletionGenerateCommand()); + } @override String get name => 'completion'; @override - String get description => 'Generate a command line completion specification'; + String get description => 'Command line completion commands'; @override - Future runWithConfig( - final Configuration commandConfig) async { - final target = commandConfig.value(CompletionOption.target); - final execName = commandConfig.optionalValue(CompletionOption.execName); - final file = commandConfig.optionalValue(CompletionOption.file); - - final betterRunner = runner; - if (betterRunner == null) { - throw Exception('BetterCommandRunner not set in the completion command'); - } - - final usage = UsageRepresentation.compile( - betterRunner, - execNameOverride: execName, - ); - - final IOSink out = file?.openWrite() ?? stdout; - - switch (target) { - case CompletionTarget.completely: - CompletelyYamlGenerator().generate(out, usage); - break; - case CompletionTarget.carapace: - CarapaceYamlGenerator().generate(out, usage); - break; - } - - if (file != null) { - await out.flush(); - await out.close(); - } - return null as T; + FutureOr? runWithConfig(final Configuration _) { + return null; } } diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_generate_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_generate_command.dart new file mode 100644 index 0000000..bbe243d --- /dev/null +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_generate_command.dart @@ -0,0 +1,72 @@ +import 'dart:io' show IOSink, stdout; + +import 'package:config/config.dart'; + +import '../better_command.dart'; +import 'carapace_generator.dart'; +import 'completely_generator.dart'; +import 'completion_command.dart' show CompletionOptions; +import 'completion_target.dart'; +import 'usage_representation.dart'; + +enum CompletionGenerateOption implements OptionDefinition { + target(CompletionOptions.targetOption), + execName(CompletionOptions.execNameOption), + file(FileOption( + argName: 'file', + argAbbrev: 'f', + helpText: 'Write the specification to a file instead of stdout', + )); + + const CompletionGenerateOption(this.option); + + @override + final ConfigOptionBase option; +} + +class CompletionGenerateCommand + extends BetterCommand { + CompletionGenerateCommand() : super(options: CompletionGenerateOption.values); + + @override + String get name => 'generate'; + + @override + String get description => 'Generate a command line completion specification'; + + @override + Future runWithConfig( + final Configuration commandConfig) async { + final target = commandConfig.value(CompletionGenerateOption.target); + final execName = + commandConfig.optionalValue(CompletionGenerateOption.execName); + final file = commandConfig.optionalValue(CompletionGenerateOption.file); + + final betterRunner = runner; + if (betterRunner == null) { + throw Exception('BetterCommandRunner not set in the completion command'); + } + + final usage = UsageRepresentation.compile( + betterRunner, + execNameOverride: execName, + ); + + final IOSink out = file?.openWrite() ?? stdout; + + switch (target) { + case CompletionTarget.completely: + CompletelyYamlGenerator().generate(out, usage); + break; + case CompletionTarget.carapace: + CarapaceYamlGenerator().generate(out, usage); + break; + } + + if (file != null) { + await out.flush(); + await out.close(); + } + return null as T; + } +} diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_target.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_target.dart new file mode 100644 index 0000000..212c716 --- /dev/null +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_target.dart @@ -0,0 +1,4 @@ +enum CompletionTarget { + completely, + carapace, +} diff --git a/packages/cli_tools/test/better_command_runner/completion_test.dart b/packages/cli_tools/test/better_command_runner/completion_test.dart index 1c3296b..3bc5c49 100644 --- a/packages/cli_tools/test/better_command_runner/completion_test.dart +++ b/packages/cli_tools/test/better_command_runner/completion_test.dart @@ -62,19 +62,26 @@ void main() { '-h, --help', 'Available commands:', 'completion', - 'Generate a command line completion specification', + 'Command line completion commands', ]), ); }); test( - 'when running subcommand "completion -t completely -f " ' + 'when running subcommand "completion generate -t completely -f " ' 'then a proper completely specification is written to the file', () async { await d.dir('test-dir').create(); final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); - await runner.run(['completion', '-t', 'completely', '-f', filePath]); + await runner.run([ + 'completion', + 'generate', + '-t', + 'completely', + '-f', + filePath, + ]); final spec = d.file( filePath, @@ -85,28 +92,29 @@ void main() { ' - -q', ' - --verbose', ' - -v', - 'test completion*--target:', + 'test completion generate*--target:', ' - completely', ' - carapace', - 'test completion*-t:', + 'test completion generate*-t:', ' - completely', ' - carapace', - 'test completion*--file:', + 'test completion generate*--file:', ' - ', - 'test completion*-f:', + 'test completion generate*-f:', ' - ', ])); await expectLater(spec.validate(), completes); }); test( - 'when running subcommand "completion -t completely -e other-exec-name -f " ' + 'when running subcommand "completion generate -t completely -e other-exec-name -f " ' 'then the completely specification contains other-exec-name', () async { await d.dir('test-dir').create(); final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); await runner.run([ 'completion', + 'generate', '-t', 'completely', '-e', @@ -124,28 +132,35 @@ void main() { ' - -q', ' - --verbose', ' - -v', - 'other-exec-name completion*--target:', + 'other-exec-name completion generate*--target:', ' - completely', ' - carapace', - 'other-exec-name completion*-t:', + 'other-exec-name completion generate*-t:', ' - completely', ' - carapace', - 'other-exec-name completion*--file:', + 'other-exec-name completion generate*--file:', ' - ', - 'other-exec-name completion*-f:', + 'other-exec-name completion generate*-f:', ' - ', ])); await expectLater(spec.validate(), completes); }); test( - 'when running subcommand "completion -t carapace -f " ' + 'when running subcommand "completion generate -t carapace -f " ' 'then a proper carapace specification is written to the file', () async { await d.dir('test-dir').create(); final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); - await runner.run(['completion', '-t', 'carapace', '-f', filePath]); + await runner.run([ + 'completion', + 'generate', + '-t', + 'carapace', + '-f', + filePath, + ]); final spec = d.file( filePath, @@ -170,13 +185,14 @@ void main() { }); test( - 'when running subcommand "completion -t carapace -e other-exec-name -f " ' + 'when running subcommand "completion generate -t carapace -e other-exec-name -f " ' 'then the carapace specification contains other-exec-name', () async { await d.dir('test-dir').create(); final filePath = p.join(d.sandbox, 'test-dir', 'test.yaml'); await runner.run([ 'completion', + 'generate', '-t', 'carapace', '-e', From 1392e6fba00d486fbe99a7023b92a80c92c69f18 Mon Sep 17 00:00:00 2001 From: Christer Date: Sat, 27 Sep 2025 21:39:39 +0200 Subject: [PATCH 11/12] docs(cli_tools): Updated READMEs --- packages/cli_tools/README.md | 6 +++ packages/cli_tools/README_completion.md | 63 ++++++++++++++++++++----- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/packages/cli_tools/README.md b/packages/cli_tools/README.md index 4367ea2..6a5f26e 100644 --- a/packages/cli_tools/README.md +++ b/packages/cli_tools/README.md @@ -9,6 +9,12 @@ These tools were developed for the Serverpod CLI but can be used in any Dart pro The `config` library has been moved into its own package, published on pub.dev as [config](https://pub.dev/packages/config). +## Command line completion + +`BetterCommandRunner` has support for command-line completion, which is currently +an experimental feature and requires installing an additional tool. See: +[README_completion.md](README_completion.md) + ## Contributing to the Project We are happy to accept contributions. To contribute, please do the following: diff --git a/packages/cli_tools/README_completion.md b/packages/cli_tools/README_completion.md index d31b7b1..a97ed72 100644 --- a/packages/cli_tools/README_completion.md +++ b/packages/cli_tools/README_completion.md @@ -1,7 +1,12 @@ -## Command line completion for scloud +## Command line completion -There is now an experimental feature to generate and install command line -completion in bash for commands using `BetterCommandRunner`. +`BetterCommandRunner` can generate and install command line completion +in bash and some other shells for all its subcommands and options. + +As command developer you will need to install an additional tool that generates +the completion shell script. Two tools are currently supported. + +The shell script needs to be installed by end users to enable completion. ### Enable experimental feature @@ -12,6 +17,8 @@ with the flag `experimentalCompletionCommand` set to `true`. Carapace supports a lot of shells, including Bash, ZSH, Fish, Elvish, Powershell, Cmd, and more. +However end users need to install the `carapace` tool as well as the generated +script for the command. https://carapace.sh/ @@ -33,10 +40,13 @@ Note that the YAML file must have the same name as the command executable before the `.yaml` extension. ```sh -my-command completion -f my-command.yaml -t carapace -cp example.yaml "${UserConfigDir}/carapace/specs/" +my-command completion generate -f my-command.yaml -t carapace +cp my-command.yaml "${UserConfigDir}/carapace/specs/" ``` +If you need to specify a specific command name to use in the generated YAML +file, use the `-e` option. + > Note: ${UserConfigDir} refers to the platform-specific user configuration directory. On MacOS this is `~/Library/Application Support/` (even though many other Bash commands use `~/.local/share/`), and on Windows this is `%APPDATA%`. @@ -69,11 +79,15 @@ location, even if the Yaml file is distributed with the command. ## Using the tool `completely` +Completely supports Bash and ZSH. It's benefit is that it doesn't require the +end user to install any tool besides the generated shell completion script. + https://github.com/bashly-framework/completely ### Prerequisites -This requires the tool `completely`, which also requires ruby to be installed. +This requires the command developer to install the tool `completely`, +which also requires ruby to be installed. ```sh gem install completely @@ -86,19 +100,26 @@ brew install brew-gem brew gem install completely ``` -### Install completion for the command +### Generate completion for the command -When the `completely` tool is installed, run e.g: +When the `completely` tool is installed, run commands similar to the following +to generate the bash completion script for the command. ```sh -my-command completion -f my-command.yaml -t completely +my-command completion generate -f my-command.yaml -t completely completely generate my-command.yaml my-command.bash +``` + +To install the completions in the bash shell, run: + +```sh mkdir -p ~/.local/share/bash-completion/completions cp my-command.bash ~/.local/share/bash-completion/completions/ ``` -This will write the completions script to `~/.local/share/bash-completion/completions/`, -where bash picks it up automatically on start. +Completions scripts in `~/.local/share/bash-completion/completions/` +are automatically picked up by bash on start. Note that they must be named +the same as the command, except for the `.bash` suffix. In order to update the completions in the current bash shell, run: @@ -106,6 +127,26 @@ In order to update the completions in the current bash shell, run: exec bash ``` +If the completions directory already exists, you can update the completion +script with this one-liner which doesn't create the intermediate files. +(This approach requires that your command does not generate any other output +to `stdout` when run this way.) + +```sh +my-command completion generate -t completely | completely generate - >~/.local/share/bash-completion/completions/my-command.bash +``` + +### ZSH + +If you are using Oh-My-Zsh, bash completions should already be enabled. +Otherwise you should enable completion by adding this to your ~/.zshrc +(if is it not already there): + +```sh +autoload -Uz +X compinit && compinit +autoload -Uz +X bashcompinit && bashcompinit +``` + ### Distribution For end users, the generated bash script can be distributed as a file for them From ca6dbe3b691456a4a5a49e998c869ee453e5def8 Mon Sep 17 00:00:00 2001 From: Christer Date: Sat, 27 Sep 2025 21:42:43 +0200 Subject: [PATCH 12/12] fix(cli_tools): Fixed parameter name lint info --- .../better_command_runner/completion/completion_command.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart index b5592b5..6bcbf83 100644 --- a/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart +++ b/packages/cli_tools/lib/src/better_command_runner/completion/completion_command.dart @@ -33,7 +33,7 @@ class CompletionCommand extends BetterCommand { String get description => 'Command line completion commands'; @override - FutureOr? runWithConfig(final Configuration _) { + FutureOr? runWithConfig(final Configuration commandConfig) { return null; } }