diff --git a/CliFx.Tests.Dummy/Commands/ShowErrorMessageThenHelpTextOnErrorCommand.cs b/CliFx.Tests.Dummy/Commands/ShowErrorMessageThenHelpTextOnErrorCommand.cs
new file mode 100644
index 0000000..6a2e453
--- /dev/null
+++ b/CliFx.Tests.Dummy/Commands/ShowErrorMessageThenHelpTextOnErrorCommand.cs
@@ -0,0 +1,20 @@
+using CliFx.Attributes;
+using CliFx.Exceptions;
+using System.Threading.Tasks;
+
+namespace CliFx.Tests.Dummy.Commands
+{
+ ///
+ /// Demos how to show an error message then help text from an organizational command.
+ ///
+ [Command("cmd-err", Description = "This is an organizational command. " +
+ "I don't do anything except provide a route to my subcommands. " +
+ "If you use just me, I print an error message then the help text " +
+ "to remind you of my subcommands.")]
+ public class ShowErrorMessageThenHelpTextOnCommandExceptionCommand : ICommand
+ {
+ public ValueTask ExecuteAsync(IConsole console) =>
+ throw new CommandException("It is an error to use me without a subcommand. " +
+ "Please refer to the help text below for guidance.", showHelp: true);
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests.Dummy/Commands/ShowHelpTextOnErrorCommand.cs b/CliFx.Tests.Dummy/Commands/ShowHelpTextOnErrorCommand.cs
new file mode 100644
index 0000000..3e6fc85
--- /dev/null
+++ b/CliFx.Tests.Dummy/Commands/ShowHelpTextOnErrorCommand.cs
@@ -0,0 +1,18 @@
+using CliFx.Attributes;
+using CliFx.Exceptions;
+using System.Threading.Tasks;
+
+namespace CliFx.Tests.Dummy.Commands
+{
+ ///
+ /// Demos how to show help text from an organizational command.
+ ///
+ [Command("cmd", Description = "This is an organizational command. " +
+ "I don't do anything except provide a route to my subcommands. " +
+ "If you use just me, I print the help text to remind you of my subcommands.")]
+ public class ShowHelpTextOnCommandExceptionCommand : ICommand
+ {
+ public ValueTask ExecuteAsync(IConsole console) =>
+ throw new CommandException(null, showHelp: false);
+ }
+}
\ No newline at end of file
diff --git a/CliFx.Tests/ErrorReportingSpecs.Commands.cs b/CliFx.Tests/ErrorReportingSpecs.Commands.cs
index c4e5a5f..d8e247c 100644
--- a/CliFx.Tests/ErrorReportingSpecs.Commands.cs
+++ b/CliFx.Tests/ErrorReportingSpecs.Commands.cs
@@ -27,5 +27,39 @@ namespace CliFx.Tests
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
}
+
+ [Command("exc")]
+ private class ShowHelpTextOnlyCommand : ICommand
+ {
+ public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(null, showHelp: true);
+ }
+
+ [Command("exc sub")]
+ private class ShowHelpTextOnlySubCommand : ICommand
+ {
+ public ValueTask ExecuteAsync(IConsole console) => default;
+ }
+
+ [Command("exc")]
+ private class ShowErrorMessageThenHelpTextCommand : ICommand
+ {
+ public ValueTask ExecuteAsync(IConsole console) =>
+ throw new CommandException("Error message.", showHelp: true);
+ }
+
+ [Command("exc sub")]
+ private class ShowErrorMessageThenHelpTextSubCommand : ICommand
+ {
+ public ValueTask ExecuteAsync(IConsole console) => default;
+ }
+
+ [Command("exc")]
+ private class StackTraceOnlyCommand : ICommand
+ {
+ [CommandOption("msg", 'm')]
+ public string? Message { get; set; }
+
+ public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(null);
+ }
}
}
\ No newline at end of file
diff --git a/CliFx.Tests/ErrorReportingSpecs.cs b/CliFx.Tests/ErrorReportingSpecs.cs
index 9515d7d..329b8c9 100644
--- a/CliFx.Tests/ErrorReportingSpecs.cs
+++ b/CliFx.Tests/ErrorReportingSpecs.cs
@@ -9,7 +9,7 @@ namespace CliFx.Tests
public partial class ErrorReportingSpecs
{
[Fact]
- public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_full_error_details()
+ public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_error_message_and_stack_trace()
{
// Arrange
await using var stdErr = new MemoryStream();
@@ -29,8 +29,10 @@ namespace CliFx.Tests
// Assert
exitCode.Should().NotBe(0);
- stdErrData.Should().Contain("Kaput");
- stdErrData.Length.Should().BeGreaterThan("Kaput".Length);
+ stdErrData.Should().ContainAll(
+ "System.Exception:",
+ "Kaput", "at",
+ "CliFx.Tests");
}
[Fact]
@@ -80,5 +82,95 @@ namespace CliFx.Tests
exitCode.Should().NotBe(0);
stdErrData.Should().NotBeEmpty();
}
+
+ [Fact]
+ public async Task Command_may_throw_a_specialized_exception_which_shows_only_the_help_text()
+ {
+ // Arrange
+ await using var stdOut = new MemoryStream();
+ await using var stdErr = new MemoryStream();
+
+ var console = new VirtualConsole(output: stdOut);
+
+ var application = new CliApplicationBuilder()
+ .AddCommand(typeof(ShowHelpTextOnlyCommand))
+ .AddCommand(typeof(ShowHelpTextOnlySubCommand))
+ .UseConsole(console)
+ .Build();
+
+ // Act
+ await application.RunAsync(new[] { "exc" });
+ var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
+ var stdErrData = console.Output.Encoding.GetString(stdErr.ToArray()).TrimEnd();
+
+ // Assert
+ stdErrData.Should().BeEmpty();
+ stdOutData.Should().ContainAll(
+ "Usage",
+ "[command]",
+ "Options",
+ "-h|--help", "Shows help text.",
+ "Commands",
+ "sub",
+ "You can run", "to show help on a specific command."
+ );
+ }
+
+ [Fact]
+ public async Task Command_may_throw_specialized_exception_which_shows_the_error_message_then_the_help_text()
+ {
+ // Arrange
+ await using var stdOut = new MemoryStream();
+ await using var stdErr = new MemoryStream();
+ var console = new VirtualConsole(output: stdOut, error: stdErr);
+
+ var application = new CliApplicationBuilder()
+ .AddCommand(typeof(ShowErrorMessageThenHelpTextCommand))
+ .AddCommand(typeof(ShowErrorMessageThenHelpTextSubCommand))
+ .UseConsole(console)
+ .Build();
+
+ // Act
+ await application.RunAsync(new[] { "exc" });
+ var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
+ var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd();
+
+ // Assert
+ stdErrData.Should().Be("Error message.");
+ stdOutData.Should().ContainAll(
+ "Usage",
+ "[command]",
+ "Options",
+ "-h|--help", "Shows help text.",
+ "Commands",
+ "sub",
+ "You can run", "to show help on a specific command."
+ );
+ }
+
+ [Fact]
+ public async Task Command_may_throw_a_specialized_exception_which_shows_only_a_stack_trace_and_no_help_text()
+ {
+ // Arrange
+ await using var stdErr = new MemoryStream();
+ var console = new VirtualConsole(error: stdErr);
+
+ var application = new CliApplicationBuilder()
+ .AddCommand(typeof(GenericExceptionCommand))
+ .UseConsole(console)
+ .Build();
+
+ // Act
+ var exitCode = await application.RunAsync(
+ new[] { "exc", "-m", "Kaput" },
+ new Dictionary());
+
+ var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
+
+ // Assert
+ exitCode.Should().NotBe(0);
+ stdErrData.Should().Contain("Kaput");
+ stdErrData.Length.Should().BeGreaterThan("Kaput".Length);
+ }
}
}
\ No newline at end of file
diff --git a/CliFx.Tests/HelpTextSpecs.Commands.cs b/CliFx.Tests/HelpTextSpecs.Commands.cs
index dccc2a8..4a3198c 100644
--- a/CliFx.Tests/HelpTextSpecs.Commands.cs
+++ b/CliFx.Tests/HelpTextSpecs.Commands.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using CliFx.Attributes;
+using CliFx.Exceptions;
namespace CliFx.Tests
{
diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs
index d4b48b4..e892e26 100644
--- a/CliFx/CliApplication.cs
+++ b/CliFx/CliApplication.cs
@@ -143,6 +143,50 @@ namespace CliFx
return 0;
}
+ ///
+ /// Handle s differently from the rest because we want to
+ /// display it different based on whether we are showing the help text or not.
+ ///
+ private int HandleCommandException(IReadOnlyList commandLineArguments, CommandException commandException)
+ {
+ var showHelp = commandException.ShowHelp;
+
+ var errorMessage = commandException.HasMessage
+ ? commandException.Message
+ : commandException.ToString();
+
+ _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
+
+ if (showHelp)
+ {
+ var applicationSchema = ApplicationSchema.Resolve(_configuration.CommandTypes);
+ var commandLineInput = CommandLineInput.Parse(commandLineArguments);
+ var commandSchema = applicationSchema.TryFindCommand(commandLineInput) ??
+ CommandSchema.StubDefaultCommand;
+ _helpTextWriter.Write(applicationSchema, commandSchema);
+ }
+
+ return commandException.ExitCode;
+ }
+
+ ///
+ /// Handles s by printing its error message if it has a value.
+ /// Otherwise, it prints the full stack trace from the exception.
+ ///
+ /// The exception to handle.
+ /// The exception's HResult.
+ private int HandleCliFxException(CliFxException cliFxException)
+ {
+ // Prefer showing message without stack trace on CliFxExceptions.
+ var errorMessage = cliFxException.HasMessage
+ ? cliFxException.Message
+ : cliFxException.ToString();
+
+ _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
+
+ return cliFxException.HResult;
+ }
+
///
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
///
@@ -162,21 +206,23 @@ namespace CliFx
HandleHelpOption(applicationSchema, commandLineInput) ??
await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
}
- catch (Exception ex)
+ catch (CommandException ce)
{
// We want to catch exceptions in order to print errors and return correct exit codes.
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
-
- // Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException
- var errorMessage = !string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)
- ? ex.Message
- : ex.ToString();
-
- _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
-
- return ex is CommandException commandException
- ? commandException.ExitCode
- : ex.HResult;
+ var exitCode = HandleCommandException(commandLineArguments, ce);
+ return exitCode;
+ }
+ catch (CliFxException cfe)
+ {
+ var hResult = HandleCliFxException(cfe);
+ return hResult;
+ }
+ catch (Exception ex)
+ {
+ // For all other errors, we just write the entire thing to stderr.
+ _console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.ToString()));
+ return ex.HResult;
}
}
diff --git a/CliFx/Exceptions/BaseCliFxException.cs b/CliFx/Exceptions/BaseCliFxException.cs
new file mode 100644
index 0000000..1b9b320
--- /dev/null
+++ b/CliFx/Exceptions/BaseCliFxException.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace CliFx.Exceptions
+{
+ ///
+ /// Provides the base functionality for exceptions thrown within CliFx
+ /// or from one of its commands.
+ ///
+ public abstract class BaseCliFxException : Exception
+ {
+ ///
+ /// Whether to show the help text after handling this exception.
+ ///
+ public bool ShowHelp { get; }
+
+ ///
+ /// Whether this exception was constructed with a message.
+ ///
+ ///
+ /// We cannot check against the 'Message' property because it will always return
+ /// a default message if it was constructed with a null value or is currently null.
+ ///
+ public bool HasMessage { get; }
+
+ ///
+ /// Initializes an instance of .
+ ///
+ protected BaseCliFxException(string? message, bool showHelp = false)
+ : this(message, null, showHelp)
+ {
+ }
+
+ ///
+ /// Initializes an instance of .
+ ///
+ protected BaseCliFxException(string? message, Exception? innerException, bool showHelp = false)
+ : base(message, innerException)
+ {
+ HasMessage = string.IsNullOrWhiteSpace(message) ? false : true;
+ ShowHelp = showHelp;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CliFx/Exceptions/CliFxException.cs b/CliFx/Exceptions/CliFxException.cs
index 47eaa9b..ca36ca5 100644
--- a/CliFx/Exceptions/CliFxException.cs
+++ b/CliFx/Exceptions/CliFxException.cs
@@ -9,21 +9,21 @@ namespace CliFx.Exceptions
///
/// Domain exception thrown within CliFx.
///
- public partial class CliFxException : Exception
+ public partial class CliFxException : BaseCliFxException
{
///
/// Initializes an instance of .
///
- public CliFxException(string? message)
- : base(message)
+ public CliFxException(string? message, bool showHelp = false)
+ : base(message, showHelp)
{
}
///
/// Initializes an instance of .
///
- public CliFxException(string? message, Exception? innerException)
- : base(message, innerException)
+ public CliFxException(string? message, Exception? innerException, bool showHelp = false)
+ : base(message, innerException, showHelp)
{
}
}
diff --git a/CliFx/Exceptions/CommandException.cs b/CliFx/Exceptions/CommandException.cs
index f43b4e9..8d9b4fb 100644
--- a/CliFx/Exceptions/CommandException.cs
+++ b/CliFx/Exceptions/CommandException.cs
@@ -7,7 +7,7 @@ namespace CliFx.Exceptions
/// Use this exception if you want to report an error that occured during execution of a command.
/// This exception also allows specifying exit code which will be returned to the calling process.
///
- public class CommandException : Exception
+ public class CommandException : BaseCliFxException
{
private const int DefaultExitCode = -100;
@@ -19,8 +19,9 @@ namespace CliFx.Exceptions
///
/// Initializes an instance of .
///
- public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode)
- : base(message, innerException)
+ public CommandException(string? message, Exception? innerException,
+ int exitCode = DefaultExitCode, bool showHelp = false)
+ : base(message, innerException, showHelp)
{
ExitCode = exitCode != 0
? exitCode
@@ -30,16 +31,16 @@ namespace CliFx.Exceptions
///
/// Initializes an instance of .
///
- public CommandException(string? message, int exitCode = DefaultExitCode)
- : this(message, null, exitCode)
+ public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false)
+ : this(message, null, exitCode, showHelp)
{
}
///
/// Initializes an instance of .
///
- public CommandException(int exitCode = DefaultExitCode)
- : this(null, exitCode)
+ public CommandException(int exitCode = DefaultExitCode, bool showHelp = false)
+ : this(null, exitCode, showHelp)
{
}
}
diff --git a/Readme.md b/Readme.md
index 8f6796d..d877a85 100644
--- a/Readme.md
+++ b/Readme.md
@@ -430,6 +430,32 @@ Division by zero is not supported.
1337
```
+You can use the `showHelp` parameter to choose whether to show the help text after handling an exception. For example, you can tell CliFx to show `ExampleCommand`'s help text upon an error like this:
+
+```c#
+[Command]
+public class ExampleCommand : ICommand
+{
+ public ValueTask ExecuteAsync(IConsole console)
+ {
+ throw new CommandException(showHelp: true);
+ }
+}
+```
+
+To display an error message before the help text, throw the `CommandException` like this:
+
+```c#
+[Command]
+public class ExampleCommand : ICommand
+{
+ public ValueTask ExecuteAsync(IConsole console)
+ {
+ throw new CommandException("My custom error message.", showHelp: true);
+ }
+}
+```
+
### Graceful cancellation
It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break), but you can easily override this behavior.
@@ -670,4 +696,4 @@ Frequency=3124994 Hz, Resolution=320.0006 ns, Timer=TSC
## Etymology
-CliFx is made out of "Cli" for "Command Line Interface" and "Fx" for "Framework". It's pronounced as "cliff ex".
\ No newline at end of file
+CliFx is made out of "Cli" for "Command Line Interface" and "Fx" for "Framework". It's pronounced as "cliff ex".