Skip to content

Commit 2436f63

Browse files
authored
Add some AnsiConsole extensions and string manipulation. Support throwing CliCommandAbortException while constructing commands (#77)
* Add `WriteLines` and `MarkupLines` extensions to `IAnsiConsole` and introduce `AnsiConsoleStringExtensions` for string markup utilities. * Extend `IAnsiConsole` methods to support params for line collections, add nullability attributes, and update `DemoCliCommand` usage. * Refactor exception handling in `DefaultCliHost` to centralize `CliCommandAbortException` processing through a dedicated method. * Enhance exception handling in `DefaultCliHost` to handle base exceptions for `CliCommandAbortException` and add test for abort scenarios. * Refactor `DefaultCliHost` to extract `FindAbortException` method for improved `CliCommandAbortException` handling. * Remove unused `[PublicAPI]` annotations in `AnsiConsoleExtensions` for cleaner code. * Update `WriteLines` and `MarkupLines` extensions to use `params string[]` instead of `params IEnumerable<string>` for improved usability.
1 parent c24052d commit 2436f63

5 files changed

Lines changed: 181 additions & 10 deletions

File tree

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
using CreativeCoders.Cli.Core;
2+
using CreativeCoders.SysConsole.Core;
23
using JetBrains.Annotations;
4+
using Spectre.Console;
35

46
namespace CliHostSampleApp;
57

68
[UsedImplicitly]
79
[CliCommand([DemoCommandGroup.Name, "do"]
810
, AlternativeCommands = ["demo1"])]
9-
public class DemoCliCommand : ICliCommand
11+
public class DemoCliCommand(IAnsiConsole ansiConsole) : ICliCommand
1012
{
1113
public Task<CommandResult> ExecuteAsync()
1214
{
1315
Console.WriteLine("Hello World from cli command !");
1416

17+
ansiConsole.WriteLines("Test", 1234.ToString());
18+
1519
return Task.FromResult(new CommandResult());
1620
}
1721
}

source/Cli/CreativeCoders.Cli.Hosting/DefaultCliHost.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ public async Task<CliResult> RunAsync(string[] args)
135135
}
136136
catch (CliCommandConstructionFailedException e)
137137
{
138+
var abortException = FindAbortException(e.InnerException);
139+
140+
if (abortException != null)
141+
{
142+
return HandleCommandAbortException(abortException);
143+
}
144+
138145
_ansiConsole.Markup(
139146
$"[red]Error creating command: {e.InnerException?.Message ?? "Unknown error"}[/] ");
140147

@@ -156,12 +163,7 @@ public async Task<CliResult> RunAsync(string[] args)
156163
}
157164
catch (CliCommandAbortException e)
158165
{
159-
if (e.PrintMessage)
160-
{
161-
_ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]");
162-
}
163-
164-
return new CliResult(e.ExitCode);
166+
return HandleCommandAbortException(e);
165167
}
166168
catch (CliExitException e)
167169
{
@@ -171,6 +173,33 @@ public async Task<CliResult> RunAsync(string[] args)
171173
}
172174
}
173175

