From 4ed6f8e6a47d15b7570af2c049ef3cc1801ccea7 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 27 May 2025 21:24:33 +0200 Subject: [PATCH] add GetRequiredValue to avoid null checks for required options and arguments --- ...ommandLine_api_is_not_changed.approved.txt | 6 ++++ src/System.CommandLine.Tests/OptionTests.cs | 22 ++++++++++-- src/System.CommandLine.Tests/ParserTests.cs | 36 +++++++++++++++++++ src/System.CommandLine/ParseResult.cs | 29 +++++++++++++++ .../Parsing/SymbolResult.cs | 30 ++++++++++++++++ 5 files changed, 121 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 e16bab36bb..e063e542d7 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 @@ -123,6 +123,9 @@ public System.Collections.Generic.IReadOnlyList UnmatchedTokens { get; } public System.CommandLine.Completions.CompletionContext GetCompletionContext() public System.Collections.Generic.IEnumerable GetCompletions(System.Nullable position = null) + public T GetRequiredValue(Argument argument) + public T GetRequiredValue(Option option) + public T GetRequiredValue(System.String name) public System.CommandLine.Parsing.ArgumentResult GetResult(Argument argument) public System.CommandLine.Parsing.CommandResult GetResult(Command command) public System.CommandLine.Parsing.OptionResult GetResult(Option option) @@ -269,6 +272,9 @@ System.CommandLine.Parsing public SymbolResult Parent { get; } public System.Collections.Generic.IReadOnlyList Tokens { get; } public System.Void AddError(System.String errorMessage) + public T GetRequiredValue(Argument argument) + public T GetRequiredValue(Option option) + public T GetRequiredValue(System.String name) public ArgumentResult GetResult(System.CommandLine.Argument argument) public CommandResult GetResult(System.CommandLine.Command command) public OptionResult GetResult(System.CommandLine.Option option) diff --git a/src/System.CommandLine.Tests/OptionTests.cs b/src/System.CommandLine.Tests/OptionTests.cs index ee47e5e563..7f34e9d3dc 100644 --- a/src/System.CommandLine.Tests/OptionTests.cs +++ b/src/System.CommandLine.Tests/OptionTests.cs @@ -215,8 +215,14 @@ public void When_options_use_different_prefixes_they_still_work(string prefix) var result = rootCommand.Parse(prefix + "c value-for-c " + prefix + "a value-for-a"); result.GetValue(optionA).Should().Be("value-for-a"); + result.GetRequiredValue(optionA).Should().Be("value-for-a"); + result.GetRequiredValue(optionA.Name).Should().Be("value-for-a"); result.GetResult(optionB).Should().BeNull(); + result.Invoking(result => result.GetRequiredValue(optionB)).Should().Throw(); + result.Invoking(result => result.GetRequiredValue(optionB.Name)).Should().Throw(); result.GetValue(optionC).Should().Be("value-for-c"); + result.GetRequiredValue(optionC).Should().Be("value-for-c"); + result.GetRequiredValue(optionC.Name).Should().Be("value-for-c"); } [Fact] @@ -243,12 +249,22 @@ public void Option_T_default_value_can_be_set_after_instantiation() DefaultValueFactory = (_) => 123 }; - new RootCommand { option } + var result = new RootCommand { option } .Parse("") - .GetResult(option) + .GetResult(option); + + result .GetValueOrDefault() .Should() .Be(123); + + result.GetRequiredValue(option) + .Should() + .Be(123); + + result.GetRequiredValue(option.Name) + .Should() + .Be(123); } [Fact] @@ -390,6 +406,8 @@ public void Multiple_identifier_token_instances_without_argument_tokens_can_be_p var result = root.Parse("-v -v -v"); result.GetValue(option).Should().BeTrue(); + result.GetRequiredValue(option).Should().BeTrue(); + result.GetRequiredValue(option.Name).Should().BeTrue(); } [Fact] diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 63c71d4580..e9c9fd709e 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -826,6 +826,33 @@ public void Commands_can_have_default_argument_values() GetValue(result, argument) .Should() .Be("default"); + + result.GetRequiredValue(argument) + .Should() + .Be("default"); + } + + [Fact] + public void GetRequiredValue_throws_when_argument_without_default_value_was_not_provided() + { + Argument argument = new("the-arg"); + Option option = new("--option"); + + Command command = new("command") + { + argument, + option + }; + + ParseResult result = command.Parse("command --option"); + + result.Invoking(result => result.GetRequiredValue(argument)) + .Should() + .Throw(); + + result.Invoking(result => result.GetRequiredValue(argument.Name)) + .Should() + .Throw(); } [Fact] @@ -925,6 +952,11 @@ public void Command_default_argument_value_does_not_override_parsed_value() .Name .Should() .Be("the-directory"); + + result.GetRequiredValue(argument) + .Name + .Should() + .Be("the-directory"); } [Fact] @@ -1160,6 +1192,10 @@ public void Arguments_can_match_subcommands() GetValue(result, argument) .Should() .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); + + result.GetRequiredValue(argument) + .Should() + .BeEquivalentSequenceTo("one", "two", "three", "subcommand", "four"); } [Theory] diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 92b209f93a..27d83bf27e 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -133,6 +133,35 @@ CommandLineText is null public T? GetValue(string name) => RootCommandResult.GetValue(name); + /// + /// Gets the parsed or default value for the specified required argument or throws. + /// + /// The argument for which to get a value. + /// The parsed value or a configured default. + /// Thrown when required argument was not parsed or has no default value configured. + public T GetRequiredValue(Argument argument) + => RootCommandResult.GetRequiredValue(argument); + + /// + /// Gets the parsed or default value for the specified required option or throws. + /// + /// The option for which to get a value. + /// The parsed value or a configured default. + /// Thrown when required option was not parsed or has no default value configured. + public T GetRequiredValue(Option option) + => RootCommandResult.GetRequiredValue(option); + + /// + /// Gets the parsed or default value for the specified required symbol name, in the context of parsed command (not entire symbol tree). + /// + /// The name of the required Symbol for which to get a value. + /// The parsed value or a configured default. + /// Thrown when parsing resulted in parse error(s) or required symbol was not parsed or has no default value configured. + /// Thrown when there was no symbol defined for given name for the parsed command. + /// Thrown when parsed result can not be cast to . + public T GetRequiredValue(string name) + => RootCommandResult.GetRequiredValue(name); + /// public override string ToString() => ParseDiagramAction.Diagram(this).ToString(); diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 18c9f1908d..aed8a3ac4b 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -122,6 +122,22 @@ public IEnumerable Errors return Argument.CreateDefaultValue(); } + /// + public T GetRequiredValue(Argument argument) + => GetResult(argument) switch + { + ArgumentResult argumentResult => argumentResult.GetValueOrDefault(), + null => throw new InvalidOperationException($"{argument.Name} is required but was not provided."), + }; + + /// + public T GetRequiredValue(Option option) + => GetResult(option) switch + { + OptionResult optionResult => optionResult.GetValueOrDefault(), + null => throw new InvalidOperationException($"{option.Name} is required but was not provided."), + }; + /// /// Gets the value for a symbol having the specified name anywhere in the parse tree. /// @@ -147,6 +163,20 @@ public IEnumerable Errors return Argument.CreateDefaultValue(); } + /// + /// Gets the value for a symbol having the specified name anywhere in the parse tree. + /// + /// The name of the symbol for which to find a result. + /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. + public T GetRequiredValue(string name) + => GetResult(name) switch + { + OptionResult optionResult => optionResult.GetValueOrDefault(), + ArgumentResult argumentResult => argumentResult.GetValueOrDefault(), + SymbolResult _ => throw new InvalidOperationException($"{name} is not an option or argument."), + _ => throw new InvalidOperationException($"{name} is required but was not provided."), + }; + internal virtual bool UseDefaultValueFor(ArgumentResult argumentResult) => false; } }