diff --git a/CliFx.Tests/ErrorReportingSpecs.cs b/CliFx.Tests/ErrorReportingSpecs.cs index eb88bff..e1205ff 100644 --- a/CliFx.Tests/ErrorReportingSpecs.cs +++ b/CliFx.Tests/ErrorReportingSpecs.cs @@ -3,11 +3,16 @@ using System.IO; using System.Threading.Tasks; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace CliFx.Tests { public partial class ErrorReportingSpecs { + private readonly ITestOutputHelper _output; + + public ErrorReportingSpecs(ITestOutputHelper output) => _output = output; + [Fact] public async Task Command_may_throw_a_generic_exception_which_exits_and_prints_error_message_and_stack_trace() { @@ -31,8 +36,10 @@ namespace CliFx.Tests exitCode.Should().NotBe(0); stdErrData.Should().ContainAll( "System.Exception:", - "Kaput", "at", + "Kaput", "at", "CliFx.Tests"); + + _output.WriteLine(stdErrData); } [Fact] @@ -57,6 +64,8 @@ namespace CliFx.Tests // Assert exitCode.Should().Be(69); stdErrData.Should().Be("Kaput"); + + _output.WriteLine(stdErrData); } [Fact] @@ -81,6 +90,8 @@ namespace CliFx.Tests // Assert exitCode.Should().NotBe(0); stdErrData.Should().NotBeEmpty(); + + _output.WriteLine(stdErrData); } [Fact] @@ -99,7 +110,7 @@ namespace CliFx.Tests .Build(); // Act - await application.RunAsync(new[] { "exc" }); + await application.RunAsync(new[] {"exc"}); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); var stdErrData = console.Output.Encoding.GetString(stdErr.ToArray()).TrimEnd(); @@ -114,6 +125,9 @@ namespace CliFx.Tests "sub", "You can run", "to show help on a specific command." ); + + _output.WriteLine(stdOutData); + _output.WriteLine(stdErrData); } [Fact] @@ -131,7 +145,7 @@ namespace CliFx.Tests .Build(); // Act - await application.RunAsync(new[] { "exc" }); + await application.RunAsync(new[] {"exc"}); var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); var stdOutData = console.Output.Encoding.GetString(stdOut.ToArray()).TrimEnd(); @@ -146,6 +160,9 @@ namespace CliFx.Tests "sub", "You can run", "to show help on a specific command." ); + + _output.WriteLine(stdOutData); + _output.WriteLine(stdErrData); } [Fact] @@ -162,7 +179,7 @@ namespace CliFx.Tests // Act var exitCode = await application.RunAsync( - new[] { "exc", "-m", "Kaput" }, + new[] {"exc", "-m", "Kaput"}, new Dictionary()); var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); @@ -173,6 +190,8 @@ namespace CliFx.Tests "System.Exception:", "Kaput", "at", "CliFx.Tests"); + + _output.WriteLine(stdErrData); } [Fact] @@ -190,7 +209,7 @@ namespace CliFx.Tests // Act var exitCode = await application.RunAsync( - new[] { "not-a-valid-command", "-r", "foo" }, + new[] {"not-a-valid-command", "-r", "foo"}, new Dictionary()); var stdErrData = console.Error.Encoding.GetString(stdErr.ToArray()).TrimEnd(); @@ -199,8 +218,9 @@ namespace CliFx.Tests // Assert exitCode.Should().NotBe(0); stdErrData.Should().ContainAll( - "Can't find a command that matches the following arguments:", - "not-a-valid-command"); + "Can't find a command that matches the following arguments:", + "not-a-valid-command" + ); stdOutData.Should().ContainAll( "Usage", "[command]", @@ -208,7 +228,11 @@ namespace CliFx.Tests "-h|--help", "Shows help text.", "Commands", "inv", - "You can run", "to show help on a specific command."); + "You can run", "to show help on a specific command." + ); + + _output.WriteLine(stdOutData); + _output.WriteLine(stdErrData); } } } \ No newline at end of file diff --git a/CliFx.Tests/HelpTextSpecs.cs b/CliFx.Tests/HelpTextSpecs.cs index c688048..1766b2e 100644 --- a/CliFx.Tests/HelpTextSpecs.cs +++ b/CliFx.Tests/HelpTextSpecs.cs @@ -2,11 +2,16 @@ using System.Threading.Tasks; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace CliFx.Tests { public partial class HelpTextSpecs { + private readonly ITestOutputHelper _output; + + public HelpTextSpecs(ITestOutputHelper output) => _output = output; + [Fact] public async Task Version_information_can_be_requested_by_providing_the_version_option_without_other_arguments() { @@ -29,6 +34,8 @@ namespace CliFx.Tests // Assert exitCode.Should().Be(0); stdOutData.Should().Be("v6.9"); + + _output.WriteLine(stdOutData); } [Fact] @@ -68,6 +75,8 @@ namespace CliFx.Tests "cmd", "NamedCommand description.", "You can run", "to show help on a specific command." ); + + _output.WriteLine(stdOutData); } [Fact] @@ -104,6 +113,8 @@ namespace CliFx.Tests "sub", "SubCommand description.", "You can run", "to show help on a specific command." ); + + _output.WriteLine(stdOutData); } [Fact] @@ -137,6 +148,8 @@ namespace CliFx.Tests "-e|--option-e", "OptionE description.", "-h|--help", "Shows help text." ); + + _output.WriteLine(stdOutData); } [Fact] @@ -167,6 +180,8 @@ namespace CliFx.Tests "cmd", "NamedCommand description.", "You can run", "to show help on a specific command." ); + + _output.WriteLine(stdOutData); } [Fact] @@ -190,6 +205,8 @@ namespace CliFx.Tests "Usage", "cmd-with-params", "", "", "", "[options]" ); + + _output.WriteLine(stdOutData); } [Fact] @@ -217,6 +234,8 @@ namespace CliFx.Tests "* -g|--option-g", "-h|--option-h" ); + + _output.WriteLine(stdOutData); } [Fact] @@ -241,6 +260,8 @@ namespace CliFx.Tests "* -a|--option-a", "Environment variable:", "ENV_OPT_A", "-b|--option-b", "Environment variable:", "ENV_OPT_B" ); + + _output.WriteLine(stdOutData); } } } \ No newline at end of file diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index 868680c..fed64c1 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -51,7 +51,6 @@ - diff --git a/CliFx/Domain/CommandOptionSchema.cs b/CliFx/Domain/CommandOptionSchema.cs index bfd4553..b7fb272 100644 --- a/CliFx/Domain/CommandOptionSchema.cs +++ b/CliFx/Domain/CommandOptionSchema.cs @@ -39,10 +39,14 @@ namespace CliFx.Domain !string.IsNullOrWhiteSpace(Name) && string.Equals(Name, name, StringComparison.OrdinalIgnoreCase); - public bool MatchesShortName(char shortName) => + public bool MatchesShortName(char? shortName) => ShortName != null && ShortName == shortName; + public bool MatchesNameOrShortName(string? name, char? shortName) => + MatchesName(name) || + MatchesShortName(shortName); + public bool MatchesNameOrShortName(string alias) => MatchesName(alias) || alias.Length == 1 && MatchesShortName(alias.Single()); diff --git a/CliFx/Domain/CommandSchema.cs b/CliFx/Domain/CommandSchema.cs index 1f24e0d..825e40b 100644 --- a/CliFx/Domain/CommandSchema.cs +++ b/CliFx/Domain/CommandSchema.cs @@ -40,6 +40,21 @@ namespace CliFx.Domain public bool MatchesName(string? name) => string.Equals(name, Name, StringComparison.OrdinalIgnoreCase); + public IReadOnlyList GetBuiltInOptions() + { + var result = new List(2); + + var helpOption = CommandOptionSchema.HelpOption; + var versionOption = CommandOptionSchema.VersionOption; + + result.Add(helpOption); + + if (IsDefault) + result.Add(versionOption); + + return result; + } + private void InjectParameters(ICommand command, IReadOnlyList parameterInputs) { // All inputs must be bound diff --git a/CliFx/Domain/HelpTextWriter.cs b/CliFx/Domain/HelpTextWriter.cs index cdb6c00..f5d77ef 100644 --- a/CliFx/Domain/HelpTextWriter.cs +++ b/CliFx/Domain/HelpTextWriter.cs @@ -212,12 +212,8 @@ namespace CliFx.Domain var options = command.Options .OrderByDescending(o => o.IsRequired) - .ToList(); - - // Add built-in options - options.Add(CommandOptionSchema.HelpOption); - if (command.IsDefault) - options.Add(CommandOptionSchema.VersionOption); + .Concat(command.GetBuiltInOptions()) + .ToArray(); foreach (var option in options) { diff --git a/CliFx/Exceptions/CliFxException.cs b/CliFx/Exceptions/CliFxException.cs index c8f5e07..b17cd85 100644 --- a/CliFx/Exceptions/CliFxException.cs +++ b/CliFx/Exceptions/CliFxException.cs @@ -25,7 +25,7 @@ namespace CliFx.Exceptions /// Whether this exception was constructed with a message. /// /// - /// We cannot check against the 'Message' property because it will always return + /// 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; } @@ -38,7 +38,7 @@ namespace CliFx.Exceptions /// /// Initializes an instance of . /// - public CliFxException(string? message, bool showHelp = false) + public CliFxException(string? message, bool showHelp = false) : this(message, null, showHelp: showHelp) { } @@ -217,7 +217,7 @@ If that's not possible, consider converting the parameter into an option, to avo { var message = $@" Command '{command.Type.FullName}' is invalid because it contains one or more options without a name: -{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))} +{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} Options must have either a name or a short name or both, because that's what identifies them. @@ -232,7 +232,7 @@ To fix this, ensure all options have their names or short names set to some valu { var message = $@" Command '{command.Type.FullName}' is invalid because it contains one or more options whose names are too short: -{string.Join(Environment.NewLine, invalidOptions.Select(p => $"{p.Property.Name} ('{p.DisplayName}')"))} +{string.Join(Environment.NewLine, invalidOptions.Select(o => $"{o.Property.Name} ('{o.DisplayName}')"))} Option names must be at least 2 characters long to avoid confusion with short names. If you intended to set the short name instead, use the corresponding attribute overload. @@ -249,7 +249,7 @@ To fix this, ensure all option names are at least 2 characters long."; { var message = $@" Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same name ('{name}'): -{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))} +{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} Options must have unique names, because that's what identifies them. Names are not case-sensitive. @@ -266,7 +266,7 @@ To fix this, ensure that all options have different names."; { var message = $@" Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same short name ('{shortName}'): -{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))} +{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} Options must have unique short names, because that's what identifies them. Short names are case-sensitive (i.e. 'a' and 'A' are different short names). @@ -283,7 +283,7 @@ To fix this, ensure that all options have different short names."; { var message = $@" Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} options with the same fallback environment variable name ('{environmentVariableName}'): -{string.Join(Environment.NewLine, invalidOptions.Select(p => p.Property.Name))} +{string.Join(Environment.NewLine, invalidOptions.Select(o => o.Property.Name))} Options cannot share the same environment variable as a fallback. Environment variable names are not case-sensitive.