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