diff --git a/Assets/CommandTerminal/CommandShell.cs b/Assets/CommandTerminal/CommandShell.cs index 518479d..1e01a60 100644 --- a/Assets/CommandTerminal/CommandShell.cs +++ b/Assets/CommandTerminal/CommandShell.cs @@ -24,7 +24,7 @@ public void Run(string line) while (_remaining != "") { - var _arg = CommandUtils.EatArgument(ref _remaining); + var _arg = CommandUtils.ParseCommand(ref _remaining); if (_arg.String != "") { diff --git a/Assets/CommandTerminal/CommandUtils.cs b/Assets/CommandTerminal/CommandUtils.cs index e878bcb..6d2e1ce 100644 --- a/Assets/CommandTerminal/CommandUtils.cs +++ b/Assets/CommandTerminal/CommandUtils.cs @@ -39,21 +39,55 @@ public static Dictionary CacheCommandMethods() return methodDictionary; } - public static CommandArg EatArgument(ref string s) + public static CommandArg ParseCommand(ref string s) { var arg = new CommandArg(); - int space_index = s.IndexOf(' '); - if (space_index >= 0) + s = s.Trim(); + + char[] quoteChars = { '\"', '\'' }; + + foreach (var quoteChar in quoteChars) + { + if (s.StartsWith(quoteChar.ToString())) + { + int quoteIndex = FindClosingQuote(s, quoteChar); + if (quoteIndex >= 0) + { + arg.String = UnescapedQuotes(s.Substring(1, quoteIndex - 1), quoteChar); + s = s.Substring(quoteIndex + 1).Trim(); + return arg; + } + } + } + + int spaceIndex = s.IndexOf(' '); + if (spaceIndex >= 0) { - arg.String = s.Substring(0, space_index); - s = s.Substring(space_index + 1); + arg.String = s.Substring(0, spaceIndex); + s = s.Substring(spaceIndex + 1).Trim(); } else { arg.String = s; s = ""; } + return arg; } + + public static int FindClosingQuote(string s, char quoteChar) + { + int quoteIndex = s.IndexOf(quoteChar, 1); + while (quoteIndex > 0 && s[quoteIndex - 1] == '\\') + { + quoteIndex = s.IndexOf(quoteChar, quoteIndex + 1); + } + return quoteIndex; + } + + public static string UnescapedQuotes(string s, char quoteChar) + { + return s.Replace("\\" + quoteChar, quoteChar.ToString()); + } } } diff --git a/Tests/CommandArgTests.cs b/Tests/CommandArgTests.cs new file mode 100644 index 0000000..f2688cc --- /dev/null +++ b/Tests/CommandArgTests.cs @@ -0,0 +1,145 @@ +using CommandTerminal; + +namespace DialogosEngine.Tests +{ + [TestFixture] + public static class CommandArgTests + { + [Test] + public static void EatArgument_GivenString_ReturnsCorrectCommandArgAndRemainingString() + { + // Arrange + string input = "echo \"Hello World\""; + TestContext.WriteLine($"Testing with input string: '{input}'."); + + // Act + CommandArg result = CommandUtils.ParseCommand(ref input); + TestContext.WriteLine($"Resulting CommandArg: '{result.String}' and remaining string: '{input}'"); + + // Assert + Assert.IsNotNull(result); + Assert.That(result.String, Is.EqualTo("echo"), "The CommandArg should contain the command 'echo'."); + Assert.That(input, Is.EqualTo("\"Hello World\""), "The remaining string should contain the quoted argument."); + TestContext.WriteLine($"Test passed: Input string '{input}' is correctly processed into CommandArg and remaining string."); + } + + [Test] + public static void EatArgument_MultipleArgumentsWithAndWithoutQuotes_ReturnsCorrectCommandArgsAndRemainingString() + { + // Arrange + string input = "print \"Hello World\" \"Another \\\"quoted\\\" string\" unquoted"; + TestContext.WriteLine($"Testing with input string: '{input}'."); + + // Act + CommandArg firstArg = CommandUtils.ParseCommand(ref input); + CommandArg secondArg = CommandUtils.ParseCommand(ref input); + CommandArg thirdArg = CommandUtils.ParseCommand(ref input); + CommandArg fourthArg = CommandUtils.ParseCommand(ref input); + + // Assert + Assert.IsNotNull(firstArg); + Assert.IsNotNull(secondArg); + Assert.IsNotNull(thirdArg); + Assert.IsNotNull(fourthArg); + + Assert.That(firstArg.String, Is.EqualTo("print"), "The first CommandArg should contain the command 'print'."); + Assert.That(secondArg.String, Is.EqualTo("Hello World"), "The second CommandArg should contain the quoted string 'Hello World'."); + Assert.That(thirdArg.String, Is.EqualTo("Another \"quoted\" string"), "The third CommandArg should contain the quoted string with an escaped quote."); + Assert.That(fourthArg.String, Is.EqualTo("unquoted"), "The fourth CommandArg should contain the unquoted string 'unquoted'."); + + Assert.That(input, Is.Empty, "The remaining string should be empty after processing all arguments."); + + TestContext.WriteLine($"Test passed: Input string '{input}' is correctly processed into CommandArgs and remaining string."); + } + + [Test] + public static void EatArgument_ComplexArgumentsWithNestedQuotesAndEscapedCharacters_ReturnsCorrectCommandArgsAndRemainingString() + { + // Arrange + string input = "complex \"Nested \\\"quotes\\\" and 'single quotes'\" 'Escaped \\'single\\' quotes' \"Mixed \\\"quotes\\\" 'and' single\" trailing"; + TestContext.WriteLine($"Testing with input string: '{input}'."); + + // Act + CommandArg firstArg = CommandUtils.ParseCommand(ref input); + CommandArg secondArg = CommandUtils.ParseCommand(ref input); + CommandArg thirdArg = CommandUtils.ParseCommand(ref input); + CommandArg fourthArg = CommandUtils.ParseCommand(ref input); + CommandArg fifthArg = CommandUtils.ParseCommand(ref input); + + // Assert + Assert.IsNotNull(firstArg); + Assert.IsNotNull(secondArg); + Assert.IsNotNull(thirdArg); + Assert.IsNotNull(fourthArg); + Assert.IsNotNull(fifthArg); + + Assert.That(firstArg.String, Is.EqualTo("complex"), "The first CommandArg should contain the command 'complex'."); + Assert.That(secondArg.String, Is.EqualTo("Nested \"quotes\" and 'single quotes'"), "The second CommandArg should contain the nested quoted string."); + Assert.That(thirdArg.String, Is.EqualTo("Escaped 'single' quotes"), "The third CommandArg should contain the escaped single quoted string."); + Assert.That(fourthArg.String, Is.EqualTo("Mixed \"quotes\" 'and' single"), "The fourth CommandArg should contain the mixed quoted string."); + Assert.That(fifthArg.String, Is.EqualTo("trailing"), "The fifth CommandArg should contain the unquoted string 'trailing'."); + + Assert.That(input, Is.Empty, "The remaining string should be empty after processing all arguments."); + + TestContext.WriteLine($"Test passed: Input string '{input}' is correctly processed into CommandArgs and remaining string."); + } + + [Test] + public static void FindClosingQuote_WithEscapedAndUnescapedQuotes_ReturnsCorrectIndex() + { + // Arrange + string input = "\"This is a \\\"test\\\" string\""; + char quoteChar = '\"'; + int expectedIndex = input.Length - 1; + + // Act + int result = CommandUtils.FindClosingQuote(input, quoteChar); + + // Assert + Assert.AreEqual(expectedIndex, result, "The index of the closing quote should be at the end of the string."); + } + + [Test] + public static void FindClosingQuote_WithNoClosingQuote_ReturnsNegativeOne() + { + // Arrange + string input = "\"No closing quote here"; + char quoteChar = '\"'; + + // Act + int result = CommandUtils.FindClosingQuote(input, quoteChar); + + // Assert + Assert.That(result, Is.EqualTo(-1), "The result should be -1 indicating no closing quote was found."); + } + + [Test] + public static void UnescapedQuotes_WithEscapedQuotes_ReturnsUnescapedString() + { + // Arrange + string input = "Escaped \\\"quotes\\\" here"; + char quoteChar = '\"'; + string expected = "Escaped \"quotes\" here"; + + // Act + string result = CommandUtils.UnescapedQuotes(input, quoteChar); + + // Assert + Assert.That(result, Is.EqualTo(expected), "The escaped quotes should be unescaped in the result string."); + } + + [Test] + public static void UnescapedQuotes_WithNoEscapedQuotes_ReturnsOriginalString() + { + // Arrange + string input = "No escaped quotes here"; + char quoteChar = '\"'; + + // Act + string result = CommandUtils.UnescapedQuotes(input, quoteChar); + + // Assert + Assert.That(result, Is.EqualTo(input), "The result should be the same as the input string when there are no escaped quotes."); + } + } +}