script-launcher/src/Program.cs

192 lines
6.6 KiB
C#
Raw Normal View History

using System.ComponentModel;
using System.Diagnostics;
2022-03-09 22:42:40 +01:00
using Cocona;
using Spectre.Console;
2022-03-11 16:32:53 +01:00
const int ScriptListSize = 15;
const int ErrorExitCode = 1;
string[] DefaultExtensions = new[] { "ps1", "*sh", "bat", "cmd" };
2022-03-11 16:32:53 +01:00
2022-03-09 22:42:40 +01:00
var app = CoconaLiteApp.Create();
app.AddCommand(RootCommand);
app.Run();
2022-03-11 18:04:07 +01:00
static async Task RootCommand(
[Option("extensions", new char[] { 'e' }, Description = "Comma separated list of script extensions to search")] string? extensions,
2022-03-11 16:32:53 +01:00
[Option("depth", new char[] { 'd' }, Description = "Folder depth of the search")] int depth = 0,
[Option("elevated", new char[] { 'E' }, Description = "Run the script with elevated privileges")] bool elevated = false,
[Option("multiple", new char[] { 'm' }, Description = "Execute multiple scripts in parallel")] bool multiple = false,
2022-03-11 16:32:53 +01:00
[Argument(Name = "Directory", Description = "Directory from which search the scripts")] string directory = ".")
2022-03-09 22:42:40 +01:00
{
if (!Directory.Exists(directory))
{
2022-03-11 16:32:53 +01:00
AnsiConsole.Markup($"[red]The directory '{directory}' does not exist.[/]");
Environment.ExitCode = ErrorExitCode;
return;
2022-03-09 22:42:40 +01:00
}
var finder = new ScriptFinder(extensions, directory, depth);
var files = finder.GetScriptFiles();
2022-03-09 22:42:40 +01:00
if (files.Length == 0)
{
AnsiConsole.Markup($"[red]No scripts script files found in '{finder.RootDirectory}' with extensions '{string.Join(", ", finder.Extensions)}'[/]");
2022-03-11 16:32:53 +01:00
Environment.ExitCode = ErrorExitCode;
return;
2022-03-09 22:42:40 +01:00
}
2022-03-11 16:32:53 +01:00
if (multiple)
{
var prompt = new MultiSelectionPrompt<FileInfo>()
2022-03-11 17:30:33 +01:00
.Title("Select a script the scripts to execute:")
2022-03-11 16:32:53 +01:00
.NotRequired()
.PageSize(ScriptListSize)
.MoreChoicesText("[grey]Move up and down to reveal more options[/]")
.InstructionsText("[grey](Press [blue]<space>[/] to toggle a script, [green]<enter>[/] to accept)[/]")
2022-03-11 17:30:33 +01:00
.UseConverter(PromptDecorator.GetStyledOption)
.HighlightStyle(PromptDecorator.SelectionHighlight)
2022-03-11 16:32:53 +01:00
.AddChoices(files);
var scripts = AnsiConsole.Prompt(prompt);
2022-03-11 18:04:07 +01:00
await ScriptExecutor.ExecAsync(scripts, elevated);
2022-03-11 16:32:53 +01:00
}
else
{
var prompt = new SelectionPrompt<FileInfo>()
2022-03-11 17:30:33 +01:00
.Title("Select a script to execute:")
2022-03-11 16:32:53 +01:00
.PageSize(ScriptListSize)
.MoreChoicesText("[grey]Move up and down to reveal more options[/]")
2022-03-11 17:30:33 +01:00
.UseConverter(PromptDecorator.GetStyledOption)
.HighlightStyle(PromptDecorator.SelectionHighlight)
2022-03-11 16:32:53 +01:00
.AddChoices(files);
2022-03-09 22:42:40 +01:00
2022-03-11 16:32:53 +01:00
var script = AnsiConsole.Prompt(prompt);
2022-03-09 22:42:40 +01:00
2022-03-11 18:04:07 +01:00
await ScriptExecutor.ExecAsync(script, elevated);
2022-03-11 16:32:53 +01:00
}
}
2022-03-09 22:42:40 +01:00
2022-03-11 17:30:33 +01:00
static class PromptDecorator
{
internal static string GetStyledOption(FileInfo info)
{
var directory = $"[blue]{info.DirectoryName ?? "."}{Path.DirectorySeparatorChar}[/]";
var filename = $"[orangered1]{Path.GetFileNameWithoutExtension(info.Name)}[/]";
var extension = $"[greenyellow]{Path.GetExtension(info.Name)}[/]";
return $"{directory}{filename}{extension}";
}
internal static Style SelectionHighlight => new(decoration: Decoration.Bold | Decoration.Underline);
2022-03-11 17:30:33 +01:00
}
2022-03-09 22:42:40 +01:00
static class ScriptExecutor
{
2022-03-11 18:04:07 +01:00
internal static async Task ExecAsync(List<FileInfo> files, bool elevated)
2022-03-09 22:42:40 +01:00
{
2022-03-11 18:04:07 +01:00
await Parallel.ForEachAsync(files, (x, ct) => ExecAsync(x, elevated, ct));
}
internal static async ValueTask ExecAsync(FileInfo file, bool elevated, CancellationToken cancellationToken = default)
{
var process = GetExecutableProcessInfo(file, elevated);
2022-03-09 22:42:40 +01:00
if (process is null) return;
try
{
await (Process.Start(process)?.WaitForExitAsync(cancellationToken) ?? Task.CompletedTask);
}
catch (Exception ex) when (ex is Win32Exception || ex is InvalidOperationException || ex is PlatformNotSupportedException)
{
AnsiConsole.Markup($"[red]{ex.Message}[/]");
}
2022-03-09 22:42:40 +01:00
}
2022-03-11 18:04:07 +01:00
private static ProcessStartInfo? GetExecutableProcessInfo(FileInfo file, bool elevated)
{
return file.Extension switch
{
".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
};
}
2022-03-09 22:42:40 +01:00
}
2022-03-11 16:32:53 +01:00
readonly struct ScriptFinder
2022-03-09 22:42:40 +01:00
{
public string[] Extensions { get; }
public string RootDirectory { get; }
public int Depth { get; }
2022-03-09 22:42:40 +01:00
private readonly EnumerationOptions _options;
public ScriptFinder(string? extensions, string directory, int depth)
2022-03-09 22:42:40 +01:00
{
Extensions = extensions?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.ToHashSet()
.Select(x => $".{x.TrimStart('.')}")
.ToArray() ?? new[] { ".ps1", ".*sh", ".bat", ".cmd" };
Depth = depth;
RootDirectory = directory;
_options = new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = Depth > 0,
MaxRecursionDepth = Depth,
};
}
2022-03-09 22:42:40 +01:00
private IEnumerable<FileInfo> GetScriptFiles(string extension)
2022-03-09 22:42:40 +01:00
{
try
{
var filenames = Directory.GetFiles(RootDirectory, $"*{extension}", _options);
2022-03-11 17:30:33 +01:00
return filenames.Select(x => new FileInfo(x));
2022-03-09 22:42:40 +01:00
}
catch (UnauthorizedAccessException)
{
2022-03-11 17:30:33 +01:00
return Enumerable.Empty<FileInfo>();
2022-03-09 22:42:40 +01:00
}
}
internal FileInfo[] GetScriptFiles() =>
2022-03-11 16:32:53 +01:00
Extensions.Select(GetScriptFiles).SelectMany(x => x).ToArray();
2022-03-09 22:42:40 +01:00
}