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.cs628
1 files changed, 628 insertions, 0 deletions
diff --git a/src/WixToolset.Core/CommandLine/CommandLineParser.cs b/src/WixToolset.Core/CommandLine/CommandLineParser.cs
new file mode 100644
index 00000000..0e7da42a
--- /dev/null
+++ b/src/WixToolset.Core/CommandLine/CommandLineParser.cs
@@ -0,0 +1,628 @@
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
3namespace WixToolset.Core.CommandLine
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Text;
10 using System.Text.RegularExpressions;
11 using WixToolset.Data;
12 using WixToolset.Extensibility;
13 using WixToolset.Extensibility.Services;
14
15 internal enum Commands
16 {
17 Unknown,
18 Build,
19 Preprocess,
20 Compile,
21 Link,
22 Bind,
23 }
24
25 internal class CommandLineParser : ICommandLine, IParseCommandLine
26 {
27 private IServiceProvider ServiceProvider { get; set; }
28
29 private IMessaging Messaging { get; set; }
30
31 public static string ExpectedArgument { get; } = "expected argument";
32
33 public string ActiveCommand { get; private set; }
34
35 public string[] OriginalArguments { get; private set; }
36
37 public Queue<string> RemainingArguments { get; } = new Queue<string>();
38
39 public IExtensionManager ExtensionManager { get; private set; }
40
41 public string ErrorArgument { get; set; }
42
43 public bool ShowHelp { get; set; }
44
45 public ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context)
46 {
47 this.ServiceProvider = context.ServiceProvider;
48
49 this.Messaging = context.Messaging ?? this.ServiceProvider.GetService<IMessaging>();
50
51 this.ExtensionManager = context.ExtensionManager ?? this.ServiceProvider.GetService<IExtensionManager>();
52
53 var args = context.ParsedArguments ?? Array.Empty<string>();
54
55 if (!String.IsNullOrEmpty(context.Arguments))
56 {
57 args = CommandLineParser.ParseArgumentsToArray(context.Arguments).Union(args).ToArray();
58 }
59
60 return this.ParseStandardCommandLine(context, args);
61 }
62
63 private ICommandLineCommand ParseStandardCommandLine(ICommandLineContext context, string[] args)
64 {
65 var next = String.Empty;
66
67 var command = Commands.Unknown;
68 var showLogo = true;
69 var showVersion = false;
70 var outputFolder = String.Empty;
71 var outputFile = String.Empty;
72 var outputType = String.Empty;
73 var verbose = false;
74 var files = new List<string>();
75 var defines = new List<string>();
76 var includePaths = new List<string>();
77 var locFiles = new List<string>();
78 var libraryFiles = new List<string>();
79 var suppressedWarnings = new List<int>();
80
81 var bindFiles = false;
82 var bindPaths = new List<string>();
83
84 var intermediateFolder = String.Empty;
85
86 var cabCachePath = String.Empty;
87 var cultures = new List<string>();
88 var contentsFile = String.Empty;
89 var outputsFile = String.Empty;
90 var builtOutputsFile = String.Empty;
91
92 this.Parse(context, args, (cmdline, arg) => Enum.TryParse(arg, true, out command), (cmdline, arg) =>
93 {
94 if (cmdline.IsSwitch(arg))
95 {
96 var parameter = arg.Substring(1);
97 switch (parameter.ToLowerInvariant())
98 {
99 case "?":
100 case "h":
101 case "help":
102 cmdline.ShowHelp = true;
103 return true;
104
105 case "bindfiles":
106 bindFiles = true;
107 return true;
108
109 case "bindpath":
110 cmdline.GetNextArgumentOrError(bindPaths);
111 return true;
112
113 case "cc":
114 cmdline.GetNextArgumentOrError(ref cabCachePath);
115 return true;
116
117 case "cultures":
118 cmdline.GetNextArgumentOrError(cultures);
119 return true;
120 case "contentsfile":
121 cmdline.GetNextArgumentOrError(ref contentsFile);
122 return true;
123 case "outputsfile":
124 cmdline.GetNextArgumentOrError(ref outputsFile);
125 return true;
126 case "builtoutputsfile":
127 cmdline.GetNextArgumentOrError(ref builtOutputsFile);
128 return true;
129
130 case "d":
131 case "define":
132 cmdline.GetNextArgumentOrError(defines);
133 return true;
134
135 case "i":
136 case "includepath":
137 cmdline.GetNextArgumentOrError(includePaths);
138 return true;
139
140 case "intermediatefolder":
141 cmdline.GetNextArgumentOrError(ref intermediateFolder);
142 return true;
143
144 case "loc":
145 cmdline.GetNextArgumentAsFilePathOrError(locFiles, "localization files");
146 return true;
147
148 case "lib":
149 cmdline.GetNextArgumentAsFilePathOrError(libraryFiles, "library files");
150 return true;
151
152 case "o":
153 case "out":
154 cmdline.GetNextArgumentOrError(ref outputFile);
155 return true;
156
157 case "outputtype":
158 cmdline.GetNextArgumentOrError(ref outputType);
159 return true;
160
161 case "nologo":
162 showLogo = false;
163 return true;
164
165 case "v":
166 case "verbose":
167 verbose = true;
168 return true;
169
170 case "version":
171 case "-version":
172 showVersion = true;
173 return true;
174 }
175
176 return false;
177 }
178 else
179 {
180 files.AddRange(CommandLineHelper.GetFiles(arg, "source code"));
181 return true;
182 }
183 });
184
185 this.Messaging.ShowVerboseMessages = verbose;
186
187 if (showVersion)
188 {
189 return new VersionCommand();
190 }
191
192 if (showLogo)
193 {
194 AppCommon.DisplayToolHeader();
195 }
196
197 if (this.ShowHelp)
198 {
199 return new HelpCommand(command);
200 }
201
202 switch (command)
203 {
204 case Commands.Build:
205 {
206 var sourceFiles = GatherSourceFiles(files, outputFolder);
207 var variables = this.GatherPreprocessorVariables(defines);
208 var bindPathList = this.GatherBindPaths(bindPaths);
209 var type = CalculateOutputType(outputType, outputFile);
210 return new BuildCommand(this.ServiceProvider, this.Messaging, this.ExtensionManager, sourceFiles, variables, locFiles, libraryFiles, outputFile, type, cabCachePath, cultures, bindFiles, bindPathList, intermediateFolder, contentsFile, outputsFile, builtOutputsFile);
211 }
212
213 case Commands.Compile:
214 {
215 var sourceFiles = GatherSourceFiles(files, outputFolder);
216 var variables = GatherPreprocessorVariables(defines);
217 return new CompileCommand(this.ServiceProvider, this.Messaging, this.ExtensionManager, sourceFiles, variables);
218 }
219 }
220
221 return null;
222 }
223
224 private static OutputType CalculateOutputType(string outputType, string outputFile)
225 {
226 if (String.IsNullOrEmpty(outputType))
227 {
228 outputType = Path.GetExtension(outputFile);
229 }
230
231 switch (outputType.ToLowerInvariant())
232 {
233 case "bundle":
234 case ".exe":
235 return OutputType.Bundle;
236
237 case "library":
238 case ".wixlib":
239 return OutputType.Library;
240
241 case "module":
242 case ".msm":
243 return OutputType.Module;
244
245 case "patch":
246 case ".msp":
247 return OutputType.Patch;
248
249 case ".pcp":
250 return OutputType.PatchCreation;
251
252 case "product":
253 case ".msi":
254 return OutputType.Product;
255
256 case "transform":
257 case ".mst":
258 return OutputType.Transform;
259
260 case "wixout":
261 case ".wixout":
262 return OutputType.Wixout;
263 }
264
265 return OutputType.Unknown;
266 }
267
268#if UNUSED
269 private static CommandLine Parse(string commandLineString, Func<CommandLine, string, bool> parseArgument)
270 {
271 var arguments = CommandLine.ParseArgumentsToArray(commandLineString).ToArray();
272
273 return CommandLine.Parse(arguments, null, parseArgument);
274 }
275
276 private static CommandLine Parse(string[] commandLineArguments, Func<CommandLine, string, bool> parseArgument)
277 {
278 return CommandLine.Parse(commandLineArguments, null, parseArgument);
279 }
280#endif
281
282 private ICommandLine Parse(ICommandLineContext context, string[] commandLineArguments, Func<CommandLineParser, string, bool> parseCommand, Func<CommandLineParser, string, bool> parseArgument)
283 {
284 this.FlattenArgumentsWithResponseFilesIntoOriginalArguments(commandLineArguments);
285
286 this.QueueArgumentsAndLoadExtensions(this.OriginalArguments);
287
288 this.ProcessRemainingArguments(context, parseArgument, parseCommand);
289
290 return this;
291 }
292
293 private static IEnumerable<SourceFile> GatherSourceFiles(IEnumerable<string> sourceFiles, string intermediateDirectory)
294 {
295 var files = new List<SourceFile>();
296
297 foreach (var item in sourceFiles)
298 {
299 var sourcePath = item;
300 var outputPath = Path.Combine(intermediateDirectory, Path.GetFileNameWithoutExtension(sourcePath) + ".wir");
301
302 files.Add(new SourceFile(sourcePath, outputPath));
303 }
304
305 return files;
306 }
307
308 private IDictionary<string, string> GatherPreprocessorVariables(IEnumerable<string> defineConstants)
309 {
310 var variables = new Dictionary<string, string>();
311
312 foreach (var pair in defineConstants)
313 {
314 string[] value = pair.Split(new[] { '=' }, 2);
315
316 if (variables.ContainsKey(value[0]))
317 {
318 this.Messaging.Write(ErrorMessages.DuplicateVariableDefinition(value[0], (1 == value.Length) ? String.Empty : value[1], variables[value[0]]));
319 continue;
320 }
321
322 variables.Add(value[0], (1 == value.Length) ? String.Empty : value[1]);
323 }
324
325 return variables;
326 }
327
328 private IEnumerable<BindPath> GatherBindPaths(IEnumerable<string> bindPaths)
329 {
330 var result = new List<BindPath>();
331
332 foreach (var bindPath in bindPaths)
333 {
334 var bp = BindPath.Parse(bindPath);
335
336 if (Directory.Exists(bp.Path))
337 {
338 result.Add(bp);
339 }
340 else if (File.Exists(bp.Path))
341 {
342 this.Messaging.Write(ErrorMessages.ExpectedDirectoryGotFile("-bindpath", bp.Path));
343 }
344 }
345
346 return result;
347 }
348
349 /// <summary>
350 /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity
351 /// </summary>
352 /// <param name="args">The list of strings to check.</param>
353 /// <param name="index">The index (in args) of the commandline parameter to be validated.</param>
354 /// <returns>True if a valid switch exists there, false if not.</returns>
355 public bool IsSwitch(string arg)
356 {
357 return arg != null && arg.Length > 1 && ('/' == arg[0] || '-' == arg[0]);
358 }
359
360 /// <summary>
361 /// Validates that a valid switch (starts with "/" or "-"), and returns a bool indicating its validity
362 /// </summary>
363 /// <param name="args">The list of strings to check.</param>
364 /// <param name="index">The index (in args) of the commandline parameter to be validated.</param>
365 /// <returns>True if a valid switch exists there, false if not.</returns>
366 public bool IsSwitchAt(IEnumerable<string> args, int index)
367 {
368 var arg = args.ElementAtOrDefault(index);
369 return IsSwitch(arg);
370 }
371
372 public void GetNextArgumentOrError(ref string arg)
373 {
374 this.TryGetNextArgumentOrError(out arg);
375 }
376
377 public void GetNextArgumentOrError(IList<string> args)
378 {
379 if (this.TryGetNextArgumentOrError(out var arg))
380 {
381 args.Add(arg);
382 }
383 }
384
385 public void GetNextArgumentAsFilePathOrError(IList<string> args, string fileType)
386 {
387 if (this.TryGetNextArgumentOrError(out var arg))
388 {
389 foreach (var path in CommandLineHelper.GetFiles(arg, fileType))
390 {
391 args.Add(path);
392 }
393 }
394 }
395
396 public bool TryGetNextArgumentOrError(out string arg)
397 {
398 if (TryDequeue(this.RemainingArguments, out arg) && !this.IsSwitch(arg))
399 {
400 return true;
401 }
402
403 this.ErrorArgument = arg ?? CommandLineParser.ExpectedArgument;
404
405 return false;
406 }
407
408 private static bool TryDequeue(Queue<string> q, out string arg)
409 {
410 if (q.Count > 0)
411 {
412 arg = q.Dequeue();
413 return true;
414 }
415
416 arg = null;
417 return false;
418 }
419
420 private void FlattenArgumentsWithResponseFilesIntoOriginalArguments(string[] commandLineArguments)
421 {
422 List<string> args = new List<string>();
423
424 foreach (var arg in commandLineArguments)
425 {
426 if ('@' == arg[0])
427 {
428 var responseFileArguments = CommandLineParser.ParseResponseFile(arg.Substring(1));
429 args.AddRange(responseFileArguments);
430 }
431 else
432 {
433 args.Add(arg);
434 }
435 }
436
437 this.OriginalArguments = args.ToArray();
438 }
439
440 private void QueueArgumentsAndLoadExtensions(string[] args)
441 {
442 for (var i = 0; i < args.Length; ++i)
443 {
444 var arg = args[i];
445
446 if ("-ext" == arg || "/ext" == arg)
447 {
448 if (!this.IsSwitchAt(args, ++i))
449 {
450 this.ExtensionManager.Load(args[i]);
451 }
452 else
453 {
454 this.ErrorArgument = arg;
455 break;
456 }
457 }
458 else
459 {
460 this.RemainingArguments.Enqueue(arg);
461 }
462 }
463 }
464
465 private void ProcessRemainingArguments(ICommandLineContext context, Func<CommandLineParser, string, bool> parseArgument, Func<CommandLineParser, string, bool> parseCommand)
466 {
467 var extensions = this.ExtensionManager.Create<IExtensionCommandLine>();
468
469 foreach (var extension in extensions)
470 {
471 extension.PreParse(context);
472 }
473
474 while (!this.ShowHelp &&
475 String.IsNullOrEmpty(this.ErrorArgument) &&
476 TryDequeue(this.RemainingArguments, out var arg))
477 {
478 if (String.IsNullOrWhiteSpace(arg)) // skip blank arguments.
479 {
480 continue;
481 }
482
483 if ('-' == arg[0] || '/' == arg[0])
484 {
485 if (!parseArgument(this, arg) &&
486 !this.TryParseCommandLineArgumentWithExtension(arg, extensions))
487 {
488 this.ErrorArgument = arg;
489 }
490 }
491 else if (String.IsNullOrEmpty(this.ActiveCommand) && parseCommand != null) // First non-switch must be the command, if commands are supported.
492 {
493 if (parseCommand(this, arg))
494 {
495 this.ActiveCommand = arg;
496 }
497 else
498 {
499 this.ErrorArgument = arg;
500 }
501 }
502 else if (!this.TryParseCommandLineArgumentWithExtension(arg, extensions) &&
503 !parseArgument(this, arg))
504 {
505 this.ErrorArgument = arg;
506 }
507 }
508 }
509
510 private bool TryParseCommandLineArgumentWithExtension(string arg, IEnumerable<IExtensionCommandLine> extensions)
511 {
512 foreach (var extension in extensions)
513 {
514 if (extension.TryParseArgument(this, arg))
515 {
516 return true;
517 }
518 }
519
520 return false;
521 }
522
523 private static List<string> ParseResponseFile(string responseFile)
524 {
525 string arguments;
526
527 using (StreamReader reader = new StreamReader(responseFile))
528 {
529 arguments = reader.ReadToEnd();
530 }
531
532 return CommandLineParser.ParseArgumentsToArray(arguments);
533 }
534
535 private static List<string> ParseArgumentsToArray(string arguments)
536 {
537 // Scan and parse the arguments string, dividing up the arguments based on whitespace.
538 // Unescaped quotes cause whitespace to be ignored, while the quotes themselves are removed.
539 // Quotes may begin and end inside arguments; they don't necessarily just surround whole arguments.
540 // Escaped quotes and escaped backslashes also need to be unescaped by this process.
541
542 // Collects the final list of arguments to be returned.
543 var argsList = new List<string>();
544
545 // True if we are inside an unescaped quote, meaning whitespace should be ignored.
546 var insideQuote = false;
547
548 // Index of the start of the current argument substring; either the start of the argument
549 // or the start of a quoted or unquoted sequence within it.
550 var partStart = 0;
551
552 // The current argument string being built; when completed it will be added to the list.
553 var arg = new StringBuilder();
554
555 for (int i = 0; i <= arguments.Length; i++)
556 {
557 if (i == arguments.Length || (Char.IsWhiteSpace(arguments[i]) && !insideQuote))
558 {
559 // Reached a whitespace separator or the end of the string.
560
561 // Finish building the current argument.
562 arg.Append(arguments.Substring(partStart, i - partStart));
563
564 // Skip over the whitespace character.
565 partStart = i + 1;
566
567 // Add the argument to the list if it's not empty.
568 if (arg.Length > 0)
569 {
570 argsList.Add(CommandLineParser.ExpandEnvironmentVariables(arg.ToString()));
571 arg.Length = 0;
572 }
573 }
574 else if (i > partStart && arguments[i - 1] == '\\')
575 {
576 // Check the character following an unprocessed backslash.
577 // Unescape quotes, and backslashes followed by a quote.
578 if (arguments[i] == '"' || (arguments[i] == '\\' && arguments.Length > i + 1 && arguments[i + 1] == '"'))
579 {
580 // Unescape the quote or backslash by skipping the preceeding backslash.
581 arg.Append(arguments.Substring(partStart, i - 1 - partStart));
582 arg.Append(arguments[i]);
583 partStart = i + 1;
584 }
585 }
586 else if (arguments[i] == '"')
587 {
588 // Add the quoted or unquoted section to the argument string.
589 arg.Append(arguments.Substring(partStart, i - partStart));
590
591 // And skip over the quote character.
592 partStart = i + 1;
593
594 insideQuote = !insideQuote;
595 }
596 }
597
598 return argsList;
599 }
600
601 private static string ExpandEnvironmentVariables(string arguments)
602 {
603 var id = Environment.GetEnvironmentVariables();
604
605 var regex = new Regex("(?<=\\%)(?:[\\w\\.]+)(?=\\%)");
606 MatchCollection matches = regex.Matches(arguments);
607
608 string value = String.Empty;
609 for (int i = 0; i <= (matches.Count - 1); i++)
610 {
611 try
612 {
613 var key = matches[i].Value;
614 regex = new Regex(String.Concat("(?i)(?:\\%)(?:", key, ")(?:\\%)"));
615 value = id[key].ToString();
616 arguments = regex.Replace(arguments, value);
617 }
618 catch (NullReferenceException)
619 {
620 // Collapse unresolved environment variables.
621 arguments = regex.Replace(arguments, value);
622 }
623 }
624
625 return arguments;
626 }
627 }
628}