From 93e4885d801646d23f5cac8d073d829aa6062bde Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 25 Jul 2023 16:38:17 -0700 Subject: [PATCH 1/5] fix #2226 --- .../EnvironmentVariableDirectiveTests.cs | 1 - .../HelpOptionTests.cs | 9 +-- .../ParseErrorReportingTests.cs | 70 ++++++++++++++++++- .../Utility/AssertionExtensions.cs | 64 +++++++++-------- .../Invocation/ParseErrorAction.cs | 26 ++++++- src/System.CommandLine/ParseResult.cs | 2 +- 6 files changed, 134 insertions(+), 38 deletions(-) diff --git a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs index 723e3c6690..ffab9d6203 100644 --- a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs +++ b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs @@ -2,7 +2,6 @@ using System.CommandLine.Invocation; using FluentAssertions; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Xunit; diff --git a/src/System.CommandLine.Tests/HelpOptionTests.cs b/src/System.CommandLine.Tests/HelpOptionTests.cs index ccdda34c1f..1d5d55f08e 100644 --- a/src/System.CommandLine.Tests/HelpOptionTests.cs +++ b/src/System.CommandLine.Tests/HelpOptionTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using System.CommandLine.Help; +using System.CommandLine.Tests.Utility; using System.IO; using System.Threading.Tasks; using Xunit; @@ -70,7 +71,7 @@ public async Task Help_option_accepts_default_values(string value) await config.InvokeAsync($"command {value}"); - console.ToString().Should().Contain("Usage:"); + console.ToString().Should().ShowHelp(); } [Fact] @@ -86,7 +87,7 @@ public async Task Help_option_does_not_display_when_option_defined_with_same_ali await command.Parse("command -h", config).InvokeAsync(); - config.Output.ToString().Should().BeEmpty(); + config.Output.ToString().Should().NotShowHelp(); } [Fact] @@ -161,7 +162,7 @@ public async Task HelpOption_with_custom_aliases_uses_aliases(string helpAlias) await config.InvokeAsync(helpAlias); - config.Output.ToString().Should().Contain("Usage:"); + config.Output.ToString().Should().ShowHelp(); } [Theory] @@ -170,7 +171,7 @@ public async Task HelpOption_with_custom_aliases_uses_aliases(string helpAlias) [InlineData("--help")] [InlineData("-?")] [InlineData("/?")] - public async Task Help_option_with_custom_aliases_default_aliases_replaced(string helpAlias) + public async Task Help_option_with_custom_aliases_does_not_recognize_default_aliases(string helpAlias) { CliRootCommand command = new(); command.Options.Clear(); diff --git a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs index cc91854881..2153b9538d 100644 --- a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs +++ b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs @@ -6,6 +6,10 @@ using System.IO; using FluentAssertions; using Xunit; +using System.CommandLine.Tests.Utility; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions.Execution; namespace System.CommandLine.Tests; @@ -20,13 +24,18 @@ public void Parse_error_reporting_reports_error_when_help_is_used_and_required_s new HelpOption() }; - var parseResult = root.Parse(""); + var output = new StringWriter(); + var parseResult = root.Parse("", new CliConfiguration(root) + { + Output = output, + }); parseResult.Errors.Should().NotBeEmpty(); var result = parseResult.Invoke(); result.Should().Be(1); + output.ToString().Should().ShowHelp(); } [Fact] @@ -51,6 +60,63 @@ public void Help_display_can_be_disabled() result.Invoke(); - config.Output.ToString().Should().NotContain("--verbose"); + var output = config.Output.ToString(); + + output.Should().NotShowHelp(); + } + + [Theory] // https://github.com/dotnet/command-line-api/issues/2226 + [InlineData(true)] + [InlineData(false)] + public void When_there_are_parse_errors_then_customized_help_action_is_used_if_present(bool useAsyncAction) + { + var wasCalled = false; + CliRootCommand rootCommand = new(); + rootCommand.Options.Clear(); + CliAction customHelpAction = useAsyncAction + ? new AsynchronousCustomHelpAction(() => wasCalled = true) + : new SynchronousCustomHelpAction(() => wasCalled = true); + + rootCommand.Add(new HelpOption + { + Action = customHelpAction + }); + + rootCommand.Parse("oops").Invoke(); + + wasCalled.Should().BeTrue(); + } + + private class SynchronousCustomHelpAction : SynchronousCliAction + { + private readonly Action _onInvoke; + + public SynchronousCustomHelpAction(Action onInvoke) + { + _onInvoke = onInvoke; + } + + public override int Invoke(ParseResult parseResult) + { + _onInvoke(); + return 0; + } + } + + private class AsynchronousCustomHelpAction : AsynchronousCliAction + { + private readonly Action _onInvoke; + + public AsynchronousCustomHelpAction(Action onInvoke) + { + _onInvoke = onInvoke; + } + + public override async Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) + { + await Task.Yield(); + _onInvoke(); + return 0; + } } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs b/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs index 3169993a94..a0ecfd5b92 100644 --- a/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs +++ b/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs @@ -5,43 +5,49 @@ using FluentAssertions; using FluentAssertions.Collections; using FluentAssertions.Execution; +using FluentAssertions.Primitives; -namespace System.CommandLine.Tests.Utility +namespace System.CommandLine.Tests.Utility; + +public static class AssertionExtensions { - public static class AssertionExtensions + public static AndConstraint> BeEquivalentSequenceTo( + this GenericCollectionAssertions assertions, + params object[] expectedValues) { - public static AndConstraint> BeEquivalentSequenceTo( - this GenericCollectionAssertions assertions, - params object[] expectedValues) - { - var actualValues = assertions.Subject.ToArray(); + var actualValues = assertions.Subject.ToArray(); + + actualValues + .Select(a => a?.GetType()) + .Should() + .BeEquivalentTo(expectedValues.Select(e => e?.GetType())); - actualValues - .Select(a => a?.GetType()) - .Should() - .BeEquivalentTo(expectedValues.Select(e => e?.GetType())); + using (new AssertionScope()) + { + foreach (var tuple in actualValues + .Zip(expectedValues, (actual, expected) => (actual, expected)) + .Where(t => (t.expected == null) || (t.expected.GetType().GetProperties().Length > 0))) - using (new AssertionScope()) { - foreach (var tuple in actualValues - .Zip(expectedValues, (actual, expected) => (actual, expected)) - .Where(t => (t.expected == null) || (t.expected.GetType().GetProperties().Length > 0))) - - { - tuple.actual - .Should() - .BeEquivalentTo(tuple.expected); - } + tuple.actual + .Should() + .BeEquivalentTo(tuple.expected); } - - return new AndConstraint>(assertions); } - public static AndConstraint BeEquivalentSequenceTo( - this StringCollectionAssertions assertions, - params string[] expectedValues) - { - return assertions.BeEquivalentTo(expectedValues, c => c.WithStrictOrderingFor(s => s)); - } + return new AndConstraint>(assertions); + } + + public static AndConstraint BeEquivalentSequenceTo( + this StringCollectionAssertions assertions, + params string[] expectedValues) + { + return assertions.BeEquivalentTo(expectedValues, c => c.WithStrictOrderingFor(s => s)); } + + public static AndConstraint ShowHelp(this StringAssertions output) => + output.Subject.Should().Match("*Description:*Usage:*"); + + public static AndConstraint NotShowHelp(this StringAssertions output) => + output.Subject.Should().NotMatch("*Description:*Usage:*"); } \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/ParseErrorAction.cs b/src/System.CommandLine/Invocation/ParseErrorAction.cs index 4881eed525..98c5ebba55 100644 --- a/src/System.CommandLine/Invocation/ParseErrorAction.cs +++ b/src/System.CommandLine/Invocation/ParseErrorAction.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.CommandLine.Help; using System.Linq; +using System.Threading; namespace System.CommandLine.Invocation; @@ -59,7 +60,30 @@ private static void WriteErrorDetails(ParseResult parseResult) private static void WriteHelp(ParseResult parseResult) { - new HelpAction().Invoke(parseResult); + var availableHelpOptions = + parseResult + .CommandResult + .Command + .RecurseWhileNotNull(c => c.Parents.OfType().FirstOrDefault()) + .Select(c => c.Options.OfType().FirstOrDefault()); + + if (availableHelpOptions.FirstOrDefault() is { Action: not null } helpOption) + { + switch (helpOption.Action) + { + case SynchronousCliAction syncAction: + syncAction.Invoke(parseResult); + break; + + case AsynchronousCliAction asyncAction: + asyncAction.InvokeAsync(parseResult, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + break; + } + } + else + { + new HelpAction().Invoke(parseResult); + } } private static void WriteTypoCorrectionSuggestions(ParseResult parseResult) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index fc3af8a7f9..cf108f1b93 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -320,7 +320,7 @@ public int Invoke() if (useAsync) { - return InvocationPipeline.InvokeAsync(this, CancellationToken.None).GetAwaiter().GetResult(); + return InvocationPipeline.InvokeAsync(this, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); } else { From 2748cfec9235a0c5bb6104f91d837bb3433ca8a1 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 25 Jul 2023 16:40:49 -0700 Subject: [PATCH 2/5] file scoped namespaces and minor cleanup --- .../Help/HelpBuilderTests.Customization.cs | 890 +++++++++--------- .../HelpOptionTests.cs | 265 +++--- .../ParseErrorReportingTests.cs | 1 - 3 files changed, 575 insertions(+), 581 deletions(-) diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs index ad15258299..e7652ef9fe 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs @@ -5,569 +5,565 @@ using System.CommandLine.Help; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using FluentAssertions; -using FluentAssertions.Common; using Xunit; using static System.Environment; -namespace System.CommandLine.Tests.Help +namespace System.CommandLine.Tests.Help; + +public partial class HelpBuilderTests { - public partial class HelpBuilderTests + public class Customization { - public class Customization - { - private readonly HelpBuilder _helpBuilder; - private readonly StringWriter _console; - private readonly string _columnPadding; - private readonly string _indentation; + private readonly HelpBuilder _helpBuilder; + private readonly StringWriter _console; + private readonly string _columnPadding; + private readonly string _indentation; - public Customization() - { - _console = new(); - _helpBuilder = GetHelpBuilder(LargeMaxWidth); - _columnPadding = new string(' ', ColumnGutterWidth); - _indentation = new string(' ', IndentationWidth); - } + public Customization() + { + _console = new(); + _helpBuilder = GetHelpBuilder(LargeMaxWidth); + _columnPadding = new string(' ', ColumnGutterWidth); + _indentation = new string(' ', IndentationWidth); + } - private HelpBuilder GetHelpBuilder(int maxWidth) => new (maxWidth); + private HelpBuilder GetHelpBuilder(int maxWidth) => new (maxWidth); - [Fact] - public void Option_can_customize_displayed_default_value() + [Fact] + public void Option_can_customize_displayed_default_value() + { + var option = new CliOption("--the-option") { DefaultValueFactory = _ => "not 42" }; + var command = new CliCommand("the-command", "command help") { - var option = new CliOption("--the-option") { DefaultValueFactory = _ => "not 42" }; - var command = new CliCommand("the-command", "command help") - { - option - }; + option + }; - _helpBuilder.CustomizeSymbol(option, defaultValue: "42"); + _helpBuilder.CustomizeSymbol(option, defaultValue: "42"); - _helpBuilder.Write(command, _console); - var expected = - $"Options:{NewLine}" + - $"{_indentation}--the-option{_columnPadding}[default: 42]{NewLine}{NewLine}"; + _helpBuilder.Write(command, _console); + var expected = + $"Options:{NewLine}" + + $"{_indentation}--the-option{_columnPadding}[default: 42]{NewLine}{NewLine}"; - _console.ToString().Should().Contain(expected); - } + _console.ToString().Should().Contain(expected); + } - [Fact] - public void Option_can_customize_first_column_text() + [Fact] + public void Option_can_customize_first_column_text() + { + var option = new CliOption("--the-option") { Description = "option description" }; + var command = new CliCommand("the-command", "command help") { - var option = new CliOption("--the-option") { Description = "option description" }; - var command = new CliCommand("the-command", "command help") - { - option - }; + option + }; - _helpBuilder.CustomizeSymbol(option, firstColumnText: "other-name"); + _helpBuilder.CustomizeSymbol(option, firstColumnText: "other-name"); - _helpBuilder.Write(command, _console); - var expected = - $"Options:{NewLine}" + - $"{_indentation}other-name{_columnPadding}option description{NewLine}{NewLine}"; + _helpBuilder.Write(command, _console); + var expected = + $"Options:{NewLine}" + + $"{_indentation}other-name{_columnPadding}option description{NewLine}{NewLine}"; - _console.ToString().Should().Contain(expected); - } + _console.ToString().Should().Contain(expected); + } - [Fact] - public void Option_can_customize_first_column_text_based_on_parse_result() + [Fact] + public void Option_can_customize_first_column_text_based_on_parse_result() + { + var option = new CliOption("option"); + var commandA = new CliCommand("a", "a command help") { - var option = new CliOption("option"); - var commandA = new CliCommand("a", "a command help") - { - option - }; - var commandB = new CliCommand("b", "b command help") - { - option - }; - var command = new CliCommand("root", "root command help") - { - commandA, commandB - }; - var optionAFirstColumnText = "option a help"; - var optionBFirstColumnText = "option b help"; - - var helpBuilder = new HelpBuilder(LargeMaxWidth); - helpBuilder.CustomizeSymbol(option, firstColumnText: ctx => - ctx.Command.Equals(commandA) - ? optionAFirstColumnText - : optionBFirstColumnText); - command.Options.Add(new HelpOption() + option + }; + var commandB = new CliCommand("b", "b command help") + { + option + }; + var command = new CliCommand("root", "root command help") + { + commandA, commandB + }; + var optionAFirstColumnText = "option a help"; + var optionBFirstColumnText = "option b help"; + + var helpBuilder = new HelpBuilder(LargeMaxWidth); + helpBuilder.CustomizeSymbol(option, firstColumnText: ctx => + ctx.Command.Equals(commandA) + ? optionAFirstColumnText + : optionBFirstColumnText); + command.Options.Add(new HelpOption() + { + Action = new HelpAction() { - Action = new HelpAction() - { - Builder = helpBuilder - } - }); + Builder = helpBuilder + } + }); - var console = new StringWriter(); - var config = new CliConfiguration(command) - { - Output = console - }; - command.Parse("root a -h", config).Invoke(); - console.ToString().Should().Contain(optionAFirstColumnText); - - console = new StringWriter(); - config.Output = console; - command.Parse("root b -h", config).Invoke(); - console.ToString().Should().Contain(optionBFirstColumnText); - } + var console = new StringWriter(); + var config = new CliConfiguration(command) + { + Output = console + }; + command.Parse("root a -h", config).Invoke(); + console.ToString().Should().Contain(optionAFirstColumnText); + + console = new StringWriter(); + config.Output = console; + command.Parse("root b -h", config).Invoke(); + console.ToString().Should().Contain(optionBFirstColumnText); + } - [Fact] - public void Option_can_customize_second_column_text_based_on_parse_result() + [Fact] + public void Option_can_customize_second_column_text_based_on_parse_result() + { + var option = new CliOption("option"); + var commandA = new CliCommand("a", "a command help") { - var option = new CliOption("option"); - var commandA = new CliCommand("a", "a command help") - { - option - }; - var commandB = new CliCommand("b", "b command help") - { - option - }; - var command = new CliCommand("root", "root command help") - { - commandA, commandB - }; - var optionADescription = "option a help"; - var optionBDescription = "option b help"; - - var helpBuilder = new HelpBuilder(LargeMaxWidth); - helpBuilder.CustomizeSymbol(option, secondColumnText: ctx => - ctx.Command.Equals(commandA) - ? optionADescription - : optionBDescription); - command.Options.Add(new HelpOption - { - Action = new HelpAction - { - Builder = helpBuilder - } - }); + option + }; + var commandB = new CliCommand("b", "b command help") + { + option + }; + var command = new CliCommand("root", "root command help") + { + commandA, commandB + }; + var optionADescription = "option a help"; + var optionBDescription = "option b help"; + + var helpBuilder = new HelpBuilder(LargeMaxWidth); + helpBuilder.CustomizeSymbol(option, secondColumnText: ctx => + ctx.Command.Equals(commandA) + ? optionADescription + : optionBDescription); + command.Options.Add(new HelpOption + { + Action = new HelpAction + { + Builder = helpBuilder + } + }); - var config = new CliConfiguration(command) - { - Output = new StringWriter() - }; + var config = new CliConfiguration(command) + { + Output = new StringWriter() + }; - config.Invoke("root a -h"); - config.Output.ToString().Should().Contain($"option {optionADescription}"); + config.Invoke("root a -h"); + config.Output.ToString().Should().Contain($"option {optionADescription}"); - config.Output = new StringWriter(); - config.Invoke("root b -h"); - config.Output.ToString().Should().Contain($"option {optionBDescription}"); - } + config.Output = new StringWriter(); + config.Invoke("root b -h"); + config.Output.ToString().Should().Contain($"option {optionBDescription}"); + } - [Fact] - public void Subcommand_can_customize_first_column_text() + [Fact] + public void Subcommand_can_customize_first_column_text() + { + var subcommand = new CliCommand("subcommand", "subcommand description"); + var command = new CliCommand("the-command", "command help") { - var subcommand = new CliCommand("subcommand", "subcommand description"); - var command = new CliCommand("the-command", "command help") - { - subcommand - }; + subcommand + }; - _helpBuilder.CustomizeSymbol(subcommand, firstColumnText: "other-name"); + _helpBuilder.CustomizeSymbol(subcommand, firstColumnText: "other-name"); - _helpBuilder.Write(command, _console); - var expected = - $"Commands:{NewLine}" + - $"{_indentation}other-name{_columnPadding}subcommand description{NewLine}{NewLine}"; + _helpBuilder.Write(command, _console); + var expected = + $"Commands:{NewLine}" + + $"{_indentation}other-name{_columnPadding}subcommand description{NewLine}{NewLine}"; - _console.ToString().Should().Contain(expected); - } + _console.ToString().Should().Contain(expected); + } - [Fact] - public void Command_arguments_can_customize_first_column_text() + [Fact] + public void Command_arguments_can_customize_first_column_text() + { + var argument = new CliArgument("arg-name") { Description = "arg description" }; + var command = new CliCommand("the-command", "command help") { - var argument = new CliArgument("arg-name") { Description = "arg description" }; - var command = new CliCommand("the-command", "command help") - { - argument - }; + argument + }; - _helpBuilder.CustomizeSymbol(argument, firstColumnText: ""); + _helpBuilder.CustomizeSymbol(argument, firstColumnText: ""); - _helpBuilder.Write(command, _console); - var expected = - $"Arguments:{NewLine}" + - $"{_indentation}{_columnPadding}arg description{NewLine}{NewLine}"; + _helpBuilder.Write(command, _console); + var expected = + $"Arguments:{NewLine}" + + $"{_indentation}{_columnPadding}arg description{NewLine}{NewLine}"; - _console.ToString().Should().Contain(expected); - } + _console.ToString().Should().Contain(expected); + } - [Fact] - public void Command_arguments_can_customize_second_column_text() + [Fact] + public void Command_arguments_can_customize_second_column_text() + { + var argument = new CliArgument("some-arg") { - var argument = new CliArgument("some-arg") - { - Description = "Default description", - DefaultValueFactory = _ => "not 42" - }; - var command = new CliCommand("the-command", "command help") - { - argument - }; + Description = "Default description", + DefaultValueFactory = _ => "not 42" + }; + var command = new CliCommand("the-command", "command help") + { + argument + }; - _helpBuilder.CustomizeSymbol(argument, secondColumnText: "Custom description"); + _helpBuilder.CustomizeSymbol(argument, secondColumnText: "Custom description"); - _helpBuilder.Write(command, _console); - var expected = - $"Arguments:{NewLine}" + - $"{_indentation}{_columnPadding}Custom description [default: not 42]{NewLine}{NewLine}"; + _helpBuilder.Write(command, _console); + var expected = + $"Arguments:{NewLine}" + + $"{_indentation}{_columnPadding}Custom description [default: not 42]{NewLine}{NewLine}"; - _console.ToString().Should().Contain(expected); - } + _console.ToString().Should().Contain(expected); + } - [Fact] - public void Command_arguments_can_customize_default_value() + [Fact] + public void Command_arguments_can_customize_default_value() + { + var argument = new CliArgument("some-arg") { - var argument = new CliArgument("some-arg") - { - DefaultValueFactory = (_) => "not 42" - }; - var command = new CliCommand("the-command", "command help") - { - argument - }; + DefaultValueFactory = (_) => "not 42" + }; + var command = new CliCommand("the-command", "command help") + { + argument + }; - _helpBuilder.CustomizeSymbol(argument, defaultValue: "42"); + _helpBuilder.CustomizeSymbol(argument, defaultValue: "42"); - _helpBuilder.Write(command, _console); - var expected = - $"Arguments:{NewLine}" + - $"{_indentation}{_columnPadding}[default: 42]{NewLine}{NewLine}"; + _helpBuilder.Write(command, _console); + var expected = + $"Arguments:{NewLine}" + + $"{_indentation}{_columnPadding}[default: 42]{NewLine}{NewLine}"; - _console.ToString().Should().Contain(expected); - } + _console.ToString().Should().Contain(expected); + } - [Fact] - public void Customize_throws_when_symbol_is_null() - { - Action action = () => new HelpBuilder().CustomizeSymbol(null!, ""); - action.Should().Throw(); - } + [Fact] + public void Customize_throws_when_symbol_is_null() + { + Action action = () => new HelpBuilder().CustomizeSymbol(null!, ""); + action.Should().Throw(); + } - [Theory] - [InlineData(false, false, "--option \\s*description")] - [InlineData(true, false, "custom 1st\\s*description")] - [InlineData(false, true, "--option \\s*custom 2nd")] - [InlineData(true, true, "custom 1st\\s*custom 2nd")] - public void Option_can_fallback_to_default_when_customizing(bool conditionA, bool conditionB, string expected) - { - var command = new CliCommand("test"); - var option = new CliOption("--option") { Description = "description" }; + [Theory] + [InlineData(false, false, "--option \\s*description")] + [InlineData(true, false, "custom 1st\\s*description")] + [InlineData(false, true, "--option \\s*custom 2nd")] + [InlineData(true, true, "custom 1st\\s*custom 2nd")] + public void Option_can_fallback_to_default_when_customizing(bool conditionA, bool conditionB, string expected) + { + var command = new CliCommand("test"); + var option = new CliOption("--option") { Description = "description" }; - command.Options.Add(option); + command.Options.Add(option); - var helpBuilder = new HelpBuilder(LargeMaxWidth); - helpBuilder.CustomizeSymbol(option, - firstColumnText: ctx => conditionA ? "custom 1st" : HelpBuilder.Default.GetOptionUsageLabel(option), - secondColumnText: ctx => conditionB ? "custom 2nd" : option.Description ?? string.Empty); + var helpBuilder = new HelpBuilder(LargeMaxWidth); + helpBuilder.CustomizeSymbol(option, + firstColumnText: ctx => conditionA ? "custom 1st" : HelpBuilder.Default.GetOptionUsageLabel(option), + secondColumnText: ctx => conditionB ? "custom 2nd" : option.Description ?? string.Empty); - command.Options.Add(new HelpOption() + command.Options.Add(new HelpOption + { + Action = new HelpAction { - Action = new HelpAction() - { - Builder = helpBuilder - } - }); + Builder = helpBuilder + } + }); - CliConfiguration config = new (command); - var console = new StringWriter(); - config.Output = console; - command.Parse("test -h", config).Invoke(); - console.ToString().Should().MatchRegex(expected); - } + CliConfiguration config = new (command); + var console = new StringWriter(); + config.Output = console; + command.Parse("test -h", config).Invoke(); + console.ToString().Should().MatchRegex(expected); + } - [Theory] - [InlineData(false, false, false, "\\\\s*description\\s*\\[default\\: default\\]")] - [InlineData(true, false, false, "custom 1st\\s*description\\s*\\[default\\: default\\]")] - [InlineData(false, true, false, "\\\\s*custom 2nd\\s*\\[default\\: default\\]")] - [InlineData(true, true, false, "custom 1st\\s*custom 2nd\\s*\\[default\\: default\\]")] - [InlineData(false, false, true, "\\\\s*description\\s*\\[default\\: custom def\\]")] - [InlineData(true, false, true, "custom 1st\\s*description\\s*\\[default\\: custom def\\]")] - [InlineData(false, true, true, "\\\\s*custom 2nd\\s*\\[default\\: custom def\\]")] - [InlineData(true, true, true, "custom 1st\\s*custom 2nd\\s*\\[default\\: custom def\\]")] - public void Argument_can_fallback_to_default_when_customizing( - bool conditionA, - bool conditionB, - bool conditionC, - string expected) - { - var command = new CliCommand("test"); - var argument = new CliArgument("arg") - { - Description = "description", - DefaultValueFactory = _ => "default" - }; + [Theory] + [InlineData(false, false, false, "\\\\s*description\\s*\\[default\\: default\\]")] + [InlineData(true, false, false, "custom 1st\\s*description\\s*\\[default\\: default\\]")] + [InlineData(false, true, false, "\\\\s*custom 2nd\\s*\\[default\\: default\\]")] + [InlineData(true, true, false, "custom 1st\\s*custom 2nd\\s*\\[default\\: default\\]")] + [InlineData(false, false, true, "\\\\s*description\\s*\\[default\\: custom def\\]")] + [InlineData(true, false, true, "custom 1st\\s*description\\s*\\[default\\: custom def\\]")] + [InlineData(false, true, true, "\\\\s*custom 2nd\\s*\\[default\\: custom def\\]")] + [InlineData(true, true, true, "custom 1st\\s*custom 2nd\\s*\\[default\\: custom def\\]")] + public void Argument_can_fallback_to_default_when_customizing( + bool conditionA, + bool conditionB, + bool conditionC, + string expected) + { + var command = new CliCommand("test"); + var argument = new CliArgument("arg") + { + Description = "description", + DefaultValueFactory = _ => "default" + }; - command.Arguments.Add(argument); + command.Arguments.Add(argument); - var helpBuilder = new HelpBuilder(LargeMaxWidth); - helpBuilder.CustomizeSymbol(argument, - firstColumnText: ctx => conditionA ? "custom 1st" : HelpBuilder.Default.GetArgumentUsageLabel(argument), - secondColumnText: ctx => conditionB ? "custom 2nd" : HelpBuilder.Default.GetArgumentDescription(argument), - defaultValue: ctx => conditionC ? "custom def" : HelpBuilder.Default.GetArgumentDefaultValue(argument)); + var helpBuilder = new HelpBuilder(LargeMaxWidth); + helpBuilder.CustomizeSymbol(argument, + firstColumnText: ctx => conditionA ? "custom 1st" : HelpBuilder.Default.GetArgumentUsageLabel(argument), + secondColumnText: ctx => conditionB ? "custom 2nd" : HelpBuilder.Default.GetArgumentDescription(argument), + defaultValue: ctx => conditionC ? "custom def" : HelpBuilder.Default.GetArgumentDefaultValue(argument)); - CliConfiguration config = new (command); + CliConfiguration config = new (command); - command.Options.Add(new HelpOption + command.Options.Add(new HelpOption + { + Action = new HelpAction { - Action = new HelpAction - { - Builder = helpBuilder - } - }); + Builder = helpBuilder + } + }); - config.Output = new StringWriter(); - command.Parse("test -h", config).Invoke(); - config.Output.ToString().Should().MatchRegex(expected); - } + config.Output = new StringWriter(); + command.Parse("test -h", config).Invoke(); + config.Output.ToString().Should().MatchRegex(expected); + } - [Fact] - public void Individual_symbols_can_be_customized() - { - var subcommand = new CliCommand("subcommand", "The default command description"); - var option = new CliOption("-x") { Description = "The default option description" }; - var argument = new CliArgument("int-value") { Description = "The default argument description" }; + [Fact] + public void Individual_symbols_can_be_customized() + { + var subcommand = new CliCommand("subcommand", "The default command description"); + var option = new CliOption("-x") { Description = "The default option description" }; + var argument = new CliArgument("int-value") { Description = "The default argument description" }; - var rootCommand = new CliRootCommand - { - subcommand, - option, - argument, - }; + var rootCommand = new CliRootCommand + { + subcommand, + option, + argument, + }; - CliConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + CliConfiguration config = new(rootCommand) + { + Output = new StringWriter() + }; - ParseResult parseResult = rootCommand.Parse("-h", config); + ParseResult parseResult = rootCommand.Parse("-h", config); - if (parseResult.Action is HelpAction helpAction) - { - helpAction.Builder.CustomizeSymbol(subcommand, secondColumnText: "The custom command description"); - helpAction.Builder.CustomizeSymbol(option, secondColumnText: "The custom option description"); - helpAction.Builder.CustomizeSymbol(argument, secondColumnText: "The custom argument description"); - } + if (parseResult.Action is HelpAction helpAction) + { + helpAction.Builder.CustomizeSymbol(subcommand, secondColumnText: "The custom command description"); + helpAction.Builder.CustomizeSymbol(option, secondColumnText: "The custom option description"); + helpAction.Builder.CustomizeSymbol(argument, secondColumnText: "The custom argument description"); + } - parseResult.Invoke(); + parseResult.Invoke(); - config.Output - .ToString() - .Should() - .ContainAll("The custom command description", - "The custom option description", - "The custom argument description"); - } + config.Output + .ToString() + .Should() + .ContainAll("The custom command description", + "The custom option description", + "The custom argument description"); + } - [Fact] - public void Help_sections_can_be_replaced() + [Fact] + public void Help_sections_can_be_replaced() + { + CliConfiguration config = new(new CliRootCommand()) { - CliConfiguration config = new(new CliRootCommand()) - { - Output = new StringWriter() - }; + Output = new StringWriter() + }; - ParseResult parseResult = config.Parse("-h"); + ParseResult parseResult = config.Parse("-h"); - if (parseResult.Action is HelpAction helpAction) - { - helpAction.Builder.CustomizeLayout(CustomLayout); - } + if (parseResult.Action is HelpAction helpAction) + { + helpAction.Builder.CustomizeLayout(CustomLayout); + } - parseResult.Invoke(); + parseResult.Invoke(); - config.Output.ToString().Should().Be($"one{NewLine}{NewLine}two{NewLine}{NewLine}three{NewLine}{NewLine}"); + config.Output.ToString().Should().Be($"one{NewLine}{NewLine}two{NewLine}{NewLine}three{NewLine}{NewLine}"); - IEnumerable> CustomLayout(HelpContext _) - { - yield return ctx => { ctx.Output.WriteLine("one"); return true; }; - yield return ctx => { ctx.Output.WriteLine("two"); return true; }; - yield return ctx => { ctx.Output.WriteLine("three"); return true; }; - } + IEnumerable> CustomLayout(HelpContext _) + { + yield return ctx => { ctx.Output.WriteLine("one"); return true; }; + yield return ctx => { ctx.Output.WriteLine("two"); return true; }; + yield return ctx => { ctx.Output.WriteLine("three"); return true; }; } + } - [Fact] - public void Help_sections_can_be_supplemented() + [Fact] + public void Help_sections_can_be_supplemented() + { + CliConfiguration config = new(new CliRootCommand("hello")) { - CliConfiguration config = new(new CliRootCommand("hello")) - { - Output = new StringWriter(), - }; - - var defaultHelp = GetDefaultHelp(config.RootCommand); - - ParseResult parseResult = config.Parse("-h"); - - if (parseResult.Action is HelpAction helpAction) - { - helpAction.Builder.CustomizeLayout(CustomLayout); - } + Output = new StringWriter(), + }; - parseResult.Invoke(); + var defaultHelp = GetDefaultHelp(config.RootCommand); - var output = config.Output.ToString(); + ParseResult parseResult = config.Parse("-h"); - var expected = $"first{NewLine}{NewLine}{defaultHelp}{NewLine}last{NewLine}{NewLine}"; + if (parseResult.Action is HelpAction helpAction) + { + helpAction.Builder.CustomizeLayout(CustomLayout); + } - output.Should().Be(expected); + parseResult.Invoke(); - IEnumerable> CustomLayout(HelpContext _) - { - yield return ctx => { ctx.Output.WriteLine("first"); return true; }; + var output = config.Output.ToString(); - foreach (var section in HelpBuilder.Default.GetLayout()) - { - yield return section; - } + var expected = $"first{NewLine}{NewLine}{defaultHelp}{NewLine}last{NewLine}{NewLine}"; - yield return ctx => { ctx.Output.WriteLine("last"); return true; }; - } - } + output.Should().Be(expected); - [Fact] - public void Layout_can_be_composed_dynamically_based_on_context() + IEnumerable> CustomLayout(HelpContext _) { - HelpBuilder helpBuilder = new(); - var commandWithTypicalHelp = new CliCommand("typical"); - var commandWithCustomHelp = new CliCommand("custom"); - var command = new CliRootCommand - { - commandWithTypicalHelp, - commandWithCustomHelp - }; + yield return ctx => { ctx.Output.WriteLine("first"); return true; }; - command.Options.OfType().Single().Action = new HelpAction + foreach (var section in HelpBuilder.Default.GetLayout()) { - Builder = helpBuilder - }; - - var config = new CliConfiguration(command); - helpBuilder.CustomizeLayout(c => - c.Command == commandWithTypicalHelp - ? HelpBuilder.Default.GetLayout() - : new Func[] { c => { c.Output.WriteLine("Custom layout!"); return true; } } - .Concat(HelpBuilder.Default.GetLayout())); + yield return section; + } - var typicalOutput = new StringWriter(); - config.Output = typicalOutput; - command.Parse("typical -h", config).Invoke(); + yield return ctx => { ctx.Output.WriteLine("last"); return true; }; + } + } - var customOutput = new StringWriter(); - config.Output = customOutput; - command.Parse("custom -h", config).Invoke(); + [Fact] + public void Layout_can_be_composed_dynamically_based_on_context() + { + HelpBuilder helpBuilder = new(); + var commandWithTypicalHelp = new CliCommand("typical"); + var commandWithCustomHelp = new CliCommand("custom"); + var command = new CliRootCommand + { + commandWithTypicalHelp, + commandWithCustomHelp + }; - typicalOutput.ToString().Should().Be(GetDefaultHelp(commandWithTypicalHelp, false)); - customOutput.ToString().Should().Be($"Custom layout!{NewLine}{NewLine}{GetDefaultHelp(commandWithCustomHelp, false)}"); - } + command.Options.OfType().Single().Action = new HelpAction + { + Builder = helpBuilder + }; + + var config = new CliConfiguration(command); + helpBuilder.CustomizeLayout(c => + c.Command == commandWithTypicalHelp + ? HelpBuilder.Default.GetLayout() + : new Func[] { c => { c.Output.WriteLine("Custom layout!"); return true; } } + .Concat(HelpBuilder.Default.GetLayout())); + + var typicalOutput = new StringWriter(); + config.Output = typicalOutput; + command.Parse("typical -h", config).Invoke(); + + var customOutput = new StringWriter(); + config.Output = customOutput; + command.Parse("custom -h", config).Invoke(); + + typicalOutput.ToString().Should().Be(GetDefaultHelp(commandWithTypicalHelp, false)); + customOutput.ToString().Should().Be($"Custom layout!{NewLine}{NewLine}{GetDefaultHelp(commandWithCustomHelp, false)}"); + } - [Fact] - public void Help_default_sections_can_be_wrapped() + [Fact] + public void Help_default_sections_can_be_wrapped() + { + CliCommand command = new("test") { - CliCommand command = new("test") + new CliOption("--option") { - new CliOption("--option") - { - Description = "option description", - HelpName = "option" - }, - new HelpOption + Description = "option description", + HelpName = "option" + }, + new HelpOption + { + Action = new HelpAction { - Action = new HelpAction - { - Builder = new HelpBuilder(30) - } + Builder = new HelpBuilder(30) } - }; + } + }; - CliConfiguration config = new(command) - { - Output = new StringWriter() - }; - - config.Invoke("test -h"); - - string result = config.Output.ToString(); - result.Should().Be( - $"Description:{NewLine}{NewLine}" + - $"Usage:{NewLine} test [options]{NewLine}{NewLine}" + - $"Options:{NewLine}" + - $" --option option {NewLine}" + - $"