From 3f4c3d9f552d60b7fdca3abdece2bb06393694ee Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Mon, 30 Jun 2025 14:12:07 -0700 Subject: [PATCH 01/16] restore System.CommandLine.v3.ncrunchsolution --- System.CommandLine.v3.ncrunchsolution | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 System.CommandLine.v3.ncrunchsolution diff --git a/System.CommandLine.v3.ncrunchsolution b/System.CommandLine.v3.ncrunchsolution new file mode 100644 index 0000000000..ed49d122c2 --- /dev/null +++ b/System.CommandLine.v3.ncrunchsolution @@ -0,0 +1,12 @@ + + + True + + TargetFrameworks = net8.0 + TargetFramework = net8.0 + + False + True + True + + \ No newline at end of file From 0d98d42fb7da6fe65bf579752e68479e031f0650 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Mon, 30 Jun 2025 17:34:31 -0700 Subject: [PATCH 02/16] split out configuration types for Parse vs Invoke --- .../SuggestionDispatcher.cs | 11 +- .../CommandLineConfigurationTests.cs | 33 ---- ...CommandLineInvocationConfigurationTests.cs | 40 +++++ .../CompletionTests.cs | 12 +- .../DirectiveTests.cs | 17 +- .../EnvironmentVariableDirectiveTests.cs | 28 ++- .../Help/CustomHelpAction.cs | 2 +- .../Help/HelpBuilderTests.Customization.cs | 125 +++++-------- .../HelpOptionTests.cs | 63 +++---- .../CancelOnProcessTerminationTests.cs | 6 +- .../Invocation/InvocationTests.cs | 21 +-- .../ParseDirectiveTests.cs | 9 +- .../ParseErrorReportingTests.cs | 19 +- .../SuggestDirectiveTests.cs | 167 ++++++++---------- .../UseExceptionHandlerTests.cs | 41 ++--- .../VersionOptionTests.cs | 125 +++++-------- .../CommandLineConfiguration.cs | 63 +------ .../CommandLineInvocationConfiguration.cs | 45 +++++ .../Completions/CompletionAction.cs | 4 +- src/System.CommandLine/Help/HelpAction.cs | 4 +- .../Invocation/InvocationPipeline.cs | 26 +-- .../Invocation/ParseErrorAction.cs | 7 +- src/System.CommandLine/ParseResult.cs | 35 +++- src/System.CommandLine/VersionOption.cs | 2 +- 24 files changed, 407 insertions(+), 498 deletions(-) create mode 100644 src/System.CommandLine.Tests/CommandLineInvocationConfigurationTests.cs create mode 100644 src/System.CommandLine/CommandLineInvocationConfiguration.cs diff --git a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs index 512712a41f..633989f93e 100644 --- a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs +++ b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.CommandLine.Completions; namespace System.CommandLine.Suggest { @@ -14,6 +13,8 @@ public class SuggestionDispatcher { private readonly ISuggestionRegistration _suggestionRegistration; private readonly ISuggestionStore _suggestionStore; + private readonly RootCommand _rootCommand; + private readonly CommandLineInvocationConfiguration _invocationConfig; public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISuggestionStore suggestionStore = null) { @@ -63,15 +64,15 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug return Task.CompletedTask; }); - var root = new RootCommand + _rootCommand = new RootCommand { ListCommand, GetCommand, RegisterCommand, CompleteScriptCommand, }; - root.TreatUnmatchedTokensAsErrors = false; - Configuration = new CommandLineConfiguration(root); + _rootCommand.TreatUnmatchedTokensAsErrors = false; + Configuration = new CommandLineConfiguration(_rootCommand); } private Command CompleteScriptCommand { get; } @@ -102,7 +103,7 @@ private static Option GetExecutableOption() public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(5000); - public Task InvokeAsync(string[] args) => Configuration.InvokeAsync(args); + public Task InvokeAsync(string[] args) => Configuration.RootCommand.Parse(args, Configuration).InvokeAsync(_invocationConfig); private void Register( string commandPath, diff --git a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs b/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs index c0587785ea..63cb3d07f0 100644 --- a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs +++ b/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs @@ -260,37 +260,4 @@ public void ThrowIfInvalid_throws_if_a_parentage_cycle_is_detected() .Should() .Be($"Cycle detected in command tree. Command '{rootCommand.Name}' is its own ancestor."); } - - [Fact] - public void It_can_be_subclassed_to_provide_additional_context() - { - var command = new RootCommand(); - var commandWasInvoked = false; - command.SetAction(parseResult => - { - var appConfig = (CustomAppConfiguration)parseResult.Configuration; - - // access custom config - - commandWasInvoked = true; - - return 0; - }); - - var config = new CustomAppConfiguration(command); - - config.Invoke(""); - - commandWasInvoked.Should().BeTrue(); - } -} - -public class CustomAppConfiguration : CommandLineConfiguration -{ - public CustomAppConfiguration(RootCommand command) : base(command) - { - EnableDefaultExceptionHandler = false; - } - - public IServiceProvider ServiceProvider { get; } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/CommandLineInvocationConfigurationTests.cs b/src/System.CommandLine.Tests/CommandLineInvocationConfigurationTests.cs new file mode 100644 index 0000000000..7d3f331c02 --- /dev/null +++ b/src/System.CommandLine.Tests/CommandLineInvocationConfigurationTests.cs @@ -0,0 +1,40 @@ +using FluentAssertions; +using Xunit; + +namespace System.CommandLine.Tests; + +public class CommandLineInvocationConfigurationTests +{ + [Fact] + public void It_can_be_subclassed_to_provide_additional_context() + { + var command = new RootCommand(); + var commandWasInvoked = false; + command.SetAction(parseResult => + { + var appConfig = (CustomAppConfiguration)parseResult.InvocationConfiguration; + + // access custom config + + commandWasInvoked = true; + + return 0; + }); + + var config = new CustomAppConfiguration(); + + command.Parse("").Invoke(config); + + commandWasInvoked.Should().BeTrue(); + } + + public class CustomAppConfiguration : CommandLineInvocationConfiguration + { + public CustomAppConfiguration() + { + EnableDefaultExceptionHandler = false; + } + + public IServiceProvider ServiceProvider { get; } + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Tests/CompletionTests.cs b/src/System.CommandLine.Tests/CompletionTests.cs index 84d931f863..a95a982724 100644 --- a/src/System.CommandLine.Tests/CompletionTests.cs +++ b/src/System.CommandLine.Tests/CompletionTests.cs @@ -851,13 +851,13 @@ public void Options_that_have_been_specified_to_their_maximum_arity_are_not_sugg [Fact] public void When_current_symbol_is_an_option_that_requires_arguments_then_parent_symbol_completions_are_omitted() { - var configuration = new CommandLineConfiguration(new RootCommand - { - new Option("--allows-one"), - new Option("--allows-many") - }); + var rootCommand = new RootCommand + { + new Option("--allows-one"), + new Option("--allows-many") + }; - var completions = configuration.Parse("--allows-one ").GetCompletions(); + var completions = rootCommand.Parse("--allows-one ").GetCompletions(); completions.Should().BeEmpty(); } diff --git a/src/System.CommandLine.Tests/DirectiveTests.cs b/src/System.CommandLine.Tests/DirectiveTests.cs index 9d2851726d..6a7432fffd 100644 --- a/src/System.CommandLine.Tests/DirectiveTests.cs +++ b/src/System.CommandLine.Tests/DirectiveTests.cs @@ -77,21 +77,21 @@ public async Task Multiple_instances_of_the_same_directive_can_be_invoked(bool i : new SynchronousTestAction(incrementCallCount, terminating: false) }; - var config = new CommandLineConfiguration(new RootCommand + var rootCommand = new RootCommand { Action = invokeAsync ? new AsynchronousTestAction(verifyActionWasCalled, terminating: false) : new SynchronousTestAction(verifyActionWasCalled, terminating: false), Directives = { testDirective } - }); + }; if (invokeAsync) { - await config.InvokeAsync("[test:1] [test:2]"); + await rootCommand.Parse("[test:1] [test:2]").InvokeAsync(); } else { - config.Invoke("[test:1] [test:2]"); + rootCommand.Parse("[test:1] [test:2]").Invoke(); } using var _ = new AssertionScope(); @@ -117,18 +117,19 @@ public async Task Multiple_different_directives_can_be_invoked(bool invokeAsync) { Action = new SynchronousTestAction(_ => directiveTwoActionWasCalled = true, terminating: false) }; - var config = new CommandLineConfiguration(new RootCommand + + var rootCommand = new RootCommand { Action = new SynchronousTestAction(_ => commandActionWasCalled = true, terminating: false), Directives = { directiveOne, directiveTwo } - }); + }; if (invokeAsync) { - await config.InvokeAsync("[one] [two]"); + await rootCommand.Parse("[one] [two]").InvokeAsync(); } else { - config.Invoke("[one] [two]"); + rootCommand.Parse("[one] [two]").Invoke(); } using var _ = new AssertionScope(); diff --git a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs index 47ee79f8ce..00638bafeb 100644 --- a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs +++ b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs @@ -1,4 +1,5 @@ -using System.CommandLine.Help; +using System.Collections; +using System.CommandLine.Help; using System.CommandLine.Invocation; using FluentAssertions; using System.Linq; @@ -28,12 +29,12 @@ public async Task Sets_environment_variable_to_value() Environment.GetEnvironmentVariable(_testVariableName).Should().Be(value); }); - var config = new CommandLineConfiguration(rootCommand) + var config = new CommandLineInvocationConfiguration { EnableDefaultExceptionHandler = false }; - await config.InvokeAsync($"[env:{_testVariableName}={value}]"); + await rootCommand.Parse($"[env:{_testVariableName}={value}]").InvokeAsync(config); asserted.Should().BeTrue(); } @@ -53,12 +54,12 @@ public async Task Sets_environment_variable_value_containing_equals_sign() Environment.GetEnvironmentVariable(_testVariableName).Should().Be(value); }); - var config = new CommandLineConfiguration(rootCommand) + var config = new CommandLineInvocationConfiguration { EnableDefaultExceptionHandler = false }; - await config.InvokeAsync($"[env:{_testVariableName}={value}]" ); + await rootCommand.Parse($"[env:{_testVariableName}={value}]" ).InvokeAsync(config); asserted.Should().BeTrue(); } @@ -78,12 +79,12 @@ public async Task Ignores_environment_directive_without_equals_sign() Environment.GetEnvironmentVariable(variable).Should().BeNull(); }); - var config = new CommandLineConfiguration(rootCommand) + var config = new CommandLineInvocationConfiguration { EnableDefaultExceptionHandler = false }; - await config.InvokeAsync( $"[env:{variable}]" ); + await rootCommand.Parse( $"[env:{variable}]" ).InvokeAsync(config); asserted.Should().BeTrue(); } @@ -97,23 +98,20 @@ public static async Task Ignores_environment_directive_with_empty_variable_name( { new EnvironmentVariablesDirective() }; + + IDictionary env = null; rootCommand.SetAction(_ => { asserted = true; - var env = Environment.GetEnvironmentVariables(); - env.Values.Cast().Should().NotContain(value); + env = Environment.GetEnvironmentVariables(); }); - var config = new CommandLineConfiguration(rootCommand) - { - EnableDefaultExceptionHandler = false - }; - - var result = config.Parse($"[env:={value}]"); + var result = rootCommand.Parse($"[env:={value}]"); await result.InvokeAsync(); asserted.Should().BeTrue(); + env.Values.Cast().Should().NotContain(value); } [Fact] diff --git a/src/System.CommandLine.Tests/Help/CustomHelpAction.cs b/src/System.CommandLine.Tests/Help/CustomHelpAction.cs index cac66dc599..087c6c97f3 100644 --- a/src/System.CommandLine.Tests/Help/CustomHelpAction.cs +++ b/src/System.CommandLine.Tests/Help/CustomHelpAction.cs @@ -21,7 +21,7 @@ internal HelpBuilder Builder /// public override int Invoke(ParseResult parseResult) { - var output = parseResult.Configuration.Output; + var output = parseResult.InvocationConfiguration?.Output ?? parseResult.Configuration.Output; var helpContext = new HelpContext(Builder, parseResult.CommandResult.Command, diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs index 22cced02a0..e45521f28c 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs @@ -1,11 +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 FluentAssertions; +using Microsoft.VisualStudio.TestPlatform.Utilities; using System.Collections.Generic; using System.CommandLine.Help; using System.IO; using System.Linq; -using FluentAssertions; using Xunit; using static System.Environment; @@ -92,26 +93,22 @@ public void Option_can_customize_first_column_text_based_on_parse_result() ctx.Command.Equals(commandA) ? optionAFirstColumnText : optionBFirstColumnText); - command.Options.Add(new HelpOption() + command.Options.Add(new HelpOption { - Action = new CustomHelpAction() + Action = new CustomHelpAction { Builder = helpBuilder } }); - var console = new StringWriter(); - var config = new CommandLineConfiguration(command) - { - Output = console - }; - command.Parse("root a -h", config).Invoke(); - console.ToString().Should().Contain(optionAFirstColumnText); + var output = new StringWriter(); + + command.Parse("root a -h").Invoke(new() { Output = output }); + output.ToString().Should().Contain(optionAFirstColumnText); - console = new StringWriter(); - config.Output = console; - command.Parse("root b -h", config).Invoke(); - console.ToString().Should().Contain(optionBFirstColumnText); + output = new StringWriter(); + command.Parse("root b -h").Invoke(new() { Output = output }); + output.ToString().Should().Contain(optionBFirstColumnText); } [Fact] @@ -146,17 +143,14 @@ public void Option_can_customize_second_column_text_based_on_parse_result() } }); - var config = new CommandLineConfiguration(command) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - config.Invoke("root a -h"); - config.Output.ToString().Should().Contain($"option {optionADescription}"); + command.Parse("root a -h").Invoke(new(){Output=output}); + output.ToString().Should().Contain($"option {optionADescription}"); - config.Output = new StringWriter(); - config.Invoke("root b -h"); - config.Output.ToString().Should().Contain($"option {optionBDescription}"); + output = new StringWriter(); + command.Parse("root b -h").Invoke(new(){Output=output}); + output.ToString().Should().Contain($"option {optionBDescription}"); } [Fact] @@ -275,11 +269,9 @@ public void Option_can_fallback_to_default_when_customizing(bool conditionA, boo } }); - CommandLineConfiguration config = new (command); - var console = new StringWriter(); - config.Output = console; - command.Parse("test -h", config).Invoke(); - console.ToString().Should().MatchRegex(expected); + var output = new StringWriter(); + command.Parse("test -h").Invoke(new() { Output = output }); + output.ToString().Should().MatchRegex(expected); } [Theory] @@ -312,9 +304,6 @@ public void Argument_can_fallback_to_default_when_customizing( secondColumnText: ctx => conditionB ? "custom 2nd" : HelpBuilder.Default.GetArgumentDescription(argument), defaultValue: ctx => conditionC ? "custom def" : HelpBuilder.Default.GetArgumentDefaultValue(argument)); - - CommandLineConfiguration config = new (command); - command.Options.Add(new HelpOption { Action = new CustomHelpAction @@ -323,9 +312,9 @@ public void Argument_can_fallback_to_default_when_customizing( } }); - config.Output = new StringWriter(); - command.Parse("test -h", config).Invoke(); - config.Output.ToString().Should().MatchRegex(expected); + var output = new StringWriter(); + command.Parse("test -h").Invoke(new() { Output = output }); + output.ToString().Should().MatchRegex(expected); } [Fact] @@ -351,17 +340,13 @@ public void Individual_symbols_can_be_customized() } }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - ParseResult parseResult = rootCommand.Parse("-h", config); + ParseResult parseResult = rootCommand.Parse("-h"); - parseResult.Invoke(); + parseResult.Invoke(new() { Output = output }); - config.Output - .ToString() + output.ToString() .Should() .ContainAll("The custom command description", "The custom option description", @@ -374,16 +359,16 @@ public void Help_sections_can_be_replaced() CustomHelpAction helpAction = new(); helpAction.Builder.CustomizeLayout(CustomLayout); - CommandLineConfiguration config = new(new Command("name") { new HelpOption() { Action = helpAction} }) + var command = new Command("name") { - Output = new StringWriter() + new HelpOption { Action = helpAction } }; - ParseResult parseResult = config.Parse("-h"); + var output = new StringWriter(); - parseResult.Invoke(); + command.Parse("-h").Invoke(new() { Output = output }); - config.Output.ToString().Should().Be($"one{NewLine}{NewLine}two{NewLine}{NewLine}three{NewLine}{NewLine}"); + output.ToString().Should().Be($"one{NewLine}{NewLine}two{NewLine}{NewLine}three{NewLine}{NewLine}"); IEnumerable> CustomLayout(HelpContext _) { @@ -399,22 +384,18 @@ public void Help_sections_can_be_supplemented() CustomHelpAction helpAction = new(); helpAction.Builder.CustomizeLayout(CustomLayout); - CommandLineConfiguration config = new(new Command("hello") { new HelpOption() { Action = helpAction } }) + var command = new Command("hello") { - Output = new StringWriter(), + new HelpOption { Action = helpAction } }; var defaultHelp = GetDefaultHelp(new Command("hello")); - ParseResult parseResult = config.Parse("-h"); - - parseResult.Invoke(); - - var output = config.Output.ToString(); + var output = new StringWriter(); - var expected = $"first{NewLine}{NewLine}{defaultHelp}{NewLine}last{NewLine}{NewLine}"; + command.Parse("-h").Invoke(new() { Output = output }); - output.Should().Be(expected); + output.ToString().Should().Be($"first{NewLine}{NewLine}{defaultHelp}{NewLine}last{NewLine}{NewLine}"); IEnumerable> CustomLayout(HelpContext _) { @@ -446,7 +427,6 @@ public void Layout_can_be_composed_dynamically_based_on_context() Builder = helpBuilder }; - var config = new CommandLineConfiguration(command); helpBuilder.CustomizeLayout(c => c.Command == commandWithTypicalHelp ? HelpBuilder.Default.GetLayout() @@ -454,12 +434,10 @@ public void Layout_can_be_composed_dynamically_based_on_context() .Concat(HelpBuilder.Default.GetLayout())); var typicalOutput = new StringWriter(); - config.Output = typicalOutput; - command.Parse("typical -h", config).Invoke(); + command.Parse("typical -h").Invoke(new() { Output = typicalOutput }); var customOutput = new StringWriter(); - config.Output = customOutput; - command.Parse("custom -h", config).Invoke(); + command.Parse("custom -h").Invoke(new() { Output = customOutput }); typicalOutput.ToString().Should().Be(GetDefaultHelp(commandWithTypicalHelp, false)); customOutput.ToString().Should().Be($"Custom layout!{NewLine}{NewLine}{GetDefaultHelp(commandWithCustomHelp, false)}"); @@ -484,15 +462,10 @@ public void Help_default_sections_can_be_wrapped() } }; - CommandLineConfiguration config = new(command) - { - Output = new StringWriter() - }; + var output = new StringWriter(); + command.Parse("test -h").Invoke(new() { Output = output }); - config.Invoke("test -h"); - - string result = config.Output.ToString(); - result.Should().Be( + output.ToString().Should().Be( $"Description:{NewLine}{NewLine}" + $"Usage:{NewLine} test [options]{NewLine}{NewLine}" + $"Options:{NewLine}" + @@ -510,16 +483,16 @@ public void Help_customized_sections_can_be_wrapped() helpAction.Builder = new HelpBuilder(10); helpAction.Builder.CustomizeLayout(CustomLayout); - CommandLineConfiguration config = new(new Command("name") { new HelpOption() { Action = helpAction } }) + var command = new Command("name") { - Output = new StringWriter() + new HelpOption { Action = helpAction } }; - ParseResult parseResult = config.Parse("-h"); + var output = new StringWriter(); - parseResult.Invoke(); + command.Parse("-h").Invoke(new() { Output = output }); - string result = config.Output.ToString(); + string result = output.ToString(); result.Should().Be($" 123 123{NewLine} 456 456{NewLine} 78 789{NewLine} 0{NewLine}{NewLine}"); IEnumerable> CustomLayout(HelpContext _) @@ -544,12 +517,12 @@ private string GetDefaultHelp(Command command, bool trimOneNewline = true) command.Options.Add(defaultHelp); } - CommandLineConfiguration config = new(command) + CommandLineInvocationConfiguration config = new() { Output = new StringWriter() }; - config.Invoke("-h"); + command.Parse("-h").Invoke(config); var output = config.Output.ToString(); diff --git a/src/System.CommandLine.Tests/HelpOptionTests.cs b/src/System.CommandLine.Tests/HelpOptionTests.cs index 401656ce0c..4fb8b9f8f3 100644 --- a/src/System.CommandLine.Tests/HelpOptionTests.cs +++ b/src/System.CommandLine.Tests/HelpOptionTests.cs @@ -2,10 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using FluentAssertions; +using Microsoft.VisualStudio.TestPlatform.Utilities; using System.CommandLine.Help; using System.CommandLine.Invocation; using System.CommandLine.Tests.Utility; using System.IO; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -24,18 +26,14 @@ public async Task Help_option_writes_help_for_the_specified_command() } }; - CommandLineConfiguration config = new(command) - { - Output = new StringWriter() - }; - - var result = command.Parse("command subcommand --help", config); + var result = command.Parse("command subcommand --help"); - await result.InvokeAsync(); + var output = new StringWriter(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().Contain($"{RootCommand.ExecutableName} command subcommand [options]"); + output.ToString().Should().Contain($"{RootCommand.ExecutableName} command subcommand [options]"); } - + [Fact] public async Task Help_option_interrupts_execution_of_the_specified_command() { @@ -62,17 +60,16 @@ public async Task Help_option_interrupts_execution_of_the_specified_command() [InlineData("/?")] public async Task Help_option_accepts_default_values(string value) { - CommandLineConfiguration config = new(new Command("command") { new HelpOption() }) + var command = new Command("command") { - Output = new StringWriter() + new HelpOption() }; - StringWriter console = new(); - config.Output = console; + StringWriter output = new(); - await config.InvokeAsync($"command {value}"); + await command.Parse($"command {value}").InvokeAsync(new() { Output = output }, CancellationToken.None); - console.ToString().Should().ShowHelp(); + output.ToString().Should().ShowHelp(); } [Fact] @@ -156,14 +153,11 @@ public async Task HelpOption_with_custom_aliases_uses_aliases(string helpAlias) { new HelpOption("/lost", "--confused") }; - CommandLineConfiguration config = new(command) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - await config.InvokeAsync(helpAlias); + await command.Parse(helpAlias).InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().ShowHelp(); + output.ToString().Should().ShowHelp(); } [Theory] @@ -178,14 +172,11 @@ public async Task Help_option_with_custom_aliases_does_not_recognize_default_ali command.Options.Clear(); command.Options.Add(new HelpOption("--confused")); - CommandLineConfiguration config = new(command) - { - Output = new StringWriter(), - }; + var output = new StringWriter(); - await config.InvokeAsync(helpAlias); + await command.Parse(helpAlias).InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().NotContain(helpAlias); + output.ToString().Should().NotContain(helpAlias); } [Theory] @@ -208,13 +199,9 @@ public void The_users_can_provide_usage_examples(bool subcommand) }); TextWriter output = new StringWriter(); - CommandLineConfiguration config = new(rootCommand) - { - Output = output - }; - - var result = subcommand ? config.Parse("subcommand -h") : config.Parse("-h"); - + + var result = subcommand ? rootCommand.Parse("subcommand -h") : rootCommand.Parse("-h"); + result.Configuration.Output = output; result.Invoke(); if (subcommand) @@ -242,13 +229,9 @@ public void The_users_can_print_help_output_of_a_subcommand() }; rootCommand.Subcommands.Add(subcommand); - TextWriter output = new StringWriter(); - CommandLineConfiguration config = new(subcommand) - { - Output = output - }; + var output = new StringWriter(); - subcommand.Parse("--help", config).Invoke(); + subcommand.Parse("--help").Invoke(new() { Output = output }); output.ToString().Should().Contain(SubcommandDescription); output.ToString().Should().NotContain(RootDescription); diff --git a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs index 656bba1424..e9ceb43521 100644 --- a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs @@ -56,10 +56,12 @@ private static Task Program(string[] args) }; command.Action = new CustomCliAction(); - return new CommandLineConfiguration(command) + var config = new CommandLineInvocationConfiguration { ProcessTerminationTimeout = TimeSpan.FromSeconds(2) - }.InvokeAsync(args); + }; + + return command.Parse(args).InvokeAsync(config); } private sealed class CustomCliAction : AsynchronousCommandLineAction diff --git a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs index b9cc930bef..058151adcc 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs @@ -2,7 +2,6 @@ // 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.IO; using System.Threading; @@ -16,7 +15,7 @@ namespace System.CommandLine.Tests.Invocation public class InvocationTests { [Fact] - public async Task Command_InvokeAsync_enables_help_by_default() + public async Task Command_InvokeAsync_displays_help_when_HelpOption_is_present() { var command = new Command("the-command") { @@ -26,12 +25,8 @@ public async Task Command_InvokeAsync_enables_help_by_default() command.Description = theHelpText; StringWriter output = new(); - CommandLineConfiguration config = new(command) - { - Output = output - }; - - await command.Parse("-h", config).InvokeAsync(); + + await command.Parse("-h").InvokeAsync(new() { Output = output }, CancellationToken.None); output.ToString() .Should() @@ -39,7 +34,7 @@ public async Task Command_InvokeAsync_enables_help_by_default() } [Fact] - public void Command_Invoke_enables_help_by_default() + public void Command_Invoke_displays_help_when_HelpOption_is_present() { var command = new Command("the-command") { @@ -49,12 +44,8 @@ public void Command_Invoke_enables_help_by_default() command.Description = theHelpText; StringWriter output = new (); - CommandLineConfiguration config = new(command) - { - Output = output - }; - - command.Parse("-h", config).Invoke(); + + command.Parse("-h").Invoke(new() { Output = output }); output.ToString() .Should() diff --git a/src/System.CommandLine.Tests/ParseDirectiveTests.cs b/src/System.CommandLine.Tests/ParseDirectiveTests.cs index 8ed409e34d..4c2f0cefce 100644 --- a/src/System.CommandLine.Tests/ParseDirectiveTests.cs +++ b/src/System.CommandLine.Tests/ParseDirectiveTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; using System.IO; +using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -151,12 +152,10 @@ public async Task When_there_are_errors_then_diagram_directive_sets_exit_code_to } }; - CommandLineConfiguration config = new(command) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - int exitCode = await config.InvokeAsync("[diagram] -x not-an-int"); + int exitCode = await command.Parse("[diagram] -x not-an-int") + .InvokeAsync(new() { Output = output }, CancellationToken.None); exitCode.Should().Be(42); } diff --git a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs index 48d3d0e2d4..e660ece6a3 100644 --- a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs +++ b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs @@ -24,14 +24,11 @@ public void Parse_error_reporting_reports_error_when_help_is_used_and_required_s }; var output = new StringWriter(); - var parseResult = root.Parse("", new CommandLineConfiguration(root) - { - Output = output, - }); + var parseResult = root.Parse(""); parseResult.Errors.Should().NotBeEmpty(); - var result = parseResult.Invoke(); + var result = parseResult.Invoke(new() { Output = output }); result.Should().Be(1); output.ToString().Should().ShowHelp(); @@ -104,9 +101,7 @@ public async Task When_there_are_parse_errors_then_customized_help_action_on_anc rootHelpWasCalled = true; }); - var config = new CommandLineConfiguration(rootCommand); - - await config.Parse("child grandchild oops").InvokeAsync(); + await rootCommand.Parse("child grandchild oops").InvokeAsync(); rootHelpWasCalled.Should().BeTrue(); } @@ -117,12 +112,8 @@ public void When_no_help_option_is_present_then_help_is_not_shown_for_parse_erro RootCommand rootCommand = new(); rootCommand.Options.Clear(); var output = new StringWriter(); - CommandLineConfiguration config = new(rootCommand) - { - Output = output - }; - - config.Parse("oops").Invoke(); + + rootCommand.Parse("oops").Invoke(new() { Output = output } ); output.ToString().Should().NotShowHelp(); } diff --git a/src/System.CommandLine.Tests/SuggestDirectiveTests.cs b/src/System.CommandLine.Tests/SuggestDirectiveTests.cs index ec1bb7209e..ccb482315f 100644 --- a/src/System.CommandLine.Tests/SuggestDirectiveTests.cs +++ b/src/System.CommandLine.Tests/SuggestDirectiveTests.cs @@ -1,11 +1,13 @@ // 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 FluentAssertions; +using Microsoft.VisualStudio.TestPlatform.Utilities; using System.CommandLine.Completions; using System.CommandLine.Help; using System.IO; +using System.Threading; using System.Threading.Tasks; -using FluentAssertions; using Xunit; using static System.Environment; @@ -42,16 +44,13 @@ public async Task It_writes_suggestions_for_option_arguments_when_under_subcomma _eatCommand, new SuggestDirective() }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output= new StringWriter(); - var result = rootCommand.Parse("[suggest:13] \"eat --fruit\"", config); + var result = rootCommand.Parse("[suggest:13] \"eat --fruit\""); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output + output .ToString() .Should() .Be($"apple{NewLine}banana{NewLine}cherry{NewLine}"); @@ -60,24 +59,21 @@ public async Task It_writes_suggestions_for_option_arguments_when_under_subcomma [Fact] public async Task It_writes_suggestions_for_option_arguments_when_under_root_command() { - RootCommand rootCommand = new () + RootCommand rootCommand = new() { _fruitOption, _vegetableOption }; - CommandLineConfiguration config = new (rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse($"[suggest:8] \"--fruit\"", config); + var result = rootCommand.Parse($"[suggest:8] \"--fruit\""); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .Be($"apple{NewLine}banana{NewLine}cherry{NewLine}"); + output + .ToString() + .Should() + .Be($"apple{NewLine}banana{NewLine}cherry{NewLine}"); } [Theory] @@ -86,21 +82,18 @@ public async Task It_writes_suggestions_for_option_arguments_when_under_root_com public async Task It_writes_suggestions_for_option_aliases_under_subcommand(string commandLine, string[] expectedCompletions) { RootCommand rootCommand = new() { _eatCommand }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + + var output = new StringWriter(); - var result = rootCommand.Parse(commandLine, config); + var result = rootCommand.Parse(commandLine); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); string expected = string.Join(NewLine, expectedCompletions) + NewLine; - config.Output - .ToString() - .Should() - .Be(expected); + output.ToString() + .Should() + .Be(expected); } [Theory] @@ -115,18 +108,14 @@ public async Task It_writes_suggestions_for_option_aliases_under_root_command(st _vegetableOption, _fruitOption }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse(input, config); - await result.InvokeAsync(); + await rootCommand.Parse(input).InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .Be($"--fruit{NewLine}--help{NewLine}--vegetable{NewLine}--version{NewLine}-?{NewLine}-h{NewLine}/?{NewLine}/h{NewLine}"); + output + .ToString() + .Should() + .Be($"--fruit{NewLine}--help{NewLine}--vegetable{NewLine}--version{NewLine}-?{NewLine}-h{NewLine}/?{NewLine}/h{NewLine}"); } [Fact] @@ -136,18 +125,15 @@ public async Task It_writes_suggestions_for_subcommand_aliases_under_root_comman { _eatCommand }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("[suggest]", config); - await result.InvokeAsync(); + var result = rootCommand.Parse("[suggest]"); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .Be($"--help{NewLine}--version{NewLine}-?{NewLine}-h{NewLine}/?{NewLine}/h{NewLine}eat{NewLine}"); + output + .ToString() + .Should() + .Be($"--help{NewLine}--version{NewLine}-?{NewLine}-h{NewLine}/?{NewLine}/h{NewLine}eat{NewLine}"); } [Fact] @@ -158,19 +144,17 @@ public async Task It_writes_suggestions_for_partial_option_aliases_under_root_co _fruitOption, _vegetableOption }; - CommandLineConfiguration config = new (rootCommand) - { - Output = new StringWriter(), - }; + + var output = new StringWriter(); - var result = rootCommand.Parse("[suggest:1] \"f\"", config); + var result = rootCommand.Parse("[suggest:1] \"f\""); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .Be($"--fruit{NewLine}"); + output + .ToString() + .Should() + .Be($"--fruit{NewLine}"); } [Fact] @@ -181,19 +165,16 @@ public async Task It_writes_suggestions_for_partial_subcommand_aliases_under_roo _eatCommand, new Command("wash-dishes") }; - CommandLineConfiguration config = new (rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("[suggest:1] \"d\"", config); + var result = rootCommand.Parse("[suggest:1] \"d\""); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .Be($"wash-dishes{NewLine}"); + output + .ToString() + .Should() + .Be($"wash-dishes{NewLine}"); } [Fact] @@ -204,19 +185,16 @@ public async Task It_writes_suggestions_for_partial_option_and_subcommand_aliase _eatCommand, new Command("wash-dishes"), }; - CommandLineConfiguration config = new (rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("[suggest:5] \"--ver\"", config); + var result = rootCommand.Parse("[suggest:5] \"--ver\""); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .Be($"--version{NewLine}"); + output + .ToString() + .Should() + .Be($"--version{NewLine}"); } [Fact] @@ -229,17 +207,14 @@ public async Task It_writes_suggestions_for_partial_option_and_subcommand_aliase new Option("--option2"), new Argument("arg") }; - CommandLineConfiguration config = new (command) - { - Output = new StringWriter() - }; - await config.InvokeAsync("[suggest:3] \"opt\""); + var output = new StringWriter(); - config.Output - .ToString() - .Should() - .Be($"--option1{NewLine}--option2{NewLine}"); + await command.Parse("[suggest:3] \"opt\"").InvokeAsync(new() { Output = output }, CancellationToken.None); + + output.ToString() + .Should() + .Be($"--option1{NewLine}--option2{NewLine}"); } [Fact] @@ -249,19 +224,17 @@ public async Task It_does_not_repeat_suggestion_for_already_specified_bool_optio { new Option("--bool-option") }; - CommandLineConfiguration config = new (command) - { - Output = new StringWriter() - }; + + var output = new StringWriter(); var commandLine = "--bool-option false"; - await command.Parse($"[suggest:{commandLine.Length + 1}] \"{commandLine}\"", config).InvokeAsync(); + await command.Parse($"[suggest:{commandLine.Length + 1}] \"{commandLine}\"").InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .NotContain("--bool-option"); + output + .ToString() + .Should() + .NotContain("--bool-option"); } } } diff --git a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs index 5acf4b59b5..7c3c194399 100644 --- a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs +++ b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs @@ -1,9 +1,10 @@ // 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 FluentAssertions; using System.IO; +using System.Threading; using System.Threading.Tasks; -using FluentAssertions; using Xunit; namespace System.CommandLine.Tests @@ -16,12 +17,7 @@ public async Task UseExceptionHandler_catches_command_handler_exceptions_and_set var command = new Command("the-command"); command.SetAction((_, __) => Task.FromException(new Exception("oops!"))); - CommandLineConfiguration config = new(command) - { - Error = new StringWriter(), - }; - - var resultCode = await config.InvokeAsync("the-command"); + var resultCode = await command.Parse("the-command").InvokeAsync(new() { Error = new StringWriter() },CancellationToken.None); resultCode.Should().Be(1); } @@ -32,14 +28,11 @@ public async Task UseExceptionHandler_catches_command_handler_exceptions_and_wri var command = new Command("the-command"); command.SetAction((_, __) => Task.FromException(new Exception("oops!"))); - CommandLineConfiguration config = new(command) - { - Error = new StringWriter(), - }; + var error = new StringWriter(); - await config.InvokeAsync("the-command"); + await command.Parse("the-command").InvokeAsync(new() { Error = error }, CancellationToken.None); - config.Error.ToString().Should().Contain("System.Exception: oops!"); + error.ToString().Should().Contain("System.Exception: oops!"); } [Fact] @@ -48,16 +41,12 @@ public async Task When_thrown_exception_is_from_cancelation_no_output_is_generat Command command = new("the-command"); command.SetAction((_, __) => throw new OperationCanceledException()); - CommandLineConfiguration config = new(command) - { - Output = new StringWriter(), - Error = new StringWriter() - }; + var output = new StringWriter(); + var error = new StringWriter(); - int resultCode = await config - .InvokeAsync("the-command"); + int resultCode = await command.Parse("the-command").InvokeAsync(new() { Output = output, Error = error }, CancellationToken.None); - config.Output.ToString().Should().BeEmpty(); + output.ToString().Should().BeEmpty(); resultCode.Should().NotBe(0); } @@ -70,24 +59,26 @@ public async Task Exception_output_can_be_customized(bool async) Command command = new("the-command"); command.SetAction((_, __) => throw expectedException); - CommandLineConfiguration config = new(command) + CommandLineInvocationConfiguration config = new() { Error = new StringWriter(), EnableDefaultExceptionHandler = false }; - ParseResult parseResult = command.Parse("the-command", config); + ParseResult parseResult = command.Parse("the-command"); int resultCode = 0; try { - resultCode = async ? await parseResult.InvokeAsync() : parseResult.Invoke(); + resultCode = async + ? await parseResult.InvokeAsync(config) + : parseResult.Invoke(config); } catch (Exception ex) { ex.Should().Be(expectedException); - parseResult.Configuration.Error.Write("Well that's awkward."); + parseResult.InvocationConfiguration.Error.Write("Well that's awkward."); resultCode = 22; } diff --git a/src/System.CommandLine.Tests/VersionOptionTests.cs b/src/System.CommandLine.Tests/VersionOptionTests.cs index b796349136..5e7f528ab2 100644 --- a/src/System.CommandLine.Tests/VersionOptionTests.cs +++ b/src/System.CommandLine.Tests/VersionOptionTests.cs @@ -1,12 +1,13 @@ // 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 FluentAssertions; -using FluentAssertions.Execution; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Execution; using Xunit; using static System.Environment; @@ -14,21 +15,21 @@ namespace System.CommandLine.Tests { public class VersionOptionTests { - private static readonly string version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()) - .GetCustomAttribute() - .InformationalVersion; + private static readonly string version = (Assembly.GetEntryAssembly() ?? + Assembly.GetExecutingAssembly()) + .GetCustomAttribute() + .InformationalVersion; [Fact] public async Task When_the_version_option_is_specified_then_the_version_is_written_to_standard_out() { - CommandLineConfiguration configuration = new(new RootCommand()) - { - Output = new StringWriter() - }; + var rootCommand = new RootCommand(); - await configuration.InvokeAsync("--version"); + var output = new StringWriter(); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + await rootCommand.Parse("--version").InvokeAsync(new() { Output = output }, CancellationToken.None); + + output.ToString().Should().Be($"{version}{NewLine}"); } [Fact] @@ -38,12 +39,9 @@ public async Task When_the_version_option_is_specified_then_invocation_is_short_ var rootCommand = new RootCommand(); rootCommand.SetAction((_) => wasCalled = true); - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - await configuration.InvokeAsync("--version"); + await rootCommand.Parse("--version").InvokeAsync(new() { Output = output }, CancellationToken.None); wasCalled.Should().BeFalse(); } @@ -51,17 +49,13 @@ public async Task When_the_version_option_is_specified_then_invocation_is_short_ [Fact] public async Task Version_option_appears_in_help() { - CommandLineConfiguration configuration = new(new RootCommand()) - { - Output = new StringWriter() - }; - - await configuration.InvokeAsync("--help"); + var output = new StringWriter(); + await new RootCommand().Parse("--help").InvokeAsync(new() { Output = output }, CancellationToken.None); - configuration.Output - .ToString() - .Should() - .Match("*Options:*--version*Show version information*"); + output + .ToString() + .Should() + .Match("*Options:*--version*Show version information*"); } [Fact] @@ -76,33 +70,27 @@ public async Task When_the_version_option_is_specified_and_there_are_default_opt }; rootCommand.SetAction((_) => { }); - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - await configuration.InvokeAsync("--version"); + await rootCommand.Parse("--version").InvokeAsync(new() { Output = output }, CancellationToken.None); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + output.ToString().Should().Be($"{version}{NewLine}"); } [Fact] public async Task When_the_version_option_is_specified_and_there_are_default_arguments_then_the_version_is_written_to_standard_out() { - RootCommand rootCommand = new () + RootCommand rootCommand = new() { - new Argument("x") { DefaultValueFactory =(_) => true }, + new Argument("x") { DefaultValueFactory = (_) => true }, }; rootCommand.SetAction((_) => { }); - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - await configuration.InvokeAsync("--version"); + await rootCommand.Parse("--version").InvokeAsync(new() { Output = output }, CancellationToken.None); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + output.ToString().Should().Be($"{version}{NewLine}"); } [Theory] @@ -119,40 +107,27 @@ public void Version_is_not_valid_with_other_tokens(string commandLine) }; rootCommand.SetAction(_ => { }); - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse(commandLine, configuration); + var result = rootCommand.Parse(commandLine); result.Errors.Should().Contain(e => e.Message == "--version option cannot be combined with other arguments."); } - + [Fact] public void Version_option_is_not_added_to_subcommands() { - var childCommand = new Command("subcommand"); - childCommand.SetAction(_ => { }); - var rootCommand = new RootCommand { - childCommand - }; - rootCommand.SetAction(_ => { }); - - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() + new Command("subcommand") }; - configuration - .RootCommand - .Subcommands - .Single(c => c.Name == "subcommand") - .Options - .Should() - .BeEmpty(); + rootCommand + .Subcommands + .Single(c => c.Name == "subcommand") + .Options + .Should() + .BeEmpty(); } [Fact] @@ -163,19 +138,16 @@ public async Task Version_can_specify_additional_alias() rootCommand.Options.Clear(); rootCommand.Add(new VersionOption("-v", "-version")); - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); using var _ = new AssertionScope(); - await configuration.InvokeAsync("-v"); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + await rootCommand.Parse("-v").InvokeAsync(new() { Output = output }, CancellationToken.None); + output.ToString().Should().Be($"{version}{NewLine}"); - configuration.Output = new StringWriter(); - await configuration.InvokeAsync("-version"); - configuration.Output.ToString().Should().Be($"{version}{NewLine}"); + output = new StringWriter(); + await rootCommand.Parse("-version").InvokeAsync(new() { Output = output }, CancellationToken.None); + output.ToString().Should().Be($"{version}{NewLine}"); } [Fact] @@ -193,14 +165,9 @@ public void Version_is_not_valid_with_other_tokens_when_it_uses_custom_alias() rootCommand.SetAction(_ => { }); - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; - - var result = rootCommand.Parse("-v subcommand", configuration); + var result = rootCommand.Parse("-v subcommand"); result.Errors.Should().ContainSingle(e => e.Message == "-v option cannot be combined with other arguments."); } } -} +} \ No newline at end of file diff --git a/src/System.CommandLine/CommandLineConfiguration.cs b/src/System.CommandLine/CommandLineConfiguration.cs index 80800653e1..a5a4504ddf 100644 --- a/src/System.CommandLine/CommandLineConfiguration.cs +++ b/src/System.CommandLine/CommandLineConfiguration.cs @@ -56,18 +56,6 @@ public CommandLineConfiguration(Command rootCommand) /// public bool EnablePosixBundling { get; set; } = true; - /// - /// Enables a default exception handler to catch any unhandled exceptions thrown during invocation. Enabled by default. - /// - public bool EnableDefaultExceptionHandler { get; set; } = true; - - /// - /// Enables signaling and handling of process termination (Ctrl+C, SIGINT, SIGTERM) via a - /// that can be passed to a during invocation. - /// If not provided, a default timeout of 2 seconds is enforced. - /// - public TimeSpan? ProcessTerminationTimeout { get; set; } = TimeSpan.FromSeconds(2); - /// /// Response file token replacer, enabled by default. /// To disable response files support, this property needs to be set to null. @@ -89,7 +77,8 @@ public CommandLineConfiguration(Command rootCommand) /// If you want to disable the output, please set it to . /// public TextWriter Output - { + { + // FIX: (Output) remove get => _output ??= Console.Out; set => _output = value ?? throw new ArgumentNullException(nameof(value), "Use TextWriter.Null to disable the output"); } @@ -101,57 +90,11 @@ public TextWriter Output /// public TextWriter Error { + // FIX: (Error) remove get => _error ??= Console.Error; set => _error = value ?? throw new ArgumentNullException(nameof(value), "Use TextWriter.Null to disable the output"); } - /// - /// Parses an array strings using the configured . - /// - /// The string arguments to parse. - /// A parse result describing the outcome of the parse operation. - public ParseResult Parse(IReadOnlyList args) - => CommandLineParser.Parse(RootCommand, args, this); - - /// - /// Parses a command line string value using the configured . - /// - /// The command line string input will be split into tokens as if it had been passed on the command line. - /// A command line string to parse, which can include spaces and quotes equivalent to what can be entered into a terminal. - /// A parse result describing the outcome of the parse operation. - public ParseResult Parse(string commandLine) - => CommandLineParser.Parse(RootCommand, commandLine, this); - - /// - /// Parses a command line string value and invokes the handler for the indicated command. - /// - /// The exit code for the invocation. - /// The command line string input will be split into tokens as if it had been passed on the command line. - public int Invoke(string commandLine) - => RootCommand.Parse(commandLine, this).Invoke(); - - /// - /// Parses a command line string array and invokes the handler for the indicated command. - /// - /// The exit code for the invocation. - public int Invoke(string[] args) - => RootCommand.Parse(args, this).Invoke(); - - /// - /// Parses a command line string value and invokes the handler for the indicated command. - /// - /// The exit code for the invocation. - /// The command line string input will be split into tokens as if it had been passed on the command line. - public Task InvokeAsync(string commandLine, CancellationToken cancellationToken = default) - => RootCommand.Parse(commandLine, this).InvokeAsync(cancellationToken); - - /// - /// Parses a command line string array and invokes the handler for the indicated command. - /// - /// The exit code for the invocation. - public Task InvokeAsync(string[] args, CancellationToken cancellationToken = default) - => RootCommand.Parse(args, this).InvokeAsync(cancellationToken); - /// /// Throws an exception if the parser configuration is ambiguous or otherwise not valid. /// diff --git a/src/System.CommandLine/CommandLineInvocationConfiguration.cs b/src/System.CommandLine/CommandLineInvocationConfiguration.cs new file mode 100644 index 0000000000..4b0b43b402 --- /dev/null +++ b/src/System.CommandLine/CommandLineInvocationConfiguration.cs @@ -0,0 +1,45 @@ +using System.CommandLine.Invocation; +using System.IO; +using System.Threading; + +namespace System.CommandLine; + +public class CommandLineInvocationConfiguration +{ + private TextWriter? _output, _error; + + /// + /// Enables a default exception handler to catch any unhandled exceptions thrown during invocation. Enabled by default. + /// + public bool EnableDefaultExceptionHandler { get; set; } = true; + + /// + /// Enables signaling and handling of process termination (Ctrl+C, SIGINT, SIGTERM) via a + /// that can be passed to a during invocation. + /// If not provided, a default timeout of 2 seconds is enforced. + /// + public TimeSpan? ProcessTerminationTimeout { get; set; } + + /// + /// The standard output. Used by Help and other facilities that write non-error information. + /// By default it's set to . + /// For testing purposes, it can be set to a new instance of . + /// If you want to disable the output, please set it to . + /// + public TextWriter Output + { + get => _output ??= Console.Out; + set => _output = value ?? throw new ArgumentNullException(nameof(value), "Use TextWriter.Null to disable the output"); + } + + /// + /// The standard error. Used for printing error information like parse errors. + /// By default it's set to . + /// For testing purposes, it can be set to a new instance of . + /// + public TextWriter Error + { + get => _error ??= Console.Error; + set => _error = value ?? throw new ArgumentNullException(nameof(value), "Use TextWriter.Null to disable the output"); + } +} \ No newline at end of file diff --git a/src/System.CommandLine/Completions/CompletionAction.cs b/src/System.CommandLine/Completions/CompletionAction.cs index 7757568630..e6e8f36518 100644 --- a/src/System.CommandLine/Completions/CompletionAction.cs +++ b/src/System.CommandLine/Completions/CompletionAction.cs @@ -30,7 +30,9 @@ public override int Invoke(ParseResult parseResult) var completions = completionParseResult.GetCompletions(position); - parseResult.Configuration.Output.WriteLine( + var output = parseResult.InvocationConfiguration.Output; + + output.WriteLine( string.Join( Environment.NewLine, completions)); diff --git a/src/System.CommandLine/Help/HelpAction.cs b/src/System.CommandLine/Help/HelpAction.cs index 7658ddeaad..06bb493056 100644 --- a/src/System.CommandLine/Help/HelpAction.cs +++ b/src/System.CommandLine/Help/HelpAction.cs @@ -21,7 +21,7 @@ internal HelpBuilder Builder /// public override int Invoke(ParseResult parseResult) { - var output = parseResult.Configuration.Output; + var output = parseResult.InvocationConfiguration?.Output ?? parseResult.Configuration.Output; var helpContext = new HelpContext(Builder, parseResult.CommandResult.Command, @@ -32,4 +32,4 @@ public override int Invoke(ParseResult parseResult) return 0; } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/InvocationPipeline.cs b/src/System.CommandLine/Invocation/InvocationPipeline.cs index 3cbca453ea..ce492fe47e 100644 --- a/src/System.CommandLine/Invocation/InvocationPipeline.cs +++ b/src/System.CommandLine/Invocation/InvocationPipeline.cs @@ -45,9 +45,12 @@ internal static async Task InvokeAsync(ParseResult parseResult, Cancellatio case AsynchronousCommandLineAction asyncAction: var startedInvocation = asyncAction.InvokeAsync(parseResult, cts.Token); - if (parseResult.Configuration.ProcessTerminationTimeout.HasValue) + + var timeout = parseResult.InvocationConfiguration.ProcessTerminationTimeout; + + if (timeout.HasValue) { - terminationHandler = new(cts, startedInvocation, parseResult.Configuration.ProcessTerminationTimeout.Value); + terminationHandler = new(cts, startedInvocation, timeout.Value); } if (terminationHandler is null) @@ -67,9 +70,9 @@ internal static async Task InvokeAsync(ParseResult parseResult, Cancellatio throw new ArgumentOutOfRangeException(nameof(parseResult.Action)); } } - catch (Exception ex) when (parseResult.Configuration.EnableDefaultExceptionHandler) + catch (Exception ex) when (parseResult.InvocationConfiguration.EnableDefaultExceptionHandler) { - return DefaultExceptionHandler(ex, parseResult.Configuration); + return DefaultExceptionHandler(ex, parseResult); } finally { @@ -96,7 +99,7 @@ internal static int Invoke(ParseResult parseResult) if (action is not SynchronousCommandLineAction) { - parseResult.Configuration.EnableDefaultExceptionHandler = false; + parseResult.InvocationConfiguration.EnableDefaultExceptionHandler = false; throw new Exception( $"This should not happen. An instance of {nameof(AsynchronousCommandLineAction)} ({action}) was called within {nameof(InvocationPipeline)}.{nameof(Invoke)}. This is supposed to be detected earlier resulting in a call to {nameof(InvocationPipeline)}{nameof(InvokeAsync)}"); } @@ -114,9 +117,9 @@ internal static int Invoke(ParseResult parseResult) return syncAction.Invoke(parseResult); } - catch (Exception ex) when (parseResult.Configuration.EnableDefaultExceptionHandler) + catch (Exception ex) when (parseResult.InvocationConfiguration.EnableDefaultExceptionHandler) { - return DefaultExceptionHandler(ex, parseResult.Configuration); + return DefaultExceptionHandler(ex, parseResult); } default: @@ -124,15 +127,18 @@ internal static int Invoke(ParseResult parseResult) } } - private static int DefaultExceptionHandler(Exception exception, CommandLineConfiguration config) + private static int DefaultExceptionHandler(Exception exception, ParseResult parseResult) { if (exception is not OperationCanceledException) { ConsoleHelpers.ResetTerminalForegroundColor(); ConsoleHelpers.SetTerminalForegroundRed(); - config.Error.Write(LocalizationResources.ExceptionHandlerHeader()); - config.Error.WriteLine(exception.ToString()); + var error = parseResult.InvocationConfiguration?.Error ?? + parseResult.Configuration.Error; + + error.Write(LocalizationResources.ExceptionHandlerHeader()); + error.WriteLine(exception.ToString()); ConsoleHelpers.ResetTerminalForegroundColor(); } diff --git a/src/System.CommandLine/Invocation/ParseErrorAction.cs b/src/System.CommandLine/Invocation/ParseErrorAction.cs index fdb4e01b45..67d0db9a6a 100644 --- a/src/System.CommandLine/Invocation/ParseErrorAction.cs +++ b/src/System.CommandLine/Invocation/ParseErrorAction.cs @@ -49,12 +49,15 @@ private static void WriteErrorDetails(ParseResult parseResult) ConsoleHelpers.ResetTerminalForegroundColor(); ConsoleHelpers.SetTerminalForegroundRed(); + var stdErr = parseResult.InvocationConfiguration.Error ?? + parseResult.Configuration.Error; + foreach (var error in parseResult.Errors) { - parseResult.Configuration.Error.WriteLine(error.Message); + stdErr.WriteLine(error.Message); } - parseResult.Configuration.Error.WriteLine(); + stdErr.WriteLine(); ConsoleHelpers.ResetTerminalForegroundColor(); } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 0bbd1ad093..ce27a0b353 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -21,6 +21,7 @@ public sealed class ParseResult private CompletionContext? _completionContext; private readonly CommandLineAction? _action; private readonly List? _preActions; + private CommandLineInvocationConfiguration? _invocationConfiguration; internal ParseResult( CommandLineConfiguration configuration, @@ -68,7 +69,16 @@ internal ParseResult( /// /// The configuration used to produce the parse result. /// - public CommandLineConfiguration Configuration { get; } + public CommandLineConfiguration Configuration { get; private set; } + + /// + /// The configuration used to specify command line runtime behavior. + /// + public CommandLineInvocationConfiguration InvocationConfiguration + { + get => _invocationConfiguration ??= new(); + set => _invocationConfiguration = value; + } /// /// Gets the root command result. @@ -267,6 +277,18 @@ static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => public Task InvokeAsync(CancellationToken cancellationToken = default) => InvocationPipeline.InvokeAsync(this, cancellationToken); + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A token that can be used to cancel an invocation. + /// A task whose result can be used as a process exit code. + public Task InvokeAsync(CommandLineInvocationConfiguration configuration, CancellationToken cancellationToken = default) + { + InvocationConfiguration = configuration; + + return InvokeAsync(cancellationToken); + } + /// /// Invokes the appropriate command handler for a parsed command line input. /// @@ -302,6 +324,17 @@ public int Invoke() } } + /// + /// Invokes the appropriate command handler for a parsed command line input. + /// + /// A value that can be used as a process exit code. + public int Invoke(CommandLineInvocationConfiguration configuration) + { + InvocationConfiguration = configuration; + + return Invoke(); + } + /// /// Gets the for parsed result. The handler represents the action /// that will be performed when the parse result is invoked. diff --git a/src/System.CommandLine/VersionOption.cs b/src/System.CommandLine/VersionOption.cs index 6c31ab1485..0fde75a764 100644 --- a/src/System.CommandLine/VersionOption.cs +++ b/src/System.CommandLine/VersionOption.cs @@ -65,7 +65,7 @@ private sealed class VersionOptionAction : SynchronousCommandLineAction { public override int Invoke(ParseResult parseResult) { - parseResult.Configuration.Output.WriteLine(RootCommand.ExecutableVersion); + parseResult.InvocationConfiguration.Output.WriteLine(RootCommand.ExecutableVersion); return 0; } } From 8f403798bb992f9eab9265f1129dd76e5ae29425 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Mon, 30 Jun 2025 18:00:17 -0700 Subject: [PATCH 03/16] remove Output and Error from CommandLineConfiguration --- .../SuggestionShellScriptHandlerTest.cs | 35 +++-- .../SuggestionDispatcher.cs | 21 +-- .../Binding/TestModels.cs | 2 +- .../Help/CustomHelpAction.cs | 7 +- .../HelpOptionTests.cs | 22 ++- .../Invocation/TypoCorrectionTests.cs | 136 ++++++++---------- .../ParseDirectiveTests.cs | 64 +++------ .../ParseErrorReportingTests.cs | 20 ++- .../Utility/ParseResultExtensions.cs | 8 +- .../CommandLineConfiguration.cs | 32 ----- src/System.CommandLine/Help/HelpAction.cs | 2 +- .../Invocation/InvocationPipeline.cs | 3 +- .../Invocation/ParseErrorAction.cs | 9 +- .../Parsing/ParseDiagramAction.cs | 2 +- 14 files changed, 137 insertions(+), 226 deletions(-) diff --git a/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs b/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs index 0ca6f6805e..5456b3fca9 100644 --- a/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs +++ b/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.IO; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Xunit; @@ -10,32 +11,34 @@ namespace System.CommandLine.Suggest.Tests { public class SuggestionShellScriptHandlerTest { - private readonly CommandLineConfiguration _configuration; + private readonly RootCommand _rootCommand; + private readonly CommandLineInvocationConfiguration _configuration; public SuggestionShellScriptHandlerTest() { - _configuration = new SuggestionDispatcher(new TestSuggestionRegistration()).Configuration; + _rootCommand = new SuggestionDispatcher(new TestSuggestionRegistration()).RootCommand; + _configuration = new() + { + Output = new StringWriter(), + Error = new StringWriter() + }; } [Fact] public async Task When_shell_type_is_not_supported_it_throws() { - _configuration.Error = new StringWriter(); - - await _configuration.InvokeAsync("script 123"); + await _rootCommand.Parse("script 123").InvokeAsync(_configuration, CancellationToken.None); _configuration.Error - .ToString() - .Should() - .Contain("Shell '123' is not supported."); + .ToString() + .Should() + .Contain("Shell '123' is not supported."); } [Fact] public async Task It_should_print_bash_shell_script() { - _configuration.Output = new StringWriter(); - - await _configuration.InvokeAsync("script bash"); + await _rootCommand.Parse("script bash").InvokeAsync(_configuration, CancellationToken.None); _configuration.Output.ToString().Should().Contain("_dotnet_bash_complete()"); _configuration.Output.ToString().Should().NotContain("\r\n"); @@ -44,9 +47,7 @@ public async Task It_should_print_bash_shell_script() [Fact] public async Task It_should_print_powershell_shell_script() { - _configuration.Output = new StringWriter(); - - await _configuration.InvokeAsync("script powershell"); + await _rootCommand.Parse("script powershell").InvokeAsync(_configuration, CancellationToken.None); _configuration.Output.ToString().Should().Contain("Register-ArgumentCompleter"); _configuration.Output.ToString().Should().Contain("\r\n"); @@ -55,12 +56,10 @@ public async Task It_should_print_powershell_shell_script() [Fact] public async Task It_should_print_zsh_shell_script() { - _configuration.Output = new StringWriter(); + await _rootCommand.Parse("script zsh").InvokeAsync(_configuration, CancellationToken.None); - await _configuration.InvokeAsync("script zsh"); - _configuration.Output.ToString().Should().Contain("_dotnet_zsh_complete()"); _configuration.Output.ToString().Should().NotContain("\r\n"); } } -} +} \ No newline at end of file diff --git a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs index 633989f93e..5a36559fb3 100644 --- a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs +++ b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs @@ -13,7 +13,6 @@ public class SuggestionDispatcher { private readonly ISuggestionRegistration _suggestionRegistration; private readonly ISuggestionStore _suggestionStore; - private readonly RootCommand _rootCommand; private readonly CommandLineInvocationConfiguration _invocationConfig; public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISuggestionStore suggestionStore = null) @@ -30,7 +29,7 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug }; CompleteScriptCommand.SetAction(context => { - SuggestionShellScriptHandler.Handle(context.Configuration.Output, context.GetValue(shellTypeArgument)); + SuggestionShellScriptHandler.Handle(context.InvocationConfiguration.Output, context.GetValue(shellTypeArgument)); }); ListCommand = new Command("list") @@ -39,7 +38,7 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug }; ListCommand.SetAction((ctx, cancellationToken) => { - ctx.Configuration.Output.WriteLine(ShellPrefixesToMatch(_suggestionRegistration)); + ctx.InvocationConfiguration.Output.WriteLine(ShellPrefixesToMatch(_suggestionRegistration)); return Task.CompletedTask; }); @@ -60,25 +59,27 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug RegisterCommand.SetAction((context, cancellationToken) => { - Register(context.GetValue(commandPathOption), context.Configuration.Output); + Register(context.GetValue(commandPathOption), context.InvocationConfiguration.Output); return Task.CompletedTask; }); - _rootCommand = new RootCommand + RootCommand = new RootCommand { ListCommand, GetCommand, RegisterCommand, CompleteScriptCommand, }; - _rootCommand.TreatUnmatchedTokensAsErrors = false; - Configuration = new CommandLineConfiguration(_rootCommand); + RootCommand.TreatUnmatchedTokensAsErrors = false; + Configuration = new CommandLineInvocationConfiguration(); } private Command CompleteScriptCommand { get; } private Command GetCommand { get; } + public RootCommand RootCommand { get; } + private Option ExecutableOption { get; } = GetExecutableOption(); private static Option GetExecutableOption() @@ -99,11 +100,11 @@ private static Option GetExecutableOption() private Command RegisterCommand { get; } - public CommandLineConfiguration Configuration { get; } + public CommandLineInvocationConfiguration Configuration { get; } public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(5000); - public Task InvokeAsync(string[] args) => Configuration.RootCommand.Parse(args, Configuration).InvokeAsync(_invocationConfig); + public Task InvokeAsync(string[] args) => RootCommand.Parse(args).InvokeAsync(Configuration); private void Register( string commandPath, @@ -169,7 +170,7 @@ private Task Get(ParseResult parseResult, CancellationToken cancellationTok Program.LogDebug($"dotnet-suggest returning: \"{completions.Replace("\r", "\\r").Replace("\n", "\\n")}\""); #endif - parseResult.Configuration.Output.Write(completions); + parseResult.InvocationConfiguration.Output.Write(completions); return Task.FromResult(0); } diff --git a/src/System.CommandLine.Tests/Binding/TestModels.cs b/src/System.CommandLine.Tests/Binding/TestModels.cs index 49cd584e22..972f5eb8a2 100644 --- a/src/System.CommandLine.Tests/Binding/TestModels.cs +++ b/src/System.CommandLine.Tests/Binding/TestModels.cs @@ -71,7 +71,7 @@ public class ClassWithMethodHavingParameter public ClassWithMethodHavingParameter(ParseResult parseResult) { - _output = parseResult.Configuration.Output; + _output = parseResult.InvocationConfiguration.Output; } public int Handle(T value) diff --git a/src/System.CommandLine.Tests/Help/CustomHelpAction.cs b/src/System.CommandLine.Tests/Help/CustomHelpAction.cs index 087c6c97f3..e24e22c729 100644 --- a/src/System.CommandLine.Tests/Help/CustomHelpAction.cs +++ b/src/System.CommandLine.Tests/Help/CustomHelpAction.cs @@ -1,4 +1,7 @@ -using System.CommandLine.Invocation; +// 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.Invocation; namespace System.CommandLine.Help { @@ -21,7 +24,7 @@ internal HelpBuilder Builder /// public override int Invoke(ParseResult parseResult) { - var output = parseResult.InvocationConfiguration?.Output ?? parseResult.Configuration.Output; + var output = parseResult.InvocationConfiguration.Output; var helpContext = new HelpContext(Builder, parseResult.CommandResult.Command, diff --git a/src/System.CommandLine.Tests/HelpOptionTests.cs b/src/System.CommandLine.Tests/HelpOptionTests.cs index 4fb8b9f8f3..5a6261a48d 100644 --- a/src/System.CommandLine.Tests/HelpOptionTests.cs +++ b/src/System.CommandLine.Tests/HelpOptionTests.cs @@ -43,12 +43,9 @@ public async Task Help_option_interrupts_execution_of_the_specified_command() subcommand.SetAction(_ => wasCalled = true); command.Subcommands.Add(subcommand); - CommandLineConfiguration config = new(command) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - await command.Parse("command subcommand --help", config).InvokeAsync(); + await command.Parse("command subcommand --help").InvokeAsync(new() { Output = output }, CancellationToken.None); wasCalled.Should().BeFalse(); } @@ -77,15 +74,12 @@ public async Task Help_option_does_not_display_when_option_defined_with_same_ali { var command = new Command("command"); command.Options.Add(new Option("-h")); + + var output = new StringWriter(); - CommandLineConfiguration config = new(command) - { - Output = new StringWriter() - }; - - await command.Parse("command -h", config).InvokeAsync(); + await command.Parse("command -h").InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().NotShowHelp(); + output.ToString().Should().NotShowHelp(); } [Fact] @@ -201,7 +195,7 @@ public void The_users_can_provide_usage_examples(bool subcommand) TextWriter output = new StringWriter(); var result = subcommand ? rootCommand.Parse("subcommand -h") : rootCommand.Parse("-h"); - result.Configuration.Output = output; + result.InvocationConfiguration.Output = output; result.Invoke(); if (subcommand) @@ -254,7 +248,7 @@ public override int Invoke(ParseResult parseResult) if (parseResult.CommandResult.Command.Name == "subcommand") { - var output = parseResult.Configuration.Output; + var output = parseResult.InvocationConfiguration.Output; output.WriteLine(CustomUsageText); } diff --git a/src/System.CommandLine.Tests/Invocation/TypoCorrectionTests.cs b/src/System.CommandLine.Tests/Invocation/TypoCorrectionTests.cs index 9a9322dc35..1bac77d500 100644 --- a/src/System.CommandLine.Tests/Invocation/TypoCorrectionTests.cs +++ b/src/System.CommandLine.Tests/Invocation/TypoCorrectionTests.cs @@ -1,9 +1,12 @@ -using System.CommandLine.Help; +// 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 FluentAssertions; +using Microsoft.VisualStudio.TestPlatform.Utilities; using System.CommandLine.Invocation; using System.IO; -using System.Linq; +using System.Threading; using System.Threading.Tasks; -using FluentAssertions; using Xunit; using static System.Environment; @@ -14,21 +17,18 @@ public class TypoCorrectionTests [Fact] public async Task When_option_is_mistyped_it_is_suggested() { - RootCommand rootCommand = new () + RootCommand rootCommand = new() { new Option("info") }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("niof", config); + var result = rootCommand.Parse("niof"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().Contain($"'niof' was not matched. Did you mean one of the following?{NewLine}info"); + output.ToString().Should().Contain($"'niof' was not matched. Did you mean one of the following?{NewLine}info"); } [Fact] @@ -39,39 +39,18 @@ public async Task Typo_corrections_can_be_disabled() new Option("info") }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("niof", config); + var result = rootCommand.Parse("niof"); if (result.Action is ParseErrorAction parseError) { parseError.ShowTypoCorrections = false; } - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().NotContain("Did you mean"); - } - - [Fact] - public async Task When_there_are_no_matches_then_nothing_is_suggested() - { - var option = new Option("info"); - RootCommand rootCommand = new() { option }; - - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; - - var result = rootCommand.Parse("zzzzzzz", configuration); - - await result.InvokeAsync(); - - configuration.Output.ToString().Should().NotContain("was not matched"); + output.ToString().Should().NotContain("Did you mean"); } [Fact] @@ -80,16 +59,13 @@ public async Task When_command_is_mistyped_it_is_suggested() var command = new Command("restore"); RootCommand rootCommand = new() { command }; - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("sertor", configuration); + var result = rootCommand.Parse("sertor"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - configuration.Output.ToString().Should().Contain($"'sertor' was not matched. Did you mean one of the following?{NewLine}restore"); + output.ToString().Should().Contain($"'sertor' was not matched. Did you mean one of the following?{NewLine}restore"); } [Fact] @@ -99,23 +75,35 @@ public async Task When_there_are_multiple_matches_it_picks_the_best_matches() var seenCommand = new Command("seen"); var aOption = new Option("a"); var beenOption = new Option("been"); - RootCommand rootCommand = new () + RootCommand rootCommand = new() { fromCommand, seenCommand, aOption, beenOption }; - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; - var result = rootCommand.Parse("een", configuration); + var output = new StringWriter(); - await result.InvokeAsync(); + var result = rootCommand.Parse("een"); - configuration.Output.ToString().Should().Contain($"'een' was not matched. Did you mean one of the following?{NewLine}seen{NewLine}been"); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); + + output.ToString().Should().Contain($"'een' was not matched. Did you mean one of the following?{NewLine}seen{NewLine}been"); + } + + [Fact] + public async Task When_there_are_no_matches_then_nothing_is_suggested() + { + var option = new Option("info"); + RootCommand rootCommand = new() { option }; + + var output = new StringWriter(); + var result = rootCommand.Parse("zzzzzzz"); + + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); + + output.ToString().Should().NotContain("was not matched"); } [Fact] @@ -131,16 +119,13 @@ public async Task Hidden_commands_are_not_suggested() beenCommand }; - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("een", configuration); + var result = rootCommand.Parse("een"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - configuration.Output.ToString().Should().Contain($"'een' was not matched. Did you mean one of the following?{NewLine}been"); + output.ToString().Should().Contain($"'een' was not matched. Did you mean one of the following?{NewLine}been"); } [Fact] @@ -154,20 +139,17 @@ public async Task Arguments_are_not_suggested() command }; - CommandLineConfiguration configuration = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("een", configuration); + var result = rootCommand.Parse("een"); var parseErrorAction = (ParseErrorAction)result.Action; parseErrorAction.ShowHelp = false; parseErrorAction.ShowTypoCorrections = true; - - await result.InvokeAsync(); - configuration.Output.ToString().Should().NotContain("the-argument"); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); + + output.ToString().Should().NotContain("the-argument"); } [Fact] @@ -182,16 +164,13 @@ public async Task Hidden_options_are_not_suggested() seenOption, beenOption }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("een", config); + var result = rootCommand.Parse("een"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().Contain($"'een' was not matched. Did you mean one of the following?{NewLine}been"); + output.ToString().Should().Contain($"'een' was not matched. Did you mean one of the following?{NewLine}been"); } [Fact] @@ -202,15 +181,12 @@ public async Task Suggestions_favor_matches_with_prefix() new Option("/call", "-call", "--call"), new Option("/email", "-email", "--email") }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; - var result = rootCommand.Parse("-all", config); + var output = new StringWriter(); + var result = rootCommand.Parse("-all"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output.ToString().Should().Contain($"'-all' was not matched. Did you mean one of the following?{NewLine}-call"); + output.ToString().Should().Contain($"'-all' was not matched. Did you mean one of the following?{NewLine}-call"); } } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/ParseDirectiveTests.cs b/src/System.CommandLine.Tests/ParseDirectiveTests.cs index 4c2f0cefce..8490e8d579 100644 --- a/src/System.CommandLine.Tests/ParseDirectiveTests.cs +++ b/src/System.CommandLine.Tests/ParseDirectiveTests.cs @@ -31,73 +31,55 @@ public async Task Diagram_directive_writes_parse_diagram(bool treatUnmatchedToke subcommand.Options.Add(option); subcommand.TreatUnmatchedTokensAsErrors = treatUnmatchedTokensAsErrors; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; - - var result = rootCommand.Parse("[diagram] subcommand -c 34 --nonexistent wat", config); + var output = new StringWriter(); - output.WriteLine(result.Diagram()); + var result = rootCommand.Parse("[diagram] subcommand -c 34 --nonexistent wat"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); string expected = treatUnmatchedTokensAsErrors ? $"[ {RootCommand.ExecutableName} ![ subcommand [ -c <34> ] ] ] ???--> --nonexistent wat" + Environment.NewLine : $"[ {RootCommand.ExecutableName} [ subcommand [ -c <34> ] ] ] ???--> --nonexistent wat" + Environment.NewLine; - config.Output - .ToString() + output.ToString() .Should() .Be(expected); } [Fact] - public async Task When_diagram_directive_is_used_the_help_is_not_displayed() + public async Task When_diagram_directive_is_used_then_help_is_not_displayed() { RootCommand rootCommand = new() { new DiagramDirective() }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter(), - }; - - var result = rootCommand.Parse("[diagram] --help", config); + var output = new StringWriter(); - output.WriteLine(result.Diagram()); + var result = rootCommand.Parse("[diagram] --help"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() - .Should() - .Be($"[ {RootCommand.ExecutableName} [ --help ] ]" + Environment.NewLine); + output.ToString() + .Should() + .Be($"[ {RootCommand.ExecutableName} [ --help ] ]" + Environment.NewLine); } [Fact] - public async Task When_diagram_directive_is_used_the_version_is_not_displayed() + public async Task When_diagram_directive_is_used_then_version_is_not_displayed() { RootCommand rootCommand = new() { new DiagramDirective() }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; - - var result = rootCommand.Parse("[diagram] --version", config); + var output = new StringWriter(); - output.WriteLine(result.Diagram()); + var result = rootCommand.Parse("[diagram] --version"); - await result.InvokeAsync(); + await result.InvokeAsync(new() { Output = output }, CancellationToken.None); - config.Output - .ToString() + output.ToString() .Should() .Be($"[ {RootCommand.ExecutableName} [ --version ] ]" + Environment.NewLine); } @@ -111,12 +93,9 @@ public async Task When_there_are_no_errors_then_diagram_directive_sets_exit_code new DiagramDirective() }; - CommandLineConfiguration config = new(command) - { - Output = new StringWriter(), - }; + var output = new StringWriter(); - var exitCode = await command.Parse("[diagram] -x 123", config).InvokeAsync(); + var exitCode = await command.Parse("[diagram] -x 123").InvokeAsync(new() { Output = output }, CancellationToken.None); exitCode.Should().Be(0); } @@ -130,12 +109,9 @@ public async Task When_there_are_errors_then_diagram_directive_sets_exit_code_1( new DiagramDirective() }; - CommandLineConfiguration config = new(command) - { - Output = new StringWriter(), - }; + var output = new StringWriter(); - var exitCode = await command.Parse("[diagram] -x not-an-int", config).InvokeAsync(); + var exitCode = await command.Parse("[diagram] -x not-an-int").InvokeAsync(new() { Output = output }, CancellationToken.None); exitCode.Should().Be(1); } diff --git a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs index e660ece6a3..d66f37d816 100644 --- a/src/System.CommandLine.Tests/ParseErrorReportingTests.cs +++ b/src/System.CommandLine.Tests/ParseErrorReportingTests.cs @@ -1,14 +1,15 @@ // 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 FluentAssertions; using System.CommandLine.Help; using System.CommandLine.Invocation; -using System.IO; -using FluentAssertions; -using Xunit; using System.CommandLine.Tests.Utility; +using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Xunit; namespace System.CommandLine.Tests; @@ -42,23 +43,18 @@ public void Help_display_can_be_disabled() new Option("--verbose") }; - CommandLineConfiguration config = new(rootCommand) - { - Output = new StringWriter() - }; + var output = new StringWriter(); - var result = rootCommand.Parse("oops", config); + var result = rootCommand.Parse("oops"); if (result.Action is ParseErrorAction parseError) { parseError.ShowHelp = false; } - result.Invoke(); + result.Invoke(new() { Output = output }); - var output = config.Output.ToString(); - - output.Should().NotShowHelp(); + output.ToString().Should().NotShowHelp(); } [Theory] // https://github.com/dotnet/command-line-api/issues/2226 diff --git a/src/System.CommandLine.Tests/Utility/ParseResultExtensions.cs b/src/System.CommandLine.Tests/Utility/ParseResultExtensions.cs index 60e46a2599..9cfadd03b2 100644 --- a/src/System.CommandLine.Tests/Utility/ParseResultExtensions.cs +++ b/src/System.CommandLine.Tests/Utility/ParseResultExtensions.cs @@ -7,19 +7,19 @@ internal static class ParseResultExtensions { internal static string Diagram(this ParseResult parseResult) { - TextWriter outputBefore = parseResult.Configuration.Output; + TextWriter outputBefore = parseResult.InvocationConfiguration.Output; try { - parseResult.Configuration.Output = new StringWriter(); + parseResult.InvocationConfiguration.Output = new StringWriter(); ((SynchronousCommandLineAction)new DiagramDirective().Action!).Invoke(parseResult); - return parseResult.Configuration.Output.ToString() + return parseResult.InvocationConfiguration.Output.ToString() .TrimEnd(); // the directive adds a new line, tests that used to rely on Diagram extension method don't expect it } finally { // some of the tests check the Output after getting the Diagram - parseResult.Configuration.Output = outputBefore; + parseResult.InvocationConfiguration.Output = outputBefore; } } } diff --git a/src/System.CommandLine/CommandLineConfiguration.cs b/src/System.CommandLine/CommandLineConfiguration.cs index a5a4504ddf..2bddefdbd8 100644 --- a/src/System.CommandLine/CommandLineConfiguration.cs +++ b/src/System.CommandLine/CommandLineConfiguration.cs @@ -1,13 +1,8 @@ // 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; using System.CommandLine.Parsing; using System.Linq; -using System.Threading.Tasks; -using System.Threading; -using System.IO; -using System.CommandLine.Invocation; namespace System.CommandLine { @@ -16,8 +11,6 @@ namespace System.CommandLine /// public class CommandLineConfiguration { - private TextWriter? _output, _error; - /// /// Initializes a new instance of the class. /// @@ -70,31 +63,6 @@ public CommandLineConfiguration(Command rootCommand) /// public Command RootCommand { get; } - /// - /// The standard output. Used by Help and other facilities that write non-error information. - /// By default it's set to . - /// For testing purposes, it can be set to a new instance of . - /// If you want to disable the output, please set it to . - /// - public TextWriter Output - { - // FIX: (Output) remove - get => _output ??= Console.Out; - set => _output = value ?? throw new ArgumentNullException(nameof(value), "Use TextWriter.Null to disable the output"); - } - - /// - /// The standard error. Used for printing error information like parse errors. - /// By default it's set to . - /// For testing purposes, it can be set to a new instance of . - /// - public TextWriter Error - { - // FIX: (Error) remove - get => _error ??= Console.Error; - set => _error = value ?? throw new ArgumentNullException(nameof(value), "Use TextWriter.Null to disable the output"); - } - /// /// Throws an exception if the parser configuration is ambiguous or otherwise not valid. /// diff --git a/src/System.CommandLine/Help/HelpAction.cs b/src/System.CommandLine/Help/HelpAction.cs index 06bb493056..c04a8db53a 100644 --- a/src/System.CommandLine/Help/HelpAction.cs +++ b/src/System.CommandLine/Help/HelpAction.cs @@ -21,7 +21,7 @@ internal HelpBuilder Builder /// public override int Invoke(ParseResult parseResult) { - var output = parseResult.InvocationConfiguration?.Output ?? parseResult.Configuration.Output; + var output = parseResult.InvocationConfiguration.Output; var helpContext = new HelpContext(Builder, parseResult.CommandResult.Command, diff --git a/src/System.CommandLine/Invocation/InvocationPipeline.cs b/src/System.CommandLine/Invocation/InvocationPipeline.cs index ce492fe47e..c41d2686d6 100644 --- a/src/System.CommandLine/Invocation/InvocationPipeline.cs +++ b/src/System.CommandLine/Invocation/InvocationPipeline.cs @@ -134,8 +134,7 @@ private static int DefaultExceptionHandler(Exception exception, ParseResult pars ConsoleHelpers.ResetTerminalForegroundColor(); ConsoleHelpers.SetTerminalForegroundRed(); - var error = parseResult.InvocationConfiguration?.Error ?? - parseResult.Configuration.Error; + var error = parseResult.InvocationConfiguration.Error; error.Write(LocalizationResources.ExceptionHandlerHeader()); error.WriteLine(exception.ToString()); diff --git a/src/System.CommandLine/Invocation/ParseErrorAction.cs b/src/System.CommandLine/Invocation/ParseErrorAction.cs index 67d0db9a6a..3f35c2cc1e 100644 --- a/src/System.CommandLine/Invocation/ParseErrorAction.cs +++ b/src/System.CommandLine/Invocation/ParseErrorAction.cs @@ -49,8 +49,7 @@ private static void WriteErrorDetails(ParseResult parseResult) ConsoleHelpers.ResetTerminalForegroundColor(); ConsoleHelpers.SetTerminalForegroundRed(); - var stdErr = parseResult.InvocationConfiguration.Error ?? - parseResult.Configuration.Error; + var stdErr = parseResult.InvocationConfiguration.Error; foreach (var error in parseResult.Errors) { @@ -99,17 +98,17 @@ private static void WriteTypoCorrectionSuggestions(ParseResult parseResult) { if (first) { - parseResult.Configuration.Output.WriteLine(LocalizationResources.SuggestionsTokenNotMatched(token)); + parseResult.InvocationConfiguration.Output.WriteLine(LocalizationResources.SuggestionsTokenNotMatched(token)); first = false; } - parseResult.Configuration.Output.WriteLine(suggestion); + parseResult.InvocationConfiguration.Output.WriteLine(suggestion); } } if (unmatchedTokens.Count != 0) { - parseResult.Configuration.Output.WriteLine(); + parseResult.InvocationConfiguration.Output.WriteLine(); } static IEnumerable GetPossibleTokens(Command targetSymbol, string token) diff --git a/src/System.CommandLine/Parsing/ParseDiagramAction.cs b/src/System.CommandLine/Parsing/ParseDiagramAction.cs index bbae57609f..f3abe87b27 100644 --- a/src/System.CommandLine/Parsing/ParseDiagramAction.cs +++ b/src/System.CommandLine/Parsing/ParseDiagramAction.cs @@ -20,7 +20,7 @@ internal sealed class ParseDiagramAction : SynchronousCommandLineAction public override int Invoke(ParseResult parseResult) { - parseResult.Configuration.Output.WriteLine(Diagram(parseResult)); + parseResult.InvocationConfiguration.Output.WriteLine(Diagram(parseResult)); return parseResult.Errors.Count == 0 ? 0 : _parseErrorReturnValue; } From ffc98c367940ec40a7b93ca17d2cd64fe323d7ba Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 1 Jul 2025 12:10:54 -0700 Subject: [PATCH 04/16] remove CommandLineConfiguration.RootCommand --- .../CommandLineConfigurationTests.cs | 44 ++++---- .../CompletionTests.cs | 106 ++++++++---------- .../CustomParsingTests.cs | 3 +- .../DirectiveTests.cs | 8 +- .../EnvironmentVariableDirectiveTests.cs | 3 +- .../Help/HelpBuilderTests.cs | 4 +- .../Invocation/InvocationTests.cs | 2 +- .../ParserTests.SetupErrors.cs | 2 +- src/System.CommandLine.Tests/ParserTests.cs | 2 +- .../ResponseFileTests.cs | 5 +- .../TokenReplacementTests.cs | 18 +-- .../CommandLineConfiguration.cs | 85 +++++--------- .../CommandLineConfigurationException.cs | 1 + .../CommandLineInvocationConfiguration.cs | 1 + .../Parsing/CommandLineParser.cs | 4 +- .../Parsing/ParseOperation.cs | 11 +- .../Parsing/StringExtensions.cs | 13 ++- 17 files changed, 142 insertions(+), 170 deletions(-) diff --git a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs b/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs index 63cb3d07f0..85c4c73486 100644 --- a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs +++ b/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs @@ -21,9 +21,9 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_ option2 }; - var config = new CommandLineConfiguration(command); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(command); validate.Should() .Throw() @@ -49,9 +49,9 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_ } }; - var config = new CommandLineConfiguration(command); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(command); validate.Should() .Throw() @@ -74,9 +74,9 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia command2 }; - var config = new CommandLineConfiguration(rootCommand); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() .Throw() @@ -98,9 +98,9 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia } }; - var config = new CommandLineConfiguration(command); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(command); validate.Should() .Throw() @@ -123,9 +123,9 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_ command }; - var config = new CommandLineConfiguration(rootCommand); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() .Throw() @@ -151,9 +151,9 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_ } }; - var config = new CommandLineConfiguration(rootCommand); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() .Throw() @@ -174,9 +174,9 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_a command.Options.Add(option1); command.Options.Add(option2); - var config = new CommandLineConfiguration(command); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(command); validate.Should() .Throw() @@ -198,9 +198,9 @@ public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_ }; rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); - var config = new CommandLineConfiguration(rootCommand); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should().NotThrow(); } @@ -217,9 +217,9 @@ public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_ }; rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); - var config = new CommandLineConfiguration(rootCommand); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should().NotThrow(); } @@ -230,9 +230,9 @@ public void ThrowIfInvalid_throws_if_a_command_is_its_own_parent() var command = new RootCommand(); command.Add(command); - var config = new CommandLineConfiguration(command); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(command); validate.Should() .Throw() @@ -249,9 +249,9 @@ public void ThrowIfInvalid_throws_if_a_parentage_cycle_is_detected() var rootCommand = new RootCommand { command }; command.Add(rootCommand); - var config = new CommandLineConfiguration(rootCommand); + var config = new CommandLineConfiguration(); - var validate = () => config.ThrowIfInvalid(); + var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() .Throw() diff --git a/src/System.CommandLine.Tests/CompletionTests.cs b/src/System.CommandLine.Tests/CompletionTests.cs index a95a982724..4d902ff07a 100644 --- a/src/System.CommandLine.Tests/CompletionTests.cs +++ b/src/System.CommandLine.Tests/CompletionTests.cs @@ -185,8 +185,8 @@ public void Command_GetCompletions_with_text_to_match_orders_by_match_position_t new Command("andmyothersubcommand"), }; - CommandLineConfiguration simpleConfig = new (command); - var completions = command.Parse("my", simpleConfig).GetCompletions(); + + var completions = command.Parse("my").GetCompletions(); completions .Select(item => item.Label) @@ -204,8 +204,8 @@ public void When_an_option_has_a_default_value_it_will_still_be_suggested() new Option("--cherry") }; - CommandLineConfiguration simpleConfig = new (command); - var result = command.Parse("", simpleConfig); + + var result = command.Parse(""); _output.WriteLine(result.ToString()); @@ -235,8 +235,7 @@ public void Command_GetCompletions_can_access_ParseResult() cloneOption }; - CommandLineConfiguration simpleConfig = new (rootCommand); - var result = rootCommand.Parse("--origin test --clone ", simpleConfig); + var result = rootCommand.Parse("--origin test --clone "); _output.WriteLine(result.ToString()); @@ -278,8 +277,8 @@ public void When_one_option_has_been_specified_then_it_and_its_siblings_will_sti }; var commandLine = "--apple grannysmith"; - CommandLineConfiguration simpleConfig = new (command); - var result = command.Parse(commandLine, simpleConfig); + + var result = command.Parse(commandLine); result.GetCompletions(commandLine.Length + 1) .Select(item => item.Label) @@ -306,9 +305,8 @@ public void When_a_subcommand_has_been_specified_then_its_sibling_commands_will_ new Option("--rainier") } }; - CommandLineConfiguration simpleConfig = new (rootCommand); - var result = rootCommand.Parse("cherry ", simpleConfig); + var result = rootCommand.Parse("cherry "); result.GetCompletions() .Select(item => item.Label) @@ -336,9 +334,8 @@ public void When_a_subcommand_has_been_specified_then_its_sibling_commands_alias apple, banana }; - CommandLineConfiguration simpleConfig = new (rootCommand); - var result = rootCommand.Parse("banana ", simpleConfig); + var result = rootCommand.Parse("banana "); result.GetCompletions() .Select(item => item.Label) @@ -356,8 +353,8 @@ public void When_a_subcommand_has_been_specified_then_its_sibling_options_will_n }; var commandLine = "child"; - CommandLineConfiguration simpleConfig = new (command); - var parseResult = command.Parse(commandLine, simpleConfig); + + var parseResult = command.Parse(commandLine); parseResult .GetCompletions(commandLine.Length + 1) @@ -377,8 +374,8 @@ public void When_a_subcommand_has_been_specified_then_its_sibling_options_with_a }; var commandLine = "--parent-option 123 child"; - CommandLineConfiguration simpleConfig = new (command); - var parseResult = command.Parse(commandLine, simpleConfig); + + var parseResult = command.Parse(commandLine); parseResult .GetCompletions(commandLine.Length + 1) @@ -400,8 +397,8 @@ public void When_a_subcommand_has_been_specified_then_its_child_options_will_be_ }; var commandLine = "child "; - CommandLineConfiguration simpleConfig = new (command); - var parseResult = command.Parse(commandLine, simpleConfig); + + var parseResult = command.Parse(commandLine); parseResult .GetCompletions(commandLine.Length + 1) @@ -430,8 +427,8 @@ public void When_a_subcommand_with_subcommands_has_been_specified_then_its_sibli }; var commandLine = "cherry"; - CommandLineConfiguration simpleConfig = new (rootCommand); - var result = rootCommand.Parse(commandLine, simpleConfig); + + var result = rootCommand.Parse(commandLine); result.GetCompletions(commandLine.Length + 1) .Select(item => item.Label) @@ -450,8 +447,8 @@ public void When_one_option_has_been_partially_specified_then_nonmatching_siblin }; var input = "a"; - CommandLineConfiguration simpleConfig = new (command); - var result = command.Parse(input, simpleConfig); + + var result = command.Parse(input); result.GetCompletions(input.Length) .Select(item => item.Label) @@ -472,8 +469,7 @@ public void An_option_can_be_hidden_from_completions_by_setting_IsHidden_to_true new Option("-n") { Description = "Not hidden" } }; - CommandLineConfiguration simpleConfig = new (command); - var completions = command.Parse("the-command ", simpleConfig).GetCompletions(); + var completions = command.Parse("the-command ").GetCompletions(); completions.Select(item => item.Label).Should().NotContain("--hide-me"); } @@ -488,8 +484,8 @@ public void Parser_options_can_supply_context_sensitive_matches() }; var commandLine = "--bread"; - CommandLineConfiguration simpleConfig = new (command); - var result = command.Parse(commandLine, simpleConfig); + + var result = command.Parse(commandLine); result.GetCompletions(commandLine.Length + 1) .Select(item => item.Label) @@ -497,7 +493,7 @@ public void Parser_options_can_supply_context_sensitive_matches() .BeEquivalentTo("rye", "sourdough", "wheat"); commandLine = "--bread wheat --cheese "; - result = command.Parse(commandLine, simpleConfig); + result = command.Parse(commandLine); result.GetCompletions(commandLine.Length + 1) .Select(item => item.Label) @@ -516,8 +512,8 @@ public void Subcommand_names_are_available_as_suggestions() }; var commandLine = "test"; - CommandLineConfiguration simpleConfig = new (command); - command.Parse(commandLine, simpleConfig) + + command.Parse(commandLine) .GetCompletions(commandLine.Length + 1) .Select(item => item.Label) .Should() @@ -535,8 +531,8 @@ public void Both_subcommands_and_options_are_available_as_suggestions() }; var commandLine = "test"; - CommandLineConfiguration simpleConfig = new (command); - command.Parse(commandLine, simpleConfig) + + command.Parse(commandLine) .GetCompletions(commandLine.Length + 1) .Select(item => item.Label) .Should() @@ -555,8 +551,8 @@ public void Option_GetCompletions_are_not_provided_without_matching_prefix(strin new Option("--three") }; - CommandLineConfiguration simpleConfig = new (command); - ParseResult result = command.Parse(input, simpleConfig); + + ParseResult result = command.Parse(input); result.GetCompletions() .Select(item => item.Label) .Should() @@ -574,8 +570,8 @@ public void Option_GetCompletions_can_be_based_on_the_proximate_option() }; var commandLine = "outer"; - CommandLineConfiguration simpleConfig = new (outer); - ParseResult result = outer.Parse(commandLine, simpleConfig); + + ParseResult result = outer.Parse(commandLine); result.GetCompletions(commandLine.Length + 1) .Select(item => item.Label) @@ -593,8 +589,8 @@ public void Argument_completions_can_be_based_on_the_proximate_option() }; var commandLine = "outer --two"; - CommandLineConfiguration simpleConfig = new (outer); - ParseResult result = outer.Parse(commandLine, simpleConfig); + + ParseResult result = outer.Parse(commandLine); result.GetCompletions(commandLine.Length + 1) .Select(item => item.Label) @@ -612,8 +608,7 @@ public void Option_GetCompletions_can_be_based_on_the_proximate_option_and_parti new Command("three", "Command three") }; - CommandLineConfiguration simpleConfig = new (outer); - ParseResult result = outer.Parse("outer o", simpleConfig); + ParseResult result = outer.Parse("outer o"); result.GetCompletions() .Select(item => item.Label) @@ -632,15 +627,15 @@ public void Completions_can_be_provided_in_the_absence_of_validation() option }; - CommandLineConfiguration simpleConfig = new (command); - command.Parse("the-command -t m", simpleConfig) + + command.Parse("the-command -t m") .GetCompletions() .Select(item => item.Label) .Should() .BeEquivalentTo("animal", "mineral"); - command.Parse("the-command -t something-else", simpleConfig) + command.Parse("the-command -t something-else") .Errors .Should() .BeEmpty(); @@ -697,9 +692,7 @@ public void When_caller_does_the_tokenizing_then_argument_completions_are_based_ CreateOptionWithAcceptOnlyFromAmong(name: "three", "three-a", "three-b", "three-c") }; - var configuration = new CommandLineConfiguration(command); - - var result = command.Parse("outer two b", configuration); + var result = command.Parse("outer two b"); result.GetCompletions() .Select(item => item.Label) @@ -788,8 +781,8 @@ public void When_parsing_from_text_if_the_proximate_option_is_completed_then_com CreateOptionWithAcceptOnlyFromAmong(name: "--language", "C#"), new Option("--langVersion") }; - var configuration = new CommandLineConfiguration(command); - var completions = command.Parse("--framework net8.0 --l", configuration).GetCompletions(); + + var completions = command.Parse("--framework net8.0 --l").GetCompletions(); completions.Select(item => item.Label) .Should() @@ -805,8 +798,8 @@ public void When_parsing_from_array_if_the_proximate_option_is_completed_then_co CreateOptionWithAcceptOnlyFromAmong(name: "--language", "C#"), new Option("--langVersion") }; - var configuration = new CommandLineConfiguration(command); - var completions = command.Parse(new[]{"--framework","net8.0","--l"}, configuration).GetCompletions(); + + var completions = command.Parse(new[]{"--framework","net8.0","--l"}).GetCompletions(); completions.Select(item => item.Label) .Should() @@ -840,8 +833,8 @@ public void Options_that_have_been_specified_to_their_maximum_arity_are_not_sugg }; var commandLine = "--allows-one x"; - CommandLineConfiguration simpleConfig = new (command); - var completions = command.Parse(commandLine, simpleConfig).GetCompletions(commandLine.Length + 1); + + var completions = command.Parse(commandLine).GetCompletions(commandLine.Length + 1); completions.Select(item => item.Label) .Should() @@ -918,8 +911,8 @@ public void Default_completions_can_be_cleared_and_replaced() { argument }; - CommandLineConfiguration simpleConfig = new (command); - var completions = command.Parse("the-command s", simpleConfig) + + var completions = command.Parse("the-command s") .GetCompletions(); completions.Select(item => item.Label) @@ -938,8 +931,8 @@ public void Default_completions_can_be_appended_to() } }; - CommandLineConfiguration simpleConfig = new (command); - var completions = command.Parse("the-command s", simpleConfig) + + var completions = command.Parse("the-command s") .GetCompletions(); completions @@ -991,9 +984,8 @@ public void When_option_completions_are_available_then_they_are_suggested_when_a { Option option = new ("--day"); RootCommand rootCommand = new () { option }; - CommandLineConfiguration simpleConfig = new (rootCommand); - var result = rootCommand.Parse("--day SleepyDay", simpleConfig); + var result = rootCommand.Parse("--day SleepyDay"); result.Errors .Should() diff --git a/src/System.CommandLine.Tests/CustomParsingTests.cs b/src/System.CommandLine.Tests/CustomParsingTests.cs index fe706a04cb..7a1d14936d 100644 --- a/src/System.CommandLine.Tests/CustomParsingTests.cs +++ b/src/System.CommandLine.Tests/CustomParsingTests.cs @@ -220,8 +220,7 @@ public void Option_ArgumentResult_Parent_is_set_correctly_when_token_is_implicit } }; - CommandLineConfiguration simpleConfig = new (command); - command.Parse("", simpleConfig); + command.Parse(""); argumentResult .Parent diff --git a/src/System.CommandLine.Tests/DirectiveTests.cs b/src/System.CommandLine.Tests/DirectiveTests.cs index 6a7432fffd..d4a70847b4 100644 --- a/src/System.CommandLine.Tests/DirectiveTests.cs +++ b/src/System.CommandLine.Tests/DirectiveTests.cs @@ -49,11 +49,11 @@ public void Multiple_directives_are_allowed() RootCommand root = new() { new Option("-y") }; Directive parseDirective = new ("parse"); Directive suggestDirective = new ("suggest"); - CommandLineConfiguration config = new(root); + root.Add(parseDirective); root.Add(suggestDirective); - var result = root.Parse("[parse] [suggest] -y", config); + var result = root.Parse("[parse] [suggest] -y"); result.GetResult(parseDirective).Should().NotBeNull(); result.GetResult(suggestDirective).Should().NotBeNull(); @@ -214,10 +214,10 @@ public void When_a_directive_is_specified_more_than_once_then_its_values_are_agg private static ParseResult Parse(Option option, Directive directive, string commandLine) { RootCommand root = new() { option }; - CommandLineConfiguration config = new(root); + root.Directives.Add(directive); - return root.Parse(commandLine, config); + return root.Parse(commandLine); } } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs index 00638bafeb..34e499d700 100644 --- a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs +++ b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs @@ -123,10 +123,9 @@ public void It_does_not_prevent_help_from_being_invoked() var customHelpAction = new CustomHelpAction(); root.Options.OfType().Single().Action = customHelpAction; - var config = new CommandLineConfiguration(root); root.Directives.Add(new EnvironmentVariablesDirective()); - root.Parse($"[env:{_testVariableName}=1] -h", config).Invoke(); + root.Parse($"[env:{_testVariableName}=1] -h").Invoke(); customHelpAction.WasCalled.Should().BeTrue(); Environment.GetEnvironmentVariable(_testVariableName).Should().Be("1"); diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs index 4a735a4aad..1b41d2b9cb 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.cs @@ -1235,9 +1235,9 @@ public void Required_options_are_indicated_when_argument_is_named() [Fact] public void Help_option_is_shown_in_help() { - var configuration = new CommandLineConfiguration(new RootCommand()); + var rootCommand = new RootCommand(); - _helpBuilder.Write(configuration.RootCommand, _console); + _helpBuilder.Write(rootCommand, _console); var help = _console.ToString(); diff --git a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs index 058151adcc..0de312a178 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs @@ -304,7 +304,7 @@ public void Directive_action_takes_precedence_over_option_action() directive }; - ParseResult parseResult = command.Parse("[directive] cmd -x", new CommandLineConfiguration(command)); + ParseResult parseResult = command.Parse("[directive] cmd -x"); using var _ = new AssertionScope(); diff --git a/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs b/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs index d5e2e52202..c6a6e05d1c 100644 --- a/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs +++ b/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs @@ -60,7 +60,7 @@ public void When_command_names_collide_with_directive_names_it_does_not_throw_on var rootCommand = new RootCommand(); rootCommand.Add(new Command("one")); rootCommand.Add(new Directive("one")); - var cliConfiguration = new CommandLineConfiguration(rootCommand); + var cliConfiguration = new CommandLineConfiguration(); rootCommand.Invoking(c => c.Parse("", cliConfiguration)).Should().NotThrow(); } diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 410c67bc82..6d0085ef88 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -137,7 +137,7 @@ public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off } }; - CommandLineConfiguration configuration = new (rootCommand) + CommandLineConfiguration configuration = new() { EnablePosixBundling = false }; diff --git a/src/System.CommandLine.Tests/ResponseFileTests.cs b/src/System.CommandLine.Tests/ResponseFileTests.cs index 5404fdee22..f0225ee334 100644 --- a/src/System.CommandLine.Tests/ResponseFileTests.cs +++ b/src/System.CommandLine.Tests/ResponseFileTests.cs @@ -287,9 +287,8 @@ public void When_response_file_parse_as_space_separated_returns_expected_values( optionOne, optionTwo }; - CommandLineConfiguration config = new (rootCommand); - var result = rootCommand.Parse($"@{responseFile}", config); + var result = rootCommand.Parse($"@{responseFile}"); result.GetValue(optionOne).Should().Be("first value"); result.GetValue(optionTwo).Should().Be(123); @@ -302,7 +301,7 @@ public void When_response_file_processing_is_disabled_then_it_returns_response_f { new Argument>("arg") }; - CommandLineConfiguration configuration = new(command) + CommandLineConfiguration configuration = new() { ResponseFileTokenReplacer = null }; diff --git a/src/System.CommandLine.Tests/TokenReplacementTests.cs b/src/System.CommandLine.Tests/TokenReplacementTests.cs index c96af9b0c2..1b991b9043 100644 --- a/src/System.CommandLine.Tests/TokenReplacementTests.cs +++ b/src/System.CommandLine.Tests/TokenReplacementTests.cs @@ -18,7 +18,7 @@ public void Token_replacer_receives_the_token_from_the_command_line_with_the_lea string receivedToken = null; - CommandLineConfiguration config = new (command) + CommandLineConfiguration config = new () { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -41,7 +41,7 @@ public void Token_replacer_can_expand_argument_values() var command = new RootCommand { argument }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -65,7 +65,7 @@ public void Custom_token_replacer_can_expand_option_argument_values() var command = new RootCommand { option }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -89,7 +89,7 @@ public void Custom_token_replacer_can_expand_subcommands_and_options_and_argumen var command = new RootCommand { new Command("subcommand") { option } }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -113,7 +113,7 @@ public void Expanded_tokens_containing_whitespace_are_parsed_as_single_tokens() var command = new RootCommand { argument }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -135,7 +135,7 @@ public void Token_replacer_can_set_a_custom_error_message() var command = new RootCommand { argument }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -159,7 +159,7 @@ public void When_token_replacer_returns_false_without_setting_an_error_message_t var command = new RootCommand { argument }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -183,7 +183,7 @@ public void Token_replacer_will_delete_token_when_delegate_returns_true_and_sets var command = new RootCommand { argument }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -207,7 +207,7 @@ public void Token_replacer_will_delete_token_when_delegate_returns_true_and_sets var command = new RootCommand { argument }; - CommandLineConfiguration config = new(command) + CommandLineConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { diff --git a/src/System.CommandLine/CommandLineConfiguration.cs b/src/System.CommandLine/CommandLineConfiguration.cs index 2bddefdbd8..923517a2e3 100644 --- a/src/System.CommandLine/CommandLineConfiguration.cs +++ b/src/System.CommandLine/CommandLineConfiguration.cs @@ -11,22 +11,7 @@ namespace System.CommandLine /// public class CommandLineConfiguration { - /// - /// Initializes a new instance of the class. - /// - /// The root command for the parser. - public CommandLineConfiguration(Command rootCommand) - { - RootCommand = rootCommand ?? throw new ArgumentNullException(nameof(rootCommand)); - } - - internal bool HasDirectives => - RootCommand switch - { - RootCommand root => root.Directives.Count > 0, - _ => false - }; - + // FIX: (CommandLineConfiguration) rename /// /// Enables the parser to recognize and expand POSIX-style bundled options. /// @@ -58,65 +43,55 @@ public CommandLineConfiguration(Command rootCommand) /// public TryReplaceToken? ResponseFileTokenReplacer { get; set; } = StringExtensions.TryReadResponseFile; - /// - /// Gets the root command. - /// - public Command RootCommand { get; } - /// /// Throws an exception if the parser configuration is ambiguous or otherwise not valid. /// /// Due to the performance cost of this method, it is recommended to be used in unit testing or in scenarios where the parser is configured dynamically at runtime. /// Thrown if the configuration is found to be invalid. - public void ThrowIfInvalid() + public void ThrowIfInvalid(Command command) { - ThrowIfInvalid(RootCommand); + if (command.Parents.FlattenBreadthFirst(c => c.Parents).Any(ancestor => ancestor == command)) + { + throw new CommandLineConfigurationException($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); + } - static void ThrowIfInvalid(Command command) + int count = command.Subcommands.Count + command.Options.Count; + for (var i = 0; i < count; i++) { - if (command.Parents.FlattenBreadthFirst(c => c.Parents).Any(ancestor => ancestor == command)) + Symbol symbol1 = GetChild(i, command, out AliasSet? aliases1); + for (var j = i + 1; j < count; j++) { - throw new CommandLineConfigurationException($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); - } + Symbol symbol2 = GetChild(j, command, out AliasSet? aliases2); - int count = command.Subcommands.Count + command.Options.Count; - for (var i = 0; i < count; i++) - { - Symbol symbol1 = GetChild(i, command, out AliasSet? aliases1); - for (var j = i + 1; j < count; j++) + if (symbol1.Name.Equals(symbol2.Name, StringComparison.Ordinal) + || (aliases1 is not null && aliases1.Contains(symbol2.Name))) { - Symbol symbol2 = GetChild(j, command, out AliasSet? aliases2); - - if (symbol1.Name.Equals(symbol2.Name, StringComparison.Ordinal) - || (aliases1 is not null && aliases1.Contains(symbol2.Name))) - { - throw new CommandLineConfigurationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'."); - } - else if (aliases2 is not null && aliases2.Contains(symbol1.Name)) - { - throw new CommandLineConfigurationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'."); - } + throw new CommandLineConfigurationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'."); + } + else if (aliases2 is not null && aliases2.Contains(symbol1.Name)) + { + throw new CommandLineConfigurationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'."); + } - if (aliases1 is not null && aliases2 is not null) + if (aliases1 is not null && aliases2 is not null) + { + // take advantage of the fact that we are dealing with two hash sets + if (aliases1.Overlaps(aliases2)) { - // take advantage of the fact that we are dealing with two hash sets - if (aliases1.Overlaps(aliases2)) + foreach (string symbol2Alias in aliases2) { - foreach (string symbol2Alias in aliases2) + if (aliases1.Contains(symbol2Alias)) { - if (aliases1.Contains(symbol2Alias)) - { - throw new CommandLineConfigurationException($"Duplicate alias '{symbol2Alias}' found on command '{command.Name}'."); - } + throw new CommandLineConfigurationException($"Duplicate alias '{symbol2Alias}' found on command '{command.Name}'."); } } } } + } - if (symbol1 is Command childCommand) - { - ThrowIfInvalid(childCommand); - } + if (symbol1 is Command childCommand) + { + ThrowIfInvalid(childCommand); } } diff --git a/src/System.CommandLine/CommandLineConfigurationException.cs b/src/System.CommandLine/CommandLineConfigurationException.cs index 35a49e88a6..3fe6c5d190 100644 --- a/src/System.CommandLine/CommandLineConfigurationException.cs +++ b/src/System.CommandLine/CommandLineConfigurationException.cs @@ -8,6 +8,7 @@ namespace System.CommandLine; /// public class CommandLineConfigurationException : Exception { + // FIX: (CommandLineConfigurationException) rename /// public CommandLineConfigurationException(string message) : base(message) { diff --git a/src/System.CommandLine/CommandLineInvocationConfiguration.cs b/src/System.CommandLine/CommandLineInvocationConfiguration.cs index 4b0b43b402..60d88f42d7 100644 --- a/src/System.CommandLine/CommandLineInvocationConfiguration.cs +++ b/src/System.CommandLine/CommandLineInvocationConfiguration.cs @@ -6,6 +6,7 @@ namespace System.CommandLine; public class CommandLineInvocationConfiguration { + // FIX: (CommandLineInvocationConfiguration) rename private TextWriter? _output, _error; /// diff --git a/src/System.CommandLine/Parsing/CommandLineParser.cs b/src/System.CommandLine/Parsing/CommandLineParser.cs index 69d629b31d..23306cc042 100644 --- a/src/System.CommandLine/Parsing/CommandLineParser.cs +++ b/src/System.CommandLine/Parsing/CommandLineParser.cs @@ -146,9 +146,10 @@ private static ParseResult Parse( throw new ArgumentNullException(nameof(arguments)); } - configuration ??= new CommandLineConfiguration(command); + configuration ??= new CommandLineConfiguration(); arguments.Tokenize( + command, configuration, inferRootCommand: rawInput is not null, out List tokens, @@ -156,6 +157,7 @@ private static ParseResult Parse( var operation = new ParseOperation( tokens, + command, configuration, tokenizationErrors, rawInput); diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index de7dcf2d9c..a2194fb5b2 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -21,22 +21,25 @@ internal sealed class ParseOperation private bool _isTerminatingDirectiveSpecified; private CommandLineAction? _primaryAction; private List? _preActions; + private readonly Command _rootCommand; public ParseOperation( List tokens, + Command rootCommand, CommandLineConfiguration configuration, List? tokenizeErrors, string? rawInput) { _tokens = tokens; _configuration = configuration; + _rootCommand = rootCommand; _rawInput = rawInput; - _symbolResultTree = new(_configuration.RootCommand, tokenizeErrors); + _symbolResultTree = new(_rootCommand, tokenizeErrors); _innermostCommandResult = _rootCommandResult = new CommandResult( - _configuration.RootCommand, + _rootCommand, CurrentToken, _symbolResultTree); - _symbolResultTree.Add(_configuration.RootCommand, _rootCommandResult); + _symbolResultTree.Add(_rootCommand, _rootCommandResult); Advance(); } @@ -296,7 +299,7 @@ private void ParseDirectives() { while (More(out TokenType currentTokenType) && currentTokenType == TokenType.Directive) { - if (_configuration.HasDirectives) + if (_rootCommand is RootCommand { Directives.Count: > 0 }) { ParseDirective(); // kept in separate method to avoid JIT } diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index b1f7b63ad2..5a0d77c45f 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -27,6 +27,7 @@ internal static int IndexOfCaseInsensitive( // this method is not returning a Value Tuple or a dedicated type to avoid JITting internal static void Tokenize( this IReadOnlyList args, + Command rootCommand, CommandLineConfiguration configuration, bool inferRootCommand, out List tokens, @@ -36,22 +37,22 @@ internal static void Tokenize( List? errorList = null; - var currentCommand = configuration.RootCommand; + var currentCommand = rootCommand; var foundDoubleDash = false; var foundEndOfDirectives = false; var tokenList = new List(args.Count); - var knownTokens = configuration.RootCommand.ValidTokens(); + var knownTokens = rootCommand.ValidTokens(); - int i = FirstArgumentIsRootCommand(args, configuration.RootCommand, inferRootCommand) + int i = FirstArgumentIsRootCommand(args, rootCommand, inferRootCommand) ? 0 : FirstArgIsNotRootCommand; for (; i < args.Count; i++) { var arg = i == FirstArgIsNotRootCommand - ? configuration.RootCommand.Name + ? rootCommand.Name : args[i]; if (foundDoubleDash) @@ -96,7 +97,7 @@ internal static void Tokenize( continue; } - if (!configuration.RootCommand.EqualsNameOrAlias(arg)) + if (!rootCommand.EqualsNameOrAlias(arg)) { foundEndOfDirectives = true; } @@ -143,7 +144,7 @@ internal static void Tokenize( Command cmd = (Command)token.Symbol!; if (cmd != currentCommand) { - if (cmd != configuration.RootCommand) + if (cmd != rootCommand) { knownTokens = cmd.ValidTokens(); // config contains Directives, they are allowed only for RootCommand } From 94ecb5cc106ffe0558d5ea9bc4e1f45d82fe956d Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 1 Jul 2025 12:54:18 -0700 Subject: [PATCH 05/16] update API baseline --- ...ommandLine_api_is_not_changed.approved.txt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 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 45cc9f778a..f35a4f3da6 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 @@ -57,23 +57,18 @@ public System.Void SetAction(System.Func action) public System.Void SetAction(System.Func> action) public class CommandLineConfiguration - .ctor(Command rootCommand) - public System.Boolean EnableDefaultExceptionHandler { get; set; } + .ctor() public System.Boolean EnablePosixBundling { get; set; } - public System.IO.TextWriter Error { get; set; } - public System.IO.TextWriter Output { get; set; } - public System.Nullable ProcessTerminationTimeout { get; set; } public System.CommandLine.Parsing.TryReplaceToken ResponseFileTokenReplacer { get; set; } - public Command RootCommand { get; } - public System.Int32 Invoke(System.String commandLine) - public System.Int32 Invoke(System.String[] args) - public System.Threading.Tasks.Task InvokeAsync(System.String commandLine, System.Threading.CancellationToken cancellationToken = null) - public System.Threading.Tasks.Task InvokeAsync(System.String[] args, System.Threading.CancellationToken cancellationToken = null) - public ParseResult Parse(System.Collections.Generic.IReadOnlyList args) - public ParseResult Parse(System.String commandLine) - public System.Void ThrowIfInvalid() + public System.Void ThrowIfInvalid(Command command) public class CommandLineConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable .ctor(System.String message) + public class CommandLineInvocationConfiguration + .ctor() + public System.Boolean EnableDefaultExceptionHandler { get; set; } + public System.IO.TextWriter Error { get; set; } + public System.IO.TextWriter Output { get; set; } + public System.Nullable ProcessTerminationTimeout { get; set; } public static class CompletionSourceExtensions public static System.Void Add(this System.Collections.Generic.List>> completionSources, System.Func> completionsDelegate) public static System.Void Add(this System.Collections.Generic.List>> completionSources, System.String[] completions) @@ -120,6 +115,7 @@ public System.CommandLine.Parsing.CommandResult CommandResult { get; } public CommandLineConfiguration Configuration { get; } public System.Collections.Generic.IReadOnlyList Errors { get; } + public CommandLineInvocationConfiguration InvocationConfiguration { get; set; } public System.CommandLine.Parsing.CommandResult RootCommandResult { get; } public System.Collections.Generic.IReadOnlyList Tokens { get; } public System.Collections.Generic.IReadOnlyList UnmatchedTokens { get; } @@ -138,7 +134,9 @@ public T GetValue(Option option) public T GetValue(System.String name) public System.Int32 Invoke() + public System.Int32 Invoke(CommandLineInvocationConfiguration configuration) public System.Threading.Tasks.Task InvokeAsync(System.Threading.CancellationToken cancellationToken = null) + public System.Threading.Tasks.Task InvokeAsync(CommandLineInvocationConfiguration configuration, System.Threading.CancellationToken cancellationToken = null) public System.String ToString() public class RootCommand : Command, System.Collections.IEnumerable public static System.String ExecutableName { get; } From adfce05b6536b9c813eadd4bb802027b3989618a Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 1 Jul 2025 13:46:37 -0700 Subject: [PATCH 06/16] rename CommandLineConfiguration to ParserConfiguration --- ...onTests.cs => ParserConfigurationTests.cs} | 24 +++++++++---------- .../ParserTests.SetupErrors.cs | 3 +-- src/System.CommandLine.Tests/ParserTests.cs | 2 +- .../ResponseFileTests.cs | 2 +- .../TokenReplacementTests.cs | 18 +++++++------- src/System.CommandLine/Command.cs | 4 ++-- src/System.CommandLine/ParseResult.cs | 4 ++-- ...onfiguration.cs => ParserConfiguration.cs} | 3 +-- .../Parsing/CommandLineParser.cs | 8 +++---- .../Parsing/ParseOperation.cs | 4 ++-- .../Parsing/StringExtensions.cs | 2 +- 11 files changed, 36 insertions(+), 38 deletions(-) rename src/System.CommandLine.Tests/{CommandLineConfigurationTests.cs => ParserConfigurationTests.cs} (92%) rename src/System.CommandLine/{CommandLineConfiguration.cs => ParserConfiguration.cs} (98%) diff --git a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs b/src/System.CommandLine.Tests/ParserConfigurationTests.cs similarity index 92% rename from src/System.CommandLine.Tests/CommandLineConfigurationTests.cs rename to src/System.CommandLine.Tests/ParserConfigurationTests.cs index 85c4c73486..2b34931c63 100644 --- a/src/System.CommandLine.Tests/CommandLineConfigurationTests.cs +++ b/src/System.CommandLine.Tests/ParserConfigurationTests.cs @@ -6,7 +6,7 @@ namespace System.CommandLine.Tests; -public class CommandLineConfigurationTests +public class ParserConfigurationTests { [Fact] public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_on_the_root_command() @@ -21,7 +21,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_ option2 }; - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(command); @@ -49,7 +49,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_ } }; - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(command); @@ -74,7 +74,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia command2 }; - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(rootCommand); @@ -98,7 +98,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia } }; - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(command); @@ -123,7 +123,7 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_ command }; - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(rootCommand); @@ -151,7 +151,7 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_ } }; - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(rootCommand); @@ -174,7 +174,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_a command.Options.Add(option1); command.Options.Add(option2); - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(command); @@ -198,7 +198,7 @@ public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_ }; rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(rootCommand); @@ -217,7 +217,7 @@ public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_ }; rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(rootCommand); @@ -230,7 +230,7 @@ public void ThrowIfInvalid_throws_if_a_command_is_its_own_parent() var command = new RootCommand(); command.Add(command); - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(command); @@ -249,7 +249,7 @@ public void ThrowIfInvalid_throws_if_a_parentage_cycle_is_detected() var rootCommand = new RootCommand { command }; command.Add(rootCommand); - var config = new CommandLineConfiguration(); + var config = new ParserConfiguration(); var validate = () => config.ThrowIfInvalid(rootCommand); diff --git a/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs b/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs index c6a6e05d1c..915a1d4290 100644 --- a/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs +++ b/src/System.CommandLine.Tests/ParserTests.SetupErrors.cs @@ -60,9 +60,8 @@ public void When_command_names_collide_with_directive_names_it_does_not_throw_on var rootCommand = new RootCommand(); rootCommand.Add(new Command("one")); rootCommand.Add(new Directive("one")); - var cliConfiguration = new CommandLineConfiguration(); - rootCommand.Invoking(c => c.Parse("", cliConfiguration)).Should().NotThrow(); + rootCommand.Invoking(c => c.Parse("")).Should().NotThrow(); } } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 6d0085ef88..09a3d8a397 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -137,7 +137,7 @@ public void Options_short_forms_do_not_get_unbundled_if_unbundling_is_turned_off } }; - CommandLineConfiguration configuration = new() + ParserConfiguration configuration = new() { EnablePosixBundling = false }; diff --git a/src/System.CommandLine.Tests/ResponseFileTests.cs b/src/System.CommandLine.Tests/ResponseFileTests.cs index f0225ee334..1528009e3b 100644 --- a/src/System.CommandLine.Tests/ResponseFileTests.cs +++ b/src/System.CommandLine.Tests/ResponseFileTests.cs @@ -301,7 +301,7 @@ public void When_response_file_processing_is_disabled_then_it_returns_response_f { new Argument>("arg") }; - CommandLineConfiguration configuration = new() + ParserConfiguration configuration = new() { ResponseFileTokenReplacer = null }; diff --git a/src/System.CommandLine.Tests/TokenReplacementTests.cs b/src/System.CommandLine.Tests/TokenReplacementTests.cs index 1b991b9043..908ba5cc23 100644 --- a/src/System.CommandLine.Tests/TokenReplacementTests.cs +++ b/src/System.CommandLine.Tests/TokenReplacementTests.cs @@ -18,7 +18,7 @@ public void Token_replacer_receives_the_token_from_the_command_line_with_the_lea string receivedToken = null; - CommandLineConfiguration config = new () + ParserConfiguration config = new () { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -41,7 +41,7 @@ public void Token_replacer_can_expand_argument_values() var command = new RootCommand { argument }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -65,7 +65,7 @@ public void Custom_token_replacer_can_expand_option_argument_values() var command = new RootCommand { option }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -89,7 +89,7 @@ public void Custom_token_replacer_can_expand_subcommands_and_options_and_argumen var command = new RootCommand { new Command("subcommand") { option } }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -113,7 +113,7 @@ public void Expanded_tokens_containing_whitespace_are_parsed_as_single_tokens() var command = new RootCommand { argument }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -135,7 +135,7 @@ public void Token_replacer_can_set_a_custom_error_message() var command = new RootCommand { argument }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -159,7 +159,7 @@ public void When_token_replacer_returns_false_without_setting_an_error_message_t var command = new RootCommand { argument }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -183,7 +183,7 @@ public void Token_replacer_will_delete_token_when_delegate_returns_true_and_sets var command = new RootCommand { argument }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { @@ -207,7 +207,7 @@ public void Token_replacer_will_delete_token_when_delegate_returns_true_and_sets var command = new RootCommand { argument }; - CommandLineConfiguration config = new() + ParserConfiguration config = new() { ResponseFileTokenReplacer = (string tokenToReplace, out IReadOnlyList tokens, out string message) => { diff --git a/src/System.CommandLine/Command.cs b/src/System.CommandLine/Command.cs index 7a1bc15941..44e60aa1e8 100644 --- a/src/System.CommandLine/Command.cs +++ b/src/System.CommandLine/Command.cs @@ -222,7 +222,7 @@ public void SetAction(Func> action) /// The string arguments to parse. /// The configuration on which the parser's grammar and behaviors are based. /// A parse result describing the outcome of the parse operation. - public ParseResult Parse(IReadOnlyList args, CommandLineConfiguration? configuration = null) + public ParseResult Parse(IReadOnlyList args, ParserConfiguration? configuration = null) => CommandLineParser.Parse(this, args, configuration); /// @@ -232,7 +232,7 @@ public ParseResult Parse(IReadOnlyList args, CommandLineConfiguration? c /// A command line string to parse, which can include spaces and quotes equivalent to what can be entered into a terminal. /// The configuration on which the parser's grammar and behaviors are based. /// A parse result describing the outcome of the parse operation. - public ParseResult Parse(string commandLine, CommandLineConfiguration? configuration = null) + public ParseResult Parse(string commandLine, ParserConfiguration? configuration = null) => CommandLineParser.Parse(this, commandLine, configuration); /// diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index ce27a0b353..a9421fd109 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -24,7 +24,7 @@ public sealed class ParseResult private CommandLineInvocationConfiguration? _invocationConfiguration; internal ParseResult( - CommandLineConfiguration configuration, + ParserConfiguration configuration, CommandResult rootCommandResult, CommandResult commandResult, List tokens, @@ -69,7 +69,7 @@ internal ParseResult( /// /// The configuration used to produce the parse result. /// - public CommandLineConfiguration Configuration { get; private set; } + public ParserConfiguration Configuration { get; private set; } /// /// The configuration used to specify command line runtime behavior. diff --git a/src/System.CommandLine/CommandLineConfiguration.cs b/src/System.CommandLine/ParserConfiguration.cs similarity index 98% rename from src/System.CommandLine/CommandLineConfiguration.cs rename to src/System.CommandLine/ParserConfiguration.cs index 923517a2e3..67fcaf71ee 100644 --- a/src/System.CommandLine/CommandLineConfiguration.cs +++ b/src/System.CommandLine/ParserConfiguration.cs @@ -9,9 +9,8 @@ namespace System.CommandLine /// /// Represents the configuration used by the . /// - public class CommandLineConfiguration + public class ParserConfiguration { - // FIX: (CommandLineConfiguration) rename /// /// Enables the parser to recognize and expand POSIX-style bundled options. /// diff --git a/src/System.CommandLine/Parsing/CommandLineParser.cs b/src/System.CommandLine/Parsing/CommandLineParser.cs index 23306cc042..ee60feddda 100644 --- a/src/System.CommandLine/Parsing/CommandLineParser.cs +++ b/src/System.CommandLine/Parsing/CommandLineParser.cs @@ -18,7 +18,7 @@ public static class CommandLineParser /// The string array typically passed to a program's Main method. /// The configuration on which the parser's grammar and behaviors are based. /// A providing details about the parse operation. - public static ParseResult Parse(Command command, IReadOnlyList args, CommandLineConfiguration? configuration = null) + public static ParseResult Parse(Command command, IReadOnlyList args, ParserConfiguration? configuration = null) => Parse(command, args, null, configuration); /// @@ -29,7 +29,7 @@ public static ParseResult Parse(Command command, IReadOnlyList args, Com /// The configuration on which the parser's grammar and behaviors are based. /// The command line string input will be split into tokens as if it had been passed on the command line. /// A providing details about the parse operation. - public static ParseResult Parse(Command command, string commandLine, CommandLineConfiguration? configuration = null) + public static ParseResult Parse(Command command, string commandLine, ParserConfiguration? configuration = null) => Parse(command, SplitCommandLine(commandLine).ToArray(), commandLine, configuration); /// @@ -139,14 +139,14 @@ private static ParseResult Parse( Command command, IReadOnlyList arguments, string? rawInput, - CommandLineConfiguration? configuration) + ParserConfiguration? configuration) { if (arguments is null) { throw new ArgumentNullException(nameof(arguments)); } - configuration ??= new CommandLineConfiguration(); + configuration ??= new ParserConfiguration(); arguments.Tokenize( command, diff --git a/src/System.CommandLine/Parsing/ParseOperation.cs b/src/System.CommandLine/Parsing/ParseOperation.cs index a2194fb5b2..8ac410566c 100644 --- a/src/System.CommandLine/Parsing/ParseOperation.cs +++ b/src/System.CommandLine/Parsing/ParseOperation.cs @@ -10,7 +10,7 @@ namespace System.CommandLine.Parsing internal sealed class ParseOperation { private readonly List _tokens; - private readonly CommandLineConfiguration _configuration; + private readonly ParserConfiguration _configuration; private readonly string? _rawInput; private readonly SymbolResultTree _symbolResultTree; private readonly CommandResult _rootCommandResult; @@ -26,7 +26,7 @@ internal sealed class ParseOperation public ParseOperation( List tokens, Command rootCommand, - CommandLineConfiguration configuration, + ParserConfiguration configuration, List? tokenizeErrors, string? rawInput) { diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 5a0d77c45f..a018f04fcd 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -28,7 +28,7 @@ internal static int IndexOfCaseInsensitive( internal static void Tokenize( this IReadOnlyList args, Command rootCommand, - CommandLineConfiguration configuration, + ParserConfiguration configuration, bool inferRootCommand, out List tokens, out List? errors) From 15272c1f94849c4fff2c85eecc7e654cd08c3007 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 1 Jul 2025 14:10:22 -0700 Subject: [PATCH 07/16] rename CommandLineInvocationConfiguration to InvocationConfiguration --- .../SuggestionShellScriptHandlerTest.cs | 2 +- src/System.CommandLine.Suggest/SuggestionDispatcher.cs | 6 +++--- .../EnvironmentVariableDirectiveTests.cs | 6 +++--- .../Help/HelpBuilderTests.Customization.cs | 2 +- .../Invocation/CancelOnProcessTerminationTests.cs | 2 +- ...figurationTests.cs => InvocationConfigurationTests.cs} | 4 ++-- src/System.CommandLine.Tests/UseExceptionHandlerTests.cs | 2 +- ...ocationConfiguration.cs => InvocationConfiguration.cs} | 3 +-- src/System.CommandLine/ParseResult.cs | 8 ++++---- 9 files changed, 17 insertions(+), 18 deletions(-) rename src/System.CommandLine.Tests/{CommandLineInvocationConfigurationTests.cs => InvocationConfigurationTests.cs} (86%) rename src/System.CommandLine/{CommandLineInvocationConfiguration.cs => InvocationConfiguration.cs} (94%) diff --git a/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs b/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs index 5456b3fca9..4cdf78fa9f 100644 --- a/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs +++ b/src/System.CommandLine.Suggest.Tests/SuggestionShellScriptHandlerTest.cs @@ -12,7 +12,7 @@ namespace System.CommandLine.Suggest.Tests public class SuggestionShellScriptHandlerTest { private readonly RootCommand _rootCommand; - private readonly CommandLineInvocationConfiguration _configuration; + private readonly InvocationConfiguration _configuration; public SuggestionShellScriptHandlerTest() { diff --git a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs index 5a36559fb3..db81098a9c 100644 --- a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs +++ b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs @@ -13,7 +13,7 @@ public class SuggestionDispatcher { private readonly ISuggestionRegistration _suggestionRegistration; private readonly ISuggestionStore _suggestionStore; - private readonly CommandLineInvocationConfiguration _invocationConfig; + private readonly InvocationConfiguration _invocationConfig; public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISuggestionStore suggestionStore = null) { @@ -71,7 +71,7 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug CompleteScriptCommand, }; RootCommand.TreatUnmatchedTokensAsErrors = false; - Configuration = new CommandLineInvocationConfiguration(); + Configuration = new InvocationConfiguration(); } private Command CompleteScriptCommand { get; } @@ -100,7 +100,7 @@ private static Option GetExecutableOption() private Command RegisterCommand { get; } - public CommandLineInvocationConfiguration Configuration { get; } + public InvocationConfiguration Configuration { get; } public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(5000); diff --git a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs index 34e499d700..218cd3a8e0 100644 --- a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs +++ b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs @@ -29,7 +29,7 @@ public async Task Sets_environment_variable_to_value() Environment.GetEnvironmentVariable(_testVariableName).Should().Be(value); }); - var config = new CommandLineInvocationConfiguration + var config = new InvocationConfiguration { EnableDefaultExceptionHandler = false }; @@ -54,7 +54,7 @@ public async Task Sets_environment_variable_value_containing_equals_sign() Environment.GetEnvironmentVariable(_testVariableName).Should().Be(value); }); - var config = new CommandLineInvocationConfiguration + var config = new InvocationConfiguration { EnableDefaultExceptionHandler = false }; @@ -79,7 +79,7 @@ public async Task Ignores_environment_directive_without_equals_sign() Environment.GetEnvironmentVariable(variable).Should().BeNull(); }); - var config = new CommandLineInvocationConfiguration + var config = new InvocationConfiguration { EnableDefaultExceptionHandler = false }; diff --git a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs index e45521f28c..703f33d221 100644 --- a/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs +++ b/src/System.CommandLine.Tests/Help/HelpBuilderTests.Customization.cs @@ -517,7 +517,7 @@ private string GetDefaultHelp(Command command, bool trimOneNewline = true) command.Options.Add(defaultHelp); } - CommandLineInvocationConfiguration config = new() + InvocationConfiguration config = new() { Output = new StringWriter() }; diff --git a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs index e9ceb43521..3197ad3198 100644 --- a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs @@ -56,7 +56,7 @@ private static Task Program(string[] args) }; command.Action = new CustomCliAction(); - var config = new CommandLineInvocationConfiguration + var config = new InvocationConfiguration { ProcessTerminationTimeout = TimeSpan.FromSeconds(2) }; diff --git a/src/System.CommandLine.Tests/CommandLineInvocationConfigurationTests.cs b/src/System.CommandLine.Tests/InvocationConfigurationTests.cs similarity index 86% rename from src/System.CommandLine.Tests/CommandLineInvocationConfigurationTests.cs rename to src/System.CommandLine.Tests/InvocationConfigurationTests.cs index 7d3f331c02..a92626c709 100644 --- a/src/System.CommandLine.Tests/CommandLineInvocationConfigurationTests.cs +++ b/src/System.CommandLine.Tests/InvocationConfigurationTests.cs @@ -3,7 +3,7 @@ namespace System.CommandLine.Tests; -public class CommandLineInvocationConfigurationTests +public class InvocationConfigurationTests { [Fact] public void It_can_be_subclassed_to_provide_additional_context() @@ -28,7 +28,7 @@ public void It_can_be_subclassed_to_provide_additional_context() commandWasInvoked.Should().BeTrue(); } - public class CustomAppConfiguration : CommandLineInvocationConfiguration + public class CustomAppConfiguration : InvocationConfiguration { public CustomAppConfiguration() { diff --git a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs index 7c3c194399..201f0ee869 100644 --- a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs +++ b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs @@ -59,7 +59,7 @@ public async Task Exception_output_can_be_customized(bool async) Command command = new("the-command"); command.SetAction((_, __) => throw expectedException); - CommandLineInvocationConfiguration config = new() + InvocationConfiguration config = new() { Error = new StringWriter(), EnableDefaultExceptionHandler = false diff --git a/src/System.CommandLine/CommandLineInvocationConfiguration.cs b/src/System.CommandLine/InvocationConfiguration.cs similarity index 94% rename from src/System.CommandLine/CommandLineInvocationConfiguration.cs rename to src/System.CommandLine/InvocationConfiguration.cs index 60d88f42d7..a7eaeedd85 100644 --- a/src/System.CommandLine/CommandLineInvocationConfiguration.cs +++ b/src/System.CommandLine/InvocationConfiguration.cs @@ -4,9 +4,8 @@ namespace System.CommandLine; -public class CommandLineInvocationConfiguration +public class InvocationConfiguration { - // FIX: (CommandLineInvocationConfiguration) rename private TextWriter? _output, _error; /// diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index a9421fd109..f265351dea 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -21,7 +21,7 @@ public sealed class ParseResult private CompletionContext? _completionContext; private readonly CommandLineAction? _action; private readonly List? _preActions; - private CommandLineInvocationConfiguration? _invocationConfiguration; + private InvocationConfiguration? _invocationConfiguration; internal ParseResult( ParserConfiguration configuration, @@ -74,7 +74,7 @@ internal ParseResult( /// /// The configuration used to specify command line runtime behavior. /// - public CommandLineInvocationConfiguration InvocationConfiguration + public InvocationConfiguration InvocationConfiguration { get => _invocationConfiguration ??= new(); set => _invocationConfiguration = value; @@ -282,7 +282,7 @@ public Task InvokeAsync(CancellationToken cancellationToken = default) /// /// A token that can be used to cancel an invocation. /// A task whose result can be used as a process exit code. - public Task InvokeAsync(CommandLineInvocationConfiguration configuration, CancellationToken cancellationToken = default) + public Task InvokeAsync(InvocationConfiguration configuration, CancellationToken cancellationToken = default) { InvocationConfiguration = configuration; @@ -328,7 +328,7 @@ public int Invoke() /// Invokes the appropriate command handler for a parsed command line input. /// /// A value that can be used as a process exit code. - public int Invoke(CommandLineInvocationConfiguration configuration) + public int Invoke(InvocationConfiguration configuration) { InvocationConfiguration = configuration; From ea6c7a4a9d57d0e2870613db5c06a4376df2005a Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 1 Jul 2025 14:11:02 -0700 Subject: [PATCH 08/16] rename CommandLineConfigurationException to ParserConfigurationException --- .../ParserConfigurationTests.cs | 18 +++++++++--------- src/System.CommandLine/ParserConfiguration.cs | 10 +++++----- ...tion.cs => ParserConfigurationException.cs} | 5 ++--- 3 files changed, 16 insertions(+), 17 deletions(-) rename src/System.CommandLine/{CommandLineConfigurationException.cs => ParserConfigurationException.cs} (63%) diff --git a/src/System.CommandLine.Tests/ParserConfigurationTests.cs b/src/System.CommandLine.Tests/ParserConfigurationTests.cs index 2b34931c63..e26ef059c6 100644 --- a/src/System.CommandLine.Tests/ParserConfigurationTests.cs +++ b/src/System.CommandLine.Tests/ParserConfigurationTests.cs @@ -26,7 +26,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_ var validate = () => config.ThrowIfInvalid(command); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -54,7 +54,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_ var validate = () => config.ThrowIfInvalid(command); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -79,7 +79,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -103,7 +103,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia var validate = () => config.ThrowIfInvalid(command); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -128,7 +128,7 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_ var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -156,7 +156,7 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_ var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -179,7 +179,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_a var validate = () => config.ThrowIfInvalid(command); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -235,7 +235,7 @@ public void ThrowIfInvalid_throws_if_a_command_is_its_own_parent() var validate = () => config.ThrowIfInvalid(command); validate.Should() - .Throw() + .Throw() .Which .Message .Should() @@ -254,7 +254,7 @@ public void ThrowIfInvalid_throws_if_a_parentage_cycle_is_detected() var validate = () => config.ThrowIfInvalid(rootCommand); validate.Should() - .Throw() + .Throw() .Which .Message .Should() diff --git a/src/System.CommandLine/ParserConfiguration.cs b/src/System.CommandLine/ParserConfiguration.cs index 67fcaf71ee..8f86e40389 100644 --- a/src/System.CommandLine/ParserConfiguration.cs +++ b/src/System.CommandLine/ParserConfiguration.cs @@ -46,12 +46,12 @@ public class ParserConfiguration /// Throws an exception if the parser configuration is ambiguous or otherwise not valid. /// /// Due to the performance cost of this method, it is recommended to be used in unit testing or in scenarios where the parser is configured dynamically at runtime. - /// Thrown if the configuration is found to be invalid. + /// Thrown if the configuration is found to be invalid. public void ThrowIfInvalid(Command command) { if (command.Parents.FlattenBreadthFirst(c => c.Parents).Any(ancestor => ancestor == command)) { - throw new CommandLineConfigurationException($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); + throw new ParserConfigurationException($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); } int count = command.Subcommands.Count + command.Options.Count; @@ -65,11 +65,11 @@ public void ThrowIfInvalid(Command command) if (symbol1.Name.Equals(symbol2.Name, StringComparison.Ordinal) || (aliases1 is not null && aliases1.Contains(symbol2.Name))) { - throw new CommandLineConfigurationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'."); + throw new ParserConfigurationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'."); } else if (aliases2 is not null && aliases2.Contains(symbol1.Name)) { - throw new CommandLineConfigurationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'."); + throw new ParserConfigurationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'."); } if (aliases1 is not null && aliases2 is not null) @@ -81,7 +81,7 @@ public void ThrowIfInvalid(Command command) { if (aliases1.Contains(symbol2Alias)) { - throw new CommandLineConfigurationException($"Duplicate alias '{symbol2Alias}' found on command '{command.Name}'."); + throw new ParserConfigurationException($"Duplicate alias '{symbol2Alias}' found on command '{command.Name}'."); } } } diff --git a/src/System.CommandLine/CommandLineConfigurationException.cs b/src/System.CommandLine/ParserConfigurationException.cs similarity index 63% rename from src/System.CommandLine/CommandLineConfigurationException.cs rename to src/System.CommandLine/ParserConfigurationException.cs index 3fe6c5d190..fbdf186e3a 100644 --- a/src/System.CommandLine/CommandLineConfigurationException.cs +++ b/src/System.CommandLine/ParserConfigurationException.cs @@ -6,11 +6,10 @@ namespace System.CommandLine; /// /// Indicates that a command line configuration is invalid. /// -public class CommandLineConfigurationException : Exception +public class ParserConfigurationException : Exception { - // FIX: (CommandLineConfigurationException) rename /// - public CommandLineConfigurationException(string message) : base(message) + public ParserConfigurationException(string message) : base(message) { } } \ No newline at end of file From 4f0834b7db22d745243941cf44d0e5d9ccb04e92 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 1 Jul 2025 14:16:59 -0700 Subject: [PATCH 09/16] update API baseline --- ...ommandLine_api_is_not_changed.approved.txt | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 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 f35a4f3da6..618403407a 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 @@ -49,26 +49,13 @@ public System.Void Add(Option option) public System.Void Add(Command command) public System.Collections.Generic.IEnumerable GetCompletions(System.CommandLine.Completions.CompletionContext context) - public ParseResult Parse(System.Collections.Generic.IReadOnlyList args, CommandLineConfiguration configuration = null) - public ParseResult Parse(System.String commandLine, CommandLineConfiguration configuration = null) + public ParseResult Parse(System.Collections.Generic.IReadOnlyList args, ParserConfiguration configuration = null) + public ParseResult Parse(System.String commandLine, ParserConfiguration configuration = null) public System.Void SetAction(System.Action action) public System.Void SetAction(System.Func action) public System.Void SetAction(System.Func action) public System.Void SetAction(System.Func action) public System.Void SetAction(System.Func> action) - public class CommandLineConfiguration - .ctor() - public System.Boolean EnablePosixBundling { get; set; } - public System.CommandLine.Parsing.TryReplaceToken ResponseFileTokenReplacer { get; set; } - public System.Void ThrowIfInvalid(Command command) - public class CommandLineConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable - .ctor(System.String message) - public class CommandLineInvocationConfiguration - .ctor() - public System.Boolean EnableDefaultExceptionHandler { get; set; } - public System.IO.TextWriter Error { get; set; } - public System.IO.TextWriter Output { get; set; } - public System.Nullable ProcessTerminationTimeout { get; set; } public static class CompletionSourceExtensions public static System.Void Add(this System.Collections.Generic.List>> completionSources, System.Func> completionsDelegate) public static System.Void Add(this System.Collections.Generic.List>> completionSources, System.String[] completions) @@ -83,6 +70,12 @@ public class EnvironmentVariablesDirective : Directive .ctor() public System.CommandLine.Invocation.CommandLineAction Action { get; set; } + public class InvocationConfiguration + .ctor() + public System.Boolean EnableDefaultExceptionHandler { get; set; } + public System.IO.TextWriter Error { get; set; } + public System.IO.TextWriter Output { get; set; } + public System.Nullable ProcessTerminationTimeout { get; set; } public abstract class Option : Symbol public System.CommandLine.Invocation.CommandLineAction Action { get; set; } public System.Collections.Generic.ICollection Aliases { get; } @@ -110,12 +103,19 @@ public static Option AcceptExistingOnly(this Option option) public static Option AcceptExistingOnly(this Option option) public static Option AcceptExistingOnly(this Option option) + public class ParserConfiguration + .ctor() + public System.Boolean EnablePosixBundling { get; set; } + public System.CommandLine.Parsing.TryReplaceToken ResponseFileTokenReplacer { get; set; } + public System.Void ThrowIfInvalid(Command command) + public class ParserConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable + .ctor(System.String message) public class ParseResult public System.CommandLine.Invocation.CommandLineAction Action { get; } public System.CommandLine.Parsing.CommandResult CommandResult { get; } - public CommandLineConfiguration Configuration { get; } + public ParserConfiguration Configuration { get; } public System.Collections.Generic.IReadOnlyList Errors { get; } - public CommandLineInvocationConfiguration InvocationConfiguration { get; set; } + public InvocationConfiguration InvocationConfiguration { get; set; } public System.CommandLine.Parsing.CommandResult RootCommandResult { get; } public System.Collections.Generic.IReadOnlyList Tokens { get; } public System.Collections.Generic.IReadOnlyList UnmatchedTokens { get; } @@ -134,9 +134,9 @@ public T GetValue(Option option) public T GetValue(System.String name) public System.Int32 Invoke() - public System.Int32 Invoke(CommandLineInvocationConfiguration configuration) + public System.Int32 Invoke(InvocationConfiguration configuration) public System.Threading.Tasks.Task InvokeAsync(System.Threading.CancellationToken cancellationToken = null) - public System.Threading.Tasks.Task InvokeAsync(CommandLineInvocationConfiguration configuration, System.Threading.CancellationToken cancellationToken = null) + public System.Threading.Tasks.Task InvokeAsync(InvocationConfiguration configuration, System.Threading.CancellationToken cancellationToken = null) public System.String ToString() public class RootCommand : Command, System.Collections.IEnumerable public static System.String ExecutableName { get; } @@ -210,8 +210,8 @@ System.CommandLine.Parsing public System.Void OnlyTake(System.Int32 numberOfTokens) public System.String ToString() public static class CommandLineParser - public static System.CommandLine.ParseResult Parse(System.CommandLine.Command command, System.Collections.Generic.IReadOnlyList args, System.CommandLine.CommandLineConfiguration configuration = null) - public static System.CommandLine.ParseResult Parse(System.CommandLine.Command command, System.String commandLine, System.CommandLine.CommandLineConfiguration configuration = null) + public static System.CommandLine.ParseResult Parse(System.CommandLine.Command command, System.Collections.Generic.IReadOnlyList args, System.CommandLine.ParserConfiguration configuration = null) + public static System.CommandLine.ParseResult Parse(System.CommandLine.Command command, System.String commandLine, System.CommandLine.ParserConfiguration configuration = null) public static System.Collections.Generic.IEnumerable SplitCommandLine(System.String commandLine) public class CommandResult : SymbolResult public System.Collections.Generic.IEnumerable Children { get; } From deb5894ba7fcca90ad52556194b401122db0ba3e Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 1 Jul 2025 16:33:23 -0700 Subject: [PATCH 10/16] fix a couple of warnings --- src/System.CommandLine.Suggest/SuggestionDispatcher.cs | 1 - src/System.CommandLine/ParseResult.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs index db81098a9c..ec16bd3f16 100644 --- a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs +++ b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs @@ -13,7 +13,6 @@ public class SuggestionDispatcher { private readonly ISuggestionRegistration _suggestionRegistration; private readonly ISuggestionStore _suggestionStore; - private readonly InvocationConfiguration _invocationConfig; public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISuggestionStore suggestionStore = null) { diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index f265351dea..0590c500d2 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -280,6 +280,7 @@ public Task InvokeAsync(CancellationToken cancellationToken = default) /// /// Invokes the appropriate command handler for a parsed command line input. /// + /// The configuration on which the parser's grammar and behaviors are based. /// A token that can be used to cancel an invocation. /// A task whose result can be used as a process exit code. public Task InvokeAsync(InvocationConfiguration configuration, CancellationToken cancellationToken = default) From e806e8adb97d48dd94985321fbad70974236f837 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Wed, 2 Jul 2025 09:30:45 -0700 Subject: [PATCH 11/16] Update src/System.CommandLine/ParseResult.cs Co-authored-by: Adam Sitnik --- src/System.CommandLine/ParseResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 0590c500d2..b5f442e61d 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -77,7 +77,7 @@ internal ParseResult( public InvocationConfiguration InvocationConfiguration { get => _invocationConfiguration ??= new(); - set => _invocationConfiguration = value; + private set => _invocationConfiguration = value; } /// From 3cf80953676b6c59fec10fc60f9ca6113ab22b8f Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Wed, 2 Jul 2025 10:53:52 -0700 Subject: [PATCH 12/16] PR comments, API baseline update --- ...ovalTests.System_CommandLine_api_is_not_changed.approved.txt | 2 +- src/System.CommandLine/ParseResult.cs | 2 +- 2 files changed, 2 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 618403407a..056ea7663a 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 @@ -115,7 +115,7 @@ public System.CommandLine.Parsing.CommandResult CommandResult { get; } public ParserConfiguration Configuration { get; } public System.Collections.Generic.IReadOnlyList Errors { get; } - public InvocationConfiguration InvocationConfiguration { get; set; } + public InvocationConfiguration InvocationConfiguration { get; } public System.CommandLine.Parsing.CommandResult RootCommandResult { get; } public System.Collections.Generic.IReadOnlyList Tokens { get; } public System.Collections.Generic.IReadOnlyList UnmatchedTokens { get; } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index b5f442e61d..2d9d879f8b 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -69,7 +69,7 @@ internal ParseResult( /// /// The configuration used to produce the parse result. /// - public ParserConfiguration Configuration { get; private set; } + public ParserConfiguration Configuration { get; } /// /// The configuration used to specify command line runtime behavior. From 9e8531442832038401934588b8cbbfe273eab241 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Wed, 2 Jul 2025 12:13:29 -0700 Subject: [PATCH 13/16] fix test app code --- .../EndToEndTestApp/Program.cs | 4 +--- src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs index d11e622ce2..eef852577c 100644 --- a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs +++ b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs @@ -33,9 +33,7 @@ static async Task Main(string[] args) return Task.CompletedTask; }); - CommandLineConfiguration commandLine = new (rootCommand); - - await commandLine.InvokeAsync(args); + await rootCommand.Parse(args).InvokeAsync(); } } } diff --git a/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs b/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs index 37c80614cd..c390af65a6 100644 --- a/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs +++ b/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs @@ -17,7 +17,7 @@ private static int Main(string[] args) command.SetAction(Run); - return new CommandLineConfiguration(command).Invoke(args); + return command.Parse(args).Invoke(); void Run(ParseResult parseResult) { From 24aed5188754a9cb037bb253548364acbb9b8baf Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Mon, 7 Jul 2025 11:09:28 -0700 Subject: [PATCH 14/16] Consolidate ParseResult.InvokeAsync overloads, remove ThrowIfInvalid --- ...ommandLine_api_is_not_changed.approved.txt | 4 +- .../ParserConfigurationTests.cs | 263 ------------------ src/System.CommandLine/ParseResult.cs | 21 +- src/System.CommandLine/ParserConfiguration.cs | 65 ----- 4 files changed, 10 insertions(+), 343 deletions(-) delete mode 100644 src/System.CommandLine.Tests/ParserConfigurationTests.cs 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 056ea7663a..3c748ba872 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 @@ -107,7 +107,6 @@ .ctor() public System.Boolean EnablePosixBundling { get; set; } public System.CommandLine.Parsing.TryReplaceToken ResponseFileTokenReplacer { get; set; } - public System.Void ThrowIfInvalid(Command command) public class ParserConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable .ctor(System.String message) public class ParseResult @@ -135,8 +134,7 @@ public T GetValue(System.String name) public System.Int32 Invoke() public System.Int32 Invoke(InvocationConfiguration configuration) - public System.Threading.Tasks.Task InvokeAsync(System.Threading.CancellationToken cancellationToken = null) - public System.Threading.Tasks.Task InvokeAsync(InvocationConfiguration configuration, System.Threading.CancellationToken cancellationToken = null) + public System.Threading.Tasks.Task InvokeAsync(InvocationConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = null) public System.String ToString() public class RootCommand : Command, System.Collections.IEnumerable public static System.String ExecutableName { get; } diff --git a/src/System.CommandLine.Tests/ParserConfigurationTests.cs b/src/System.CommandLine.Tests/ParserConfigurationTests.cs deleted file mode 100644 index e26ef059c6..0000000000 --- a/src/System.CommandLine.Tests/ParserConfigurationTests.cs +++ /dev/null @@ -1,263 +0,0 @@ -// 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 FluentAssertions; -using Xunit; - -namespace System.CommandLine.Tests; - -public class ParserConfigurationTests -{ - [Fact] - public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_on_the_root_command() - { - var option1 = new Option("--dupe"); - var option2 = new Option("-y"); - option2.Aliases.Add("--dupe"); - - var command = new RootCommand - { - option1, - option2 - }; - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(command); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be($"Duplicate alias '--dupe' found on command '{command.Name}'."); - } - - [Fact] - public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_on_a_subcommand() - { - var option1 = new Option("--dupe"); - var option2 = new Option("--ok"); - option2.Aliases.Add("--dupe"); - - var command = new RootCommand - { - new Command("subcommand") - { - option1, - option2 - } - }; - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(command); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be("Duplicate alias '--dupe' found on command 'subcommand'."); - } - - [Fact] - public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_aliases_on_the_root_command() - { - var command1 = new Command("dupe"); - var command2 = new Command("not-a-dupe"); - command2.Aliases.Add("dupe"); - - var rootCommand = new RootCommand - { - command1, - command2 - }; - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(rootCommand); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'."); - } - - [Fact] - public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_aliases_on_a_subcommand() - { - var command = new RootCommand - { - new Command("subcommand") - { - new Command("dupe"), - new Command("not-a-dupe") { Aliases = { "dupe" } } - } - }; - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(command); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be("Duplicate alias 'dupe' found on command 'subcommand'."); - } - - [Fact] - public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_on_the_root_command() - { - var option = new Option("dupe"); - var command = new Command("not-a-dupe"); - command.Aliases.Add("dupe"); - - var rootCommand = new RootCommand - { - option, - command - }; - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(rootCommand); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be($"Duplicate alias 'dupe' found on command '{rootCommand.Name}'."); - } - - [Fact] - public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_on_a_subcommand() - { - var option = new Option("dupe"); - var command = new Command("not-a-dupe"); - command.Aliases.Add("dupe"); - - var rootCommand = new RootCommand - { - new Command("subcommand") - { - option, - command - } - }; - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(rootCommand); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be("Duplicate alias 'dupe' found on command 'subcommand'."); - } - - [Fact] - public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_aliases_on_the_root_command() - { - var option1 = new Option("--dupe") { Recursive = true }; - var option2 = new Option("-y") { Recursive = true }; - option2.Aliases.Add("--dupe"); - - var command = new RootCommand(); - command.Options.Add(option1); - command.Options.Add(option2); - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(command); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be($"Duplicate alias '--dupe' found on command '{command.Name}'."); - } - - [Fact] - public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_local_option_alias() - { - var rootCommand = new RootCommand - { - new Command("subcommand") - { - new Option("--dupe") - } - }; - rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(rootCommand); - - validate.Should().NotThrow(); - } - - [Fact] - public void ThrowIfInvalid_does_not_throw_if_global_option_alias_is_the_same_as_subcommand_alias() - { - var rootCommand = new RootCommand - { - new Command("subcommand") - { - new Command("--dupe") - } - }; - rootCommand.Options.Add(new Option("--dupe") { Recursive = true }); - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(rootCommand); - - validate.Should().NotThrow(); - } - - [Fact] - public void ThrowIfInvalid_throws_if_a_command_is_its_own_parent() - { - var command = new RootCommand(); - command.Add(command); - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(command); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); - } - - [Fact] - public void ThrowIfInvalid_throws_if_a_parentage_cycle_is_detected() - { - var command = new Command("command"); - var rootCommand = new RootCommand { command }; - command.Add(rootCommand); - - var config = new ParserConfiguration(); - - var validate = () => config.ThrowIfInvalid(rootCommand); - - validate.Should() - .Throw() - .Which - .Message - .Should() - .Be($"Cycle detected in command tree. Command '{rootCommand.Name}' is its own ancestor."); - } -} \ No newline at end of file diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 2d9d879f8b..5c4d4bca69 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -272,22 +272,19 @@ static string[] OptionsWithArgumentLimitReached(CommandResult commandResult) => /// /// Invokes the appropriate command handler for a parsed command line input. /// + /// The configuration used to define invocation behaviors. /// A token that can be used to cancel an invocation. /// A task whose result can be used as a process exit code. - public Task InvokeAsync(CancellationToken cancellationToken = default) - => InvocationPipeline.InvokeAsync(this, cancellationToken); - - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// The configuration on which the parser's grammar and behaviors are based. - /// A token that can be used to cancel an invocation. - /// A task whose result can be used as a process exit code. - public Task InvokeAsync(InvocationConfiguration configuration, CancellationToken cancellationToken = default) + public Task InvokeAsync( + InvocationConfiguration? configuration = null, + CancellationToken cancellationToken = default) { - InvocationConfiguration = configuration; + if (configuration is not null) + { + InvocationConfiguration = configuration; + } - return InvokeAsync(cancellationToken); + return InvocationPipeline.InvokeAsync(this, cancellationToken); } /// diff --git a/src/System.CommandLine/ParserConfiguration.cs b/src/System.CommandLine/ParserConfiguration.cs index 8f86e40389..f239c3c4b4 100644 --- a/src/System.CommandLine/ParserConfiguration.cs +++ b/src/System.CommandLine/ParserConfiguration.cs @@ -41,70 +41,5 @@ public class ParserConfiguration /// When enabled, any token prefixed with @ can be replaced with zero or more other tokens. This is mostly commonly used to expand tokens from response files and interpolate them into a command line prior to parsing. /// public TryReplaceToken? ResponseFileTokenReplacer { get; set; } = StringExtensions.TryReadResponseFile; - - /// - /// Throws an exception if the parser configuration is ambiguous or otherwise not valid. - /// - /// Due to the performance cost of this method, it is recommended to be used in unit testing or in scenarios where the parser is configured dynamically at runtime. - /// Thrown if the configuration is found to be invalid. - public void ThrowIfInvalid(Command command) - { - if (command.Parents.FlattenBreadthFirst(c => c.Parents).Any(ancestor => ancestor == command)) - { - throw new ParserConfigurationException($"Cycle detected in command tree. Command '{command.Name}' is its own ancestor."); - } - - int count = command.Subcommands.Count + command.Options.Count; - for (var i = 0; i < count; i++) - { - Symbol symbol1 = GetChild(i, command, out AliasSet? aliases1); - for (var j = i + 1; j < count; j++) - { - Symbol symbol2 = GetChild(j, command, out AliasSet? aliases2); - - if (symbol1.Name.Equals(symbol2.Name, StringComparison.Ordinal) - || (aliases1 is not null && aliases1.Contains(symbol2.Name))) - { - throw new ParserConfigurationException($"Duplicate alias '{symbol2.Name}' found on command '{command.Name}'."); - } - else if (aliases2 is not null && aliases2.Contains(symbol1.Name)) - { - throw new ParserConfigurationException($"Duplicate alias '{symbol1.Name}' found on command '{command.Name}'."); - } - - if (aliases1 is not null && aliases2 is not null) - { - // take advantage of the fact that we are dealing with two hash sets - if (aliases1.Overlaps(aliases2)) - { - foreach (string symbol2Alias in aliases2) - { - if (aliases1.Contains(symbol2Alias)) - { - throw new ParserConfigurationException($"Duplicate alias '{symbol2Alias}' found on command '{command.Name}'."); - } - } - } - } - } - - if (symbol1 is Command childCommand) - { - ThrowIfInvalid(childCommand); - } - } - - static Symbol GetChild(int index, Command command, out AliasSet? aliases) - { - if (index < command.Subcommands.Count) - { - aliases = command.Subcommands[index]._aliases; - return command.Subcommands[index]; - } - - aliases = command.Options[index - command.Subcommands.Count]._aliases; - return command.Options[index - command.Subcommands.Count]; - } - } } } \ No newline at end of file From 51866f4ad053b5f9daa5e4804fc3a43d88931b7c Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Mon, 7 Jul 2025 11:26:02 -0700 Subject: [PATCH 15/16] Remove unused custom exception type --- ...em_CommandLine_api_is_not_changed.approved.txt | 2 -- .../ParserConfigurationException.cs | 15 --------------- 2 files changed, 17 deletions(-) delete mode 100644 src/System.CommandLine/ParserConfigurationException.cs 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 3c748ba872..e7cebefab4 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 @@ -107,8 +107,6 @@ .ctor() public System.Boolean EnablePosixBundling { get; set; } public System.CommandLine.Parsing.TryReplaceToken ResponseFileTokenReplacer { get; set; } - public class ParserConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable - .ctor(System.String message) public class ParseResult public System.CommandLine.Invocation.CommandLineAction Action { get; } public System.CommandLine.Parsing.CommandResult CommandResult { get; } diff --git a/src/System.CommandLine/ParserConfigurationException.cs b/src/System.CommandLine/ParserConfigurationException.cs deleted file mode 100644 index fbdf186e3a..0000000000 --- a/src/System.CommandLine/ParserConfigurationException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -namespace System.CommandLine; - -/// -/// Indicates that a command line configuration is invalid. -/// -public class ParserConfigurationException : Exception -{ - /// - public ParserConfigurationException(string message) : base(message) - { - } -} \ No newline at end of file From faee41a1fdfa2226d8e75ad7d2bab27b880aacf9 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Tue, 8 Jul 2025 10:06:17 -0700 Subject: [PATCH 16/16] consolidate Invoke overloads into one method per PR feedback --- ...ommandLine_api_is_not_changed.approved.txt | 3 +-- src/System.CommandLine/ParseResult.cs | 19 +++++++------------ 2 files changed, 8 insertions(+), 14 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 e7cebefab4..1ef7bd503e 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 @@ -130,8 +130,7 @@ public T GetValue(Argument argument) public T GetValue(Option option) public T GetValue(System.String name) - public System.Int32 Invoke() - public System.Int32 Invoke(InvocationConfiguration configuration) + public System.Int32 Invoke(InvocationConfiguration configuration = null) public System.Threading.Tasks.Task InvokeAsync(InvocationConfiguration configuration = null, System.Threading.CancellationToken cancellationToken = null) public System.String ToString() public class RootCommand : Command, System.Collections.IEnumerable diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 5c4d4bca69..3153acaacf 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -290,11 +290,17 @@ public Task InvokeAsync( /// /// Invokes the appropriate command handler for a parsed command line input. /// + /// The configuration used to define invocation behaviors. /// A value that can be used as a process exit code. - public int Invoke() + public int Invoke(InvocationConfiguration? configuration = null) { var useAsync = false; + if (configuration is not null) + { + InvocationConfiguration = configuration; + } + if (Action is AsynchronousCommandLineAction) { useAsync = true; @@ -322,17 +328,6 @@ public int Invoke() } } - /// - /// Invokes the appropriate command handler for a parsed command line input. - /// - /// A value that can be used as a process exit code. - public int Invoke(InvocationConfiguration configuration) - { - InvocationConfiguration = configuration; - - return Invoke(); - } - /// /// Gets the for parsed result. The handler represents the action /// that will be performed when the parse result is invoked.