From 80a6e4fcfdc40e0535a2719e8f4d0171f5993b42 Mon Sep 17 00:00:00 2001 From: Aaron Sherber Date: Tue, 6 Aug 2019 06:59:45 -0400 Subject: [PATCH 1/5] Improve spacing in HelpText --- src/CommandLine/Text/HelpText.cs | 14 +- .../Unit/Text/HelpTextTests.cs | 180 ++++++++++++------ 2 files changed, 133 insertions(+), 61 deletions(-) diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index b23bb804..1eed8ea9 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -352,7 +352,7 @@ public static HelpText AutoBuild( { var heading = auto.SentenceBuilder.UsageHeadingText(); if (heading.Length > 0) - auto.AddPreOptionsLine(heading); + auto.AddPreOptionsLine(string.Concat(Environment.NewLine, heading)); } usageAttr.Do( @@ -707,12 +707,20 @@ public static IEnumerable RenderUsageTextAsLines(ParserResult pars public override string ToString() { const int ExtraLength = 10; + + string ExtraLineIfNeeded = (heading.SafeLength() > 0 && copyright.SafeLength() > 0 + && preOptionsHelp.Length >= Environment.NewLine.Length + && preOptionsHelp.ToString(0, Environment.NewLine.Length) != Environment.NewLine) + ? Environment.NewLine + : null; + return new StringBuilder( heading.SafeLength() + copyright.SafeLength() + preOptionsHelp.SafeLength() + - optionsHelp.SafeLength() + ExtraLength).Append(heading) + optionsHelp.SafeLength() + ExtraLength) + .Append(heading) .AppendWhen(!string.IsNullOrEmpty(copyright), Environment.NewLine, copyright) - .AppendWhen(preOptionsHelp.Length > 0, Environment.NewLine, preOptionsHelp.ToString()) + .AppendWhen(preOptionsHelp.Length > 0, ExtraLineIfNeeded, Environment.NewLine, preOptionsHelp.ToString()) .AppendWhen( optionsHelp != null && optionsHelp.Length > 0, Environment.NewLine, diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index 1dd8d45f..bf09f941 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -41,14 +41,15 @@ public void Create_instance_without_options() .AddPostOptionsLine("post-options line 2"); // Verify outcome - var lines = sut.ToString().ToNotEmptyLines(); + var lines = sut.ToString().ToLines(); lines[0].Should().BeEquivalentTo("Unit-tests 2.0"); lines[1].Should().BeEquivalentTo("Copyright (C) 2005 - 2013 Author"); - lines[2].Should().BeEquivalentTo("pre-options line 1"); - lines[3].Should().BeEquivalentTo("pre-options line 2"); - lines[4].Should().BeEquivalentTo("post-options line 1"); - lines[5].Should().BeEquivalentTo("post-options line 2"); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("pre-options line 1"); + lines[4].Should().BeEquivalentTo("pre-options line 2"); + lines[5].Should().BeEquivalentTo("post-options line 1"); + lines[6].Should().BeEquivalentTo("post-options line 2"); // Teardown } @@ -320,17 +321,23 @@ public void Invoke_AutoBuild_for_Options_returns_appropriate_formatted_text() var helpText = HelpText.AutoBuild(fakeResult); // Verify outcome - var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); + var lines = helpText.ToString().ToLines().TrimStringArray(); lines[0].Should().Be(HeadingInfo.Default.ToString()); - lines[1].Should().Be(CopyrightInfo.Default.ToString()); - lines[2].Should().BeEquivalentTo("ERROR(S):"); - lines[3].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); - lines[4].Should().BeEquivalentTo("A sequence option 'i' is defined with fewer or more items than required."); - lines[5].Should().BeEquivalentTo("--stringvalue Define a string value here."); - lines[6].Should().BeEquivalentTo("-s, --shortandlong Example with both short and long name."); - lines[7].Should().BeEquivalentTo("-i Define a int sequence here."); - lines[8].Should().BeEquivalentTo("-x Define a boolean or switch value here."); - lines[9].Should().BeEquivalentTo("--help Display this help screen."); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("ERROR(S):"); + lines[4].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); + lines[5].Should().BeEquivalentTo("A sequence option 'i' is defined with fewer or more items than required."); + lines[6].Should().BeEmpty(); + lines[7].Should().BeEquivalentTo("--stringvalue Define a string value here."); + lines[8].Should().BeEmpty(); + lines[9].Should().BeEquivalentTo("-s, --shortandlong Example with both short and long name."); + lines[10].Should().BeEmpty(); + lines[11].Should().BeEquivalentTo("-i Define a int sequence here."); + lines[12].Should().BeEmpty(); + lines[13].Should().BeEquivalentTo("-x Define a boolean or switch value here."); + lines[14].Should().BeEmpty(); + lines[15].Should().BeEquivalentTo("--help Display this help screen."); // Teardown } @@ -349,15 +356,19 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo var helpText = HelpText.AutoBuild(fakeResult); // Verify outcome - var lines = helpText.ToString().ToNotEmptyLines().TrimStringArray(); + var lines = helpText.ToString().ToLines().TrimStringArray(); lines[0].Should().Be(HeadingInfo.Default.ToString()); - lines[1].Should().Be(CopyrightInfo.Default.ToString()); - lines[2].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which"); - lines[3].Should().BeEquivalentTo("changes to commit."); - lines[4].Should().BeEquivalentTo("--amend Used to amend the tip of the current branch."); - lines[5].Should().BeEquivalentTo("-m, --message Use the given message as the commit message."); - lines[6].Should().BeEquivalentTo("--help Display this help screen."); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which"); + lines[4].Should().BeEquivalentTo("changes to commit."); + lines[5].Should().BeEmpty(); + lines[6].Should().BeEquivalentTo("--amend Used to amend the tip of the current branch."); + lines[7].Should().BeEmpty(); + lines[8].Should().BeEquivalentTo("-m, --message Use the given message as the commit message."); + lines[9].Should().BeEmpty(); + lines[10].Should().BeEquivalentTo("--help Display this help screen."); // Teardown } @@ -418,23 +429,26 @@ public void Create_instance_with_options_and_values() { // Fixture setup // Exercize system - var sut = new HelpText { AddDashesToOption = true } + var sut = new HelpText { AddDashesToOption = true, AdditionalNewLineAfterOption = false } .AddPreOptionsLine("pre-options") .AddOptions(new NotParsed(TypeInfo.Create(typeof(Options_With_HelpText_And_MetaValue)), Enumerable.Empty())) .AddPostOptionsLine("post-options"); // Verify outcome - var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].Should().BeEquivalentTo("pre-options"); - lines[1].Should().BeEquivalentTo("--stringvalue=STR Define a string value here."); - lines[2].Should().BeEquivalentTo("-i INTSEQ Define a int sequence here."); - lines[3].Should().BeEquivalentTo("-x Define a boolean or switch value here."); - lines[4].Should().BeEquivalentTo("--help Display this help screen."); - lines[5].Should().BeEquivalentTo("--version Display version information."); - lines[6].Should().BeEquivalentTo("number (pos. 0) NUM Define a long value here."); - lines[7].Should().BeEquivalentTo("paintcolor (pos. 1) COLOR Define a color value here."); - lines[8].Should().BeEquivalentTo("post-options", lines[8]); + var lines = sut.ToString().ToLines().TrimStringArray(); + lines[0].Should().BeEmpty(); + lines[1].Should().BeEquivalentTo("pre-options"); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("--stringvalue=STR Define a string value here."); + lines[4].Should().BeEquivalentTo("-i INTSEQ Define a int sequence here."); + lines[5].Should().BeEquivalentTo("-x Define a boolean or switch value here."); + lines[6].Should().BeEquivalentTo("--help Display this help screen."); + lines[7].Should().BeEquivalentTo("--version Display version information."); + lines[8].Should().BeEquivalentTo("number (pos. 0) NUM Define a long value here."); + lines[9].Should().BeEquivalentTo("paintcolor (pos. 1) COLOR Define a color value here."); + lines[10].Should().BeEmpty(); + lines[11].Should().BeEquivalentTo("post-options", lines[11]); // Teardown } @@ -482,36 +496,86 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte // Verify outcome var text = helpText.ToString(); - var lines = text.ToNotEmptyLines().TrimStringArray(); + var lines = text.ToLines().TrimStringArray(); lines[0].Should().Be(HeadingInfo.Default.ToString()); - lines[1].Should().Be(CopyrightInfo.Default.ToString()); - lines[2].Should().BeEquivalentTo("ERROR(S):"); - lines[3].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); - lines[4].Should().BeEquivalentTo("USAGE:"); - lines[5].Should().BeEquivalentTo("Normal scenario:"); - lines[6].Should().BeEquivalentTo("mono testapp.exe --input file.bin --output out.bin"); - lines[7].Should().BeEquivalentTo("Logging warnings:"); - lines[8].Should().BeEquivalentTo("mono testapp.exe -w --input file.bin"); - lines[9].Should().BeEquivalentTo("Logging errors:"); - lines[10].Should().BeEquivalentTo("mono testapp.exe -e --input file.bin"); - lines[11].Should().BeEquivalentTo("mono testapp.exe --errs --input=file.bin"); - lines[12].Should().BeEquivalentTo("List:"); - lines[13].Should().BeEquivalentTo("mono testapp.exe -l 1,2"); - lines[14].Should().BeEquivalentTo("Value:"); - lines[15].Should().BeEquivalentTo("mono testapp.exe value"); - lines[16].Should().BeEquivalentTo("-i, --input Set input file."); - lines[17].Should().BeEquivalentTo("-i, --output Set output file."); - lines[18].Should().BeEquivalentTo("--verbose Set verbosity level."); - lines[19].Should().BeEquivalentTo("-w, --warns Log warnings."); - lines[20].Should().BeEquivalentTo("-e, --errs Log errors."); - lines[21].Should().BeEquivalentTo("-l List."); - lines[22].Should().BeEquivalentTo("--help Display this help screen."); - lines[23].Should().BeEquivalentTo("--version Display version information."); - lines[24].Should().BeEquivalentTo("value pos. 0 Value."); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("ERROR(S):"); + lines[4].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); + lines[5].Should().BeEmpty(); + lines[6].Should().BeEquivalentTo("USAGE:"); + lines[7].Should().BeEquivalentTo("Normal scenario:"); + lines[8].Should().BeEquivalentTo("mono testapp.exe --input file.bin --output out.bin"); + lines[9].Should().BeEquivalentTo("Logging warnings:"); + lines[10].Should().BeEquivalentTo("mono testapp.exe -w --input file.bin"); + lines[11].Should().BeEquivalentTo("Logging errors:"); + lines[12].Should().BeEquivalentTo("mono testapp.exe -e --input file.bin"); + lines[13].Should().BeEquivalentTo("mono testapp.exe --errs --input=file.bin"); + lines[14].Should().BeEquivalentTo("List:"); + lines[15].Should().BeEquivalentTo("mono testapp.exe -l 1,2"); + lines[16].Should().BeEquivalentTo("Value:"); + lines[17].Should().BeEquivalentTo("mono testapp.exe value"); + lines[18].Should().BeEmpty(); + lines[19].Should().BeEquivalentTo("-i, --input Set input file."); + lines[20].Should().BeEmpty(); + lines[21].Should().BeEquivalentTo("-i, --output Set output file."); + lines[22].Should().BeEmpty(); + lines[23].Should().BeEquivalentTo("--verbose Set verbosity level."); + lines[24].Should().BeEmpty(); + lines[25].Should().BeEquivalentTo("-w, --warns Log warnings."); + lines[26].Should().BeEmpty(); + lines[27].Should().BeEquivalentTo("-e, --errs Log errors."); + lines[28].Should().BeEmpty(); + lines[29].Should().BeEquivalentTo("-l List."); + lines[30].Should().BeEmpty(); + lines[31].Should().BeEquivalentTo("--help Display this help screen."); + lines[32].Should().BeEmpty(); + lines[33].Should().BeEquivalentTo("--version Display version information."); + lines[34].Should().BeEmpty(); + lines[35].Should().BeEquivalentTo("value pos. 0 Value."); // Teardown } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWithNewline) + { + // Fixture setup + var fakeResult = new NotParsed( + TypeInfo.Create(typeof(Simple_Options_Without_HelpText)), + new Error[] + { + new BadFormatTokenError("badtoken") + }); + + // Exercize system + var helpText = HelpText.AutoBuild(fakeResult, + h => + { + h.AddPreOptionsLine((startWithNewline ? Environment.NewLine : null) + "pre-options"); + return HelpText.DefaultParsingErrorsHandler(fakeResult, h); + }, + e => e + ); + + // Verify outcome + var text = helpText.ToString(); + var lines = text.ToLines().TrimStringArray(); + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("pre-options"); + lines[4].Should().BeEmpty(); + lines[5].Should().BeEquivalentTo("ERROR(S):"); + lines[6].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); + lines[7].Should().BeEmpty(); + lines[8].Should().BeEquivalentTo("-v, --verbose"); + lines[9].Should().BeEmpty(); + lines[10].Should().BeEquivalentTo("--input-file"); + } + [Fact] public void Default_set_to_sequence_should_be_properly_printed() { From f540c178a917d723a17030abf72aa7b57526a537 Mon Sep 17 00:00:00 2001 From: Aaron Sherber Date: Wed, 21 Aug 2019 20:50:02 -0400 Subject: [PATCH 2/5] Make new spacing an option --- src/CommandLine/Text/HelpText.cs | 19 +- .../Unit/Text/HelpTextTests.cs | 178 +++++++++++------- 2 files changed, 127 insertions(+), 70 deletions(-) diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index 1eed8ea9..f8d8c538 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -109,6 +109,7 @@ ComparableOption ToComparableOption(Specification spec, int index) private bool addEnumValuesToHelpText; private bool autoHelp; private bool autoVersion; + private bool addNewLineBetweenHelpSections; /// /// Initializes a new instance of the class. @@ -258,6 +259,15 @@ public bool AdditionalNewLineAfterOption set { additionalNewLineAfterOption = value; } } + /// + /// Gets or sets a value indicating whether to add newlines between help sections. + /// + public bool AddNewLineBetweenHelpSections + { + get { return addNewLineBetweenHelpSections; } + set { addNewLineBetweenHelpSections = value; } + } + /// /// Gets or sets a value indicating whether to add the values of an enum after the description of the specification. /// @@ -352,7 +362,11 @@ public static HelpText AutoBuild( { var heading = auto.SentenceBuilder.UsageHeadingText(); if (heading.Length > 0) - auto.AddPreOptionsLine(string.Concat(Environment.NewLine, heading)); + { + if (auto.AddNewLineBetweenHelpSections) + heading = Environment.NewLine + heading; + auto.AddPreOptionsLine(heading); + } } usageAttr.Do( @@ -708,7 +722,8 @@ public override string ToString() { const int ExtraLength = 10; - string ExtraLineIfNeeded = (heading.SafeLength() > 0 && copyright.SafeLength() > 0 + string ExtraLineIfNeeded = AddNewLineBetweenHelpSections + && (heading.SafeLength() > 0 && copyright.SafeLength() > 0 && preOptionsHelp.Length >= Environment.NewLine.Length && preOptionsHelp.ToString(0, Environment.NewLine.Length) != Environment.NewLine) ? Environment.NewLine diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index bf09f941..a21a235d 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -28,29 +28,39 @@ public void Create_empty_instance() string.Empty.Should().BeEquivalentTo(new HelpText().ToString()); } - [Fact] - public void Create_instance_without_options() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Create_instance_without_options(bool newlineBetweenSections) { // Fixture setup // Exercize system var sut = - new HelpText(new HeadingInfo("Unit-tests", "2.0"), new CopyrightInfo(true, "Author", 2005, 2013)) - .AddPreOptionsLine("pre-options line 1") + new HelpText(new HeadingInfo("Unit-tests", "2.0"), new CopyrightInfo(true, "Author", 2005, 2013)); + sut.AddNewLineBetweenHelpSections = newlineBetweenSections; + sut.AddPreOptionsLine("pre-options line 1") .AddPreOptionsLine("pre-options line 2") .AddPostOptionsLine("post-options line 1") .AddPostOptionsLine("post-options line 2"); // Verify outcome - var lines = sut.ToString().ToLines(); + var expected = new[] + { + "Unit-tests 2.0", + "Copyright (C) 2005 - 2013 Author", + "pre-options line 1", + "pre-options line 2", + "post-options line 1", + "post-options line 2" + }; - lines[0].Should().BeEquivalentTo("Unit-tests 2.0"); - lines[1].Should().BeEquivalentTo("Copyright (C) 2005 - 2013 Author"); - lines[2].Should().BeEmpty(); - lines[3].Should().BeEquivalentTo("pre-options line 1"); - lines[4].Should().BeEquivalentTo("pre-options line 2"); - lines[5].Should().BeEquivalentTo("post-options line 1"); - lines[6].Should().BeEquivalentTo("post-options line 2"); - // Teardown + var takeCount = newlineBetweenSections ? expected.Length + 1 : expected.Length; + var lines = sut.ToString().ToLines().Take(takeCount).ToArray(); + + lines.Should().ContainInOrder(expected); + + if (newlineBetweenSections) + lines[2].Should().BeEmpty(); } [Fact] @@ -480,8 +490,10 @@ public static void RenderUsageText_returns_properly_formatted_text() lines[10].Should().BeEquivalentTo(" mono testapp.exe value"); } - [Fact] - public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatted_text() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatted_text(bool newlineBetweenSections) { // Fixture setup var fakeResult = new NotParsed( @@ -492,55 +504,74 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte }); // Exercize system - var helpText = HelpText.AutoBuild(fakeResult); + var helpText = HelpText.AutoBuild(fakeResult, + h => + { + h.AddNewLineBetweenHelpSections = newlineBetweenSections; + return HelpText.DefaultParsingErrorsHandler(fakeResult, h); + }, + e => e + ); // Verify outcome + var expected = new[] + { + HeadingInfo.Default.ToString(), + CopyrightInfo.Default.ToString(), + "", + "ERROR(S):", + "Token 'badtoken' is not recognized.", + "USAGE:", + "Normal scenario:", + "mono testapp.exe --input file.bin --output out.bin", + "Logging warnings:", + "mono testapp.exe -w --input file.bin", + "Logging errors:", + "mono testapp.exe -e --input file.bin", + "mono testapp.exe --errs --input=file.bin", + "List:", + "mono testapp.exe -l 1,2", + "Value:", + "mono testapp.exe value", + "", + "-i, --input Set input file.", + "", + "-i, --output Set output file.", + "", + "--verbose Set verbosity level.", + "", + "-w, --warns Log warnings.", + "", + "-e, --errs Log errors.", + "", + "-l List.", + "", + "--help Display this help screen.", + "", + "--version Display version information.", + "", + "value pos. 0 Value." + }; + var takeCount = newlineBetweenSections ? expected.Length + 1 : expected.Length; + var text = helpText.ToString(); - var lines = text.ToLines().TrimStringArray(); - lines[0].Should().Be(HeadingInfo.Default.ToString()); - lines[1].Should().Be(CopyrightInfo.Default.ToString()); - lines[2].Should().BeEmpty(); - lines[3].Should().BeEquivalentTo("ERROR(S):"); - lines[4].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); - lines[5].Should().BeEmpty(); - lines[6].Should().BeEquivalentTo("USAGE:"); - lines[7].Should().BeEquivalentTo("Normal scenario:"); - lines[8].Should().BeEquivalentTo("mono testapp.exe --input file.bin --output out.bin"); - lines[9].Should().BeEquivalentTo("Logging warnings:"); - lines[10].Should().BeEquivalentTo("mono testapp.exe -w --input file.bin"); - lines[11].Should().BeEquivalentTo("Logging errors:"); - lines[12].Should().BeEquivalentTo("mono testapp.exe -e --input file.bin"); - lines[13].Should().BeEquivalentTo("mono testapp.exe --errs --input=file.bin"); - lines[14].Should().BeEquivalentTo("List:"); - lines[15].Should().BeEquivalentTo("mono testapp.exe -l 1,2"); - lines[16].Should().BeEquivalentTo("Value:"); - lines[17].Should().BeEquivalentTo("mono testapp.exe value"); - lines[18].Should().BeEmpty(); - lines[19].Should().BeEquivalentTo("-i, --input Set input file."); - lines[20].Should().BeEmpty(); - lines[21].Should().BeEquivalentTo("-i, --output Set output file."); - lines[22].Should().BeEmpty(); - lines[23].Should().BeEquivalentTo("--verbose Set verbosity level."); - lines[24].Should().BeEmpty(); - lines[25].Should().BeEquivalentTo("-w, --warns Log warnings."); - lines[26].Should().BeEmpty(); - lines[27].Should().BeEquivalentTo("-e, --errs Log errors."); - lines[28].Should().BeEmpty(); - lines[29].Should().BeEquivalentTo("-l List."); - lines[30].Should().BeEmpty(); - lines[31].Should().BeEquivalentTo("--help Display this help screen."); - lines[32].Should().BeEmpty(); - lines[33].Should().BeEquivalentTo("--version Display version information."); - lines[34].Should().BeEmpty(); - lines[35].Should().BeEquivalentTo("value pos. 0 Value."); + var lines = text.ToLines().TrimStringArray().Take(takeCount).ToArray(); + + lines.Should().ContainInOrder(expected); + + if (newlineBetweenSections) + lines[5].Should().BeEmpty(); + // Teardown } [Theory] - [InlineData(true)] - [InlineData(false)] - public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWithNewline) + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWithNewline, bool newlineBetweenSections) { // Fixture setup var fakeResult = new NotParsed( @@ -554,6 +585,7 @@ public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWit var helpText = HelpText.AutoBuild(fakeResult, h => { + h.AddNewLineBetweenHelpSections = newlineBetweenSections; h.AddPreOptionsLine((startWithNewline ? Environment.NewLine : null) + "pre-options"); return HelpText.DefaultParsingErrorsHandler(fakeResult, h); }, @@ -561,19 +593,29 @@ public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWit ); // Verify outcome + var expected = new[] + { + HeadingInfo.Default.ToString(), + CopyrightInfo.Default.ToString(), + "pre-options", + "", + "ERROR(S):", + "Token 'badtoken' is not recognized.", + "", + "-v, --verbose", + "", + "--input-file" + }; + + var takeCount = newlineBetweenSections || startWithNewline ? expected.Length + 1 : expected.Length; + var text = helpText.ToString(); - var lines = text.ToLines().TrimStringArray(); - lines[0].Should().Be(HeadingInfo.Default.ToString()); - lines[1].Should().Be(CopyrightInfo.Default.ToString()); - lines[2].Should().BeEmpty(); - lines[3].Should().BeEquivalentTo("pre-options"); - lines[4].Should().BeEmpty(); - lines[5].Should().BeEquivalentTo("ERROR(S):"); - lines[6].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); - lines[7].Should().BeEmpty(); - lines[8].Should().BeEquivalentTo("-v, --verbose"); - lines[9].Should().BeEmpty(); - lines[10].Should().BeEquivalentTo("--input-file"); + var lines = text.ToLines().TrimStringArray().Take(takeCount).ToArray(); + + lines.Should().ContainInOrder(expected); + + if (newlineBetweenSections || startWithNewline) + lines[2].Should().BeEmpty(); } [Fact] From 4aa52ad828570a9d4c45a9daf12dd8a7704a1750 Mon Sep 17 00:00:00 2001 From: Aaron Sherber Date: Mon, 26 Aug 2019 13:27:07 -0400 Subject: [PATCH 3/5] Refactor --- .../Infrastructure/StringBuilderExtensions.cs | 20 +++- src/CommandLine/Text/HelpText.cs | 38 ++++--- .../Unit/StringBuilderExtensionsTests.cs | 104 ++++++++++++++++++ 3 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 tests/CommandLine.Tests/Unit/StringBuilderExtensionsTests.cs diff --git a/src/CommandLine/Infrastructure/StringBuilderExtensions.cs b/src/CommandLine/Infrastructure/StringBuilderExtensions.cs index 6519b66f..0e73cadd 100644 --- a/src/CommandLine/Infrastructure/StringBuilderExtensions.cs +++ b/src/CommandLine/Infrastructure/StringBuilderExtensions.cs @@ -113,5 +113,23 @@ public static int TrailingSpaces(this StringBuilder builder) } return c; } + + public static bool SafeStartsWith(this StringBuilder builder, string s) + { + if (string.IsNullOrEmpty(s)) + return false; + + return builder?.Length >= s.Length + && builder.ToString(0, s.Length) == s; + } + + public static bool SafeEndsWith(this StringBuilder builder, string s) + { + if (string.IsNullOrEmpty(s)) + return false; + + return builder?.Length >= s.Length + && builder.ToString(builder.Length - s.Length, s.Length) == s; + } } -} \ No newline at end of file +} diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index f8d8c538..8bbcce5b 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -722,27 +722,33 @@ public override string ToString() { const int ExtraLength = 10; - string ExtraLineIfNeeded = AddNewLineBetweenHelpSections - && (heading.SafeLength() > 0 && copyright.SafeLength() > 0 - && preOptionsHelp.Length >= Environment.NewLine.Length - && preOptionsHelp.ToString(0, Environment.NewLine.Length) != Environment.NewLine) - ? Environment.NewLine - : null; - - return - new StringBuilder( - heading.SafeLength() + copyright.SafeLength() + preOptionsHelp.SafeLength() + - optionsHelp.SafeLength() + ExtraLength) - .Append(heading) + var sbLength = heading.SafeLength() + copyright.SafeLength() + preOptionsHelp.SafeLength() + + optionsHelp.SafeLength() + postOptionsHelp.SafeLength() + ExtraLength; + var result = new StringBuilder(sbLength); + + result.Append(heading) .AppendWhen(!string.IsNullOrEmpty(copyright), Environment.NewLine, copyright) - .AppendWhen(preOptionsHelp.Length > 0, ExtraLineIfNeeded, Environment.NewLine, preOptionsHelp.ToString()) .AppendWhen( - optionsHelp != null && optionsHelp.Length > 0, + preOptionsHelp.SafeLength() > 0, + Environment.NewLine, + NewLineIfNeededBefore(preOptionsHelp), + preOptionsHelp.ToString()) + .AppendWhen( + optionsHelp.SafeLength() > 0, Environment.NewLine, Environment.NewLine, optionsHelp.SafeToString()) - .AppendWhen(postOptionsHelp.Length > 0, Environment.NewLine, postOptionsHelp.ToString()) - .ToString(); + .AppendWhen(postOptionsHelp.SafeLength() > 0, Environment.NewLine, postOptionsHelp.ToString()); + + string NewLineIfNeededBefore(StringBuilder sb) + { + if (AddNewLineBetweenHelpSections && result.Length > 0 && !sb.SafeStartsWith(Environment.NewLine)) + return Environment.NewLine; + else + return null; + } + + return result.ToString(); } internal static void AddLine(StringBuilder builder, string value, int maximumLength) diff --git a/tests/CommandLine.Tests/Unit/StringBuilderExtensionsTests.cs b/tests/CommandLine.Tests/Unit/StringBuilderExtensionsTests.cs new file mode 100644 index 00000000..8fe73a60 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/StringBuilderExtensionsTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using FluentAssertions; +using CommandLine.Infrastructure; + +namespace CommandLine.Tests.Unit +{ + public class StringBuilderExtensionsTests + { + private static StringBuilder _sb = new StringBuilder("test string"); + private static StringBuilder _emptySb = new StringBuilder(); + private static StringBuilder _nullSb = null; + + public static IEnumerable GoodStartsWithData => new [] + { + new object[] { "t" }, + new object[] { "te" }, + new object[] { "test " }, + new object[] { "test string" } + }; + + public static IEnumerable BadTestData => new [] + { + new object[] { null }, + new object[] { "" }, + new object[] { "xyz" }, + new object[] { "some long test string" } + }; + + public static IEnumerable GoodEndsWithData => new[] + { + new object[] { "g" }, + new object[] { "ng" }, + new object[] { " string" }, + new object[] { "test string" } + }; + + + + [Theory] + [MemberData(nameof(GoodStartsWithData))] + [MemberData(nameof(BadTestData))] + public void StartsWith_null_builder_returns_false(string input) + { + _nullSb.SafeStartsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodStartsWithData))] + [MemberData(nameof(BadTestData))] + public void StartsWith_empty_builder_returns_false(string input) + { + _emptySb.SafeStartsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodStartsWithData))] + public void StartsWith_good_data_returns_true(string input) + { + _sb.SafeStartsWith(input).Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(BadTestData))] + public void StartsWith_bad_data_returns_false(string input) + { + _sb.SafeStartsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodEndsWithData))] + [MemberData(nameof(BadTestData))] + public void EndsWith_null_builder_returns_false(string input) + { + _nullSb.SafeEndsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodEndsWithData))] + [MemberData(nameof(BadTestData))] + public void EndsWith_empty_builder_returns_false(string input) + { + _emptySb.SafeEndsWith(input).Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(GoodEndsWithData))] + public void EndsWith_good_data_returns_true(string input) + { + _sb.SafeEndsWith(input).Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(BadTestData))] + public void EndsWith_bad_data_returns_false(string input) + { + _sb.SafeEndsWith(input).Should().BeFalse(); + } + } +} From 0eec5afc08c48608b90b84f6735123d1ef781333 Mon Sep 17 00:00:00 2001 From: Aaron Sherber Date: Mon, 26 Aug 2019 17:14:30 -0400 Subject: [PATCH 4/5] Extend logic to PostOptions --- src/CommandLine/Text/HelpText.cs | 22 ++++-- .../Unit/Text/HelpTextTests.cs | 79 ++++++++++--------- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index 8bbcce5b..2112a904 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -727,22 +727,28 @@ public override string ToString() var result = new StringBuilder(sbLength); result.Append(heading) - .AppendWhen(!string.IsNullOrEmpty(copyright), Environment.NewLine, copyright) - .AppendWhen( - preOptionsHelp.SafeLength() > 0, - Environment.NewLine, + .AppendWhen(!string.IsNullOrEmpty(copyright), + Environment.NewLine, + copyright) + .AppendWhen(preOptionsHelp.SafeLength() > 0, NewLineIfNeededBefore(preOptionsHelp), + Environment.NewLine, preOptionsHelp.ToString()) - .AppendWhen( - optionsHelp.SafeLength() > 0, + .AppendWhen(optionsHelp.SafeLength() > 0, Environment.NewLine, Environment.NewLine, optionsHelp.SafeToString()) - .AppendWhen(postOptionsHelp.SafeLength() > 0, Environment.NewLine, postOptionsHelp.ToString()); + .AppendWhen(postOptionsHelp.SafeLength() > 0, + NewLineIfNeededBefore(postOptionsHelp), + Environment.NewLine, + postOptionsHelp.ToString()); string NewLineIfNeededBefore(StringBuilder sb) { - if (AddNewLineBetweenHelpSections && result.Length > 0 && !sb.SafeStartsWith(Environment.NewLine)) + if (AddNewLineBetweenHelpSections + && result.Length > 0 + && !result.SafeEndsWith(Environment.NewLine) + && !sb.SafeStartsWith(Environment.NewLine)) return Environment.NewLine; else return null; diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index a21a235d..32e7ee90 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -44,7 +44,7 @@ public void Create_instance_without_options(bool newlineBetweenSections) .AddPostOptionsLine("post-options line 2"); // Verify outcome - var expected = new[] + var expected = new List() { "Unit-tests 2.0", "Copyright (C) 2005 - 2013 Author", @@ -54,38 +54,47 @@ public void Create_instance_without_options(bool newlineBetweenSections) "post-options line 2" }; - var takeCount = newlineBetweenSections ? expected.Length + 1 : expected.Length; - var lines = sut.ToString().ToLines().Take(takeCount).ToArray(); - - lines.Should().ContainInOrder(expected); - if (newlineBetweenSections) - lines[2].Should().BeEmpty(); + { + expected.Insert(2, ""); + expected.Insert(5, ""); + } + + var lines = sut.ToString().ToLines(); + lines.Should().StartWith(expected); } - [Fact] - public void Create_instance_with_options() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Create_instance_with_options(bool newlineBetweenSections) { // Fixture setup // Exercize system - var sut = new HelpText { AddDashesToOption = true } + var sut = new HelpText { AddDashesToOption = true, AddNewLineBetweenHelpSections = newlineBetweenSections } .AddPreOptionsLine("pre-options") .AddOptions(new NotParsed(TypeInfo.Create(typeof(Simple_Options)), Enumerable.Empty())) .AddPostOptionsLine("post-options"); // Verify outcome + var expected = new [] + { + "", + "pre-options", + "", + "--stringvalue Define a string value here.", + "-s, --shortandlong Example with both short and long name.", + "-i Define a int sequence here.", + "-x Define a boolean or switch value here.", + "--help Display this help screen.", + "--version Display version information.", + "value pos. 0 Define a long value here.", + "", + "post-options" + }; - var lines = sut.ToString().ToNotEmptyLines().TrimStringArray(); - lines[0].Should().BeEquivalentTo("pre-options"); - lines[1].Should().BeEquivalentTo("--stringvalue Define a string value here."); - lines[2].Should().BeEquivalentTo("-s, --shortandlong Example with both short and long name."); - lines[3].Should().BeEquivalentTo("-i Define a int sequence here."); - lines[4].Should().BeEquivalentTo("-x Define a boolean or switch value here."); - lines[5].Should().BeEquivalentTo("--help Display this help screen."); - lines[6].Should().BeEquivalentTo("--version Display version information."); - lines[7].Should().BeEquivalentTo("value pos. 0 Define a long value here."); - lines[8].Should().BeEquivalentTo("post-options"); - // Teardown + var lines = sut.ToString().ToLines().TrimStringArray(); + lines.Should().StartWith(expected); } [Fact] @@ -514,7 +523,7 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte ); // Verify outcome - var expected = new[] + var expected = new List() { HeadingInfo.Default.ToString(), CopyrightInfo.Default.ToString(), @@ -552,18 +561,14 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte "", "value pos. 0 Value." }; - var takeCount = newlineBetweenSections ? expected.Length + 1 : expected.Length; - - var text = helpText.ToString(); - var lines = text.ToLines().TrimStringArray().Take(takeCount).ToArray(); - - lines.Should().ContainInOrder(expected); if (newlineBetweenSections) - lines[5].Should().BeEmpty(); + expected.Insert(5, ""); - - // Teardown + var text = helpText.ToString(); + var lines = text.ToLines().TrimStringArray(); + + lines.Should().StartWith(expected); } [Theory] @@ -593,7 +598,7 @@ public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWit ); // Verify outcome - var expected = new[] + var expected = new List() { HeadingInfo.Default.ToString(), CopyrightInfo.Default.ToString(), @@ -607,15 +612,13 @@ public void AutoBuild_with_errors_and_preoptions_renders_correctly(bool startWit "--input-file" }; - var takeCount = newlineBetweenSections || startWithNewline ? expected.Length + 1 : expected.Length; + if (newlineBetweenSections || startWithNewline) + expected.Insert(2, ""); var text = helpText.ToString(); - var lines = text.ToLines().TrimStringArray().Take(takeCount).ToArray(); - - lines.Should().ContainInOrder(expected); + var lines = text.ToLines().TrimStringArray(); - if (newlineBetweenSections || startWithNewline) - lines[2].Should().BeEmpty(); + lines.Should().StartWith(expected); } [Fact] From f2be394be6b35e7b6255c09529c22e8ea7672e52 Mon Sep 17 00:00:00 2001 From: Aaron Sherber Date: Tue, 27 Aug 2019 21:12:43 -0400 Subject: [PATCH 5/5] Add XML docs --- .../Infrastructure/StringBuilderExtensions.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/CommandLine/Infrastructure/StringBuilderExtensions.cs b/src/CommandLine/Infrastructure/StringBuilderExtensions.cs index 0e73cadd..ae4ccbc4 100644 --- a/src/CommandLine/Infrastructure/StringBuilderExtensions.cs +++ b/src/CommandLine/Infrastructure/StringBuilderExtensions.cs @@ -114,6 +114,14 @@ public static int TrailingSpaces(this StringBuilder builder) return c; } + /// + /// Indicates whether the string value of a + /// starts with the input parameter. Returns false if either + /// the StringBuilder or input string is null or empty. + /// + /// The to test. + /// The to look for. + /// public static bool SafeStartsWith(this StringBuilder builder, string s) { if (string.IsNullOrEmpty(s)) @@ -123,6 +131,14 @@ public static bool SafeStartsWith(this StringBuilder builder, string s) && builder.ToString(0, s.Length) == s; } + /// + /// Indicates whether the string value of a + /// ends with the input parameter. Returns false if either + /// the StringBuilder or input string is null or empty. + /// + /// The to test. + /// The to look for. + /// public static bool SafeEndsWith(this StringBuilder builder, string s) { if (string.IsNullOrEmpty(s))