From d432c15d766e8a2dc2260b16d84a5c4dc4882c7e Mon Sep 17 00:00:00 2001 From: deca Date: Fri, 17 May 2024 21:48:30 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8=20Better=20support=20quoted=20stri?= =?UTF-8?q?ngs=20and=20escaped=20quotes=20in=20strings=20+semver:minor=20f?= =?UTF-8?q?ix=20#22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VCF.Core/Common/Utility.cs | 68 +++++++++++++++++++++++++++++--------- VCF.Tests/GetPartsTests.cs | 62 ++++++++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 22 deletions(-) diff --git a/VCF.Core/Common/Utility.cs b/VCF.Core/Common/Utility.cs index 35f5f85..fd57b38 100644 --- a/VCF.Core/Common/Utility.cs +++ b/VCF.Core/Common/Utility.cs @@ -9,32 +9,68 @@ namespace VampireCommandFramework.Common; internal static class Utility { - // also todo: maybe bad code to rewrite, look later - internal static IEnumerable GetParts(string input) + + /// + /// This method splits the input string into parts based on spaces, removing whitespace, + /// but preserving quoted strings as literal parts. + /// + /// + /// This should support escaping quotes with \ + /// + internal static List GetParts(string input) { - var parts = input.Split(" ", StringSplitOptions.RemoveEmptyEntries); - for (var i = 0; i < parts.Length; i++) + var parts = new List(); + if (string.IsNullOrWhiteSpace(input)) return parts; + + bool inQuotes = false; + var sb = new StringBuilder(); + + for (int i = 0; i < input.Length; i++) { - if (parts[i].StartsWith('"')) + char ch = input[i]; + + // Handle escaped quotes + if (ch == '\\' && i + 1 < input.Length) { - parts[i] = parts[i].TrimStart('"'); - for (var start = i++; i < parts.Length; i++) + char nextChar = input[i + 1]; + if (nextChar == '"') { - if (parts[i].EndsWith('"')) - { - parts[i] = parts[i].TrimEnd('"'); - yield return string.Join(" ", parts[start..(i + 1)]); - break; - } + sb.Append(nextChar); + i++; // Skip the escaped quote + continue; + } + } + + if (ch == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (ch == ' ' && !inQuotes) + { + if (sb.Length > 0) + { + parts.Add(sb.ToString()); + sb.Clear(); } } else { - yield return parts[i]; + sb.Append(ch); } } + + if (sb.Length > 0) + { + parts.Add(sb.ToString()); + } + + return parts; } + + internal static void InternalError(this ICommandContext ctx) => ctx.SysReply("An internal error has occurred."); internal static void SysReply(this ICommandContext ctx, string input) => ctx.Reply($"[vcf] ".Color(Color.Primary) + input.Color(Color.White)); @@ -70,7 +106,7 @@ internal static string[] SplitIntoPages(string rawText, int pageSize = MAX_MESSA var page = new StringBuilder(); var rawLines = rawText.Split(Environment.NewLine); // todo: does this work on both platofrms? var lines = new List(); - + // process rawLines -> lines of length <= pageSize foreach (var line in rawLines) { @@ -97,7 +133,7 @@ internal static string[] SplitIntoPages(string rawText, int pageSize = MAX_MESSA lines.Add(line); } } - + // batch as many lines together into pageSize foreach (var line in lines) { diff --git a/VCF.Tests/GetPartsTests.cs b/VCF.Tests/GetPartsTests.cs index 9fe19fc..4b13ad4 100644 --- a/VCF.Tests/GetPartsTests.cs +++ b/VCF.Tests/GetPartsTests.cs @@ -6,22 +6,72 @@ namespace VCF.Tests; public class GetPartsTests { [Test] - public void GetParts_No_Quotes() + public void GetParts_Empty() { var empty = Array.Empty(); - Assert.That(Utility.GetParts("blah blah"), Is.EqualTo(new[] { "blah", "blah" })); Assert.That(Utility.GetParts(""), Is.EqualTo(empty)); Assert.That(Utility.GetParts(" "), Is.EqualTo(empty)); + Assert.That(Utility.GetParts(null), Is.EqualTo(empty)); + } + + [Test] + public void GetParts_No_Quotes() + { + Assert.That(Utility.GetParts("blah blah"), Is.EqualTo(new[] { "blah", "blah" })); Assert.That(Utility.GetParts("a "), Is.EqualTo(new[] { "a" })); Assert.That(Utility.GetParts(" a"), Is.EqualTo(new[] { "a" })); Assert.That(Utility.GetParts(" a b c "), Is.EqualTo(new[] { "a", "b", "c" })); } [Test] - public void GetParts_Quotes() + public void GetParts_Quotes_Preserves_Spacing() + { + Assert.That(Utility.GetParts(" a \" b c \""), Is.EqualTo(new[] { "a", " b c " })); + } + + [Test] + public void GetParts_Quotes_Many() + { + Assert.That(Utility.GetParts(" \"a\" \" b c \" not quoted "), Is.EqualTo(new[] { "a", " b c ", "not", "quoted" })); + } + + [Test] + public void GetParts_Quote_SingleTerm() { - Assert.That(Utility.GetParts("a \"b c\""), Is.EqualTo(new[] { "a", "b c" })); - // TODO: Consider if this should be the result, it fails now - // Assert.That(CommandRegistry.GetParts(" a \" b c \""), Is.EqualTo(new[] { "a", " b c " })); + Assert.That(Utility.GetParts("a"), Is.EqualTo(new[] { "a" })); + Assert.That(Utility.GetParts("\"a\""), Is.EqualTo(new[] { "a" })); + Assert.That(Utility.GetParts("\"abc\""), Is.EqualTo(new[] { "abc" })); + } + + [Test] + public void GetParts_Quote_SingleTerm_Positional() + { + Assert.That(Utility.GetParts("a \"b\" c"), Is.EqualTo(new[] { "a", "b", "c" })); + Assert.That(Utility.GetParts("\"a\" b c"), Is.EqualTo(new[] { "a", "b", "c" })); + Assert.That(Utility.GetParts("a b \"c\""), Is.EqualTo(new[] { "a", "b", "c" })); + } + + [Test] + public void GetParts_Escape_Quotes() + { + Assert.That(Utility.GetParts("\"I'm like \\\"O'rly?\\\", they're like \\\"ya rly\\\"\""), Is.EqualTo(new[] { "I'm like \"O'rly?\", they're like \"ya rly\"" })); + Assert.That(Utility.GetParts(@"a\b\\""c\"""), Is.EqualTo(new[] { @"a\b\""c""" })); + } + + [Test] + public void GetParts_Escape_Slashliteral() + { + Assert.That(Utility.GetParts(@"\"), Is.EqualTo(new[] { @"\" })); + Assert.That(Utility.GetParts(@"\\\"), Is.EqualTo(new[] { @"\\\" })); + + Assert.That(Utility.GetParts("\\a"), Is.EqualTo(new[] { "\\a" })); + Assert.That(Utility.GetParts("\\\""), Is.EqualTo(new[] { "\"" })); + } + + [Test] + public void GetParts_Escape_Grouping() + { + Assert.That(Utility.GetParts(@"\ \"), Is.EqualTo(new[] { @"\", @"\" })); + Assert.That(Utility.GetParts(@"a\b \ c"), Is.EqualTo(new[] { @"a\b", @"\", "c" })); } }