using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SDKDocGenerator
{
///
/// Parses the command line into argument settings controlling the documentation
/// 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 = "clean",
ShortName = "c",
Parse = (arguments, argValue) => arguments.ParsedOptions.Clean = true,
HelpText = "Deletes all content in the specified out folder prior to generation.\n"
+ "The default behavior is to keep existing generated content and overwrite only changed items."
},
new ArgDeclaration
{
OptionName = "testmode",
ShortName = "t",
Parse = (arguments, argValue) => arguments.ParsedOptions.TestMode = true,
HelpText = "If set, generates a subset of the documentation for a predefined set of assemblies. Use for testing generator code changes."
},
new ArgDeclaration
{
OptionName = "writestaticcontent",
ShortName = "wsc",
Parse = (arguments, argValue) => arguments.ParsedOptions.WriteStaticContent = true,
HelpText = "If set, also generates the static content of the documentation framework."
},
new ArgDeclaration
{
OptionName = "sdkassembliesroot",
ShortName = "sdk",
HasValue = true,
Parse = (arguments, argValue) => arguments.ParsedOptions.SDKAssembliesRoot = argValue,
HelpText = "The folder containing the version info manifest file (_sdk-versions.json) and the built SDK assemblies, organized into per-platform subfolders."
},
new ArgDeclaration
{
OptionName = "sdkversionfile",
ShortName = "sdkversion",
HasValue = true,
Parse = (arguments, argValue) => arguments.ParsedOptions.SDKVersionFilePath = argValue,
HelpText = "The path to the _sdk-versions.json."
},
new ArgDeclaration
{
OptionName = "platform",
ShortName = "p",
HasValue = true,
Parse = (arguments, argValue) => arguments.ParsedOptions.Platform = argValue,
HelpText = "The primary platform subfolder used for assembly discovery, controlling which assemblies get used in doc generation. "
+ "'net45', or the first available subfolder, is used if not specified."
},
new ArgDeclaration
{
OptionName = "services",
ShortName = "svc",
HasValue = true,
Parse = (arguments, argValue) => {
var services = argValue.Split(',').ToList();
if (!services.Contains("Core")) services.Add("Core");
arguments.ParsedOptions.Services = services.ToArray();
},
HelpText = "Comma-delimited set of service names to process. If not specified all assemblies within the primary platform folder matching the SDK naming pattern are used."
},
new ArgDeclaration
{
OptionName = "samplesfolder",
ShortName = "sf",
HasValue = true,
Parse = (arguments, argValue) => arguments.ParsedOptions.CodeSamplesRootFolder = argValue,
HelpText = "The root folder containing the SDK code samples."
},
new ArgDeclaration
{
OptionName = "outputfolder",
ShortName = "o",
HasValue = true,
Parse = (arguments, argValue) => arguments.ParsedOptions.OutputFolder = argValue,
HelpText = "The root folder beneath which the generated documentation will be placed."
}
};
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);
}
}
}