From c5c1852fc316b4bfd8ba6e560044de3dd77286d2 Mon Sep 17 00:00:00 2001 From: Nils Andresen Date: Sat, 30 Oct 2021 00:20:49 +0200 Subject: [PATCH] (#606) added ExceptionHandler to ICommandAppSettings So exceptions can be handled in Spectre.Console.Cli. Also, some documentation was added. --- docs/input/cli/exceptions.md | 116 ++++++++++++++++++ src/Spectre.Console/Cli/CommandApp.cs | 5 + .../Cli/ConfiguratorExtensions.cs | 34 +++++ .../Cli/ICommandAppSettings.cs | 10 ++ .../Configuration/CommandAppSettings.cs | 2 + .../Unit/Cli/CommandAppTests.Exceptions.cs | 100 +++++++++++++++ .../Unit/Cli/CommandAppTests.cs | 41 ------- 7 files changed, 267 insertions(+), 41 deletions(-) create mode 100644 docs/input/cli/exceptions.md create mode 100644 test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Exceptions.cs diff --git a/docs/input/cli/exceptions.md b/docs/input/cli/exceptions.md new file mode 100644 index 0000000..22d2092 --- /dev/null +++ b/docs/input/cli/exceptions.md @@ -0,0 +1,116 @@ +Title: Exceptions +Order: 12 +Description: "Handling exceptions in *Spectre.Console.Cli*" +--- + +Exceptions happen. + +`Spectre.Console.Cli` handles exceptions, writes a user friendly message to the console and sets the exitCode +of the application to `-1`. +While this might be enough for the needs of most applications, there are some options to customize this behavior. + +## Propagating exceptions + +The most basic way is to set `PropagateExceptions()` during configuration and handle everything. +While this option grants the most freedom, it also requires the most work: Setting `PropagateExceptions` +means that `Spectre.Console.Cli` effectively re-throws exceptions. +This means that `app.Run()` should be wrapped in a `try`-`catch`-block which has to handle the exception +(i.e. outputting something useful) and also provide the exitCode (or `return` value) for the application. + +```csharp +using Spectre.Console.Cli; + +namespace MyApp +{ + public static class Program + { + public static int Main(string[] args) + { + var app = new CommandApp(); + + app.Configure(config => + { + config.PropagateExceptions(); + }); + + try + { + return app.Run(args); + } + catch (Exception ex) + { + AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); + return -99; + } + } + } +} +``` + +## Using a custom ExceptionHandler + +Using the `SetErrorHandler()` during configuration it is possible to handle exceptions in a defined way. +This method comes in two flavours: One that uses the default exitCode (or `return` value) of `-1` and one +where the exitCode needs to be supplied. + +### Using `SetErrorHandler(Func handler)` + +Using this method exceptions can be handled in a custom way. The return value of the handler is used as +the exitCode for the application. + +```csharp +using Spectre.Console.Cli; + +namespace MyApp +{ + public static class Program + { + public static int Main(string[] args) + { + var app = new CommandApp(); + + app.Configure(config => + { + config.SetExceptionHandler(ex => + { + AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); + return -99; + }); + }); + + return app.Run(args); + } + } +} +``` + +### Using `SetErrorHandler(Action handler)` + +Using this method exceptions can be handled in a custom way, much the same as with the `SetErrorHandler(Func handler)`. +Using the `Action` as the handler however, it is not possible (or required) to supply a return value. +The exitCode for the application will be `-1`. + +```csharp +using Spectre.Console.Cli; + +namespace MyApp +{ + public static class Program + { + public static int Main(string[] args) + { + var app = new CommandApp(); + + app.Configure(config => + { + config.SetExceptionHandler(ex => + { + AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); + }); + }); + + return app.Run(args); + } + } +} +``` \ No newline at end of file diff --git a/src/Spectre.Console/Cli/CommandApp.cs b/src/Spectre.Console/Cli/CommandApp.cs index 0de2f7a..392bf44 100644 --- a/src/Spectre.Console/Cli/CommandApp.cs +++ b/src/Spectre.Console/Cli/CommandApp.cs @@ -103,6 +103,11 @@ namespace Spectre.Console.Cli throw; } + if (_configurator.Settings.ExceptionHandler != null) + { + return _configurator.Settings.ExceptionHandler(ex); + } + // Render the exception. var pretty = GetRenderableErrorMessage(ex); if (pretty != null) diff --git a/src/Spectre.Console/Cli/ConfiguratorExtensions.cs b/src/Spectre.Console/Cli/ConfiguratorExtensions.cs index 1ab577b..853ac60 100644 --- a/src/Spectre.Console/Cli/ConfiguratorExtensions.cs +++ b/src/Spectre.Console/Cli/ConfiguratorExtensions.cs @@ -224,5 +224,39 @@ namespace Spectre.Console.Cli return configurator.AddDelegate(name, (c, _) => func(c)); } + + /// + /// Sets the ExceptionsHandler. + /// Setting this way will use the + /// default exit code of -1. + /// + /// The configurator. + /// The Action that handles the exception. + /// A configurator that can be used to configure the application further. + public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Action exceptionHandler) + { + return configurator.SetExceptionHandler(ex => + { + exceptionHandler(ex); + return -1; + }); + } + + /// + /// Sets the ExceptionsHandler. + /// + /// The configurator. + /// The Action that handles the exception. + /// A configurator that can be used to configure the application further. + public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func? exceptionHandler) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.ExceptionHandler = exceptionHandler; + return configurator; + } } } diff --git a/src/Spectre.Console/Cli/ICommandAppSettings.cs b/src/Spectre.Console/Cli/ICommandAppSettings.cs index 7d3af6c..20580e9 100644 --- a/src/Spectre.Console/Cli/ICommandAppSettings.cs +++ b/src/Spectre.Console/Cli/ICommandAppSettings.cs @@ -1,3 +1,5 @@ +using System; + namespace Spectre.Console.Cli { /// @@ -43,6 +45,8 @@ namespace Spectre.Console.Cli /// /// Gets or sets a value indicating whether or not exceptions should be propagated. + /// Setting this to true will disable default Exception handling and + /// any , if set. /// bool PropagateExceptions { get; set; } @@ -50,5 +54,11 @@ namespace Spectre.Console.Cli /// Gets or sets a value indicating whether or not examples should be validated. /// bool ValidateExamples { get; set; } + + /// + /// Gets or sets a handler for Exceptions. + /// This handler will not be called, if is set to true. + /// + public Func? ExceptionHandler { get; set; } } } \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs index 5f07cb2..726d6fd 100644 --- a/src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs +++ b/src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs @@ -17,6 +17,8 @@ namespace Spectre.Console.Cli public ParsingMode ParsingMode => StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; + public Func? ExceptionHandler { get; set; } + public CommandAppSettings(ITypeRegistrar registrar) { Registrar = new TypeRegistrar(registrar); diff --git a/test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Exceptions.cs b/test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Exceptions.cs new file mode 100644 index 0000000..261be84 --- /dev/null +++ b/test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Exceptions.cs @@ -0,0 +1,100 @@ +using System; +using Shouldly; +using Spectre.Console.Cli; +using Spectre.Console.Tests.Data; +using Spectre.Console.Testing; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class Exception_Handling + { + [Fact] + public void Should_Not_Propagate_Runtime_Exceptions_If_Not_Explicitly_Told_To_Do_So() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + var result = app.Run(new[] { "animal", "4", "dog", "101", "--name", "Rufus" }); + + // Then + result.ExitCode.ShouldBe(-1); + } + + [Fact] + public void Should_Not_Propagate_Exceptions_If_Not_Explicitly_Told_To_Do_So() + { + // Given + var app = new CommandAppTester(); + app.Configure(config => + { + config.AddCommand("throw"); + }); + + // When + var result = app.Run(new[] { "throw" }); + + // Then + result.ExitCode.ShouldBe(-1); + } + + [Fact] + public void Should_Handle_Exceptions_If_ExceptionHandler_Is_Set_Using_Action() + { + // Given + var exceptionHandled = false; + var app = new CommandAppTester(); + app.Configure(config => + { + config.AddCommand("throw"); + config.SetExceptionHandler(_ => + { + exceptionHandled = true; + }); + }); + + // When + var result = app.Run(new[] { "throw" }); + + // Then + result.ExitCode.ShouldBe(-1); + exceptionHandled.ShouldBeTrue(); + } + + [Fact] + public void Should_Handle_Exceptions_If_ExceptionHandler_Is_Set_Using_Function() + { + // Given + var exceptionHandled = false; + var app = new CommandAppTester(); + app.Configure(config => + { + config.AddCommand("throw"); + config.SetExceptionHandler(_ => + { + exceptionHandled = true; + return -99; + }); + }); + + // When + var result = app.Run(new[] { "throw" }); + + // Then + result.ExitCode.ShouldBe(-99); + exceptionHandled.ShouldBeTrue(); + } + } + } +} diff --git a/test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs b/test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs index 42d0594..859a967 100644 --- a/test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs +++ b/test/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs @@ -816,46 +816,5 @@ namespace Spectre.Console.Tests.Unit.Cli result.Context.Remaining.Raw[4].ShouldBe("qux"); } } - - public sealed class Exception_Handling - { - [Fact] - public void Should_Not_Propagate_Runtime_Exceptions_If_Not_Explicitly_Told_To_Do_So() - { - // Given - var app = new CommandAppTester(); - app.Configure(config => - { - config.AddBranch("animal", animal => - { - animal.AddCommand("dog"); - animal.AddCommand("horse"); - }); - }); - - // When - var result = app.Run(new[] { "animal", "4", "dog", "101", "--name", "Rufus" }); - - // Then - result.ExitCode.ShouldBe(-1); - } - - [Fact] - public void Should_Not_Propagate_Exceptions_If_Not_Explicitly_Told_To_Do_So() - { - // Given - var app = new CommandAppTester(); - app.Configure(config => - { - config.AddCommand("throw"); - }); - - // When - var result = app.Run(new[] { "throw" }); - - // Then - result.ExitCode.ShouldBe(-1); - } - } } }