Skip to content
6 changes: 5 additions & 1 deletion samples/CliHostSampleApp/DemoCliCommand.cs
Original file line number Diff line number Diff line change
@@ -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<CommandResult> ExecuteAsync()
{
Console.WriteLine("Hello World from cli command !");

ansiConsole.WriteLines("Test", 1234.ToString());
Comment thread
darthsharp marked this conversation as resolved.

return Task.FromResult(new CommandResult());
}
}
41 changes: 35 additions & 6 deletions source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ public async Task<CliResult> 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"}[/] ");

Expand All @@ -156,12 +163,7 @@ public async Task<CliResult> 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)
{
Expand All @@ -171,6 +173,33 @@ public async Task<CliResult> 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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ public static IAnsiConsolePrint PrintBlock(this IAnsiConsole ansiConsole, bool c
return new AnsiConsolePrint(ansiConsole);
}

[PublicAPI]
public static IAnsiConsole Write<T>(this IAnsiConsole ansiConsole, T value, Color foregroundColor,
Color? backgroundColor = null)
{
Expand All @@ -34,7 +33,6 @@ public static IAnsiConsole Write<T>(this IAnsiConsole ansiConsole, T value, Colo
return ansiConsole;
}

[PublicAPI]
public static IAnsiConsole WriteLine<T>(this IAnsiConsole ansiConsole, T value, Color foregroundColor,
Color? backgroundColor = null)
{
Expand All @@ -45,7 +43,6 @@ public static IAnsiConsole WriteLine<T>(this IAnsiConsole ansiConsole, T value,
return ansiConsole;
}

[PublicAPI]
public static void PrintTable<T>(this IAnsiConsole ansiConsole, IEnumerable<T> items,
TableColumnDef<T>[] columns, Action<Table>? configureTable = null)
{
Expand Down Expand Up @@ -83,4 +80,24 @@ public static void PrintTable<T>(this IAnsiConsole ansiConsole, IEnumerable<T> 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)
{
Comment thread
darthsharp marked this conversation as resolved.
foreach (var line in lines)
{
ansiConsole.MarkupLine(line);
}

return ansiConsole;
}
}
Original file line number Diff line number Diff line change
@@ -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}[/]";
Comment thread
darthsharp marked this conversation as resolved.
}

public static string ToEscapedMarkup(this string text)
{
return Markup.Escape(text);
}
}
54 changes: 54 additions & 0 deletions tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAnsiConsole>();
var commandStore = A.Fake<ICliCommandStore>();
var serviceProvider = A.Fake<IServiceProvider>();
var helpHandler = A.Fake<ICliCommandHelpHandler>();

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<CliCommandNode>(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()
{
Expand Down Expand Up @@ -863,6 +901,22 @@ public Task<CommandResult> ExecuteAsync()
}
}

private sealed class FailingCommandWithAbort : ICliCommand
{
public const int ExitCode = 123876;

[UsedImplicitly]
public FailingCommandWithAbort()
{
throw new CliCommandAbortException("Failure in constructor", ExitCode);
}

public Task<CommandResult> ExecuteAsync()
{
return Task.FromResult(new CommandResult());
}
}

private sealed class DummyCommandWithErrorAbortException : ICliCommand
{
public const int ExitCode = 12349876;
Expand Down