From d5629a49a9b3623a9452edbfb2f1954e6a588b78 Mon Sep 17 00:00:00 2001 From: Louis Seubert Date: Sun, 10 May 2026 15:08:55 +0200 Subject: [PATCH] feat: remove spectre.console Replace `Spectre.Console.Cli` with `System.CommandLine` --- Directory.Packages.props | 5 +-- src/process.dummy.app/AsyncOutputCommand.cs | 13 ------ .../Geekeey.Process.Dummy.App.csproj | 5 +-- src/process.dummy.app/Output.cs | 17 ------- src/process.dummy.app/Program.cs | 45 +++++++++++-------- .../_commands/EchoCommand.cs | 22 +++++---- .../_commands/EchoStdinCommand.cs | 23 ++++++---- .../_commands/EnvironmentCommand.cs | 19 +++++--- .../_commands/ExitCommand.cs | 20 +++++---- .../_commands/GenerateBlobCommand.cs | 28 +++++++----- .../_commands/GenerateClobCommand.cs | 27 ++++++----- .../_commands/LengthCommand.cs | 19 +++++--- .../_commands/SleepCommand.cs | 27 ++++++----- .../_commands/WorkingDirectoryCommand.cs | 14 +++--- 14 files changed, 150 insertions(+), 134 deletions(-) delete mode 100644 src/process.dummy.app/AsyncOutputCommand.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index c8c0525..bf29479 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,6 @@ - - + - \ No newline at end of file + diff --git a/src/process.dummy.app/AsyncOutputCommand.cs b/src/process.dummy.app/AsyncOutputCommand.cs deleted file mode 100644 index 6ec859c..0000000 --- a/src/process.dummy.app/AsyncOutputCommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) The Geekeey Authors -// SPDX-License-Identifier: EUPL-1.2 - -using Spectre.Console.Cli; - -internal abstract class AsyncOutputCommand : AsyncCommand where T : OutputCommandSettings -{ -} - -internal abstract class OutputCommandSettings : CommandSettings -{ - [CommandOption("--target")] public OutputTarget Target { get; init; } = OutputTarget.StdOut; -} \ No newline at end of file diff --git a/src/process.dummy.app/Geekeey.Process.Dummy.App.csproj b/src/process.dummy.app/Geekeey.Process.Dummy.App.csproj index 64c3538..a8234c4 100644 --- a/src/process.dummy.app/Geekeey.Process.Dummy.App.csproj +++ b/src/process.dummy.app/Geekeey.Process.Dummy.App.csproj @@ -11,7 +11,6 @@ - - + - \ No newline at end of file + diff --git a/src/process.dummy.app/Output.cs b/src/process.dummy.app/Output.cs index 8080687..554cafa 100644 --- a/src/process.dummy.app/Output.cs +++ b/src/process.dummy.app/Output.cs @@ -3,32 +3,17 @@ internal sealed class Output : IDisposable { - private readonly CancellationTokenSource _cts = new(); - - public Output() - { - Console.CancelKeyPress += Cancel; - } - public StreamReader Stdin { get; } = new(Console.OpenStandardInput(), leaveOpen: false); public StreamWriter Stdout { get; } = new(Console.OpenStandardOutput(), leaveOpen: false); public StreamWriter Stderr { get; } = new(Console.OpenStandardError(), leaveOpen: false); - public CancellationToken CancellationToken => _cts.Token; - public static Output Connect() { return new Output(); } - private void Cancel(object? sender, ConsoleCancelEventArgs args) - { - args.Cancel = true; - _cts.Cancel(); - } - public void Dispose() { Stdout.BaseStream.Flush(); @@ -36,7 +21,5 @@ internal sealed class Output : IDisposable Stderr.BaseStream.Flush(); Stderr.Dispose(); Stdin.Dispose(); - Console.CancelKeyPress -= Cancel; - _cts.Dispose(); } } \ No newline at end of file diff --git a/src/process.dummy.app/Program.cs b/src/process.dummy.app/Program.cs index d6cc593..b207174 100644 --- a/src/process.dummy.app/Program.cs +++ b/src/process.dummy.app/Program.cs @@ -1,11 +1,10 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 +using System.CommandLine; using System.Reflection; using System.Runtime.InteropServices; -using Spectre.Console.Cli; - namespace Geekeey.Process.Testing.Fixture; public static class Program @@ -18,28 +17,36 @@ public static class Program public static string FilePath { get; } = Path.ChangeExtension(AssemblyPath, FileExtension); - private static Task Main(string[] args) + private static async Task Main(string[] args) { Environment.SetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", "false"); - var app = new CommandApp(); - app.Configure(Configuration); - return app.RunAsync(args); - static void Configuration(IConfigurator configuration) + var app = new RootCommand { - configuration.AddCommand("echo"); - configuration.AddCommand("echo-stdin"); - configuration.AddCommand("env"); - configuration.AddCommand("cwd"); - configuration.AddCommand("cwd"); - configuration.AddCommand("exit"); - configuration.AddCommand("length"); - configuration.AddCommand("sleep"); - configuration.AddBranch("generate", static generate => + new EchoCommand(), + new EchoStdinCommand(), + new EnvironmentCommand(), + new WorkingDirectoryCommand(), + new ExitCommand(), + new LengthCommand(), + new SleepCommand(), + new Command("generate") { - generate.AddCommand("blob"); - generate.AddCommand("clob"); - }); + new GenerateBlobCommand(), // + new GenerateClobCommand() + } + }; + + var cts = new CancellationTokenSource(); + using (PosixSignalRegistration.Create(PosixSignal.SIGINT, Cancel)) + { + return await app.Parse(args).InvokeAsync(configuration: null, cts.Token); + } + + void Cancel(PosixSignalContext context) + { + context.Cancel = true; + cts.Cancel(); } } } \ No newline at end of file diff --git a/src/process.dummy.app/_commands/EchoCommand.cs b/src/process.dummy.app/_commands/EchoCommand.cs index 29dfb4c..a6ea50a 100644 --- a/src/process.dummy.app/_commands/EchoCommand.cs +++ b/src/process.dummy.app/_commands/EchoCommand.cs @@ -1,23 +1,29 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 -using Spectre.Console.Cli; +using System.CommandLine; -internal sealed class EchoCommand : AsyncOutputCommand +internal sealed class EchoCommand : Command { - public sealed class Settings : OutputCommandSettings + private static readonly Option Target = new("--target") { DefaultValueFactory = _ => OutputTarget.StdOut }; + private static readonly Option Separator = new("--separator") { DefaultValueFactory = _ => " " }; + private static readonly Argument Items = new("line") { Arity = ArgumentArity.ZeroOrMore, DefaultValueFactory = _ => [] }; + + public EchoCommand() : base("echo") { - [CommandOption("--separator ")] public string Separator { get; init; } = " "; - [CommandArgument(0, "[line]")] public string[] Items { get; init; } = []; + Add(Target); + Add(Separator); + Add(Items); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); - foreach (var writer in output.GetWriters(settings.Target)) + foreach (var writer in output.GetWriters(result.GetValue(Target))) { - await writer.WriteLineAsync(string.Join(settings.Separator, settings.Items)); + await writer.WriteLineAsync(string.Join(result.GetRequiredValue(Separator), result.GetRequiredValue(Items))); } return 0; diff --git a/src/process.dummy.app/_commands/EchoStdinCommand.cs b/src/process.dummy.app/_commands/EchoStdinCommand.cs index 92517a4..f9ea769 100644 --- a/src/process.dummy.app/_commands/EchoStdinCommand.cs +++ b/src/process.dummy.app/_commands/EchoStdinCommand.cs @@ -2,25 +2,30 @@ // SPDX-License-Identifier: EUPL-1.2 using System.Buffers; +using System.CommandLine; -using Spectre.Console.Cli; - -internal sealed class EchoStdinCommand : AsyncOutputCommand +internal sealed class EchoStdinCommand : Command { - public sealed class Settings : OutputCommandSettings + private static readonly Option Target = new("--target") { DefaultValueFactory = _ => OutputTarget.StdOut }; + private static readonly Option Length = new("--length") { DefaultValueFactory = _ => long.MaxValue }; + + public EchoStdinCommand() : base("echo-stdin") { - [CommandOption("--length")] public long Length { get; init; } = long.MaxValue; + Add(Target); + Add(Length); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); using var buffer = MemoryPool.Shared.Rent(81920); var count = 0L; - while (count < settings.Length) + var max = result.GetRequiredValue(Length); + while (count < max) { - var bytesWanted = (int)Math.Min(buffer.Memory.Length, settings.Length - count); + var bytesWanted = (int)Math.Min(buffer.Memory.Length, max - count); var bytesRead = await output.Stdin.BaseStream.ReadAsync(buffer.Memory[..bytesWanted], cancellationToken); if (bytesRead <= 0) @@ -28,7 +33,7 @@ internal sealed class EchoStdinCommand : AsyncOutputCommand +internal sealed class EnvironmentCommand : Command { - public sealed class Settings : OutputCommandSettings + private static readonly Option Target = new("--target") { DefaultValueFactory = _ => OutputTarget.StdOut }; + private static readonly Argument Variables = new("argument") { Arity = ArgumentArity.ZeroOrMore, DefaultValueFactory = _ => [] }; + + public EnvironmentCommand() : base("env") { - [CommandArgument(0, "")] public string[] Variables { get; init; } = []; + Add(Target); + Add(Variables); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); - foreach (var name in settings.Variables) + foreach (var name in result.GetRequiredValue(Variables)) { var value = Environment.GetEnvironmentVariable(name) ?? string.Empty; - foreach (var writer in output.GetWriters(settings.Target)) + foreach (var writer in output.GetWriters(result.GetRequiredValue(Target))) { await writer.WriteLineAsync(value); } diff --git a/src/process.dummy.app/_commands/ExitCommand.cs b/src/process.dummy.app/_commands/ExitCommand.cs index d429027..60f5fd0 100644 --- a/src/process.dummy.app/_commands/ExitCommand.cs +++ b/src/process.dummy.app/_commands/ExitCommand.cs @@ -1,21 +1,23 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 -using Spectre.Console.Cli; +using System.CommandLine; -internal sealed class ExitCommand : AsyncCommand +internal sealed class ExitCommand : Command { - public sealed class Settings : CommandSettings + private static readonly Argument Code = new("code"); + + public ExitCommand() : base("exit") { - [CommandArgument(1, "")] public int Code { get; init; } + Add(Code); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); - - await output.Stderr.WriteLineAsync($"Exit code set to {settings.Code}"); - - return settings.Code; + var code = result.GetValue(Code); + await output.Stderr.WriteLineAsync($"Exit code set to {code}"); + return code; } } \ No newline at end of file diff --git a/src/process.dummy.app/_commands/GenerateBlobCommand.cs b/src/process.dummy.app/_commands/GenerateBlobCommand.cs index 3479cde..b078a27 100644 --- a/src/process.dummy.app/_commands/GenerateBlobCommand.cs +++ b/src/process.dummy.app/_commands/GenerateBlobCommand.cs @@ -2,32 +2,38 @@ // SPDX-License-Identifier: EUPL-1.2 using System.Buffers; +using System.CommandLine; -using Spectre.Console.Cli; - -internal sealed class GenerateBlobCommand : AsyncOutputCommand +internal sealed class GenerateBlobCommand : Command { private readonly Random _random = new(1234567); - public sealed class Settings : OutputCommandSettings + private static readonly Option Target = new("--target") { DefaultValueFactory = _ => OutputTarget.StdOut }; + private static readonly Option Length = new("--length") { DefaultValueFactory = _ => 100_000L }; + private static readonly Option Buffer = new("--buffer") { DefaultValueFactory = _ => 1024 }; + + public GenerateBlobCommand() : base("blob") { - [CommandOption("--length")] public long Length { get; init; } = 100_000; - [CommandOption("--buffer")] public int BufferSize { get; init; } = 1024; + Add(Target); + Add(Length); + Add(Buffer); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); - using var bytes = MemoryPool.Shared.Rent(settings.BufferSize); + using var bytes = MemoryPool.Shared.Rent(result.GetRequiredValue(Buffer)); var total = 0L; - while (total < settings.Length) + var length = result.GetRequiredValue(Length); + while (total < length) { _random.NextBytes(bytes.Memory.Span); - var count = (int)Math.Min(bytes.Memory.Length, settings.Length - total); - foreach (var writer in output.GetWriters(settings.Target)) + var count = (int)Math.Min(bytes.Memory.Length, length - total); + foreach (var writer in output.GetWriters(result.GetRequiredValue(Target))) { await writer.BaseStream.WriteAsync(bytes.Memory[..count], cancellationToken); } diff --git a/src/process.dummy.app/_commands/GenerateClobCommand.cs b/src/process.dummy.app/_commands/GenerateClobCommand.cs index e285071..066a538 100644 --- a/src/process.dummy.app/_commands/GenerateClobCommand.cs +++ b/src/process.dummy.app/_commands/GenerateClobCommand.cs @@ -1,37 +1,42 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 +using System.CommandLine; using System.Text; -using Spectre.Console.Cli; - -internal sealed class GenerateClobCommand : AsyncOutputCommand +internal sealed class GenerateClobCommand : Command { private readonly Random _random = new(1234567); private readonly char[] _chars = [.. Enumerable.Range(32, 94).Select(i => (char)i)]; - public sealed class Settings : OutputCommandSettings + private static readonly Option Target = new("--target") { DefaultValueFactory = _ => OutputTarget.StdOut }; + private static readonly Option Length = new("--length") { DefaultValueFactory = _ => 100_000 }; + private static readonly Option Lines = new("--lines") { DefaultValueFactory = _ => 1 }; + + public GenerateClobCommand() : base("clob") { - [CommandOption("--length")] public int Length { get; init; } = 100_000; - [CommandOption("--lines")] public int LinesCount { get; init; } = 1; + Add(Target); + Add(Length); + Add(Lines); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); - var buffer = new StringBuilder(settings.Length); + var buffer = new StringBuilder(result.GetRequiredValue(Length)); - for (var line = 0; line < settings.LinesCount; line++) + for (var line = 0; line < result.GetRequiredValue(Lines); line++) { buffer.Clear(); - for (var i = 0; i < settings.Length; i++) + for (var i = 0; i < result.GetRequiredValue(Length); i++) { buffer.Append(_chars[_random.Next(0, _chars.Length)]); } - foreach (var writer in output.GetWriters(settings.Target)) + foreach (var writer in output.GetWriters(result.GetRequiredValue(Target))) { await writer.WriteLineAsync(buffer.ToString()); } diff --git a/src/process.dummy.app/_commands/LengthCommand.cs b/src/process.dummy.app/_commands/LengthCommand.cs index 45c7aa6..c3e883f 100644 --- a/src/process.dummy.app/_commands/LengthCommand.cs +++ b/src/process.dummy.app/_commands/LengthCommand.cs @@ -2,21 +2,26 @@ // SPDX-License-Identifier: EUPL-1.2 using System.Buffers; +using System.CommandLine; using System.Globalization; -using Spectre.Console.Cli; - -internal sealed class LengthCommand : AsyncOutputCommand +internal sealed class LengthCommand : Command { - public sealed class Settings : OutputCommandSettings + private static readonly Option Target = new("--target") { DefaultValueFactory = _ => OutputTarget.StdOut }; + private static readonly Option Buffer = new("--buffer") { DefaultValueFactory = _ => 81920 }; + + public LengthCommand() : base("length") { + Add(Target); + Add(Buffer); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); - using var buffer = MemoryPool.Shared.Rent(81920); + using var buffer = MemoryPool.Shared.Rent(result.GetRequiredValue(Buffer)); var count = 0L; while (true) @@ -30,7 +35,7 @@ internal sealed class LengthCommand : AsyncOutputCommand count += bytesRead; } - foreach (var writer in output.GetWriters(settings.Target)) + foreach (var writer in output.GetWriters(result.GetRequiredValue(Target))) { await writer.WriteLineAsync(count.ToString(CultureInfo.InvariantCulture)); } diff --git a/src/process.dummy.app/_commands/SleepCommand.cs b/src/process.dummy.app/_commands/SleepCommand.cs index 95bcbe6..0c4448b 100644 --- a/src/process.dummy.app/_commands/SleepCommand.cs +++ b/src/process.dummy.app/_commands/SleepCommand.cs @@ -1,34 +1,37 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 -using Spectre.Console.Cli; +using System.CommandLine; -internal sealed class SleepCommand : AsyncCommand +internal sealed class SleepCommand : Command { - public sealed class Settings : CommandSettings + private static readonly Argument Duration = new("duration") { DefaultValueFactory = _ => TimeSpan.FromSeconds(1) }; + + public SleepCommand() : base("sleep") { - [CommandArgument(0, "[duration]")] public TimeSpan Duration { get; init; } = TimeSpan.FromSeconds(1); + Add(Duration); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); try { - await Console.Out.WriteLineAsync($"Sleeping for {settings.Duration}..."); - await Console.Out.FlushAsync(CancellationToken.None); + await output.Stdout.WriteLineAsync($"Sleeping for {result.GetRequiredValue(Duration)}..."); + await output.Stdout.FlushAsync(CancellationToken.None); - await Task.Delay(settings.Duration, output.CancellationToken); + await Task.Delay(result.GetRequiredValue(Duration), cancellationToken); } catch (OperationCanceledException) { - await Console.Out.WriteLineAsync("Canceled."); - await Console.Out.FlushAsync(CancellationToken.None); + await output.Stdout.WriteLineAsync("Canceled."); + await output.Stdout.FlushAsync(CancellationToken.None); } - await Console.Out.WriteLineAsync("Done."); - await Console.Out.FlushAsync(CancellationToken.None); + await output.Stdout.WriteLineAsync("Done."); + await output.Stdout.FlushAsync(CancellationToken.None); return 0; } } \ No newline at end of file diff --git a/src/process.dummy.app/_commands/WorkingDirectoryCommand.cs b/src/process.dummy.app/_commands/WorkingDirectoryCommand.cs index 6aa32d0..1e65131 100644 --- a/src/process.dummy.app/_commands/WorkingDirectoryCommand.cs +++ b/src/process.dummy.app/_commands/WorkingDirectoryCommand.cs @@ -1,19 +1,23 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 -using Spectre.Console.Cli; +using System.CommandLine; -internal sealed class WorkingDirectoryCommand : AsyncOutputCommand +internal sealed class WorkingDirectoryCommand : Command { - public sealed class Settings : OutputCommandSettings + private static readonly Option Target = new("--target") { DefaultValueFactory = _ => OutputTarget.StdOut }; + + public WorkingDirectoryCommand() : base("cwd") { + Add(Target); + SetAction(ExecuteAsync); } - public override async Task ExecuteAsync(CommandContext context, Settings settings, CancellationToken cancellationToken) + public async Task ExecuteAsync(ParseResult result, CancellationToken cancellationToken) { using var output = Output.Connect(); - foreach (var writer in output.GetWriters(settings.Target)) + foreach (var writer in output.GetWriters(result.GetRequiredValue(Target))) { await writer.WriteLineAsync(Directory.GetCurrentDirectory()); }