aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core/CommandLine/CommandLineParser.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core/CommandLine/CommandLineParser.cs')
-rw-r--r--src/WixToolset.Core/CommandLine/CommandLineParser.cs514
1 files changed, 173 insertions, 341 deletions
diff --git a/src/WixToolset.Core/CommandLine/CommandLineParser.cs b/src/WixToolset.Core/CommandLine/CommandLineParser.cs
index d0484e45..11e5751d 100644
--- a/src/WixToolset.Core/CommandLine/CommandLineParser.cs
+++ b/src/WixToolset.Core/CommandLine/CommandLineParser.cs
@@ -1,4 +1,4 @@
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. 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 2
3namespace WixToolset.Core.CommandLine 3namespace WixToolset.Core.CommandLine
4{ 4{
@@ -6,437 +6,269 @@ namespace WixToolset.Core.CommandLine
6 using System.Collections.Generic; 6 using System.Collections.Generic;
7 using System.IO; 7 using System.IO;
8 using WixToolset.Data; 8 using WixToolset.Data;
9 using WixToolset.Extensibility;
10 using WixToolset.Extensibility.Data;
11 using WixToolset.Extensibility.Services; 9 using WixToolset.Extensibility.Services;
12 10
13 internal enum Commands
14 {
15 Unknown,
16 Build,
17 Preprocess,
18 Compile,
19 Link,
20 Bind,
21 }
22
23 internal class CommandLineParser : ICommandLineParser 11 internal class CommandLineParser : ICommandLineParser
24 { 12 {
25 private static readonly char[] BindPathSplit = { '=' }; 13 private const string ExpectedArgument = "expected argument";
26
27 public CommandLineParser(IServiceProvider serviceProvider)
28 {
29 this.ServiceProvider = serviceProvider;
30
31 this.Messaging = this.ServiceProvider.GetService<IMessaging>();
32 }
33 14
34 private IServiceProvider ServiceProvider { get; } 15 public string ErrorArgument { get; set; }
35 16
36 private IMessaging Messaging { get; set; } 17 private Queue<string> RemainingArguments { get; }
37 18
38 public IExtensionManager ExtensionManager { get; set; } 19 private IMessaging Messaging { get; }
39 20
40 public ICommandLineArguments Arguments { get; set; } 21 public CommandLineParser(IMessaging messaging, string[] arguments, string errorArgument)
41 22 {
42 public static string ExpectedArgument { get; } = "expected argument"; 23 this.Messaging = messaging;
43 24 this.RemainingArguments = new Queue<string>(arguments);
44 public string ActiveCommand { get; private set; } 25 this.ErrorArgument = errorArgument;
26 }
45 27
46 public bool ShowHelp { get; private set; } 28 public bool IsSwitch(string arg)
29 {
30 return !String.IsNullOrEmpty(arg) && ('/' == arg[0] || '-' == arg[0]);
31 }
47 32
48 public ICommandLineCommand ParseStandardCommandLine() 33 public string GetArgumentAsFilePathOrError(string argument, string fileType)
49 { 34 {
50 var context = this.ServiceProvider.GetService<ICommandLineContext>(); 35 if (!File.Exists(argument))
51 context.ExtensionManager = this.ExtensionManager ?? this.ServiceProvider.GetService<IExtensionManager>();
52 context.Arguments = this.Arguments;
53
54 var next = String.Empty;
55
56 var command = Commands.Unknown;
57 var showLogo = true;
58 var showVersion = false;
59 var outputFolder = String.Empty;
60 var outputFile = String.Empty;
61 var outputType = String.Empty;
62 var platformType = String.Empty;
63 var verbose = false;
64 var files = new List<string>();
65 var defines = new List<string>();
66 var includePaths = new List<string>();
67 var locFiles = new List<string>();
68 var libraryFiles = new List<string>();
69 var suppressedWarnings = new List<int>();
70
71 var bindFiles = false;
72 var bindPaths = new List<string>();
73
74 var intermediateFolder = String.Empty;
75
76 var cabCachePath = String.Empty;
77 var cultures = new List<string>();
78 var contentsFile = String.Empty;
79 var outputsFile = String.Empty;
80 var builtOutputsFile = String.Empty;
81
82 this.Parse(context, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, parser, arg) =>
83 { 36 {
84 if (parser.IsSwitch(arg)) 37 this.Messaging.Write(ErrorMessages.FileNotFound(null, argument, fileType));
85 { 38 return null;
86 var parameter = arg.Substring(1); 39 }
87 switch (parameter.ToLowerInvariant())
88 {
89 case "?":
90 case "h":
91 case "help":
92 cmdline.ShowHelp = true;
93 return true;
94
95 case "arch":
96 case "platform":
97 platformType = parser.GetNextArgumentOrError(arg);
98 return true;
99
100 case "bindfiles":
101 bindFiles = true;
102 return true;
103
104 case "bindpath":
105 parser.GetNextArgumentOrError(arg, bindPaths);
106 return true;
107
108 case "cc":
109 cabCachePath = parser.GetNextArgumentOrError(arg);
110 return true;
111
112 case "culture":
113 parser.GetNextArgumentOrError(arg, cultures);
114 return true;
115 case "contentsfile":
116 contentsFile = parser.GetNextArgumentAsFilePathOrError(arg);
117 return true;
118 case "outputsfile":
119 outputsFile = parser.GetNextArgumentAsFilePathOrError(arg);
120 return true;
121 case "builtoutputsfile":
122 builtOutputsFile = parser.GetNextArgumentAsFilePathOrError(arg);
123 return true;
124
125 case "d":
126 case "define":
127 parser.GetNextArgumentOrError(arg, defines);
128 return true;
129
130 case "i":
131 case "includepath":
132 parser.GetNextArgumentOrError(arg, includePaths);
133 return true;
134
135 case "intermediatefolder":
136 intermediateFolder = parser.GetNextArgumentAsDirectoryOrError(arg);
137 return true;
138
139 case "loc":
140 parser.GetNextArgumentAsFilePathOrError(arg, "localization files", locFiles);
141 return true;
142
143 case "lib":
144 parser.GetNextArgumentAsFilePathOrError(arg, "library files", libraryFiles);
145 return true;
146
147 case "o":
148 case "out":
149 outputFile = parser.GetNextArgumentAsFilePathOrError(arg);
150 return true;
151
152 case "outputtype":
153 outputType = parser.GetNextArgumentOrError(arg);
154 return true;
155
156 case "nologo":
157 showLogo = false;
158 return true;
159
160 case "v":
161 case "verbose":
162 verbose = true;
163 return true;
164
165 case "version":
166 case "-version":
167 showVersion = true;
168 return true;
169
170 case "sval":
171 // todo: implement
172 return true;
173
174 case "sw":
175 case "suppresswarning":
176 var warning = parser.GetNextArgumentOrError(arg);
177 if (!String.IsNullOrEmpty(warning))
178 {
179 var warningNumber = Convert.ToInt32(warning);
180 this.Messaging.SuppressWarningMessage(warningNumber);
181 }
182 return true;
183 }
184
185 return false;
186 }
187 else
188 {
189 parser.GetArgumentAsFilePathOrError(arg, "source code", files);
190 return true;
191 }
192 });
193 40
194 this.Messaging.ShowVerboseMessages = verbose; 41 return argument;
42 }
195 43
196 if (showVersion) 44 public void GetArgumentAsFilePathOrError(string argument, string fileType, IList<string> paths)
45 {
46 foreach (var path in this.GetFiles(argument, fileType))
197 { 47 {
198 return new VersionCommand(); 48 paths.Add(path);
199 } 49 }
50 }
200 51
201 if (showLogo) 52 public string GetNextArgumentOrError(string commandLineSwitch)
53 {
54 if (this.TryGetNextNonSwitchArgumentOrError(out var argument))
202 { 55 {
203 AppCommon.DisplayToolHeader(); 56 return argument;
204 } 57 }
205 58
206 if (this.ShowHelp) 59 this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
207 { 60 return null;
208 return new HelpCommand(command); 61 }
209 }
210 62
211 switch (command) 63 public bool GetNextArgumentOrError(string commandLineSwitch, IList<string> args)
212 { 64 {
213 case Commands.Build: 65 if (this.TryGetNextNonSwitchArgumentOrError(out var arg))
214 { 66 {
215 var sourceFiles = GatherSourceFiles(files, outputFolder); 67 args.Add(arg);
216 var variables = this.GatherPreprocessorVariables(defines); 68 return true;
217 var bindPathList = this.GatherBindPaths(bindPaths);
218 var filterCultures = CalculateFilterCultures(cultures);
219 var type = CalculateOutputType(outputType, outputFile);
220 var platform = CalculatePlatform(platformType);
221 return new BuildCommand(this.ServiceProvider, sourceFiles, variables, locFiles, libraryFiles, filterCultures, outputFile, type, platform, cabCachePath, bindFiles, bindPathList, includePaths, intermediateFolder, contentsFile, outputsFile, builtOutputsFile);
222 } 69 }
223 70
224 case Commands.Compile: 71 this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
72 return false;
73 }
74
75 public string GetNextArgumentAsDirectoryOrError(string commandLineSwitch)
76 {
77 if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && this.TryGetDirectory(commandLineSwitch, this.Messaging, arg, out var directory))
225 { 78 {
226 var sourceFiles = GatherSourceFiles(files, outputFolder); 79 return directory;
227 var variables = this.GatherPreprocessorVariables(defines);
228 var platform = CalculatePlatform(platformType);
229 return new CompileCommand(this.ServiceProvider, sourceFiles, variables, platform);
230 }
231 } 80 }
232 81
82 this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
233 return null; 83 return null;
234 } 84 }
235 85
236 private static IEnumerable<string> CalculateFilterCultures(List<string> cultures) 86 public bool GetNextArgumentAsDirectoryOrError(string commandLineSwitch, IList<string> directories)
237 { 87 {
238 var result = new List<string>(); 88 if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && this.TryGetDirectory(commandLineSwitch, this.Messaging, arg, out var directory))
239
240 if (cultures == null)
241 {
242 }
243 else if (cultures.Count == 1 && cultures[0].Equals("null", StringComparison.OrdinalIgnoreCase))
244 { 89 {
245 // When null is used treat it as if cultures wasn't specified. This is 90 directories.Add(directory);
246 // needed for batching in the MSBuild task since MSBuild doesn't support 91 return true;
247 // empty items.
248 }
249 else
250 {
251 foreach (var culture in cultures)
252 {
253 // Neutral is different from null. For neutral we still want to do culture filtering.
254 // Set the culture to the empty string = identifier for the invariant culture.
255 var filter = (culture.Equals("neutral", StringComparison.OrdinalIgnoreCase)) ? String.Empty : culture;
256 result.Add(filter);
257 }
258 } 92 }
259 93
260 return result; 94 this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
95 return false;
261 } 96 }
262 97
263 private static OutputType CalculateOutputType(string outputType, string outputFile) 98 public string GetNextArgumentAsFilePathOrError(string commandLineSwitch)
264 { 99 {
265 if (String.IsNullOrEmpty(outputType)) 100 if (this.TryGetNextNonSwitchArgumentOrError(out var arg) && this.TryGetFile(commandLineSwitch, arg, out var path))
266 { 101 {
267 outputType = Path.GetExtension(outputFile); 102 return path;
268 } 103 }
269 104
270 switch (outputType.ToLowerInvariant()) 105 this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
271 { 106 return null;
272 case "bundle": 107 }
273 case ".exe":
274 return OutputType.Bundle;
275
276 case "library":
277 case ".wixlib":
278 return OutputType.Library;
279
280 case "module":
281 case ".msm":
282 return OutputType.Module;
283
284 case "patch":
285 case ".msp":
286 return OutputType.Patch;
287
288 case ".pcp":
289 return OutputType.PatchCreation;
290
291 case "product":
292 case "package":
293 case ".msi":
294 return OutputType.Product;
295 108
296 case "transform": 109 public bool GetNextArgumentAsFilePathOrError(string commandLineSwitch, string fileType, IList<string> paths)
297 case ".mst": 110 {
298 return OutputType.Transform; 111 if (this.TryGetNextNonSwitchArgumentOrError(out var arg))
112 {
113 foreach (var path in this.GetFiles(arg, fileType))
114 {
115 paths.Add(path);
116 }
299 117
300 case "intermediatepostlink": 118 return true;
301 case ".wixipl":
302 return OutputType.IntermediatePostLink;
303 } 119 }
304 120
305 return OutputType.Unknown; 121 this.Messaging.Write(ErrorMessages.ExpectedArgument(commandLineSwitch));
122 return false;
306 } 123 }
307 124
308 private static Platform CalculatePlatform(string platformType) 125 public bool TryGetNextSwitchOrArgument(out string arg)
309 { 126 {
310 return Enum.TryParse(platformType, true, out Platform platform) ? platform : Platform.X86; 127 return TryDequeue(this.RemainingArguments, out arg);
311 } 128 }
312 129
313 private ICommandLineParser Parse(ICommandLineContext context, Func<CommandLineParser, string, bool> parseCommand, Func<CommandLineParser, IParseCommandLine, string, bool> parseArgument) 130 private bool TryGetNextNonSwitchArgumentOrError(out string arg)
314 { 131 {
315 var extensions = this.ExtensionManager.Create<IExtensionCommandLine>(); 132 var result = this.TryGetNextSwitchOrArgument(out arg);
316 133
317 foreach (var extension in extensions) 134 if (!result && !this.IsSwitch(arg))
318 { 135 {
319 extension.PreParse(context); 136 this.ErrorArgument = arg ?? CommandLineParser.ExpectedArgument;
320 } 137 }
321 138
322 var parser = context.Arguments.Parse(); 139 return result;
323 140 }
324 while (!this.ShowHelp &&
325 String.IsNullOrEmpty(parser.ErrorArgument) &&
326 parser.TryGetNextSwitchOrArgument(out var arg))
327 {
328 if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments.
329 {
330 continue;
331 }
332 141
333 if (parser.IsSwitch(arg)) 142 private static bool IsValidArg(string arg)
334 { 143 {
335 if (!parseArgument(this, parser, arg) && 144 return !(String.IsNullOrEmpty(arg) || '/' == arg[0] || '-' == arg[0]);
336 !this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions)) 145 }
337 {
338 parser.ErrorArgument = arg;
339 }
340 }
341 else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported.
342 {
343 if (parseCommand(this, arg))
344 {
345 this.ActiveCommand = arg;
346 }
347 else
348 {
349 parser.ErrorArgument = arg;
350 }
351 }
352 else if (!this.TryParseCommandLineArgumentWithExtension(arg, parser, extensions) &&
353 !parseArgument(this, parser, arg))
354 {
355 parser.ErrorArgument = arg;
356 }
357 }
358 146
359 foreach (var extension in extensions) 147 private static bool TryDequeue(Queue<string> q, out string arg)
148 {
149 if (q.Count > 0)
360 { 150 {
361 extension.PostParse(); 151 arg = q.Dequeue();
152 return true;
362 } 153 }
363 154
364 return this; 155 arg = null;
156 return false;
365 } 157 }
366 158
367 private static IEnumerable<SourceFile> GatherSourceFiles(IEnumerable<string> sourceFiles, string intermediateDirectory) 159 private bool TryGetDirectory(string commandlineSwitch, IMessaging messageHandler, string arg, out string directory)
368 { 160 {
369 var files = new List<SourceFile>(); 161 directory = null;
370 162
371 foreach (var item in sourceFiles) 163 if (File.Exists(arg))
372 { 164 {
373 var sourcePath = item; 165 this.Messaging.Write(ErrorMessages.ExpectedDirectoryGotFile(commandlineSwitch, arg));
374 var outputPath = Path.Combine(intermediateDirectory, Path.GetFileNameWithoutExtension(sourcePath) + ".wir"); 166 return false;
375
376 files.Add(new SourceFile(sourcePath, outputPath));
377 } 167 }
378 168
379 return files; 169 directory = this.VerifyPath(arg);
170 return directory != null;
380 } 171 }
381 172
382 private IDictionary<string, string> GatherPreprocessorVariables(IEnumerable<string> defineConstants) 173 private bool TryGetFile(string commandlineSwitch, string arg, out string path)
383 { 174 {
384 var variables = new Dictionary<string, string>(); 175 path = null;
385 176
386 foreach (var pair in defineConstants) 177 if (!IsValidArg(arg))
387 { 178 {
388 var value = pair.Split(new[] { '=' }, 2); 179 this.Messaging.Write(ErrorMessages.FilePathRequired(commandlineSwitch));
389 180 }
390 if (variables.ContainsKey(value[0])) 181 else if (Directory.Exists(arg))
391 { 182 {
392 this.Messaging.Write(ErrorMessages.DuplicateVariableDefinition(value[0], (1 == value.Length) ? String.Empty : value[1], variables[value[0]])); 183 this.Messaging.Write(ErrorMessages.ExpectedFileGotDirectory(commandlineSwitch, arg));
393 continue; 184 }
394 } 185 else
395 186 {
396 variables.Add(value[0], (1 == value.Length) ? String.Empty : value[1]); 187 path = this.VerifyPath(arg);
397 } 188 }
398 189
399 return variables; 190 return path != null;
400 } 191 }
401 192
402 private IEnumerable<BindPath> GatherBindPaths(IEnumerable<string> bindPaths) 193 /// <summary>
194 /// Get a set of files that possibly have a search pattern in the path (such as '*').
195 /// </summary>
196 /// <param name="searchPath">Search path to find files in.</param>
197 /// <param name="fileType">Type of file; typically "Source".</param>
198 /// <returns>An array of files matching the search path.</returns>
199 /// <remarks>
200 /// This method is written in this verbose way because it needs to support ".." in the path.
201 /// It needs the directory path isolated from the file name in order to use Directory.GetFiles
202 /// or DirectoryInfo.GetFiles. The only way to get this directory path is manually since
203 /// Path.GetDirectoryName does not support ".." in the path.
204 /// </remarks>
205 /// <exception cref="WixFileNotFoundException">Throws WixFileNotFoundException if no file matching the pattern can be found.</exception>
206 private string[] GetFiles(string searchPath, string fileType)
403 { 207 {
404 var result = new List<BindPath>(); 208 if (null == searchPath)
405
406 foreach (var bindPath in bindPaths)
407 { 209 {
408 var bp = ParseBindPath(bindPath); 210 throw new ArgumentNullException(nameof(searchPath));
211 }
409 212
410 if (File.Exists(bp.Path)) 213 // Convert alternate directory separators to the standard one.
214 var filePath = searchPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
215 var lastSeparator = filePath.LastIndexOf(Path.DirectorySeparatorChar);
216 var files = new string[0];
217
218 try
219 {
220 if (0 > lastSeparator)
411 { 221 {
412 this.Messaging.Write(ErrorMessages.ExpectedDirectoryGotFile("-bindpath", bp.Path)); 222 files = Directory.GetFiles(".", filePath);
413 } 223 }
414 else 224 else // found directory separator
415 { 225 {
416 result.Add(bp); 226 files = Directory.GetFiles(filePath.Substring(0, lastSeparator + 1), filePath.Substring(lastSeparator + 1));
417 } 227 }
418 } 228 }
229 catch (DirectoryNotFoundException)
230 {
231 // Don't let this function throw the DirectoryNotFoundException. This exception
232 // occurs for non-existant directories and invalid characters in the searchPattern.
233 }
234 catch (ArgumentException)
235 {
236 // Don't let this function throw the ArgumentException. This exception
237 // occurs in certain situations such as when passing a malformed UNC path.
238 }
239 catch (IOException)
240 {
241 }
419 242
420 return result; 243 if (0 == files.Length)
244 {
245 this.Messaging.Write(ErrorMessages.FileNotFound(null, searchPath, fileType));
246 }
247
248 return files;
421 } 249 }
422 250
423 private bool TryParseCommandLineArgumentWithExtension(string arg, IParseCommandLine parse, IEnumerable<IExtensionCommandLine> extensions) 251 private string VerifyPath(string path)
424 { 252 {
425 foreach (var extension in extensions) 253 string fullPath;
254
255 if (0 <= path.IndexOf('\"'))
426 { 256 {
427 if (extension.TryParseArgument(parse, arg)) 257 this.Messaging.Write(ErrorMessages.PathCannotContainQuote(path));
428 { 258 return null;
429 return true;
430 }
431 } 259 }
432 260
433 return false; 261 try
434 } 262 {
263 fullPath = Path.GetFullPath(path);
264 }
265 catch (Exception e)
266 {
267 this.Messaging.Write(ErrorMessages.InvalidCommandLineFileName(path, e.Message));
268 return null;
269 }
435 270
436 public static BindPath ParseBindPath(string bindPath) 271 return fullPath;
437 {
438 var namedPath = bindPath.Split(BindPathSplit, 2);
439 return (1 == namedPath.Length) ? new BindPath(namedPath[0]) : new BindPath(namedPath[0], namedPath[1]);
440 } 272 }
441 } 273 }
442} 274}