diff --git a/src/Commands.cs b/src/Commands.cs new file mode 100644 index 0000000..5337cc6 --- /dev/null +++ b/src/Commands.cs @@ -0,0 +1,98 @@ +using System.ComponentModel; + +using Spectre.Console; +using Spectre.Console.Cli; + +namespace ScriptLauncher; + +internal sealed class RootCommand : AsyncCommand + { + private const int Failure = 1; + private const int Success = 0; + + public override async Task ExecuteAsync( + CommandContext context, + RootCommandSettings settings + ) + { + if (!Directory.Exists(settings.Directory)) + { + AnsiConsole.Markup($"[red]The directory '{settings.Directory}' does not exist.[/]"); + return Failure; + } + + FileInfo[] files; + var finder = new ScriptFinder(settings.Extensions, settings.Directory, settings.Depth); + + if (settings.Group) + { + var dict = finder.GetScriptsByDirectory(); + + if (dict.Count == 0) + { + AnsiConsole.Markup( + $"[red]No scripts script files found in '{finder.RootDirectory}' with extensions '{string.Join(", ", finder.Extensions)}'[/]" + ); + return Failure; + } + + var dirPrompt = PromptConstructor.GetDirectoryPrompt(dict.Keys.ToArray()); + var directoryInfo = AnsiConsole.Prompt(dirPrompt); + files = dict[directoryInfo]; + } + else + { + files = finder.GetScripts(); + } + + if (files.Length == 0) + { + AnsiConsole.Markup( + $"[red]No scripts script files found in '{finder.RootDirectory}' with extensions '{string.Join(", ", finder.Extensions)}'[/]" + ); + return Failure; + } + + var prompt = PromptConstructor.GetScriptPrompt(files, settings.Brief); + var scripts = AnsiConsole.Prompt(prompt); + + try + { + await ScriptExecutor.ExecAsync(scripts, settings.Elevated); + } + catch (Exception ex) when (ex is Win32Exception or InvalidOperationException or PlatformNotSupportedException) + { + AnsiConsole.Markup($"[red]{ex.Message}[/]"); + return Failure; + } + + return Success; + } + } + + internal class RootCommandSettings : CommandSettings + { + [Description("Comma separated list of script extensions")] + [CommandOption("-x|--extensions")] + public string? Extensions { get; init; } + + [Description("Search depth")] + [CommandOption("-d|--depth")] + public int Depth { get; init; } = 1; + + [Description("Run with elevated privileges")] + [CommandOption("-e|--elevated")] + public bool Elevated { get; init; } = false; + + [Description("Group scripts by folder")] + [CommandOption("-g|--group")] + public bool Group { get; init; } = false; + + [Description("Show brief information")] + [CommandOption("-b|--brief")] + public bool Brief { get; init; } = false; + + [Description("Starting directory (Default: .)")] + [CommandArgument(0, "")] + public string Directory { get; init; } = "."; + } diff --git a/src/Program.cs b/src/Program.cs index a935a1d..f53f375 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,93 +1,6 @@ -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using Spectre.Console; +using ScriptLauncher; using Spectre.Console.Cli; var app = new CommandApp(); app.Configure(x => x.SetApplicationName("scrl")); return app.Run(args); - -sealed class RootCommand : AsyncCommand -{ - private const int Failure = 1; - private const int Success = 0; - - public override async Task ExecuteAsync( - [NotNull] CommandContext context, - [NotNull] RootCommandSettings settings - ) - { - if (!Directory.Exists(settings.Directory)) - { - AnsiConsole.Markup($"[red]The directory '{settings.Directory}' does not exist.[/]"); - // Environment.ExitCode = 1; - return Failure; - } - - FileInfo[] files; - var finder = new ScriptFinder(settings.Extensions, settings.Directory, settings.Depth); - - if (settings.Group) - { - var dict = finder.GetScriptsByDirectory(); - - if (dict.Count == 0) - { - AnsiConsole.Markup( - $"[red]No scripts script files found in '{finder.RootDirectory}' with extensions '{string.Join(", ", finder.Extensions)}'[/]" - ); - return Failure; - } - - var dirPrompt = PromptConstructor.GetDirectoryPrompt(dict.Keys.ToArray()); - var directoryInfo = AnsiConsole.Prompt(dirPrompt); - files = dict[directoryInfo]; - } - else - { - files = finder.GetScripts(); - } - - if (files.Length == 0) - { - AnsiConsole.Markup( - $"[red]No scripts script files found in '{finder.RootDirectory}' with extensions '{string.Join(", ", finder.Extensions)}'[/]" - ); - return Failure; - } - - var prompt = PromptConstructor.GetScriptPrompt(files, settings.Brief); - var scripts = AnsiConsole.Prompt(prompt); - - await ScriptExecutor.ExecAsync(scripts, settings.Elevated); - - return Success; - } -} - -internal class RootCommandSettings : CommandSettings -{ - [Description("Comma separated list of script extensions")] - [CommandOption("-x|--extensions")] - public string? Extensions { get; init; } - - [Description("Search depth")] - [CommandOption("-d|--depth")] - public int Depth { get; init; } = 1; - - [Description("Run with elevated privileges")] - [CommandOption("-e|--elevated")] - public bool Elevated { get; init; } = false; - - [Description("Group scripts by folder")] - [CommandOption("-g|--group")] - public bool Group { get; init; } = false; - - [Description("Show brief information")] - [CommandOption("-b|--brief")] - public bool Brief { get; init; } = false; - - [Description("Starting directory (Default: .)")] - [CommandArgument(0, "")] - public string Directory { get; init; } = "."; -} diff --git a/src/PromptConstructor.cs b/src/PromptConstructor.cs index e01d3fa..8c0952e 100644 --- a/src/PromptConstructor.cs +++ b/src/PromptConstructor.cs @@ -1,11 +1,14 @@ using System.Text; using Spectre.Console; -static class PromptConstructor +namespace ScriptLauncher; + +internal static class PromptConstructor { const int ScriptListSize = 15; - private static Style SelectionHighlight => new(decoration: Decoration.Bold | Decoration.Underline); + private static Style SelectionHighlight => + new(decoration: Decoration.Bold | Decoration.Underline); private static string FileStyle(FileInfo info, bool brief) { @@ -28,14 +31,16 @@ static class PromptConstructor public static MultiSelectionPrompt GetScriptPrompt(FileInfo[] files, bool brief) { var prompt = new MultiSelectionPrompt() - .Title("Select the scripts to execute:") - .NotRequired() - .PageSize(ScriptListSize) - .InstructionsText("[grey](Press [blue][/] to toggle a script, [green][/] to accept)[/]") - .MoreChoicesText("[grey]Move up and down to reveal more options[/]") - .UseConverter(x => FileStyle(x, brief)) - .HighlightStyle(SelectionHighlight) - .AddChoices(files); + .Title("Select the scripts to execute:") + .NotRequired() + .PageSize(ScriptListSize) + .InstructionsText( + "[grey](Press [blue][/] to toggle a script, [green][/] to accept)[/]" + ) + .MoreChoicesText("[grey]Move up and down to reveal more options[/]") + .UseConverter(x => FileStyle(x, brief)) + .HighlightStyle(SelectionHighlight) + .AddChoices(files); return prompt; } @@ -43,12 +48,12 @@ static class PromptConstructor public static SelectionPrompt GetDirectoryPrompt(DirectoryInfo[] directories) { var prompt = new SelectionPrompt() - .Title("Select a directory:") - .PageSize(ScriptListSize) - .MoreChoicesText("[grey]Move up and down to reveal more options[/]") - .UseConverter(DirectoryStyle) - .HighlightStyle(SelectionHighlight) - .AddChoices(directories); + .Title("Select a directory:") + .PageSize(ScriptListSize) + .MoreChoicesText("[grey]Move up and down to reveal more options[/]") + .UseConverter(DirectoryStyle) + .HighlightStyle(SelectionHighlight) + .AddChoices(directories); return prompt; } diff --git a/src/ScriptExecutor.cs b/src/ScriptExecutor.cs index a80e9b3..494abf2 100644 --- a/src/ScriptExecutor.cs +++ b/src/ScriptExecutor.cs @@ -1,70 +1,46 @@ -using System.ComponentModel; using System.Diagnostics; -using Spectre.Console; -static class ScriptExecutor +namespace ScriptLauncher; + +internal static class ScriptExecutor { - public static async Task ExecAsync(List files, bool elevated) - { + public static async Task ExecAsync(List files, bool elevated) => await Parallel.ForEachAsync(files, (x, ct) => ExecAsync(x, elevated, ct)); - } - public static async ValueTask ExecAsync(FileInfo file, bool elevated, CancellationToken cancellationToken = default) + private static async ValueTask ExecAsync(FileInfo file, bool elevated, CancellationToken cancellationToken = default) { var process = GetExecutableProcessInfo(file, elevated); - - if (process is null) return; - - try + if (process is null) { - await (Process.Start(process)?.WaitForExitAsync(cancellationToken) ?? Task.CompletedTask); - } - catch (Exception ex) when (ex is Win32Exception or InvalidOperationException or PlatformNotSupportedException) - { - AnsiConsole.Markup($"[red]{ex.Message}[/]"); + return; } + + await (Process.Start(process)?.WaitForExitAsync(cancellationToken) ?? Task.CompletedTask); } - private static ProcessStartInfo? GetExecutableProcessInfo(FileInfo file, bool elevated) + private static ProcessStartInfo? GetExecutableProcessInfo(FileInfo file, bool elevated) => file.Extension switch { - return file.Extension switch + ".bat" or ".cmd" => new ProcessStartInfo { - ".bat" or ".cmd" => new ProcessStartInfo - { - FileName = "cmd", - Arguments = $"/Q /C .\\{file.Name}", - Verb = elevated ? "runas /user:Administrator" : string.Empty, - WorkingDirectory = file.DirectoryName - }, - ".ps1" => new ProcessStartInfo - { - FileName = "powershell.exe", - Arguments = $"-ExecutionPolicy Bypass -File .\\{file.Name}", - Verb = elevated ? "runas /user:Administrator" : string.Empty, - WorkingDirectory = file.DirectoryName - }, - ".sh" => new ProcessStartInfo - { - FileName = "bash", - Arguments = $"-c ./{file.Name}", - Verb = elevated ? "sudo" : string.Empty, - WorkingDirectory = file.DirectoryName - }, - ".zsh" => new ProcessStartInfo - { - FileName = "zsh", - Arguments = $"-c ./{file.Name}", - Verb = elevated ? "sudo" : string.Empty, - WorkingDirectory = file.DirectoryName - }, - ".fish" => new ProcessStartInfo - { - FileName = "fish", - Arguments = $"-c ./{file.Name}", - Verb = elevated ? "sudo" : string.Empty, - WorkingDirectory = file.DirectoryName - }, - _ => null - }; - } -} + FileName = "cmd", + Arguments = $"/Q /C .\\{file.Name}", + Verb = elevated ? "runas /user:Administrator" : string.Empty, + WorkingDirectory = file.DirectoryName + }, + ".ps1" => new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = $"-ExecutionPolicy Bypass -File .\\{file.Name}", + Verb = elevated ? "runas /user:Administrator" : string.Empty, + WorkingDirectory = file.DirectoryName + }, + ".sh" or ".zsh" or ".fish" => new ProcessStartInfo + { + FileName = "sh", + Arguments = $"-c ./{file.Name}", + Verb = elevated ? "sudo" : string.Empty, + WorkingDirectory = file.DirectoryName + }, + var _ => null + }; +} \ No newline at end of file diff --git a/src/ScriptFinder.cs b/src/ScriptFinder.cs index 58c512a..375ca4f 100644 --- a/src/ScriptFinder.cs +++ b/src/ScriptFinder.cs @@ -1,6 +1,6 @@ -using Spectre.Console; +namespace ScriptLauncher; -readonly struct ScriptFinder +internal readonly struct ScriptFinder { static readonly string[] DefaultExtensions = new[] { ".ps1", ".*sh", ".bat", ".cmd" }; public string[] Extensions { get; } @@ -10,10 +10,12 @@ readonly struct ScriptFinder public ScriptFinder(string? extensions, string directory, int depth) { - Extensions = extensions?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .ToHashSet() - .Select(x => $".{x.TrimStart('.')}") - .ToArray() ?? DefaultExtensions; + Extensions = + extensions + ?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .ToHashSet() + .Select(x => $".{x.TrimStart('.')}") + .ToArray() ?? DefaultExtensions; Depth = depth; RootDirectory = directory; @@ -44,9 +46,9 @@ readonly struct ScriptFinder public IDictionary GetScriptsByDirectory() => Extensions - .Select(GetScriptFilesWithExtension) - .SelectMany(x => x) - .GroupBy(x => x.DirectoryName!) - .OrderBy(x => x.Key) - .ToDictionary(x => new DirectoryInfo(x.Key), x => x.ToArray()); + .Select(GetScriptFilesWithExtension) + .SelectMany(x => x) + .GroupBy(x => x.DirectoryName!) + .OrderBy(x => x.Key) + .ToDictionary(x => new DirectoryInfo(x.Key), x => x.ToArray()); }