mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Show help text on demand (#49)
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.Dummy.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Demos how to show an error message then help text from an organizational command.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
CliFx.Tests.Dummy/Commands/ShowHelpTextOnErrorCommand.cs
Normal file
18
CliFx.Tests.Dummy/Commands/ShowHelpTextOnErrorCommand.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CliFx.Tests.Dummy.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Demos how to show help text from an organizational command.
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,5 +27,39 @@ namespace CliFx.Tests
|
|||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ namespace CliFx.Tests
|
|||||||
public partial class ErrorReportingSpecs
|
public partial class ErrorReportingSpecs
|
||||||
{
|
{
|
||||||
[Fact]
|
[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
|
// Arrange
|
||||||
await using var stdErr = new MemoryStream();
|
await using var stdErr = new MemoryStream();
|
||||||
@@ -29,8 +29,10 @@ namespace CliFx.Tests
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
exitCode.Should().NotBe(0);
|
exitCode.Should().NotBe(0);
|
||||||
stdErrData.Should().Contain("Kaput");
|
stdErrData.Should().ContainAll(
|
||||||
stdErrData.Length.Should().BeGreaterThan("Kaput".Length);
|
"System.Exception:",
|
||||||
|
"Kaput", "at",
|
||||||
|
"CliFx.Tests");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -80,5 +82,95 @@ namespace CliFx.Tests
|
|||||||
exitCode.Should().NotBe(0);
|
exitCode.Should().NotBe(0);
|
||||||
stdErrData.Should().NotBeEmpty();
|
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<string, string>());
|
||||||
|
|
||||||
|
var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().NotBe(0);
|
||||||
|
stdErrData.Should().Contain("Kaput");
|
||||||
|
stdErrData.Length.Should().BeGreaterThan("Kaput".Length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
|
using CliFx.Exceptions;
|
||||||
|
|
||||||
namespace CliFx.Tests
|
namespace CliFx.Tests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -143,6 +143,50 @@ namespace CliFx
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle <see cref="CommandException"/>s differently from the rest because we want to
|
||||||
|
/// display it different based on whether we are showing the help text or not.
|
||||||
|
/// </summary>
|
||||||
|
private int HandleCommandException(IReadOnlyList<string> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles <see cref="CliFxException"/>s by printing its error message if it has a value.
|
||||||
|
/// Otherwise, it prints the full stack trace from the exception.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cliFxException">The exception to handle.</param>
|
||||||
|
/// <returns>The exception's HResult.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
|
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -162,21 +206,23 @@ namespace CliFx
|
|||||||
HandleHelpOption(applicationSchema, commandLineInput) ??
|
HandleHelpOption(applicationSchema, commandLineInput) ??
|
||||||
await HandleCommandExecutionAsync(applicationSchema, commandLineInput, environmentVariables);
|
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.
|
// 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.
|
// Doing this also gets rid of the annoying Windows troubleshooting dialog that shows up on unhandled exceptions.
|
||||||
|
var exitCode = HandleCommandException(commandLineArguments, ce);
|
||||||
// Prefer showing message without stack trace on exceptions coming from CliFx or on CommandException
|
return exitCode;
|
||||||
var errorMessage = !string.IsNullOrWhiteSpace(ex.Message) && (ex is CliFxException || ex is CommandException)
|
}
|
||||||
? ex.Message
|
catch (CliFxException cfe)
|
||||||
: ex.ToString();
|
{
|
||||||
|
var hResult = HandleCliFxException(cfe);
|
||||||
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(errorMessage));
|
return hResult;
|
||||||
|
}
|
||||||
return ex is CommandException commandException
|
catch (Exception ex)
|
||||||
? commandException.ExitCode
|
{
|
||||||
: ex.HResult;
|
// For all other errors, we just write the entire thing to stderr.
|
||||||
|
_console.WithForegroundColor(ConsoleColor.Red, () => _console.Error.WriteLine(ex.ToString()));
|
||||||
|
return ex.HResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
CliFx/Exceptions/BaseCliFxException.cs
Normal file
43
CliFx/Exceptions/BaseCliFxException.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the base functionality for exceptions thrown within CliFx
|
||||||
|
/// or from one of its commands.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseCliFxException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to show the help text after handling this exception.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowHelp { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this exception was constructed with a message.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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.
|
||||||
|
/// </remarks>
|
||||||
|
public bool HasMessage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="BaseCliFxException"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected BaseCliFxException(string? message, bool showHelp = false)
|
||||||
|
: this(message, null, showHelp)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="BaseCliFxException"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected BaseCliFxException(string? message, Exception? innerException, bool showHelp = false)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
HasMessage = string.IsNullOrWhiteSpace(message) ? false : true;
|
||||||
|
ShowHelp = showHelp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,21 +9,21 @@ namespace CliFx.Exceptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Domain exception thrown within CliFx.
|
/// Domain exception thrown within CliFx.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CliFxException : Exception
|
public partial class CliFxException : BaseCliFxException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CliFxException"/>.
|
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CliFxException(string? message)
|
public CliFxException(string? message, bool showHelp = false)
|
||||||
: base(message)
|
: base(message, showHelp)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CliFxException"/>.
|
/// Initializes an instance of <see cref="CliFxException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CliFxException(string? message, Exception? innerException)
|
public CliFxException(string? message, Exception? innerException, bool showHelp = false)
|
||||||
: base(message, innerException)
|
: base(message, innerException, showHelp)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace CliFx.Exceptions
|
|||||||
/// Use this exception if you want to report an error that occured during execution of a command.
|
/// 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.
|
/// This exception also allows specifying exit code which will be returned to the calling process.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommandException : Exception
|
public class CommandException : BaseCliFxException
|
||||||
{
|
{
|
||||||
private const int DefaultExitCode = -100;
|
private const int DefaultExitCode = -100;
|
||||||
|
|
||||||
@@ -19,8 +19,9 @@ namespace CliFx.Exceptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandException"/>.
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandException(string? message, Exception? innerException, int exitCode = DefaultExitCode)
|
public CommandException(string? message, Exception? innerException,
|
||||||
: base(message, innerException)
|
int exitCode = DefaultExitCode, bool showHelp = false)
|
||||||
|
: base(message, innerException, showHelp)
|
||||||
{
|
{
|
||||||
ExitCode = exitCode != 0
|
ExitCode = exitCode != 0
|
||||||
? exitCode
|
? exitCode
|
||||||
@@ -30,16 +31,16 @@ namespace CliFx.Exceptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandException"/>.
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandException(string? message, int exitCode = DefaultExitCode)
|
public CommandException(string? message, int exitCode = DefaultExitCode, bool showHelp = false)
|
||||||
: this(message, null, exitCode)
|
: this(message, null, exitCode, showHelp)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandException"/>.
|
/// Initializes an instance of <see cref="CommandException"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CommandException(int exitCode = DefaultExitCode)
|
public CommandException(int exitCode = DefaultExitCode, bool showHelp = false)
|
||||||
: this(null, exitCode)
|
: this(null, exitCode, showHelp)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
Readme.md
28
Readme.md
@@ -430,6 +430,32 @@ Division by zero is not supported.
|
|||||||
1337
|
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
|
### 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.
|
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
|
## Etymology
|
||||||
|
|
||||||
CliFx is made out of "Cli" for "Command Line Interface" and "Fx" for "Framework". It's pronounced as "cliff ex".
|
CliFx is made out of "Cli" for "Command Line Interface" and "Fx" for "Framework". It's pronounced as "cliff ex".
|
||||||
|
|||||||
Reference in New Issue
Block a user