diff --git a/samples/CliHostSampleApp/DemoCliCommand.cs b/samples/CliHostSampleApp/DemoCliCommand.cs index 2c5b8537..acef22a5 100644 --- a/samples/CliHostSampleApp/DemoCliCommand.cs +++ b/samples/CliHostSampleApp/DemoCliCommand.cs @@ -1,17 +1,21 @@ using CreativeCoders.Cli.Core; +using CreativeCoders.SysConsole.Core; using JetBrains.Annotations; +using Spectre.Console; namespace CliHostSampleApp; [UsedImplicitly] [CliCommand([DemoCommandGroup.Name, "do"] , AlternativeCommands = ["demo1"])] -public class DemoCliCommand : ICliCommand +public class DemoCliCommand(IAnsiConsole ansiConsole) : ICliCommand { public Task ExecuteAsync() { Console.WriteLine("Hello World from cli command !"); + ansiConsole.WriteLines("Test", 1234.ToString()); + return Task.FromResult(new CommandResult()); } } diff --git a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs index 87cd1c6d..05ade4f4 100644 --- a/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs +++ b/source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs @@ -135,6 +135,13 @@ public async Task RunAsync(string[] args) } catch (CliCommandConstructionFailedException e) { + var abortException = FindAbortException(e.InnerException); + + if (abortException != null) + { + return HandleCommandAbortException(abortException); + } + _ansiConsole.Markup( $"[red]Error creating command: {e.InnerException?.Message ?? "Unknown error"}[/] "); @@ -156,12 +163,7 @@ public async Task RunAsync(string[] args) } catch (CliCommandAbortException e) { - if (e.PrintMessage) - { - _ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]"); - } - - return new CliResult(e.ExitCode); + return HandleCommandAbortException(e); } catch (CliExitException e) { @@ -171,6 +173,33 @@ public async Task RunAsync(string[] args) } } + private static CliCommandAbortException? FindAbortException(Exception? exception) + { + var e = exception; + + while (e != null) + { + if (e is CliCommandAbortException abortException) + { + return abortException; + } + + e = e.InnerException; + } + + return null; + } + + private CliResult HandleCommandAbortException(CliCommandAbortException e) + { + if (e.PrintMessage) + { + _ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]"); + } + + return new CliResult(e.ExitCode); + } + private async Task ExecuteHelpPreProcessorsAsync(string[] args) { CliProcessorExecutionCondition[] conditions = diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs index 19531e17..ffcb62d4 100644 --- a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs @@ -23,7 +23,6 @@ public static IAnsiConsolePrint PrintBlock(this IAnsiConsole ansiConsole, bool c return new AnsiConsolePrint(ansiConsole); } - [PublicAPI] public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -34,7 +33,6 @@ public static IAnsiConsole Write(this IAnsiConsole ansiConsole, T value, Colo return ansiConsole; } - [PublicAPI] public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, Color foregroundColor, Color? backgroundColor = null) { @@ -45,7 +43,6 @@ public static IAnsiConsole WriteLine(this IAnsiConsole ansiConsole, T value, return ansiConsole; } - [PublicAPI] public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable items, TableColumnDef[] columns, Action? configureTable = null) { @@ -83,4 +80,24 @@ public static void PrintTable(this IAnsiConsole ansiConsole, IEnumerable i ansiConsole.Write(table); } + + public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, params string[] lines) + { + foreach (var line in lines) + { + ansiConsole.WriteLine(line); + } + + return ansiConsole; + } + + public static IAnsiConsole MarkupLines(this IAnsiConsole ansiConsole, params string[] lines) + { + foreach (var line in lines) + { + ansiConsole.MarkupLine(line); + } + + return ansiConsole; + } } diff --git a/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs new file mode 100644 index 00000000..4c5cfad4 --- /dev/null +++ b/source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleStringExtensions.cs @@ -0,0 +1,67 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using Spectre.Console; + +namespace CreativeCoders.SysConsole.Core; + +[ExcludeFromCodeCoverage] +[PublicAPI] +public static class AnsiConsoleStringExtensions +{ + public static string ToErrorMarkup(this string text) + { + return $"[red]{text}[/]"; + } + + public static string ToSuccessMarkup(this string text) + { + return $"[green]{text}[/]"; + } + + public static string ToWarningMarkup(this string text) + { + return $"[yellow]{text}[/]"; + } + + public static string ToInfoMarkup(this string text) + { + return $"[blue]{text}[/]"; + } + + public static string ToWhiteMarkup(this string text) + { + return $"[white]{text}[/]"; + } + + public static string ToBoldMarkup(this string text) + { + return $"[bold]{text}[/]"; + } + + public static string ToItalicMarkup(this string text) + { + return $"[italic]{text}[/]"; + } + + public static string ToUnderlineMarkup(this string text) + { + return $"[underline]{text}[/]"; + } + + public static string ToStrikethroughMarkup(this string text) + { + return $"[strikethrough]{text}[/]"; + } + + public static string ToLinkMarkup(this string text, string url = "") + { + return string.IsNullOrWhiteSpace(url) + ? $"[link]{text}[/]" + : $"[link={url}]{text}[/]"; + } + + public static string ToEscapedMarkup(this string text) + { + return Markup.Escape(text); + } +} diff --git a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs index 95f859ae..b6e7c3dc 100644 --- a/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs +++ b/tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs @@ -650,6 +650,44 @@ public async Task RunAsync_WhenCommandCreationFails_PrintsErrorAndReturnsExitCod .Be(CliExitCodes.CommandCreationFailed); } + [Fact] + public async Task RunAsync_WhenCommandCreationFailsWithAbort_PrintsErrorAndReturnsExitCodeFromAbort() + { + // Arrange + var args = new[] { "run" }; + + var ansiConsole = A.Fake(); + var commandStore = A.Fake(); + var serviceProvider = A.Fake(); + var helpHandler = A.Fake(); + + SetupServiceProvider(serviceProvider, null); + + A.CallTo(() => helpHandler.ShouldPrintHelp(args)) + .Returns(false); + + var commandInfo = new CliCommandInfo + { + CommandAttribute = new CliCommandAttribute(["run"]), + CommandType = typeof(FailingCommandWithAbort) + }; + + var commandNode = new CliCommandNode(commandInfo, "run", null); + + A.CallTo(() => commandStore.FindCommandNode(args)) + .Returns(new FindCommandNodeResult(commandNode, [])); + + var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []); + + // Act + var result = await host.RunAsync(args); + + // Assert + result.ExitCode + .Should() + .Be(FailingCommandWithAbort.ExitCode); + } + [Fact] public async Task RunAsync_WithOptionsValidation_ExecutesAndReturnsResult() { @@ -863,6 +901,22 @@ public Task ExecuteAsync() } } + private sealed class FailingCommandWithAbort : ICliCommand + { + public const int ExitCode = 123876; + + [UsedImplicitly] + public FailingCommandWithAbort() + { + throw new CliCommandAbortException("Failure in constructor", ExitCode); + } + + public Task ExecuteAsync() + { + return Task.FromResult(new CommandResult()); + } + } + private sealed class DummyCommandWithErrorAbortException : ICliCommand { public const int ExitCode = 12349876;