main
cs 213 lines 7.59 KB
Raw
1 // 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.
2
3 namespace WixToolset.Core.CommandLine
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Text;
9 using System.Text.RegularExpressions;
10 using WixToolset.Extensibility.Data;
11 using WixToolset.Extensibility.Services;
12
13 internal class CommandLineArguments : ICommandLineArguments
14 {
15 public CommandLineArguments(IServiceProvider serviceProvider)
16 {
17 this.Messaging = serviceProvider.GetService<IMessaging>();
18 }
19
20 public string[] OriginalArguments { get; set; }
21
22 public string[] Arguments { get; set; }
23
24 public string[] Extensions { get; set; }
25
26 public string ErrorArgument { get; set; }
27
28 private IMessaging Messaging { get; }
29
30 public void Populate(string commandLine)
31 {
32 var args = CommandLineArguments.ParseArgumentsToArray(commandLine);
33
34 this.Populate(args.ToArray());
35 }
36
37 public void Populate(string[] args)
38 {
39 this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(args);
40
41 this.ProcessArgumentsAndParseExtensions(this.OriginalArguments);
42 }
43
44 public ICommandLineParser Parse()
45 {
46 return new CommandLineParser(this.Messaging, this.Arguments, this.ErrorArgument);
47 }
48
49 private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments)
50 {
51 var args = new List<string>();
52
53 foreach (var arg in commandLineArguments)
54 {
55 if (!String.IsNullOrEmpty(arg))
56 {
57 if ('@' == arg[0])
58 {
59 var responseFileArguments = CommandLineArguments.ParseResponseFile(arg.Substring(1));
60 args.AddRange(responseFileArguments);
61 }
62 else
63 {
64 args.Add(arg);
65 }
66 }
67 }
68
69 this.OriginalArguments = args.ToArray();
70 }
71
72 private void ProcessArgumentsAndParseExtensions(string[] args)
73 {
74 var arguments = new List<string>();
75 var extensions = new List<string>();
76
77 for (var i = 0; i < args.Length; ++i)
78 {
79 var arg = args[i];
80
81 if ("-ext" == arg || "/ext" == arg)
82 {
83 if (!CommandLineArguments.IsSwitchAt(args, ++i))
84 {
85 extensions.Add(args[i]);
86 }
87 else
88 {
89 this.ErrorArgument = arg;
90 break;
91 }
92 }
93 else
94 {
95 arguments.Add(arg);
96 }
97 }
98
99 this.Arguments = arguments.ToArray();
100 this.Extensions = extensions.ToArray();
101 }
102
103 private static List<string> ParseResponseFile(string responseFile)
104 {
105 string arguments;
106
107 using (var reader = new StreamReader(responseFile))
108 {
109 arguments = reader.ReadToEnd();
110 }
111
112 return CommandLineArguments.ParseArgumentsToArray(arguments);
113 }
114
115 private static List<string> ParseArgumentsToArray(string arguments)
116 {
117 // Scan and parse the arguments string, dividing up the arguments based on whitespace.
118 // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed.
119 // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments.
120 // Escaped quotes and escaped backslashes also need to be unescaped by this process.
121
122 // Collects the final list of arguments to be returned.
123 var argsList = new List<string>();
124
125 // True if we are inside an unescaped quote, meaning whitespace should be ignored.
126 var insideQuote = false;
127
128 // Index of the start of the current argument substring; either the start of the argument
129 // or the start of a quoted or unquoted sequence within it.
130 var partStart = 0;
131
132 // The current argument string being built; when completed it will be added to the list.
133 var arg = new StringBuilder();
134
135 for (var i = 0; i <= arguments.Length; i++)
136 {
137 if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote))
138 {
139 // Reached a whitespace separator or the end of the string.
140
141 // Finish building the current argument.
142 arg.Append(arguments.Substring(partStart, i - partStart));
143
144 // Skip over the whitespace character.
145 partStart = i + 1;
146
147 // Add the argument to the list if it's not empty.
148 if (arg.Length > 0)
149 {
150 argsList.Add(CommandLineArguments.ExpandEnvironmentVariables(arg.ToString()));
151 arg.Length = 0;
152 }
153 }
154 else if (i > partStart && arguments[i - 1] == '\\')
155 {
156 // Check the character following an unprocessed backslash.
157 // Unescape quotes, and backslashes followed by a quote.
158 if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"'))
159 {
160 // Unescape the quote or backslash by skipping the preceeding backslash.
161 arg.Append(arguments.Substring(partStart, i - 1 - partStart));
162 arg.Append(arguments[i]);
163 partStart = i + 1;
164 }
165 }
166 else if (arguments[i] == '"')
167 {
168 // Add the quoted or unquoted section to the argument string.
169 arg.Append(arguments.Substring(partStart, i - partStart));
170
171 // And skip over the quote character.
172 partStart = i + 1;
173
174 insideQuote = !insideQuote;
175 }
176 }
177
178 return argsList;
179 }
180
181 private static string ExpandEnvironmentVariables(string arguments)
182 {
183 var id = Environment.GetEnvironmentVariables();
184
185 var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)");
186 var matches = regex.Matches(arguments);
187
188 var value = String.Empty;
189 for (var i = 0; i <= (matches.Count - 1); i++)
190 {
191 try
192 {
193 var key = matches[i].Value;
194 regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)"));
195 value = id[key].ToString();
196 arguments = regex.Replace(arguments, value);
197 }
198 catch (NullReferenceException)
199 {
200 // Collapse unresolved environment variables.
201 arguments = regex.Replace(arguments, value);
202 }
203 }
204
205 return arguments;
206 }
207
208 private static bool IsSwitchAt(string[] args, int index)
209 {
210 return args.Length > index && !String.IsNullOrEmpty(args[index]) && ('/' == args[index][0] || '-' == args[index][0]);
211 }
212 }
213 }