diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28731172b1..9d97a2dde9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ Contributing ============ -Please read [.NET Guidelines](https://github.com/dotnet/runtime/blob/master/CONTRIBUTING.md) for more general information about coding styles, source structure, making pull requests, and more. +Please read [.NET Guidelines](https://github.com/dotnet/runtime/blob/main/CONTRIBUTING.md) for more general information about coding styles, source structure, making pull requests, and more. ## Developer guide @@ -9,7 +9,7 @@ This project can be developed on any platform. To get started, follow instructio ### Prerequisites -This project depends on .NET 7. Before working on the project, check that the [.NET SDK](https://dotnet.microsoft.com/en-us/download) is installed. +This project depends on the .NET 9 SDK. Before working on the project, check that the [.NET SDK](https://dotnet.microsoft.com/en-us/download) is installed. ### Visual Studio diff --git a/Directory.Packages.props b/Directory.Packages.props index 0efd501b78..4de7f06b05 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,8 @@ + + @@ -20,9 +22,10 @@ + - + \ No newline at end of file diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt index d6319ed1be..45cc9f778a 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt @@ -153,10 +153,11 @@ public System.Collections.Generic.IEnumerable Parents { get; } public System.Collections.Generic.IEnumerable GetCompletions(System.CommandLine.Completions.CompletionContext context) public System.String ToString() - public class VersionOption : Option + public class VersionOption : Option .ctor() .ctor(System.String name, System.String[] aliases) public System.CommandLine.Invocation.CommandLineAction Action { get; set; } + public System.Type ValueType { get; } System.CommandLine.Completions public class CompletionContext public static CompletionContext Empty { get; } @@ -185,10 +186,11 @@ System.CommandLine.Help public class HelpAction : System.CommandLine.Invocation.SynchronousCommandLineAction .ctor() public System.Int32 Invoke(System.CommandLine.ParseResult parseResult) - public class HelpOption : System.CommandLine.Option + public class HelpOption : System.CommandLine.Option .ctor() .ctor(System.String name, System.String[] aliases) public System.CommandLine.Invocation.CommandLineAction Action { get; set; } + public System.Type ValueType { get; } System.CommandLine.Invocation public abstract class AsynchronousCommandLineAction : CommandLineAction public System.Threading.Tasks.Task InvokeAsync(System.CommandLine.ParseResult parseResult, System.Threading.CancellationToken cancellationToken = null) diff --git a/src/System.CommandLine.Tests/ArgumentTests.cs b/src/System.CommandLine.Tests/ArgumentTests.cs index 037934cbab..8672fbd379 100644 --- a/src/System.CommandLine.Tests/ArgumentTests.cs +++ b/src/System.CommandLine.Tests/ArgumentTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using System.Linq; +using FluentAssertions.Execution; using Xunit; namespace System.CommandLine.Tests; @@ -41,6 +42,53 @@ public void When_there_is_no_default_value_then_GetDefaultValue_throws() .Be("Argument \"the-arg\" does not have a default value"); } + [Fact] + public void GetRequiredValue_does_not_throw_when_help_is_requested_and_DefaultValueFactory_is_set() + { + var argument = new Argument("the-arg") + { + DefaultValueFactory = _ => "default" + }; + + var result = new RootCommand { argument }.Parse("-h"); + + using var _ = new AssertionScope(); + + result.Invoking(r => r.GetRequiredValue(argument)).Should().NotThrow(); + result.GetRequiredValue(argument).Should().Be("default"); + + result.Invoking(r => r.GetRequiredValue("the-arg")).Should().NotThrow(); + result.GetRequiredValue("the-arg").Should().Be("default"); + + result.Errors.Should().BeEmpty(); + } + + [Fact] + public void When_there_is_no_default_value_then_GetDefaultValue_does_not_throw_for_bool() + { + var argument = new Argument("the-arg"); + + argument.GetDefaultValue().Should().Be(false); + } + + [Fact] + public void When_there_is_no_default_value_then_GetRequiredValue_does_not_throw_for_bool() + { + var argument = new Argument("the-arg"); + + var result = new RootCommand { argument }.Parse(""); + + using var _ = new AssertionScope(); + + result.Invoking(r => r.GetRequiredValue(argument)).Should().NotThrow(); + result.GetRequiredValue(argument).Should().BeFalse(); + + result.Invoking(r => r.GetRequiredValue("the-arg")).Should().NotThrow(); + result.GetRequiredValue("the-arg").Should().BeFalse(); + + result.Errors.Should().BeEmpty(); + } + [Fact] public void Argument_of_enum_can_limit_enum_members_as_valid_values() { diff --git a/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs b/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs index 7e2fcec4bc..454942a873 100644 --- a/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs +++ b/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Net; +using FluentAssertions.Execution; using Xunit; namespace System.CommandLine.Tests.Binding @@ -585,6 +586,7 @@ public void Values_can_be_correctly_converted_to_Uri_when_custom_parser_is_provi [Fact] public void Options_with_arguments_specified_can_be_correctly_converted_to_bool_without_the_parser_specifying_a_custom_converter() { + using var _ = new AssertionScope(); GetValue(new Option("-x"), "-x false").Should().BeFalse(); GetValue(new Option("-x"), "-x true").Should().BeTrue(); } diff --git a/src/System.CommandLine.Tests/DirectiveTests.cs b/src/System.CommandLine.Tests/DirectiveTests.cs index b1e9bd33ba..9d2851726d 100644 --- a/src/System.CommandLine.Tests/DirectiveTests.cs +++ b/src/System.CommandLine.Tests/DirectiveTests.cs @@ -1,6 +1,7 @@ // 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 System.CommandLine.Parsing; using System.Linq; using System.Threading.Tasks; using FluentAssertions; @@ -22,14 +23,14 @@ public void Directives_should_be_considered_as_unmatched_tokens_when_they_are_no } [Fact] - public void Raw_tokens_still_hold_directives() + public void Tokens_still_hold_directives() { Directive directive = new ("parse"); ParseResult result = Parse(new Option("-y"), directive, "[parse] -y"); result.GetResult(directive).Should().NotBeNull(); - result.Tokens.Should().Contain(t => t.Value == "[parse]"); + result.Tokens.Should().Contain(t => t.Value == "[parse]" && t.Type == TokenType.Directive); } [Fact] diff --git a/src/System.CommandLine.Tests/GetValueByNameParserTests.cs b/src/System.CommandLine.Tests/GetValueByNameParserTests.cs index 7a2a8721e7..b787337c16 100644 --- a/src/System.CommandLine.Tests/GetValueByNameParserTests.cs +++ b/src/System.CommandLine.Tests/GetValueByNameParserTests.cs @@ -368,4 +368,30 @@ public void Recursive_option_on_parent_command_can_be_looked_up_when_subcommand_ result.GetValue("--opt").Should().Be("hello"); } + + [Fact] + public void When_argument_type_is_unknown_then_named_lookup_can_be_used_to_get_value_as_supertype() + { + var command = new RootCommand + { + new Argument("arg") + }; + + var result = command.Parse("value"); + + result.GetValue("arg").Should().Be("value"); + } + + [Fact] + public void When_option_type_is_unknown_then_named_lookup_can_be_used_to_get_value_as_supertype() + { + var command = new RootCommand + { + new Option("-x") + }; + + var result = command.Parse("-x value"); + + result.GetValue("-x").Should().Be("value"); + } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/GlobalOptionTests.cs b/src/System.CommandLine.Tests/GlobalOptionTests.cs index 1acb39213c..7c51a2e751 100644 --- a/src/System.CommandLine.Tests/GlobalOptionTests.cs +++ b/src/System.CommandLine.Tests/GlobalOptionTests.cs @@ -25,8 +25,8 @@ public void When_a_required_global_option_is_omitted_it_results_in_an_error() { var command = new Command("child"); var rootCommand = new RootCommand { command }; - command.SetAction((_) => { }); - var requiredOption = new Option("--i-must-be-set") + command.SetAction(_ => { }); + var requiredOption = new Option("--i-must-be-set") { Required = true, Recursive = true @@ -45,7 +45,7 @@ public void When_a_required_global_option_is_omitted_it_results_in_an_error() public void When_a_required_global_option_has_multiple_aliases_the_error_message_uses_the_name() { var rootCommand = new RootCommand(); - var requiredOption = new Option("-i", "--i-must-be-set") + var requiredOption = new Option("-i", "--i-must-be-set") { Required = true, Recursive = true diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs index b2d9335ed3..22cced02a0 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs @@ -328,7 +328,6 @@ public void Argument_can_fallback_to_default_when_customizing( config.Output.ToString().Should().MatchRegex(expected); } - [Fact] public void Individual_symbols_can_be_customized() { diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs index 89fb9fca8a..4a735a4aad 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs @@ -778,13 +778,15 @@ public void Help_describes_default_value_for_argument() help.Should().Contain("[default: the-arg-value]"); } - [Fact] - public void Help_does_not_show_default_value_for_argument_when_default_value_is_empty() + [Theory] + [InlineData("")] + [InlineData(null)] + public void Help_does_not_show_default_value_for_argument_when_default_value_is_null_or_empty(string defaultValue) { var argument = new Argument("the-arg") { Description = "The argument description", - DefaultValueFactory = (_) => "" + DefaultValueFactory = _ => defaultValue }; var command = new Command("the-command", "The command description") @@ -798,7 +800,32 @@ public void Help_does_not_show_default_value_for_argument_when_default_value_is_ var help = _console.ToString(); - help.Should().NotContain("[default"); + help.Should().NotContain("[]"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Help_does_not_show_default_value_for_option_when_default_value_is_null_or_empty(string defaultValue) + { + var argument = new Option("--opt") + { + Description = "The option description", + DefaultValueFactory = _ => defaultValue + }; + + var command = new Command("the-command", "The command description") + { + argument + }; + + var helpBuilder = GetHelpBuilder(SmallMaxWidth); + + helpBuilder.Write(command, _console); + + var help = _console.ToString(); + + help.Should().NotContain("[]"); } [Fact] diff --git a/src/System.CommandLine.Tests/OptionTests.cs b/src/System.CommandLine.Tests/OptionTests.cs index 7f34e9d3dc..804af82386 100644 --- a/src/System.CommandLine.Tests/OptionTests.cs +++ b/src/System.CommandLine.Tests/OptionTests.cs @@ -4,6 +4,7 @@ using System.CommandLine.Parsing; using FluentAssertions; using System.Linq; +using FluentAssertions.Execution; using Xunit; namespace System.CommandLine.Tests @@ -272,16 +273,82 @@ public void Option_T_default_value_factory_can_be_set_after_instantiation() { var option = new Option("-x"); - option.DefaultValueFactory = (_) => 123; + option.DefaultValueFactory = _ => 123; - new RootCommand { option } - .Parse("") + var parseResult = new RootCommand { option }.Parse(""); + + parseResult .GetResult(option) .GetValueOrDefault() .Should() .Be(123); } + [Fact] + public void When_there_is_no_default_value_then_GetRequiredValue_does_not_throw_for_bool() + { + var option = new Option("-x"); + + var result = new RootCommand { option }.Parse(""); + + using var _ = new AssertionScope(); + + result.Invoking(r => r.GetRequiredValue(option)).Should().NotThrow(); + result.GetRequiredValue(option).Should().BeFalse(); + + result.Invoking(r => r.GetRequiredValue("-x")).Should().NotThrow(); + result.GetRequiredValue("-x").Should().BeFalse(); + + result.Errors.Should().BeEmpty(); + } + + [Fact] + public void GetRequiredValue_does_not_throw_when_help_is_requested_and_DefaultValueFactory_is_set() + { + var option = new Option("-x") + { + DefaultValueFactory = _ => "default" + }; + + var result = new RootCommand { option }.Parse("-h"); + + using var _ = new AssertionScope(); + + result.Invoking(r => r.GetRequiredValue(option)).Should().NotThrow(); + result.GetRequiredValue(option).Should().Be("default"); + + result.Invoking(r => r.GetRequiredValue("-x")).Should().NotThrow(); + result.GetRequiredValue("-x").Should().Be("default"); + + result.Errors.Should().BeEmpty(); + } + + [Fact] + public void When_there_is_no_default_value_then_GetDefaultValue_does_not_throw_for_bool() + { + var option = new Option("-x"); + + option.GetDefaultValue().Should().Be(false); + } + + [Fact] + public void When_there_is_a_default_value_then_GetRequiredValue_does_not_throw() + { + var option = new Option("-x") + { + Required = true, + DefaultValueFactory = _ => "default" + }; + + var result = new RootCommand { option }.Parse(""); + + using var _ = new AssertionScope(); + + result.Invoking(r => r.GetRequiredValue(option)).Should().NotThrow(); + result.Invoking(r => r.GetRequiredValue("-x")).Should().NotThrow(); + result.GetRequiredValue(option).Should().Be("default"); + } + [Fact] public void Option_T_default_value_is_validated() { @@ -322,9 +389,6 @@ public void Option_of_boolean_defaults_to_false_when_not_specified() var result = new RootCommand { option }.Parse(""); - result.GetResult(option) - .Should() - .BeNull(); result.GetValue(option) .Should() .BeFalse(); @@ -405,6 +469,8 @@ public void Multiple_identifier_token_instances_without_argument_tokens_can_be_p var result = root.Parse("-v -v -v"); + using var _ = new AssertionScope(); + result.GetValue(option).Should().BeTrue(); result.GetRequiredValue(option).Should().BeTrue(); result.GetRequiredValue(option.Name).Should().BeTrue(); diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index e9c9fd709e..410c67bc82 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -10,7 +10,6 @@ using System.Linq; using FluentAssertions.Common; using Xunit; -using Xunit.Abstractions; namespace System.CommandLine.Tests { @@ -25,12 +24,12 @@ private T GetValue(ParseResult parseResult, Argument argument) [Fact] public void An_option_can_be_checked_by_object_instance() { - var option = new Option("--flag"); - var option2 = new Option("--flag2"); - var result = new RootCommand { option, option2 } - .Parse("--flag"); + var option1 = new Option("--option1"); + var option2 = new Option("--option2"); - result.GetResult(option).Should().NotBeNull(); + var result = new RootCommand { option1, option2 }.Parse("--option1"); + + result.GetResult(option1).Should().NotBeNull(); result.GetResult(option2).Should().BeNull(); } @@ -166,7 +165,9 @@ public void Option_long_forms_do_not_get_unbundled() result.CommandResult .Children - .Select(o => ((OptionResult)o).Option.Name) + .OfType() + .Where(r => !r.Implicit) + .Select(o => o.Option.Name) .Should() .BeEquivalentTo("--xyz"); } @@ -660,9 +661,9 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm .Should() .BeOfType() .Which - .Children + .Command .Should() - .AllBeAssignableTo(); + .Be(outer); result.CommandResult .Children .Should() @@ -673,25 +674,17 @@ public void When_options_with_the_same_name_are_defined_on_parent_and_child_comm public void When_options_with_the_same_name_are_defined_on_parent_and_child_commands_and_specified_in_between_then_it_attaches_to_the_outer_command() { var outer = new Command("outer"); - outer.Options.Add(new Option("-x")); + var outerOption = new Option("-x"); + outer.Options.Add(outerOption); var inner = new Command("inner"); - inner.Options.Add(new Option("-x")); + var innerOption = new Option("-x"); + inner.Options.Add(innerOption); outer.Subcommands.Add(inner); var result = outer.Parse("outer -x inner"); - result.CommandResult - .Children - .Should() - .BeEmpty(); - result.CommandResult - .Parent - .Should() - .BeOfType() - .Which - .Children - .Should() - .ContainSingle(o => o is OptionResult && ((OptionResult)o).Option.Name == "-x"); + result.GetValue(outerOption).Should().BeTrue(); + result.GetValue(innerOption).Should().BeFalse(); } [Fact] @@ -1049,8 +1042,8 @@ public void Option_and_Command_can_have_the_same_alias() [Fact] public void Options_can_have_the_same_alias_differentiated_only_by_prefix() { - var option1 = new Option("-a"); - var option2 = new Option("--a"); + var option1 = new Option("-a"); + var option2 = new Option("--a"); var parser = new RootCommand { @@ -1058,16 +1051,16 @@ public void Options_can_have_the_same_alias_differentiated_only_by_prefix() option2 }; - parser.Parse("-a").CommandResult + parser.Parse("-a value").CommandResult .Children .Select(s => ((OptionResult)s).Option) .Should() - .BeEquivalentTo(new[] { option1 }); - parser.Parse("--a").CommandResult + .BeEquivalentTo([option1]); + parser.Parse("--a value").CommandResult .Children .Select(s => ((OptionResult)s).Option) .Should() - .BeEquivalentTo(new[] { option2 }); + .BeEquivalentTo([option2]); } [Theory] diff --git a/src/System.CommandLine.Tests/ResponseFileTests.cs b/src/System.CommandLine.Tests/ResponseFileTests.cs index 17e34ee9ae..5404fdee22 100644 --- a/src/System.CommandLine.Tests/ResponseFileTests.cs +++ b/src/System.CommandLine.Tests/ResponseFileTests.cs @@ -211,8 +211,8 @@ public void Response_file_can_contain_comments_which_are_ignored_when_loaded() [Fact] public void When_response_file_does_not_exist_then_error_is_returned() { - var optionOne = new Option("--flag"); - var optionTwo = new Option("--flag2"); + var optionOne = new Option("-x"); + var optionTwo = new Option("-y"); var result = new RootCommand { @@ -229,8 +229,8 @@ public void When_response_file_does_not_exist_then_error_is_returned() [Fact] public void When_response_filepath_is_not_specified_then_error_is_returned() { - var optionOne = new Option("--flag"); - var optionTwo = new Option("--flag2"); + var optionOne = new Option("-x"); + var optionTwo = new Option("-y"); var result = new RootCommand { @@ -253,8 +253,8 @@ public void When_response_filepath_is_not_specified_then_error_is_returned() public void When_response_file_cannot_be_read_then_specified_error_is_returned() { var nonexistent = Path.GetTempFileName(); - var optionOne = new Option("--flag"); - var optionTwo = new Option("--flag2"); + var optionOne = new Option("--flag"); + var optionTwo = new Option("--flag2"); using (File.Open(nonexistent, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 29b661c36f..e448549980 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -6,12 +6,12 @@ $(DefaultExcludesInProjectFolder);TestApps\** $(NoWarn);CS8632 - - + + - + @@ -24,11 +24,11 @@ - + - + @@ -36,13 +36,17 @@ + + + + + - + diff --git a/src/System.CommandLine.Tests/VersionOptionTests.cs b/src/System.CommandLine.Tests/VersionOptionTests.cs index f7552c84f3..b796349136 100644 --- a/src/System.CommandLine.Tests/VersionOptionTests.cs +++ b/src/System.CommandLine.Tests/VersionOptionTests.cs @@ -1,12 +1,12 @@ // 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 System.CommandLine.Help; +using FluentAssertions; +using FluentAssertions.Execution; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using FluentAssertions; using Xunit; using static System.Environment; @@ -160,17 +160,16 @@ public async Task Version_can_specify_additional_alias() { RootCommand rootCommand = new(); - for (int i = 0; i < rootCommand.Options.Count; i++) - { - if (rootCommand.Options[i] is VersionOption) - rootCommand.Options[i] = new VersionOption("-v", "-version"); - } + rootCommand.Options.Clear(); + rootCommand.Add(new VersionOption("-v", "-version")); CommandLineConfiguration configuration = new(rootCommand) { Output = new StringWriter() }; + using var _ = new AssertionScope(); + await configuration.InvokeAsync("-v"); configuration.Output.ToString().Should().Be($"{version}{NewLine}"); @@ -180,18 +179,19 @@ public async Task Version_can_specify_additional_alias() } [Fact] - public void Version_is_not_valid_with_other_tokens_uses_custom_alias() + public void Version_is_not_valid_with_other_tokens_when_it_uses_custom_alias() { var childCommand = new Command("subcommand"); - childCommand.SetAction((_) => { }); + childCommand.SetAction(_ => { }); var rootCommand = new RootCommand { childCommand }; - rootCommand.Options[1] = new VersionOption("-v"); + rootCommand.Options.Clear(); + rootCommand.Add(new VersionOption("-v")); - rootCommand.SetAction((_) => { }); + rootCommand.SetAction(_ => { }); CommandLineConfiguration configuration = new(rootCommand) { diff --git a/src/System.CommandLine/Argument.cs b/src/System.CommandLine/Argument.cs index aae85e2f26..61c41c309c 100644 --- a/src/System.CommandLine/Argument.cs +++ b/src/System.CommandLine/Argument.cs @@ -131,5 +131,20 @@ public override IEnumerable GetCompletions(CompletionContext con public override string ToString() => $"{nameof(Argument)}: {Name}"; internal bool IsBoolean() => ValueType == typeof(bool) || ValueType == typeof(bool?); + + internal static Argument None { get; } = new NoArgument(); + + internal class NoArgument : Argument + { + internal NoArgument() : base("@none") + { + } + + public override Type ValueType { get; } = typeof(void); + + internal override object? GetDefaultValue(ArgumentResult argumentResult) => null; + + public override bool HasDefaultValue => false; + } } } diff --git a/src/System.CommandLine/Argument{T}.cs b/src/System.CommandLine/Argument{T}.cs index 42263f819e..7bc9c5835b 100644 --- a/src/System.CommandLine/Argument{T}.cs +++ b/src/System.CommandLine/Argument{T}.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// 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 System.Collections.Generic; @@ -10,6 +10,7 @@ namespace System.CommandLine public class Argument : Argument { private Func? _customParser; + private Func? _defaultValueFactory; /// /// Initializes a new instance of the Argument class. @@ -27,7 +28,23 @@ public Argument(string name) : base(name) /// The same instance can be set as , in such case /// the delegate is also invoked when an input was provided. /// - public Func? DefaultValueFactory { get; set; } + public Func? DefaultValueFactory + { + get + { + if (_defaultValueFactory is null) + { + switch (this) + { + case Argument boolArgument: + boolArgument.DefaultValueFactory = _ => false; + break; + } + } + return _defaultValueFactory; + } + set => _defaultValueFactory = value; + } /// /// A custom argument parser. diff --git a/src/System.CommandLine/Help/HelpBuilder.Default.cs b/src/System.CommandLine/Help/HelpBuilder.Default.cs index 23199dab53..33e53c0da0 100644 --- a/src/System.CommandLine/Help/HelpBuilder.Default.cs +++ b/src/System.CommandLine/Help/HelpBuilder.Default.cs @@ -18,13 +18,17 @@ public static class Default /// /// Gets an argument's default value to be displayed in help. /// - /// The argument or option to get the default value for. - public static string GetArgumentDefaultValue(Symbol parameter) + /// The argument or option to get the default value for. + public static string GetArgumentDefaultValue(Symbol symbol) { - return parameter switch + return symbol switch { - Argument argument => argument.HasDefaultValue ? ToString(argument.GetDefaultValue()) : "", - Option option => option.HasDefaultValue ? ToString(option.GetDefaultValue()) : "", + Argument argument => ShouldShowDefaultValue(argument) + ? ToString(argument.GetDefaultValue()) + : "", + Option option => ShouldShowDefaultValue(option) + ? ToString(option.GetDefaultValue()) + : "", _ => throw new InvalidOperationException("Symbol must be an Argument or Option.") }; @@ -37,6 +41,22 @@ public static string GetArgumentDefaultValue(Symbol parameter) }; } + public static bool ShouldShowDefaultValue(Symbol symbol) => + symbol switch + { + Option option => ShouldShowDefaultValue(option), + Argument argument => ShouldShowDefaultValue(argument), + _ => false + }; + + public static bool ShouldShowDefaultValue(Option option) => + option.HasDefaultValue && + !(option.ValueType == typeof(bool) || option.ValueType == typeof(bool?)); + + public static bool ShouldShowDefaultValue(Argument argument) => + argument.HasDefaultValue && + !(argument.ValueType == typeof(bool) || argument.ValueType == typeof(bool?)); + /// /// Gets the description for an argument (typically used in the second column text in the arguments section). /// diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine/Help/HelpBuilder.cs index 2a035e24f4..af6046d898 100644 --- a/src/System.CommandLine/Help/HelpBuilder.cs +++ b/src/System.CommandLine/Help/HelpBuilder.cs @@ -301,7 +301,7 @@ private string FormatArgumentUsage(IList arguments) if (isOptional) { sb.Append($"[<{argument.Name}>{arityIndicator}"); - (end ??= new ()).Add(']'); + (end ??= []).Add(']'); } else { @@ -315,11 +315,11 @@ private string FormatArgumentUsage(IList arguments) { sb.Length--; - if (end is { }) + if (end is not null) { while (end.Count > 0) { - sb.Append(end[end.Count - 1]); + sb.Append(end[^1]); end.RemoveAt(end.Count - 1); } } @@ -348,7 +348,7 @@ private static IEnumerable WrapText(string text, int maxWidth) } //First handle existing new lines - var parts = text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + var parts = text.Split(["\r\n", "\n"], StringSplitOptions.None); foreach (string part in parts) { @@ -405,7 +405,7 @@ public TwoColumnHelpRow GetTwoColumnRow( Customization? customization = null; - if (_customizationsBySymbol is { }) + if (_customizationsBySymbol is not null) { _customizationsBySymbol.TryGetValue(symbol, out customization); } @@ -436,8 +436,8 @@ TwoColumnHelpRow GetOptionOrCommandRow() //in case symbol description is customized, do not output default value //default value output is not customizable for identifier symbols - var defaultValueDescription = customizedSymbolDescription == null - ? GetSymbolDefaultValue(symbol) + var defaultValueDescription = customizedSymbolDescription is null + ? GetOptionOrCommandDefaultValue() : string.Empty; var secondColumnText = $"{symbolDescription} {defaultValueDescription}".Trim(); @@ -454,7 +454,8 @@ TwoColumnHelpRow GetCommandArgumentRow(Argument argument) customization?.GetSecondColumn?.Invoke(context) ?? Default.GetArgumentDescription(argument); var defaultValueDescription = - argument.HasDefaultValue + Default.ShouldShowDefaultValue(argument) && + !string.IsNullOrEmpty(GetArgumentDefaultValue(context.Command, argument, true, context)) ? $"[{GetArgumentDefaultValue(context.Command, argument, true, context)}]" : ""; @@ -463,17 +464,25 @@ TwoColumnHelpRow GetCommandArgumentRow(Argument argument) return new TwoColumnHelpRow(firstColumnText, secondColumnText); } - string GetSymbolDefaultValue(Symbol symbol) + string GetOptionOrCommandDefaultValue() { var arguments = symbol.GetParameters(); - var defaultArguments = arguments.Where(x => !x.Hidden && (x is Argument { HasDefaultValue: true } || x is Option { HasDefaultValue: true })).ToArray(); + var defaultArguments = arguments.Where(x => !x.Hidden && Default.ShouldShowDefaultValue(x)).ToArray(); - if (defaultArguments.Length == 0) return ""; + if (defaultArguments.Length == 0) + { + return ""; + } var isSingleArgument = defaultArguments.Length == 1; - var argumentDefaultValues = defaultArguments - .Select(argument => GetArgumentDefaultValue(symbol, argument, isSingleArgument, context)); - return $"[{string.Join(", ", argumentDefaultValues)}]"; + var argumentDefaultValues = string.Join( + ", ", + defaultArguments + .Select(argument => GetArgumentDefaultValue(symbol, argument, isSingleArgument, context))); + + return string.IsNullOrEmpty(argumentDefaultValues) + ? "" + : $"[{argumentDefaultValues}]"; } } @@ -483,10 +492,6 @@ private string GetArgumentDefaultValue( bool displayArgumentName, HelpContext context) { - string label = displayArgumentName - ? LocalizationResources.HelpArgumentDefaultValueLabel() - : parameter.Name; - string? displayedDefaultValue = null; if (_customizationsBySymbol is not null) @@ -510,6 +515,9 @@ private string GetArgumentDefaultValue( return ""; } + string label = displayArgumentName + ? LocalizationResources.HelpArgumentDefaultValueLabel() + : parameter.Name; return $"{label}: {displayedDefaultValue}"; } diff --git a/src/System.CommandLine/Help/HelpBuilderExtensions.cs b/src/System.CommandLine/Help/HelpBuilderExtensions.cs index a130f13220..4fb1999a64 100644 --- a/src/System.CommandLine/Help/HelpBuilderExtensions.cs +++ b/src/System.CommandLine/Help/HelpBuilderExtensions.cs @@ -1,6 +1,7 @@ // 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 System; using System.Collections.Generic; namespace System.CommandLine.Help @@ -30,21 +31,13 @@ internal static IEnumerable GetParameters(this Symbol symbol) internal static (string? Prefix, string Alias) SplitPrefix(this string rawAlias) { - if (rawAlias[0] == '/') + return rawAlias[0] switch { - return ("/", rawAlias.Substring(1)); - } - else if (rawAlias[0] == '-') - { - if (rawAlias.Length > 1 && rawAlias[1] == '-') - { - return ("--", rawAlias.Substring(2)); - } - - return ("-", rawAlias.Substring(1)); - } - - return (null, rawAlias); + '/' => ("/", rawAlias[1..]), + '-' when rawAlias.Length > 1 && rawAlias[1] is '-' => ("--", rawAlias[2..]), + '-' => ("-", rawAlias[1..]), + _ => (null, rawAlias) + }; } internal static IEnumerable RecurseWhileNotNull(this T? source, Func next) where T : class diff --git a/src/System.CommandLine/Help/HelpOption.cs b/src/System.CommandLine/Help/HelpOption.cs index 17957a179c..f1d40583e9 100644 --- a/src/System.CommandLine/Help/HelpOption.cs +++ b/src/System.CommandLine/Help/HelpOption.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Help /// /// A standard option that indicates that command line help should be displayed. /// - public sealed class HelpOption : Option + public sealed class HelpOption : Option { private CommandLineAction? _action; @@ -22,7 +22,7 @@ public sealed class HelpOption : Option /// /? /// /// - public HelpOption() : this("--help", new[] { "-h", "/h", "-?", "/?" }) + public HelpOption() : this("--help", ["-h", "/h", "-?", "/?"]) { } @@ -30,10 +30,11 @@ public HelpOption() : this("--help", new[] { "-h", "/h", "-?", "/?" }) /// When added to a , it configures the application to show help when given name or one of the aliases are specified on the command line. /// public HelpOption(string name, params string[] aliases) - : base(name, aliases, new Argument(name) { Arity = ArgumentArity.Zero }) + : base(name, aliases) { Recursive = true; Description = LocalizationResources.HelpOptionDescription(); + Arity = ArgumentArity.Zero; } /// @@ -42,5 +43,9 @@ public override CommandLineAction? Action get => _action ??= new HelpAction(); set => _action = value ?? throw new ArgumentNullException(nameof(value)); } + + internal override Argument Argument => Argument.None; + + public override Type ValueType => typeof(void); } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index 35482c5d1a..6561f5a386 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -71,16 +71,16 @@ internal void Validate(bool completeValidation) if (Command.HasOptions) { - ValidateOptions(completeValidation); + ValidateOptionsAndAddDefaultResults(completeValidation); } if (Command.HasArguments) { - ValidateArguments(completeValidation); + ValidateArgumentsAndAddDefaultResults(completeValidation); } } - private void ValidateOptions(bool completeValidation) + private void ValidateOptionsAndAddDefaultResults(bool completeValidation) { var options = Command.Options; for (var i = 0; i < options.Count; i++) @@ -105,7 +105,7 @@ private void ValidateOptions(bool completeValidation) argumentResult = new(optionResult.Option.Argument, SymbolResultTree, optionResult); SymbolResultTree.Add(optionResult.Option.Argument, argumentResult); - if (option.Required && !option.Argument.HasDefaultValue) + if (option is { Required: true, Argument.HasDefaultValue: false }) { argumentResult.AddError(LocalizationResources.RequiredOptionWasNotProvided(option.Name)); continue; @@ -148,7 +148,7 @@ private void ValidateOptions(bool completeValidation) } } - private void ValidateArguments(bool completeValidation) + private void ValidateArgumentsAndAddDefaultResults(bool completeValidation) { var arguments = Command.Arguments; for (var i = 0; i < arguments.Count; i++) diff --git a/src/System.CommandLine/Parsing/OptionResult.cs b/src/System.CommandLine/Parsing/OptionResult.cs index 489c80d38d..478a3a9d23 100644 --- a/src/System.CommandLine/Parsing/OptionResult.cs +++ b/src/System.CommandLine/Parsing/OptionResult.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.CommandLine.Binding; -using System.Diagnostics.CodeAnalysis; using System.Linq; namespace System.CommandLine.Parsing diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index 8ebdc84d01..de7dcf2d9c 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -58,9 +58,11 @@ internal ParseResult Parse() ParseCommandChildren(); - if (!_isHelpRequested) + ValidateAndAddDefaultResults(); + + if (_isHelpRequested) { - Validate(); + _symbolResultTree.Errors?.Clear(); } if (_primaryAction is null) @@ -366,7 +368,7 @@ private void AddCurrentTokenToUnmatched() _symbolResultTree.AddUnmatchedToken(CurrentToken, _innermostCommandResult, _rootCommandResult); } - private void Validate() + private void ValidateAndAddDefaultResults() { // Only the inner most command goes through complete validation, // for other commands only a subset of options is checked. diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 4778c4093e..49abc6be86 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// 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 System.Collections.Generic; diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 09fd8f326a..a11b00a17e 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -1,12 +1,11 @@ - $(NetMinimum);netstandard2.0 + $(NetMinimum);netstandard2.0;$(NetFrameworkMinimum) true enable Support for parsing command lines, supporting both POSIX and Windows conventions and shell-agnostic command line completions. true - portable @@ -14,12 +13,13 @@ true true - + - + + diff --git a/src/System.CommandLine/VersionOption.cs b/src/System.CommandLine/VersionOption.cs index 9ba607198b..6c31ab1485 100644 --- a/src/System.CommandLine/VersionOption.cs +++ b/src/System.CommandLine/VersionOption.cs @@ -1,6 +1,7 @@ // 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 System.CommandLine.Help; using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; @@ -10,14 +11,14 @@ namespace System.CommandLine /// /// A standard option that indicates that version information should be displayed for the app. /// - public sealed class VersionOption : Option + public sealed class VersionOption : Option { private CommandLineAction? _action; /// /// When added to a , it enables the use of a --version option, which when specified in command line input will short circuit normal command handling and instead write out version information before exiting. /// - public VersionOption() : this("--version", Array.Empty()) + public VersionOption() : this("--version") { } @@ -25,10 +26,11 @@ public VersionOption() : this("--version", Array.Empty()) /// When added to a , it enables the use of a provided option name and aliases, which when specified in command line input will short circuit normal command handling and instead write out version information before exiting. /// public VersionOption(string name, params string[] aliases) - : base(name, aliases, new Argument("--version") { Arity = ArgumentArity.Zero }) + : base(name, aliases) { Description = LocalizationResources.VersionOptionDescription(); AddValidators(); + Arity = ArgumentArity.Zero; } /// @@ -43,7 +45,9 @@ private void AddValidators() Validators.Add(static result => { if (result.Parent is CommandResult parent && - parent.Children.Any(r => r is not OptionResult { Option: VersionOption })) + parent.Children.Any(r => + r is not OptionResult { Option: VersionOption } && + r is not OptionResult { Implicit: true })) { result.AddError(LocalizationResources.VersionOptionCannotBeCombinedWithOtherArguments(result.IdentifierToken?.Value ?? result.Option.Name)); } @@ -52,6 +56,11 @@ private void AddValidators() internal override bool Greedy => false; + internal override Argument Argument => Argument.None; + + /// + public override Type ValueType => typeof(void); + private sealed class VersionOptionAction : SynchronousCommandLineAction { public override int Invoke(ParseResult parseResult)