2022-03-12 10:46:20 +01:00
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
using System.Diagnostics;
|
2022-06-10 16:25:12 +02:00
|
|
|
|
using System.Text;
|
2022-03-09 22:42:40 +01:00
|
|
|
|
using Cocona;
|
|
|
|
|
using Spectre.Console;
|
|
|
|
|
|
|
|
|
|
var app = CoconaLiteApp.Create();
|
|
|
|
|
|
|
|
|
|
app.AddCommand(RootCommand);
|
|
|
|
|
app.Run();
|
|
|
|
|
|
2022-03-11 18:04:07 +01:00
|
|
|
|
static async Task RootCommand(
|
2022-05-24 20:26:13 +02:00
|
|
|
|
[Option("extensions", new[] { 'x' }, Description = "Comma separated list of script extensions")] string? extensions,
|
|
|
|
|
[Option("depth", new[] { 'd' }, Description = "Search depth")] int depth = 1,
|
|
|
|
|
[Option("elevated", new[] { 'e' }, Description = "Run with elevated privileges")] bool elevated = false,
|
|
|
|
|
[Option("group", new[] { 'g' }, Description = "Group scripts by folder")] bool group = false,
|
2022-06-10 16:25:12 +02:00
|
|
|
|
[Option("brief", new[] { 'b' }, Description = "Show brief information")] bool brief = false,
|
2022-05-24 20:26:13 +02:00
|
|
|
|
[Argument(Name = "Directory", Description = "Starting directory")] 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.[/]");
|
2022-03-16 12:27:33 +01:00
|
|
|
|
Environment.ExitCode = 1;
|
2022-03-11 16:32:53 +01:00
|
|
|
|
return;
|
2022-03-09 22:42:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-16 12:27:33 +01:00
|
|
|
|
FileInfo[] files;
|
2022-03-12 12:23:18 +01:00
|
|
|
|
var finder = new ScriptFinder(extensions, directory, depth);
|
2022-03-14 20:45:01 +01:00
|
|
|
|
|
2022-05-24 20:26:13 +02:00
|
|
|
|
if (group)
|
2022-03-14 20:45:01 +01:00
|
|
|
|
{
|
|
|
|
|
var dict = finder.GetScriptsByDirectory();
|
2022-03-16 09:54:01 +01:00
|
|
|
|
|
|
|
|
|
if (dict.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
AnsiConsole.Markup($"[red]No scripts script files found in '{finder.RootDirectory}' with extensions '{string.Join(", ", finder.Extensions)}'[/]");
|
2022-03-16 12:27:33 +01:00
|
|
|
|
Environment.ExitCode = 1;
|
2022-03-16 09:54:01 +01:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 16:25:12 +02:00
|
|
|
|
var dirPrompt = PromptConstructor.GetDirectoryPrompt(dict.Keys.ToArray());
|
2022-05-24 18:46:04 +02:00
|
|
|
|
var directoryInfo = AnsiConsole.Prompt(dirPrompt);
|
2022-03-14 20:45:01 +01:00
|
|
|
|
files = dict[directoryInfo];
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
files = finder.GetScripts();
|
|
|
|
|
}
|
2022-03-09 22:42:40 +01:00
|
|
|
|
|
|
|
|
|
if (files.Length == 0)
|
|
|
|
|
{
|
2022-03-12 12:23:18 +01:00
|
|
|
|
AnsiConsole.Markup($"[red]No scripts script files found in '{finder.RootDirectory}' with extensions '{string.Join(", ", finder.Extensions)}'[/]");
|
2022-03-16 12:27:33 +01:00
|
|
|
|
Environment.ExitCode = 1;
|
2022-03-11 16:32:53 +01:00
|
|
|
|
return;
|
2022-03-09 22:42:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 16:25:12 +02:00
|
|
|
|
|
|
|
|
|
var prompt = PromptConstructor.GetScriptPrompt(files, brief);
|
2022-05-24 18:46:04 +02:00
|
|
|
|
var scripts = AnsiConsole.Prompt(prompt);
|
2022-03-09 22:42:40 +01:00
|
|
|
|
|
2022-05-24 18:46:04 +02:00
|
|
|
|
await ScriptExecutor.ExecAsync(scripts, elevated);
|
2022-06-10 16:25:12 +02:00
|
|
|
|
|
2022-03-12 10:46:20 +01:00
|
|
|
|
}
|
2022-03-09 22:42:40 +01:00
|
|
|
|
|
2022-03-16 12:27:33 +01:00
|
|
|
|
static class PromptConstructor
|
2022-03-11 17:30:33 +01:00
|
|
|
|
{
|
2022-03-16 12:27:33 +01:00
|
|
|
|
const int ScriptListSize = 15;
|
|
|
|
|
|
|
|
|
|
private static Style SelectionHighlight => new(decoration: Decoration.Bold | Decoration.Underline);
|
|
|
|
|
|
2022-06-10 16:25:12 +02:00
|
|
|
|
private static string FileStyle(FileInfo info, bool brief)
|
|
|
|
|
{
|
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
|
|
|
|
|
if (!brief)
|
|
|
|
|
{
|
|
|
|
|
builder.Append($"[blue]{info.DirectoryName}{Path.DirectorySeparatorChar}[/]");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
builder
|
|
|
|
|
.Append($"[orangered1]{Path.GetFileNameWithoutExtension(info.Name)}[/]")
|
|
|
|
|
.Append($"[greenyellow]{info.Extension}[/]");
|
|
|
|
|
|
|
|
|
|
return builder.ToString();
|
|
|
|
|
}
|
2022-03-16 12:27:33 +01:00
|
|
|
|
|
|
|
|
|
private static string DirectoryStyle(DirectoryInfo info) => $"[blue]{info}[/]";
|
|
|
|
|
|
2022-06-10 16:25:12 +02:00
|
|
|
|
public static MultiSelectionPrompt<FileInfo> GetScriptPrompt(FileInfo[] files, bool brief)
|
2022-03-16 12:27:33 +01:00
|
|
|
|
{
|
|
|
|
|
var prompt = new MultiSelectionPrompt<FileInfo>()
|
|
|
|
|
.Title("Select a script the scripts to execute:")
|
|
|
|
|
.NotRequired()
|
|
|
|
|
.PageSize(ScriptListSize)
|
|
|
|
|
.InstructionsText("[grey](Press [blue]<space>[/] to toggle a script, [green]<enter>[/] to accept)[/]")
|
|
|
|
|
.MoreChoicesText("[grey]Move up and down to reveal more options[/]")
|
2022-06-10 16:25:12 +02:00
|
|
|
|
.UseConverter(x => FileStyle(x, brief))
|
2022-03-16 12:27:33 +01:00
|
|
|
|
.HighlightStyle(SelectionHighlight)
|
|
|
|
|
.AddChoices(files);
|
2022-03-14 19:23:24 +01:00
|
|
|
|
|
2022-03-16 12:27:33 +01:00
|
|
|
|
return prompt;
|
2022-03-11 17:30:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-10 16:25:12 +02:00
|
|
|
|
public static SelectionPrompt<DirectoryInfo> GetDirectoryPrompt(DirectoryInfo[] directories)
|
2022-03-16 12:27:33 +01:00
|
|
|
|
{
|
|
|
|
|
var prompt = new SelectionPrompt<DirectoryInfo>()
|
|
|
|
|
.Title("Select a directory:")
|
|
|
|
|
.PageSize(ScriptListSize)
|
|
|
|
|
.MoreChoicesText("[grey]Move up and down to reveal more options[/]")
|
|
|
|
|
.UseConverter(DirectoryStyle)
|
|
|
|
|
.HighlightStyle(SelectionHighlight)
|
|
|
|
|
.AddChoices(directories);
|
2022-03-14 20:45:01 +01:00
|
|
|
|
|
2022-03-16 12:27:33 +01:00
|
|
|
|
return prompt;
|
|
|
|
|
}
|
2022-03-11 17:30:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-03-09 22:42:40 +01:00
|
|
|
|
static class ScriptExecutor
|
|
|
|
|
{
|
2022-03-16 12:27:33 +01:00
|
|
|
|
public 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));
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-16 12:27:33 +01:00
|
|
|
|
public static async ValueTask ExecAsync(FileInfo file, bool elevated, CancellationToken cancellationToken = default)
|
2022-03-11 18:04:07 +01:00
|
|
|
|
{
|
|
|
|
|
var process = GetExecutableProcessInfo(file, elevated);
|
2022-03-09 22:42:40 +01:00
|
|
|
|
|
2022-03-11 16:39:40 +01:00
|
|
|
|
if (process is null) return;
|
|
|
|
|
|
2022-03-12 10:46:20 +01:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await (Process.Start(process)?.WaitForExitAsync(cancellationToken) ?? Task.CompletedTask);
|
|
|
|
|
}
|
2022-03-14 19:23:24 +01:00
|
|
|
|
catch (Exception ex) when (ex is Win32Exception or InvalidOperationException or PlatformNotSupportedException)
|
2022-03-12 10:46:20 +01:00
|
|
|
|
{
|
|
|
|
|
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)
|
2022-03-11 16:39:40 +01:00
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
{
|
2022-03-16 12:27:33 +01:00
|
|
|
|
static readonly string[] DefaultExtensions = new[] { ".ps1", ".*sh", ".bat", ".cmd" };
|
2022-03-12 12:23:18 +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;
|
|
|
|
|
|
2022-03-12 12:23:18 +01:00
|
|
|
|
public ScriptFinder(string? extensions, string directory, int depth)
|
2022-03-09 22:42:40 +01:00
|
|
|
|
{
|
2022-03-12 12:23:18 +01:00
|
|
|
|
Extensions = extensions?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
|
|
|
|
.ToHashSet()
|
|
|
|
|
.Select(x => $".{x.TrimStart('.')}")
|
2022-03-16 12:27:33 +01:00
|
|
|
|
.ToArray() ?? DefaultExtensions;
|
2022-03-12 12:23:18 +01:00
|
|
|
|
|
|
|
|
|
Depth = depth;
|
|
|
|
|
RootDirectory = directory;
|
|
|
|
|
|
|
|
|
|
_options = new EnumerationOptions
|
|
|
|
|
{
|
|
|
|
|
IgnoreInaccessible = true,
|
|
|
|
|
RecurseSubdirectories = Depth > 0,
|
|
|
|
|
MaxRecursionDepth = Depth,
|
|
|
|
|
};
|
|
|
|
|
}
|
2022-03-09 22:42:40 +01:00
|
|
|
|
|
2022-03-14 20:45:01 +01:00
|
|
|
|
private IEnumerable<FileInfo> GetScriptFilesWithExtension(string extension)
|
2022-03-09 22:42:40 +01:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2022-03-12 12:23:18 +01:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-16 12:27:33 +01:00
|
|
|
|
public FileInfo[] GetScripts() =>
|
2022-03-14 20:45:01 +01:00
|
|
|
|
Extensions.Select(GetScriptFilesWithExtension).SelectMany(x => x).ToArray();
|
|
|
|
|
|
2022-03-16 12:27:33 +01:00
|
|
|
|
public IDictionary<DirectoryInfo, FileInfo[]> GetScriptsByDirectory() =>
|
2022-03-14 20:45:01 +01:00
|
|
|
|
Extensions
|
|
|
|
|
.Select(GetScriptFilesWithExtension)
|
|
|
|
|
.SelectMany(x => x)
|
|
|
|
|
.GroupBy(x => x.DirectoryName!)
|
|
|
|
|
.OrderBy(x => x.Key)
|
|
|
|
|
.ToDictionary(x => new DirectoryInfo(x.Key), x => x.ToArray());
|
2022-03-14 19:23:24 +01:00
|
|
|
|
}
|