Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚸 Better support quoted strings and escaped quotes in strings #23

Merged
merged 2 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 52 additions & 16 deletions VCF.Core/Common/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,68 @@ namespace VampireCommandFramework.Common;
internal static class Utility
{

// also todo: maybe bad code to rewrite, look later
internal static IEnumerable<string> GetParts(string input)

/// <summary>
/// This method splits the input string into parts based on spaces, removing whitespace,
/// but preserving quoted strings as literal parts.
/// </summary>
/// <remarks>
/// This should support escaping quotes with \
/// </remarks>
internal static List<string> GetParts(string input)
{
var parts = input.Split(" ", StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < parts.Length; i++)
var parts = new List<string>();
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));
Expand Down Expand Up @@ -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<string>();

// process rawLines -> lines of length <= pageSize
foreach (var line in rawLines)
{
Expand All @@ -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)
{
Expand Down
6 changes: 3 additions & 3 deletions VCF.Core/VCF.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
<Copy SourceFiles="$(OutDir)\VampireCommandFramework.dll" DestinationFolder="$(SolutionDir)/dist" />
</Target>
<ItemGroup>
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.668" IncludeAssets="compile" />
<PackageReference Include="BepInEx.Core" Version="6.0.0-be.668" IncludeAssets="compile" />
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.690" IncludeAssets="compile" />
<PackageReference Include="BepInEx.Core" Version="6.0.0-be.690" IncludeAssets="compile" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="1.*" />
<PackageReference Include="VRising.Unhollowed.Client" Version="1.0.0.*" />
<PackageReference Include="VRising.Unhollowed.Client" Version="1.0.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion VCF.SimpleSamplePlugin/VCF.SimpleSamplePlugin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be*" IncludeAssets="compile" />
<PackageReference Include="BepInEx.Core" Version="6.0.0-be*" IncludeAssets="compile" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" />
<PackageReference Include="VRising.Unhollowed.Client" Version="1.0.0.*" />
<PackageReference Include="VRising.Unhollowed.Client" Version="1.0.*" />
<PackageReference Include="Vrising.Bloodstone" Version="0.1.*" />
</ItemGroup>
<ItemGroup>
Expand Down
62 changes: 56 additions & 6 deletions VCF.Tests/GetPartsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,72 @@ namespace VCF.Tests;
public class GetPartsTests
{
[Test]
public void GetParts_No_Quotes()
public void GetParts_Empty()
{
var empty = Array.Empty<string>();
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" }));
}
}
Loading