Skip to content
Closed
4 changes: 1 addition & 3 deletions lib/installer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@
/// {@canonicalFor system_shell.SystemShell}
library installer;

export 'src/installer/completion_installation.dart';
export 'src/installer/exceptions.dart';
export 'src/installer/shell_completion_configuration.dart';
export 'src/installer/installer.dart';
export 'src/system_shell.dart';
179 changes: 74 additions & 105 deletions lib/src/installer/completion_installation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class CompletionInstallation {
/// shell. It is null if the current shell is unknown.
final ShellCompletionConfiguration? configuration;

/// {@template completion_config_dir}
/// Define the [Directory] in which the
/// completion configuration files will be stored.
///
Expand All @@ -73,6 +74,7 @@ class CompletionInstallation {
///
/// If [isWindows] is false, it will return the directory defined by
/// $XDG_CONFIG_HOME/.dart_cli_completion or $HOME/.dart_cli_completion
/// {@endtemplate}
@visibleForTesting
Directory get completionConfigDir {
if (isWindows) {
Expand All @@ -90,39 +92,39 @@ class CompletionInstallation {
}
}

/// Install completion configuration files for a [rootCommand] in the
/// Install completion configuration files for a [executableName] in the
/// current shell.
///
/// It will create:
/// - A completion script file in [completionConfigDir] that is named after
/// the [rootCommand] and the current shell (e.g. `very_good.bash`).
/// the [executableName] and the current shell (e.g. `very_good.bash`).
/// - A config file in [completionConfigDir] that is named after the current
/// shell (e.g. `bash-config.bash`) that sources the aforementioned
/// completion script file.
/// - A line in the shell config file (e.g. `.bash_profile`) that sources
/// the aforementioned config file.
void install(String rootCommand) {
void install(String executableName) {
final configuration = this.configuration;

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

logger.detail(
'Installing completion for the command $rootCommand '
'on ${configuration.name}',
'''Installing completion for the command $executableName on ${configuration.name}''',
);

createCompletionConfigDir();
final completionFileCreated = writeCompletionScriptForCommand(rootCommand);
writeCompletionConfigForShell(rootCommand);
writeToShellConfigFile(rootCommand);
final completionFileCreated =
writeCompletionScriptForExecutable(executableName);
writeCompletionConfigForShell(executableName);
writeToShellConfigFile(executableName);

if (completionFileCreated) {
_logSourceInstructions(rootCommand);
_logSourceInstructions(executableName);
}
}

Expand All @@ -135,8 +137,7 @@ class CompletionInstallation {
final completionConfigDirPath = completionConfigDir.path;

logger.info(
'Creating completion configuration directory '
'at $completionConfigDirPath',
'''Creating completion configuration directory at $completionConfigDirPath''',
);

if (completionConfigDir.existsSync()) {
Expand All @@ -149,10 +150,10 @@ class CompletionInstallation {
completionConfigDir.createSync();
}

/// Creates a configuration file exclusively to [rootCommand] and the
/// Creates a configuration file exclusively to [executableName] and the
/// identified shell.
///
/// The file will be named after the [rootCommand] and the current shell
/// The file will be named after the [executableName] and the current shell
/// (e.g. `very_good.bash`).
///
/// The file will be created in [completionConfigDir].
Expand All @@ -161,69 +162,72 @@ class CompletionInstallation {
///
/// Returns true if the file was created, false otherwise.
@visibleForTesting
bool writeCompletionScriptForCommand(String rootCommand) {
bool writeCompletionScriptForExecutable(String executableName) {
final configuration = this.configuration!;
final completionConfigDirPath = completionConfigDir.path;
final commandScriptName = '$rootCommand.${configuration.name}';
final commandScriptPath = path.join(
completionConfigDirPath,
commandScriptName,
);
final executableCompletionScriptFile =
ExecutableCompletionConfiguration.fromShellConfiguration(
executabelName: executableName,
shellConfiguration: configuration,
).completionScriptFile(completionConfigDir);

logger.info(
'Writing completion script for $rootCommand on $commandScriptPath',
'''Writing completion script for $executableName on ${executableCompletionScriptFile.path}''',
);

final scriptFile = File(commandScriptPath);

if (scriptFile.existsSync()) {
if (executableCompletionScriptFile.existsSync()) {
logger.warn(
'A script file for $rootCommand was already found on '
'$commandScriptPath.',
'''A script file for $executableName was already found on ${executableCompletionScriptFile.path}''',
);
return false;
}

scriptFile.writeAsStringSync(configuration.scriptTemplate(rootCommand));

executableCompletionScriptFile.writeAsStringSync(
configuration.scriptTemplate(executableName),
);
return true;
}

/// Adds a reference for the command-specific config file created on
/// [writeCompletionScriptForCommand] the the global completion config file.
/// Adds a reference for the executable-specific config file created on
/// [writeCompletionScriptForExecutable] the the global completion config
/// file.
@visibleForTesting
void writeCompletionConfigForShell(String rootCommand) {
void writeCompletionConfigForShell(String executableName) {
final configuration = this.configuration!;
final completionConfigDirPath = completionConfigDir.path;
final shellCompletionConfig =
configuration.completionScriptFile(completionConfigDir);

final configPath = path.join(
completionConfigDirPath,
configuration.completionConfigForShellFileName,
logger.info(
'''Adding config for $executableName config entry to ${shellCompletionConfig.path}''',
);
logger.info('Adding config for $rootCommand config entry to $configPath');

final configFile = File(configPath);

if (!configFile.existsSync()) {
logger.info('No file found at $configPath, creating one now');
configFile.createSync();
if (!shellCompletionConfig.existsSync()) {
logger.info(
'''No file found at ${shellCompletionConfig.path}, creating one now''',
);
shellCompletionConfig.createSync();
}
final commandScriptName = '$rootCommand.${configuration.name}';

final containsLine =
configFile.readAsStringSync().contains(commandScriptName);
final executable = ExecutableCompletionConfiguration.fromShellConfiguration(
executabelName: executableName,
shellConfiguration: configuration,
);
final executableEntry = executable.entry;

if (containsLine) {
if (executableEntry.existsIn(shellCompletionConfig)) {
logger.warn(
'A config entry for $rootCommand was already found on $configPath.',
'''A config entry for $executableName was already found on ${shellCompletionConfig.path}.''',
);
return;
}

_sourceScriptOnFile(
configFile: configFile,
scriptName: rootCommand,
scriptPath: path.join(completionConfigDirPath, commandScriptName),
final executableScriptFile = executable.completionScriptFile(
completionConfigDir,
);
final content = configuration.completionReferenceTemplate(
executableName: executableName,
executableScriptFilePath: executableScriptFile.path,
);
executableEntry.appendTo(shellCompletionConfig, content: content);
logger.info('Added config to ${shellCompletionConfig.path}');
}

String get _shellRCFilePath =>
Expand All @@ -232,53 +236,47 @@ class CompletionInstallation {
/// Write a source to the completion global script in the shell configuration
/// file, which its location is described by the [configuration].
@visibleForTesting
void writeToShellConfigFile(String rootCommand) {
void writeToShellConfigFile(String executableName) {
final configuration = this.configuration!;

logger.info(
'Adding dart cli completion config entry '
'to $_shellRCFilePath',
'''Adding dart cli completion config entry to $_shellRCFilePath''',
);

final completionConfigDirPath = completionConfigDir.path;

final completionConfigPath = path.join(
completionConfigDirPath,
configuration.completionConfigForShellFileName,
);
final shellCompletionConfigFile =
configuration.completionScriptFile(completionConfigDir);

final shellRCFile = File(_shellRCFilePath);

if (!shellRCFile.existsSync()) {
throw CompletionInstallationException(
rootCommand: rootCommand,
executableName: executableName,
message: 'No configuration file found at ${shellRCFile.path}',
);
}

final containsLine =
shellRCFile.readAsStringSync().contains(completionConfigPath);
shellRCFile.readAsStringSync().contains(shellCompletionConfigFile.path);

if (containsLine) {
logger.warn('A completion config entry was already found on'
' $_shellRCFilePath.');
logger.warn(
'''A completion config entry was already found on $_shellRCFilePath''',
);
return;
}

_sourceScriptOnFile(
configFile: shellRCFile,
scriptName: 'Completion',
description: 'Completion scripts setup. '
'Remove the following line to uninstall',
scriptPath: path.join(
completionConfigDir.path,
configuration.completionConfigForShellFileName,
),
// TODO(alestiago): Define a template function instead.
final content = '''
## Completion scripts setup. Remove the following line to uninstall
${configuration.sourceLineTemplate(shellCompletionConfigFile.path)}''';
const ScriptEntry('Completion').appendTo(
shellRCFile,
content: content,
);
logger.info('Added config to ${shellRCFile.path}');
}

/// Tells the user to source the shell configuration file.
void _logSourceInstructions(String rootCommand) {
void _logSourceInstructions(String executableName) {
final level = logger.level;
logger
..level = Level.info
Expand All @@ -291,35 +289,6 @@ class CompletionInstallation {
)
..level = level;
}

void _sourceScriptOnFile({
required File configFile,
required String scriptName,
required String scriptPath,
String? description,
}) {
assert(
configFile.existsSync(),
'Sourcing a script line into an nonexistent config file.',
);

final configFilePath = configFile.path;

description ??= 'Completion config for "$scriptName"';

configFile.writeAsStringSync(
mode: FileMode.append,
'''
\n## [$scriptName]
## $description
${configuration!.sourceLineTemplate(scriptPath)}
## [/$scriptName]

''',
);

logger.info('Added config to $configFilePath');
}
}

/// Resolve the home from a path string
Expand Down
10 changes: 5 additions & 5 deletions lib/src/installer/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ class CompletionInstallationException implements Exception {
/// {@macro completion_installation_exception}
CompletionInstallationException({
required this.message,
required this.rootCommand,
required this.executableName,
});

/// The error message for this exception
final String message;

/// The command for which the installation failed.
final String rootCommand;
/// The executable name for which the installation failed.
final String executableName;

@override
String toString() => 'Could not install completion scripts for $rootCommand: '
'$message';
String toString() =>
'''Could not install completion scripts for $executableName: $message''';
}
Loading