generator/AWSPSGeneratorLib/Generators/HelpGeneratorBase.cs (705 lines of code) (raw):
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using AWSPowerShellGenerator.Analysis;
using AWSPowerShellGenerator.Utils;
using AWSPowerShellGenerator.Writers.Help;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using Parser = YamlDotNet.Core.Parser;
namespace AWSPowerShellGenerator.Generators
{
public abstract class HelpGeneratorBase : Generator
{
#region Public properties
public Assembly CmdletAssembly { get; set; }
public XmlDocument AssemblyDocumentation { get; set; }
public string Name { get; set; }
#endregion
public const string WebApiReferenceBaseUrl = "http://docs.aws.amazon.com/powershell/latest/reference";
protected List<Type> CmdletTypes;
// keyed by cmdlet name
protected Dictionary<string, XmlDocument> ExamplesCache;
protected Dictionary<string, XmlDocument> LinksCache;
protected string ExamplesSearchPattern = "*.yaml";
protected string[] ExampleMetadataRelativePaths = {".doc_gen/metadata", @".doc_gen/common"};
protected List<string> ExampleMetadataFiles = new List<string>();
protected class ExampleMetadata
{
public string title { get; set; }
public string title_abbrev { get; set; }
public string synopsis { get; set; }
public MetadataLanguage languages { get; set; }
public Dictionary<object, object> services { get; set; }
}
protected class MetadataLanguage
{
public MetadataPowerShell PowerShell { get; set; }
}
protected class MetadataPowerShell
{
public MetadataVersion[] versions { get; set; }
}
protected class MetadataVersion
{
public Excerpt[] excerpts { get; set; }
public string sdk_version { get; set; }
public string sdk_action { get; set; }
}
protected class Excerpt
{
public string description { get; set; }
public string[] snippet_files { get; set; }
}
protected class XmlExample
{
public string Description { get; set; }
public List<string> CodeAndOutput { get; set; } = new List<string>();
}
protected class CmdletInfo
{
public CmdletInfo(List<CmdletAttribute> cmdletAttributes, Attribute awsCmdletAttribute, List<Attribute> awsCmdletOutputAttributes)
{
CmdletAttributes = cmdletAttributes;
AWSCmdletAttribute = awsCmdletAttribute;
AWSCmdletOutputAttributes = awsCmdletOutputAttributes;
}
public readonly List<CmdletAttribute> CmdletAttributes; // should actually only be one
public readonly Attribute AWSCmdletAttribute;
public readonly List<Attribute> AWSCmdletOutputAttributes;
}
// lists cmdlets where we want to expose the dynamic parameters based on the
// AWSCredentialsArguments or AWSRegionArguments types as first-class parameters
// for the cmdlet
const string AWSCredentialsArgumentsFullTypename = "Amazon.PowerShell.Common.AWSCredentialsArgumentsFull";
const string AWSRegionArgumentsTypename = "Amazon.PowerShell.Common.AWSRegionArguments";
private readonly List<string> _dynamicParameterCmdlets = new List<string>
{
"Amazon.PowerShell.Common.SetCredentialCmdlet",
"Amazon.PowerShell.Common.NewCredentialCmdlet",
"Amazon.PowerShell.Common.SetDefaultRegionCmdlet",
"Amazon.PowerShell.Common.InitializeDefaultConfigurationCmdlet",
};
// matchs same collection in the sdk, where we shorten paths to avoid issues with
// Windows path lengths
public static Dictionary<string, string> ServiceNamespaceContractions = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
{"ElasticLoadBalancing", "ELB"},
{"ElasticBeanstalk", "EB"},
{"ElasticMapReduce", "EMR"},
{"ElasticTranscoder", "ETS"},
{"SimpleNotificationService", "SNS"},
{"IdentityManagement", "IAM"},
{"DatabaseMigrationService", "DMS"},
{"ApplicationDiscoveryService", "ADS"},
{"SimpleSystemsManagement", "SSM"},
};
protected override void GenerateHelper()
{
var psCmdletType = typeof(PSCmdlet);
CmdletTypes = CmdletAssembly.GetTypes()
.Where(t => psCmdletType.IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract)
.ToList();
var servicePrefix = GetServicePrefixFromCmdletAssembly();
LoadExampleMetadataFiles(servicePrefix);
LoadExamplesCache(servicePrefix);
LoadLinksCache(servicePrefix);
DocumentationUtils.CacheMemberDocumentationSummary(AssemblyDocumentation);
}
private string GetServicePrefixFromCmdletAssembly()
{
if (Name.Equals("AWS.Tools.Common", StringComparison.OrdinalIgnoreCase))
{
return "Common";
}
if (!Name.StartsWith("AWSPowerShell"))
{
var assemblyNamespaces = CmdletAssembly.GetTypes().Select(t => t.Namespace).Where(t => !string.IsNullOrEmpty(t)).Distinct().ToList();
if (assemblyNamespaces.Count() == 1)
{
var cmdletPrefixParts = assemblyNamespaces[0].Split(".");
if (cmdletPrefixParts != null && cmdletPrefixParts.Length == 4)
{
return cmdletPrefixParts[3];
}
}
}
return null;
}
protected CmdletInfo InspectCmdletAttributes(Type cmdletType)
{
var customAttributes = cmdletType.GetCustomAttributes(true).Cast<Attribute>();
var cmdletAttributes =
customAttributes.Select(att => att as CmdletAttribute).Where(catt => catt != null).ToList();
var awsCmdletAttribute =
customAttributes.FirstOrDefault(
att => string.Equals(att.GetType().FullName, "Amazon.PowerShell.Common.AWSCmdletAttribute",
StringComparison.Ordinal));
var awsCmdletOutputAttributes = customAttributes
.Where(att => att.GetType().FullName == "Amazon.PowerShell.Common.AWSCmdletOutputAttribute")
.ToList();
return new CmdletInfo(cmdletAttributes, awsCmdletAttribute, awsCmdletOutputAttributes);
}
protected void LoadExampleMetadataFiles(string servicePrefix)
{
// when servicePrefix is null, all example files are loaded
if (servicePrefix != null)
{
if(servicePrefix == "Common")
{
// if servicePrefix is Common, load files from .doc_gen/common
ExampleMetadataRelativePaths = new[] { ".doc_gen/common" };
}
else
{
// if servicePrefix has value and is not common, look for files that have the servicePrefix in the name.
// e.g workspaces_Edit-WKSWorkspaceState_metadata.yaml search for *-WKS*.yaml
ExamplesSearchPattern = $"*-{servicePrefix}{ExamplesSearchPattern}";
}
}
foreach (var metadataRelativePath in ExampleMetadataRelativePaths)
{
var metadataDirectories = Path.Combine(Options.RootPath, metadataRelativePath);
ExampleMetadataFiles.AddRange(Directory.GetFiles(metadataDirectories, ExamplesSearchPattern));
}
}
protected void LoadExamplesCache(string servicePrefix)
{
ExamplesCache = new Dictionary<string, XmlDocument>();
if (servicePrefix != null)
{
Console.WriteLine(ExampleMetadataFiles.Count > 0
? $"Loading example metadata files for {servicePrefix}"
: $"No example metadata files found for {servicePrefix}");
}
foreach (var metadataPath in ExampleMetadataFiles)
{
try
{
var cmdletName = GetCmdletNameFromMetadataPath(metadataPath);
var document = GetExampleXmlFromMetadata(metadataPath, Options.RootPath);
ExamplesCache[cmdletName] = document;
Console.WriteLine("...loaded examples for {0}", cmdletName);
}
catch (Exception ex)
{
Logger.LogError($"Error processing {metadataPath}. {ex}");
}
}
Console.WriteLine();
}
private string GetCmdletNameFromMetadataPath(string metadataPath)
{
var filenameParts = Path.GetFileNameWithoutExtension(metadataPath).Split('_');
if (filenameParts.Length != 3)
{
throw new InvalidDataException($"The following metadata file name is invalid. expected format is servicename_cmdletname_metadata.yaml. {metadataPath}");
}
if (!filenameParts[1].Contains("-"))
{
throw new InvalidDataException($"The following metadata file name is invalid. expected format is servicename_cmdletname_metadata.yaml. The cmdletname should be of Verb-Noun format but got {filenameParts[1]}. {metadataPath}");
}
return filenameParts[1];
}
private XmlDocument GetExampleXmlFromMetadata(string metadataPath, string rootPath)
{
var exampleMetadata = ExtractExcerptsFromMetadataYaml(metadataPath);
var examples = SplitExcerpts(exampleMetadata.languages.PowerShell.versions[0].excerpts, rootPath);
return GetExampleXmlFromExcerpts(examples);
}
// extracts excerpts from example metadata yaml file
private ExampleMetadata ExtractExcerptsFromMetadataYaml(string metadataPath)
{
using var reader = new StreamReader(metadataPath);
var parser = new Parser(reader);
var deserializer = new DeserializerBuilder()
.Build();
// the yaml files have a dynamic property name of the form Servicename_OpertaionName
// which can't be mapped to a C# class. To overcome this, the code skips just ahead of the Servicename_OpertaionName node
// by consuming MappingStart and Scalar nodes. and then serializes the rest of the yaml document to ExampleMetadata
parser.Consume<StreamStart>();
parser.Consume<DocumentStart>();
parser.Consume<MappingStart>(); // service node
parser.Consume<Scalar>(); // service name mode
var exampleMetadata = deserializer.Deserialize<ExampleMetadata>(parser);
return exampleMetadata;
}
// Entities represent localized keywords in the Example YAML format.
// These keywords will be replaced by the SOS to the localized values. e.g &AWS is replaced by Amazon in China
// The PowerShell XML Help isn't aware of entities so these are substituted.
private string ReplaceEntities(string excerptContent)
{
var replacedString = excerptContent;
var entities = new Dictionary<string, string>()
{
{"&AWS;","AWS"},
{"&AWS-account;","AWS account"},
{"&AWS-accounts;","AWS accounts"},
{"&AWS-Region;","AWS Region"},
{"&AWS-Regions;","AWS Regions"},
{"&AWS-service;","AWS service"},
{"&AWS-services;","AWS services"},
{"&AWS-Cloud;","AWS Cloud"}
};
foreach (var entity in entities)
{
replacedString = replacedString.Replace(entity.Key, entity.Value);
}
return replacedString;
}
// Split Excerpts into List of examples
private List<XmlExample> SplitExcerpts(Excerpt[] excerpts, string rootPath)
{
const string exampleDescriptionBeginPattern = @"Example \d+:";
var exampleBeginPattern = $@"<emphasis role=""bold"">{exampleDescriptionBeginPattern}";
var examples = new List<XmlExample>();
var example = new XmlExample();
bool firstExample = true;
for (int i = 0; i < excerpts.Length; i++)
{
var currentExcerpt = excerpts[i].description;
var match = Regex.Match(currentExcerpt ?? "", exampleBeginPattern);
// Process Example Excerpt
if (match.Success)
{
if (example.CodeAndOutput.Count > 0)
{
examples.Add(example);
}
else if (!firstExample)
{
// There should always be either code and/or output for an example except when it's the first example.
throw new InvalidDataException($"Invalid example. There must be at least one snippet and optional output for an example: {currentExcerpt}");
}
example = new XmlExample();
var outputXml = new XmlDocument() { PreserveWhitespace = true };
outputXml.LoadXml(ReplaceEntities(currentExcerpt));
// Yaml examples description begin with Example followed by a number. e.g Example 11. Replace this with empty string.
var exampleDescription = Regex.Replace(outputXml.DocumentElement.InnerXml, exampleDescriptionBeginPattern, "").Trim();
example.Description = exampleDescription;
firstExample = false;
}
else if (i == 0)
{
// First excerpt must be an example
throw new InvalidDataException($"Invalid excerpt. First excerpt must be an example: {excerpts[0].description}");
}
// Process Snippet excerpt
else if (excerpts[i].snippet_files.Length > 0)
{
var snippetFilePath = Path.Combine(rootPath, excerpts[i].snippet_files[0]);
var snippetContent = File.ReadAllText(snippetFilePath);
example.CodeAndOutput.Add(snippetContent);
}
// Process Output excerpt
else if (currentExcerpt.Contains("<emphasis role=\"bold\">Output:"))
{
var outputXml = new XmlDocument() { PreserveWhitespace = true };
// the next excerpt should have the actual output. Check that current excerpt is not the last and the next excerpt has <programlisting> xml.
if (i == excerpts.Length - 1)
{
throw new InvalidDataException(
$"Invalid Output excerpt. Output excerpt with actual output data is missing: {currentExcerpt}");
}
currentExcerpt = excerpts[++i].description;
if (!currentExcerpt.Contains("<programlisting language=\"none\" role=\"nocopy\">"))
{
throw new InvalidDataException($"Invalid Output excerpt. Expected Output excerpt to have `<programlisting language=\"none\" role=\"nocopy\">`: {currentExcerpt}");
}
outputXml.LoadXml(ReplaceEntities(currentExcerpt));
example.CodeAndOutput.Add(outputXml.InnerText);
}
else
{
throw new InvalidDataException($"Invalid excerpt. Expected excerpt description to contain Example followed by a number or have '<emphasis role=\"bold\">Output:' or snippet_files : {currentExcerpt}");
}
}
if (example.CodeAndOutput.Count > 0)
{
examples.Add(example);
}
//Validate that all examples have at least one CodeAndOutput
if (examples.Any(e => e.CodeAndOutput.Count == 0))
{
throw new InvalidDataException($"The yaml metadata file is malformed. The examples should have at least one code snippet.");
}
return examples;
}
private XmlDocument GetExampleXmlFromExcerpts(List<XmlExample> examples)
{
var examplesXml = new XmlDocument() { PreserveWhitespace = true };
var newLine = Environment.NewLine;
var settings = new XmlWriterSettings() { Indent = true, IndentChars = " ", Encoding = Encoding.UTF8 };
var sw = new StringWriter();
using var writer = XmlWriter.Create(sw, settings);
writer.WriteStartElement("examples");
foreach (var example in examples)
{
writer.WriteStartElement("example");
writer.WriteStartElement("code");
writer.WriteString(string.Join($"{newLine}{newLine}", example.CodeAndOutput));
writer.WriteEndElement();
writer.WriteStartElement("description");
writer.WriteRaw(example.Description);
writer.WriteEndElement();
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.Flush();
examplesXml.LoadXml(sw.ToString());
return examplesXml;
}
protected void LoadLinksCache(string servicePrefix)
{
// when servicePrefix is null all links are loaded otherwise links specific to a service are loaded.
var linkLibrariesPath = Path.Combine(Options.RootPath, "generator", "AWSPSGeneratorLib", "HelpMaterials", "LinkLibraries");
string searchPattern = "*.xml";
if (servicePrefix != null)
{
searchPattern = $"{servicePrefix}.xml";
}
LinksCache = new Dictionary<string, XmlDocument>();
var linkFiles = Directory.GetFiles(linkLibrariesPath, searchPattern);
if (servicePrefix != null)
{
Console.WriteLine(linkFiles.Length > 0
? $"Loading link files for {servicePrefix}"
: $"No link files found for {servicePrefix}");
}
foreach (var linkFile in linkFiles)
{
if (Path.GetFileNameWithoutExtension(linkFile).Equals("Template", StringComparison.OrdinalIgnoreCase))
continue;
var document = new XmlDocument {PreserveWhitespace = true};
document.Load(linkFile);
var servicePrefixFromFileName = Path.GetFileNameWithoutExtension(linkFile);
LinksCache[servicePrefixFromFileName] = document;
Console.WriteLine("...loaded links library for {0}", servicePrefixFromFileName);
}
Console.WriteLine();
}
protected XmlNodeList GetRelatedLinks(XmlDocument document, string target)
{
var xpath = string.Format("links/set[@target='{0}']", target);
var set = document.SelectSingleNode(xpath);
if (set == null)
{
Console.WriteLine("Unable to find related links for target {0} in service {1}, probed xpath {2}", target,
document.Name, xpath);
return null;
}
// to allow easier management over time of the link files, filter out sets who don't yet have
// content but are present simply as templates, that way we don't need to comment out sections
// of the file
var firstLink = set.FirstChild;
if (firstLink == null || string.IsNullOrEmpty(firstLink.InnerText))
{
Console.WriteLine("Skipping empty link set for target {0} in {1}", target, document.Name);
return null;
}
return set.SelectNodes("link");
}
private const string namespacePrefix = "Amazon.PowerShell.Cmdlets.";
private const string commonCmdletsNamespace = "Amazon.PowerShell.Common";
protected string GetServiceAbbreviation(Type cmdletType)
{
string ns = cmdletType.Namespace;
if (string.Equals(ns, commonCmdletsNamespace, StringComparison.OrdinalIgnoreCase))
return "Common";
if (ns.IndexOf(namespacePrefix) != 0)
{
Logger.LogError("Cmdlet namespace \"{0}\" does not contain expected namespace prefix \"{1}\"", ns,
namespacePrefix);
return null;
}
ns = ns.Substring(namespacePrefix.Length);
var components = ns.Split(new char[] {'.'}, StringSplitOptions.RemoveEmptyEntries);
return components[0];
}
private static IEnumerable<object> RecursiveGetCustomAttributes(Type type)
{
while (type != null)
{
foreach (var attribute in type.GetCustomAttributes())
{
yield return attribute;
}
type = type.BaseType;
}
}
protected static (string ModuleName, string ServiceName) DetermineCmdletServiceOwner(Type cmdletType)
{
dynamic attribute = RecursiveGetCustomAttributes(cmdletType).Where(attr => attr.GetType().FullName == "Amazon.PowerShell.Common.AWSClientCmdletAttribute").SingleOrDefault();
if (attribute != null)
{
return (attribute.ModuleName, attribute.ServiceName);
}
// nope, declare it as a misc cmdlet
return ("Common", TOCWriter.CommonTOCName);
}
protected IEnumerable<SimplePropertyInfo> GetRootSimpleProperties(Type requestType)
{
IEnumerable<PropertyInfo> properties = new PropertyInfo[0];
for (var type = requestType; type != null; type = type.BaseType)
{
properties = properties.Concat(type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance));
}
var simpleProperties = properties
.Where(p => p.GetCustomAttributes(typeof(ParameterAttribute), false).Any())
.Select(p => CreateSimpleProperty(p, null))
.Where(sp => sp.IsReadWrite)
.OrderBy(sp => sp.ParameterPosition)
.ToList();
return simpleProperties;
}
protected static void InspectParameter(SimplePropertyInfo property, out HashSet<string> isRequiredForParameterSets,
out string pipelineInput, out string position, out string[] aliases)
{
pipelineInput = "False";
position = "Named";
aliases = new string[0];
// 'Required? true | false'
isRequiredForParameterSets = property.IsRequiredForParameterSets;
if (property.PsParameterAttribute == null)
return;
// 'Accept pipeline input? true ([ByValue,] [ByPropertyName]) | false'
var markedValueFromPipeline = IsMarkedValueFromPipeline(property.PsParameterAttribute);
var markedValueFromPropertyName = IsMarkedValueFromPipelineByName(property.PsParameterAttribute);
if (markedValueFromPipeline | markedValueFromPropertyName)
{
pipelineInput = string.Format("True ({0}{1}{2})",
markedValueFromPipeline ? "ByValue" : "",
markedValueFromPipeline && markedValueFromPropertyName ? ", " : "",
markedValueFromPropertyName ? "ByPropertyName" : "");
}
// 'Position named | ordinal'. Shell convention is to start indexing at 1, but we
// start at 0 internally.
var pos = HasPositionalData(property.PsParameterAttribute);
if (pos >= 0)
position = (pos + 1).ToString(CultureInfo.InvariantCulture);
if (property.PsAliasAttribute != null)
{
aliases = property.PsAliasAttribute.AliasNames.ToArray();
}
}
protected static bool IsMarkedValueFromPipeline(IEnumerable<ParameterAttribute> parameterAttributes)
{
// todo: need to confirm how built-in pshell help handles parameters
// that are pipelinable in one set, not in another. For now, we scann
// all sets
var isMarked = false;
if (parameterAttributes != null)
{
foreach (var pa in parameterAttributes)
{
if (pa.ValueFromPipeline)
isMarked = true;
}
}
return isMarked;
}
protected static bool IsMarkedValueFromPipelineByName(IEnumerable<ParameterAttribute> parameterAttributes)
{
// todo: need to confirm how built-in pshell help handles parameters
// that are assignable in one set, not in another. For now, we scann
// all sets
var isMarked = false;
if (parameterAttributes != null)
{
foreach (var pa in parameterAttributes)
{
if (pa.ValueFromPipelineByPropertyName)
isMarked = true;
}
}
return isMarked;
}
protected static int HasPositionalData(IEnumerable<ParameterAttribute> parameterAttributes)
{
// todo: need to confirm how built-in pshell help handles parameters
// that have a position in one set, not in another. For now, we scann
// all sets
if (parameterAttributes != null)
{
foreach (var pa in parameterAttributes)
{
if (pa.Position >= 0)
return pa.Position;
}
}
return -1;
}
private SimplePropertyInfo CreateSimpleProperty(PropertyInfo property, SimplePropertyInfo parent)
{
var propertyTypeName = GetValidTypeName(property.PropertyType);
var simpleProperty = new SimplePropertyInfo(property, parent, propertyTypeName, AssemblyDocumentation);
return simpleProperty;
}
private string GetValidTypeName(Type type)
{
if (type.IsArray)
return GetValidTypeName(type.GetElementType()) + "[]";
if (type.IsGenericType)
{
var genericArguments = type.GetGenericArguments();
var genericType = type.GetGenericTypeDefinition();
if (genericType.IsAssignableFrom(typeof(List<>)))
return string.Format("List<{0}>", GetValidTypeName(genericArguments[0]));
if (genericType.IsAssignableFrom(typeof(IEnumerable<>)))
return string.Format("IEnumerable<{0}>", GetValidTypeName(genericArguments[0]));
if (genericType.IsAssignableFrom(typeof(Dictionary<,>)))
return string.Format("Dictionary<{0}, {1}>", GetValidTypeName(genericArguments[0]), GetValidTypeName(genericArguments[1]));
if (string.Equals(genericType.FullName, "Amazon.S3.Model.Tuple`2", StringComparison.Ordinal))
return string.Format("Tuple<{0}, {1}>", GetValidTypeName(genericArguments[0]), GetValidTypeName(genericArguments[1]));
if (genericType.IsAssignableFrom(typeof(Nullable<>)))
return string.Format("{0}", GetValidTypeName(genericArguments[0]));
Logger.LogError("Can't determine generic type. Type = [{0}], GenericType = [{1}]", type.FullName, genericType.FullName);
return null;
}
return type.Namespace + "." + type.Name;
}
public static string GetTypeDisplayName(Type propertyType, bool useFullName, bool stripNullable = true)
{
string name;
if (propertyType.IsGenericParameter)
name = propertyType.Name;
else if (propertyType.IsGenericType)
{
if (stripNullable && propertyType.FullName.StartsWith("System.Nullable`"))
{
return GetTypeDisplayName(propertyType.GetGenericArguments()[0], useFullName, false);
}
var baseName = useFullName ? propertyType.FullName : propertyType.Name;
var pos = baseName.IndexOf('`');
baseName = baseName.Substring(0, pos);
var paramCount = propertyType.GetGenericArguments().Length;
var pars = new StringBuilder();
if (propertyType.IsGenericTypeDefinition)
{
if (paramCount == 1)
pars.Append("T");
else
{
for (var i = 1; i <= paramCount; i++)
{
if (pars.Length > 0)
pars.Append(", ");
pars.AppendFormat("T{0}", i);
}
}
}
else
{
foreach (var t in propertyType.GetGenericArguments())
{
if (pars.Length > 0)
pars.Append(", ");
pars.AppendFormat(GetTypeDisplayName(t, useFullName, false));
}
}
name = string.Format("{0}<{1}>", baseName, pars.ToString());
}
else
name = useFullName ? propertyType.FullName : propertyType.Name;
if (name == null)
throw new ApplicationException(string.Format("Failed to resolve display for type {0}", propertyType.ToString()));
return name;
}
}
internal static class XmlWriterExtensions
{
public static void WriteUnescapedElementString(this XmlWriter self, string localName, string value)
{
if (self == null) throw new ArgumentNullException("self");
self.WriteStartElement(localName);
var escapedString = EscapeString(value);
self.WriteRaw(escapedString);
self.WriteEndElement();
}
public static void WriteRawElementString(this XmlWriter self, string localName, string value)
{
if (self == null) throw new ArgumentNullException("self");
self.WriteStartElement(localName);
self.WriteRaw(value);
self.WriteEndElement();
}
private static string EscapeString(string value)
{
StringBuilder sb = new StringBuilder(value);
sb.Replace("&", "&");
return sb.ToString();
}
}
/// <summary>
/// Class handling the partitioning of parameters into one or more parameter sets.
/// At doc generation time, we use construct an instance per cmdlet and allow it
/// to partition the declared parameters based on the presence or absence of a
/// parameter set declaration. We then use the instance to generate set-specific
/// syntax diagrams in the native and web help materials.
/// </summary>
internal class CmdletParameterSetPartitions
{
public CmdletParameterSetPartitions(IEnumerable<SimplePropertyInfo> parameters, string defaultParameterSetName)
{
Parameters = parameters;
DefaultParameterSetName = defaultParameterSetName;
ParameterSets = PartitionParametersBySet();
}
/// <summary>
/// The flat list of all parameters declared for a cmdlet.
/// </summary>
public IEnumerable<SimplePropertyInfo> Parameters { get; private set; }
/// <summary>
/// Name of the default parameter set for a cmdlet. If the cmdlet does not partition
/// parameters into sets (ie has one global set) this is null. When we return the
/// sets for a cmdlet that has custom sets we return the default set first.
/// </summary>
public string DefaultParameterSetName { get; private set; }
/// <summary>
/// The cmdlet parameter names grouped by parameter set. Parameters not defined
/// as belonging to a custom set or sets are found in the __AllParameterSets set,
/// which is the first set in the collection.
/// </summary>
/// <remarks>
/// As we generate syntax charts by walking the declared parameters on a cmdlet
/// in order, we only need to store the parameter name here - a simple lookup
/// by name as we traverse the Parameters collection is all that's required to
/// determine presence/absence in a set.
/// </remarks>
public Dictionary<string, HashSet<string>> ParameterSets { get; private set; }
/// <summary>
/// True if custom named parameter sets were found.
/// </summary>
public bool HasNamedParameterSets
{
get { return ParameterSets.Keys.Count > 1; }
}
/// <summary>
/// Returns the collection of custom named parameter sets. The default set name
/// is always the first in the returned collection.
/// </summary>
public IEnumerable<string> NamedParameterSets
{
get
{
if (!HasNamedParameterSets)
throw new InvalidOperationException("Cannot query custom named parameter sets when none exist");
var l = new List<string>();
if (!string.IsNullOrEmpty(DefaultParameterSetName))
l.Add(DefaultParameterSetName);
foreach (var k in ParameterSets.Keys)
{
if (k.Equals(AllSetsKey, StringComparison.Ordinal))
continue;
if (k.Equals(DefaultParameterSetName, StringComparison.Ordinal))
continue;
l.Add(k);
}
return l;
}
}
/// <summary>
/// Returns the collection of parameter names defined as belonging to the specified
/// set. For syntax diagrams we also want the all parameters defined as belonging
/// to the 'all sets' set.
/// </summary>
/// <param name="setName"></param>
/// <param name="appendAllSets"></param>
/// <returns></returns>
public HashSet<string> ParameterNamesForSet(string setName, bool appendAllSets)
{
if (!ParameterSets.ContainsKey(setName))
throw new ArgumentException("Parameter set is unknown: " + setName, setName);
var parameters = new HashSet<string>(ParameterSets[setName]);
if (appendAllSets && !setName.Equals(AllSetsKey, StringComparison.Ordinal))
{
var allSets = ParameterSets[AllSetsKey];
parameters.UnionWith(allSets);
}
return parameters;
}
private Dictionary<string, HashSet<string>> PartitionParametersBySet()
{
var partitions = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase)
{
{ AllSetsKey, new HashSet<string>() }
};
if (!string.IsNullOrEmpty(DefaultParameterSetName))
partitions.Add(DefaultParameterSetName, new HashSet<string>());
foreach (var p in Parameters)
{
if (p.PsParameterAttribute == null || p.PsParameterAttribute.Length == 0)
continue;
foreach (var pa in p.PsParameterAttribute)
{
if (pa.ParameterSetName.Equals(AllSetsKey))
{
partitions[AllSetsKey].Add(p.CmdletParameterName);
}
else
{
HashSet<string> parameterNames;
if (partitions.ContainsKey(pa.ParameterSetName))
parameterNames = partitions[pa.ParameterSetName];
else
{
parameterNames = new HashSet<string>();
partitions.Add(pa.ParameterSetName, parameterNames);
}
parameterNames.Add(p.CmdletParameterName);
}
}
}
return partitions;
}
// The set name used in PowerShell to contain parameters that are not
// marked as belonging to a particular set (or sets). This also covers
// the scenario when no parameter sets are declared for a cmdlet.
public const string AllSetsKey = "__AllParameterSets";
}
}