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

Support rsp in MTP #4072

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,18 @@

namespace Microsoft.Testing.Platform.CommandLine;

internal sealed class CommandLineParseResult(string? toolName, IReadOnlyList<OptionRecord> options, IReadOnlyList<string> errors, IReadOnlyList<string> originalArguments) : IEquatable<CommandLineParseResult>
internal sealed class CommandLineParseResult(string? toolName, IReadOnlyList<OptionRecord> options, IReadOnlyList<string> errors) : IEquatable<CommandLineParseResult>
{
public const char OptionPrefix = '-';

public static CommandLineParseResult Empty => new(null, [], [], []);
public static CommandLineParseResult Empty => new(null, [], []);

public string? ToolName { get; } = toolName;

public IReadOnlyList<OptionRecord> Options { get; } = options;

public IReadOnlyList<string> Errors { get; } = errors;

public IReadOnlyList<string> OriginalArguments { get; } = originalArguments;

public bool HasError => Errors.Count > 0;

public bool HasTool => ToolName is not null;
Expand Down
13 changes: 11 additions & 2 deletions src/Platform/Microsoft.Testing.Platform/CommandLine/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ internal static class CommandLineParser
/// * A POSIX convention lets you omit the delimiter when you are specifying a single-character option alias, i.e. myapp -vquiet.
/// </summary>
public static CommandLineParseResult Parse(string[] args, IEnvironment environment)
=> Parse(args.ToList(), environment);

private static CommandLineParseResult Parse(List<string> args, IEnvironment environment)
{
List<OptionRecord> options = [];
List<string> errors = [];
Expand All @@ -41,8 +44,14 @@ public static CommandLineParseResult Parse(string[] args, IEnvironment environme
string? currentArg = null;
string? toolName = null;
List<string> currentOptionArguments = [];
for (int i = 0; i < args.Length; i++)
for (int i = 0; i < args.Count; i++)
{
if (args[i].StartsWith('@') && ResponseFileHelper.TryReadResponseFile(args[i].Substring(1), errors, out string[]? newArguments))
{
args.InsertRange(i + 1, newArguments);
continue;
}

bool argumentHandled = false;
currentArg = args[i];

Expand Down Expand Up @@ -118,7 +127,7 @@ public static CommandLineParseResult Parse(string[] args, IEnvironment environme
options.Add(new(currentOption, currentOptionArguments.ToArray()));
}

return new CommandLineParseResult(toolName, options, errors, args);
return new CommandLineParseResult(toolName, options, errors);

static void ParseOptionAndSeparators(string arg, out string? currentOption, out string? currentArg)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics.CodeAnalysis;
using System.Globalization;

using Microsoft.Testing.Platform.Resources;

// Most of the core logic is from https://github.com/dotnet/command-line-api/blob/feb61c7f328a2401d74f4317b39d02126cfdfe24/src/System.CommandLine/Parsing/CliParser.cs#L49
MarcoRossignoli marked this conversation as resolved.
Show resolved Hide resolved
internal static class ResponseFileHelper
{
internal static bool TryReadResponseFile(string rspFilePath, List<string> errors, [NotNullWhen(true)] out string[]? newArguments)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: let's use higher abstraction for errors.

Suggested change
internal static bool TryReadResponseFile(string rspFilePath, List<string> errors, [NotNullWhen(true)] out string[]? newArguments)
internal static bool TryReadResponseFile(string rspFilePath, ICollection<string> errors, [NotNullWhen(true)] out string[]? newArguments)

{
try
{
newArguments = ExpandResponseFile(rspFilePath).ToArray();
return true;
}
catch (FileNotFoundException)
{
errors.Add(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineParserResponseFileNotFound, rspFilePath));
}
catch (IOException e)
{
errors.Add(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineParserFailedToReadResponseFile, rspFilePath, e.Message));
Copy link
Member

Choose a reason for hiding this comment

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

It might be worth adding the tostring of the exception.

}

newArguments = null;
return false;

static IEnumerable<string> ExpandResponseFile(string filePath)
Copy link
Member

Choose a reason for hiding this comment

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

Stylistic nit:

Suggested change
static IEnumerable<string> ExpandResponseFile(string filePath)
// Local functions
static IEnumerable<string> ExpandResponseFile(string filePath)

{
string[] lines = File.ReadAllLines(filePath);
Copy link
Member

Choose a reason for hiding this comment

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

Could it be worth reading async (cc @MarcoRossignoli)?


for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];

foreach (string p in SplitLine(line))
{
yield return p;
}
}
}

static IEnumerable<string> SplitLine(string line)
{
string arg = line.Trim();

if (arg.Length == 0 || arg[0] == '#')
{
yield break;
}

foreach (string word in SplitCommandLine(arg))
{
yield return word;
}
}
}

private enum Boundary
{
TokenStart,
WordEnd,
QuoteStart,
QuoteEnd,
}

public static IEnumerable<string> SplitCommandLine(string commandLine)
{
int startTokenIndex = 0;

int pos = 0;

Boundary seeking = Boundary.TokenStart;
Boundary seekingQuote = Boundary.QuoteStart;

while (pos < commandLine.Length)
{
char c = commandLine[pos];

if (char.IsWhiteSpace(c))
{
if (seekingQuote == Boundary.QuoteStart)
{
switch (seeking)
{
case Boundary.WordEnd:
yield return CurrentToken();
startTokenIndex = pos;
seeking = Boundary.TokenStart;
break;

case Boundary.TokenStart:
startTokenIndex = pos;
break;
}
}
}
else if (c == '\"')
{
if (seeking == Boundary.TokenStart)
{
switch (seekingQuote)
{
case Boundary.QuoteEnd:
yield return CurrentToken();
startTokenIndex = pos;
seekingQuote = Boundary.QuoteStart;
break;

case Boundary.QuoteStart:
startTokenIndex = pos + 1;
seekingQuote = Boundary.QuoteEnd;
break;
}
}
else
{
switch (seekingQuote)
{
case Boundary.QuoteEnd:
seekingQuote = Boundary.QuoteStart;
break;

case Boundary.QuoteStart:
seekingQuote = Boundary.QuoteEnd;
break;
}
}
}
else if (seeking == Boundary.TokenStart && seekingQuote == Boundary.QuoteStart)
{
seeking = Boundary.WordEnd;
startTokenIndex = pos;
}

Advance();

if (IsAtEndOfInput())
{
switch (seeking)
{
case Boundary.TokenStart:
break;
default:
yield return CurrentToken();
break;
}
}
}

void Advance() => pos++;

string CurrentToken() => commandLine.Substring(startTokenIndex, IndexOfEndOfToken()).ToString().Replace("\"", string.Empty);

int IndexOfEndOfToken() => pos - startTokenIndex;

bool IsAtEndOfInput() => pos == commandLine.Length;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -655,4 +655,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
<data name="ExitCode" xml:space="preserve">
<value>Exit code</value>
</data>
<data name="CommandLineParserResponseFileNotFound" xml:space="preserve">
<value>The response file '{0}' was not found</value>
</data>
<data name="CommandLineParserFailedToReadResponseFile" xml:space="preserve">
<value>Failed to read response file '{0}'. {1}.</value>
<comment>{1} is the exception</comment>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">Rozhraní ICommandLineOptions ještě není sestavené.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">Neočekávaný argument {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">ICommandLineOptions wurde noch nicht erstellt.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">Unerwartetes Argument {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">ICommandLineOptions aún no se ha compilado.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">Argumento inesperado {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">ICommandLineOptions n’a pas encore été généré.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">Arguments inattendue {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">ICommandLineOptions non è stato ancora compilato.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">Argomento imprevisto {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">ICommandLineOptions はまだ構築されていません。</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">予期しない引数 {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">ICommandLineOptions가 아직 빌드되지 않았습니다.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">예기치 않은 인수 {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">Obiekt ICommandLineOptions nie został jeszcze skompilowany.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">Nieoczekiwany argument {0}</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@
<target state="translated">O ICommandLineOptions ainda não foi criado.</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserFailedToReadResponseFile">
<source>Failed to read response file '{0}'. {1}.</source>
<target state="new">Failed to read response file '{0}'. {1}.</target>
<note>{1} is the exception</note>
</trans-unit>
<trans-unit id="CommandLineParserResponseFileNotFound">
<source>The response file '{0}' was not found</source>
<target state="new">The response file '{0}' was not found</target>
<note />
</trans-unit>
<trans-unit id="CommandLineParserUnexpectedArgument">
<source>Unexpected argument {0}</source>
<target state="translated">Argumento inesperado {0}</target>
Expand Down
Loading