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 +++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 src/WixToolset.Core/CommandLine/CommandLineArguments.cs (limited to 'src/WixToolset.Core/CommandLine/CommandLineArguments.cs') 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]); + } + } +} -- cgit v1.2.3-55-g6feb