diff --git a/.gitignore b/.gitignore index 6ad51d28e..3e10eb953 100644 --- a/.gitignore +++ b/.gitignore @@ -151,4 +151,7 @@ nCrunchTemp* TestResults/ .fake -.ionide \ No newline at end of file +.ionide + +# ApprovalTests +*.received.txt diff --git a/src/System.CommandLine.Tests/ApprovalTests/ApprovalTests.Config.cs b/src/System.CommandLine.Tests/ApprovalTests/ApprovalTests.Config.cs new file mode 100644 index 000000000..d690d9590 --- /dev/null +++ b/src/System.CommandLine.Tests/ApprovalTests/ApprovalTests.Config.cs @@ -0,0 +1,9 @@ +using ApprovalTests.Reporters; +using ApprovalTests.Reporters.TestFrameworks; + +// Use globally defined Reporter for ApprovalTests. Please see +// https://github.com/approvals/ApprovalTests.Net/blob/master/docs/ApprovalTests/Reporters.md + +[assembly: UseReporter(typeof(FrameworkAssertReporter))] + +[assembly: ApprovalTests.Namers.UseApprovalSubdirectory("Approvals")] 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 new file mode 100644 index 000000000..1286fccee --- /dev/null +++ b/src/System.CommandLine.Tests/ApprovalTests/Help/Approvals/HelpBuilderTests.Help_describes_default_values_for_complex_root_command_scenario.approved.txt @@ -0,0 +1,21 @@ +the-root-command: + Test description + +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] + the-root-arg-enum-default-description [default: Read] + +Options: + -trna, --the-root-option-no-arg (REQUIRED) the-root-option-no-arg-description + -trondda, --the-root-option-no-description-default-arg [default: the-root-option--no-description-default-arg-value] + -tronda, --the-root-option-no-default-arg (REQUIRED) the-root-option-no-default-description + -troda, --the-root-option-default-arg the-root-option-default-arg-description [default: the-root-option-arg-value] + -troea, --the-root-option-enum-arg the-root-option-description [default: Read] + -trorea, --the-root-option-required-enum-arg (REQUIRED) the-root-option-description [default: Read] + diff --git a/src/System.CommandLine.Tests/ApprovalTests/Help/HelpBuilderTests.Approval.cs b/src/System.CommandLine.Tests/ApprovalTests/Help/HelpBuilderTests.Approval.cs new file mode 100644 index 000000000..ead3f3b2f --- /dev/null +++ b/src/System.CommandLine.Tests/ApprovalTests/Help/HelpBuilderTests.Approval.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Xunit; +using System.CommandLine.Help; +using System.IO; +using ApprovalTests; + +namespace System.CommandLine.Tests.Help +{ + public partial class HelpBuilderTests + { + [Fact] + public void Help_describes_default_values_for_complex_root_command_scenario() + { + var command = new RootCommand(description: "Test description") + { + new Argument("the-root-arg-no-description-no-default"), + new Argument("the-root-arg-no-description-default", + argResult => "the-root-arg-no-description-default-value", + isDefault: true), + new Argument("the-root-arg-no-default") + { + Description = "the-root-arg-no-default-description", + }, + new Argument("the-root-arg", () => "the-root-arg-one-value") + { + Description = "the-root-arg-description" + }, + new Argument("the-root-arg-enum-default", () => FileAccess.Read) + { + Description = "the-root-arg-enum-default-description", + ArgumentType = typeof(FileAccess) + }, + new Option(aliases: new string[] {"--the-root-option-no-arg", "-trna"}) { + Description = "the-root-option-no-arg-description", + Required = true + }, + new Option( + aliases: new string[] {"--the-root-option-no-description-default-arg", "-trondda"}, + parseArgument: _ => "the-root-option--no-description-default-arg-value", + isDefault: true + ), + new Option(aliases: new string[] {"--the-root-option-no-default-arg", "-tronda"}) { + Description = "the-root-option-no-default-description", + Argument = new Argument("the-root-option-arg-no-default-arg") + { + Description = "the-root-option-arg-no-default-description" + }, + Required = true + }, + new Option(aliases: new string[] {"--the-root-option-default-arg", "-troda"}) { + Description = "the-root-option-default-arg-description", + Argument = new Argument("the-root-option-arg", () => "the-root-option-arg-value") + { + Description = "the-root-option-arg-description" + }, + }, + new Option(aliases: new string[] {"--the-root-option-enum-arg", "-troea"}) { + Description = "the-root-option-description", + Argument = new Argument("the-root-option-arg", () => FileAccess.Read) + { + Description = "the-root-option-arg-description", + }, + }, + new Option(aliases: new string[] {"--the-root-option-required-enum-arg", "-trorea"}) { + Description = "the-root-option-description", + Argument = new Argument("the-root-option-arg", () => FileAccess.Read) + { + Description = "the-root-option-arg-description", + }, + Required = true + } + }; + command.Name = "the-root-command"; + + HelpBuilder helpBuilder = GetHelpBuilder(LargeMaxWidth); + helpBuilder.Write(command); + var output = _console.Out.ToString(); + Approvals.Verify(output); + } + + } +} diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs index eadebf564..7e1c325b3 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs @@ -16,7 +16,7 @@ namespace System.CommandLine.Tests.Help { - public class HelpBuilderTests + public partial class HelpBuilderTests { private const int SmallMaxWidth = 70; private const int LargeMaxWidth = 200; @@ -842,6 +842,55 @@ public void Option_argument_descriptor_indicates_enums_values(Type type) _console.Out.ToString().Should().Contain($"--opt {_columnPadding}{description}"); } + [Fact] + public void Help_describes_default_value_for_defaultable_argument() + { + var argument = new Argument + { + Name = "the-arg", + Description = "Help text from HelpDetail", + }; + argument.SetDefaultValue("the-arg-value"); + + var command = new Command("the-command", + "Help text from description") { argument }; + + HelpBuilder helpBuilder = GetHelpBuilder(SmallMaxWidth); + + helpBuilder.Write(command); + + var help = _console.Out.ToString(); + + help.Should().Contain($"[default: the-arg-value]"); + } + + [Fact] + public void Command_arguments_default_value_provided() + { + var argument = new Argument + { + Name = "the-arg", + }; + + var otherArgument = new Argument + { + Name = "the-other-arg", + }; + argument.SetDefaultValue("the-arg-value"); + otherArgument.SetDefaultValue("the-other-arg-value"); + var command = new Command("the-command", + "Help text from description") { argument, otherArgument }; + + HelpBuilder helpBuilder = GetHelpBuilder(SmallMaxWidth); + + helpBuilder.Write(command); + + var help = _console.Out.ToString(); + + help.Should().Contain($"[default: the-arg-value]") + .And.Contain($"[default: the-other-arg-value]"); + } + #endregion Arguments #region Options @@ -1188,6 +1237,58 @@ public void Option_aliases_are_shown_before_long_names_regardless_of_alphabetica .ToString().Should().Contain("-a, -z, --aaa, --zzz"); } + [Fact] + public void Help_describes_default_value_for_option_with_defaultable_argument() + { + var argument = new Argument + { + Name = "the-arg", + }; + argument.SetDefaultValue("the-arg-value"); + + var command = new Command("the-command", "command help") + { + new Option(new[] { "-arg"}) + { + Argument = argument + } + }; + + HelpBuilder helpBuilder = GetHelpBuilder(SmallMaxWidth); + + helpBuilder.Write(command); + + var help = _console.Out.ToString(); + + help.Should().Contain($"[default: the-arg-value]"); + } + + [Fact] + public void Help_should_not_contain_default_value_for_hidden_argument_defined_for_option () + { + var argument = new Argument + { + Name = "the-arg", + IsHidden = true + }; + argument.SetDefaultValue("the-arg-value"); + var command = new Command("the-command", "command help") + { + new Option(new[] { "-arg"}) + { + Argument = argument + } + }; + + HelpBuilder helpBuilder = GetHelpBuilder(LargeMaxWidth); + + helpBuilder.Write(command); + + var help = _console.Out.ToString(); + + help.Should().NotContain($"[default: the-arg-value]"); + } + #endregion Options #region Subcommands @@ -1391,6 +1492,82 @@ public void Help_text_can_be_added_after_default_text_by_inheriting_HelpBuilder( console.Out.ToString().Should().EndWith("The text to add"); } + [Fact] + public void Help_describes_default_value_for_subcommand_with_arguments_and_only_defaultable_is_shown() + { + var argument = new Argument + { + Name = "the-arg", + }; + var otherArgumentHidden = new Argument + { + Name = "the-other-hidden-arg", + IsHidden = true + }; + argument.SetDefaultValue("the-arg-value"); + otherArgumentHidden.SetDefaultValue("the-other-hidden-arg-value"); + + var command = new Command("outer", "outer command help") + { + new Argument + { + Name = "outer-args" + }, + new Command("inner", $"inner command help") + { + argument, + otherArgumentHidden, + new Argument + { + Name = "inner-other-arg-no-default" + } + } + }; + + HelpBuilder helpBuilder = GetHelpBuilder(LargeMaxWidth); + + helpBuilder.Write(command); + + var help = _console.Out.ToString(); + + help.Should().Contain($"[default: the-arg-value]"); + } + + [Fact] + public void Help_describes_default_values_for_subcommand_with_multiple_defaultable_arguments () + { + var argument = new Argument + { + Name = "the-arg", + }; + var otherArgument = new Argument + { + Name = "the-other-arg" + }; + argument.SetDefaultValue("the-arg-value"); + otherArgument.SetDefaultValue("the-other-arg-value"); + + var command = new Command("outer", "outer command help") + { + new Argument + { + Name = "outer-args" + }, + new Command("inner", $"inner command help") + { + argument, otherArgument + } + }; + + HelpBuilder helpBuilder = GetHelpBuilder(LargeMaxWidth); + + helpBuilder.Write(command); + + var help = _console.Out.ToString(); + + help.Should().Contain($"[the-arg: the-arg-value, the-other-arg: the-other-arg-value]"); + } + private class CustomHelpBuilderThatAddsTextAfterDefaultText : HelpBuilder { private readonly string _theTextToAdd; diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 130c65401..79b6afc6e 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -19,9 +19,10 @@ + - + diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine/Help/HelpBuilder.cs index 56c2eae51..15caaa06e 100644 --- a/src/System.CommandLine/Help/HelpBuilder.cs +++ b/src/System.CommandLine/Help/HelpBuilder.cs @@ -20,11 +20,11 @@ public class HelpBuilder : IHelpBuilder protected IConsole Console { get; } - public int ColumnGutter { get; } + public int ColumnGutter { get; } - public int IndentationSize { get; } + public int IndentationSize { get; } - public int MaxWidth { get; } + public int MaxWidth { get; } /// /// Brokers the generation and output of help text of @@ -215,8 +215,13 @@ protected void AppendHelpItem(HelpItem helpItem, int maxInvocationWidth) var offset = maxInvocationWidth + ColumnGutter - helpItem.Invocation.Length; var availableWidth = GetAvailableWidth(); var maxDescriptionWidth = availableWidth - maxInvocationWidth - ColumnGutter; - - var descriptionLines = SplitText(helpItem.Description, maxDescriptionWidth); + var descriptionColumn = helpItem.Description; + if (helpItem.HasDefaultValueHint) + { + var postfix = string.IsNullOrEmpty(helpItem.Description) ? string.Empty : " "; + descriptionColumn += postfix + helpItem.DefaultValueHint; + } + var descriptionLines = SplitText(descriptionColumn, maxDescriptionWidth); var lineCount = descriptionLines.Count; AppendLine(descriptionLines.FirstOrDefault(), offset); @@ -253,7 +258,7 @@ protected virtual IReadOnlyCollection SplitText(string text, int maxLeng if (string.IsNullOrWhiteSpace(cleanText) || textLength < maxLength) { - return new[] {cleanText}; + return new[] { cleanText }; } var lines = new List(); @@ -294,7 +299,7 @@ private IEnumerable GetArgumentHelpItems(ISymbol symbol) { foreach (var argument in symbol.Arguments()) { - if(ShouldShowHelp(argument)) + if (ShouldShowHelp(argument)) { var argumentDescriptor = ArgumentDescriptor(argument); @@ -303,15 +308,23 @@ private IEnumerable GetArgumentHelpItems(ISymbol symbol) : $"<{argumentDescriptor}>"; var argumentDescription = argument?.Description ?? ""; - - yield return new HelpItem(invocation, argumentDescription); + var defaultValueHint = argument != null + ? BuildDefaultValueHint(argument) + : null; + yield return new HelpItem(invocation, argumentDescription, defaultValueHint); } } + + string? BuildDefaultValueHint(IArgument argument) + { + var hint = DefaultValueHint(argument); + return !string.IsNullOrWhiteSpace(hint) ? $"[{hint}]" : null; + } } protected virtual string ArgumentDescriptor(IArgument argument) { - if (argument.ValueType == typeof(bool) || argument.ValueType == typeof(bool?) ) + if (argument.ValueType == typeof(bool) || argument.ValueType == typeof(bool?)) { return ""; } @@ -325,6 +338,14 @@ protected virtual string ArgumentDescriptor(IArgument argument) return argument.Name; } + protected virtual string DefaultValueHint(IArgument argument, bool isSingleArgument = true) => + (argument.HasDefaultValue, isSingleArgument, ShouldShowDefaultValueHint(argument)) switch + { + (true, true, true) => $"default: {argument.GetDefaultValue()}", + (true, false, true) => $"{argument.Name}: {argument.GetDefaultValue()}", + _ => "" + }; + /// /// Formats the help rows for a given option /// @@ -365,7 +386,22 @@ private IEnumerable GetOptionHelpItems(ISymbol symbol) invocation += " (REQUIRED)"; } - yield return new HelpItem(invocation, symbol.Description); + yield return new HelpItem(invocation, + symbol.Description, + BuildDefaultValueHint(symbol.Arguments())); + + string? BuildDefaultValueHint(IEnumerable arguments) + { + int defaultableArgumentCount = arguments + .Count(ShouldShowDefaultValueHint); + bool isSingleDefault = defaultableArgumentCount == 1; + var argumentDefaultValues = arguments + .Where(ShouldShowDefaultValueHint) + .Select(argument => DefaultValueHint(argument, isSingleDefault)); + return defaultableArgumentCount > 0 + ? $"[{string.Join(", ", argumentDefaultValues)}]" + : null; + } } /// @@ -424,7 +460,7 @@ protected virtual void AddUsage(ICommand command) { usage.Add(Usage.Options); } - + usage.Add(FormatArgumentUsage(command.Arguments.ToArray())); var hasCommandHelp = command.Children @@ -574,34 +610,42 @@ private bool ShouldDisplayArgumentHelp(ICommand? command) private int GetConsoleWindowWidth() { - try + try { return System.Console.WindowWidth; } catch (System.IO.IOException) { return int.MaxValue; - } + } } protected class HelpItem { - public HelpItem(string invocation, string? description = null) + public HelpItem( + string invocation, + string? description = null, + string? defaultValueHint = null) { Invocation = invocation; Description = description ?? ""; + DefaultValueHint = defaultValueHint ?? ""; } public string Invocation { get; } public string Description { get; } - protected bool Equals(HelpItem other) => + public string DefaultValueHint { get; } + + protected bool Equals(HelpItem other) => (Invocation, Description) == (other.Invocation, other.Description); - public override bool Equals(object obj) => Equals((HelpItem) obj); + public override bool Equals(object obj) => Equals((HelpItem)obj); public override int GetHashCode() => (Invocation, Description).GetHashCode(); + + public bool HasDefaultValueHint => !string.IsNullOrWhiteSpace(DefaultValueHint); } private static class HelpSection @@ -699,5 +743,10 @@ internal bool ShouldShowHelp(ISymbol symbol) { return !symbol.IsHidden; } + + internal bool ShouldShowDefaultValueHint(IArgument argument) + { + return argument.HasDefaultValue && ShouldShowHelp(argument); + } } }