diff --git a/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs b/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs new file mode 100644 index 0000000000..993b8c2d10 --- /dev/null +++ b/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.CommandLine.Parsing; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace System.CommandLine.Tests; + +public partial class ParserTests +{ + public partial class RootCommandAndArg0 + { + [Fact] + public void When_parsing_a_string_array_a_root_command_can_be_omitted_from_the_parsed_args() + { + var command = new Command("outer") + { + new Command("inner") + { + new Option("-x") + } + }; + + var result1 = command.Parse(Split("inner -x hello")); + var result2 = command.Parse(Split("outer inner -x hello")); + + result1.Diagram().Should().Be(result2.Diagram()); + } + + [Fact] + public void When_parsing_a_string_array_input_then_a_full_path_to_an_executable_is_not_matched_by_the_root_command() + { + var command = new RootCommand + { + new Command("inner") + { + new Option("-x") + } + }; + + command.Parse(Split("inner -x hello")).Errors.Should().BeEmpty(); + + command.Parse(Split($"{RootCommand.ExecutablePath} inner -x hello")) + .Errors + .Should() + .ContainSingle(e => e.Message == $"{LocalizationResources.Instance.UnrecognizedCommandOrArgument(RootCommand.ExecutablePath)}"); + } + + [Fact] + public void When_parsing_an_unsplit_string_a_root_command_can_be_omitted_from_the_parsed_args() + { + var command = new Command("outer") + { + new Command("inner") + { + new Option("-x") + } + }; + + var result1 = command.Parse("inner -x hello"); + var result2 = command.Parse("outer inner -x hello"); + + result1.Diagram().Should().Be(result2.Diagram()); + } + + [Fact] + public void When_parsing_an_unsplit_string_then_input_a_full_path_to_an_executable_is_matched_by_the_root_command() + { + var command = new RootCommand + { + new Command("inner") + { + new Option("-x") + } + }; + + var result2 = command.Parse($"{RootCommand.ExecutablePath} inner -x hello"); + + result2.RootCommandResult.Token.Value.Should().Be(RootCommand.ExecutablePath); + } + + [Fact] + public void When_parsing_an_unsplit_string_then_a_renamed_RootCommand_can_be_omitted_from_the_parsed_args() + { + var rootCommand = new RootCommand + { + new Command("inner") + { + new Option("-x") + } + }; + rootCommand.Name = "outer"; + + var result1 = rootCommand.Parse("inner -x hello"); + var result2 = rootCommand.Parse("outer inner -x hello"); + var result3 = rootCommand.Parse($"{RootCommand.ExecutableName} inner -x hello"); + + result2.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command); + result3.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command); + } + + string[] Split(string value) => CommandLineStringSplitter.Instance.Split(value).ToArray(); + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 429e9f698e..f3ff47ca5a 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -16,7 +16,9 @@ namespace System.CommandLine.Tests { public partial class ParserTests - { + {public partial class RootCommandAndArg0 + { + } private readonly ITestOutputHelper _output; public ParserTests(ITestOutputHelper output) @@ -774,61 +776,6 @@ public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_a completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command"); } - [Fact] - public void A_root_command_can_be_omitted_from_the_parsed_args() - { - var command = new Command("outer") - { - new Command("inner") - { - new Option("-x") - } - }; - - var result1 = command.Parse("inner -x hello"); - var result2 = command.Parse("outer inner -x hello"); - - result1.Diagram().Should().Be(result2.Diagram()); - } - - [Fact] - public void A_root_command_can_match_a_full_path_to_an_executable() - { - var command = new RootCommand - { - new Command("inner") - { - new Option("-x") - } - }; - - ParseResult result1 = command.Parse("inner -x hello"); - - ParseResult result2 = command.Parse($"{RootCommand.ExecutablePath} inner -x hello"); - - result1.Diagram().Should().Be(result2.Diagram()); - } - - [Fact] - public void A_renamed_RootCommand_can_be_omitted_from_the_parsed_args() - { - var rootCommand = new RootCommand - { - new Command("inner") - { - new Option("-x") - } - }; - rootCommand.Name = "outer"; - - var result1 = rootCommand.Parse("inner -x hello"); - var result2 = rootCommand.Parse("outer inner -x hello"); - var result3 = rootCommand.Parse($"{RootCommand.ExecutableName} inner -x hello"); - - result2.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command); - result3.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command); - } - [Fact] public void Absolute_unix_style_paths_are_lexed_correctly() { @@ -1581,7 +1528,7 @@ public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_opti } [Fact] - public void A_subcommand_wont_overflow_when_checking_maximum_argument_capcity() + public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity() { // Tests bug identified in https://github.com/dotnet/command-line-api/issues/997 diff --git a/src/System.CommandLine/Parsing/Parser.cs b/src/System.CommandLine/Parsing/Parser.cs index 74262e8e62..72f5e61d19 100644 --- a/src/System.CommandLine/Parsing/Parser.cs +++ b/src/System.CommandLine/Parsing/Parser.cs @@ -44,7 +44,9 @@ public ParseResult Parse( IReadOnlyList arguments, string? rawInput = null) { - var tokenizeResult = arguments.Tokenize(Configuration); + var tokenizeResult = arguments.Tokenize( + Configuration, + inferRootCommand: rawInput is not null); var operation = new ParseOperation( tokenizeResult, diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 2874054b8f..370c17c2fa 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; namespace System.CommandLine.Parsing { @@ -66,14 +67,18 @@ internal static (string? Prefix, string Alias) SplitPrefix(this string rawAlias) internal static TokenizeResult Tokenize( this IReadOnlyList args, - CommandLineConfiguration configuration) + CommandLineConfiguration configuration, + bool inferRootCommand = true) { var errorList = new List(); Command currentCommand = configuration.RootCommand; var foundDoubleDash = false; var foundEndOfDirectives = !configuration.EnableDirectives; - var argList = NormalizeRootCommand(configuration, args); + + List argList; + argList = NormalizeRootCommand(args, configuration.RootCommand, inferRootCommand); + var tokenList = new List(argList.Count); var knownTokens = configuration.RootCommand.ValidTokens(); @@ -308,8 +313,9 @@ void ReadResponseFile(string filePath, int i) } private static List NormalizeRootCommand( - CommandLineConfiguration commandLineConfiguration, - IReadOnlyList? args) + IReadOnlyList? args, + Command rootCommand, + bool inferRootCommand = true) { if (args is null) { @@ -318,64 +324,41 @@ private static List NormalizeRootCommand( var list = new List(); - string? potentialRootCommand = null; - if (args.Count > 0) { - try + if (inferRootCommand && + args[0] == RootCommand.ExecutablePath) { - potentialRootCommand = Path.GetFileName(args[0]); + list.AddRange(args); + return list; } - catch (ArgumentException) + else { - // possible exception for illegal characters in path on .NET Framework - } + try + { + var potentialRootCommand = Path.GetFileName(args[0]); - if (potentialRootCommand != null && - commandLineConfiguration.RootCommand.HasAlias(potentialRootCommand)) - { - list.AddRange(args); - return list; + if (rootCommand.HasAlias(potentialRootCommand)) + { + list.AddRange(args); + return list; + } + } + catch (ArgumentException) + { + // possible exception for illegal characters in path on .NET Framework + } } } - var commandName = commandLineConfiguration.RootCommand.Name; - - list.Add(commandName); - - int startAt = 0; + list.Add(rootCommand.Name); - if (FirstArgMatchesRootCommand()) - { - startAt = 1; - } - - for (var i = startAt; i < args.Count; i++) + for (var i = 0; i < args.Count; i++) { list.Add(args[i]); } return list; - - bool FirstArgMatchesRootCommand() - { - if (potentialRootCommand is null) - { - return false; - } - - if (potentialRootCommand.Equals($"{commandName}.dll", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (potentialRootCommand.Equals($"{commandName}.exe", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - return false; - } } private static string? GetResponseFileReference(this string arg) =>