Skip to content
Open
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
54 changes: 39 additions & 15 deletions src/Microsoft.TestPlatform.Utilities/CommandLineUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ public static bool SplitCommandLineIntoArguments(string args, out string[] argum

try
{
while (true)
while (index < args.Length)
{
// skip whitespace
// Skip whitespace.
while (char.IsWhiteSpace(args[index]))
{
index++;
Expand All @@ -33,49 +33,73 @@ public static bool SplitCommandLineIntoArguments(string args, out string[] argum
if (args[index] == '#')
{
index++;
while (args[index] != '\n')
while (index < args.Length && args[index] != '\n')
{
index++;
}

// We are done processing comment move to next statement.
continue;
}

// do one argument
// Read argument until next whitespace (not in quotes).
do
{
if (args[index] == '\\')
{
int cSlashes = 1;
// Move to next char.
index++;
while (index == args.Length && args[index] == '\\')
{
cSlashes++;
}

if (index == args.Length || args[index] != '"')
// If this was the last char then output the slash.
if (index == args.Length)
{
currentArg.Append('\\', cSlashes);
currentArg.Append('\\');

index++;
continue;
}
else
{
currentArg.Append('\\', (cSlashes >> 1));
if (0 != (cSlashes & 1))
// If the char after '\' is also a '\', output the second '\' and skip over to the next char.
if (args[index] == '\\')
{
currentArg.Append('\\');

// We processed the escaped \, move to next char.
index++;
continue;
}

// If the char after '\' is a '"', output '"' and skip over to the next char.
if (index <= args.Length && args[index] == '"')
{
currentArg.Append('"');
Copy link
Member Author

@nohwnd nohwnd Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main bug was here imho. We handled the quote, but we don't advance to the next character because the logic is trying to be too clever with the \ being or not being doubled.

The " is then processed in the next loop, and stops the string from being quoted, even though the quote was escaped.


// We processed the escaped " move to next char.
index++;
continue;
}
else

// If the char after '\' is anything else, output the slash. And continue processing the next char.
if (index <= args.Length)
{
inQuotes = !inQuotes;
currentArg.Append('\\');

// Don't skip to the next char. We outputted the \ because it was not escaping \ or ". Let the next character to be processed by the loop.
// index++;
continue;
}
}
}
// Unescaped quote enters and leaves quoted mode.
else if (args[index] == '"')
{
inQuotes = !inQuotes;
index++;
}
else
{
// Collect all other characters.
currentArg.Append(args[index]);
index++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,28 @@ namespace Microsoft.TestPlatform.Utilities.Tests;
[TestClass]
public class CommandLineUtilitiesTest
{
private static void VerifyCommandLineSplitter(string commandLine, string[] expected)
[TestMethod]
[DataRow("", new string[] { })]
[DataRow(" /a:b ", new string[] { "/a:b" })]
[DataRow("""
/param1
/param2:value2
/param3:"value with spaces"
""", new string[] { "/param1", "/param2:value2", "/param3:value with spaces" })]
[DataRow("""/param3 #comment""", new string[] { "/param3" })]
[DataRow("""
/param3 #comment ends with newline \" \\
/param4
""", new string[] { "/param3", "/param4" })]
[DataRow("""/testadapterpath:"c:\Path" """, new string[] { @"/testadapterpath:c:\Path" })]
[DataRow("""/testadapterpath:"c:\Path" /logger:"trx" """, new string[] { @"/testadapterpath:c:\Path", "/logger:trx" })]
[DataRow("""/testadapterpath:"c:\Path" /logger:"trx" /diag:"log.txt" """, new string[] { @"/testadapterpath:c:\Path", "/logger:trx", "/diag:log.txt" })]
[DataRow("""/Tests:"Test(\"iCT 256\")" """, new string[] { """/Tests:Test("iCT 256")""" })]
public void VerifyCommandLineSplitter(string input, string[] expected)
{
CommandLineUtilities.SplitCommandLineIntoArguments(commandLine, out var actual);
CommandLineUtilities.SplitCommandLineIntoArguments(input, out var actual);

Assert.AreEqual(expected.Length, actual.Length);
for (int i = 0; i < actual.Length; ++i)
{
Assert.AreEqual(expected[i], actual[i]);
}
CollectionAssert.AreEqual(expected, actual);
}

[TestMethod]
public void TestCommandLineSplitter()
{
VerifyCommandLineSplitter("", []);
VerifyCommandLineSplitter("/testadapterpath:\"c:\\Path\"", [@"/testadapterpath:c:\Path"]);
VerifyCommandLineSplitter("/testadapterpath:\"c:\\Path\" /logger:\"trx\"", [@"/testadapterpath:c:\Path", "/logger:trx"]);
VerifyCommandLineSplitter("/testadapterpath:\"c:\\Path\" /logger:\"trx\" /diag:\"log.txt\"", [@"/testadapterpath:c:\Path", "/logger:trx", "/diag:log.txt"]);
}
}