From 18e5e7850d9dfd8b33b7116f11fa252434340fa5 Mon Sep 17 00:00:00 2001 From: Neil MacMullen Date: Thu, 6 Jun 2019 18:19:59 +0100 Subject: [PATCH 1/9] Improve support for multiline help text --- src/CommandLine/Text/HelpText.cs | 205 +++++++++++------- ...WithLineBreaksAndSubIndentation_Options.cs | 13 ++ .../Fakes/HelpTextWithLineBreaks_Options.cs | 23 ++ .../Unit/Text/HelpTextTests.cs | 85 +++++++- 4 files changed, 240 insertions(+), 86 deletions(-) create mode 100644 tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs create mode 100644 tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index cd11a475..5a1b6953 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -21,6 +21,18 @@ public class HelpText { private const int BuilderCapacity = 128; private const int DefaultMaximumLength = 80; // default console width + /// + /// The number of spaces between an option and its associated help text + /// + private const int OptionToHelpTextSeparatorWidth = 4; + /// + /// The width of the option prefix (either "--" or " " + /// + private const int OptionPrefixWidth = 2; + /// + /// The total amount of extra space that needs to accounted for when indenting Option help text + /// + private const int TotalOptionPadding = OptionToHelpTextSeparatorWidth + OptionPrefixWidth; private readonly StringBuilder preOptionsHelp; private readonly StringBuilder postOptionsHelp; private readonly SentenceBuilder sentenceBuilder; @@ -608,7 +620,7 @@ public static IEnumerable RenderUsageTextAsLines(ParserResult pars var styles = example.GetFormatStylesOrDefault(); foreach (var s in styles) { - var commandLine = new StringBuilder(2.Spaces()) + var commandLine = new StringBuilder(OptionPrefixWidth.Spaces()) .Append(appAlias) .Append(' ') .Append(Parser.Default.FormatCommandLine(example.Sample, @@ -645,7 +657,7 @@ public override string ToString() .ToString(); } - internal static void AddLine(StringBuilder builder, string value, int maximumLength) + internal static void AddLine(StringBuilder builder, string value, int maximumLength) { if (builder == null) { @@ -665,37 +677,7 @@ internal static void AddLine(StringBuilder builder, string value, int maximumLen value = value.TrimEnd(); builder.AppendWhen(builder.Length > 0, Environment.NewLine); - do - { - var wordBuffer = 0; - var words = value.Split(' '); - for (var i = 0; i < words.Length; i++) - { - if (words[i].Length < (maximumLength - wordBuffer)) - { - builder.Append(words[i]); - wordBuffer += words[i].Length; - if ((maximumLength - wordBuffer) > 1 && i != words.Length - 1) - { - builder.Append(" "); - wordBuffer++; - } - } - else if (words[i].Length >= maximumLength && wordBuffer == 0) - { - builder.Append(words[i].Substring(0, maximumLength)); - wordBuffer = maximumLength; - break; - } - else - break; - } - value = value.Substring(Math.Min(wordBuffer, value.Length)); - builder.AppendWhen(value.Length > 0, Environment.NewLine); - } - while (value.Length > maximumLength); - - builder.Append(value); + builder.Append(WrapAndIndentText(value, 0, maximumLength)); } private IEnumerable GetSpecificationsFromType(Type type) @@ -748,7 +730,7 @@ private IEnumerable AdaptVerbsToSpecifications(IEnumerable return optionSpecs; } - private HelpText AddOptionsImpl( + private HelpText AddOptionsImpl( IEnumerable specifications, string requiredWord, int maximumLength) @@ -757,7 +739,7 @@ private HelpText AddOptionsImpl( optionsHelp = new StringBuilder(BuilderCapacity); - var remainingSpace = maximumLength - (maxLength + 6); + var remainingSpace = maximumLength - (maxLength + TotalOptionPadding); specifications.ForEach( option => @@ -809,7 +791,7 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe optionsHelp .Append(name.Length < maxLength ? name.ToString().PadRight(maxLength) : name.ToString()) - .Append(" "); + .Append(OptionToHelpTextSeparatorWidth.Spaces()); var optionHelpText = specification.HelpText; @@ -821,44 +803,13 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe if (specification.Required) optionHelpText = "{0} ".FormatInvariant(requiredWord) + optionHelpText; - - if (!string.IsNullOrEmpty(optionHelpText)) - { - do - { - var wordBuffer = 0; - var words = optionHelpText.Split(' '); - for (var i = 0; i < words.Length; i++) - { - if (words[i].Length < (widthOfHelpText - wordBuffer)) - { - optionsHelp.Append(words[i]); - wordBuffer += words[i].Length; - if ((widthOfHelpText - wordBuffer) > 1 && i != words.Length - 1) - { - optionsHelp.Append(" "); - wordBuffer++; - } - } - else if (words[i].Length >= widthOfHelpText && wordBuffer == 0) - { - optionsHelp.Append(words[i].Substring(0, widthOfHelpText)); - wordBuffer = widthOfHelpText; - break; - } - else - break; - } - - optionHelpText = optionHelpText.Substring(Math.Min(wordBuffer, optionHelpText.Length)).Trim(); - optionsHelp.AppendWhen(optionHelpText.Length > 0, Environment.NewLine, - new string(' ', maxLength + 6)); - } - while (optionHelpText.Length > widthOfHelpText); - } - + + //note that we need to indent trim the start of the string because it's going to be + //appended to an existing line that is as long as the indent-level + var indented = WrapAndIndentText(optionHelpText, maxLength+TotalOptionPadding, widthOfHelpText).TrimStart(); + optionsHelp - .Append(optionHelpText) + .Append(indented) .Append(Environment.NewLine) .AppendWhen(additionalNewLineAfterOption, Environment.NewLine); @@ -944,13 +895,13 @@ private int GetMaxOptionLength(OptionSpecification spec) { specLength += spec.LongName.Length; if (AddDashesToOption) - specLength += 2; + specLength += OptionPrefixWidth; specLength += metaLength; } if (hasShort && hasLong) - specLength += 2; // ", " + specLength += OptionPrefixWidth; return specLength; } @@ -997,5 +948,107 @@ private static string FormatDefaultValue(T value) ? builder.ToString(0, builder.Length - 1) : string.Empty; } + + /// + /// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation + /// + /// The string to wrap + /// The amount of padding at the start of each string + /// The number of characters we can use for text + /// + /// The use of "width" is slightly confusing in other methods. In this method, the columnWidth + /// parameter is the number of characters we can use for text regardless of the indent level. + /// For example, if columnWidth is 10 and indentLevel is 2, the input + /// "a string for wrapping 01234567890123" + /// would return + /// " a string" + newline + + /// " for" + newline + + /// " wrapping" + newline + + /// " 0123456789" + newline + + /// " 0123" + /// + /// A string that has been word-wrapped with padding on each line to indent it + private static string WrapAndIndentText(string input,int indentLevel,int columnWidth) + { + //start by splitting at newlines and then reinserting the newline as a separate word + var lines = input.Split(new[] {Environment.NewLine}, StringSplitOptions.None); + var lineCount = lines.Length; + + var tokens = lines + .Zip(new string[lineCount], (a, _) => new string[] {a, Environment.NewLine}) + .SelectMany(linePair=>linePair) + .Take(lineCount * 2 - 1); + + //split into words + var words= tokens + .SelectMany(l=>l.Split(' ')); + + //create a list of individual indented lines + var wrappedLines = words + .Aggregate>( + new List(), + (lineList,word)=>AddWordToLastLineOrCreateNewLineIfNecessary(lineList,word,columnWidth) + ) + .Select(builder => indentLevel.Spaces() + builder.ToString().TrimEnd()); + + //return the whole thing as a single string + return string.Join(Environment.NewLine,wrappedLines); + } + + /// + /// When presented with a word, either append to the last line in the list or start a new line + /// + /// A list of stringbuilders containing results so far + /// The individual word to append + /// The usable text space + /// + /// The 'word' can actually be an empty string or a linefeed. It's important to keep these - + /// empty strings allow us to preserve indentation and extra spaces within a line and linefeeds + /// allow us to honour the users formatting wishes when the pass in multi-line helptext. + /// + /// The same list as is passed in + private static List AddWordToLastLineOrCreateNewLineIfNecessary(List lines, string word,int columnWidth) + { + if (word == Environment.NewLine) + { + //A newline token just means advance to the next line. + lines.Add(new StringBuilder()); + return lines; + } + //The current indentLevel is based on the previous line. + var previousLine = lines.LastOrDefault()?.ToString() ??string.Empty; + var currentIndentLevel = previousLine.Length - previousLine.TrimStart().Length; + + var wouldWrap = !lines.Any() || previousLine.Length + word.Length > columnWidth; + + if (!wouldWrap) + { + //The usual case is we just append the 'word' and a space to the current line + //Note that trailing spaces will get removed later when we turn the line list + //into a single string + lines.Last().Append(word + ' '); + } + else + { + //The 'while' here is to take account of the possibility of someone providing a word + //which just can't fit in the current column. In that case we just split it at the + //column end. + //That's a rare case though - most of the time we'll succeed in a single pass without + //having to split + while (word.Length >0) + { + var availableCharacters = Math.Min(columnWidth - currentIndentLevel,word.Length); + + var segmentToAdd = currentIndentLevel.Spaces() + + word.Substring(0, availableCharacters) + ' '; + + lines.Add(new StringBuilder(segmentToAdd)); + word = word.Substring(availableCharacters); + } + } + return lines; + } + + } -} \ No newline at end of file +} diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs new file mode 100644 index 00000000..afa77f3a --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaksAndSubIndentation_Options.cs @@ -0,0 +1,13 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithLineBreaksAndSubIndentation_Options + { + + [Option(HelpText = @"This is a help text description where we want: + * The left pad after a linebreak to be honoured and the indentation to be preserved across to the next line + * The ability to return to no indent. +Like this.")] + public string StringValue { get; set; } + + } +} \ No newline at end of file diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs new file mode 100644 index 00000000..c93e73aa --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithLineBreaks_Options.cs @@ -0,0 +1,23 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithLineBreaks_Options + { + [Option(HelpText = + @"This is a help text description. +It has multiple lines. +We also want to ensure that indentation is correct.")] + public string StringValue { get; set; } + + + [Option(HelpText = @"This is a help text description where we want + The left pad after a linebreak to be honoured so that + we can sub-indent within a description.")] + public string StringValu2 { get; set; } + + + [Option(HelpText = @"This is a help text description where we want + The left pad after a linebreak to be honoured and the indentation to be preserved across to the next line in a way that looks pleasing")] + public string StringValu3 { get; set; } + + } +} diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index 7c4d7590..8412e66e 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -77,6 +77,8 @@ public void Create_instance_with_options() // Teardown } + + //[Fact] public void Create_instance_with_enum_options_enabled() { @@ -154,10 +156,10 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); lines[2].Should().BeEquivalentTo(" v, verbose This is the description"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; - lines[3].Should().BeEquivalentTo(" of the verbosity to "); - lines[4].Should().BeEquivalentTo(" test out the wrapping "); - lines[5].Should().BeEquivalentTo(" capabilities of the "); - lines[6].Should().BeEquivalentTo(" Help Text."); + lines[3].Should().BeEquivalentTo(" of the verbosity to test"); + lines[4].Should().BeEquivalentTo(" out the wrapping"); + lines[5].Should().BeEquivalentTo(" capabilities of the Help"); + lines[6].Should().BeEquivalentTo(" Text."); // Teardown } @@ -176,7 +178,7 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c // Verify outcome var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); - lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the wrapping capabilities of "); //"The first line should have the arguments and the start of the Help Text."); + lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the wrapping capabilities of"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; lines[3].Should().BeEquivalentTo(" the Help Text."); // Teardown @@ -216,10 +218,10 @@ public void Long_help_text_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].Should().BeEquivalentTo(" v, verbose Before "); + lines[1].Should().BeEquivalentTo(" v, verbose Before"); lines[2].Should().BeEquivalentTo(" 012345678901234567890123"); lines[3].Should().BeEquivalentTo(" After"); - lines[4].Should().BeEquivalentTo(" input-file Before "); + lines[4].Should().BeEquivalentTo(" input-file Before"); lines[5].Should().BeEquivalentTo(" 012345678901234567890123"); lines[6].Should().BeEquivalentTo(" 456789 After"); // Teardown @@ -238,12 +240,12 @@ public void Long_pre_and_post_lines_without_spaces() // Verify outcome var lines = sut.ToString().ToNotEmptyLines(); - lines[1].Should().BeEquivalentTo("Before "); + lines[1].Should().BeEquivalentTo("Before"); lines[2].Should().BeEquivalentTo("0123456789012345678901234567890123456789"); lines[3].Should().BeEquivalentTo("012 After"); - lines[lines.Length - 3].Should().BeEquivalentTo("Before "); + lines[lines.Length - 3].Should().BeEquivalentTo("Before"); lines[lines.Length - 2].Should().BeEquivalentTo("0123456789012345678901234567890123456789"); - lines[lines.Length - 1].Should().BeEquivalentTo(" After"); + lines[lines.Length - 1].Should().BeEquivalentTo("After"); // Teardown } @@ -653,5 +655,68 @@ public void Add_line_with_two_empty_spaces_at_the_end() Assert.Equal("T" + Environment.NewLine + "e" + Environment.NewLine + "s" + Environment.NewLine + "t", b.ToString()); } + + [Fact] + public void HelpTextHonoursLineBreaks() + { + // Fixture setup + // Exercize system + var sut = new HelpText {AddDashesToOption = true} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description."); + lines[1].Should().BeEquivalentTo(" It has multiple lines."); + lines[2].Should().BeEquivalentTo(" We also want to ensure that indentation is correct."); + + // Teardown + } + + [Fact] + public void HelpTextHonoursIndentationAfterLineBreaks() + { + // Fixture setup + // Exercize system + var sut = new HelpText {AddDashesToOption = true} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[3].Should().BeEquivalentTo(" --stringvalu2 This is a help text description where we want"); + lines[4].Should().BeEquivalentTo(" the left pad after a linebreak to be honoured so that"); + lines[5].Should().BeEquivalentTo(" we can sub-indent within a description."); + + // Teardown + } + + [Fact] + public void HelpTextPreservesIndentationAcrossWordWrap() + { + // Fixture setup + // Exercise system + var sut = new HelpText {AddDashesToOption = true,MaximumDisplayWidth = 60} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaksAndSubIndentation_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description where we"); + lines[1].Should().BeEquivalentTo(" want:"); + lines[2].Should().BeEquivalentTo(" * The left pad after a linebreak to"); + lines[3].Should().BeEquivalentTo(" be honoured and the indentation to be"); + lines[4].Should().BeEquivalentTo(" preserved across to the next line"); + lines[5].Should().BeEquivalentTo(" * The ability to return to no indent."); + lines[6].Should().BeEquivalentTo(" Like this."); + + // Teardown + } + + } } From f5aebb0b9fdc812cd189694325f1bed635eba7a6 Mon Sep 17 00:00:00 2001 From: Neil MacMullen Date: Thu, 6 Jun 2019 19:34:48 +0100 Subject: [PATCH 2/9] Better portability for line-break handling --- src/CommandLine/Text/HelpText.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index 5a1b6953..dc5a1c0d 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -971,7 +971,12 @@ private static string FormatDefaultValue(T value) private static string WrapAndIndentText(string input,int indentLevel,int columnWidth) { //start by splitting at newlines and then reinserting the newline as a separate word - var lines = input.Split(new[] {Environment.NewLine}, StringSplitOptions.None); + //Note that on the input side, we can't assume the line-break style at run time so we have to + //be able to handle both. We cant use Environment.NewLine because that changes at + //_runtime_ and may not match the line-break style that was compiled in + var lines = input + .Replace("\r","") + .Split(new[] {'\n'}, StringSplitOptions.None); var lineCount = lines.Length; var tokens = lines From e30e5a6afbfc8cb36d59c3467132531d50151353 Mon Sep 17 00:00:00 2001 From: Neil MacMullen Date: Thu, 6 Jun 2019 19:44:36 +0100 Subject: [PATCH 3/9] Remove a couple of spurious indentation changes --- src/CommandLine/Text/HelpText.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index dc5a1c0d..13129b37 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -657,7 +657,7 @@ public override string ToString() .ToString(); } - internal static void AddLine(StringBuilder builder, string value, int maximumLength) + internal static void AddLine(StringBuilder builder, string value, int maximumLength) { if (builder == null) { @@ -730,7 +730,7 @@ private IEnumerable AdaptVerbsToSpecifications(IEnumerable return optionSpecs; } - private HelpText AddOptionsImpl( + private HelpText AddOptionsImpl( IEnumerable specifications, string requiredWord, int maximumLength) From 0a0fa208a008cf5707582ba6a2ab665497fa1532 Mon Sep 17 00:00:00 2001 From: Neil MacMullen Date: Thu, 6 Jun 2019 19:59:31 +0100 Subject: [PATCH 4/9] Add extra test to ensure mixed line-end styles are handled correctly --- .../HelpTextWithMixedLineBreaks_Options.cs | 9 +++++++++ .../Unit/Text/HelpTextTests.cs | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs diff --git a/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs b/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs new file mode 100644 index 00000000..9950dbc7 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/HelpTextWithMixedLineBreaks_Options.cs @@ -0,0 +1,9 @@ +namespace CommandLine.Tests.Fakes +{ + public class HelpTextWithMixedLineBreaks_Options + { + [Option(HelpText = + "This is a help text description\n It has multiple lines.\r\n Third line")] + public string StringValue { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index 8412e66e..f384e4dd 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -718,5 +718,24 @@ public void HelpTextPreservesIndentationAcrossWordWrap() } + [Fact] + public void HelpTextIsConsitentRegardlessOfCompileTimeLineStyle() + { + // Fixture setup + // Exercize system + var sut = new HelpText {AddDashesToOption = true} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithMixedLineBreaks_Options)), + Enumerable.Empty())); + + // Verify outcome + + var lines = sut.ToString().ToNotEmptyLines(); + lines[0].Should().BeEquivalentTo(" --stringvalue This is a help text description"); + lines[1].Should().BeEquivalentTo(" It has multiple lines."); + lines[2].Should().BeEquivalentTo(" Third line"); + + // Teardown + } + } } From ac9e31c5aa9840d3647a8f4f0d8c17b370cda14e Mon Sep 17 00:00:00 2001 From: Neil MacMullen Date: Sun, 9 Jun 2019 14:30:58 +0100 Subject: [PATCH 5/9] Move TextWrapper code in separate class to aid testability --- src/CommandLine/Text/HelpText.cs | 112 +----------- src/CommandLine/Text/TextWrapper.cs | 173 ++++++++++++++++++ .../Unit/Core/TextWrapperTests.cs | 170 +++++++++++++++++ 3 files changed, 348 insertions(+), 107 deletions(-) create mode 100644 src/CommandLine/Text/TextWrapper.cs create mode 100644 tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index 13129b37..575c6fbc 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -677,7 +677,7 @@ internal static void AddLine(StringBuilder builder, string value, int maximumLen value = value.TrimEnd(); builder.AppendWhen(builder.Length > 0, Environment.NewLine); - builder.Append(WrapAndIndentText(value, 0, maximumLength)); + builder.Append(TextWrapper.WrapAndIndentText(value, 0, maximumLength)); } private IEnumerable GetSpecificationsFromType(Type type) @@ -806,7 +806,7 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe //note that we need to indent trim the start of the string because it's going to be //appended to an existing line that is as long as the indent-level - var indented = WrapAndIndentText(optionHelpText, maxLength+TotalOptionPadding, widthOfHelpText).TrimStart(); + var indented = TextWrapper.WrapAndIndentText(optionHelpText, maxLength+TotalOptionPadding, widthOfHelpText).TrimStart(); optionsHelp .Append(indented) @@ -949,111 +949,9 @@ private static string FormatDefaultValue(T value) : string.Empty; } - /// - /// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation - /// - /// The string to wrap - /// The amount of padding at the start of each string - /// The number of characters we can use for text - /// - /// The use of "width" is slightly confusing in other methods. In this method, the columnWidth - /// parameter is the number of characters we can use for text regardless of the indent level. - /// For example, if columnWidth is 10 and indentLevel is 2, the input - /// "a string for wrapping 01234567890123" - /// would return - /// " a string" + newline + - /// " for" + newline + - /// " wrapping" + newline + - /// " 0123456789" + newline + - /// " 0123" - /// - /// A string that has been word-wrapped with padding on each line to indent it - private static string WrapAndIndentText(string input,int indentLevel,int columnWidth) - { - //start by splitting at newlines and then reinserting the newline as a separate word - //Note that on the input side, we can't assume the line-break style at run time so we have to - //be able to handle both. We cant use Environment.NewLine because that changes at - //_runtime_ and may not match the line-break style that was compiled in - var lines = input - .Replace("\r","") - .Split(new[] {'\n'}, StringSplitOptions.None); - var lineCount = lines.Length; - - var tokens = lines - .Zip(new string[lineCount], (a, _) => new string[] {a, Environment.NewLine}) - .SelectMany(linePair=>linePair) - .Take(lineCount * 2 - 1); - - //split into words - var words= tokens - .SelectMany(l=>l.Split(' ')); - - //create a list of individual indented lines - var wrappedLines = words - .Aggregate>( - new List(), - (lineList,word)=>AddWordToLastLineOrCreateNewLineIfNecessary(lineList,word,columnWidth) - ) - .Select(builder => indentLevel.Spaces() + builder.ToString().TrimEnd()); - - //return the whole thing as a single string - return string.Join(Environment.NewLine,wrappedLines); - } - - /// - /// When presented with a word, either append to the last line in the list or start a new line - /// - /// A list of stringbuilders containing results so far - /// The individual word to append - /// The usable text space - /// - /// The 'word' can actually be an empty string or a linefeed. It's important to keep these - - /// empty strings allow us to preserve indentation and extra spaces within a line and linefeeds - /// allow us to honour the users formatting wishes when the pass in multi-line helptext. - /// - /// The same list as is passed in - private static List AddWordToLastLineOrCreateNewLineIfNecessary(List lines, string word,int columnWidth) - { - if (word == Environment.NewLine) - { - //A newline token just means advance to the next line. - lines.Add(new StringBuilder()); - return lines; - } - //The current indentLevel is based on the previous line. - var previousLine = lines.LastOrDefault()?.ToString() ??string.Empty; - var currentIndentLevel = previousLine.Length - previousLine.TrimStart().Length; - - var wouldWrap = !lines.Any() || previousLine.Length + word.Length > columnWidth; - - if (!wouldWrap) - { - //The usual case is we just append the 'word' and a space to the current line - //Note that trailing spaces will get removed later when we turn the line list - //into a single string - lines.Last().Append(word + ' '); - } - else - { - //The 'while' here is to take account of the possibility of someone providing a word - //which just can't fit in the current column. In that case we just split it at the - //column end. - //That's a rare case though - most of the time we'll succeed in a single pass without - //having to split - while (word.Length >0) - { - var availableCharacters = Math.Min(columnWidth - currentIndentLevel,word.Length); - - var segmentToAdd = currentIndentLevel.Spaces() + - word.Substring(0, availableCharacters) + ' '; - - lines.Add(new StringBuilder(segmentToAdd)); - word = word.Substring(availableCharacters); - } - } - return lines; - } - + } } + + diff --git a/src/CommandLine/Text/TextWrapper.cs b/src/CommandLine/Text/TextWrapper.cs new file mode 100644 index 00000000..15c22d76 --- /dev/null +++ b/src/CommandLine/Text/TextWrapper.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CommandLine.Infrastructure; + +namespace CommandLine.Text +{ + /// + /// A utility class to word-wrap and indent blocks of text + /// + public class TextWrapper + { + private string[] lines; + public TextWrapper(string input) + { + //start by splitting at newlines and then reinserting the newline as a separate word + //Note that on the input side, we can't assume the line-break style at run time so we have to + //be able to handle both. We can't use Environment.NewLine because that changes at + //_runtime_ and may not match the line-break style that was compiled in + lines = input + .Replace("\r","") + .Split(new[] {'\n'}, StringSplitOptions.None); + } + + /// + /// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation + /// + /// The number of characters we can use for text + /// + /// This method attempts to wrap text without breaking words + /// For example, if columnWidth is 10 , the input + /// "a string for wrapping 01234567890123" + /// would return + /// "a string + /// "for + /// "wrapping + /// "0123456789 + /// "0123" + /// + /// this + public TextWrapper WordWrap(int columnWidth) + { + + lines= lines + .SelectMany(line => WordWrapLine(line, columnWidth)) + .ToArray(); + return this; + } + + /// + /// Indent all lines in the TextWrapper by the desired number of spaces + /// + /// The number of spaces to indent by + /// this + public TextWrapper Indent(int numberOfSpaces) + { + lines = lines + .Select(line => numberOfSpaces.Spaces() + line) + .ToArray(); + return this; + } + + /// + /// Returns the current state of the TextWrapper as a string + /// + /// + public string ToText() + { + //return the whole thing as a single string + return string.Join(Environment.NewLine,lines); + } + + /// + /// Convenience method to wraps and indent a string in a single operation + /// + /// The string to operate on + /// The number of spaces to indent by + /// The width of the column used for wrapping + /// + /// The string is wrapped _then_ indented so the columnWidth is the width of the + /// usable text block, and does NOT include the indentLevel. + /// + /// the processed string + public static string WrapAndIndentText(string input, int indentLevel,int columnWidth) + { + return new TextWrapper(input) + .WordWrap(columnWidth) + .Indent(indentLevel) + .ToText(); + } + + + private string [] WordWrapLine(string line,int columnWidth) + { + //create a list of individual lines generated from the supplied line + + //When handling sub-indentation we must always reserve at least one column for text! + var unindentedLine = line.TrimStart(); + var currentIndentLevel = Math.Min(line.Length - unindentedLine.Length,columnWidth-1) ; + columnWidth -= currentIndentLevel; + + return unindentedLine.Split(' ') + .Aggregate( + new List(), + (lineList, word) => AddWordToLastLineOrCreateNewLineIfNecessary(lineList, word, columnWidth) + ) + .Select(builder => currentIndentLevel.Spaces()+builder.ToString().TrimEnd()) + .ToArray(); + } + + /// + /// When presented with a word, either append to the last line in the list or start a new line + /// + /// A list of StringBuilders containing results so far + /// The individual word to append + /// The usable text space + /// + /// The 'word' can actually be an empty string. It's important to keep these - + /// empty strings allow us to preserve indentation and extra spaces within a line. + /// + /// The same list as is passed in + private static List AddWordToLastLineOrCreateNewLineIfNecessary(List lines, string word,int columnWidth) + { + //The current indentation level is based on the previous line but we need to be careful + var previousLine = lines.LastOrDefault()?.ToString() ??string.Empty; + + var wouldWrap = !lines.Any() || (word.Length>0 && previousLine.Length + word.Length > columnWidth); + + if (!wouldWrap) + { + //The usual case is we just append the 'word' and a space to the current line + //Note that trailing spaces will get removed later when we turn the line list + //into a single string + lines.Last().Append(word + ' '); + } + else + { + //The 'while' here is to take account of the possibility of someone providing a word + //which just can't fit in the current column. In that case we just split it at the + //column end. + //That's a rare case though - most of the time we'll succeed in a single pass without + //having to split + //Note that we always do at least one pass even if the 'word' is empty in order to + //honour sub-indentation and extra spaces within strings + do + { + var availableCharacters = Math.Min(columnWidth, word.Length); + var segmentToAdd = LeftString(word,availableCharacters) + ' '; + lines.Add(new StringBuilder(segmentToAdd)); + word = RightString(word,availableCharacters); + } while (word.Length > 0); + } + return lines; + } + + + private static string RightString(string str,int n) + { + return (n >= str.Length || str.Length==0) + ? string.Empty + : str.Substring(n); + } + + private static string LeftString(string str,int n) + { + + return (n >= str.Length || str.Length==0) + ? str + : str.Substring(0,n); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs new file mode 100644 index 00000000..eaa52d1d --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs @@ -0,0 +1,170 @@ +using CommandLine.Tests.Fakes; +using CommandLine.Text; +using FluentAssertions; +using Xunit; + +namespace CommandLine.Tests.Unit.Core +{ + public class TextWrapperTests + { + [Fact] + public void IndentWorksCorrectly() + { + + var input = + @"line1 +line2"; + var expected = @" line1 + line2"; + var wrapper = new TextWrapper(input); + wrapper.Indent(2).ToText().Should().Be(expected); + + } + + [Fact] + public void SimpleWrappingIsAsExpected() + { + + var input = + @"here is some text that needs wrapping"; + var expected = @"here is +some text +that needs +wrapping"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(10).ToText().Should().Be(expected); + + } + + [Fact] + public void WrappingAvoidsBreakingWords() + { + + var input = + @"here hippopotamus is some text that needs wrapping"; + var expected = @"here +hippopotamus is +some text that +needs wrapping"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(15).ToText().Should().Be(expected); + + } + + [Fact] + public void WrappingObeysLineBreaksOfAllStyles() + { + + var input = + "here is some text\nthat needs\r\nwrapping"; + var expected = @"here is some text +that needs +wrapping"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(20).ToText().Should().Be(expected); + + } + + + [Fact] + public void WrappingPreservesSubIndentation() + { + + var input = + "here is some text\n that needs wrapping where we want the wrapped part to preserve indentation\nand this part to not be indented"; + var expected = @"here is some text + that needs + wrapping where we + want the wrapped + part to preserve + indentation +and this part to not +be indented"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(20).ToText().Should().Be(expected); + + } + + [Fact] + public void LongWordsAreBroken() + { + + var input = + "here is some text that contains a veryLongWordThatWontFitOnASingleLine"; + var expected = @"here is some text +that contains a +veryLongWordThatWont +FitOnASingleLine"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(20).ToText().Should().Be(expected); + + } + + [Fact] + public void SubIndentationIsPreservedWhenBreakingWords() + { + + var input = + "here is some text that contains \n a veryLongWordThatWontFitOnASingleLine"; + var expected = @"here is some text +that contains + a + veryLongWordThatWo + ntFitOnASingleLine"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(20).ToText().Should().Be(expected); + + } + + [Fact] + public void SpacesWithinStringAreRespected() + { + + var input = + "here is some text with some extra spacing"; + var expected = @"here is some +text with some extra +spacing"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(20).ToText().Should().Be(expected); + + } + + + [Fact] + public void ExtraSpacesAreTreatedAsNonBreaking() + { + + var input = + "here is some text with some extra spacing"; + var expected = @"here is some text +with some extra +spacing"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(20).ToText().Should().Be(expected); + + } + + + [Fact] + public void WrappingExtraSpacesObeySubIndent() + { + + var input = + "here is some\n text with some extra spacing"; + var expected = @"here is some + text + with some extra + spacing"; + var wrapper = new TextWrapper(input); + wrapper.WordWrap(20).ToText().Should().Be(expected); + + } + + + + } + + + +} From c78b312dff496fe6324449229f595450627299e5 Mon Sep 17 00:00:00 2001 From: Neil MacMullen Date: Sun, 9 Jun 2019 14:37:53 +0100 Subject: [PATCH 6/9] Add some comments to kick AppVeyor again --- src/CommandLine/Text/TextWrapper.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CommandLine/Text/TextWrapper.cs b/src/CommandLine/Text/TextWrapper.cs index 15c22d76..5af21576 100644 --- a/src/CommandLine/Text/TextWrapper.cs +++ b/src/CommandLine/Text/TextWrapper.cs @@ -155,13 +155,18 @@ private static List AddWordToLastLineOrCreateNewLineIfNecessary(L } + /// + /// Return the right part of a string in a way that compensates for Substring's deficiencies + /// private static string RightString(string str,int n) { return (n >= str.Length || str.Length==0) ? string.Empty : str.Substring(n); } - + /// + /// Return the left part of a string in a way that compensates for Substring's deficiencies + /// private static string LeftString(string str,int n) { From 6224f26e07e76613102016cc7ac8e367e9026d20 Mon Sep 17 00:00:00 2001 From: Neil MacMullen Date: Sun, 9 Jun 2019 14:45:43 +0100 Subject: [PATCH 7/9] Workaround line-end issue on build server --- .../Unit/Core/TextWrapperTests.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs index eaa52d1d..bcba9095 100644 --- a/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs @@ -7,6 +7,17 @@ namespace CommandLine.Tests.Unit.Core { public class TextWrapperTests { + private string NormalizeLineBreaks(string str) + { + return str.Replace("\r", ""); + } + private void EnsureEquivalent(string a,string b) + { + //workaround build system line-end inconsistencies + NormalizeLineBreaks(a).Should().Be(NormalizeLineBreaks(b)); + } + + [Fact] public void IndentWorksCorrectly() { @@ -17,7 +28,7 @@ public void IndentWorksCorrectly() var expected = @" line1 line2"; var wrapper = new TextWrapper(input); - wrapper.Indent(2).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.Indent(2).ToText(),expected); } @@ -32,7 +43,7 @@ some text that needs wrapping"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(10).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(10).ToText(),expected); } @@ -47,7 +58,7 @@ hippopotamus is some text that needs wrapping"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(15).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(15).ToText(),expected); } @@ -61,7 +72,7 @@ public void WrappingObeysLineBreaksOfAllStyles() that needs wrapping"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(20).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); } @@ -81,7 +92,7 @@ part to preserve and this part to not be indented"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(20).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); } @@ -96,7 +107,7 @@ that contains a veryLongWordThatWont FitOnASingleLine"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(20).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); } @@ -112,7 +123,7 @@ that contains veryLongWordThatWo ntFitOnASingleLine"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(20).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); } @@ -126,7 +137,7 @@ public void SpacesWithinStringAreRespected() text with some extra spacing"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(20).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); } @@ -141,7 +152,7 @@ public void ExtraSpacesAreTreatedAsNonBreaking() with some extra spacing"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(20).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); } @@ -157,7 +168,7 @@ public void WrappingExtraSpacesObeySubIndent() with some extra spacing"; var wrapper = new TextWrapper(input); - wrapper.WordWrap(20).ToText().Should().Be(expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); } From 5573afbe55af77f7876cb96a40b8d4234732f841 Mon Sep 17 00:00:00 2001 From: neil macmullen Date: Fri, 28 Jun 2019 09:10:32 +0100 Subject: [PATCH 8/9] Fix bug where TextWrapper didn't cope gracefully with negative columnWidth. Also fix up old broken tests --- src/CommandLine/Text/TextWrapper.cs | 3 +- .../Unit/Core/TextWrapperTests.cs | 188 ++++++++++-------- .../Unit/Text/HelpTextTests.cs | 83 ++++---- 3 files changed, 149 insertions(+), 125 deletions(-) diff --git a/src/CommandLine/Text/TextWrapper.cs b/src/CommandLine/Text/TextWrapper.cs index 5af21576..19a93f15 100644 --- a/src/CommandLine/Text/TextWrapper.cs +++ b/src/CommandLine/Text/TextWrapper.cs @@ -41,7 +41,8 @@ public TextWrapper(string input) /// this public TextWrapper WordWrap(int columnWidth) { - + //ensure we always use at least 1 column even if the client has told us there's no space available + columnWidth = Math.Max(1, columnWidth); lines= lines .SelectMany(line => WordWrapLine(line, columnWidth)) .ToArray(); diff --git a/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs index bcba9095..1db7497e 100644 --- a/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TextWrapperTests.cs @@ -1,4 +1,5 @@ -using CommandLine.Tests.Fakes; +using System; +using System.Linq; using CommandLine.Text; using FluentAssertions; using Xunit; @@ -11,110 +12,120 @@ private string NormalizeLineBreaks(string str) { return str.Replace("\r", ""); } - private void EnsureEquivalent(string a,string b) + + private void EnsureEquivalent(string a, string b) { //workaround build system line-end inconsistencies NormalizeLineBreaks(a).Should().Be(NormalizeLineBreaks(b)); } - + [Fact] - public void IndentWorksCorrectly() + public void ExtraSpacesAreTreatedAsNonBreaking() { + var input = + "here is some text with some extra spacing"; + var expected = @"here is some text +with some extra +spacing"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + + [Fact] + public void IndentWorksCorrectly() + { var input = @"line1 line2"; var expected = @" line1 line2"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.Indent(2).ToText(),expected); - + EnsureEquivalent(wrapper.Indent(2).ToText(), expected); } [Fact] - public void SimpleWrappingIsAsExpected() + public void LongWordsAreBroken() { - var input = - @"here is some text that needs wrapping"; - var expected = @"here is -some text -that needs -wrapping"; + "here is some text that contains a veryLongWordThatWontFitOnASingleLine"; + var expected = @"here is some text +that contains a +veryLongWordThatWont +FitOnASingleLine"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(10).ToText(),expected); - + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); } [Fact] - public void WrappingAvoidsBreakingWords() + public void NegativeColumnWidthStillProducesOutput() { - - var input = - @"here hippopotamus is some text that needs wrapping"; - var expected = @"here -hippopotamus is -some text that -needs wrapping"; + var input = @"test"; + var expected = string.Join(Environment.NewLine, input.Select(c => c.ToString())); var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(15).ToText(),expected); - + EnsureEquivalent(wrapper.WordWrap(-1).ToText(), expected); } [Fact] - public void WrappingObeysLineBreaksOfAllStyles() + public void SimpleWrappingIsAsExpected() { - var input = - "here is some text\nthat needs\r\nwrapping"; - var expected = @"here is some text + @"here is some text that needs wrapping"; + var expected = @"here is +some text that needs wrapping"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); - + EnsureEquivalent(wrapper.WordWrap(10).ToText(), expected); } - [Fact] - public void WrappingPreservesSubIndentation() + public void SingleColumnStillProducesOutputForSubIndentation() { - - var input = - "here is some text\n that needs wrapping where we want the wrapped part to preserve indentation\nand this part to not be indented"; - var expected = @"here is some text - that needs - wrapping where we - want the wrapped - part to preserve - indentation -and this part to not -be indented"; + var input = @"test + ind"; + + var expected = @"t +e +s +t +i +n +d"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); - + EnsureEquivalent(wrapper.WordWrap(-1).ToText(), expected); } [Fact] - public void LongWordsAreBroken() + public void SpacesWithinStringAreRespected() { - var input = - "here is some text that contains a veryLongWordThatWontFitOnASingleLine"; - var expected = @"here is some text -that contains a -veryLongWordThatWont -FitOnASingleLine"; + "here is some text with some extra spacing"; + var expected = @"here is some +text with some extra +spacing"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + [Fact] + public void SubIndentationCorrectlyWrapsWhenColumnWidthRequiresIt() + { + var input = @"test + indented"; + var expected = @"test + in + de + nt + ed"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(6).ToText(), expected); } [Fact] public void SubIndentationIsPreservedWhenBreakingWords() { - var input = "here is some text that contains \n a veryLongWordThatWontFitOnASingleLine"; var expected = @"here is some text @@ -123,44 +134,26 @@ that contains veryLongWordThatWo ntFitOnASingleLine"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); - + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); } [Fact] - public void SpacesWithinStringAreRespected() + public void WrappingAvoidsBreakingWords() { - var input = - "here is some text with some extra spacing"; - var expected = @"here is some -text with some extra -spacing"; + @"here hippopotamus is some text that needs wrapping"; + var expected = @"here +hippopotamus is +some text that +needs wrapping"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); - + EnsureEquivalent(wrapper.WordWrap(15).ToText(), expected); } - - [Fact] - public void ExtraSpacesAreTreatedAsNonBreaking() - { - var input = - "here is some text with some extra spacing"; - var expected = @"here is some text -with some extra -spacing"; - var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); - - } - - [Fact] public void WrappingExtraSpacesObeySubIndent() { - var input = "here is some\n text with some extra spacing"; var expected = @"here is some @@ -168,14 +161,37 @@ public void WrappingExtraSpacesObeySubIndent() with some extra spacing"; var wrapper = new TextWrapper(input); - EnsureEquivalent(wrapper.WordWrap(20).ToText(),expected); - + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); } + [Fact] + public void WrappingObeysLineBreaksOfAllStyles() + { + var input = + "here is some text\nthat needs\r\nwrapping"; + var expected = @"here is some text +that needs +wrapping"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } + [Fact] + public void WrappingPreservesSubIndentation() + { + var input = + "here is some text\n that needs wrapping where we want the wrapped part to preserve indentation\nand this part to not be indented"; + var expected = @"here is some text + that needs + wrapping where we + want the wrapped + part to preserve + indentation +and this part to not +be indented"; + var wrapper = new TextWrapper(input); + EnsureEquivalent(wrapper.WordWrap(20).ToText(), expected); + } } - - - } diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index f384e4dd..d3d51254 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -79,7 +79,7 @@ public void Create_instance_with_options() - //[Fact] + [Fact] public void Create_instance_with_enum_options_enabled() { // Fixture setup @@ -184,7 +184,7 @@ public void When_help_text_is_longer_than_width_it_will_wrap_around_as_if_in_a_c // Teardown } - //[Fact] + [Fact] public void When_help_text_has_hidden_option_it_should_not_be_added_to_help_text_output() { // Fixture setup @@ -198,7 +198,7 @@ public void When_help_text_has_hidden_option_it_should_not_be_added_to_help_text // Verify outcome var lines = sut.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); - lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the "); //"The first line should have the arguments and the start of the Help Text."); + lines[2].Should().BeEquivalentTo(" v, verbose This is the description of the verbosity to test out the"); //"The first line should have the arguments and the start of the Help Text."); //string formattingMessage = "Beyond the second line should be formatted as though it's in a column."; lines[3].Should().BeEquivalentTo(" wrapping capabilities of the Help Text."); // Teardown @@ -309,7 +309,7 @@ public void Invoking_RenderParsingErrorsText_returns_appropriate_formatted_text( // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Options_returns_appropriate_formatted_text() { // Fixture setup @@ -330,9 +330,7 @@ public void Invoke_AutoBuild_for_Options_returns_appropriate_formatted_text() lines[0].Should().StartWithEquivalent("CommandLine"); lines[1].Should().StartWithEquivalent("Copyright (c)"); #else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); + // The first two lines depend on the test-runner so ignore them #endif lines[2].Should().BeEquivalentTo("ERROR(S):"); lines[3].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); @@ -345,7 +343,7 @@ public void Invoke_AutoBuild_for_Options_returns_appropriate_formatted_text() // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_formatted_text() { // Fixture setup @@ -366,9 +364,7 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo lines[0].Should().StartWithEquivalent("CommandLine"); lines[1].Should().StartWithEquivalent("Copyright (c)"); #else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); + // The first two lines depend on the test-runner so ignore them #endif lines[2].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which"); lines[3].Should().BeEquivalentTo("changes to commit."); @@ -378,7 +374,7 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_formatted_text_given_display_width_100() { // Fixture setup @@ -399,9 +395,7 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo lines[0].Should().StartWithEquivalent("CommandLine"); lines[1].Should().BeEquivalentTo("Copyright (c) 2005 - 2018 Giacomo Stelluti Scala & Contributors"); #else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); + // The first two lines depend on the test-runner so ignore them #endif lines[2].Should().BeEquivalentTo("-p, --patch Use the interactive patch selection interface to chose which changes to commit."); lines[3].Should().BeEquivalentTo("--amend Used to amend the tip of the current branch."); @@ -410,7 +404,7 @@ public void Invoke_AutoBuild_for_Verbs_with_specific_verb_returns_appropriate_fo // Teardown } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Verbs_with_unknown_verb_returns_appropriate_formatted_text() { // Fixture setup @@ -431,9 +425,7 @@ public void Invoke_AutoBuild_for_Verbs_with_unknown_verb_returns_appropriate_for lines[0].Should().StartWithEquivalent("CommandLine"); lines[1].Should().StartWithEquivalent("Copyright (c)"); #else - // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); + // The first two lines depend on the test-runner so ignore them #endif lines[2].Should().BeEquivalentTo("add Add file contents to the index."); lines[3].Should().BeEquivalentTo("commit Record changes to the repository."); @@ -496,7 +488,7 @@ public static void RenderUsageText_returns_properly_formatted_text() lines[10].Should().BeEquivalentTo(" mono testapp.exe value"); } - //[Fact] + [Fact] public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatted_text() { // Fixture setup @@ -518,32 +510,32 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte lines[1].Should().StartWithEquivalent("Copyright (c)"); #else // Takes the name of the xUnit test program - lines[0].Should().StartWithEquivalent("xUnit"); - lines[1].Should().StartWithEquivalent("Copyright (C) Outercurve Foundation"); + //the first lines may depend on the test-runner (Ncrunch in my case) so ignore them + #endif lines[2].Should().BeEquivalentTo("ERROR(S):"); - lines[3].Should().BeEquivalentTo("Token 'badtoken' is not recognized."); + 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[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[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[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[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[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."); // Teardown } @@ -736,6 +728,21 @@ public void HelpTextIsConsitentRegardlessOfCompileTimeLineStyle() // Teardown } + [Fact] + public void HelpTextPreservesIndentationAcrossWordWrapWithSmallMaximumDisplayWidth() + { + // Fixture setup + // Exercise system + var sut = new HelpText {AddDashesToOption = true,MaximumDisplayWidth = 10} + .AddOptions(new NotParsed(TypeInfo.Create(typeof(HelpTextWithLineBreaksAndSubIndentation_Options)), + Enumerable.Empty())); + // Verify outcome + + Assert.True(sut.ToString().Length>0); + + // Teardown + } + } } From 5a3828edd37808893db3a45bafabebd46a2c507c Mon Sep 17 00:00:00 2001 From: neil macmullen Date: Fri, 28 Jun 2019 18:41:46 +0100 Subject: [PATCH 9/9] Add another extra couple of tests --- tests/CommandLine.Tests/StringExtensions.cs | 5 +++ tests/CommandLine.Tests/Unit/ParserTests.cs | 20 ++++++++++++ .../Unit/Text/HelpTextTests.cs | 32 +++++++++++++++++-- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/tests/CommandLine.Tests/StringExtensions.cs b/tests/CommandLine.Tests/StringExtensions.cs index e3830b0c..1ea18538 100644 --- a/tests/CommandLine.Tests/StringExtensions.cs +++ b/tests/CommandLine.Tests/StringExtensions.cs @@ -13,6 +13,11 @@ public static string[] ToNotEmptyLines(this string value) return value.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); } + public static string[] ToLines(this string value) + { + return value.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + } + public static string[] TrimStringArray(this IEnumerable array) { return array.Select(item => item.Trim()).ToArray(); diff --git a/tests/CommandLine.Tests/Unit/ParserTests.cs b/tests/CommandLine.Tests/Unit/ParserTests.cs index c183c47d..15ca2e79 100644 --- a/tests/CommandLine.Tests/Unit/ParserTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserTests.cs @@ -860,5 +860,25 @@ public void Parse_options_with_shuffled_index_values() Assert.Equal("two", args.Arg2); }); } + + + [Fact] + public void Blank_lines_are_inserted_between_verbs() + { + // Fixture setup + var help = new StringWriter(); + var sut = new Parser(config => config.HelpWriter = help); + + // Exercize system + sut.ParseArguments(new string[] { }); + var result = help.ToString(); + + // Verify outcome + var lines = result.ToLines().TrimStringArray(); + lines[6].Should().BeEquivalentTo("add Add file contents to the index."); + lines[8].Should().BeEquivalentTo("help Display more information on a specific command."); + lines[10].Should().BeEquivalentTo("version Display version information."); + // Teardown + } } } diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index d3d51254..b9d1531d 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -540,7 +540,7 @@ public void Invoke_AutoBuild_for_Options_with_Usage_returns_appropriate_formatte // Teardown } -#if !PLATFORM_DOTNET + [Fact] public void Default_set_to_sequence_should_be_properly_printed() { @@ -566,7 +566,6 @@ public void Default_set_to_sequence_should_be_properly_printed() // Teardown } -#endif [Fact] public void AutoBuild_when_no_assembly_attributes() @@ -744,5 +743,34 @@ public void HelpTextPreservesIndentationAcrossWordWrapWithSmallMaximumDisplayWid // Teardown } + + + [Fact] + public void Options_should_be_separated_by_spaces() + { + // Fixture setup + var handlers = new CultureInfo("en-US").MakeCultureHandlers(); + var fakeResult = + new NotParsed( + typeof(Options_With_Default_Set_To_Sequence).ToTypeInfo(), + Enumerable.Empty() + ); + + // Exercize system + handlers.ChangeCulture(); + var helpText = HelpText.AutoBuild(fakeResult); + handlers.ResetCulture(); + + // Verify outcome + var text = helpText.ToString(); + var lines = text.ToLines().TrimStringArray(); + Console.WriteLine(text); + lines[3].Should().Be("-z, --strseq (Default: a b c)"); + lines[5].Should().Be("-y, --intseq (Default: 1 2 3)"); + lines[7].Should().Be("-q, --dblseq (Default: 1.1 2.2 3.3)"); + + // Teardown + } + } }