Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ When `enableAutoInstall` is set to false, users will have to call `install-compl
$ example_cli install-completion-files
```

Additionally, when `enableAutoInstall` is disabled a `completion-script` command is exposed. It prints the completion script for the current shell to stdout, so users can install it wherever they prefer:

```bash
$ example_cli completion-script >> ~/.zshrc
```

## Documentation 📝

For an overview of how this package works, check out the [documentation][docs_link].
Expand Down
2 changes: 1 addition & 1 deletion doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ We call this process [parsing](#how-parsing-completion-works).

The class `CompletionCommandRunner` tries to create these files upon any command run. It does nothing if the completion files exist and displays a short error message if there is an error in the process.

To disable this behavior, set `enableAutoInstall` to false on your `CompletionCommandRunner` subclass.
To disable this behavior, set `enableAutoInstall` to false on your `CompletionCommandRunner` subclass. In that case, users can install the completion files manually with the `install-completion-files` command, or print the completion script for the current shell with the `completion-script` command (e.g. `example_cli completion-script >> ~/.zshrc`), which is only available while auto installation is disabled.

### How Parsing Completion Works

Expand Down
1 change: 1 addition & 0 deletions lib/src/command_runner/commands/commands.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'handle_completion_command.dart';
export 'install_completion_files_command.dart';
export 'print_completion_script_command.dart';
export 'uninstall_completion_files_command.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'dart:async';

import 'package:args/command_runner.dart';
import 'package:cli_completion/cli_completion.dart';

/// {@template print_completion_script_command}
/// A [Command] added by [CompletionCommandRunner] only when auto installation
/// is disabled that prints the completion script for the current shell to
/// stdout.
///
/// It allows users to install the completion script manually by piping the
/// output into their shell configuration file, for example:
/// ```sh
/// my_cli completion-script >> ~/.zshrc
/// ```
///
/// This mirrors the approach used by other CLIs such as npm and the GitHub CLI.
/// {@endtemplate}
class PrintCompletionScriptCommand<T> extends Command<T> {
/// {@macro print_completion_script_command}
PrintCompletionScriptCommand();

@override
String get description {
return 'Prints the completion script for the current shell to stdout.';
}

/// The string that the user can call to print the completion script.
static const commandName = 'completion-script';

@override
String get name => commandName;

@override
CompletionCommandRunner<T> get runner {
return super.runner! as CompletionCommandRunner<T>;
}

@override
FutureOr<T>? run() {
runner.printCompletionScript();
return null;
}
}
30 changes: 30 additions & 0 deletions lib/src/command_runner/completion_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import 'package:meta/meta.dart';
///
/// Adds [InstallCompletionFilesCommand] to enable the user to
/// manually install completion files.
///
/// When [enableAutoInstall] is disabled, it also adds
/// [PrintCompletionScriptCommand] so the user can print the completion script
/// and install it manually.
abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
/// {@macro completion_command_runner}
CompletionCommandRunner(
Expand All @@ -31,6 +35,13 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
addCommand(HandleCompletionRequestCommand<T>());
addCommand(InstallCompletionFilesCommand<T>());
addCommand(UnistallCompletionFilesCommand<T>());

// The print completion script command is only useful when the completion
// files are not installed automatically. Otherwise, users should rely on
// the auto installation (or the `install-completion-files` command).
if (!enableAutoInstall) {
addCommand(PrintCompletionScriptCommand<T>());
}
}

/// The [Logger] used to prompt the completion suggestions.
Expand Down Expand Up @@ -102,6 +113,25 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
}
}

/// Prints the completion script for the current shell to stdout.
///
/// This is used by [PrintCompletionScriptCommand] to allow users to install
/// the completion script manually, for example:
/// ```sh
/// my_cli completion-script >> ~/.zshrc
/// ```
@internal
void printCompletionScript() {
try {
final script = completionInstallation.completionScriptFor(executableName);
completionLogger.info(script);
} on CompletionInstallationException catch (e) {
completionInstallationLogger.warn(e.toString());
} on Exception catch (e) {
completionInstallationLogger.err(e.toString());
}
}

