From fc92b28f87599ac25d35399dc2df2f356a285960 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Thu, 12 Jul 2018 22:27:09 -0700 Subject: Refactor command line parsing to enable extensions there in light.exe Fixes wixtoolset/issues#5845 --- .../CommandLine/CommandLineArguments.cs | 211 ++++++++ .../CommandLine/CommandLineContext.cs | 4 +- .../CommandLine/CommandLineHelper.cs | 216 -------- .../CommandLine/CommandLineParser.cs | 593 ++++++--------------- .../CommandLine/ParseCommandLine.cs | 257 +++++++++ src/WixToolset.Core/Preprocessor.cs | 2 +- src/WixToolset.Core/WixToolsetServiceProvider.cs | 1 + 7 files changed, 636 insertions(+), 648 deletions(-) create mode 100644 src/WixToolset.Core/CommandLine/CommandLineArguments.cs delete mode 100644 src/WixToolset.Core/CommandLine/CommandLineHelper.cs create mode 100644 src/WixToolset.Core/CommandLine/ParseCommandLine.cs (limited to 'src/WixToolset.Core') diff --git a/src/WixToolset.Core/CommandLine/CommandLineArguments.cs b/src/WixToolset.Core/CommandLine/CommandLineArguments.cs new file mode 100644 index 00000000..37adcfd3 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/CommandLineArguments.cs @@ -0,0 +1,211 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.CommandLine +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + using WixToolset.Extensibility.Services; + + internal class CommandLineArguments : ICommandLineArguments + { + public string[] OriginalArguments { get; set; } + + public string[] Arguments { get; set; } + + public string[] Extensions { get; set; } + + public string ErrorArgument { get; set; } + + private IServiceProvider ServiceProvider { get; } + + public CommandLineArguments(IServiceProvider serviceProvider) + { + this.ServiceProvider = serviceProvider; + } + + public void Populate(string commandLine) + { + var args = CommandLineArguments.ParseArgumentsToArray(commandLine); + + this.Populate(args.ToArray()); + } + + public void Populate(string[] args) + { + this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(args); + + this.ProcessArgumentsAndParseExtensions(this.OriginalArguments); + } + + public IParseCommandLine Parse() + { + var messaging = (IMessaging)this.ServiceProvider.GetService(typeof(IMessaging)); + + return new ParseCommandLine(messaging, this.Arguments, this.ErrorArgument); + } + + private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments) + { + List args = new List(); + + foreach (var arg in commandLineArguments) + { + if ('@' == arg[0]) + { + var responseFileArguments = CommandLineArguments.ParseResponseFile(arg.Substring(1)); + args.AddRange(responseFileArguments); + } + else + { + args.Add(arg); + } + } + + this.OriginalArguments = args.ToArray(); + } + + private void ProcessArgumentsAndParseExtensions(string[] args) + { + var arguments = new List(); + var extensions = new List(); + + for (var i = 0; i < args.Length; ++i) + { + var arg = args[i]; + + if ("-ext" == arg || "/ext" == arg) + { + if (!CommandLineArguments.IsSwitchAt(args, ++i)) + { + extensions.Add(args[i]); + } + else + { + this.ErrorArgument = arg; + break; + } + } + else + { + arguments.Add(arg); + } + } + + this.Arguments = arguments.ToArray(); + this.Extensions = extensions.ToArray(); + } + + private static List ParseResponseFile(string responseFile) + { + string arguments; + + using (StreamReader reader = new StreamReader(responseFile)) + { + arguments = reader.ReadToEnd(); + } + + return CommandLineArguments.ParseArgumentsToArray(arguments); + } + + private static List ParseArgumentsToArray(string arguments) + { + // Scan and parse the arguments string, dividing up the arguments based on whitespace. + // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed. + // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments. + // Escaped quotes and escaped backslashes also need to be unescaped by this process. + + // Collects the final list of arguments to be returned. + var argsList = new List(); + + // True if we are inside an unescaped quote, meaning whitespace should be ignored. + var insideQuote = false; + + // Index of the start of the current argument substring; either the start of the argument + // or the start of a quoted or unquoted sequence within it. + var partStart = 0; + + // The current argument string being built; when completed it will be added to the list. + var arg = new StringBuilder(); + + for (int i = 0; i <= arguments.Length; i++) + { + if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote)) + { + // Reached a whitespace separator or the end of the string. + + // Finish building the current argument. + arg.Append(arguments.Substring(partStart, i - partStart)); + + // Skip over the whitespace character. + partStart = i + 1; + + // Add the argument to the list if it's not empty. + if (arg.Length > 0) + { + argsList.Add(CommandLineArguments.ExpandEnvironmentVariables(arg.ToString())); + arg.Length = 0; + } + } + else if (i > partStart && arguments[i - 1] == '\\') + { + // Check the character following an unprocessed backslash. + // Unescape quotes, and backslashes followed by a quote. + if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"')) + { + // Unescape the quote or backslash by skipping the preceeding backslash. + arg.Append(arguments.Substring(partStart, i - 1 - partStart)); + arg.Append(arguments[i]); + partStart = i + 1; + } + } + else if (arguments[i] == '"') + { + // Add the quoted or unquoted section to the argument string. + arg.Append(arguments.Substring(partStart, i - partStart)); + + // And skip over the quote character. + partStart = i + 1; + + insideQuote = !insideQuote; + } + } + + return argsList; + } + + private static string ExpandEnvironmentVariables(string arguments) + { + var id = Environment.GetEnvironmentVariables(); + + var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)"); + MatchCollection matches = regex.Matches(arguments); + + string value = String.Empty; + for (int i = 0; i <= (matches.Count - 1); i++) + { + try + { + var key = matches[i].Value; + regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)")); + value = id[key].ToString(); + arguments = regex.Replace(arguments, value); + } + catch (NullReferenceException) + { + // Collapse unresolved environment variables. + arguments = regex.Replace(arguments, value); + } + } + + return arguments; + } + + private static bool IsSwitchAt(string[] args, int index) + { + return args.Length > index && !String.IsNullOrEmpty(args[index]) && ('/' == args[index][0] || '-' == args[index][0]); + } + } +} diff --git a/src/WixToolset.Core/CommandLine/CommandLineContext.cs b/src/WixToolset.Core/CommandLine/CommandLineContext.cs index 2ff2c1fd..c589222d 100644 --- a/src/WixToolset.Core/CommandLine/CommandLineContext.cs +++ b/src/WixToolset.Core/CommandLine/CommandLineContext.cs @@ -18,8 +18,6 @@ namespace WixToolset.Core.CommandLine public IExtensionManager ExtensionManager { get; set; } - public string Arguments { get; set; } - - public string[] ParsedArguments { get; set; } + public ICommandLineArguments Arguments { get; set; } } } diff --git a/src/WixToolset.Core/CommandLine/CommandLineHelper.cs b/src/WixToolset.Core/CommandLine/CommandLineHelper.cs deleted file mode 100644 index 51ece0f7..00000000 --- a/src/WixToolset.Core/CommandLine/CommandLineHelper.cs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - -namespace WixToolset.Core.CommandLine -{ - using System; - using System.IO; - using WixToolset.Data; - using WixToolset.Extensibility.Services; - - public class CommandLineHelper - { - /// - /// Validates that a string is a valid directory name, and throws appropriate warnings/errors if not - /// - /// The commandline switch we're parsing (for error display purposes). - /// The messagehandler to report warnings/errors to. - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be parsed. - /// The string if it is valid, null if it is invalid. - public static string GetDirectory(string commandlineSwitch, IMessaging messageHandler, string[] args, int index) - { - return GetDirectory(commandlineSwitch, messageHandler, args, index, false); - } - - /// - /// Validates that a string is a valid directory name, and throws appropriate warnings/errors if not - /// - /// The commandline switch we're parsing (for error display purposes). - /// The messagehandler to report warnings/errors to. - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be parsed. - /// Indicates if a colon-delimited prefix is allowed. - /// The string if it is valid, null if it is invalid. - public static string GetDirectory(string commandlineSwitch, IMessaging messageHandler, string[] args, int index, bool allowPrefix) - { - commandlineSwitch = String.Concat("-", commandlineSwitch); - - if (!IsValidArg(args, index)) - { - messageHandler.Write(ErrorMessages.DirectoryPathRequired(commandlineSwitch)); - return null; - } - - if (File.Exists(args[index])) - { - messageHandler.Write(ErrorMessages.ExpectedDirectoryGotFile(commandlineSwitch, args[index])); - return null; - } - - return VerifyPath(messageHandler, args[index], allowPrefix); - } - - /// - /// Validates that a string is a valid filename, and throws appropriate warnings/errors if not - /// - /// The commandline switch we're parsing (for error display purposes). - /// The messagehandler to report warnings/errors to. - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be parsed. - /// The string if it is valid, null if it is invalid. - public static string GetFile(string commandlineSwitch, IMessaging messageHandler, string[] args, int index) - { - commandlineSwitch = String.Concat("-", commandlineSwitch); - - if (!IsValidArg(args, index)) - { - messageHandler.Write(ErrorMessages.FilePathRequired(commandlineSwitch)); - return null; - } - - if (Directory.Exists(args[index])) - { - messageHandler.Write(ErrorMessages.ExpectedFileGotDirectory(commandlineSwitch, args[index])); - return null; - } - - return VerifyPath(messageHandler, args[index]); - } - - /// - /// Get a set of files that possibly have a search pattern in the path (such as '*'). - /// - /// Search path to find files in. - /// Type of file; typically "Source". - /// An array of files matching the search path. - /// - /// This method is written in this verbose way because it needs to support ".." in the path. - /// It needs the directory path isolated from the file name in order to use Directory.GetFiles - /// or DirectoryInfo.GetFiles. The only way to get this directory path is manually since - /// Path.GetDirectoryName does not support ".." in the path. - /// - /// Throws WixFileNotFoundException if no file matching the pattern can be found. - public static string[] GetFiles(string searchPath, string fileType) - { - if (null == searchPath) - { - throw new ArgumentNullException(nameof(searchPath)); - } - - // Convert alternate directory separators to the standard one. - string filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - int lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar); - string[] files = null; - - try - { - if (0 > lastSeparator) - { - files = Directory.GetFiles(".", filePath); - } - else // found directory separator - { - files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), filePath.Substring(lastSeparator + 1)); - } - } - catch (DirectoryNotFoundException) - { - // Don't let this function throw the DirectoryNotFoundException. This exception - // occurs for non-existant directories and invalid characters in the searchPattern. - } - catch (ArgumentException) - { - // Don't let this function throw the ArgumentException. This exception - // occurs in certain situations such as when passing a malformed UNC path. - } - catch (IOException) - { - throw new WixFileNotFoundException(searchPath, fileType); - } - - if (null == files || 0 == files.Length) - { - throw new WixFileNotFoundException(searchPath, fileType); - } - - return files; - } - - /// - /// Validates that a valid string parameter (without "/" or "-"), and returns a bool indicating its validity - /// - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be validated. - /// True if a valid string parameter exists there, false if not. - public static bool IsValidArg(string[] args, int index) - { - if (args.Length <= index || String.IsNullOrEmpty(args[index]) || '/' == args[index][0] || '-' == args[index][0]) - { - return false; - } - else - { - return true; - } - } - - /// - /// Validates that a commandline parameter is a valid file or directory name, and throws appropriate warnings/errors if not - /// - /// The messagehandler to report warnings/errors to. - /// The path to test. - /// The string if it is valid, null if it is invalid. - public static string VerifyPath(IMessaging messageHandler, string path) - { - return VerifyPath(messageHandler, path, false); - } - - /// - /// Validates that a commandline parameter is a valid file or directory name, and throws appropriate warnings/errors if not - /// - /// The messagehandler to report warnings/errors to. - /// The path to test. - /// Indicates if a colon-delimited prefix is allowed. - /// The full path if it is valid, null if it is invalid. - public static string VerifyPath(IMessaging messageHandler, string path, bool allowPrefix) - { - string fullPath; - - if (0 <= path.IndexOf('\"')) - { - messageHandler.Write(ErrorMessages.PathCannotContainQuote(path)); - return null; - } - - try - { - string prefix = null; - if (allowPrefix) - { - int prefixLength = path.IndexOf('=') + 1; - if (0 != prefixLength) - { - prefix = path.Substring(0, prefixLength); - path = path.Substring(prefixLength); - } - } - - if (String.IsNullOrEmpty(prefix)) - { - fullPath = Path.GetFullPath(path); - } - else - { - fullPath = String.Concat(prefix, Path.GetFullPath(path)); - } - } - catch (Exception e) - { - messageHandler.Write(ErrorMessages.InvalidCommandLineFileName(path, e.Message)); - return null; - } - - return fullPath; - } - } -} diff --git a/src/WixToolset.Core/CommandLine/CommandLineParser.cs b/src/WixToolset.Core/CommandLine/CommandLineParser.cs index f4bc8ade..da0e979c 100644 --- a/src/WixToolset.Core/CommandLine/CommandLineParser.cs +++ b/src/WixToolset.Core/CommandLine/CommandLineParser.cs @@ -5,9 +5,6 @@ namespace WixToolset.Core.CommandLine using System; using System.Collections.Generic; using System.IO; - using System.Linq; - using System.Text; - using System.Text.RegularExpressions; using WixToolset.Data; using WixToolset.Extensibility; using WixToolset.Extensibility.Services; @@ -22,7 +19,7 @@ namespace WixToolset.Core.CommandLine Bind, } - internal class CommandLineParser : ICommandLine, IParseCommandLine + internal class CommandLineParser : ICommandLine { private IServiceProvider ServiceProvider { get; set; } @@ -32,14 +29,8 @@ namespace WixToolset.Core.CommandLine public string ActiveCommand { get; private set; } - public string[] OriginalArguments { get; private set; } - - public Queue RemainingArguments { get; } = new Queue(); - public IExtensionManager ExtensionManager { get; private set; } - public string ErrorArgument { get; set; } - public bool ShowHelp { get; set; } public ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context) @@ -50,18 +41,6 @@ namespace WixToolset.Core.CommandLine this.ExtensionManager = context.ExtensionManager ?? this.ServiceProvider.GetService(); - var args = context.ParsedArguments ?? Array.Empty(); - - if (!String.IsNullOrEmpty(context.Arguments)) - { - args = CommandLineParser.ParseArgumentsToArray(context.Arguments).Concat(args).ToArray(); - } - - return this.ParseStandardCommandLine(context, args); - } - - private ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context, string[] args) - { var next = String.Empty; var command = Commands.Unknown; @@ -89,99 +68,99 @@ namespace WixToolset.Core.CommandLine var outputsFile = String.Empty; var builtOutputsFile = String.Empty; - this.Parse(context, args, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, arg) => + this.Parse(context, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, parser, arg) => { - if (cmdline.IsSwitch(arg)) + if (parser.IsSwitch(arg)) { var parameter = arg.Substring(1); switch (parameter.ToLowerInvariant()) { - case "?": - case "h": - case "help": - cmdline.ShowHelp = true; - return true; - - case "bindfiles": - bindFiles = true; - return true; - - case "bindpath": - cmdline.GetNextArgumentOrError(bindPaths); - return true; - - case "cc": - cmdline.GetNextArgumentOrError(ref cabCachePath); - return true; - - case "culture": - cmdline.GetNextArgumentOrError(cultures); - return true; - case "contentsfile": - cmdline.GetNextArgumentOrError(ref contentsFile); - return true; - case "outputsfile": - cmdline.GetNextArgumentOrError(ref outputsFile); - return true; - case "builtoutputsfile": - cmdline.GetNextArgumentOrError(ref builtOutputsFile); - return true; - - case "d": - case "define": - cmdline.GetNextArgumentOrError(defines); - return true; - - case "i": - case "includepath": - cmdline.GetNextArgumentOrError(includePaths); - return true; - - case "intermediatefolder": - cmdline.GetNextArgumentOrError(ref intermediateFolder); - return true; - - case "loc": - cmdline.GetNextArgumentAsFilePathOrError(locFiles, "localization files"); - return true; - - case "lib": - cmdline.GetNextArgumentAsFilePathOrError(libraryFiles, "library files"); - return true; - - case "o": - case "out": - cmdline.GetNextArgumentOrError(ref outputFile); - return true; - - case "outputtype": - cmdline.GetNextArgumentOrError(ref outputType); - return true; - - case "nologo": - showLogo = false; - return true; - - case "v": - case "verbose": - verbose = true; - return true; - - case "version": - case "-version": - showVersion = true; - return true; - - case "sval": - // todo: implement - return true; + case "?": + case "h": + case "help": + cmdline.ShowHelp = true; + return true; + + case "bindfiles": + bindFiles = true; + return true; + + case "bindpath": + parser.GetNextArgumentOrError(arg, bindPaths); + return true; + + case "cc": + cabCachePath = parser.GetNextArgumentOrError(arg); + return true; + + case "culture": + parser.GetNextArgumentOrError(arg, cultures); + return true; + case "contentsfile": + contentsFile = parser.GetNextArgumentAsFilePathOrError(arg); + return true; + case "outputsfile": + outputsFile = parser.GetNextArgumentAsFilePathOrError(arg); + return true; + case "builtoutputsfile": + builtOutputsFile = parser.GetNextArgumentAsFilePathOrError(arg); + return true; + + case "d": + case "define": + parser.GetNextArgumentOrError(arg, defines); + return true; + + case "i": + case "includepath": + parser.GetNextArgumentOrError(arg, includePaths); + return true; + + case "intermediatefolder": + intermediateFolder = parser.GetNextArgumentAsDirectoryOrError(arg); + return true; + + case "loc": + parser.GetNextArgumentAsFilePathOrError(arg, "localization files", locFiles); + return true; + + case "lib": + parser.GetNextArgumentAsFilePathOrError(arg, "library files", libraryFiles); + return true; + + case "o": + case "out": + outputFile = parser.GetNextArgumentAsFilePathOrError(arg); + return true; + + case "outputtype": + outputType= parser.GetNextArgumentOrError(arg); + return true; + + case "nologo": + showLogo = false; + return true; + + case "v": + case "verbose": + verbose = true; + return true; + + case "version": + case "-version": + showVersion = true; + return true; + + case "sval": + // todo: implement + return true; } return false; } else { - files.AddRange(CommandLineHelper.GetFiles(arg, "source code")); + parser.GetArgumentAsFilePathOrError(arg, "source code", files); return true; } }); @@ -205,22 +184,22 @@ namespace WixToolset.Core.CommandLine switch (command) { - case Commands.Build: - { - var sourceFiles = GatherSourceFiles(files, outputFolder); - var variables = this.GatherPreprocessorVariables(defines); - var bindPathList = this.GatherBindPaths(bindPaths); - var filterCultures = CalculateFilterCultures(cultures); - var type = CalculateOutputType(outputType, outputFile); - return new BuildCommand(this.ServiceProvider, sourceFiles, variables, locFiles, libraryFiles, filterCultures, outputFile, type, cabCachePath, bindFiles, bindPathList, includePaths, intermediateFolder, contentsFile, outputsFile, builtOutputsFile); - } + case Commands.Build: + { + var sourceFiles = GatherSourceFiles(files, outputFolder); + var variables = this.GatherPreprocessorVariables(defines); + var bindPathList = this.GatherBindPaths(bindPaths); + var filterCultures = CalculateFilterCultures(cultures); + var type = CalculateOutputType(outputType, outputFile); + return new BuildCommand(this.ServiceProvider, sourceFiles, variables, locFiles, libraryFiles, filterCultures, outputFile, type, cabCachePath, bindFiles, bindPathList, includePaths, intermediateFolder, contentsFile, outputsFile, builtOutputsFile); + } - case Commands.Compile: - { - var sourceFiles = GatherSourceFiles(files, outputFolder); - var variables = GatherPreprocessorVariables(defines); - return new CompileCommand(this.ServiceProvider, sourceFiles, variables); - } + case Commands.Compile: + { + var sourceFiles = GatherSourceFiles(files, outputFolder); + var variables = GatherPreprocessorVariables(defines); + return new CompileCommand(this.ServiceProvider, sourceFiles, variables); + } } return null; @@ -262,63 +241,87 @@ namespace WixToolset.Core.CommandLine switch (outputType.ToLowerInvariant()) { - case "bundle": - case ".exe": - return OutputType.Bundle; + case "bundle": + case ".exe": + return OutputType.Bundle; - case "library": - case ".wixlib": - return OutputType.Library; + case "library": + case ".wixlib": + return OutputType.Library; - case "module": - case ".msm": - return OutputType.Module; + case "module": + case ".msm": + return OutputType.Module; - case "patch": - case ".msp": - return OutputType.Patch; + case "patch": + case ".msp": + return OutputType.Patch; - case ".pcp": - return OutputType.PatchCreation; + case ".pcp": + return OutputType.PatchCreation; - case "product": - case "package": - case ".msi": - return OutputType.Product; + case "product": + case "package": + case ".msi": + return OutputType.Product; - case "transform": - case ".mst": - return OutputType.Transform; + case "transform": + case ".mst": + return OutputType.Transform; - case "intermediatepostlink": - case ".wixipl": - return OutputType.IntermediatePostLink; + case "intermediatepostlink": + case ".wixipl": + return OutputType.IntermediatePostLink; } return OutputType.Unknown; } -#if UNUSED - private static CommandLine Parse(string commandLineString, Func parseArgument) + private ICommandLine Parse(ICommandLineContext context, Func parseCommand, Func parseArgument) { - var arguments = CommandLine.ParseArgumentsToArray(commandLineString).ToArray(); - - return CommandLine.Parse(arguments, null, parseArgument); - } + var extensions = this.ExtensionManager.Create(); - private static CommandLine Parse(string[] commandLineArguments, Func parseArgument) - { - return CommandLine.Parse(commandLineArguments, null, parseArgument); - } -#endif + foreach (var extension in extensions) + { + extension.PreParse(context); + } - private ICommandLine Parse(ICommandLineContext context, string[] commandLineArguments, Func parseCommand, Func parseArgument) - { - this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(commandLineArguments); + var parser = context.Arguments.Parse(); - this.QueueArgumentsAndLoadExtensions(this.OriginalArguments); + while (!this.ShowHelp && + String.IsNullOrEmpty(parser.ErrorArgument) && + parser.TryGetNextSwitchOrArgument(out var arg)) + { + if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments. + { + continue; + } - this.ProcessRemainingArguments(context, parseArgument, parseCommand); + if (parser.IsSwitch(arg)) + { + if (!parseArgument(this, parser, arg) && + !this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions)) + { + parser.ErrorArgument = arg; + } + } + else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported. + { + if (parseCommand(this, arg)) + { + this.ActiveCommand = arg; + } + else + { + parser.ErrorArgument = arg; + } + } + else if (!this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions) && + !parseArgument(this, parser, arg)) + { + parser.ErrorArgument = arg; + } + } return this; } @@ -358,7 +361,7 @@ namespace WixToolset.Core.CommandLine return variables; } - private IEnumerable GatherBindPaths(IEnumerable bindPaths) + private IEnumerable GatherBindPaths(IEnumerable bindPaths) { var result = new List(); @@ -379,172 +382,11 @@ namespace WixToolset.Core.CommandLine return result; } - /// - /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity - /// - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be validated. - /// True if a valid switch exists there, false if not. - public bool IsSwitch(string arg) - { - return arg != null && arg.Length > 1 && ('/' == arg[0] || '-' == arg[0]); - } - - /// - /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity - /// - /// The list of strings to check. - /// The index (in args) of the commandline parameter to be validated. - /// True if a valid switch exists there, false if not. - public bool IsSwitchAt(IEnumerable args, int index) - { - var arg = args.ElementAtOrDefault(index); - return IsSwitch(arg); - } - - public void GetNextArgumentOrError(ref string arg) - { - this.TryGetNextArgumentOrError(out arg); - } - - public void GetNextArgumentOrError(IList args) - { - if (this.TryGetNextArgumentOrError(out var arg)) - { - args.Add(arg); - } - } - - public void GetNextArgumentAsFilePathOrError(IList args, string fileType) - { - if (this.TryGetNextArgumentOrError(out var arg)) - { - foreach (var path in CommandLineHelper.GetFiles(arg, fileType)) - { - args.Add(path); - } - } - } - - public bool TryGetNextArgumentOrError(out string arg) - { - if (TryDequeue(this.RemainingArguments, out arg) && !this.IsSwitch(arg)) - { - return true; - } - - this.ErrorArgument = arg ?? CommandLineParser.ExpectedArgument; - - return false; - } - - private static bool TryDequeue(Queue q, out string arg) - { - if (q.Count > 0) - { - arg = q.Dequeue(); - return true; - } - - arg = null; - return false; - } - - private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments) - { - List args = new List(); - - foreach (var arg in commandLineArguments) - { - if ('@' == arg[0]) - { - var responseFileArguments = CommandLineParser.ParseResponseFile(arg.Substring(1)); - args.AddRange(responseFileArguments); - } - else - { - args.Add(arg); - } - } - - this.OriginalArguments = args.ToArray(); - } - - private void QueueArgumentsAndLoadExtensions(string[] args) + private bool TryParseCommandLineArgumentWithExtension(string arg, IParseCommandLine parse, IEnumerable extensions) { - for (var i = 0; i < args.Length; ++i) - { - var arg = args[i]; - - if ("-ext" == arg || "/ext" == arg) - { - if (!this.IsSwitchAt(args, ++i)) - { - this.ExtensionManager.Load(args[i]); - } - else - { - this.ErrorArgument = arg; - break; - } - } - else - { - this.RemainingArguments.Enqueue(arg); - } - } - } - - private void ProcessRemainingArguments(ICommandLineContext context, Func parseArgument, Func parseCommand) - { - var extensions = this.ExtensionManager.Create(); - foreach (var extension in extensions) { - extension.PreParse(context); - } - - while (!this.ShowHelp && - String.IsNullOrEmpty(this.ErrorArgument) && - TryDequeue(this.RemainingArguments, out var arg)) - { - if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments. - { - continue; - } - - if ('-' == arg[0] || '/' == arg[0]) - { - if (!parseArgument(this, arg) && - !this.TryParseCommandLineArgumentWithExtension(arg, extensions)) - { - this.ErrorArgument = arg; - } - } - else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported. - { - if (parseCommand(this, arg)) - { - this.ActiveCommand = arg; - } - else - { - this.ErrorArgument = arg; - } - } - else if (!this.TryParseCommandLineArgumentWithExtension(arg, extensions) && - !parseArgument(this, arg)) - { - this.ErrorArgument = arg; - } - } - } - - private bool TryParseCommandLineArgumentWithExtension(string arg, IEnumerable extensions) - { - foreach (var extension in extensions) - { - if (extension.TryParseArgument(this, arg)) + if (extension.TryParseArgument(parse, arg)) { return true; } @@ -552,110 +394,5 @@ namespace WixToolset.Core.CommandLine return false; } - - private static List ParseResponseFile(string responseFile) - { - string arguments; - - using (StreamReader reader = new StreamReader(responseFile)) - { - arguments = reader.ReadToEnd(); - } - - return CommandLineParser.ParseArgumentsToArray(arguments); - } - - private static List ParseArgumentsToArray(string arguments) - { - // Scan and parse the arguments string, dividing up the arguments based on whitespace. - // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed. - // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments. - // Escaped quotes and escaped backslashes also need to be unescaped by this process. - - // Collects the final list of arguments to be returned. - var argsList = new List(); - - // True if we are inside an unescaped quote, meaning whitespace should be ignored. - var insideQuote = false; - - // Index of the start of the current argument substring; either the start of the argument - // or the start of a quoted or unquoted sequence within it. - var partStart = 0; - - // The current argument string being built; when completed it will be added to the list. - var arg = new StringBuilder(); - - for (int i = 0; i <= arguments.Length; i++) - { - if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote)) - { - // Reached a whitespace separator or the end of the string. - - // Finish building the current argument. - arg.Append(arguments.Substring(partStart, i - partStart)); - - // Skip over the whitespace character. - partStart = i + 1; - - // Add the argument to the list if it's not empty. - if (arg.Length > 0) - { - argsList.Add(CommandLineParser.ExpandEnvironmentVariables(arg.ToString())); - arg.Length = 0; - } - } - else if (i > partStart && arguments[i - 1] == '\\') - { - // Check the character following an unprocessed backslash. - // Unescape quotes, and backslashes followed by a quote. - if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"')) - { - // Unescape the quote or backslash by skipping the preceeding backslash. - arg.Append(arguments.Substring(partStart, i - 1 - partStart)); - arg.Append(arguments[i]); - partStart = i + 1; - } - } - else if (arguments[i] == '"') - { - // Add the quoted or unquoted section to the argument string. - arg.Append(arguments.Substring(partStart, i - partStart)); - - // And skip over the quote character. - partStart = i + 1; - - insideQuote = !insideQuote; - } - } - - return argsList; - } - - private static string ExpandEnvironmentVariables(string arguments) - { - var id = Environment.GetEnvironmentVariables(); - - var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)"); - MatchCollection matches = regex.Matches(arguments); - - string value = String.Empty; - for (int i = 0; i <= (matches.Count - 1); i++) - { - try - { - var key = matches[i].Value; - regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)")); - value = id[key].ToString(); - arguments = regex.Replace(arguments, value); - } - catch (NullReferenceException) - { - // Collapse unresolved environment variables. - arguments = regex.Replace(arguments, value); - } - } - - return arguments; - } } } diff --git a/src/WixToolset.Core/CommandLine/ParseCommandLine.cs b/src/WixToolset.Core/CommandLine/ParseCommandLine.cs new file mode 100644 index 00000000..7d0dcbd8 --- /dev/null +++ b/src/WixToolset.Core/CommandLine/ParseCommandLine.cs @@ -0,0 +1,257 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.CommandLine +{ + using System; + using System.Collections.Generic; + using System.IO; + using WixToolset.Data; + using WixToolset.Extensibility.Services; + + internal class ParseCommandLine : IParseCommandLine + { + private const string ExpectedArgument = "expected argument"; + + public string ErrorArgument { get; set; } + + private Queue RemainingArguments { get; } + + private IMessaging Messaging { get; } + + public ParseCommandLine(IMessaging messaging, string[] arguments, string errorArgument) + { + this.Messaging = messaging; + this.RemainingArguments = new Queue(arguments); + this.ErrorArgument = errorArgument; + } + + public bool IsSwitch(string arg) => !String.IsNullOrEmpty(arg) && ('/' == arg[0] || '-' == arg[0]); + + public void GetArgumentAsFilePathOrError(string argument, string fileType, IList paths) + { + foreach (var path in GetFiles(argument, fileType)) + { + paths.Add(path); + } + } + + public string GetNextArgumentOrError(string commandLineSwitch) + { + if (this.TryGetNextNonSwitchArgumentOrError(out var argument)) + { + return argument; + } + + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch)); + return null; + } + + public bool GetNextArgumentOrError(string commandLineSwitch, IList args) + { + if (this.TryGetNextNonSwitchArgumentOrError(out var arg)) + { + args.Add(arg); + return true; + } + + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch)); + return false; + } + + public string GetNextArgumentAsDirectoryOrError(string commandLineSwitch) + { + if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && TryGetDirectory(commandLineSwitch, this.Messaging, arg, out var directory)) + { + return directory; + } + + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch)); + return null; + } + + public bool GetNextArgumentAsDirectoryOrError(string commandLineSwitch, IList directories) + { + if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && TryGetDirectory(commandLineSwitch, this.Messaging, arg, out var directory)) + { + directories.Add(directory); + return true; + } + + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch)); + return false; + } + + public string GetNextArgumentAsFilePathOrError(string commandLineSwitch) + { + if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && this.TryGetFile(commandLineSwitch, arg, out var path)) + { + return path; + } + + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch)); + return null; + } + + public bool GetNextArgumentAsFilePathOrError(string commandLineSwitch, string fileType, IList paths) + { + if (this.TryGetNextNonSwitchArgumentOrError(out var arg)) + { + foreach (var path in GetFiles(arg, fileType)) + { + paths.Add(path); + } + + return true; + } + + this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch)); + return false; + } + + public bool TryGetNextSwitchOrArgument(out string arg) + { + return TryDequeue(this.RemainingArguments, out arg); + } + + private bool TryGetNextNonSwitchArgumentOrError(out string arg) + { + var result = this.TryGetNextSwitchOrArgument(out arg); + + if (!result && !this.IsSwitch(arg)) + { + this.ErrorArgument = arg ?? ParseCommandLine.ExpectedArgument; + } + + return result; + } + + private static bool IsValidArg(string arg) => !(String.IsNullOrEmpty(arg) || '/' == arg[0] || '-' == arg[0]); + + private static bool TryDequeue(Queue q, out string arg) + { + if (q.Count > 0) + { + arg = q.Dequeue(); + return true; + } + + arg = null; + return false; + } + + private bool TryGetDirectory(string commandlineSwitch, IMessaging messageHandler, string arg, out string directory) + { + directory = null; + + if (File.Exists(arg)) + { + this.Messaging.Write(ErrorMessages.ExpectedDirectoryGotFile(commandlineSwitch, arg)); + return false; + } + + directory = this.VerifyPath(arg); + return directory != null; + } + + private bool TryGetFile(string commandlineSwitch, string arg, out string path) + { + path = null; + + if (!IsValidArg(arg)) + { + this.Messaging.Write(ErrorMessages.FilePathRequired(commandlineSwitch)); + } + else if (Directory.Exists(arg)) + { + this.Messaging.Write(ErrorMessages.ExpectedFileGotDirectory(commandlineSwitch, arg)); + } + else + { + path = this.VerifyPath(arg); + } + + return path != null; + } + + /// + /// Get a set of files that possibly have a search pattern in the path (such as '*'). + /// + /// Search path to find files in. + /// Type of file; typically "Source". + /// An array of files matching the search path. + /// + /// This method is written in this verbose way because it needs to support ".." in the path. + /// It needs the directory path isolated from the file name in order to use Directory.GetFiles + /// or DirectoryInfo.GetFiles. The only way to get this directory path is manually since + /// Path.GetDirectoryName does not support ".." in the path. + /// + /// Throws WixFileNotFoundException if no file matching the pattern can be found. + private string[] GetFiles(string searchPath, string fileType) + { + if (null == searchPath) + { + throw new ArgumentNullException(nameof(searchPath)); + } + + // Convert alternate directory separators to the standard one. + string filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + int lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar); + var files = new string[0]; + + try + { + if (0 > lastSeparator) + { + files = Directory.GetFiles(".", filePath); + } + else // found directory separator + { + files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), filePath.Substring(lastSeparator + 1)); + } + } + catch (DirectoryNotFoundException) + { + // Don't let this function throw the DirectoryNotFoundException. This exception + // occurs for non-existant directories and invalid characters in the searchPattern. + } + catch (ArgumentException) + { + // Don't let this function throw the ArgumentException. This exception + // occurs in certain situations such as when passing a malformed UNC path. + } + catch (IOException) + { + } + + if (0 == files.Length) + { + this.Messaging.Write(ErrorMessages.FileNotFound(null, searchPath, fileType)); + } + + return files; + } + + private string VerifyPath(string path) + { + string fullPath; + + if (0 <= path.IndexOf('\"')) + { + this.Messaging.Write(ErrorMessages.PathCannotContainQuote(path)); + return null; + } + + try + { + fullPath = Path.GetFullPath(path); + } + catch (Exception e) + { + this.Messaging.Write(ErrorMessages.InvalidCommandLineFileName(path, e.Message)); + return null; + } + + return fullPath; + } + } +} diff --git a/src/WixToolset.Core/Preprocessor.cs b/src/WixToolset.Core/Preprocessor.cs index 6733f493..23d3f205 100644 --- a/src/WixToolset.Core/Preprocessor.cs +++ b/src/WixToolset.Core/Preprocessor.cs @@ -644,7 +644,7 @@ namespace WixToolset.Core if (null == includeFile) { - throw new WixException(ErrorMessages.FileNotFound(sourceLineNumbers, includePath, "include")); + throw new WixFileNotFoundException(sourceLineNumbers, includePath, "include"); } using (XmlReader reader = XmlReader.Create(includeFile, DocumentXmlReaderSettings)) diff --git a/src/WixToolset.Core/WixToolsetServiceProvider.cs b/src/WixToolset.Core/WixToolsetServiceProvider.cs index 20c6c309..7d318648 100644 --- a/src/WixToolset.Core/WixToolsetServiceProvider.cs +++ b/src/WixToolset.Core/WixToolsetServiceProvider.cs @@ -25,6 +25,7 @@ namespace WixToolset.Core { typeof(IWindowsInstallerBackendHelper), (provider, singletons) => AddSingleton(singletons, typeof(IWindowsInstallerBackendHelper), new WindowsInstallerBackendHelper(provider)) }, // Transients. + { typeof(ICommandLineArguments), (provider, singletons) => new CommandLineArguments(provider) }, { typeof(ICommandLineContext), (provider, singletons) => new CommandLineContext(provider) }, { typeof(ICommandLine), (provider, singletons) => new CommandLineParser() }, { typeof(IPreprocessContext), (provider, singletons) => new PreprocessContext(provider) }, -- cgit v1.2.3-55-g6feb