Skip to content

Commit 7b1726b

Browse files
feat: include uninstallation logic (#70)
Co-authored-by: Renan <[email protected]>
1 parent a9e21de commit 7b1726b

File tree

6 files changed

+666
-8
lines changed

6 files changed

+666
-8
lines changed

lib/src/installer/completion_installation.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,85 @@ ${configuration!.sourceLineTemplate(scriptPath)}''';
304304

305305
logger.info('Added config to $configFilePath');
306306
}
307+
308+
/// Uninstalls the completion for the command [rootCommand] on the current
309+
/// shell.
310+
///
311+
/// Before uninstalling, it checks if the completion is installed:
312+
/// - The shell has an existing RCFile with a completion
313+
/// [ScriptConfigurationEntry].
314+
/// - The shell has an existing completion configuration file with a
315+
/// [ScriptConfigurationEntry] for the [rootCommand].
316+
///
317+
/// If any of the above is not true, it throws a
318+
/// [CompletionUnistallationException].
319+
///
320+
/// Upon a successful uninstallation the executable [ScriptConfigurationEntry]
321+
/// is removed from the shell configuration file. If after this removal the
322+
/// latter is empty, it is deleted together with the the executable completion
323+
/// script and the completion [ScriptConfigurationEntry] from the shell RC
324+
/// file. In the case that there are no other completion scripts installed on
325+
/// other shells the completion config directory is deleted, leaving the
326+
/// user's system as it was before the installation.
327+
void uninstall(String rootCommand) {
328+
final configuration = this.configuration!;
329+
logger.detail(
330+
'''Uninstalling completion for the command $rootCommand on ${configuration.shell.name}''',
331+
);
332+
333+
final shellRCFile = File(_shellRCFilePath);
334+
if (!shellRCFile.existsSync()) {
335+
throw CompletionUnistallationException(
336+
executableName: rootCommand,
337+
message: 'No shell RC file found at ${shellRCFile.path}',
338+
);
339+
}
340+
341+
const completionEntry = ScriptConfigurationEntry('Completion');
342+
if (!completionEntry.existsIn(shellRCFile)) {
343+
throw CompletionUnistallationException(
344+
executableName: rootCommand,
345+
message: 'Completion is not installed at ${shellRCFile.path}',
346+
);
347+
}
348+
349+
final shellCompletionConfigurationFile = File(
350+
path.join(
351+
completionConfigDir.path,
352+
configuration.completionConfigForShellFileName,
353+
),
354+
);
355+
final executableEntry = ScriptConfigurationEntry(rootCommand);
356+
if (!executableEntry.existsIn(shellCompletionConfigurationFile)) {
357+
throw CompletionUnistallationException(
358+
executableName: rootCommand,
359+
message:
360+
'''No shell script file found at ${shellCompletionConfigurationFile.path}''',
361+
);
362+
}
363+
364+
final executableShellCompletionScriptFile = File(
365+
path.join(
366+
completionConfigDir.path,
367+
'$rootCommand.${configuration.shell.name}',
368+
),
369+
);
370+
if (executableShellCompletionScriptFile.existsSync()) {
371+
executableShellCompletionScriptFile.deleteSync();
372+
}
373+
374+
executableEntry.removeFrom(
375+
shellCompletionConfigurationFile,
376+
shouldDelete: true,
377+
);
378+
if (!shellCompletionConfigurationFile.existsSync()) {
379+
completionEntry.removeFrom(shellRCFile);
380+
}
381+
382+
if (completionConfigDir.listSync().isEmpty) {
383+
completionConfigDir.deleteSync();
384+
}
385+
}
307386
}
308387

309388
/// Resolve the home from a path string

lib/src/installer/exceptions.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,24 @@ class CompletionInstallationException implements Exception {
1818
String toString() => 'Could not install completion scripts for $rootCommand: '
1919
'$message';
2020
}
21+
22+
/// {@template completion_unistallation_exception}
23+
/// Describes an exception during the uninstallation of completion scripts.
24+
/// {@endtemplate}
25+
class CompletionUnistallationException implements Exception {
26+
/// {@macro completion_unistallation_exception}
27+
CompletionUnistallationException({
28+
required this.message,
29+
required this.executableName,
30+
});
31+
32+
/// The error message for this exception
33+
final String message;
34+
35+
/// The command for which the installation failed.
36+
final String executableName;
37+
38+
@override
39+
String toString() =>
40+
'''Could not uninstall completion scripts for $executableName: $message''';
41+
}

lib/src/installer/script_configuration_entry.dart

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,46 @@ class ScriptConfigurationEntry {
3838
file.createSync(recursive: true);
3939
}
4040

41-
final entry = StringBuffer()
41+
final stringBuffer = StringBuffer()
4242
..writeln()
43-
..writeln(_startComment)
44-
..writeln(content)
43+
..writeln(_startComment);
44+
if (content != null) stringBuffer.writeln(content);
45+
stringBuffer
4546
..writeln(_endComment)
4647
..writeln();
4748

4849
file.writeAsStringSync(
49-
entry.toString(),
50+
stringBuffer.toString(),
5051
mode: FileMode.append,
5152
);
5253
}
54+
55+
/// Removes the entry with [name] from the [file].
56+
///
57+
/// If the [file] does not exist, this will do nothing.
58+
///
59+
/// If a file has multiple entries with the same [name], all of them will be
60+
/// removed.
61+
///
62+
/// If [shouldDelete] is true, the [file] will be deleted if it is empty after
63+
/// removing the entry. Otherwise, the [file] will be left empty.
64+
void removeFrom(File file, {bool shouldDelete = false}) {
65+
if (!file.existsSync()) return;
66+
67+
final content = file.readAsStringSync();
68+
final stringPattern = '\n$_startComment.*$_endComment\n\n'
69+
.replaceAll('[', r'\[')
70+
.replaceAll(']', r'\]');
71+
final pattern = RegExp(
72+
stringPattern,
73+
multiLine: true,
74+
dotAll: true,
75+
);
76+
final newContent = content.replaceAllMapped(pattern, (_) => '');
77+
file.writeAsStringSync(newContent);
78+
79+
if (shouldDelete && newContent.trim().isEmpty) {
80+
file.deleteSync();
81+
}
82+
}
5383
}

0 commit comments

Comments
 (0)