From 1e49943981502bffb6066ee07caf17f66d558e92 Mon Sep 17 00:00:00 2001 From: apogeeoak <59737221+apogeeoak@users.noreply.github.com> Date: Fri, 26 Jun 2020 11:27:45 -0400 Subject: [PATCH 1/2] Wrap `HelpItem`s to table. (#629) --- ...complex_root_command_scenario.approved.txt | 2 +- .../Help/HelpBuilderTests.cs | 150 +++++++--- src/System.CommandLine/Help/HelpBuilder.cs | 281 ++++++++++++------ 3 files changed, 304 insertions(+), 129 deletions(-) diff --git a/src/System.CommandLine.Tests/ApprovalTests/Help/Approvals/HelpBuilderTests.Help_describes_default_values_for_complex_root_command_scenario.approved.txt b/src/System.CommandLine.Tests/ApprovalTests/Help/Approvals/HelpBuilderTests.Help_describes_default_values_for_complex_root_command_scenario.approved.txt index 1286fccee3..c5ec4faf3a 100644 --- a/src/System.CommandLine.Tests/ApprovalTests/Help/Approvals/HelpBuilderTests.Help_describes_default_values_for_complex_root_command_scenario.approved.txt +++ b/src/System.CommandLine.Tests/ApprovalTests/Help/Approvals/HelpBuilderTests.Help_describes_default_values_for_complex_root_command_scenario.approved.txt @@ -5,7 +5,7 @@ Usage: the-root-command [options] [ [ []]] Arguments: - + [default: the-root-arg-no-description-default-value] the-root-arg-no-default-description the-root-arg-description [default: the-root-arg-one-value] diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs index 5527e5a149..9e2847ea5b 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs @@ -111,9 +111,9 @@ public void Synopsis_section_properly_wraps_description() public void Command_name_in_synopsis_can_be_specified() { var command = new RootCommand - { - Name = "custom-name" - }; + { + Name = "custom-name" + }; var helpBuilder = GetHelpBuilder(SmallMaxWidth); helpBuilder.Write(command); @@ -138,10 +138,10 @@ public void Usage_section_shows_arguments_if_there_are_arguments_for_command_whe string expectedDescriptor) { var argument = new Argument - { - Name = "the-args", - Arity = new ArgumentArity(minArity, maxArity) - }; + { + Name = "the-args", + Arity = new ArgumentArity(minArity, maxArity) + }; var command = new Command("the-command", "command help") { argument, @@ -178,18 +178,18 @@ public void Usage_section_shows_arguments_if_there_are_arguments_for_command_whe string expectedDescriptor) { var arg1 = new Argument - { - Name = "arg1", - Arity = new ArgumentArity( - minArityForArg1, - maxArityForArg1) - }; + { + Name = "arg1", + Arity = new ArgumentArity( + minArityForArg1, + maxArityForArg1) + }; var arg2 = new Argument - { - Name = "arg2", - Arity = new ArgumentArity( - minArityForArg2, - maxArityForArg2) + { + Name = "arg2", + Arity = new ArgumentArity( + minArityForArg2, + maxArityForArg2) }; var command = new Command("the-command", "command help") { @@ -426,7 +426,7 @@ public void Usage_section_does_not_contain_hidden_argument() var command = new Command(commandName, "Does things"); var hiddenArg = new Argument { - Name = "hidden", + Name = "hidden", IsHidden = true }; var visibleArg = new Argument @@ -442,7 +442,7 @@ public void Usage_section_does_not_contain_hidden_argument() var expected = $"Usage:{NewLine}" + $"{_indentation}{commandName} <{visibleArgName}>{NewLine}{NewLine}"; - + _console.Out.ToString().Should().Contain(expected); } @@ -465,7 +465,7 @@ public void Arguments_section_is_not_included_if_there_are_commands_but_no_argum _helpBuilder.Write(command); _console.Out.ToString().Should().NotContain("Arguments:"); - + _helpBuilder.Write(command); _console.Out.ToString().Should().NotContain("Arguments:"); } @@ -534,7 +534,7 @@ public void Arguments_section_includes_configured_argument_aliases() Description = "Sets the verbosity." } }; - + _helpBuilder.Write(command); var help = _console.Out.ToString(); @@ -589,14 +589,14 @@ public void Arguments_section_does_not_contain_hidden_argument() var expected = $"Arguments:{NewLine}" + - $"{_indentation}<{visibleArgName}>{_columnPadding}{visibleDesc}{NewLine}{NewLine}"; + $"{_indentation}<{visibleArgName}>{_columnPadding}{visibleDesc}{NewLine}{NewLine}"; _helpBuilder.Write(command); var help = _console.Out.ToString(); help.Should().Contain(expected); help.Should().NotContain(hiddenArgName); - help.Should().NotContain(hiddenDesc); + help.Should().NotContain(hiddenDesc); } [Fact] @@ -736,6 +736,34 @@ public void Arguments_section_properly_wraps_description() _console.Out.ToString().Should().Contain(expected); } + [Fact] + public void Arguments_section_properly_wraps() + { + var name = "argument-name-for-a-command-that-is-long-enough-to-wrap-to-a-new-line"; + var description = "Argument description for a command with line breaks that is long enough to wrap to a new line."; + + var command = new RootCommand() + { + new Argument + { + Name = name, + Description = description + } + }; + + HelpBuilder helpBuilder = GetHelpBuilder(SmallMaxWidth); + helpBuilder.Write(command); + + var expected = + $"Arguments:{NewLine}" + + $"{_indentation} {_columnPadding}is long enough to wrap to a new{NewLine}" + + $"{_indentation} {_columnPadding}line.{NewLine}{NewLine}"; + + _console.Out.ToString().Should().Contain(expected); + } + [Theory] [InlineData(typeof(bool))] [InlineData(typeof(bool?))] @@ -813,10 +841,10 @@ public void Option_argument_descriptor_is_empty_for_boolean_values(Type type) HelpBuilder helpBuilder = GetHelpBuilder(SmallMaxWidth); helpBuilder.Write(command); - + _console.Out.ToString().Should().Contain($"--opt{_columnPadding}{description}"); } - + [Theory] [InlineData(typeof(FileAccess))] [InlineData(typeof(FileAccess?))] @@ -947,7 +975,7 @@ public void Options_section_does_not_contain_option_with_HelpDefinition_that_IsH { IsHidden = false }); - + _helpBuilder.Write(command); @@ -1084,6 +1112,29 @@ public void Options_section_properly_wraps_description() _console.Out.ToString().Should().Contain(expected); } + [Fact] + public void Options_section_properly_wraps() + { + var alias = "--option-alias-for-a-command-that-is-long-enough-to-wrap-to-a-new-line"; + var description = "Option description that is long enough to wrap."; + + var command = new RootCommand() + { + new Option(alias, description) + }; + + HelpBuilder helpBuilder = GetHelpBuilder(SmallMaxWidth); + helpBuilder.Write(command); + + var expected = + $"Options:{NewLine}" + + $"{_indentation}--option-alias-for-a-command-th{_columnPadding}Option description that is long{NewLine}" + + $"{_indentation}at-is-long-enough-to-wrap-to-a-{_columnPadding}enough to wrap.{NewLine}" + + $"{_indentation}new-line{NewLine}{NewLine}"; + + _console.Out.ToString().Should().Contain(expected); + } + [Fact] public void Options_section_does_not_contain_hidden_argument() { @@ -1126,13 +1177,13 @@ public void Required_options_are_indicated() }; _helpBuilder.Write(command); - + var help = _console.Out.ToString(); help.Should() .Contain("--required (REQUIRED)"); } - + [Fact] public void Required_options_are_indicated_when_argument_is_named() { @@ -1146,7 +1197,7 @@ public void Required_options_are_indicated_when_argument_is_named() }; _helpBuilder.Write(command); - + var help = _console.Out.ToString(); help.Should() @@ -1177,12 +1228,12 @@ public void Options_aliases_differing_only_by_prefix_are_deduplicated_favoring_d }; _helpBuilder.Write(command); - + var help = _console.Out.ToString(); help.Should().NotContain("/x"); } - + [Fact] public void Options_aliases_differing_only_by_prefix_are_deduplicated_favoring_double_dashed_prefixes() { @@ -1192,7 +1243,7 @@ public void Options_aliases_differing_only_by_prefix_are_deduplicated_favoring_d }; _helpBuilder.Write(command); - + var help = _console.Out.ToString(); help.Should().NotContain("/long"); @@ -1265,7 +1316,7 @@ public void Help_describes_default_value_for_option_with_argument_having_default } [Fact] - public void Help_should_not_contain_default_value_for_hidden_argument_defined_for_option () + public void Help_should_not_contain_default_value_for_hidden_argument_defined_for_option() { var argument = new Argument { @@ -1427,7 +1478,7 @@ public void Subcommands_properly_wraps_description() } }; - helpBuilder.Write(command); + helpBuilder.Write(command); var expected = $"Commands:{NewLine}" + @@ -1437,6 +1488,29 @@ public void Subcommands_properly_wraps_description() _console.Out.ToString().Should().Contain(expected); } + [Fact] + public void Subcommands_section_properly_wraps() + { + var name = "subcommand-name-that-is-long-enough-to-wrap-to-a-new-line"; + var description = "Subcommand description that is really long. So long that it caused the line to wrap."; + + var command = new RootCommand() + { + new Command(name, description) + }; + + var helpBuilder = GetHelpBuilder(SmallMaxWidth); + helpBuilder.Write(command); + + var expected = + $"Commands:{NewLine}" + + $"{_indentation}subcommand-name-that-is-long-en{_columnPadding}Subcommand description that is{NewLine}" + + $"{_indentation}ough-to-wrap-to-a-new-line {_columnPadding}really long. So long that it{NewLine}" + + $"{_indentation} {_columnPadding}caused the line to wrap.{NewLine}{NewLine}"; + + _console.Out.ToString().Should().Contain(expected); + } + [Fact] public void Subcommand_help_contains_command_with_empty_description() { @@ -1479,12 +1553,12 @@ public void Subcommand_help_does_not_contain_hidden_argument() var subCommand = new Command("the-subcommand"); var hidden = new Argument() { - Name = "the-hidden", + Name = "the-hidden", IsHidden = true }; var visible = new Argument() { - Name = "the-visible", + Name = "the-visible", IsHidden = false }; subCommand.AddArgument(hidden); @@ -1557,7 +1631,7 @@ public void Help_describes_default_value_for_subcommand_with_arguments_and_only_ } [Fact] - public void Help_describes_default_values_for_subcommand_with_multiple_defaultable_arguments () + public void Help_describes_default_values_for_subcommand_with_multiple_defaultable_arguments() { var argument = new Argument { diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine/Help/HelpBuilder.cs index a97858da88..eef5828093 100644 --- a/src/System.CommandLine/Help/HelpBuilder.cs +++ b/src/System.CommandLine/Help/HelpBuilder.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.Collections.ObjectModel; using System.CommandLine.IO; using System.CommandLine.Parsing; +using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -47,11 +49,7 @@ public HelpBuilder( Console = console ?? throw new ArgumentNullException(nameof(console)); ColumnGutter = columnGutter ?? DefaultColumnGutter; IndentationSize = indentationSize ?? DefaultIndentationSize; - - MaxWidth = maxWidth - ?? (Console is SystemConsole - ? GetConsoleWindowWidth() - : int.MaxValue); + MaxWidth = maxWidth ?? GetConsoleWindowWidth(Console); } /// @@ -106,13 +104,17 @@ protected void Outdent(int levels = 1) } /// - /// Gets the currently available space based on the from the window - /// and the current indentation level + /// Gets the currently available space based on the + /// of the window and the current indentation level. /// - /// The number of characters available on the current line + /// + /// The number of characters available on the current line. If no space is + /// available then is returned. + /// protected int GetAvailableWidth() { - return MaxWidth - CurrentIndentation - WindowMargin; + var width = MaxWidth - CurrentIndentation - WindowMargin; + return (width > 0) ? width : int.MaxValue; } /// @@ -204,103 +206,199 @@ private void AppendDescription(string description) } /// - /// Adds columnar content for a using the current indentation - /// for the line, and adding the appropriate padding between the columns + /// Writes a collection of to the console. /// - /// - /// Current to write to the console - /// - /// - /// Maximum number of characters accross all help items - /// occupied by the invocation text + /// + /// Collection of to write to the console. /// - protected void AppendHelpItem( - HelpItem helpItem, - int maxInvocationWidth) + /// + protected virtual void AppendHelpItems(IReadOnlyCollection helpItems) { - if (helpItem is null) - { - throw new ArgumentNullException(nameof(helpItem)); - } - - AppendText(helpItem.Invocation, CurrentIndentation); + if (helpItems is null) + throw new ArgumentNullException(nameof(helpItems)); - var offset = maxInvocationWidth + ColumnGutter - helpItem.Invocation.Length; - var availableWidth = GetAvailableWidth(); - var maxDescriptionWidth = availableWidth - maxInvocationWidth - ColumnGutter; - var descriptionColumn = helpItem.Description; - if (helpItem.HasDefaultValueHint) + var table = CreateTable(helpItems, item => new[] { - var postfix = string.IsNullOrEmpty(helpItem.Description) ? string.Empty : " "; - descriptionColumn += postfix + helpItem.DefaultValueHint; - } - var descriptionLines = SplitText(descriptionColumn, maxDescriptionWidth); - var lineCount = descriptionLines.Count; + item.Invocation, + JoinNonEmpty(" ", item.Description, item.DefaultValueHint) + }); - AppendLine(descriptionLines.FirstOrDefault(), offset); + var columnWidths = ColumnWidths(table); + AppendTable(table, columnWidths); + } - if (lineCount == 1) - { - return; + /// + /// Create a read only table of strings using the projection of a collection. + /// + /// The type of the elements of . + /// The collection of values to create the table from. + /// A transformation function to apply to each element of . + /// + /// A read only table of strings whose elements are the projection of the collection with whitespace formatting removed. + /// + protected virtual ReadOnlyCollection> CreateTable(IEnumerable collection, Func> selector) + { + return collection.Select(selector) + .Select(row => row + .Select(element => RemoveFormatting(element)) + .ToList().AsReadOnly()) + .ToList().AsReadOnly(); + } + + /// + /// Allocate space for columns favoring minimal rows. Wider columns are allocated more space if it is available. + /// + /// The table of values to determine column widths for. + /// A collection of column widths. + protected virtual ReadOnlyCollection ColumnWidths(IEnumerable> table) + { + if (!table.Any()) + return Array.AsReadOnly(Array.Empty()); + + var columns = table.First().Count; + Debug.Assert(table.All(e => e.Count == columns), $"Every row in {nameof(table)} must have the same number of columns."); + var unsetWidth = -1; + var widths = new int[columns]; + for (int i = 0; i < columns; ++i) + widths[i] = unsetWidth; + var maxWidths = new int[columns]; + for (int i = 0; i < columns; ++i) + maxWidths[i] = table.Max(row => row[i].Length); + + var nonEmptyColumns = maxWidths.Count(width => width > 0); + + // Usable width is the total available width minus space between columns. + var available = GetAvailableWidth() - (ColumnGutter * (nonEmptyColumns - 1)); + // If available space is not sufficent then do not wrap. + // If all columns are empty then return array of zeros. + if (available - nonEmptyColumns < 0 || nonEmptyColumns == 0) + return Array.AsReadOnly(maxWidths); + + // Loop variables. + var unset = nonEmptyColumns; + var previousUnset = 0; + // Limit looping to avoid O(columns^2) runtime. + var loopLimit = 5; + + while (unset > 0) + { + var equal = (available - widths.Where(width => width > 0).Sum()) / unset; + // Allocate remaining space equally if no other columns fit on a single line. Or if loop limit has been reached. + var allocateRemaining = unset == previousUnset || loopLimit <= 1; + for (int i = 0; i < columns; ++i) + { + // If width has not been set. + if (widths[i] == unsetWidth) + { + // Attempt to fit column to single line. + var width = maxWidths[i]; + if (allocateRemaining) + width = Math.Min(width, equal); + if (width <= equal) + widths[i] = width; + } + } + previousUnset = unset; + unset = widths.Count(width => width < 0); + --loopLimit; } - offset = CurrentIndentation + maxInvocationWidth + ColumnGutter; + return Array.AsReadOnly(widths); + } - foreach (var descriptionLine in descriptionLines.Skip(1)) + /// + /// Writes a table of strings to the console. + /// + /// The table of values to write. + /// The width of each column of the table. + protected virtual void AppendTable(IEnumerable> table, IReadOnlyList columnWidths) + { + foreach (var row in table) + AppendRow(row, columnWidths); + } + + /// + /// Writes a row of strings to the console with columns of the given width. + /// + /// The row of elements to write. + /// The width of each column of the table. + protected virtual void AppendRow(IEnumerable row, IReadOnlyList columnWidths) + { + var split = row.Select((element, index) => SplitText(element, columnWidths[index])).ToArray(); + var longest = split.Max(lines => lines.Count); + for (int line = 0; line < longest; ++line) { - AppendLine(descriptionLine, offset); + var columnStart = 0; + var appended = 0; + AppendPadding(CurrentIndentation); + + for (int column = 0; column < split.Length; ++column) + { + var lines = split[column]; + if (line < lines.Count) + { + var text = lines[line]; + if (!string.IsNullOrEmpty(text)) + { + var offset = columnStart - appended; + AppendText(text, offset); + appended += offset + text.Length; + } + } + columnStart += columnWidths[column] + ColumnGutter; + } + AppendBlankLine(); } } /// - /// Takes a string of text and breaks it into lines of - /// characters. This does not preserve any formatting of the incoming text. + /// Takes a string of text and breaks it into lines of + /// characters. Whitespace formatting of the incoming text is removed. /// - /// Text content to split into writable lines - /// - /// Maximum number of characters allowed for writing the supplied - /// + /// Text content to split into lines. + /// Maximum number of characters allowed per line. /// - /// Collection of lines of at most characters - /// generated from the supplied + /// Collection of lines of at most characters + /// generated from the supplied . /// - protected virtual IReadOnlyCollection SplitText(string text, int maxLength) + protected virtual ReadOnlyCollection SplitText(string text, int width) { - var cleanText = Regex.Replace(text, "\\s+", " "); - var textLength = cleanText.Length; + if (text is null) + throw new ArgumentNullException(nameof(text), $"{nameof(text)} cannot be null."); + if (width < 0) + throw new ArgumentOutOfRangeException(nameof(width), $"{nameof(width)} must be non-negative."); - if (string.IsNullOrWhiteSpace(cleanText) || textLength < maxLength) - { - return new[] { cleanText }; - } + if (width == 0) + return Array.AsReadOnly(Array.Empty()); + var separator = ' '; + + var start = 0; var lines = new List(); - var builder = new StringBuilder(); + text = RemoveFormatting(text); - foreach (var item in cleanText.Split(new char[0], StringSplitOptions.RemoveEmptyEntries)) + while (start < text.Length - width) { - var length = item.Length + builder.Length; + var end = text.LastIndexOf(separator, start + width); - if (length >= maxLength) + // If last word starts before width / 2 include entire width. + if (end - start <= width / 2) { - lines.Add(builder.ToString()); - builder.Clear(); + lines.Add(text.Substring(start, width)); + // Start next line directly after current line. "abcdef" => abc|def + start = start + width; } - - if (builder.Length > 0) + else { - builder.Append(" "); + lines.Add(text.Substring(start, end - start)); + // Move past separator for start of next line. "abc def" => abc| |def + start = end + 1; } - - builder.Append(item); } - if (builder.Length > 0) - { - lines.Add(builder.ToString()); - } + lines.Add(text.Substring(start, text.Length - start)); - return lines; + return lines.AsReadOnly(); } /// @@ -599,7 +697,7 @@ protected virtual void AddSubcommands(ICommand command) .Where(ShouldShowHelp) .ToArray(); - HelpSection.WriteItems(this, + HelpSection.WriteItems(this, Commands.Title, subcommands.SelectMany(GetOptionHelpItems).ToArray()); } @@ -624,18 +722,31 @@ private bool ShouldDisplayArgumentHelp(ICommand? command) return command.Arguments.Any(ShouldShowHelp); } - private int GetConsoleWindowWidth() + private int GetConsoleWindowWidth(IConsole console) { try { - return System.Console.WindowWidth; + if (console is SystemConsole && System.Console.WindowWidth > 0) + return System.Console.WindowWidth; + else + return int.MaxValue; } - catch (System.IO.IOException) + catch { return int.MaxValue; } } + private string RemoveFormatting(string input) + { + return Regex.Replace(input, @"\s+", " "); + } + + private string JoinNonEmpty(string separator, params string?[] values) + { + return string.Join(separator, values.Where(str => !string.IsNullOrEmpty(str))); + } + protected class HelpItem { public HelpItem( @@ -734,19 +845,9 @@ private static void AddDescription(HelpBuilder builder, string? description = nu builder.AppendDescription(description!); } - private static void AddInvocation( - HelpBuilder builder, - IReadOnlyCollection helpItems) + private static void AddInvocation(HelpBuilder builder, IReadOnlyCollection helpItems) { - var maxWidth = helpItems - .Select(line => line.Invocation.Length) - .OrderByDescending(textLength => textLength) - .First(); - - foreach (var helpItem in helpItems) - { - builder.AppendHelpItem(helpItem, maxWidth); - } + builder.AppendHelpItems(helpItems); } } From 47f47aebc9f1b291fa4032affdf928b2115dd3c2 Mon Sep 17 00:00:00 2001 From: apogeeoak <59737221+apogeeoak@users.noreply.github.com> Date: Fri, 26 Jun 2020 14:42:11 -0400 Subject: [PATCH 2/2] Made requested changes. --- src/System.CommandLine/Help/HelpBuilder.cs | 61 ++++++++++++---------- src/System.CommandLine/IO/SystemConsole.cs | 2 + 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine/Help/HelpBuilder.cs index eef5828093..6abd611649 100644 --- a/src/System.CommandLine/Help/HelpBuilder.cs +++ b/src/System.CommandLine/Help/HelpBuilder.cs @@ -215,7 +215,9 @@ private void AppendDescription(string description) protected virtual void AppendHelpItems(IReadOnlyCollection helpItems) { if (helpItems is null) + { throw new ArgumentNullException(nameof(helpItems)); + } var table = CreateTable(helpItems, item => new[] { @@ -228,21 +230,20 @@ protected virtual void AppendHelpItems(IReadOnlyCollection helpItems) } /// - /// Create a read only table of strings using the projection of a collection. + /// Create a table of strings using the projection of a collection. /// /// The type of the elements of . /// The collection of values to create the table from. /// A transformation function to apply to each element of . /// - /// A read only table of strings whose elements are the projection of the collection with whitespace formatting removed. + /// A table of strings whose elements are the projection of the collection with whitespace formatting removed. /// - protected virtual ReadOnlyCollection> CreateTable(IEnumerable collection, Func> selector) + protected virtual IEnumerable> CreateTable(IEnumerable collection, Func> selector) { return collection.Select(selector) .Select(row => row - .Select(element => RemoveFormatting(element)) - .ToList().AsReadOnly()) - .ToList().AsReadOnly(); + .Select(element => ShortenWhitespace(element)) + .ToArray()); } /// @@ -250,20 +251,25 @@ protected virtual ReadOnlyCollection> CreateTable( /// /// The table of values to determine column widths for. /// A collection of column widths. - protected virtual ReadOnlyCollection ColumnWidths(IEnumerable> table) + private IReadOnlyList ColumnWidths(IEnumerable> table) { if (!table.Any()) - return Array.AsReadOnly(Array.Empty()); + { + return Array.Empty(); + } var columns = table.First().Count; - Debug.Assert(table.All(e => e.Count == columns), $"Every row in {nameof(table)} must have the same number of columns."); var unsetWidth = -1; var widths = new int[columns]; for (int i = 0; i < columns; ++i) + { widths[i] = unsetWidth; + } var maxWidths = new int[columns]; for (int i = 0; i < columns; ++i) + { maxWidths[i] = table.Max(row => row[i].Length); + } var nonEmptyColumns = maxWidths.Count(width => width > 0); @@ -272,7 +278,9 @@ protected virtual ReadOnlyCollection ColumnWidths(IEnumerable // If available space is not sufficent then do not wrap. // If all columns are empty then return array of zeros. if (available - nonEmptyColumns < 0 || nonEmptyColumns == 0) - return Array.AsReadOnly(maxWidths); + { + return maxWidths; + } // Loop variables. var unset = nonEmptyColumns; @@ -293,9 +301,13 @@ protected virtual ReadOnlyCollection ColumnWidths(IEnumerable // Attempt to fit column to single line. var width = maxWidths[i]; if (allocateRemaining) + { width = Math.Min(width, equal); + } if (width <= equal) + { widths[i] = width; + } } } previousUnset = unset; @@ -303,7 +315,7 @@ protected virtual ReadOnlyCollection ColumnWidths(IEnumerable --loopLimit; } - return Array.AsReadOnly(widths); + return widths; } /// @@ -311,7 +323,7 @@ protected virtual ReadOnlyCollection ColumnWidths(IEnumerable /// /// The table of values to write. /// The width of each column of the table. - protected virtual void AppendTable(IEnumerable> table, IReadOnlyList columnWidths) + private void AppendTable(IEnumerable> table, IReadOnlyList columnWidths) { foreach (var row in table) AppendRow(row, columnWidths); @@ -322,7 +334,7 @@ protected virtual void AppendTable(IEnumerable> table, IRead /// /// The row of elements to write. /// The width of each column of the table. - protected virtual void AppendRow(IEnumerable row, IReadOnlyList columnWidths) + private void AppendRow(IEnumerable row, IReadOnlyList columnWidths) { var split = row.Select((element, index) => SplitText(element, columnWidths[index])).ToArray(); var longest = split.Max(lines => lines.Count); @@ -361,7 +373,7 @@ protected virtual void AppendRow(IEnumerable row, IReadOnlyList col /// Collection of lines of at most characters /// generated from the supplied . /// - protected virtual ReadOnlyCollection SplitText(string text, int width) + protected virtual IReadOnlyList SplitText(string text, int width) { if (text is null) throw new ArgumentNullException(nameof(text), $"{nameof(text)} cannot be null."); @@ -369,13 +381,13 @@ protected virtual ReadOnlyCollection SplitText(string text, int width) throw new ArgumentOutOfRangeException(nameof(width), $"{nameof(width)} must be non-negative."); if (width == 0) - return Array.AsReadOnly(Array.Empty()); + return Array.Empty(); var separator = ' '; var start = 0; var lines = new List(); - text = RemoveFormatting(text); + text = ShortenWhitespace(text); while (start < text.Length - width) { @@ -398,7 +410,7 @@ protected virtual ReadOnlyCollection SplitText(string text, int width) lines.Add(text.Substring(start, text.Length - start)); - return lines.AsReadOnly(); + return lines; } /// @@ -724,20 +736,13 @@ private bool ShouldDisplayArgumentHelp(ICommand? command) private int GetConsoleWindowWidth(IConsole console) { - try - { - if (console is SystemConsole && System.Console.WindowWidth > 0) - return System.Console.WindowWidth; - else - return int.MaxValue; - } - catch - { + if (console is SystemConsole systemConsole) + return systemConsole.GetConsoleWindowWidth(); + else return int.MaxValue; - } } - private string RemoveFormatting(string input) + private string ShortenWhitespace(string input) { return Regex.Replace(input, @"\s+", " "); } diff --git a/src/System.CommandLine/IO/SystemConsole.cs b/src/System.CommandLine/IO/SystemConsole.cs index 1cefa4fd15..07f9758542 100644 --- a/src/System.CommandLine/IO/SystemConsole.cs +++ b/src/System.CommandLine/IO/SystemConsole.cs @@ -20,5 +20,7 @@ public SystemConsole() public bool IsOutputRedirected => Console.IsOutputRedirected; public bool IsInputRedirected => Console.IsInputRedirected; + + public int GetConsoleWindowWidth() => IsOutputRedirected ? int.MaxValue : System.Console.WindowWidth; } }