From 27fc9c49f432ce1ea4379ae2c9efd82c524c9106 Mon Sep 17 00:00:00 2001 From: mauricel Date: Thu, 25 Mar 2021 15:37:09 +0000 Subject: [PATCH] #13 - CommandLineSplitter utility, used to split input from powershell --- CliFx.Tests/CommandLineUtilsSpecs.cs | 22 ++++++ CliFx/CliFx.csproj | 6 ++ CliFx/Utils/CommandLineSplitter.cs | 108 +++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 CliFx.Tests/CommandLineUtilsSpecs.cs create mode 100644 CliFx/Utils/CommandLineSplitter.cs diff --git a/CliFx.Tests/CommandLineUtilsSpecs.cs b/CliFx.Tests/CommandLineUtilsSpecs.cs new file mode 100644 index 00000000..ae83d77f --- /dev/null +++ b/CliFx.Tests/CommandLineUtilsSpecs.cs @@ -0,0 +1,22 @@ +using CliFx.Utils; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace CliFx.Tests +{ + public class CommandLineSplitterSpecs + { + [Theory] + [InlineData("MyApp alpha beta", new string[] { "MyApp", "alpha", "beta" })] + [InlineData("MyApp \"alpha with spaces\" \"beta with spaces\"", new string[] { "MyApp", "alpha with spaces", "beta with spaces" })] + [InlineData("MyApp 'alpha with spaces' beta", new string[] { "MyApp", "'alpha", "with", "spaces'", "beta" })] + [InlineData("MyApp \\\\\\alpha \\\\\\\\\"beta", new string[] { "MyApp", "\\\\\\alpha", "\\\\beta" })] + [InlineData("MyApp \\\\\\\\\\\"alpha \\\"beta", new string[] { "MyApp", "\\\\\"alpha", "\"beta" })] + public void Suggestion_service_can_emulate_GetCommandLineArgs(string input, string[] expected) + { + var output = CommandLineSplitter.Split(input); + output.Should().BeEquivalentTo(expected); + } + } +} diff --git a/CliFx/CliFx.csproj b/CliFx/CliFx.csproj index 35b550c8..061e0840 100644 --- a/CliFx/CliFx.csproj +++ b/CliFx/CliFx.csproj @@ -44,4 +44,10 @@ + + + <_Parameter1>CliFx.Tests + + + \ No newline at end of file diff --git a/CliFx/Utils/CommandLineSplitter.cs b/CliFx/Utils/CommandLineSplitter.cs new file mode 100644 index 00000000..58a0939f --- /dev/null +++ b/CliFx/Utils/CommandLineSplitter.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CliFx.Utils +{ + internal static class CommandLineSplitter + { + /// + /// Reproduces Environment.GetCommandLineArgs() as per https://docs.microsoft.com/en-us/dotnet/api/system.environment.getcommandlineargs?view=net-5.0 + /// + /// Input at the command line Resulting command line arguments + /// MyApp alpha beta MyApp, alpha, beta + /// MyApp "alpha with spaces" "beta with spaces" MyApp, alpha with spaces, beta with spaces + /// MyApp 'alpha with spaces' beta MyApp, 'alpha, with, spaces', beta + /// MyApp \\\alpha \\\\"beta MyApp, \\\alpha, \\beta + /// MyApp \\\\\"alpha \"beta MyApp, \\"alpha, "beta + /// + /// Used to parse autocomplete text as it is passed in as a single argument by Powershell + /// + /// + public static string[] Split(string s) + { + int escapeSequenceLength = 0; + int escapeSequenceEnd = 0; + bool ignoreSpaces = false; + + var tokens = new List(); + StringBuilder tokenBuilder = new StringBuilder(); + + for (int i = 0; i < s.Length; i++) + { + // determine how long the escape character sequence is + if (s[i] == '\\' && i > escapeSequenceEnd) + { + for (int j = i; j < s.Length; j++) + { + if (s[j] == '\\') + { + continue; + } + else if (s[j] != '\"') + { + // edge case: \\\alpha --> \\\alpha (no escape) + escapeSequenceLength = 0; + break; + } + + escapeSequenceLength = j - i; + + // edge case: \\\\"beta -> \\beta + // treat the " as an escape character so that we skip over it + if (escapeSequenceLength == 4) + { + escapeSequenceLength = 6; + } + else + { + // capture the escaped character in our escape sequence + escapeSequenceLength++; + } + + escapeSequenceEnd = i + escapeSequenceLength; + break; + } + } + + if (escapeSequenceLength > 0 && escapeSequenceLength % 2 == 0) + { + // skip escape characters + } + else + { + bool characterIsEscaped = escapeSequenceLength != 0; + + // edge case: '"' character is used to divide tokens eg: MyApp "alpha with spaces" "beta with spaces" + // skip the '"' character + if (!characterIsEscaped && s[i] == '"') + { + ignoreSpaces = !ignoreSpaces; + } + // edge case: ' ' character is used to divide tokens + else if (!characterIsEscaped && char.IsWhiteSpace(s[i]) && !ignoreSpaces) + { + tokens.Add(tokenBuilder.ToString()); + tokenBuilder.Clear(); + } + else + { + tokenBuilder.Append(s[i]); + } + } + + if (escapeSequenceLength > 0) + { + escapeSequenceLength--; + } + } + + var token = tokenBuilder.ToString(); + if (!string.IsNullOrWhiteSpace(token)) + { + tokens.Add(token); + } + return tokens.ToArray(); + } + } +}