From e7c1e5dff81123b8450698c4c24fcd0ca42e19a3 Mon Sep 17 00:00:00 2001 From: Chris Peterson Date: Wed, 1 Apr 2026 13:31:53 -0700 Subject: [PATCH] Fix FirstArgumentIsRootCommand falsely matching option values Path.GetFileName(args[0]) extracts the last path segment, which falsely matches command names embedded in option values. For example, "/p:Key=something/dotnet" yields "dotnet" from GetFileName, matching a command named "dotnet". This causes the tokenizer to consume args[0] as the root command instead of parsing it as an option. Replace the Path.GetFileName heuristic with an exact match against the command name. The only other case (args[0] == RootCommand.ExecutablePath) is already handled by a separate check. --- .../ParserTests.RootCommandAndArg0.cs | 33 +++++++++++++++++++ .../Parsing/StringExtensions.cs | 13 ++------ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs b/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs index d3f83bac35..7776ff0c32 100644 --- a/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs +++ b/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs @@ -101,6 +101,39 @@ public void When_parsing_an_unsplit_string_then_a_renamed_RootCommand_can_be_omi result3.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command); } + [Fact] + public void When_parsing_a_string_array_option_values_containing_command_name_after_slash_are_not_treated_as_root_command() + { + // Path.GetFileName("/p:Key=something/myapp") returns "myapp", + // which should not be matched as the root command name. + var option = new Option("--property", "/p") { Arity = ArgumentArity.ExactlyOne }; + var command = new Command("myapp"); + command.Options.Add(option); + + var result = command.Parse(Split("/p:Key=something/myapp")); + + result.Errors.Should().BeEmpty(); + result.GetResult(option).Should().NotBeNull(); + } + + [Theory] + [InlineData("/p:DockerImage=registry.example.com/project/dotnet")] + [InlineData("-p:DockerImage=registry.example.com/project/dotnet")] + [InlineData("--property:DockerImage=registry.example.com/project/dotnet")] + public void When_parsing_a_string_array_option_values_ending_with_slash_command_name_are_preserved(string arg) + { + // Regression test: Path.GetFileName extracts the last segment after '/', + // so option values like ".../dotnet" falsely matched a command named "dotnet". + var option = new Option("--property", "/p", "-p") { Arity = ArgumentArity.ExactlyOne }; + var command = new Command("dotnet"); + command.Options.Add(option); + + var result = command.Parse(Split(arg)); + + result.Errors.Should().BeEmpty(); + result.GetResult(option).Should().NotBeNull(); + } + string[] Split(string value) => CommandLineParser.SplitCommandLine(value).ToArray(); } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index a018f04fcd..ab6be26ba8 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -275,18 +275,9 @@ private static bool FirstArgumentIsRootCommand(IReadOnlyList args, Comma return true; } - try + if (rootCommand.EqualsNameOrAlias(args[0])) { - var potentialRootCommand = Path.GetFileName(args[0]); - - if (rootCommand.EqualsNameOrAlias(potentialRootCommand)) - { - return true; - } - } - catch (ArgumentException) - { - // possible exception for illegal characters in path on .NET Framework + return true; } }