From 8012d54e89e149adfb7f7d212dac7a35a41a055e Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:39:44 +0100 Subject: [PATCH 1/9] Refactor `EnableHelp` functionality to support multiple `HelpCommandKind` values - Updated `EnableHelp` to accept an array of `HelpCommandKind` for improved flexibility. - Enhanced `HelpHandlerSettings` to handle multiple command kinds. - Introduced new `EmptyArgs` support in `HelpCommandKind`. - Added relevant unit tests to validate the updated help behavior. - Streamlined code by refactoring help printing logic into reusable methods. --- .../ICliPostProcessor.cs | 6 +++ .../ICliPreProcessor.cs | 6 +++ .../DefaultCliHostBuilder.cs | 8 ++-- .../Help/CliCommandHelpHandler.cs | 16 ++++--- .../Help/HelpCommandKind.cs | 1 + .../Help/HelpHandlerSettings.cs | 2 +- .../ICliHostBuilder.cs | 2 +- .../Hosting/CliCommandHelpHandlerTests.cs | 42 ++++++++++++++++--- .../Hosting/DefaultCliHostBuilderTests.cs | 4 +- 9 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs create mode 100644 source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs new file mode 100644 index 00000000..235b290d --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs @@ -0,0 +1,6 @@ +namespace CreativeCoders.Cli.Core; + +public interface ICliPostProcessor +{ + +} diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs new file mode 100644 index 00000000..ccc9e126 --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs @@ -0,0 +1,6 @@ +namespace CreativeCoders.Cli.Core; + +public interface ICliPreProcessor +{ + +} diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs index 18b7226a..1ce0da55 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs @@ -19,7 +19,7 @@ public class DefaultCliHostBuilder : ICliHostBuilder private bool _helpEnabled; - private HelpCommandKind _helpCommandKind; + private HelpCommandKind[] _helpCommandKinds = []; private bool _skipScanEntryAssembly; @@ -65,10 +65,10 @@ public ICliHostBuilder ScanAssemblies(params Assembly[] assemblies) return this; } - public ICliHostBuilder EnableHelp(HelpCommandKind commandKind) + public ICliHostBuilder EnableHelp(params HelpCommandKind[] commandKinds) { _helpEnabled = true; - _helpCommandKind = commandKind; + _helpCommandKinds = commandKinds; return this; } @@ -96,7 +96,7 @@ private IServiceProvider BuildServiceProvider() { services.TryAddSingleton(_ => new HelpHandlerSettings { - CommandKind = _helpCommandKind + CommandKinds = _helpCommandKinds }); services.TryAddSingleton(); } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs index 1e2b02fc..6c6e67db 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/CliCommandHelpHandler.cs @@ -28,12 +28,18 @@ public bool ShouldPrintHelp(string[] args) { var lowerCaseArgs = args.Select(x => x.ToLower()).ToArray(); - return _settings.CommandKind switch + return _settings.CommandKinds.Any(x => ShouldPrintHelpFor(x, lowerCaseArgs)); + } + + private static bool ShouldPrintHelpFor(HelpCommandKind helpCommandKind, string[] args) + { + return helpCommandKind switch { - HelpCommandKind.Command => lowerCaseArgs.FirstOrDefault() == "help", - HelpCommandKind.Argument => lowerCaseArgs.Contains("--help"), - HelpCommandKind.CommandOrArgument => lowerCaseArgs.FirstOrDefault() == "help" || - lowerCaseArgs.Contains("--help"), + HelpCommandKind.Command => args.FirstOrDefault() == "help", + HelpCommandKind.Argument => args.Contains("--help"), + HelpCommandKind.EmptyArgs => args.Length == 0, + HelpCommandKind.CommandOrArgument => args.FirstOrDefault() == "help" || + args.Contains("--help"), _ => false }; } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs index c929ce62..169bcfb9 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpCommandKind.cs @@ -4,5 +4,6 @@ public enum HelpCommandKind { Command, Argument, + EmptyArgs, CommandOrArgument } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs index ab7568bd..3a47678e 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/Help/HelpHandlerSettings.cs @@ -2,5 +2,5 @@ namespace CreativeCoders.Cli.Hosting.Help; public class HelpHandlerSettings { - public HelpCommandKind CommandKind { get; init; } + public HelpCommandKind[] CommandKinds { get; init; } = []; } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs index 657959fa..349b7163 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs @@ -54,7 +54,7 @@ ICliHostBuilder UseContext(Action? configu /// This can be a command-specific help, argument-specific help, or both, as specified by the values in . /// /// The same instance to allow for method chaining. - ICliHostBuilder EnableHelp(HelpCommandKind commandKind); + ICliHostBuilder EnableHelp(params HelpCommandKind[] commandKinds); /// /// Enables or disables command options validation. diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/CliCommandHelpHandlerTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/CliCommandHelpHandlerTests.cs index 8800ea2b..76efab75 100644 --- a/tests/CreativeCoders.Cli.Tests/Hosting/CliCommandHelpHandlerTests.cs +++ b/tests/CreativeCoders.Cli.Tests/Hosting/CliCommandHelpHandlerTests.cs @@ -16,11 +16,41 @@ namespace CreativeCoders.Cli.Tests.Hosting; [SuppressMessage("ReSharper", "NullableWarningSuppressionIsUsed")] public class CliCommandHelpHandlerTests { + [Theory] + [InlineData(new[] { HelpCommandKind.Command }, "help", "run")] + [InlineData(new[] { HelpCommandKind.Argument }, "run", "--help")] + [InlineData(new[] { HelpCommandKind.CommandOrArgument }, "help", "run")] + [InlineData(new[] { HelpCommandKind.CommandOrArgument }, "run", "--help")] + [InlineData(new[] { HelpCommandKind.Command, HelpCommandKind.Argument }, "help", "run")] + [InlineData(new[] { HelpCommandKind.Command, HelpCommandKind.Argument }, "run", "--help")] + [InlineData(new[] { HelpCommandKind.EmptyArgs })] + [InlineData(new[] { HelpCommandKind.Command, HelpCommandKind.Argument, HelpCommandKind.EmptyArgs }, + "help", "run")] + [InlineData(new[] { HelpCommandKind.Command, HelpCommandKind.Argument, HelpCommandKind.EmptyArgs }, "run", + "--help")] + [InlineData(new[] { HelpCommandKind.Command, HelpCommandKind.Argument, HelpCommandKind.EmptyArgs })] + public void ShouldPrintHelp_DifferentKinds_RespectsHelpCommand(HelpCommandKind[] commandKinds, + params string[] args) + { + // Arrange + var handler = CreateHandler(commandKinds, out var stringWriter, out _); + + // Act + var resultHelp = handler.ShouldPrintHelp(args); + + // Assert + resultHelp + .Should() + .BeTrue(); + + stringWriter.Dispose(); + } + [Fact] public void ShouldPrintHelp_CommandKindCommand_RespectsHelpCommand() { // Arrange - var handler = CreateHandler(HelpCommandKind.Command, out var stringWriter, out _); + var handler = CreateHandler([HelpCommandKind.Command], out var stringWriter, out _); // Act var resultHelp = handler.ShouldPrintHelp(["help"]); @@ -42,7 +72,7 @@ public void ShouldPrintHelp_CommandKindCommand_RespectsHelpCommand() public void ShouldPrintHelp_CommandKindArgument_RespectsHelpArgument() { // Arrange - var handler = CreateHandler(HelpCommandKind.Argument, out var stringWriter, out _); + var handler = CreateHandler([HelpCommandKind.Argument], out var stringWriter, out _); // Act var resultHelp = handler.ShouldPrintHelp(["run", "--help"]); @@ -85,7 +115,7 @@ public void PrintHelp_WhenCommandFound_PrintsDescriptionSyntaxAndOptions() var commandStore = new CliCommandStore(); commandStore.AddCommands([commandInfo]); - var helpHandler = CreateHandler(HelpCommandKind.CommandOrArgument, out var writer, + var helpHandler = CreateHandler([HelpCommandKind.CommandOrArgument], out var writer, out var optionsHelpGenerator, commandStore); A.CallTo(() => optionsHelpGenerator.CreateHelp(typeof(DummyOptions))) @@ -152,7 +182,7 @@ public void PrintHelp_WhenRootRequested_PrintsGroupsAndCommands() var commandStore = new CliCommandStore(); commandStore.AddCommands([commandInfoOne, commandInfoTwo]); - var handler = CreateHandler(HelpCommandKind.CommandOrArgument, out var writer, out _, commandStore); + var handler = CreateHandler([HelpCommandKind.CommandOrArgument], out var writer, out _, commandStore); // Act handler.PrintHelp([]); @@ -170,7 +200,7 @@ public void PrintHelp_WhenRootRequested_PrintsGroupsAndCommands() } private static CliCommandHelpHandler CreateHandler( - HelpCommandKind commandKind, + HelpCommandKind[] commandKinds, out StringWriter writer, out IOptionsHelpGenerator optionsHelpGenerator, ICliCommandStore? commandStore = null) @@ -182,7 +212,7 @@ private static CliCommandHelpHandler CreateHandler( Out = new AnsiConsoleOutput(writer) }); - var settings = new HelpHandlerSettings { CommandKind = commandKind }; + var settings = new HelpHandlerSettings { CommandKinds = commandKinds }; optionsHelpGenerator = A.Fake(); var store = commandStore ?? new CliCommandStore(); diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostBuilderTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostBuilderTests.cs index 53d4c75b..37196103 100644 --- a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostBuilderTests.cs +++ b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostBuilderTests.cs @@ -51,9 +51,9 @@ public void Build_WithHelpEnabled_RegistersHelpHandlerAndSettings() services .GetRequiredService() - .CommandKind + .CommandKinds .Should() - .Be(HelpCommandKind.Command); + .BeEquivalentTo([HelpCommandKind.Command]); } [Fact] From 0bebd4cd94c517e9561bb499da628e1d499273f8 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:31:02 +0100 Subject: [PATCH 2/9] Introduce pre- and post-processors in CLI framework - Added `ICliPreProcessor` and `ICliPostProcessor` interfaces with customizable execution conditions. - Implemented `PrintHeaderPreProcessor` to support printing header text/markup during CLI execution. - Enhanced `DefaultCliHost` to incorporate pre- and post-processing logic for commands and help printing. - Updated `CliHostBuilderExtensions` to provide helper methods for registering pre- and post-processors. - Migrated `CliResult` to `CreativeCoders.Cli.Core` namespace for consistency. - Updated unit tests to reflect pre- and post-processor injections in `DefaultCliHost`. --- samples/CliHostSampleApp/Program.cs | 1 + .../CliResult.cs | 2 +- .../ICliPostProcessor.cs | 2 +- .../ICliPreProcessor.cs | 4 +- .../PreProcessorExecutionCondition.cs | 8 ++++ .../CliHostBuilderExtensions.cs | 29 ++++++++++++ .../DefaultCliHost.cs | 44 ++++++++++++++++++- .../DefaultCliHostBuilder.cs | 36 +++++++++++++++ .../CreativeCoders.Cli.Hosting/ICliHost.cs | 1 + .../ICliHostBuilder.cs | 4 ++ .../PreProcessors/PrintHeaderPreProcessor.cs | 36 +++++++++++++++ .../Hosting/DefaultCliHostTests.cs | 22 +++++----- 12 files changed, 173 insertions(+), 16 deletions(-) rename source/Cli/{CreativeCoders.Cli.Hosting => CreativeCoders.Cli.Core}/CliResult.cs (77%) create mode 100644 source/Cli/CreativeCoders.Cli.Core/PreProcessorExecutionCondition.cs create mode 100644 source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs create mode 100644 source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs diff --git a/samples/CliHostSampleApp/Program.cs b/samples/CliHostSampleApp/Program.cs index 7b535186..00793999 100644 --- a/samples/CliHostSampleApp/Program.cs +++ b/samples/CliHostSampleApp/Program.cs @@ -14,6 +14,7 @@ private static ICliHost CreateCliHost() { return CliHostBuilder.Create() .EnableHelp(HelpCommandKind.CommandOrArgument) + .PrintHeaderMarkup(["[red]This is a sample cli host[/]"]) .Build(); } } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliResult.cs b/source/Cli/CreativeCoders.Cli.Core/CliResult.cs similarity index 77% rename from source/Cli/CreativeCoders.Cli.Hosting/CliResult.cs rename to source/Cli/CreativeCoders.Cli.Core/CliResult.cs index a7e70592..5748f546 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/CliResult.cs +++ b/source/Cli/CreativeCoders.Cli.Core/CliResult.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace CreativeCoders.Cli.Hosting; +namespace CreativeCoders.Cli.Core; [PublicAPI] public class CliResult(int exitCode) diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs index 235b290d..0fa36f98 100644 --- a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs @@ -2,5 +2,5 @@ namespace CreativeCoders.Cli.Core; public interface ICliPostProcessor { - + Task ExecuteAsync(CliResult cliResult); } diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs index ccc9e126..d3b50ec1 100644 --- a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs @@ -2,5 +2,7 @@ namespace CreativeCoders.Cli.Core; public interface ICliPreProcessor { - + Task ExecuteAsync(string[] args); + + PreProcessorExecutionCondition ExecutionCondition { get; } } diff --git a/source/Cli/CreativeCoders.Cli.Core/PreProcessorExecutionCondition.cs b/source/Cli/CreativeCoders.Cli.Core/PreProcessorExecutionCondition.cs new file mode 100644 index 00000000..c0758ae7 --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Core/PreProcessorExecutionCondition.cs @@ -0,0 +1,8 @@ +namespace CreativeCoders.Cli.Core; + +public enum PreProcessorExecutionCondition +{ + Always, + OnlyOnHelp, + OnlyOnCommand +} diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs new file mode 100644 index 00000000..532332c9 --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs @@ -0,0 +1,29 @@ +using CreativeCoders.Cli.Core; +using CreativeCoders.Cli.Hosting.PreProcessors; + +namespace CreativeCoders.Cli.Hosting; + +public static class CliHostBuilderExtensions +{ + public static ICliHostBuilder PrintHeaderText(this ICliHostBuilder builder, IEnumerable lines, + PreProcessorExecutionCondition executionCondition = PreProcessorExecutionCondition.Always) + { + return builder.RegisterPreProcessor(x => + { + x.Lines = lines; + x.PlainText = true; + x.ExecutionCondition = executionCondition; + }); + } + + public static ICliHostBuilder PrintHeaderMarkup(this ICliHostBuilder builder, IEnumerable lines, + PreProcessorExecutionCondition executionCondition = PreProcessorExecutionCondition.Always) + { + return builder.RegisterPreProcessor(x => + { + x.Lines = lines; + x.PlainText = false; + x.ExecutionCondition = executionCondition; + }); + } +} diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs index c1484791..5e2fa5a5 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs @@ -17,7 +17,9 @@ public class DefaultCliHost( IAnsiConsole ansiConsole, ICliCommandStore commandStore, IServiceProvider serviceProvider, - ICliCommandHelpHandler commandHelpHandler) + ICliCommandHelpHandler commandHelpHandler, + IEnumerable preProcessors, + IEnumerable postProcessors) : ICliHost { private readonly IServiceProvider _serviceProvider = Ensure.NotNull(serviceProvider); @@ -110,18 +112,24 @@ public async Task RunAsync(string[] args) { if (_commandHelpHandler.ShouldPrintHelp(args)) { + await ExecuteHelpPostProcessorsAsync(args).ConfigureAwait(false); + _commandHelpHandler.PrintHelp(args); return new CliResult(CliExitCodes.Success); } + await ExecuteCommandPostProcessorsAsync(args).ConfigureAwait(false); + var (command, optionsArgs, commandInfo) = CreateCliCommand(args); var commandContext = _serviceProvider.GetRequiredService(); commandContext.AllArgs = args; commandContext.OptionsArgs = optionsArgs; - return await ExecuteAsync(commandInfo, command, optionsArgs).ConfigureAwait(false); + var cliResult = await ExecuteAsync(commandInfo, command, optionsArgs).ConfigureAwait(false); + + return await ExecutePostProcessorsAsync(cliResult).ConfigureAwait(false); } catch (CliCommandConstructionFailedException e) { @@ -155,6 +163,38 @@ public async Task RunAsync(string[] args) } } + private async Task ExecuteHelpPostProcessorsAsync(string[] args) + { + PreProcessorExecutionCondition[] conditions = + [PreProcessorExecutionCondition.OnlyOnHelp, PreProcessorExecutionCondition.Always]; + + foreach (var preProcessor in preProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) + { + await preProcessor.ExecuteAsync(args).ConfigureAwait(false); + } + } + + private async Task ExecuteCommandPostProcessorsAsync(string[] args) + { + PreProcessorExecutionCondition[] conditions = + [PreProcessorExecutionCondition.OnlyOnCommand, PreProcessorExecutionCondition.Always]; + + foreach (var preProcessor in preProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) + { + await preProcessor.ExecuteAsync(args).ConfigureAwait(false); + } + } + + private async Task ExecutePostProcessorsAsync(CliResult cliResult) + { + foreach (var postProcessor in postProcessors) + { + await postProcessor.ExecuteAsync(cliResult).ConfigureAwait(false); + } + + return cliResult; + } + /// public async Task RunMainAsync(string[] args) { diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs index 1ce0da55..ba16ac83 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHostBuilder.cs @@ -87,6 +87,42 @@ public ICliHostBuilder SkipScanEntryAssembly(bool skipScanEntryAssembly = true) return this; } + public ICliHostBuilder RegisterPreProcessor(Action? configure = null) + where T : class, ICliPreProcessor + { + if (configure != null) + { + return ConfigureServices(x => x.AddSingleton(sp => + { + var preProcessor = sp.GetServiceOrCreateInstance(); + + configure(preProcessor); + + return preProcessor; + })); + } + + return ConfigureServices(x => x.AddSingleton()); + } + + public ICliHostBuilder RegisterPostProcessor(Action? configure = null) + where T : class, ICliPostProcessor + { + if (configure != null) + { + return ConfigureServices(x => x.AddSingleton(sp => + { + var postProcessor = sp.GetServiceOrCreateInstance(); + + configure(postProcessor); + + return postProcessor; + })); + } + + return ConfigureServices(x => x.AddSingleton()); + } + [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")] private IServiceProvider BuildServiceProvider() { diff --git a/source/Cli/CreativeCoders.Cli.Hosting/ICliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/ICliHost.cs index 9a364536..4dce2bf5 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/ICliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/ICliHost.cs @@ -1,3 +1,4 @@ +using CreativeCoders.Cli.Core; using JetBrains.Annotations; namespace CreativeCoders.Cli.Hosting; diff --git a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs index 349b7163..1e4074c8 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs @@ -74,6 +74,10 @@ ICliHostBuilder UseContext(Action? configu /// The same instance. ICliHostBuilder SkipScanEntryAssembly(bool skipScanEntryAssembly = true); + ICliHostBuilder RegisterPreProcessor(Action? configure = null) where T : class, ICliPreProcessor; + + ICliHostBuilder RegisterPostProcessor(Action? configure = null) where T : class, ICliPostProcessor; + /// /// Builds and creates an instance of configured through the current builder. /// diff --git a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs new file mode 100644 index 00000000..36442af3 --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs @@ -0,0 +1,36 @@ +using CreativeCoders.Cli.Core; +using CreativeCoders.Core; +using Spectre.Console; + +namespace CreativeCoders.Cli.Hosting.PreProcessors; + +public class PrintHeaderPreProcessor(IAnsiConsole ansiConsole) : ICliPreProcessor +{ + private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); + + public Task ExecuteAsync(string[] args) + { + if (PlainText) + { + foreach (var line in Lines) + { + _ansiConsole.WriteLine(line); + } + + return Task.CompletedTask; + } + + foreach (var line in Lines) + { + _ansiConsole.MarkupLine(line); + } + + return Task.CompletedTask; + } + + public PreProcessorExecutionCondition ExecutionCondition { get; set; } + + public IEnumerable Lines { get; set; } = []; + + public bool PlainText { get; set; } +} diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs index 288c9c2c..15e3ea0d 100644 --- a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs +++ b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs @@ -31,7 +31,7 @@ public async Task RunAsync_WhenHelpIsRequested_PrintsHelpAndReturnsSuccess() A.CallTo(() => helpHandler.ShouldPrintHelp(args)) .Returns(true); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -69,7 +69,7 @@ public async Task RunAsync_WhenCommandNotFound_PrintsSuggestionsAndReturnsNotFou A.CallTo(() => commandStore.FindCommandGroupNode(args)) .Returns(new FindCommandNodeResult(groupNode, [])); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -113,7 +113,7 @@ public async Task RunAsync_WhenCommandWithoutOptions_ExecutesAndReturnsResult() A.CallTo(() => serviceProvider.GetService(typeof(int))) .Returns(5); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -161,7 +161,7 @@ public async Task RunAsync_WhenCommandWithAbortException_ExecutesAndReturnsExcep A.CallTo(() => commandStore.FindCommandNode(args)) .Returns(new FindCommandNodeResult(commandNode, [])); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -223,7 +223,7 @@ public async Task RunMainAsync_WhenCommandWithoutOptions_ExecutesAndReturnsIntRe A.CallTo(() => serviceProvider.GetService(typeof(int))) .Returns(5); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunMainAsync(args); @@ -265,7 +265,7 @@ public async Task RunAsync_OnlyArgsForCommand_CommandContextHasCorrectArgs() A.CallTo(() => serviceProvider.GetService(typeof(int))) .Returns(5); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -315,7 +315,7 @@ public async Task RunAsync_ArgsForCommandAndOptions_CommandContextHasCorrectArgs A.CallTo(() => serviceProvider.GetService(typeof(int))) .Returns(5); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -361,7 +361,7 @@ public async Task RunAsync_WhenCommandCreationFails_PrintsErrorAndReturnsExitCod A.CallTo(() => commandStore.FindCommandNode(args)) .Returns(new FindCommandNodeResult(commandNode, [])); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -401,7 +401,7 @@ public async Task RunAsync_WithOptionsValidation_ExecutesAndReturnsResult() A.CallTo(() => commandStore.FindCommandNode(args)) .Returns(new FindCommandNodeResult(commandNode, [])); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -441,7 +441,7 @@ public async Task RunAsync_WithOptionsValidationActivatedButWithoutValidation_Ex A.CallTo(() => commandStore.FindCommandNode(args)) .Returns(new FindCommandNodeResult(commandNode, [])); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); @@ -481,7 +481,7 @@ public async Task RunAsync_WithOptionsValidationIsInvalid_ExecutesAndReturnsInva A.CallTo(() => commandStore.FindCommandNode(args)) .Returns(new FindCommandNodeResult(commandNode, [])); - var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler); + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); // Act var result = await host.RunAsync(args); From 2dfa381e3483b04240a5a7691fb95bbdbd68c035 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:51:31 +0100 Subject: [PATCH 3/9] Refactor and extend CLI processing framework - Renamed `PreProcessorExecutionCondition` to `CliProcessorExecutionCondition` for better alignment with naming conventions. - Added `PrintFooterPostProcessor` for footer rendering as text or markup. - Enhanced `DefaultCliHost` with separated pre- and post-processor execution for help and command handling. - Updated `CliHostBuilderExtensions` with new methods for printing header/footer text and markup. - Adjusted existing pre- and post-processor logic to support the renamed execution condition type. --- samples/CliHostSampleApp/Program.cs | 5 ++- ...n.cs => CliProcessorExecutionCondition.cs} | 2 +- .../ICliPostProcessor.cs | 2 + .../ICliPreProcessor.cs | 2 +- .../CliHostBuilderExtensions.cs | 26 ++++++++++++- .../DefaultCliHost.cs | 38 ++++++++++++++----- .../PreProcessors/PrintFooterPostProcessor.cs | 36 ++++++++++++++++++ .../PreProcessors/PrintHeaderPreProcessor.cs | 2 +- 8 files changed, 97 insertions(+), 16 deletions(-) rename source/Cli/CreativeCoders.Cli.Core/{PreProcessorExecutionCondition.cs => CliProcessorExecutionCondition.cs} (66%) create mode 100644 source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs diff --git a/samples/CliHostSampleApp/Program.cs b/samples/CliHostSampleApp/Program.cs index 00793999..8414e26e 100644 --- a/samples/CliHostSampleApp/Program.cs +++ b/samples/CliHostSampleApp/Program.cs @@ -14,7 +14,10 @@ private static ICliHost CreateCliHost() { return CliHostBuilder.Create() .EnableHelp(HelpCommandKind.CommandOrArgument) - .PrintHeaderMarkup(["[red]This is a sample cli host[/]"]) + .PrintHeaderText(["[red]This is a sample cli host[/]"]) + .PrintHeaderMarkup(["[green]This is a second pre processor[/]"]) + .PrintFooterText(["Bye bye"]) + .PrintFooterMarkup(["[red]This is a second post processor[/]"]) .Build(); } } diff --git a/source/Cli/CreativeCoders.Cli.Core/PreProcessorExecutionCondition.cs b/source/Cli/CreativeCoders.Cli.Core/CliProcessorExecutionCondition.cs similarity index 66% rename from source/Cli/CreativeCoders.Cli.Core/PreProcessorExecutionCondition.cs rename to source/Cli/CreativeCoders.Cli.Core/CliProcessorExecutionCondition.cs index c0758ae7..5b326e1a 100644 --- a/source/Cli/CreativeCoders.Cli.Core/PreProcessorExecutionCondition.cs +++ b/source/Cli/CreativeCoders.Cli.Core/CliProcessorExecutionCondition.cs @@ -1,6 +1,6 @@ namespace CreativeCoders.Cli.Core; -public enum PreProcessorExecutionCondition +public enum CliProcessorExecutionCondition { Always, OnlyOnHelp, diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs index 0fa36f98..db4007b4 100644 --- a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs @@ -3,4 +3,6 @@ namespace CreativeCoders.Cli.Core; public interface ICliPostProcessor { Task ExecuteAsync(CliResult cliResult); + + CliProcessorExecutionCondition ExecutionCondition { get; } } diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs index d3b50ec1..8c3c2ada 100644 --- a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs @@ -4,5 +4,5 @@ public interface ICliPreProcessor { Task ExecuteAsync(string[] args); - PreProcessorExecutionCondition ExecutionCondition { get; } + CliProcessorExecutionCondition ExecutionCondition { get; } } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs index 532332c9..42e2ea9b 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs @@ -6,7 +6,7 @@ namespace CreativeCoders.Cli.Hosting; public static class CliHostBuilderExtensions { public static ICliHostBuilder PrintHeaderText(this ICliHostBuilder builder, IEnumerable lines, - PreProcessorExecutionCondition executionCondition = PreProcessorExecutionCondition.Always) + CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) { return builder.RegisterPreProcessor(x => { @@ -17,7 +17,7 @@ public static ICliHostBuilder PrintHeaderText(this ICliHostBuilder builder, IEnu } public static ICliHostBuilder PrintHeaderMarkup(this ICliHostBuilder builder, IEnumerable lines, - PreProcessorExecutionCondition executionCondition = PreProcessorExecutionCondition.Always) + CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) { return builder.RegisterPreProcessor(x => { @@ -26,4 +26,26 @@ public static ICliHostBuilder PrintHeaderMarkup(this ICliHostBuilder builder, IE x.ExecutionCondition = executionCondition; }); } + + public static ICliHostBuilder PrintFooterText(this ICliHostBuilder builder, IEnumerable lines, + CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) + { + return builder.RegisterPostProcessor(x => + { + x.Lines = lines; + x.PlainText = true; + x.ExecutionCondition = executionCondition; + }); + } + + public static ICliHostBuilder PrintFooterMarkup(this ICliHostBuilder builder, IEnumerable lines, + CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) + { + return builder.RegisterPostProcessor(x => + { + x.Lines = lines; + x.PlainText = false; + x.ExecutionCondition = executionCondition; + }); + } } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs index 5e2fa5a5..d089a1db 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs @@ -112,11 +112,13 @@ public async Task RunAsync(string[] args) { if (_commandHelpHandler.ShouldPrintHelp(args)) { - await ExecuteHelpPostProcessorsAsync(args).ConfigureAwait(false); + await ExecuteHelpPreProcessorsAsync(args).ConfigureAwait(false); _commandHelpHandler.PrintHelp(args); - return new CliResult(CliExitCodes.Success); + var cliHelpResult = new CliResult(CliExitCodes.Success); + + return await ExecuteHelpPostProcessorsAsync(cliHelpResult).ConfigureAwait(false); } await ExecuteCommandPostProcessorsAsync(args).ConfigureAwait(false); @@ -129,7 +131,7 @@ public async Task RunAsync(string[] args) var cliResult = await ExecuteAsync(commandInfo, command, optionsArgs).ConfigureAwait(false); - return await ExecutePostProcessorsAsync(cliResult).ConfigureAwait(false); + return await ExecuteCommandPostProcessorsAsync(cliResult).ConfigureAwait(false); } catch (CliCommandConstructionFailedException e) { @@ -163,10 +165,10 @@ public async Task RunAsync(string[] args) } } - private async Task ExecuteHelpPostProcessorsAsync(string[] args) + private async Task ExecuteHelpPreProcessorsAsync(string[] args) { - PreProcessorExecutionCondition[] conditions = - [PreProcessorExecutionCondition.OnlyOnHelp, PreProcessorExecutionCondition.Always]; + CliProcessorExecutionCondition[] conditions = + [CliProcessorExecutionCondition.OnlyOnHelp, CliProcessorExecutionCondition.Always]; foreach (var preProcessor in preProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) { @@ -176,8 +178,8 @@ private async Task ExecuteHelpPostProcessorsAsync(string[] args) private async Task ExecuteCommandPostProcessorsAsync(string[] args) { - PreProcessorExecutionCondition[] conditions = - [PreProcessorExecutionCondition.OnlyOnCommand, PreProcessorExecutionCondition.Always]; + CliProcessorExecutionCondition[] conditions = + [CliProcessorExecutionCondition.OnlyOnCommand, CliProcessorExecutionCondition.Always]; foreach (var preProcessor in preProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) { @@ -185,9 +187,25 @@ private async Task ExecuteCommandPostProcessorsAsync(string[] args) } } - private async Task ExecutePostProcessorsAsync(CliResult cliResult) + private async Task ExecuteCommandPostProcessorsAsync(CliResult cliResult) { - foreach (var postProcessor in postProcessors) + CliProcessorExecutionCondition[] conditions = + [CliProcessorExecutionCondition.OnlyOnCommand, CliProcessorExecutionCondition.Always]; + + foreach (var postProcessor in postProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) + { + await postProcessor.ExecuteAsync(cliResult).ConfigureAwait(false); + } + + return cliResult; + } + + private async Task ExecuteHelpPostProcessorsAsync(CliResult cliResult) + { + CliProcessorExecutionCondition[] conditions = + [CliProcessorExecutionCondition.OnlyOnHelp, CliProcessorExecutionCondition.Always]; + + foreach (var postProcessor in postProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) { await postProcessor.ExecuteAsync(cliResult).ConfigureAwait(false); } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs new file mode 100644 index 00000000..e05362ed --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs @@ -0,0 +1,36 @@ +using CreativeCoders.Cli.Core; +using CreativeCoders.Core; +using Spectre.Console; + +namespace CreativeCoders.Cli.Hosting.PreProcessors; + +public class PrintFooterPostProcessor(IAnsiConsole ansiConsole) : ICliPostProcessor +{ + private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); + + public Task ExecuteAsync(CliResult cliResult) + { + if (PlainText) + { + foreach (var line in Lines) + { + _ansiConsole.WriteLine(line); + } + + return Task.CompletedTask; + } + + foreach (var line in Lines) + { + _ansiConsole.MarkupLine(line); + } + + return Task.CompletedTask; + } + + public CliProcessorExecutionCondition ExecutionCondition { get; set; } + + public IEnumerable Lines { get; set; } = []; + + public bool PlainText { get; set; } +} diff --git a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs index 36442af3..0bc09651 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs @@ -28,7 +28,7 @@ public Task ExecuteAsync(string[] args) return Task.CompletedTask; } - public PreProcessorExecutionCondition ExecutionCondition { get; set; } + public CliProcessorExecutionCondition ExecutionCondition { get; set; } public IEnumerable Lines { get; set; } = []; From caa5b2d960b16e6eb6801e5e02288200c8a87e89 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:56:38 +0100 Subject: [PATCH 4/9] Add unit tests for `DefaultCliHost` to verify pre-and post-processor behavior with help and command execution --- .../Hosting/DefaultCliHostTests.cs | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs index 15e3ea0d..e918a2a0 100644 --- a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs +++ b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs @@ -45,6 +45,76 @@ public async Task RunAsync_WhenHelpIsRequested_PrintsHelpAndReturnsSuccess() .Be(CliExitCodes.Success); } + [Fact] + public async Task RunAsync_WhenHelpIsRequested_ExecutesHelpPreAndPostProcessors() + { + // Arrange + var args = new[] { "help" }; + + var ansiConsole = A.Fake(); + var commandStore = A.Fake(); + var serviceProvider = A.Fake(); + var helpHandler = A.Fake(); + + var preHelpProcessor = A.Fake(); + var preAlwaysProcessor = A.Fake(); + var preCommandProcessor = A.Fake(); + var postHelpProcessor = A.Fake(); + var postAlwaysProcessor = A.Fake(); + var postCommandProcessor = A.Fake(); + + SetupServiceProvider(serviceProvider, null); + + A.CallTo(() => helpHandler.ShouldPrintHelp(args)) + .Returns(true); + + A.CallTo(() => preHelpProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnHelp); + A.CallTo(() => preAlwaysProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + A.CallTo(() => preCommandProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnCommand); + + A.CallTo(() => postHelpProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnHelp); + A.CallTo(() => postAlwaysProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + A.CallTo(() => postCommandProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnCommand); + + var host = new DefaultCliHost( + ansiConsole, + commandStore, + serviceProvider, + helpHandler, + [preHelpProcessor, preAlwaysProcessor, preCommandProcessor], + [postHelpProcessor, postAlwaysProcessor, postCommandProcessor]); + + // Act + var result = await host.RunAsync(args); + + // Assert + A.CallTo(() => preHelpProcessor.ExecuteAsync(args)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => preAlwaysProcessor.ExecuteAsync(args)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => preCommandProcessor.ExecuteAsync(args)) + .MustNotHaveHappened(); + + A.CallTo(() => postHelpProcessor.ExecuteAsync(A.That.Matches(r => + r.ExitCode == CliExitCodes.Success))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => postAlwaysProcessor.ExecuteAsync(A.That.Matches(r => + r.ExitCode == CliExitCodes.Success))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => postCommandProcessor.ExecuteAsync(A.Ignored)) + .MustNotHaveHappened(); + + result.ExitCode + .Should() + .Be(CliExitCodes.Success); + } + [Fact] public async Task RunAsync_WhenCommandNotFound_PrintsSuggestionsAndReturnsNotFound() { @@ -83,6 +153,90 @@ public async Task RunAsync_WhenCommandNotFound_PrintsSuggestionsAndReturnsNotFou .MustHaveHappenedOnceExactly(); } + [Fact] + public async Task RunAsync_WhenCommandIsExecuted_ExecutesCommandPreAndPostProcessors() + { + // Arrange + var args = new[] { "run" }; + + var ansiConsole = A.Fake(); + var commandStore = A.Fake(); + var serviceProvider = A.Fake(); + var helpHandler = A.Fake(); + + var preHelpProcessor = A.Fake(); + var preAlwaysProcessor = A.Fake(); + var preCommandProcessor = A.Fake(); + var postHelpProcessor = A.Fake(); + var postAlwaysProcessor = A.Fake(); + var postCommandProcessor = A.Fake(); + + SetupServiceProvider(serviceProvider, new CliCommandContext()); + + A.CallTo(() => helpHandler.ShouldPrintHelp(args)) + .Returns(false); + + A.CallTo(() => preHelpProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnHelp); + A.CallTo(() => preAlwaysProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + A.CallTo(() => preCommandProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnCommand); + + A.CallTo(() => postHelpProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnHelp); + A.CallTo(() => postAlwaysProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + A.CallTo(() => postCommandProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.OnlyOnCommand); + + var commandInfo = new CliCommandInfo + { + CommandAttribute = new CliCommandAttribute(["run"]), + CommandType = typeof(DummyCommandWithResult) + }; + + var commandNode = new CliCommandNode(commandInfo, "run", null); + + A.CallTo(() => commandStore.FindCommandNode(args)) + .Returns(new FindCommandNodeResult(commandNode, [])); + + A.CallTo(() => serviceProvider.GetService(typeof(int))) + .Returns(17); + + var host = new DefaultCliHost( + ansiConsole, + commandStore, + serviceProvider, + helpHandler, + [preHelpProcessor, preAlwaysProcessor, preCommandProcessor], + [postHelpProcessor, postAlwaysProcessor, postCommandProcessor]); + + // Act + var result = await host.RunAsync(args); + + // Assert + A.CallTo(() => preCommandProcessor.ExecuteAsync(args)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => preAlwaysProcessor.ExecuteAsync(args)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => preHelpProcessor.ExecuteAsync(args)) + .MustNotHaveHappened(); + + A.CallTo(() => postCommandProcessor.ExecuteAsync(A.That.Matches(r => + r.ExitCode == 17))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => postAlwaysProcessor.ExecuteAsync(A.That.Matches(r => + r.ExitCode == 17))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => postHelpProcessor.ExecuteAsync(A.Ignored)) + .MustNotHaveHappened(); + + result.ExitCode + .Should() + .Be(17); + } + [Fact] public async Task RunAsync_WhenCommandWithoutOptions_ExecutesAndReturnsResult() { From efc26660721cfb2173cde36959a2e9b2ad4cf4cf Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:59:07 +0100 Subject: [PATCH 5/9] Annotate processors and builder extension methods with `[UsedImplicitly]` and `[ExcludeFromCodeCoverage]` for improved code clarity and coverage analysis. --- .../CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs | 2 ++ .../PreProcessors/PrintFooterPostProcessor.cs | 4 ++++ .../PreProcessors/PrintHeaderPreProcessor.cs | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs index 42e2ea9b..3d00e370 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using CreativeCoders.Cli.Core; using CreativeCoders.Cli.Hosting.PreProcessors; namespace CreativeCoders.Cli.Hosting; +[ExcludeFromCodeCoverage] public static class CliHostBuilderExtensions { public static ICliHostBuilder PrintHeaderText(this ICliHostBuilder builder, IEnumerable lines, diff --git a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs index e05362ed..1153f950 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintFooterPostProcessor.cs @@ -1,9 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using CreativeCoders.Cli.Core; using CreativeCoders.Core; +using JetBrains.Annotations; using Spectre.Console; namespace CreativeCoders.Cli.Hosting.PreProcessors; +[UsedImplicitly] +[ExcludeFromCodeCoverage] public class PrintFooterPostProcessor(IAnsiConsole ansiConsole) : ICliPostProcessor { private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); diff --git a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs index 0bc09651..4feefd99 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/PreProcessors/PrintHeaderPreProcessor.cs @@ -1,9 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using CreativeCoders.Cli.Core; using CreativeCoders.Core; +using JetBrains.Annotations; using Spectre.Console; namespace CreativeCoders.Cli.Hosting.PreProcessors; +[UsedImplicitly] +[ExcludeFromCodeCoverage] public class PrintHeaderPreProcessor(IAnsiConsole ansiConsole) : ICliPreProcessor { private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); From 45a017303bd85b9e72bc206afeae4af7f67ce72d Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:04:07 +0100 Subject: [PATCH 6/9] Add `[PublicAPI]` annotations and minor cleanup - Added `[PublicAPI]` attributes to interfaces and classes to enhance IDE support and improve code clarity. - Removed unused property `IsDefaultCommand` from `CliCommandAttribute`. - Fixed a minor parameter name typo in XML documentation. --- Core.sln.DotSettings | 1 + source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs | 2 -- source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs | 3 +++ source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs | 3 +++ .../Exceptions/CliCommandAbortException.cs | 3 +++ source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs | 2 +- 6 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Core.sln.DotSettings b/Core.sln.DotSettings index c36b219f..85a2d11f 100644 --- a/Core.sln.DotSettings +++ b/Core.sln.DotSettings @@ -10,6 +10,7 @@ SUGGESTION SUGGESTION SUGGESTION + DO_NOT_SHOW WARNING DO_NOT_SHOW WARNING diff --git a/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs b/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs index 56471762..f19a08ee 100644 --- a/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs +++ b/source/Cli/CreativeCoders.Cli.Core/CliCommandAttribute.cs @@ -12,6 +12,4 @@ public class CliCommandAttribute(string[] commands) : Attribute public string Description { get; set; } = string.Empty; public string[] AlternativeCommands { get; init; } = []; - - public bool IsDefaultCommand { get; set; } } diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs index db4007b4..7348bf0c 100644 --- a/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPostProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace CreativeCoders.Cli.Core; +[PublicAPI] public interface ICliPostProcessor { Task ExecuteAsync(CliResult cliResult); diff --git a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs index 8c3c2ada..29a5bc6a 100644 --- a/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs +++ b/source/Cli/CreativeCoders.Cli.Core/ICliPreProcessor.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace CreativeCoders.Cli.Core; +[PublicAPI] public interface ICliPreProcessor { Task ExecuteAsync(string[] args); diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs index 78f71a23..fef13f6f 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliCommandAbortException.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace CreativeCoders.Cli.Hosting.Exceptions; +[PublicAPI] public class CliCommandAbortException(string message, int exitCode, Exception? exception = null) : CliExitException(message, exitCode, exception) { diff --git a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs index 1e4074c8..6d4a2186 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs @@ -49,7 +49,7 @@ ICliHostBuilder UseContext(Action? configu /// /// Enables help functionality for the CLI application by specifying the type of help commands to be supported. /// - /// + /// /// Defines the type of help commands that can be used within the application. /// This can be a command-specific help, argument-specific help, or both, as specified by the values in . /// From 3ce0b7801d9e60dd14fad67479d5e1e0b3814734 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:04:01 +0100 Subject: [PATCH 7/9] Add pre/post-processor exception handling and refactor processor execution in `DefaultCliHost` - Introduced `CliPreProcessorException` and `CliPostProcessorException` for detailed error reporting during pre- and post-processor execution failures. - Added `PreProcessorFailed` and `PostProcessorFailed` exit codes in `CliExitCodes`. - Refactored `DefaultCliHost` to centralize processor execution logic with dedicated methods (`ExecutePreProcessorsAsync` and `ExecutePostProcessorsAsync`). - Created `CliHostSettings` to support additional configuration options like `UseValidation`. - Adjusted exception handling to improve error feedback in CLI execution. --- .../CliExitCodes.cs | 4 ++ .../CliHostSettings.cs | 6 ++ .../DefaultCliHost.cs | 62 +++++++++++++------ .../Exceptions/CliPostProcessorException.cs | 11 ++++ .../Exceptions/CliPreProcessorException.cs | 12 ++++ 5 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs create mode 100644 source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs create mode 100644 source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs index 1a5a1832..b7461b6c 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/CliExitCodes.cs @@ -11,4 +11,8 @@ public static class CliExitCodes public const int CommandResultUnknown = int.MinValue + 2; public const int CommandOptionsInvalid = int.MinValue + 3; + + public const int PreProcessorFailed = int.MinValue + 4; + + public const int PostProcessorFailed = int.MinValue + 5; } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs new file mode 100644 index 00000000..f8ea4728 --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostSettings.cs @@ -0,0 +1,6 @@ +namespace CreativeCoders.Cli.Hosting; + +public class CliHostSettings +{ + public bool UseValidation { get; init; } +} diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs index d089a1db..87cd1c6d 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs @@ -121,7 +121,7 @@ public async Task RunAsync(string[] args) return await ExecuteHelpPostProcessorsAsync(cliHelpResult).ConfigureAwait(false); } - await ExecuteCommandPostProcessorsAsync(args).ConfigureAwait(false); + await ExecuteCommandPreProcessorsAsync(args).ConfigureAwait(false); var (command, optionsArgs, commandInfo) = CreateCliCommand(args); @@ -161,6 +161,12 @@ public async Task RunAsync(string[] args) _ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]"); } + return new CliResult(e.ExitCode); + } + catch (CliExitException e) + { + _ansiConsole.MarkupLine($"[red]{e.Message}[/]"); + return new CliResult(e.ExitCode); } } @@ -170,20 +176,33 @@ private async Task ExecuteHelpPreProcessorsAsync(string[] args) CliProcessorExecutionCondition[] conditions = [CliProcessorExecutionCondition.OnlyOnHelp, CliProcessorExecutionCondition.Always]; - foreach (var preProcessor in preProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) - { - await preProcessor.ExecuteAsync(args).ConfigureAwait(false); - } + await ExecutePreProcessorsAsync(preProcessors, conditions, args) + .ConfigureAwait(false); } - private async Task ExecuteCommandPostProcessorsAsync(string[] args) + private async Task ExecuteCommandPreProcessorsAsync(string[] args) { CliProcessorExecutionCondition[] conditions = [CliProcessorExecutionCondition.OnlyOnCommand, CliProcessorExecutionCondition.Always]; + await ExecutePreProcessorsAsync(preProcessors, conditions, args) + .ConfigureAwait(false); + } + + private static async Task ExecutePreProcessorsAsync(IEnumerable preProcessors, + CliProcessorExecutionCondition[] conditions, string[] args) + { foreach (var preProcessor in preProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) { - await preProcessor.ExecuteAsync(args).ConfigureAwait(false); + try + { + await preProcessor.ExecuteAsync(args).ConfigureAwait(false); + } + catch (Exception e) + { + throw new CliPreProcessorException(preProcessor, + $"PreProcessor execution failed: {e.Message}", e); + } } } @@ -192,12 +211,7 @@ private async Task ExecuteCommandPostProcessorsAsync(CliResult cliRes CliProcessorExecutionCondition[] conditions = [CliProcessorExecutionCondition.OnlyOnCommand, CliProcessorExecutionCondition.Always]; - foreach (var postProcessor in postProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) - { - await postProcessor.ExecuteAsync(cliResult).ConfigureAwait(false); - } - - return cliResult; + return await ExecutePostProcessorsAsync(postProcessors, conditions, cliResult).ConfigureAwait(false); } private async Task ExecuteHelpPostProcessorsAsync(CliResult cliResult) @@ -205,9 +219,24 @@ private async Task ExecuteHelpPostProcessorsAsync(CliResult cliResult CliProcessorExecutionCondition[] conditions = [CliProcessorExecutionCondition.OnlyOnHelp, CliProcessorExecutionCondition.Always]; + return await ExecutePostProcessorsAsync(postProcessors, conditions, cliResult).ConfigureAwait(false); + } + + private static async Task ExecutePostProcessorsAsync( + IEnumerable postProcessors, CliProcessorExecutionCondition[] conditions, + CliResult cliResult) + { foreach (var postProcessor in postProcessors.Where(x => conditions.Contains(x.ExecutionCondition))) { - await postProcessor.ExecuteAsync(cliResult).ConfigureAwait(false); + try + { + await postProcessor.ExecuteAsync(cliResult).ConfigureAwait(false); + } + catch (Exception e) + { + throw new CliPostProcessorException(postProcessor, + $"PostProcessor execution failed: {e.Message}", e); + } } return cliResult; @@ -239,8 +268,3 @@ private void PrintNearestMatch(string[] args) _commandHelpHandler.PrintHelpFor(findCommandGroupNodeResult.Node.ChildNodes); } } - -public class CliHostSettings -{ - public bool UseValidation { get; init; } -} diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs new file mode 100644 index 00000000..c76898f7 --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPostProcessorException.cs @@ -0,0 +1,11 @@ +using CreativeCoders.Cli.Core; + +namespace CreativeCoders.Cli.Hosting.Exceptions; + +public class CliPostProcessorException( + ICliPostProcessor postProcessor, + string message, + Exception? innerException) : CliExitException(message, CliExitCodes.PostProcessorFailed, innerException) +{ + public ICliPostProcessor PostProcessor { get; } = postProcessor; +} diff --git a/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs new file mode 100644 index 00000000..a0d6ceff --- /dev/null +++ b/source/Cli/CreativeCoders.Cli.Hosting/Exceptions/CliPreProcessorException.cs @@ -0,0 +1,12 @@ +using CreativeCoders.Cli.Core; + +namespace CreativeCoders.Cli.Hosting.Exceptions; + +public class CliPreProcessorException( + ICliPreProcessor preProcessor, + string message, + Exception? innerException) + : CliExitException(message, CliExitCodes.PreProcessorFailed, innerException) +{ + public ICliPreProcessor PreProcessor { get; } = preProcessor; +} From d7cbf479ae9fca1b52d92f5ea5706d22e612130f Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:20:44 +0100 Subject: [PATCH 8/9] Add unit tests to verify exception handling for pre- and post-processors in `DefaultCliHost` - Added tests to ensure exceptions thrown by pre-processors and post-processors are caught, and appropriate exit codes (`PreProcessorFailed`, `PostProcessorFailed`) are set. --- .../Hosting/DefaultCliHostTests.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs index e918a2a0..95f859ae 100644 --- a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs +++ b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs @@ -237,6 +237,130 @@ public async Task RunAsync_WhenCommandIsExecuted_ExecutesCommandPreAndPostProces .Be(17); } + [Fact] + public async Task RunAsync_PreProcessorThrowsException_ExceptionIsCatchedAndExitCodeSet() + { + // Arrange + var args = new[] { "run" }; + + var ansiConsole = A.Fake(); + var commandStore = A.Fake(); + var serviceProvider = A.Fake(); + var helpHandler = A.Fake(); + + var preProcessor = A.Fake(); + var secondPreProcessor = A.Fake(); + + SetupServiceProvider(serviceProvider, new CliCommandContext()); + + A.CallTo(() => helpHandler.ShouldPrintHelp(args)) + .Returns(false); + + A.CallTo(() => preProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + + A.CallTo(() => secondPreProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + + A.CallTo(() => preProcessor.ExecuteAsync(args)) + .Throws(new Exception("Test exception")); + + var commandInfo = new CliCommandInfo + { + CommandAttribute = new CliCommandAttribute(["run"]), + CommandType = typeof(DummyCommandWithResult) + }; + + var commandNode = new CliCommandNode(commandInfo, "run", null); + + A.CallTo(() => commandStore.FindCommandNode(args)) + .Returns(new FindCommandNodeResult(commandNode, [])); + + A.CallTo(() => serviceProvider.GetService(typeof(int))) + .Returns(17); + + var host = new DefaultCliHost( + ansiConsole, + commandStore, + serviceProvider, + helpHandler, + [preProcessor, secondPreProcessor], + []); + + // Act + var result = await host.RunAsync(args); + + // Assert + A.CallTo(() => secondPreProcessor.ExecuteAsync(args)) + .MustNotHaveHappened(); + + result.ExitCode + .Should() + .Be(CliExitCodes.PreProcessorFailed); + } + + [Fact] + public async Task RunAsync_PostProcessorThrowsException_ExceptionIsCatchedAndExitCodeSet() + { + // Arrange + var args = new[] { "run" }; + + var ansiConsole = A.Fake(); + var commandStore = A.Fake(); + var serviceProvider = A.Fake(); + var helpHandler = A.Fake(); + + var postProcessor = A.Fake(); + var secondPostProcessor = A.Fake(); + + SetupServiceProvider(serviceProvider, new CliCommandContext()); + + A.CallTo(() => helpHandler.ShouldPrintHelp(args)) + .Returns(false); + + A.CallTo(() => postProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + + A.CallTo(() => secondPostProcessor.ExecutionCondition) + .Returns(CliProcessorExecutionCondition.Always); + + A.CallTo(() => postProcessor.ExecuteAsync(A.Ignored)) + .Throws(new Exception("Test exception")); + + var commandInfo = new CliCommandInfo + { + CommandAttribute = new CliCommandAttribute(["run"]), + CommandType = typeof(DummyCommandWithResult) + }; + + var commandNode = new CliCommandNode(commandInfo, "run", null); + + A.CallTo(() => commandStore.FindCommandNode(args)) + .Returns(new FindCommandNodeResult(commandNode, [])); + + A.CallTo(() => serviceProvider.GetService(typeof(int))) + .Returns(17); + + var host = new DefaultCliHost( + ansiConsole, + commandStore, + serviceProvider, + helpHandler, + [], + [postProcessor, secondPostProcessor]); + + // Act + var result = await host.RunAsync(args); + + // Assert + A.CallTo(() => secondPostProcessor.ExecuteAsync(A.Ignored)) + .MustNotHaveHappened(); + + result.ExitCode + .Should() + .Be(CliExitCodes.PostProcessorFailed); + } + [Fact] public async Task RunAsync_WhenCommandWithoutOptions_ExecutesAndReturnsResult() { From 0c0d6de9b4be4d3e22de671afee7f3c1abb45c30 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:27:06 +0100 Subject: [PATCH 9/9] Add methods for pre- and post-processor registration and header/footer rendering in `ICliHostBuilder` and extensions - Introduced `RegisterPreProcessor` and `RegisterPostProcessor` methods to `ICliHostBuilder` for better encapsulation of processor logic. - Added `PrintHeaderText`, `PrintHeaderMarkup`, `PrintFooterText`, and `PrintFooterMarkup` extension methods for streamlined header/footer rendering support. - Enhanced usability by supporting optional execution conditions and configuration actions for processors. --- .../CliHostBuilderExtensions.cs | 72 +++++++++++++++++++ .../ICliHostBuilder.cs | 17 +++++ 2 files changed, 89 insertions(+) diff --git a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs index 3d00e370..21bfb4f7 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/CliHostBuilderExtensions.cs @@ -7,6 +7,24 @@ namespace CreativeCoders.Cli.Hosting; [ExcludeFromCodeCoverage] public static class CliHostBuilderExtensions { + /// + /// Registers a pre-processor to print a header as plain text. The header is displayed when the CLI application + /// is executed, before the command processing, based on the specified execution condition. + /// + /// + /// The instance to which the pre-processor is being added. + /// + /// + /// An enumerable collection of strings representing the lines of text to be displayed in the header. + /// Each string represents an individual line. + /// + /// + /// A value from that determines when the header should be displayed. + /// Defaults to . + /// + /// + /// The instance for method chaining. + /// public static ICliHostBuilder PrintHeaderText(this ICliHostBuilder builder, IEnumerable lines, CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) { @@ -18,6 +36,24 @@ public static ICliHostBuilder PrintHeaderText(this ICliHostBuilder builder, IEnu }); } + /// + /// Registers a pre-processor to print a header using markup text. The header is displayed during the CLI application + /// execution, before the command processing, based on the specified execution condition. + /// + /// + /// The instance to which the pre-processor is being added. + /// + /// + /// An enumerable collection of strings representing the lines of markup text to be displayed in the header. + /// The markup can include formatting instructions that are interpreted during rendering. + /// + /// + /// A value from that determines when the header should be displayed. + /// Defaults to . + /// + /// + /// The instance for method chaining. + /// public static ICliHostBuilder PrintHeaderMarkup(this ICliHostBuilder builder, IEnumerable lines, CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) { @@ -29,6 +65,24 @@ public static ICliHostBuilder PrintHeaderMarkup(this ICliHostBuilder builder, IE }); } + /// + /// Registers a post-processor to print a footer as plain text. The footer is displayed after the command processing + /// is complete, based on the specified execution condition. + /// + /// + /// The instance to which the post-processor is being added. + /// + /// + /// An enumerable collection of strings representing the lines of text to be displayed in the footer. + /// Each string represents an individual line. + /// + /// + /// A value from that determines when the footer should be displayed. + /// Defaults to . + /// + /// + /// The instance for method chaining. + /// public static ICliHostBuilder PrintFooterText(this ICliHostBuilder builder, IEnumerable lines, CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) { @@ -40,6 +94,24 @@ public static ICliHostBuilder PrintFooterText(this ICliHostBuilder builder, IEnu }); } + /// + /// Registers a post-processor to print a footer using markup formatting. The footer is displayed + /// after the command execution, based on the specified execution condition. + /// + /// + /// The instance to which the post-processor is being added. + /// + /// + /// An enumerable collection of strings representing the lines of footer text to be displayed. + /// Each string supports markup formatting for better visual representation. + /// + /// + /// A value from that determines when the footer should be displayed. + /// Defaults to . + /// + /// + /// The instance for method chaining. + /// public static ICliHostBuilder PrintFooterMarkup(this ICliHostBuilder builder, IEnumerable lines, CliProcessorExecutionCondition executionCondition = CliProcessorExecutionCondition.Always) { diff --git a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs index 6d4a2186..6dca7d9a 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/ICliHostBuilder.cs @@ -74,8 +74,25 @@ ICliHostBuilder UseContext(Action? configu /// The same instance. ICliHostBuilder SkipScanEntryAssembly(bool skipScanEntryAssembly = true); + /// + /// Registers a pre-processor for the CLI application. + /// + /// The type of the pre-processor to be registered. Must implement . + /// + /// An optional action to configure the instance of . The action receives + /// the instance as a parameter. + /// + /// The same instance. ICliHostBuilder RegisterPreProcessor(Action? configure = null) where T : class, ICliPreProcessor; + /// + /// Registers a post-processor to be executed after the CLI command is processed. + /// + /// The type of the post-processor that implements . + /// + /// An optional action to configure the post-processor instance. The action receives a single parameter of type . + /// + /// The same instance. ICliHostBuilder RegisterPostProcessor(Action? configure = null) where T : class, ICliPostProcessor; ///