using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; namespace ServiceClientGenerator { /// /// Parses the command line into argument settings controlling the /// sdk code generator. /// internal class CommandArguments { /// /// Takes the command line arguments, fuses them with any response file that may also have /// been specified, and parses them. /// /// The set of arguments supplied to the program /// public static CommandArguments Parse(string[] cmdlineArgs) { var arguments = new CommandArguments(); arguments.Process(cmdlineArgs); return arguments; } /// /// Set if an error is encountered whilst parsing arguments. /// public string Error { get; private set; } /// /// /// The collection of options parsed from the command line. /// These arguments exist on the command line as individual entries /// prefixed with '-' or '/'. Options can be set in a response file /// and indicated with a '@' prefix, in which case the contents /// of the file will be read and inserted into the same relative /// location in the arguments to parse (allowing for later /// arguments to override). /// /// /// Options not specified on the command line are set from internal /// defaults. /// /// public GeneratorOptions ParsedOptions { get; private set; } private CommandArguments() { ParsedOptions = new GeneratorOptions(); } private void Process(IEnumerable cmdlineArgs) { // walk the supplied command line looking for any response file(s), indicated using // @filename, and fuse into one logical set of arguments we can parse var argsToParse = new List(); foreach (var a in cmdlineArgs) { if (a.StartsWith("@", StringComparison.OrdinalIgnoreCase)) AddResponseFileArguments(a.Substring(1), argsToParse); else argsToParse.Add(a); } if (string.IsNullOrEmpty(Error)) { for (var argIndex = 0; argIndex < argsToParse.Count; argIndex++) { if (!IsSwitch(argsToParse[argIndex])) continue; var argDeclaration = FindArgDeclaration(argsToParse[argIndex]); if (argDeclaration != null) { if (argDeclaration.HasValue) argIndex++; if (argIndex < argsToParse.Count) argDeclaration.Parse(this, argsToParse[argIndex]); else Error = "Expected value for argument: " + argDeclaration.OptionName; } else Error = "Unrecognised argument: " + argsToParse[argIndex]; if (!string.IsNullOrEmpty(Error)) break; } } } private void AddResponseFileArguments(string responseFile, ICollection args) { try { // Response file format is one argument (plus optional value) // per line. Comments can be used by putting # as the first char. using (var s = new StreamReader(ResolvePath(responseFile))) { var line = s.ReadLine(); while (line != null) { if (line.Length != 0 && line[0] != '#') { // trying to be flexible here and allow for lines with or without keyword // prefix in the response file var keywordEnd = line.IndexOf(' '); var keyword = keywordEnd > 0 ? line.Substring(0, keywordEnd) : line; if (ArgumentPrefixes.Any(prefix => keyword.StartsWith(prefix.ToString(CultureInfo.InvariantCulture)))) args.Add(keyword); else args.Add(ArgumentPrefixes[0] + keyword); if (keywordEnd > 0) { keywordEnd++; if (keywordEnd < line.Length) { var value = line.Substring(keywordEnd).Trim(' '); if (!string.IsNullOrEmpty(value)) args.Add(value); } } } line = s.ReadLine(); } } } catch (Exception e) { Error = string.Format("Caught exception processing response file {0} - {1}", responseFile, e.Message); } } delegate void ParseArgument(CommandArguments arguments, string argValue = null); class ArgDeclaration { public string OptionName { get; set; } public string ShortName { get; set; } public bool HasValue { get; set; } public ParseArgument Parse { get; set; } public string HelpText { get; set; } public string HelpExample { get; set; } } static readonly ArgDeclaration[] ArgumentsTable = { new ArgDeclaration { OptionName = "verbose", ShortName = "v", Parse = (arguments, argValue) => arguments.ParsedOptions.Verbose = true, HelpText = "Enable verbose output." }, new ArgDeclaration { OptionName = "waitonexit", ShortName = "w", Parse = (arguments, argValue) => arguments.ParsedOptions.WaitOnExit = true, HelpText = "Pauses waiting for a keypress after code generation completes." }, new ArgDeclaration { OptionName = "manifest", ShortName = "m", HasValue = true, Parse = (arguments, argValue) => arguments.ParsedOptions.Manifest = argValue, HelpText = "The name and location of the control manifest listing all supported services for generation." }, new ArgDeclaration { OptionName = "versions", ShortName = "vs", HasValue = true, Parse = (arguments, argValue) => arguments.ParsedOptions.Versions = argValue, HelpText = "The name and location of the manifest listing versions of all supported services." }, new ArgDeclaration { OptionName = "modelsfolder", ShortName = "mf", HasValue = true, Parse = (arguments, argValue) => arguments.ParsedOptions.ModelsFolder = argValue, HelpText = "The location of the folder containing the service model files." }, new ArgDeclaration { OptionName = "sdkroot", ShortName = "sdk", HasValue = true, Parse = (arguments, argValue) => arguments.ParsedOptions.SdkRootFolder = argValue, HelpText = "The root folder beneath which the source and test code for the SDK is arranged." }, new ArgDeclaration { OptionName = "servicemodels", ShortName = "sm", HasValue = true, Parse = (arguments, argValue) => arguments.ParsedOptions.ServiceModels = argValue, HelpText = "Collection of one or more service model identifiers, separated by the ';' character.\n" + "If specified only these service(s) will be generated. The values specified are matched with\n" + "the 'model' entry values in the service manifest file." }, new ArgDeclaration { OptionName = "skipremoveorphanscleanup", ShortName = "sro", Parse = (arguments, argValue) => arguments.ParsedOptions.SkipRemoveOrphanCleanup = true, HelpText = "Allows disabling orphaned shape and service cleanup. Useful to set if you \n" + "a) specify ServiceModels and b) don't want to remove previously generated services." + "This is often the case in a development environment." }, new ArgDeclaration { OptionName = "clean", ShortName = "c", Parse = (arguments, argValue) => arguments.ParsedOptions.Clean = true, HelpText = "Deletes all content in the 'Generated' subfolder for services prior to generation.\n" + "The default behavior is to keep existing generated content." }, new ArgDeclaration { OptionName = "createvsix", ShortName = "cvx", Parse = (arguments, argValue) => arguments.ParsedOptions.CreateCodeAnalysisVsixAssets = true, HelpText = "Creates a VSIX project and manifest for code analysis" }, new ArgDeclaration { OptionName = "self.modelpath", Parse = (arguments, argValue) => arguments.ParsedOptions.SelfServiceModel = argValue, HasValue = true, HelpText = "Path to service model for self service." }, new ArgDeclaration { OptionName = "self.basename", Parse = (arguments, argValue) => arguments.ParsedOptions.SelfServiceBaseName = argValue, HasValue = true, HelpText = "Self Service base name used for namespace and client name." }, new ArgDeclaration { OptionName = "self.endpoint-prefix", Parse = (arguments, argValue) => arguments.ParsedOptions.SelfServiceEndpointPrefix = argValue, HasValue = true, HelpText = "Endpoint prefix for self service." }, new ArgDeclaration { OptionName = "self.sig-v4-service-name", Parse = (arguments, argValue) => arguments.ParsedOptions.SelfServiceSigV4Name = argValue, HasValue = true, HelpText = "Sig V4 service signing name." } }; static readonly char[] ArgumentPrefixes = { '-', '/' }; /// /// Returns true if the supplied value has a argument prefix indicator, marking it as /// an argumentkeyword. /// /// /// static bool IsSwitch(string argument) { return ArgumentPrefixes.Any(c => argument.StartsWith(c.ToString(CultureInfo.InvariantCulture))); } /// /// Scans the argument declaration table to find a matching entry for a token from the command line /// that is potentially an option keyword. /// /// Keyword found on the command line. Any prefixes will be removed before attempting to match. /// Matching declaration or null if keyword not recognised static ArgDeclaration FindArgDeclaration(string argument) { var keyword = argument.TrimStart(ArgumentPrefixes); return ArgumentsTable.FirstOrDefault(argDeclation => keyword.Equals(argDeclation.ShortName, StringComparison.OrdinalIgnoreCase) || keyword.Equals(argDeclation.OptionName, StringComparison.OrdinalIgnoreCase)); } /// /// Resolves any relatively pathed filename. /// /// /// static string ResolvePath(string filePath) { if (Path.IsPathRooted(filePath)) return filePath; return Path.GetFullPath(filePath); } } }