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 9688ad3dcf..73e49a9e72 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 @@ -336,8 +336,6 @@ System.CommandLine.Parsing public T GetValueOrDefault() public System.Void OnlyTake(System.Int32 numberOfTokens) public System.String ToString() - public class CommandLineStringSplitter - public System.Collections.Generic.IEnumerable Split(System.String commandLine) public class CommandResult : SymbolResult public System.Collections.Generic.IEnumerable Children { get; } public System.CommandLine.Command Command { get; } @@ -361,6 +359,7 @@ System.CommandLine.Parsing public static class Parser 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.Collections.Generic.IEnumerable SplitCommandLine(System.String commandLine) public static class ParseResultExtensions public static System.String Diagram(this System.CommandLine.ParseResult parseResult) public abstract class SymbolResult diff --git a/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs b/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs index b26325d4e2..ab0f0867a4 100644 --- a/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs +++ b/src/System.CommandLine.Suggest.Tests/SuggestionDispatcherTests.cs @@ -39,7 +39,7 @@ public async Task InvokeAsync_executes_registered_executable() { string receivedTargetExeName = null; - string[] args = CommandLineStringSplitter.Instance.Split($@"get -p 12 -e ""{CurrentExeFullPath()}"" -- ""{_currentExeName} add""").ToArray(); + string[] args = Parser.SplitCommandLine($@"get -p 12 -e ""{CurrentExeFullPath()}"" -- ""{_currentExeName} add""").ToArray(); await InvokeAsync( args, @@ -112,13 +112,13 @@ await InvokeAsync( private static string[] PrepareArgs(string args) { var formattableString = args.Replace("$", ""); - return CommandLineStringSplitter.Instance.Split(formattableString).ToArray(); + return Parser.SplitCommandLine(formattableString).ToArray(); } [Fact] public async Task InvokeAsync_with_unknown_suggestion_provider_returns_empty_string() { - string[] args = Enumerable.ToArray(( CommandLineStringSplitter.Instance.Split(@"get -p 10 -e ""testcli.exe"" -- command op"))); + string[] args = Enumerable.ToArray(Parser.SplitCommandLine(@"get -p 10 -e ""testcli.exe"" -- command op")); (await InvokeAsync(args, new TestSuggestionRegistration())) .Should() .BeEmpty(); @@ -132,7 +132,7 @@ public async Task When_command_suggestions_use_process_that_remains_open_it_retu dispatcher.Timeout = TimeSpan.FromMilliseconds(1); var testConsole = new TestConsole(); - var args = CommandLineStringSplitter.Instance.Split($@"get -p 0 -e ""{_currentExeName}"" -- {_currentExeName} add").ToArray(); + var args = Parser.SplitCommandLine($@"get -p 0 -e ""{_currentExeName}"" -- {_currentExeName} add").ToArray(); await dispatcher.InvokeAsync(args, testConsole); @@ -168,7 +168,7 @@ public async Task Register_command_adds_new_suggestion_entry() var provider = new TestSuggestionRegistration(); var dispatcher = new SuggestionDispatcher(provider); - var args = CommandLineStringSplitter.Instance.Split($"register --command-path \"{_netExeFullPath}\"").ToArray(); + var args = Parser.SplitCommandLine($"register --command-path \"{_netExeFullPath}\"").ToArray(); await dispatcher.InvokeAsync(args); @@ -182,7 +182,7 @@ public async Task Register_command_will_not_add_duplicate_entry() var provider = new TestSuggestionRegistration(); var dispatcher = new SuggestionDispatcher(provider); - var args = CommandLineStringSplitter.Instance.Split($"register --command-path \"{_netExeFullPath}\"").ToArray(); + var args = Parser.SplitCommandLine($"register --command-path \"{_netExeFullPath}\"").ToArray(); await dispatcher.InvokeAsync(args); await dispatcher.InvokeAsync(args); diff --git a/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs b/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs index 4d55461259..fc0aff4571 100644 --- a/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs +++ b/src/System.CommandLine.Tests/ParserTests.RootCommandAndArg0.cs @@ -101,6 +101,6 @@ public void When_parsing_an_unsplit_string_then_a_renamed_RootCommand_can_be_omi result3.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command); } - string[] Split(string value) => CommandLineStringSplitter.Instance.Split(value).ToArray(); + string[] Split(string value) => Parser.SplitCommandLine(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 824fb59afe..e90a974f01 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -473,7 +473,7 @@ public void Relative_order_of_arguments_and_options_within_a_command_does_not_ma [InlineData("not a valid command line --one 1")] public void Original_order_of_tokens_is_preserved_in_ParseResult_Tokens(string commandLine) { - var rawSplit = CommandLineStringSplitter.Instance.Split(commandLine); + var rawSplit = Parser.SplitCommandLine(commandLine); var command = new Command("the-command") { diff --git a/src/System.CommandLine.Tests/Parsing/CommandLineStringSplitterTests.cs b/src/System.CommandLine.Tests/Parsing/CommandLineStringSplitterTests.cs index 59c3b71d65..5b624a9015 100644 --- a/src/System.CommandLine.Tests/Parsing/CommandLineStringSplitterTests.cs +++ b/src/System.CommandLine.Tests/Parsing/CommandLineStringSplitterTests.cs @@ -11,8 +11,6 @@ namespace System.CommandLine.Tests.Parsing { public class CommandLineStringSplitterTests { - private readonly CommandLineStringSplitter _splitter = CommandLineStringSplitter.Instance; - [Theory] [InlineData("one two three four")] [InlineData("one two\tthree four ")] @@ -21,7 +19,7 @@ public class CommandLineStringSplitterTests [InlineData(" one\r\ntwo\r\nthree\r\nfour\r\n")] public void It_splits_strings_based_on_whitespace(string commandLine) { - _splitter.Split(commandLine) + Parser.SplitCommandLine(commandLine) .Should() .BeEquivalentSequenceTo("one", "two", "three", "four"); } @@ -31,7 +29,7 @@ public void It_does_not_break_up_double_quote_delimited_values() { var commandLine = @"rm -r ""c:\temp files\"""; - _splitter.Split(commandLine) + Parser.SplitCommandLine(commandLine) .Should() .BeEquivalentSequenceTo("rm", "-r", @"c:\temp files\"); } @@ -51,7 +49,7 @@ public void It_does_not_split_double_quote_delimited_values_when_a_non_whitespac var commandLine = $"the-command {optionAndArgument}"; - _splitter.Split(commandLine) + Parser.SplitCommandLine(commandLine) .Should() .BeEquivalentSequenceTo("the-command", optionAndArgument.Replace("\"", "")); } @@ -64,7 +62,7 @@ public void It_handles_multiple_options_with_quoted_arguments() var commandLine = $"move --from \"{source}\" --to \"{destination}\" --verbose"; - var tokenized = _splitter.Split(commandLine); + var tokenized = Parser.SplitCommandLine(commandLine); tokenized.Should() .BeEquivalentSequenceTo( @@ -81,7 +79,7 @@ public void Internal_quotes_do_not_cause_string_to_be_split() { var commandLine = @"POST --raw='{""Id"":1,""Name"":""Alice""}'"; - _splitter.Split(commandLine) + Parser.SplitCommandLine(commandLine) .Should() .BeEquivalentTo("POST", "--raw='{Id:1,Name:Alice}'"); } @@ -91,7 +89,7 @@ public void Internal_whitespaces_are_preserved_and_do_not_cause_string_to_be_spl { var commandLine = @"command --raw='{""Id"":1,""Movie Name"":""The Three Musketeers""}'"; - _splitter.Split(commandLine) + Parser.SplitCommandLine(commandLine) .Should() .BeEquivalentTo("command", "--raw='{Id:1,Movie Name:The Three Musketeers}'"); } diff --git a/src/System.CommandLine.Tests/SplitCommandLineTests.cs b/src/System.CommandLine.Tests/SplitCommandLineTests.cs index f74ee9d3d3..4f0d6fb327 100644 --- a/src/System.CommandLine.Tests/SplitCommandLineTests.cs +++ b/src/System.CommandLine.Tests/SplitCommandLineTests.cs @@ -24,10 +24,10 @@ public void It_splits_strings_based_on_whitespace() { var commandLine = "one two\tthree four "; - CommandLineStringSplitter.Instance - .Split(commandLine) - .Should() - .BeEquivalentSequenceTo("one", "two", "three", "four"); + Parser + .SplitCommandLine(commandLine) + .Should() + .BeEquivalentSequenceTo("one", "two", "three", "four"); } [Fact] @@ -35,9 +35,8 @@ public void It_does_not_break_up_double_quote_delimited_values() { var commandLine = @"rm -r ""c:\temp files\"""; - CommandLineStringSplitter - .Instance - .Split(commandLine) + Parser + .SplitCommandLine(commandLine) .Should() .BeEquivalentSequenceTo("rm", "-r", @"c:\temp files\"); } @@ -57,9 +56,8 @@ public void It_does_not_split_double_quote_delimited_values_when_a_non_whitespac var commandLine = $"the-command {optionAndArgument}"; - CommandLineStringSplitter - .Instance - .Split(commandLine) + Parser + .SplitCommandLine(commandLine) .Should() .BeEquivalentSequenceTo("the-command", optionAndArgument.Replace("\"", "")); } @@ -72,7 +70,7 @@ public void It_handles_multiple_options_with_quoted_arguments() var commandLine = $"move --from \"{source}\" --to \"{destination}\""; - var tokenized = CommandLineStringSplitter.Instance.Split(commandLine); + var tokenized = Parser.SplitCommandLine(commandLine); _output.WriteLine(commandLine); diff --git a/src/System.CommandLine/CommandExtensions.cs b/src/System.CommandLine/CommandExtensions.cs index e1ade4092e..20bacc39ce 100644 --- a/src/System.CommandLine/CommandExtensions.cs +++ b/src/System.CommandLine/CommandExtensions.cs @@ -43,7 +43,7 @@ public static int Invoke( this Command command, string commandLine, IConsole? console = null) => - command.Invoke(CommandLineStringSplitter.Instance.Split(commandLine).ToArray(), console); + command.Invoke(Parser.SplitCommandLine(commandLine).ToArray(), console); /// /// Parses and invokes a command. @@ -78,6 +78,6 @@ public static Task InvokeAsync( string commandLine, IConsole? console = null, CancellationToken cancellationToken = default) => - command.InvokeAsync(CommandLineStringSplitter.Instance.Split(commandLine).ToArray(), console, cancellationToken); + command.InvokeAsync(Parser.SplitCommandLine(commandLine).ToArray(), console, cancellationToken); } } \ No newline at end of file diff --git a/src/System.CommandLine/Parsing/CommandLineStringSplitter.cs b/src/System.CommandLine/Parsing/CommandLineStringSplitter.cs deleted file mode 100644 index 790cb8d370..0000000000 --- a/src/System.CommandLine/Parsing/CommandLineStringSplitter.cs +++ /dev/null @@ -1,133 +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 System.Collections.Generic; - -namespace System.CommandLine.Parsing -{ - /// - /// Splits a string based on whitespace and quotation marks - /// - public class CommandLineStringSplitter - { - /// - /// A single instance of - /// - public static readonly CommandLineStringSplitter Instance = new(); - - private CommandLineStringSplitter() - { - } - - private enum Boundary - { - TokenStart, - WordEnd, - QuoteStart, - QuoteEnd - } - - /// - /// Splits a string into a sequence of strings based on whitespace and quotation marks. - /// - /// A command line input string. - /// A sequence of strings. - public IEnumerable Split(string commandLine) - { - var memory = commandLine.AsMemory(); - - var startTokenIndex = 0; - - var pos = 0; - - var seeking = Boundary.TokenStart; - var seekingQuote = Boundary.QuoteStart; - - while (pos < memory.Length) - { - var c = memory.Span[pos]; - - if (char.IsWhiteSpace(c)) - { - if (seekingQuote == Boundary.QuoteStart) - { - switch (seeking) - { - case Boundary.WordEnd: - yield return CurrentToken(); - startTokenIndex = pos; - seeking = Boundary.TokenStart; - break; - - case Boundary.TokenStart: - startTokenIndex = pos; - break; - } - } - } - else if (c == '\"') - { - if (seeking == Boundary.TokenStart) - { - switch (seekingQuote) - { - case Boundary.QuoteEnd: - yield return CurrentToken(); - startTokenIndex = pos; - seekingQuote = Boundary.QuoteStart; - break; - - case Boundary.QuoteStart: - startTokenIndex = pos + 1; - seekingQuote = Boundary.QuoteEnd; - break; - } - } - else - { - switch (seekingQuote) - { - case Boundary.QuoteEnd: - seekingQuote = Boundary.QuoteStart; - break; - - case Boundary.QuoteStart: - seekingQuote = Boundary.QuoteEnd; - break; - } - } - } - else if (seeking == Boundary.TokenStart && seekingQuote == Boundary.QuoteStart) - { - seeking = Boundary.WordEnd; - startTokenIndex = pos; - } - - Advance(); - - if (IsAtEndOfInput()) - { - switch (seeking) - { - case Boundary.TokenStart: - break; - default: - yield return CurrentToken(); - break; - } - } - } - - void Advance() => pos++; - - string CurrentToken() - { - return memory.Slice(startTokenIndex, IndexOfEndOfToken()).ToString().Replace("\"", ""); - } - - int IndexOfEndOfToken() => pos - startTokenIndex; - - bool IsAtEndOfInput() => pos == memory.Length; - } - } -} diff --git a/src/System.CommandLine/Parsing/Parser.cs b/src/System.CommandLine/Parsing/Parser.cs index a6fba6501d..79e4753306 100644 --- a/src/System.CommandLine/Parsing/Parser.cs +++ b/src/System.CommandLine/Parsing/Parser.cs @@ -30,12 +30,109 @@ public static ParseResult Parse(Command command, IReadOnlyList args, Com /// 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) + => Parse(command, SplitCommandLine(commandLine).ToArray(), commandLine, configuration); + + /// + /// Splits a string into a sequence of strings based on whitespace and quotation marks. + /// + /// A command line input string. + /// A sequence of strings. + public static IEnumerable SplitCommandLine(string commandLine) { - var splitter = CommandLineStringSplitter.Instance; + var memory = commandLine.AsMemory(); + + var startTokenIndex = 0; + + var pos = 0; + + var seeking = Boundary.TokenStart; + var seekingQuote = Boundary.QuoteStart; + + while (pos < memory.Length) + { + var c = memory.Span[pos]; + + if (char.IsWhiteSpace(c)) + { + if (seekingQuote == Boundary.QuoteStart) + { + switch (seeking) + { + case Boundary.WordEnd: + yield return CurrentToken(); + startTokenIndex = pos; + seeking = Boundary.TokenStart; + break; + + case Boundary.TokenStart: + startTokenIndex = pos; + break; + } + } + } + else if (c == '\"') + { + if (seeking == Boundary.TokenStart) + { + switch (seekingQuote) + { + case Boundary.QuoteEnd: + yield return CurrentToken(); + startTokenIndex = pos; + seekingQuote = Boundary.QuoteStart; + break; + + case Boundary.QuoteStart: + startTokenIndex = pos + 1; + seekingQuote = Boundary.QuoteEnd; + break; + } + } + else + { + switch (seekingQuote) + { + case Boundary.QuoteEnd: + seekingQuote = Boundary.QuoteStart; + break; - var readOnlyCollection = splitter.Split(commandLine).ToArray(); + case Boundary.QuoteStart: + seekingQuote = Boundary.QuoteEnd; + break; + } + } + } + else if (seeking == Boundary.TokenStart && seekingQuote == Boundary.QuoteStart) + { + seeking = Boundary.WordEnd; + startTokenIndex = pos; + } - return Parse(command, readOnlyCollection, commandLine, configuration); + Advance(); + + if (IsAtEndOfInput()) + { + switch (seeking) + { + case Boundary.TokenStart: + break; + default: + yield return CurrentToken(); + break; + } + } + } + + void Advance() => pos++; + + string CurrentToken() + { + return memory.Slice(startTokenIndex, IndexOfEndOfToken()).ToString().Replace("\"", ""); + } + + int IndexOfEndOfToken() => pos - startTokenIndex; + + bool IsAtEndOfInput() => pos == memory.Length; } private static ParseResult Parse( @@ -65,5 +162,13 @@ private static ParseResult Parse( return operation.Parse(); } + + private enum Boundary + { + TokenStart, + WordEnd, + QuoteStart, + QuoteEnd + } } } diff --git a/src/System.CommandLine/Parsing/StringExtensions.cs b/src/System.CommandLine/Parsing/StringExtensions.cs index 4c5e649b32..36646e4ec7 100644 --- a/src/System.CommandLine/Parsing/StringExtensions.cs +++ b/src/System.CommandLine/Parsing/StringExtensions.cs @@ -399,7 +399,7 @@ static IEnumerable SplitLine(string line) yield break; } - foreach (var word in CommandLineStringSplitter.Instance.Split(arg)) + foreach (var word in Parser.SplitCommandLine(arg)) { yield return word; }