diff --git a/src/Microsoft.TestPlatform.Utilities/CommandLineUtilities.cs b/src/Microsoft.TestPlatform.Utilities/CommandLineUtilities.cs index db5b9676d7..93aed86fd9 100644 --- a/src/Microsoft.TestPlatform.Utilities/CommandLineUtilities.cs +++ b/src/Microsoft.TestPlatform.Utilities/CommandLineUtilities.cs @@ -1,157 +1,108 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -// Code taken from: https://github.com/dotnet/roslyn/blob/d00363d8f892f4f3c514718a964ea37783d21de5/src/Compilers/Core/Portable/InternalUtilities/CommandLineUtilities.cs - namespace Microsoft.VisualStudio.TestPlatform.Utilities { + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + using System; using System.Collections.Generic; + using System.Globalization; + using System.IO; using System.Text; public static class CommandLineUtilities { - /// - /// Split a command line by the same rules as Main would get the commands except the original - /// state of backslashes and quotes are preserved. For example in normal Windows command line - /// parsing the following command lines would produce equivalent Main arguments: - /// - /// - /r:a,b - /// - /r:"a,b" - /// - /// This method will differ as the latter will have the quotes preserved. The only case where - /// quotes are removed is when the entire argument is surrounded by quotes without any inner - /// quotes. - /// - /// - /// Rules for command line parsing, according to MSDN: - /// - /// Arguments are delimited by white space, which is either a space or a tab. - /// - /// A string surrounded by double quotation marks ("string") is interpreted - /// as a single argument, regardless of white space contained within. - /// A quoted string can be embedded in an argument. - /// - /// A double quotation mark preceded by a backslash (\") is interpreted as a - /// literal double quotation mark character ("). - /// - /// Backslashes are interpreted literally, unless they immediately precede a - /// double quotation mark. - /// - /// If an even number of backslashes is followed by a double quotation mark, - /// one backslash is placed in the argv array for every pair of backslashes, - /// and the double quotation mark is interpreted as a string delimiter. - /// - /// If an odd number of backslashes is followed by a double quotation mark, - /// one backslash is placed in the argv array for every pair of backslashes, - /// and the double quotation mark is "escaped" by the remaining backslash, - /// causing a literal double quotation mark (") to be placed in argv. - /// - public static IEnumerable SplitCommandLineIntoArguments(string commandLine, bool removeHashComments) - { - char? unused; - return SplitCommandLineIntoArguments(commandLine, removeHashComments, out unused); - } - - public static IEnumerable SplitCommandLineIntoArguments(string commandLine, bool removeHashComments, out char? illegalChar) + public static bool SplitCommandLineIntoArguments(string args, out string[] arguments) { - var builder = new StringBuilder(commandLine.Length); - var list = new List(); - var i = 0; + bool hadError = false; + var argArray = new List(); + var currentArg = new StringBuilder(); + bool inQuotes = false; + int index = 0; - illegalChar = null; - while (i < commandLine.Length) + try { - while (i < commandLine.Length && char.IsWhiteSpace(commandLine[i])) + while (true) { - i++; - } - - if (i == commandLine.Length) - { - break; - } + // skip whitespace + while (char.IsWhiteSpace(args[index])) + { + index += 1; + } - if (commandLine[i] == '#' && removeHashComments) - { - break; - } + // # - comment to end of line + if (args[index] == '#') + { + index += 1; + while (args[index] != '\n') + { + index += 1; + } + continue; + } - var quoteCount = 0; - builder.Length = 0; - while (i < commandLine.Length && (!char.IsWhiteSpace(commandLine[i]) || (quoteCount % 2 != 0))) - { - var current = commandLine[i]; - switch (current) + // do one argument + do { - case '\\': + if (args[index] == '\\') + { + int cSlashes = 1; + index += 1; + while (index == args.Length && args[index] == '\\') { - var slashCount = 0; - do - { - builder.Append(commandLine[i]); - i++; - slashCount++; - } while (i < commandLine.Length && commandLine[i] == '\\'); - - // Slashes not followed by a quote character can be ignored for now - if (i >= commandLine.Length || commandLine[i] != '"') - { - break; - } - - // If there is an odd number of slashes then it is escaping the quote - // otherwise it is just a quote. - if (slashCount % 2 == 0) - { - quoteCount++; - } - - builder.Append('"'); - i++; - break; + cSlashes += 1; } - case '"': - builder.Append(current); - quoteCount++; - i++; - break; - - default: - if ((current >= 0x1 && current <= 0x1f) || current == '|') + if (index == args.Length || args[index] != '"') { - if (illegalChar == null) - { - illegalChar = current; - } + currentArg.Append('\\', cSlashes); } else { - builder.Append(current); + currentArg.Append('\\', (cSlashes >> 1)); + if (0 != (cSlashes & 1)) + { + currentArg.Append('"'); + } + else + { + inQuotes = !inQuotes; + } } - - i++; - break; - } + } + else if (args[index] == '"') + { + inQuotes = !inQuotes; + index += 1; + } + else + { + currentArg.Append(args[index]); + index += 1; + } + } while (!char.IsWhiteSpace(args[index]) || inQuotes); + argArray.Add(currentArg.ToString()); + currentArg.Clear(); } - - // If the quote string is surrounded by quotes with no interior quotes then - // remove the quotes here. - if (quoteCount == 2 && builder[0] == '"' && builder[builder.Length - 1] == '"') + } + catch (IndexOutOfRangeException) + { + // got EOF + if (inQuotes) { - builder.Remove(0, length: 1); - builder.Remove(builder.Length - 1, length: 1); + EqtTrace.Verbose("Executor.Execute: Exiting with exit code of {0}", 1); + EqtTrace.Error(string.Format(CultureInfo.InvariantCulture, "Error: Unbalanced '\"' in command line argument file")); + hadError = true; } - - if (builder.Length > 0) + else if (currentArg.Length > 0) { - list.Add(builder.ToString()); + // valid argument can be terminated by EOF + argArray.Add(currentArg.ToString()); } } - return list; + arguments = argArray.ToArray(); + return hadError; } } } diff --git a/src/vstest.console/CommandLine/Executor.cs b/src/vstest.console/CommandLine/Executor.cs index f7e0d55b6f..6787a8cf61 100644 --- a/src/vstest.console/CommandLine/Executor.cs +++ b/src/vstest.console/CommandLine/Executor.cs @@ -230,7 +230,7 @@ private int GetArgumentProcessors(string[] args, out List pr this.Output.Error(false, ex.Message); result = 1; } - else if(ex is SettingsException) + else if (ex is SettingsException) { this.Output.Error(false, ex.Message); result = 1; @@ -246,7 +246,7 @@ private int GetArgumentProcessors(string[] args, out List pr } // If some argument was invalid, add help argument processor in beginning(i.e. at highest priority) - if(result == 1 && this.showHelp && processors.First().Metadata.Value.CommandName != HelpArgumentProcessor.CommandName) + if (result == 1 && this.showHelp && processors.First().Metadata.Value.CommandName != HelpArgumentProcessor.CommandName) { processors.Insert(0, processorFactory.CreateArgumentProcessor(HelpArgumentProcessor.CommandName)); } @@ -381,7 +381,6 @@ private void PrintSplashScreen() /// Arguments provided to perform execution with. /// Array of flattened arguments. /// 0 if successful and 1 otherwise. - /// private int FlattenArguments(IEnumerable arguments, out string[] flattenedArguments) { List outputArguments = new List(); @@ -393,8 +392,17 @@ private int FlattenArguments(IEnumerable arguments, out string[] flatten { // response file: string path = arg.Substring(1).TrimEnd(null); - result |= ParseResponseFile(path, out var responseFileArguments); - outputArguments.AddRange(responseFileArguments.Reverse()); + var hadError = this.ReadArgumentsAndSanitize(path, out var responseFileArgs, out var nestedArgs); + + if (hadError) + { + result |= 1; + } + else + { + this.Output.WriteLine(string.Format("vstest.console.exe {0}", responseFileArgs), OutputLevel.Information); + outputArguments.AddRange(nestedArgs); + } } else { @@ -407,58 +415,46 @@ private int FlattenArguments(IEnumerable arguments, out string[] flatten } /// - /// Parse a response file into a set of arguments. Errors opening the response file are output as errors. + /// Read and sanitize the arguments. /// - /// Full path to the response file. - /// Enumeration of the response file arguments. + /// File provided by user. + /// argument in the file as string. + /// Modified argument after sanitizing the contents of the file. /// 0 if successful and 1 otherwise. - /// - private int ParseResponseFile(string fullPath, out IEnumerable responseFileArguments) + public bool ReadArgumentsAndSanitize(string fileName, out string args, out string[] arguments) { - int result = 0; - List lines = new List(); - try + arguments = null; + if (GetContentUsingFile(fileName, out args)) { - using (var reader = new StreamReader( - new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read), - detectEncodingFromByteOrderMarks: true)) - { - string str; - while ((str = reader.ReadLine()) != null) - { - lines.Add(str); - } - } - - responseFileArguments = ParseResponseLines(lines); + return true; } - catch (Exception) + + if (string.IsNullOrEmpty(args)) { - this.Output.Error(false, string.Format(CultureInfo.CurrentCulture, CommandLineResources.OpenResponseFileError, fullPath)); - responseFileArguments = new string[0]; - result = 1; + return false; } - return result; + return CommandLineUtilities.SplitCommandLineIntoArguments(args, out arguments); } - /// - /// Take a string of lines from a response file, remove comments, - /// and split into a set of command line arguments. - /// - /// - private static IEnumerable ParseResponseLines(IEnumerable lines) + private bool GetContentUsingFile(string fileName, out string contents) { - List arguments = new List(); - - foreach (string line in lines) + contents = null; + try + { + contents = File.ReadAllText(fileName); + } + catch (Exception e) { - arguments.AddRange(CommandLineUtilities.SplitCommandLineIntoArguments(line, removeHashComments: true)); + EqtTrace.Verbose("Executor.Execute: Exiting with exit code of {0}", 1); + EqtTrace.Error(string.Format(CultureInfo.InvariantCulture, "Error: Can't open command line argument file '{0}' : '{1}'", fileName, e.Message)); + this.Output.Error(false, string.Format(CultureInfo.CurrentCulture, CommandLineResources.OpenResponseFileError, fileName)); + return true; } - return arguments; + return false; } #endregion } -} +} \ No newline at end of file diff --git a/test/Microsoft.TestPlatform.Utilities.UnitTests/CommandLineUtilitiesTest.cs b/test/Microsoft.TestPlatform.Utilities.UnitTests/CommandLineUtilitiesTest.cs index 5af7b6cd26..f63ec574b6 100644 --- a/test/Microsoft.TestPlatform.Utilities.UnitTests/CommandLineUtilitiesTest.cs +++ b/test/Microsoft.TestPlatform.Utilities.UnitTests/CommandLineUtilitiesTest.cs @@ -3,18 +3,15 @@ namespace Microsoft.TestPlatform.Utilities.Tests { - using System.Linq; - using Microsoft.VisualStudio.TestPlatform.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] public class CommandLineUtilitiesTest { - /// - private void VerifyCommandLineSplitter(string commandLine, string[] expected, bool removeHashComments = false) + private void VerifyCommandLineSplitter(string commandLine, string[] expected) { - var actual = CommandLineUtilities.SplitCommandLineIntoArguments(commandLine, removeHashComments).ToArray(); + CommandLineUtilities.SplitCommandLineIntoArguments(commandLine, out var actual); Assert.AreEqual(expected.Length, actual.Length); for (int i = 0; i < actual.Length; ++i) @@ -23,31 +20,13 @@ private void VerifyCommandLineSplitter(string commandLine, string[] expected, bo } } - /// [TestMethod] public void TestCommandLineSplitter() { VerifyCommandLineSplitter("", new string[0]); - VerifyCommandLineSplitter(" \t ", new string[0]); - VerifyCommandLineSplitter(" abc\tdef baz quuz ", new[] { "abc", "def", "baz", "quuz" }); - VerifyCommandLineSplitter(@" ""abc def"" fi""ddle dee de""e ""hi there ""dude he""llo there"" ", - new string[] { @"abc def", @"fi""ddle dee de""e", @"""hi there ""dude", @"he""llo there""" }); - VerifyCommandLineSplitter(@" ""abc def \"" baz quuz"" ""\""straw berry"" fi\""zz \""buzz fizzbuzz", - new string[] { @"abc def \"" baz quuz", @"\""straw berry", @"fi\""zz", @"\""buzz", @"fizzbuzz" }); - VerifyCommandLineSplitter(@" \\""abc def"" \\\""abc def"" ", - new string[] { @"\\""abc def""", @"\\\""abc", @"def"" " }); - VerifyCommandLineSplitter(@" \\\\""abc def"" \\\\\""abc def"" ", - new string[] { @"\\\\""abc def""", @"\\\\\""abc", @"def"" " }); - VerifyCommandLineSplitter(@" \\\\""abc def"" \\\\\""abc def"" q a r ", - new string[] { @"\\\\""abc def""", @"\\\\\""abc", @"def"" q a r " }); - VerifyCommandLineSplitter(@"abc #Comment ignored", - new string[] { @"abc" }, removeHashComments: true); - VerifyCommandLineSplitter(@"""foo bar"";""baz"" ""tree""", - new string[] { @"""foo bar"";""baz""", "tree" }); - VerifyCommandLineSplitter(@"/reference:""a, b"" ""test""", - new string[] { @"/reference:""a, b""", "test" }); - VerifyCommandLineSplitter(@"fo""o ba""r", - new string[] { @"fo""o ba""r" }); + VerifyCommandLineSplitter("/testadapterpath:\"c:\\Path\"", new[] { @"/testadapterpath:c:\Path" }); + VerifyCommandLineSplitter("/testadapterpath:\"c:\\Path\" /logger:\"trx\"", new[] { @"/testadapterpath:c:\Path", "/logger:trx" }); + VerifyCommandLineSplitter("/testadapterpath:\"c:\\Path\" /logger:\"trx\" /diag:\"log.txt\"", new[] { @"/testadapterpath:c:\Path", "/logger:trx", "/diag:log.txt" }); } } }