From ac9cd26e82e35fd21814d92ace73230471ddd73f Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:04:48 +0300 Subject: [PATCH 1/3] Omit empty description from help output Fix #2114 --- .../Help/HelpBuilderTests.Customization.cs | 3 +-- .../Help/HelpBuilderTests.cs | 13 +++++++++++++ .../Utility/AssertionExtensions.cs | 6 +++--- src/System.CommandLine/Help/HelpBuilder.Default.cs | 8 +++++++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs index c3d73d818f..3194811959 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs @@ -466,7 +466,6 @@ public void Help_default_sections_can_be_wrapped() command.Parse("test -h").Invoke(new() { Output = output }); output.ToString().Should().Be( - $"Description:{NewLine}{NewLine}" + $"Usage:{NewLine} test [options]{NewLine}{NewLine}" + $"Options:{NewLine}" + $" --option option {NewLine}" + @@ -534,4 +533,4 @@ private string GetDefaultHelp(Command command, bool trimOneNewline = true) return output; } } -} \ No newline at end of file +} diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs index 019c4b7e3b..2a98ba44ca 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs @@ -74,6 +74,19 @@ public void Synopsis_section_properly_wraps_description() _console.ToString().Should().Contain(expected); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Synopsis_section_is_omitted_when_description_is_missing(string? description) + { + var command = new RootCommand(description); + + _helpBuilder.Write(command, _console); + + _console.ToString().Should().NotContain(LocalizationResources.HelpDescriptionTitle()); + } + [Fact] public void Command_name_in_synopsis_can_be_specified() { diff --git a/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs b/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs index 74dcc77879..d6a0446653 100644 --- a/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs +++ b/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs @@ -47,8 +47,8 @@ public static AndConstraint>> BeE } public static AndConstraint ShowHelp(this StringAssertions output) => - output.Subject.Should().Match("*Description:*Usage:*"); + output.Subject.Should().Match("*Usage:*Options:*"); public static AndConstraint NotShowHelp(this StringAssertions output) => - output.Subject.Should().NotMatch("*Description:*Usage:*"); -} \ No newline at end of file + output.Subject.Should().NotMatch("*Usage:*Options:*"); +} diff --git a/src/System.CommandLine/Help/HelpBuilder.Default.cs b/src/System.CommandLine/Help/HelpBuilder.Default.cs index 00364784dd..221e13515f 100644 --- a/src/System.CommandLine/Help/HelpBuilder.Default.cs +++ b/src/System.CommandLine/Help/HelpBuilder.Default.cs @@ -185,7 +185,13 @@ public static IEnumerable> GetLayout() public static Func SynopsisSection() => ctx => { - ctx.HelpBuilder.WriteHeading(LocalizationResources.HelpDescriptionTitle(), ctx.Command.Description, ctx.Output); + string? description = ctx.Command.Description; + if (string.IsNullOrWhiteSpace(description)) + { + return false; + } + + ctx.HelpBuilder.WriteHeading(LocalizationResources.HelpDescriptionTitle(), description, ctx.Output); return true; }; From cd33dc0b2b4a17e2e308dbf7f7d8e4c7ce5d8ea4 Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:43:33 +0300 Subject: [PATCH 2/3] Refine help output assertions Replace shared help assertions with direct checks at their call sites. Do not use optional help sections as a proxy for help output. Signed-off-by: subotac <73706465+subotac@users.noreply.github.com> --- src/System.CommandLine.Tests/Help/HelpBuilderTests.cs | 4 +++- src/System.CommandLine.Tests/HelpOptionTests.cs | 9 ++++----- src/System.CommandLine.Tests/ParseErrorReportingTests.cs | 9 ++++----- .../Utility/AssertionExtensions.cs | 7 ------- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs index 2a98ba44ca..51ff8a7d99 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs @@ -84,7 +84,9 @@ public void Synopsis_section_is_omitted_when_description_is_missing(string? desc _helpBuilder.Write(command, _console); - _console.ToString().Should().NotContain(LocalizationResources.HelpDescriptionTitle()); + _console.ToString().Should() + .NotContain(LocalizationResources.HelpDescriptionTitle()) + .And.Contain(LocalizationResources.HelpUsageTitle()); } [Fact] diff --git a/src/System.CommandLine.Tests/HelpOptionTests.cs b/src/System.CommandLine.Tests/HelpOptionTests.cs index f91242b6eb..5a040a97d2 100644 --- a/src/System.CommandLine.Tests/HelpOptionTests.cs +++ b/src/System.CommandLine.Tests/HelpOptionTests.cs @@ -4,7 +4,6 @@ using FluentAssertions; using System.CommandLine.Help; using System.CommandLine.Invocation; -using System.CommandLine.Tests.Utility; using System.IO; using System.Linq; using System.Threading; @@ -66,7 +65,7 @@ public async Task Help_option_accepts_default_values(string value) await command.Parse($"command {value}").InvokeAsync(new() { Output = output }, CancellationToken.None); - output.ToString().Should().ShowHelp(); + output.ToString().Should().Contain("--help"); } [Fact] @@ -79,7 +78,7 @@ public async Task Help_option_does_not_display_when_option_defined_with_same_ali await command.Parse("command -h").InvokeAsync(new() { Output = output }, CancellationToken.None); - output.ToString().Should().NotShowHelp(); + output.ToString().Should().BeNullOrWhiteSpace(); } [Fact] @@ -151,7 +150,7 @@ public async Task HelpOption_with_custom_aliases_uses_aliases(string helpAlias) await command.Parse(helpAlias).InvokeAsync(new() { Output = output }, CancellationToken.None); - output.ToString().Should().ShowHelp(); + output.ToString().Should().Contain(helpAlias); } [Theory] @@ -385,4 +384,4 @@ public override int Invoke(ParseResult parseResult) return 0; } } -} \ No newline at end of file +} diff --git a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs index 6b59494ea8..be8b607bd2 100644 --- a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs +++ b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs @@ -4,7 +4,6 @@ using FluentAssertions; using System.CommandLine.Help; using System.CommandLine.Invocation; -using System.CommandLine.Tests.Utility; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -31,7 +30,7 @@ public void Help_is_shown_when_required_subcommand_is_missing() var result = parseResult.Invoke(new() { Output = output }); result.Should().Be(1); - output.ToString().Should().ShowHelp(); + output.ToString().Should().Contain("--help"); } [Fact] @@ -53,7 +52,7 @@ public void Help_display_can_be_disabled() result.Invoke(new() { Output = output }); - output.ToString().Should().NotShowHelp(); + output.ToString().Should().BeNullOrWhiteSpace(); } [Theory] // https://github.com/dotnet/command-line-api/issues/2226 @@ -110,7 +109,7 @@ public void When_no_help_option_is_present_then_help_is_not_shown_for_parse_erro rootCommand.Parse("oops").Invoke(new() { Output = output } ); - output.ToString().Should().NotShowHelp(); + output.ToString().Should().BeNullOrWhiteSpace(); } [Fact] @@ -172,4 +171,4 @@ public void Pre_actions_cannot_clear_parse_errors() result.Errors.Should().NotBeEmpty(); } -} \ 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 d6a0446653..b7e8b49bc6 100644 --- a/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs +++ b/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs @@ -6,7 +6,6 @@ using FluentAssertions; using FluentAssertions.Collections; using FluentAssertions.Execution; -using FluentAssertions.Primitives; namespace System.CommandLine.Tests.Utility; @@ -45,10 +44,4 @@ public static AndConstraint>> BeE { return assertions.BeEquivalentTo(expectedValues, c => c.WithStrictOrderingFor(s => s)); } - - public static AndConstraint ShowHelp(this StringAssertions output) => - output.Subject.Should().Match("*Usage:*Options:*"); - - public static AndConstraint NotShowHelp(this StringAssertions output) => - output.Subject.Should().NotMatch("*Usage:*Options:*"); } From 41b2151a09ce3f89b6e363c55b299369651c9c5b Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:49:02 +0300 Subject: [PATCH 3/3] Preserve semantic help assertions Restore the shared help assertions and give affected test commands descriptions so the helpers continue to verify complete help output. Signed-off-by: subotac <73706465+subotac@users.noreply.github.com> --- src/System.CommandLine.Tests/HelpOptionTests.cs | 13 +++++++------ .../ParseErrorReportingTests.cs | 13 +++++++------ .../Utility/AssertionExtensions.cs | 7 +++++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/System.CommandLine.Tests/HelpOptionTests.cs b/src/System.CommandLine.Tests/HelpOptionTests.cs index 5a040a97d2..1b1a282c5f 100644 --- a/src/System.CommandLine.Tests/HelpOptionTests.cs +++ b/src/System.CommandLine.Tests/HelpOptionTests.cs @@ -4,6 +4,7 @@ using FluentAssertions; using System.CommandLine.Help; using System.CommandLine.Invocation; +using System.CommandLine.Tests.Utility; using System.IO; using System.Linq; using System.Threading; @@ -56,7 +57,7 @@ public async Task Help_option_interrupts_execution_of_the_specified_command() [InlineData("/?")] public async Task Help_option_accepts_default_values(string value) { - var command = new Command("command") + var command = new Command("command", "command description") { new HelpOption() }; @@ -65,20 +66,20 @@ public async Task Help_option_accepts_default_values(string value) await command.Parse($"command {value}").InvokeAsync(new() { Output = output }, CancellationToken.None); - output.ToString().Should().Contain("--help"); + output.ToString().Should().ShowHelp(); } [Fact] public async Task Help_option_does_not_display_when_option_defined_with_same_alias() { - var command = new Command("command"); + var command = new Command("command", "command description"); command.Options.Add(new Option("-h")); var output = new StringWriter(); await command.Parse("command -h").InvokeAsync(new() { Output = output }, CancellationToken.None); - output.ToString().Should().BeNullOrWhiteSpace(); + output.ToString().Should().NotShowHelp(); } [Fact] @@ -142,7 +143,7 @@ public void There_are_no_parse_errors_when_help_is_invoked_on_a_command_with_req [InlineData("--confused")] public async Task HelpOption_with_custom_aliases_uses_aliases(string helpAlias) { - RootCommand command = new() + RootCommand command = new("root description") { new HelpOption("/lost", "--confused") }; @@ -150,7 +151,7 @@ public async Task HelpOption_with_custom_aliases_uses_aliases(string helpAlias) await command.Parse(helpAlias).InvokeAsync(new() { Output = output }, CancellationToken.None); - output.ToString().Should().Contain(helpAlias); + output.ToString().Should().ShowHelp(); } [Theory] diff --git a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs index be8b607bd2..12c886f5bf 100644 --- a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs +++ b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs @@ -4,6 +4,7 @@ using FluentAssertions; using System.CommandLine.Help; using System.CommandLine.Invocation; +using System.CommandLine.Tests.Utility; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -16,7 +17,7 @@ public class ParseErrorReportingTests [Fact] // https://github.com/dotnet/command-line-api/issues/817 public void Help_is_shown_when_required_subcommand_is_missing() { - var root = new RootCommand + var root = new RootCommand("root description") { new Command("inner"), new HelpOption() @@ -30,13 +31,13 @@ public void Help_is_shown_when_required_subcommand_is_missing() var result = parseResult.Invoke(new() { Output = output }); result.Should().Be(1); - output.ToString().Should().Contain("--help"); + output.ToString().Should().ShowHelp(); } [Fact] public void Help_display_can_be_disabled() { - RootCommand rootCommand = new() + RootCommand rootCommand = new("root description") { new Option("--verbose") }; @@ -52,7 +53,7 @@ public void Help_display_can_be_disabled() result.Invoke(new() { Output = output }); - output.ToString().Should().BeNullOrWhiteSpace(); + output.ToString().Should().NotShowHelp(); } [Theory] // https://github.com/dotnet/command-line-api/issues/2226 @@ -103,13 +104,13 @@ public async Task When_there_are_parse_errors_then_customized_help_action_on_anc [Fact] public void When_no_help_option_is_present_then_help_is_not_shown_for_parse_errors() { - RootCommand rootCommand = new(); + RootCommand rootCommand = new("root description"); rootCommand.Options.Clear(); var output = new StringWriter(); rootCommand.Parse("oops").Invoke(new() { Output = output } ); - output.ToString().Should().BeNullOrWhiteSpace(); + output.ToString().Should().NotShowHelp(); } [Fact] diff --git a/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs b/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs index b7e8b49bc6..61c70c4b61 100644 --- a/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs +++ b/src/System.CommandLine.Tests/Utility/AssertionExtensions.cs @@ -6,6 +6,7 @@ using FluentAssertions; using FluentAssertions.Collections; using FluentAssertions.Execution; +using FluentAssertions.Primitives; namespace System.CommandLine.Tests.Utility; @@ -44,4 +45,10 @@ public static AndConstraint>> BeE { 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:*"); }