/// Tries to uninstall completion files for the current shell.
@internal
void tryUninstallCompletionFiles(Level level) {
Expand Down
24 changes: 24 additions & 0 deletions lib/src/installer/completion_installation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,30 @@ class CompletionInstallation {
.writeTo(completionConfigurationFile);
}

/// Returns the completion script for the [rootCommand] on the current shell
/// without writing it to any file.
///
/// This can be used to print the completion script to stdout so that a user
/// can source it manually, for example:
/// ```sh
/// my_cli completion-script >> ~/.zshrc
/// ```
///
/// Throws a [CompletionInstallationException] if the current shell is
/// unknown.
String completionScriptFor(String rootCommand) {
final configuration = this.configuration;

if (configuration == null) {
throw CompletionInstallationException(
message: 'Unknown shell.',
rootCommand: rootCommand,
);
}

return configuration.scriptTemplate(rootCommand);
}

/// Wether the completion configuration files for a [rootCommand] should be
/// installed or not.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import 'package:cli_completion/cli_completion.dart';
import 'package:cli_completion/installer.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class _MockLogger extends Mock implements Logger {}

class _MockCompletionInstallation extends Mock
implements CompletionInstallation {}

class _TestCompletionCommandRunner extends CompletionCommandRunner<int> {
_TestCompletionCommandRunner() : super('test', 'Test command runner');

@override
bool get enableAutoInstall => false;

@override
// Override acceptable for test files
// ignore: overridden_fields
final Logger completionLogger = _MockLogger();

@override
// Override acceptable for test files
// ignore: overridden_fields
final Logger completionInstallationLogger = _MockLogger();

@override
final CompletionInstallation completionInstallation =
_MockCompletionInstallation();
}

void main() {
group('PrintCompletionScriptCommand', () {
late _TestCompletionCommandRunner commandRunner;

setUp(() {
commandRunner = _TestCompletionCommandRunner();
});

test('can be instantiated', () {
expect(PrintCompletionScriptCommand<int>(), isNotNull);
});

test('is not hidden', () {
expect(PrintCompletionScriptCommand<int>().hidden, isFalse);
});

test('description', () {
expect(
PrintCompletionScriptCommand<int>().description,
'Prints the completion script for the current shell to stdout.',
);
});

group('completion-script', () {
test('prints the completion script to stdout', () async {
when(
() => commandRunner.completionInstallation.completionScriptFor(
commandRunner.executableName,
),
).thenReturn('some completion script');

await commandRunner.run(['completion-script']);

verify(
() => commandRunner.completionLogger.info('some completion script'),
).called(1);
});

test(
'logs a warning when it throws a CompletionInstallationException',
() async {
when(
() => commandRunner.completionInstallation.completionScriptFor(
commandRunner.executableName,
),
).thenThrow(
CompletionInstallationException(
message: 'oops',
rootCommand: 'test',
),
);

await commandRunner.run(['completion-script']);

verify(
() => commandRunner.completionInstallationLogger.warn(any()),
).called(1);
},
);

test(
'logs an error when an unknown exception happens',
() async {
when(
() => commandRunner.completionInstallation.completionScriptFor(
commandRunner.executableName,
),
).thenThrow(Exception('oops'));

await commandRunner.run(['completion-script']);

verify(
() => commandRunner.completionInstallationLogger.err(any()),
).called(1);
},
);
});
});
}
35 changes: 35 additions & 0 deletions test/src/command_runner/completion_command_runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ class _TestCompletionCommandRunner extends CompletionCommandRunner<int> {
mockCompletionInstallation ?? super.completionInstallation;
}

class _TestNoAutoInstallCompletionCommandRunner
extends CompletionCommandRunner<int> {
_TestNoAutoInstallCompletionCommandRunner()
: super('test', 'Test command runner');

@override
bool get enableAutoInstall => false;
}

class _TestUserCommand extends Command<int> {
@override
String get description => 'some command';
Expand Down Expand Up @@ -114,6 +123,32 @@ void main() {
);
});

group('print completion script command', () {
test(
'is not added when auto install is enabled',
() {
final commandRunner = _TestCompletionCommandRunner();

expect(
commandRunner.commands.keys,
isNot(contains('completion-script')),
);
},
);

test(
'is added when auto install is disabled',
() {
final commandRunner = _TestNoAutoInstallCompletionCommandRunner();

expect(
commandRunner.commands.keys,
contains('completion-script'),
);
},
);
});

group('auto install', () {
test('Tries to install completion files on test subcommand', () async {
final commandRunner = _TestCompletionCommandRunner()
Expand Down
50 changes: 50 additions & 0 deletions test/src/installer/completion_installation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,56 @@ void main() {
});
});

group('completionScriptFor', () {
test('returns the completion script for the given shell', () {
final zshInstallation = CompletionInstallation(
configuration: zshConfiguration,
logger: logger,
isWindows: false,
environment: {
'HOME': tempDir.path,
},
);

expect(
zshInstallation.completionScriptFor('very_good'),
zshConfiguration.scriptTemplate('very_good'),
);

final bashInstallation = CompletionInstallation(
configuration: bashConfiguration,
logger: logger,
isWindows: false,
environment: {
'HOME': tempDir.path,
},
);

expect(
bashInstallation.completionScriptFor('very_good'),
bashConfiguration.scriptTemplate('very_good'),
);
});

test('throws when the shell is unknown', () {
final installation = CompletionInstallation.fromSystemShell(
systemShell: null,
logger: logger,
);

expect(
() => installation.completionScriptFor('very_good'),
throwsA(
isA<CompletionInstallationException>().having(
(e) => e.message,
'message',
'Unknown shell.',
),
),
);
});
});

group('install', () {
test('createCompletionConfigDir', () {
final installation = CompletionInstallation(
Expand Down