176+
private static CliCommandAbortException? FindAbortException(Exception? exception)
177+
{
178+
var e = exception;
179+
180+
while (e != null)
181+
{
182+
if (e is CliCommandAbortException abortException)
183+
{
184+
return abortException;
185+
}
186+
187+
e = e.InnerException;
188+
}
189+
190+
return null;
191+
}
192+
193+
private CliResult HandleCommandAbortException(CliCommandAbortException e)
194+
{
195+
if (e.PrintMessage)
196+
{
197+
_ansiConsole.MarkupLine(e.IsError ? $"[red]{e.Message}[/]" : $"[yellow]{e.Message}[/]");
198+
}
199+
200+
return new CliResult(e.ExitCode);
201+
}
202+
174203
private async Task ExecuteHelpPreProcessorsAsync(string[] args)
175204
{
176205
CliProcessorExecutionCondition[] conditions =

source/SysConsole/CreativeCoders.SysConsole.Core/AnsiConsoleExtensions.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ public static IAnsiConsolePrint PrintBlock(this IAnsiConsole ansiConsole, bool c
2323
return new AnsiConsolePrint(ansiConsole);
2424
}
2525

26-
[PublicAPI]
2726
public static IAnsiConsole Write<T>(this IAnsiConsole ansiConsole, T value, Color foregroundColor,
2827
Color? backgroundColor = null)
2928
{
@@ -34,7 +33,6 @@ public static IAnsiConsole Write<T>(this IAnsiConsole ansiConsole, T value, Colo
3433
return ansiConsole;
3534
}
3635

37-
[PublicAPI]
3836
public static IAnsiConsole WriteLine<T>(this IAnsiConsole ansiConsole, T value, Color foregroundColor,
3937
Color? backgroundColor = null)
4038
{
@@ -45,7 +43,6 @@ public static IAnsiConsole WriteLine<T>(this IAnsiConsole ansiConsole, T value,
4543
return ansiConsole;
4644
}
4745

48-
[PublicAPI]
4946
public static void PrintTable<T>(this IAnsiConsole ansiConsole, IEnumerable<T> items,
5047
TableColumnDef<T>[] columns, Action<Table>? configureTable = null)
5148
{
@@ -83,4 +80,24 @@ public static void PrintTable<T>(this IAnsiConsole ansiConsole, IEnumerable<T> i
8380

8481
ansiConsole.Write(table);
8582
}
83+
84+
public static IAnsiConsole WriteLines(this IAnsiConsole ansiConsole, params string[] lines)
85+
{
86+
foreach (var line in lines)
87+
{
88+
ansiConsole.WriteLine(line);
89+
}
90+
91+
return ansiConsole;
92+
}
93+
94+
public static IAnsiConsole MarkupLines(this IAnsiConsole ansiConsole, params string[] lines)
95+
{
96+
foreach (var line in lines)
97+
{
98+
ansiConsole.MarkupLine(line);
99+
}
100+
101+
return ansiConsole;
102+
}
86103
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using JetBrains.Annotations;
3+
using Spectre.Console;
4+
5+
namespace CreativeCoders.SysConsole.Core;
6+
7+
[ExcludeFromCodeCoverage]
8+
[PublicAPI]
9+
public static class AnsiConsoleStringExtensions
10+
{
11+
public static string ToErrorMarkup(this string text)
12+
{
13+
return $"[red]{text}[/]";
14+
}
15+
16+
public static string ToSuccessMarkup(this string text)
17+
{
18+
return $"[green]{text}[/]";
19+
}
20+
21+
public static string ToWarningMarkup(this string text)
22+
{
23+
return $"[yellow]{text}[/]";
24+
}
25+
26+
public static string ToInfoMarkup(this string text)
27+
{
28+
return $"[blue]{text}[/]";
29+
}
30+
31+
public static string ToWhiteMarkup(this string text)
32+
{
33+
return $"[white]{text}[/]";
34+
}
35+
36+
public static string ToBoldMarkup(this string text)
37+
{
38+
return $"[bold]{text}[/]";
39+
}
40+
41+
public static string ToItalicMarkup(this string text)
42+
{
43+
return $"[italic]{text}[/]";
44+
}
45+
46+
public static string ToUnderlineMarkup(this string text)
47+
{
48+
return $"[underline]{text}[/]";
49+
}
50+
51+
public static string ToStrikethroughMarkup(this string text)
52+
{
53+
return $"[strikethrough]{text}[/]";
54+
}
55+
56+
public static string ToLinkMarkup(this string text, string url = "")
57+
{
58+
return string.IsNullOrWhiteSpace(url)
59+
? $"[link]{text}[/]"
60+
: $"[link={url}]{text}[/]";
61+
}
62+
63+
public static string ToEscapedMarkup(this string text)
64+
{
65+
return Markup.Escape(text);
66+
}
67+
}

tests/CreativeCoders.Cli.Tests/Hosting/DefaultCliHostTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,44 @@ public async Task RunAsync_WhenCommandCreationFails_PrintsErrorAndReturnsExitCod
650650
.Be(CliExitCodes.CommandCreationFailed);
651651
}
652652

653+
[Fact]
654+
public async Task RunAsync_WhenCommandCreationFailsWithAbort_PrintsErrorAndReturnsExitCodeFromAbort()
655+
{
656+
// Arrange
657+
var args = new[] { "run" };
658+
659+
var ansiConsole = A.Fake<IAnsiConsole>();
660+
var commandStore = A.Fake<ICliCommandStore>();
661+
var serviceProvider = A.Fake<IServiceProvider>();
662+
var helpHandler = A.Fake<ICliCommandHelpHandler>();
663+
664+
SetupServiceProvider(serviceProvider, null);
665+
666+
A.CallTo(() => helpHandler.ShouldPrintHelp(args))
667+
.Returns(false);
668+
669+
var commandInfo = new CliCommandInfo
670+
{
671+
CommandAttribute = new CliCommandAttribute(["run"]),
672+
CommandType = typeof(FailingCommandWithAbort)
673+
};
674+
675+
var commandNode = new CliCommandNode(commandInfo, "run", null);
676+
677+
A.CallTo(() => commandStore.FindCommandNode(args))
678+
.Returns(new FindCommandNodeResult<CliCommandNode>(commandNode, []));
679+
680+
var host = new DefaultCliHost(ansiConsole, commandStore, serviceProvider, helpHandler, [], []);
681+
682+
// Act
683+
var result = await host.RunAsync(args);
684+
685+
// Assert
686+
result.ExitCode
687+
.Should()
688+
.Be(FailingCommandWithAbort.ExitCode);
689+
}
690+
653691
[Fact]
654692
public async Task RunAsync_WithOptionsValidation_ExecutesAndReturnsResult()
655693
{
@@ -863,6 +901,22 @@ public Task<CommandResult> ExecuteAsync()
863901
}
864902
}
865903

904+
private sealed class FailingCommandWithAbort : ICliCommand
905+
{
906+
public const int ExitCode = 123876;
907+
908+
[UsedImplicitly]
909+
public FailingCommandWithAbort()
910+
{
911+
throw new CliCommandAbortException("Failure in constructor", ExitCode);
912+
}
913+
914+
public Task<CommandResult> ExecuteAsync()
915+
{
916+
return Task.FromResult(new CommandResult());
917+
}
918+
}
919+
866920
private sealed class DummyCommandWithErrorAbortException : ICliCommand
867921
{
868922
public const int ExitCode = 12349876;

0 commit comments

Comments
 (0)