From 9d557bd28311971ff63cbc85bf4ffc0f0882a86b Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 26 Jun 2025 16:57:29 -0700 Subject: [PATCH 1/8] include Microsoft.NET.Test.Sdk when Arcade is disabled --- Directory.Packages.props | 7 ++----- .../System.CommandLine.Tests.csproj | 7 +++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 66cde42f3c..3613334de5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,12 +1,10 @@ - true false $(NoWarn);NU1507 - @@ -27,11 +25,10 @@ - + - - + \ No newline at end of file diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 9aaa6f4b20..84d5e4cbe1 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -37,13 +37,16 @@ + + + + - + From 84e446b461fbb3d28a76c9c8719c3bc0bdf68c90 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 26 Jun 2025 17:11:56 -0700 Subject: [PATCH 2/8] fix #2592, #2582, #2573 --- src/System.CommandLine.Tests/ArgumentTests.cs | 48 +++ .../Binding/TypeConversionTests.cs | 2 + .../DirectiveTests.cs | 5 +- .../GetValueByNameParserTests.cs | 26 ++ .../GlobalOptionTests.cs | 6 +- .../Help/HelpBuilderTests.Customization.cs | 1 - .../Help/HelpBuilderTests.cs | 35 ++- src/System.CommandLine.Tests/OptionTests.cs | 78 ++++- src/System.CommandLine.Tests/ParserTests.cs | 51 ++-- .../ResponseFileTests.cs | 12 +- .../VersionOptionTests.cs | 22 +- src/System.CommandLine/Argument{T}.cs | 24 +- .../Help/HelpBuilder.Default.cs | 30 +- src/System.CommandLine/Help/HelpBuilder.cs | 44 +-- .../Help/HelpBuilderExtensions.cs | 20 +- src/System.CommandLine/Help/HelpOption.cs | 6 +- .../Parsing/CommandResult.cs | 10 +- .../Parsing/OptionResult.cs | 1 - .../Parsing/ParseOperation.cs | 8 +- .../Parsing/SymbolResultTree.cs | 2 +- .../System.Runtime.CompilerServices/Range.cs | 278 ++++++++++++++++++ src/System.CommandLine/VersionOption.cs | 10 +- 22 files changed, 602 insertions(+), 117 deletions(-) create mode 100644 src/System.CommandLine/System.Runtime.CompilerServices/Range.cs 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/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{T}.cs b/src/System.CommandLine/Argument{T}.cs index 42263f819e..719e5fd64e 100644 --- a/src/System.CommandLine/Argument{T}.cs +++ b/src/System.CommandLine/Argument{T}.cs @@ -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. @@ -76,6 +93,11 @@ public Argument(string name) : base(name) { if (DefaultValueFactory is null) { + if (IsBoolean()) + { + return false; + } + throw new InvalidOperationException($"Argument \"{Name}\" does not have a default value"); } 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..bc84661096 100644 --- a/src/System.CommandLine/Help/HelpBuilderExtensions.cs +++ b/src/System.CommandLine/Help/HelpBuilderExtensions.cs @@ -30,21 +30,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..706d2e2f9d 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,7 +30,7 @@ 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, new Argument(name) { Arity = ArgumentArity.Zero }) { Recursive = true; Description = LocalizationResources.HelpOptionDescription(); 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.Runtime.CompilerServices/Range.cs b/src/System.CommandLine/System.Runtime.CompilerServices/Range.cs new file mode 100644 index 0000000000..01342202b7 --- /dev/null +++ b/src/System.CommandLine/System.Runtime.CompilerServices/Range.cs @@ -0,0 +1,278 @@ +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Index.cs +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Range.cs + +#if NETSTANDARD2_0 +#nullable enable + +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + internal readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + else + { + return _value; + } + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } + } + + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + internal readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return Start.GetHashCode() * 31 + End.GetHashCode(); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return Start + ".." + End; + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start; + var startIndex = Start; + if (startIndex.IsFromEnd) + start = length - startIndex.Value; + else + start = startIndex.Value; + + int end; + var endIndex = End; + if (endIndex.IsFromEnd) + end = length - endIndex.Value; + else + end = endIndex.Value; + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } + } +} + +namespace System.Runtime.CompilerServices +{ + internal static class RuntimeHelpers + { + /// + /// Slices the specified array using the specified range. + /// + public static T[] GetSubArray(T[] array, Range range) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + (int offset, int length) = range.GetOffsetAndLength(array.Length); + + if (default(T) != null || typeof(T[]) == array.GetType()) + { + // We know the type of the array to be exactly T[]. + + if (length == 0) + { + return Array.Empty(); + } + + var dest = new T[length]; + Array.Copy(array, offset, dest, 0, length); + return dest; + } + else + { + // The array is actually a U[] where U:T. + var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); + Array.Copy(array, offset, dest, 0, length); + return dest; + } + } + } +} +#endif diff --git a/src/System.CommandLine/VersionOption.cs b/src/System.CommandLine/VersionOption.cs index 9ba607198b..7873eea7ac 100644 --- a/src/System.CommandLine/VersionOption.cs +++ b/src/System.CommandLine/VersionOption.cs @@ -10,14 +10,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,7 +25,7 @@ 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, new Argument("--version") { Arity = ArgumentArity.Zero }) { Description = LocalizationResources.VersionOptionDescription(); AddValidators(); @@ -43,7 +43,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)); } From a55108c5f4ccff75e7a6da69b1a8eb1cddfc36ef Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Fri, 27 Jun 2025 12:12:51 -0700 Subject: [PATCH 3/8] remove dead code --- src/System.CommandLine/Argument{T}.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/System.CommandLine/Argument{T}.cs b/src/System.CommandLine/Argument{T}.cs index 719e5fd64e..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; @@ -93,11 +93,6 @@ public Func? DefaultValueFactory { if (DefaultValueFactory is null) { - if (IsBoolean()) - { - return false; - } - throw new InvalidOperationException($"Argument \"{Name}\" does not have a default value"); } From e2bb3e88f29405b0315829d149e9377d4bc84e28 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Fri, 27 Jun 2025 12:19:00 -0700 Subject: [PATCH 4/8] model HelpOption and VersionOption without a generic type parameter --- src/System.CommandLine/Argument.cs | 15 +++++++++++++++ src/System.CommandLine/Help/HelpOption.cs | 9 +++++++-- src/System.CommandLine/VersionOption.cs | 11 +++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) 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/Help/HelpOption.cs b/src/System.CommandLine/Help/HelpOption.cs index 706d2e2f9d..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; @@ -30,10 +30,11 @@ public HelpOption() : this("--help", ["-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/VersionOption.cs b/src/System.CommandLine/VersionOption.cs index 7873eea7ac..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,7 +11,7 @@ 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; @@ -25,10 +26,11 @@ public VersionOption() : this("--version") /// 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; } /// @@ -54,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) From 5bf2a46a48e389881e013f18d1f0df3dc1ade277 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Sun, 29 Jun 2025 11:45:49 -0700 Subject: [PATCH 5/8] remove $(NetFrameworkCurrent) from test project --- src/System.CommandLine.Tests/System.CommandLine.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 84d5e4cbe1..86bdfa95bc 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -1,7 +1,7 @@  - $(TargetFrameworkForNETSDK);$(NetFrameworkCurrent) + $(TargetFrameworkForNETSDK) false $(DefaultExcludesInProjectFolder);TestApps\** $(NoWarn);CS8632 From 42c78bfd59bb469561d412f2ec6a814fb77c32c2 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Sun, 29 Jun 2025 11:53:25 -0700 Subject: [PATCH 6/8] update API baseline --- ...Tests.System_CommandLine_api_is_not_changed.approved.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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) From ba023b13ae0eb22318dadba8efd4e263af416f1b Mon Sep 17 00:00:00 2001 From: Viktor Hofer Date: Mon, 30 Jun 2025 12:12:52 +0200 Subject: [PATCH 7/8] Bring netfx test tfm back --- Directory.Packages.props | 5 +++++ src/System.CommandLine.Tests/System.CommandLine.Tests.csproj | 3 ++- src/System.CommandLine/Help/HelpBuilderExtensions.cs | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3613334de5..a12cbb670d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,10 +1,12 @@ + true false $(NoWarn);NU1507 + @@ -12,6 +14,7 @@ + @@ -25,10 +28,12 @@ + + \ No newline at end of file diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 86bdfa95bc..e8f890f838 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -1,7 +1,7 @@  - $(TargetFrameworkForNETSDK) + $(TargetFrameworkForNETSDK);$(NetFrameworkCurrent) false $(DefaultExcludesInProjectFolder);TestApps\** $(NoWarn);CS8632 @@ -42,6 +42,7 @@ + diff --git a/src/System.CommandLine/Help/HelpBuilderExtensions.cs b/src/System.CommandLine/Help/HelpBuilderExtensions.cs index bc84661096..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 From 86658c7cdd684904cc3f4156119bd2cb61249405 Mon Sep 17 00:00:00 2001 From: Viktor Hofer Date: Mon, 30 Jun 2025 12:57:32 +0200 Subject: [PATCH 8/8] More work --- CONTRIBUTING.md | 4 +- .../System.CommandLine.Tests.csproj | 10 +- .../System.CommandLine.csproj | 8 +- .../System.Runtime.CompilerServices/Range.cs | 278 ------------------ 4 files changed, 11 insertions(+), 289 deletions(-) delete mode 100644 src/System.CommandLine/System.Runtime.CompilerServices/Range.cs 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/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 7714dcc70a..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 @@ - + - + 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/System.Runtime.CompilerServices/Range.cs b/src/System.CommandLine/System.Runtime.CompilerServices/Range.cs deleted file mode 100644 index 01342202b7..0000000000 --- a/src/System.CommandLine/System.Runtime.CompilerServices/Range.cs +++ /dev/null @@ -1,278 +0,0 @@ -// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Index.cs -// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Range.cs - -#if NETSTANDARD2_0 -#nullable enable - -using System.Runtime.CompilerServices; - -namespace System -{ - /// Represent a type can be used to index a collection either from the start or the end. - /// - /// Index is used by the C# compiler to support the new index syntax - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; - /// int lastElement = someArray[^1]; // lastElement = 5 - /// - /// - internal readonly struct Index : IEquatable - { - private readonly int _value; - - /// Construct an Index using a value and indicating if the index is from the start or from the end. - /// The index value. it has to be zero or positive number. - /// Indicating if the index is from the start or from the end. - /// - /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Index(int value, bool fromEnd = false) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - if (fromEnd) - _value = ~value; - else - _value = value; - } - - // The following private constructors mainly created for perf reason to avoid the checks - private Index(int value) - { - _value = value; - } - - /// Create an Index pointing at first element. - public static Index Start => new Index(0); - - /// Create an Index pointing at beyond last element. - public static Index End => new Index(~0); - - /// Create an Index from the start at the position indicated by the value. - /// The index value from the start. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromStart(int value) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - return new Index(value); - } - - /// Create an Index from the end at the position indicated by the value. - /// The index value from the end. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromEnd(int value) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - return new Index(~value); - } - - /// Returns the index value. - public int Value - { - get - { - if (_value < 0) - { - return ~_value; - } - else - { - return _value; - } - } - } - - /// Indicates whether the index is from the start or the end. - public bool IsFromEnd => _value < 0; - - /// Calculate the offset from the start using the giving collection length. - /// The length of the collection that the Index will be used with. length has to be a positive value - /// - /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. - /// we don't validate either the returned offset is greater than the input length. - /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and - /// then used to index a collection will get out of range exception which will be same affect as the validation. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOffset(int length) - { - var offset = _value; - if (IsFromEnd) - { - // offset = length - (~value) - // offset = length + (~(~value) + 1) - // offset = length + value + 1 - - offset += length + 1; - } - return offset; - } - - /// Indicates whether the current Index object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; - - /// Indicates whether the current Index object is equal to another Index object. - /// An object to compare with this object - public bool Equals(Index other) => _value == other._value; - - /// Returns the hash code for this instance. - public override int GetHashCode() => _value; - - /// Converts integer number to an Index. - public static implicit operator Index(int value) => FromStart(value); - - /// Converts the value of the current Index object to its equivalent string representation. - public override string ToString() - { - if (IsFromEnd) - return "^" + ((uint)Value).ToString(); - - return ((uint)Value).ToString(); - } - } - - /// Represent a range has start and end indexes. - /// - /// Range is used by the C# compiler to support the range syntax. - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; - /// int[] subArray1 = someArray[0..2]; // { 1, 2 } - /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } - /// - /// - internal readonly struct Range : IEquatable - { - /// Represent the inclusive start index of the Range. - public Index Start { get; } - - /// Represent the exclusive end index of the Range. - public Index End { get; } - - /// Construct a Range object using the start and end indexes. - /// Represent the inclusive start index of the range. - /// Represent the exclusive end index of the range. - public Range(Index start, Index end) - { - Start = start; - End = end; - } - - /// Indicates whether the current Range object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => - value is Range r && - r.Start.Equals(Start) && - r.End.Equals(End); - - /// Indicates whether the current Range object is equal to another Range object. - /// An object to compare with this object - public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); - - /// Returns the hash code for this instance. - public override int GetHashCode() - { - return Start.GetHashCode() * 31 + End.GetHashCode(); - } - - /// Converts the value of the current Range object to its equivalent string representation. - public override string ToString() - { - return Start + ".." + End; - } - - /// Create a Range object starting from start index to the end of the collection. - public static Range StartAt(Index start) => new Range(start, Index.End); - - /// Create a Range object starting from first element in the collection to the end Index. - public static Range EndAt(Index end) => new Range(Index.Start, end); - - /// Create a Range object starting from first element to the end. - public static Range All => new Range(Index.Start, Index.End); - - /// Calculate the start offset and length of range object using a collection length. - /// The length of the collection that the range will be used with. length has to be a positive value. - /// - /// For performance reason, we don't validate the input length parameter against negative values. - /// It is expected Range will be used with collections which always have non negative length/count. - /// We validate the range is inside the length scope though. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int Offset, int Length) GetOffsetAndLength(int length) - { - int start; - var startIndex = Start; - if (startIndex.IsFromEnd) - start = length - startIndex.Value; - else - start = startIndex.Value; - - int end; - var endIndex = End; - if (endIndex.IsFromEnd) - end = length - endIndex.Value; - else - end = endIndex.Value; - - if ((uint)end > (uint)length || (uint)start > (uint)end) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return (start, end - start); - } - } -} - -namespace System.Runtime.CompilerServices -{ - internal static class RuntimeHelpers - { - /// - /// Slices the specified array using the specified range. - /// - public static T[] GetSubArray(T[] array, Range range) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - (int offset, int length) = range.GetOffsetAndLength(array.Length); - - if (default(T) != null || typeof(T[]) == array.GetType()) - { - // We know the type of the array to be exactly T[]. - - if (length == 0) - { - return Array.Empty(); - } - - var dest = new T[length]; - Array.Copy(array, offset, dest, 0, length); - return dest; - } - else - { - // The array is actually a U[] where U:T. - var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); - Array.Copy(array, offset, dest, 0, length); - return dest; - } - } - } -} -#endif pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy