Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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> {
Comment thread
marcossevilla marked this conversation as resolved.
/// {@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(
Comment thread
marcossevilla marked this conversation as resolved.
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