PowerShell/src/System.Management.Automation/help/HelpCommentsParser.cs
2021-02-08 17:13:33 -08:00

1227 lines
52 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management.Automation.Help;
using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace System.Management.Automation
{
/// <summary>
/// Parses help comments and turns them into HelpInfo objects.
/// </summary>
internal class HelpCommentsParser
{
private HelpCommentsParser()
{
}
private HelpCommentsParser(List<string> parameterDescriptions)
{
_parameterDescriptions = parameterDescriptions;
}
private HelpCommentsParser(CommandInfo commandInfo, List<string> parameterDescriptions)
{
FunctionInfo fi = commandInfo as FunctionInfo;
if (fi != null)
{
_scriptBlock = fi.ScriptBlock;
_commandName = fi.Name;
}
else
{
ExternalScriptInfo si = commandInfo as ExternalScriptInfo;
if (si != null)
{
_scriptBlock = si.ScriptBlock;
_commandName = si.Path;
}
}
_commandMetadata = commandInfo.CommandMetadata;
_parameterDescriptions = parameterDescriptions;
}
private readonly Language.CommentHelpInfo _sections = new Language.CommentHelpInfo();
private readonly Dictionary<string, string> _parameters = new Dictionary<string, string>();
private readonly List<string> _examples = new List<string>();
private readonly List<string> _inputs = new List<string>();
private readonly List<string> _outputs = new List<string>();
private readonly List<string> _links = new List<string>();
internal bool isExternalHelpSet = false;
private readonly ScriptBlock _scriptBlock;
private readonly CommandMetadata _commandMetadata;
private readonly string _commandName;
private readonly List<string> _parameterDescriptions;
private XmlDocument _doc;
internal static readonly string mshURI = "http://msh";
internal static readonly string mamlURI = "http://schemas.microsoft.com/maml/2004/10";
internal static readonly string commandURI = "http://schemas.microsoft.com/maml/dev/command/2004/10";
internal static readonly string devURI = "http://schemas.microsoft.com/maml/dev/2004/10";
private const string directive = @"^\s*\.(\w+)(\s+(\S.*))?\s*$";
private const string blankline = @"^\s*$";
// Although "http://msh" is the default namespace, it still must be explicitly qualified with non-empty prefix,
// because XPath 1.0 will associate empty prefix with "null" namespace (not with "default") and query will fail.
// See: http://www.w3.org/TR/1999/REC-xpath-19991116/#node-tests
internal static readonly string ProviderHelpCommandXPath =
"/msh:helpItems/msh:providerHelp/msh:CmdletHelpPaths/msh:CmdletHelpPath{0}/command:command[command:details/command:verb='{1}' and command:details/command:noun='{2}']";
private void DetermineParameterDescriptions()
{
int i = 0;
foreach (string parameterName in _commandMetadata.StaticCommandParameterMetadata.BindableParameters.Keys)
{
string description;
if (!_parameters.TryGetValue(parameterName.ToUpperInvariant(), out description))
{
if (i < _parameterDescriptions.Count)
{
_parameters.Add(parameterName.ToUpperInvariant(), _parameterDescriptions[i]);
}
}
++i;
}
}
private string GetParameterDescription(string parameterName)
{
Diagnostics.Assert(!string.IsNullOrEmpty(parameterName), "Parameter name must not be empty");
string description;
_parameters.TryGetValue(parameterName.ToUpperInvariant(), out description);
return description;
}
private XmlElement BuildXmlForParameter(
string parameterName,
bool isMandatory,
bool valueFromPipeline,
bool valueFromPipelineByPropertyName,
string position,
Type type,
string description,
bool supportsWildcards,
string defaultValue,
bool forSyntax)
{
XmlElement command_parameter = _doc.CreateElement("command:parameter", commandURI);
command_parameter.SetAttribute("required", isMandatory ? "true" : "false");
// command_parameter.SetAttribute("variableLength", "unknown");
command_parameter.SetAttribute("globbing", supportsWildcards ? "true" : "false");
string fromPipeline;
if (valueFromPipeline && valueFromPipelineByPropertyName)
{
fromPipeline = "true (ByValue, ByPropertyName)";
}
else if (valueFromPipeline)
{
fromPipeline = "true (ByValue)";
}
else if (valueFromPipelineByPropertyName)
{
fromPipeline = "true (ByPropertyName)";
}
else
{
fromPipeline = "false";
}
command_parameter.SetAttribute("pipelineInput", fromPipeline);
command_parameter.SetAttribute("position", position);
XmlElement name = _doc.CreateElement("maml:name", mamlURI);
XmlText name_text = _doc.CreateTextNode(parameterName);
command_parameter.AppendChild(name).AppendChild(name_text);
if (!string.IsNullOrEmpty(description))
{
XmlElement maml_description = _doc.CreateElement("maml:description", mamlURI);
XmlElement maml_para = _doc.CreateElement("maml:para", mamlURI);
XmlText maml_para_text = _doc.CreateTextNode(description);
command_parameter.AppendChild(maml_description).AppendChild(maml_para).AppendChild(maml_para_text);
}
if (type == null)
type = typeof(object);
var elementType = type.IsArray ? type.GetElementType() : type;
if (elementType.IsEnum)
{
XmlElement parameterValueGroup = _doc.CreateElement("command:parameterValueGroup", commandURI);
foreach (string valueName in Enum.GetNames(elementType))
{
XmlElement parameterValue = _doc.CreateElement("command:parameterValue", commandURI);
parameterValue.SetAttribute("required", "false");
XmlText parameterValue_text = _doc.CreateTextNode(valueName);
parameterValueGroup.AppendChild(parameterValue).AppendChild(parameterValue_text);
}
command_parameter.AppendChild(parameterValueGroup);
}
else
{
bool isSwitchParameter = elementType == typeof(SwitchParameter);
if (!forSyntax || !isSwitchParameter)
{
XmlElement parameterValue = _doc.CreateElement("command:parameterValue", commandURI);
parameterValue.SetAttribute("required", isSwitchParameter ? "false" : "true");
// parameterValue.SetAttribute("variableLength", "unknown");
XmlText parameterValue_text = _doc.CreateTextNode(type.Name);
command_parameter.AppendChild(parameterValue).AppendChild(parameterValue_text);
}
}
if (!forSyntax)
{
XmlElement devType = _doc.CreateElement("dev:type", devURI);
XmlElement typeName = _doc.CreateElement("maml:name", mamlURI);
XmlText typeName_text = _doc.CreateTextNode(type.Name);
command_parameter.AppendChild(devType).AppendChild(typeName).AppendChild(typeName_text);
XmlElement defaultValueElement = _doc.CreateElement("dev:defaultValue", devURI);
XmlText defaultValue_text = _doc.CreateTextNode(defaultValue);
command_parameter.AppendChild(defaultValueElement).AppendChild(defaultValue_text);
}
return command_parameter;
}
/// <summary>
/// Create the maml xml after a successful analysis of the comments.
/// </summary>
/// <returns>The xml node for the command constructed.</returns>
internal XmlDocument BuildXmlFromComments()
{
Diagnostics.Assert(!string.IsNullOrEmpty(_commandName), "Name can never be null");
_doc = new XmlDocument();
XmlElement command = _doc.CreateElement("command:command", commandURI);
command.SetAttribute("xmlns:maml", mamlURI);
command.SetAttribute("xmlns:command", commandURI);
command.SetAttribute("xmlns:dev", devURI);
_doc.AppendChild(command);
XmlElement details = _doc.CreateElement("command:details", commandURI);
command.AppendChild(details);
XmlElement name = _doc.CreateElement("command:name", commandURI);
XmlText name_text = _doc.CreateTextNode(_commandName);
details.AppendChild(name).AppendChild(name_text);
if (!string.IsNullOrEmpty(_sections.Synopsis))
{
XmlElement synopsis = _doc.CreateElement("maml:description", mamlURI);
XmlElement synopsis_para = _doc.CreateElement("maml:para", mamlURI);
XmlText synopsis_text = _doc.CreateTextNode(_sections.Synopsis);
details.AppendChild(synopsis).AppendChild(synopsis_para).AppendChild(synopsis_text);
}
#region Syntax
// The syntax is automatically generated from parameter metadata
DetermineParameterDescriptions();
XmlElement syntax = _doc.CreateElement("command:syntax", commandURI);
MergedCommandParameterMetadata parameterMetadata = _commandMetadata.StaticCommandParameterMetadata;
if (parameterMetadata.ParameterSetCount > 0)
{
for (int i = 0; i < parameterMetadata.ParameterSetCount; ++i)
{
BuildSyntaxForParameterSet(command, syntax, parameterMetadata, i);
}
}
else
{
BuildSyntaxForParameterSet(command, syntax, parameterMetadata, int.MaxValue);
}
#endregion Syntax
#region Parameters
XmlElement commandParameters = _doc.CreateElement("command:parameters", commandURI);
foreach (KeyValuePair<string, MergedCompiledCommandParameter> pair in parameterMetadata.BindableParameters)
{
MergedCompiledCommandParameter mergedParameter = pair.Value;
if (mergedParameter.BinderAssociation == ParameterBinderAssociation.CommonParameters)
{
continue;
}
string parameterName = pair.Key;
string description = GetParameterDescription(parameterName);
ParameterSetSpecificMetadata parameterSetData;
bool isMandatory = false;
bool valueFromPipeline = false;
bool valueFromPipelineByPropertyName = false;
string position = "named";
int i = 0;
CompiledCommandParameter parameter = mergedParameter.Parameter;
parameter.ParameterSetData.TryGetValue(ParameterAttribute.AllParameterSets, out parameterSetData);
while (parameterSetData == null && i < 32)
{
parameterSetData = parameter.GetParameterSetData(1u << i++);
}
if (parameterSetData != null)
{
isMandatory = parameterSetData.IsMandatory;
valueFromPipeline = parameterSetData.ValueFromPipeline;
valueFromPipelineByPropertyName = parameterSetData.ValueFromPipelineByPropertyName;
position = parameterSetData.IsPositional ? (1 + parameterSetData.Position).ToString(CultureInfo.InvariantCulture) : "named";
}
var compiledAttributes = parameter.CompiledAttributes;
bool supportsWildcards = compiledAttributes.OfType<SupportsWildcardsAttribute>().Any();
string defaultValueStr = string.Empty;
object defaultValue = null;
var defaultValueAttribute = compiledAttributes.OfType<PSDefaultValueAttribute>().FirstOrDefault();
if (defaultValueAttribute != null)
{
defaultValueStr = defaultValueAttribute.Help;
if (string.IsNullOrEmpty(defaultValueStr))
{
defaultValue = defaultValueAttribute.Value;
}
}
if (string.IsNullOrEmpty(defaultValueStr))
{
if (defaultValue == null)
{
RuntimeDefinedParameter rdp;
if (_scriptBlock.RuntimeDefinedParameters.TryGetValue(parameterName, out rdp))
{
defaultValue = rdp.Value;
}
}
var wrapper = defaultValue as Compiler.DefaultValueExpressionWrapper;
if (wrapper != null)
{
defaultValueStr = wrapper.Expression.Extent.Text;
}
else if (defaultValue != null)
{
defaultValueStr = PSObject.ToStringParser(null, defaultValue);
}
}
XmlElement parameterElement = BuildXmlForParameter(parameterName, isMandatory,
valueFromPipeline, valueFromPipelineByPropertyName, position,
parameter.Type, description, supportsWildcards, defaultValueStr, forSyntax: false);
commandParameters.AppendChild(parameterElement);
}
command.AppendChild(commandParameters);
#endregion Parameters
if (!string.IsNullOrEmpty(_sections.Description))
{
XmlElement description = _doc.CreateElement("maml:description", mamlURI);
XmlElement description_para = _doc.CreateElement("maml:para", mamlURI);
XmlText description_text = _doc.CreateTextNode(_sections.Description);
command.AppendChild(description).AppendChild(description_para).AppendChild(description_text);
}
if (!string.IsNullOrEmpty(_sections.Notes))
{
XmlElement alertSet = _doc.CreateElement("maml:alertSet", mamlURI);
XmlElement alert = _doc.CreateElement("maml:alert", mamlURI);
XmlElement alert_para = _doc.CreateElement("maml:para", mamlURI);
XmlText alert_para_text = _doc.CreateTextNode(_sections.Notes);
command.AppendChild(alertSet).AppendChild(alert).AppendChild(alert_para).AppendChild(alert_para_text);
}
if (_examples.Count > 0)
{
XmlElement examples = _doc.CreateElement("command:examples", commandURI);
int count = 1;
foreach (string example in _examples)
{
XmlElement example_node = _doc.CreateElement("command:example", commandURI);
// The title is automatically generated
XmlElement title = _doc.CreateElement("maml:title", mamlURI);
string titleStr = string.Format(CultureInfo.InvariantCulture,
"\t\t\t\t-------------------------- {0} {1} --------------------------",
HelpDisplayStrings.ExampleUpperCase, count++);
XmlText title_text = _doc.CreateTextNode(titleStr);
example_node.AppendChild(title).AppendChild(title_text);
string prompt_str;
string code_str;
string remarks_str;
GetExampleSections(example, out prompt_str, out code_str, out remarks_str);
// Introduction (usually the prompt)
XmlElement introduction = _doc.CreateElement("maml:introduction", mamlURI);
XmlElement introduction_para = _doc.CreateElement("maml:para", mamlURI);
XmlText introduction_para_text = _doc.CreateTextNode(prompt_str);
example_node.AppendChild(introduction).AppendChild(introduction_para).AppendChild(introduction_para_text);
// Example code
XmlElement code = _doc.CreateElement("dev:code", devURI);
XmlText code_text = _doc.CreateTextNode(code_str);
example_node.AppendChild(code).AppendChild(code_text);
// Remarks are comments on the example
XmlElement remarks = _doc.CreateElement("dev:remarks", devURI);
XmlElement remarks_para = _doc.CreateElement("maml:para", mamlURI);
XmlText remarks_para_text = _doc.CreateTextNode(remarks_str);
example_node.AppendChild(remarks).AppendChild(remarks_para).AppendChild(remarks_para_text);
// The convention is to have 4 blank paras after the example for spacing
for (int i = 0; i < 4; i++)
{
remarks.AppendChild(_doc.CreateElement("maml:para", mamlURI));
}
examples.AppendChild(example_node);
}
command.AppendChild(examples);
}
if (_inputs.Count > 0)
{
XmlElement inputTypes = _doc.CreateElement("command:inputTypes", commandURI);
foreach (string inputStr in _inputs)
{
XmlElement inputType = _doc.CreateElement("command:inputType", commandURI);
XmlElement type = _doc.CreateElement("dev:type", devURI);
XmlElement maml_name = _doc.CreateElement("maml:name", mamlURI);
XmlText maml_name_text = _doc.CreateTextNode(inputStr);
inputTypes.AppendChild(inputType).AppendChild(type).AppendChild(maml_name).AppendChild(maml_name_text);
}
command.AppendChild(inputTypes);
}
// For outputs, we prefer what was specified in the comments, but if there are no comments
// and the OutputType attribute was specified, we'll use those instead.
IEnumerable outputs = null;
if (_outputs.Count > 0)
{
outputs = _outputs;
}
else if (_scriptBlock.OutputType.Count > 0)
{
outputs = _scriptBlock.OutputType;
}
if (outputs != null)
{
XmlElement returnValues = _doc.CreateElement("command:returnValues", commandURI);
foreach (object output in outputs)
{
XmlElement returnValue = _doc.CreateElement("command:returnValue", commandURI);
XmlElement type = _doc.CreateElement("dev:type", devURI);
XmlElement maml_name = _doc.CreateElement("maml:name", mamlURI);
string returnValueStr = output as string ?? ((PSTypeName)output).Name;
XmlText maml_name_text = _doc.CreateTextNode(returnValueStr);
returnValues.AppendChild(returnValue).AppendChild(type).AppendChild(maml_name).AppendChild(maml_name_text);
}
command.AppendChild(returnValues);
}
if (_links.Count > 0)
{
XmlElement links = _doc.CreateElement("maml:relatedLinks", mamlURI);
foreach (string link in _links)
{
XmlElement navigationLink = _doc.CreateElement("maml:navigationLink", mamlURI);
bool isOnlineHelp = Uri.IsWellFormedUriString(link, UriKind.Absolute);
string nodeName = isOnlineHelp ? "maml:uri" : "maml:linkText";
XmlElement linkText = _doc.CreateElement(nodeName, mamlURI);
XmlText linkText_text = _doc.CreateTextNode(link);
links.AppendChild(navigationLink).AppendChild(linkText).AppendChild(linkText_text);
}
command.AppendChild(links);
}
return _doc;
}
private void BuildSyntaxForParameterSet(XmlElement command, XmlElement syntax, MergedCommandParameterMetadata parameterMetadata, int i)
{
XmlElement syntaxItem = _doc.CreateElement("command:syntaxItem", commandURI);
XmlElement syntaxItemName = _doc.CreateElement("maml:name", mamlURI);
XmlText syntaxItemName_text = _doc.CreateTextNode(_commandName);
syntaxItem.AppendChild(syntaxItemName).AppendChild(syntaxItemName_text);
Collection<MergedCompiledCommandParameter> compiledParameters =
parameterMetadata.GetParametersInParameterSet(1u << i);
foreach (MergedCompiledCommandParameter mergedParameter in compiledParameters)
{
if (mergedParameter.BinderAssociation == ParameterBinderAssociation.CommonParameters)
{
continue;
}
CompiledCommandParameter parameter = mergedParameter.Parameter;
ParameterSetSpecificMetadata parameterSetData = parameter.GetParameterSetData(1u << i);
string description = GetParameterDescription(parameter.Name);
bool supportsWildcards = parameter.CompiledAttributes.Any(attribute => attribute is SupportsWildcardsAttribute);
XmlElement parameterElement = BuildXmlForParameter(parameter.Name,
parameterSetData.IsMandatory, parameterSetData.ValueFromPipeline,
parameterSetData.ValueFromPipelineByPropertyName,
parameterSetData.IsPositional ? (1 + parameterSetData.Position).ToString(CultureInfo.InvariantCulture) : "named",
parameter.Type, description, supportsWildcards, defaultValue: string.Empty, forSyntax: true);
syntaxItem.AppendChild(parameterElement);
}
command.AppendChild(syntax).AppendChild(syntaxItem);
}
private static void GetExampleSections(string content, out string prompt_str, out string code_str, out string remarks_str)
{
const string default_prompt_str = "PS > ";
var promptMatch = Regex.Match(content, "^.*?>");
prompt_str = promptMatch.Success ? promptMatch.Value : default_prompt_str;
if (promptMatch.Success)
{
content = content.Substring(prompt_str.Length);
}
var codeAndRemarksMatch = Regex.Match(content, "^(?<code>.*?)\r?\n\r?\n(?<remarks>.*)$", RegexOptions.Singleline);
if (codeAndRemarksMatch.Success)
{
code_str = codeAndRemarksMatch.Groups["code"].Value.Trim();
remarks_str = codeAndRemarksMatch.Groups["remarks"].Value;
}
else
{
code_str = content.Trim();
remarks_str = string.Empty;
}
}
/// <summary>
/// Split the text in the comment token into multiple lines, appending commentLines.
/// </summary>
/// <param name="comment">A single line or multiline comment token.</param>
/// <param name="commentLines"></param>
private static void CollectCommentText(Token comment, List<string> commentLines)
{
string text = comment.Text;
CollectCommentText(text, commentLines);
}
private static void CollectCommentText(string text, List<string> commentLines)
{
int i = 0;
if (text[0] == '<')
{
int start = 2;
// The full text includes '<#', so start at index 2 to skip those characters,
// and the full text also includes '#>' at the end, so skip those as well.
for (i = 2; i < text.Length - 2; i++)
{
if (text[i] == '\n')
{
commentLines.Add(text.Substring(start, i - start));
start = i + 1;
}
else if (text[i] == '\r')
{
commentLines.Add(text.Substring(start, i - start));
// No need to check length here, comment text has at least '#>' at the end.
if (text[i + 1] == '\n')
{
i++;
}
start = i + 1;
}
}
commentLines.Add(text.Substring(start, i - start));
}
else
{
for (; i < text.Length; i++)
{
// Skip all leading '#' characters as it is a common convention
// to use more than one '#' character.
if (text[i] != '#')
{
break;
}
}
commentLines.Add(text.Substring(i));
}
}
/// <summary>
/// Collect the text of a section. Stop collecting the section
/// when a new directive is found (even if it is an unknown directive).
/// </summary>
/// <param name="commentLines">The comment block, as a list of lines.</param>
/// <param name="i"></param>
/// <returns>The text of the help section, with 'i' left on the last line collected.</returns>
private static string GetSection(List<string> commentLines, ref int i)
{
bool capturing = false;
int countLeadingWS = 0;
StringBuilder sb = new StringBuilder();
const char nbsp = (char)0xA0;
for (i++; i < commentLines.Count; i++)
{
string line = commentLines[i];
if (!capturing && Regex.IsMatch(line, blankline))
{
// Skip blank lines before capturing anything in the section.
continue;
}
if (Regex.IsMatch(line, directive))
{
// Break on any directive even if we haven't started capturing.
i--;
break;
}
// The first line of a section sets how much whitespace we'll ignore (and hence strip).
if (!capturing)
{
int j = 0;
while (j < line.Length && (line[j] == ' ' || line[j] == '\t' || line[j] == nbsp))
{
countLeadingWS++;
j++;
}
}
capturing = true;
// Skip leading whitespace based on the first line in the section, skipping
// only as much whitespace as the first line had, no more (and possibly less.)
int start = 0;
while (start < line.Length && start < countLeadingWS &&
(line[start] == ' ' || line[start] == '\t' || line[start] == nbsp))
{
start++;
}
sb.Append(line.Substring(start));
sb.Append('\n');
}
return sb.ToString();
}
internal string GetHelpFile(CommandInfo commandInfo)
{
if (_sections.MamlHelpFile == null)
{
return null;
}
string helpFileToLoad = _sections.MamlHelpFile;
Collection<string> searchPaths = new Collection<string>();
string scriptFile = ((IScriptCommandInfo)commandInfo).ScriptBlock.File;
if (!string.IsNullOrEmpty(scriptFile))
{
helpFileToLoad = Path.Combine(Path.GetDirectoryName(scriptFile), _sections.MamlHelpFile);
}
else if (commandInfo.Module != null)
{
helpFileToLoad = Path.Combine(Path.GetDirectoryName(commandInfo.Module.Path), _sections.MamlHelpFile);
}
string location = MUIFileSearcher.LocateFile(helpFileToLoad, searchPaths);
return location;
}
internal RemoteHelpInfo GetRemoteHelpInfo(ExecutionContext context, CommandInfo commandInfo)
{
if (string.IsNullOrEmpty(_sections.ForwardHelpTargetName) || string.IsNullOrEmpty(_sections.RemoteHelpRunspace))
{
return null;
}
// get the PSSession object from the variable specified in the comments
IScriptCommandInfo scriptCommandInfo = (IScriptCommandInfo)commandInfo;
SessionState sessionState = scriptCommandInfo.ScriptBlock.SessionState;
object runspaceInfoAsObject = sessionState.PSVariable.GetValue(_sections.RemoteHelpRunspace);
PSSession runspaceInfo;
if (runspaceInfoAsObject == null ||
!LanguagePrimitives.TryConvertTo(runspaceInfoAsObject, out runspaceInfo))
{
string errorMessage = HelpErrors.RemoteRunspaceNotAvailable;
throw new InvalidOperationException(errorMessage);
}
return new RemoteHelpInfo(
context,
(RemoteRunspace)runspaceInfo.Runspace,
commandInfo.Name,
_sections.ForwardHelpTargetName,
_sections.ForwardHelpCategory,
commandInfo.HelpCategory);
}
/// <summary>
/// Look for special comments indicating the comment block is meant
/// to be used for help.
/// </summary>
/// <param name="comments">The list of comments to process.</param>
/// <returns>True if any special comments are found, false otherwise.</returns>
internal bool AnalyzeCommentBlock(List<Token> comments)
{
if (comments == null || comments.Count == 0)
{
return false;
}
List<string> commentLines = new List<string>();
foreach (Token comment in comments)
{
CollectCommentText(comment, commentLines);
}
return AnalyzeCommentBlock(commentLines);
}
private bool AnalyzeCommentBlock(List<string> commentLines)
{
bool directiveFound = false;
for (int i = 0; i < commentLines.Count; i++)
{
Match match = Regex.Match(commentLines[i], directive);
if (match.Success)
{
directiveFound = true;
if (match.Groups[3].Success)
{
switch (match.Groups[1].Value.ToUpperInvariant())
{
case "PARAMETER":
{
string param = match.Groups[3].Value.ToUpperInvariant().Trim();
string section = GetSection(commentLines, ref i);
if (!_parameters.ContainsKey(param))
{
_parameters.Add(param, section);
}
break;
}
case "FORWARDHELPTARGETNAME":
_sections.ForwardHelpTargetName = match.Groups[3].Value.Trim();
break;
case "FORWARDHELPCATEGORY":
_sections.ForwardHelpCategory = match.Groups[3].Value.Trim();
break;
case "REMOTEHELPRUNSPACE":
_sections.RemoteHelpRunspace = match.Groups[3].Value.Trim();
break;
case "EXTERNALHELP":
_sections.MamlHelpFile = match.Groups[3].Value.Trim();
isExternalHelpSet = true;
break;
default:
return false;
}
}
else
{
switch (match.Groups[1].Value.ToUpperInvariant())
{
case "SYNOPSIS":
_sections.Synopsis = GetSection(commentLines, ref i);
break;
case "DESCRIPTION":
_sections.Description = GetSection(commentLines, ref i);
break;
case "NOTES":
_sections.Notes = GetSection(commentLines, ref i);
break;
case "LINK":
_links.Add(GetSection(commentLines, ref i).Trim());
break;
case "EXAMPLE":
_examples.Add(GetSection(commentLines, ref i));
break;
case "INPUTS":
_inputs.Add(GetSection(commentLines, ref i));
break;
case "OUTPUTS":
_outputs.Add(GetSection(commentLines, ref i));
break;
case "COMPONENT":
_sections.Component = GetSection(commentLines, ref i).Trim();
break;
case "ROLE":
_sections.Role = GetSection(commentLines, ref i).Trim();
break;
case "FUNCTIONALITY":
_sections.Functionality = GetSection(commentLines, ref i).Trim();
break;
default:
return false;
}
}
}
else if (!Regex.IsMatch(commentLines[i], blankline))
{
return false;
}
}
_sections.Examples = new ReadOnlyCollection<string>(_examples);
_sections.Inputs = new ReadOnlyCollection<string>(_inputs);
_sections.Outputs = new ReadOnlyCollection<string>(_outputs);
_sections.Links = new ReadOnlyCollection<string>(_links);
// TODO, Changing this to an IDictionary because ReadOnlyDictionary is available only in .NET 4.5
// This is a temporary workaround and will be fixed later. Tracked by Win8: 354135
_sections.Parameters = new Dictionary<string, string>(_parameters);
return directiveFound;
}
/// <summary>
/// The analysis of the comments finds the component, functionality, and role fields, but
/// those fields aren't added to the xml because they aren't children of the command xml
/// node, they are under a sibling of the command xml node and apply to all command nodes
/// in a maml file.
/// </summary>
/// <param name="helpInfo">The helpInfo object to set the fields on.</param>
internal void SetAdditionalData(MamlCommandHelpInfo helpInfo)
{
helpInfo.SetAdditionalDataFromHelpComment(
_sections.Component,
_sections.Functionality,
_sections.Role);
}
internal static CommentHelpInfo GetHelpContents(List<Language.Token> comments, List<string> parameterDescriptions)
{
HelpCommentsParser helpCommentsParser = new HelpCommentsParser(parameterDescriptions);
helpCommentsParser.AnalyzeCommentBlock(comments);
return helpCommentsParser._sections;
}
internal static HelpInfo CreateFromComments(ExecutionContext context,
CommandInfo commandInfo,
List<Language.Token> comments,
List<string> parameterDescriptions,
bool dontSearchOnRemoteComputer,
out string helpFile, out string helpUriFromDotLink)
{
HelpCommentsParser helpCommentsParser = new HelpCommentsParser(commandInfo, parameterDescriptions);
helpCommentsParser.AnalyzeCommentBlock(comments);
if (helpCommentsParser._sections.Links != null && helpCommentsParser._sections.Links.Count != 0)
{
helpUriFromDotLink = helpCommentsParser._sections.Links[0];
}
else
{
helpUriFromDotLink = null;
}
helpFile = helpCommentsParser.GetHelpFile(commandInfo);
// If only .ExternalHelp is defined and the help file is not found, then we
// use the metadata driven help
if (comments.Count == 1 && helpCommentsParser.isExternalHelpSet && helpFile == null)
{
return null;
}
return CreateFromComments(context, commandInfo, helpCommentsParser, dontSearchOnRemoteComputer);
}
internal static HelpInfo CreateFromComments(ExecutionContext context, CommandInfo commandInfo, HelpCommentsParser helpCommentsParser,
bool dontSearchOnRemoteComputer)
{
if (!dontSearchOnRemoteComputer)
{
RemoteHelpInfo remoteHelpInfo = helpCommentsParser.GetRemoteHelpInfo(context, commandInfo);
if (remoteHelpInfo != null)
{
// Add HelpUri if necessary
if (remoteHelpInfo.GetUriForOnlineHelp() == null)
{
DefaultCommandHelpObjectBuilder.AddRelatedLinksProperties(remoteHelpInfo.FullHelp,
commandInfo.CommandMetadata.HelpUri);
}
return remoteHelpInfo;
}
}
XmlDocument doc = helpCommentsParser.BuildXmlFromComments();
HelpCategory helpCategory = commandInfo.HelpCategory;
MamlCommandHelpInfo localHelpInfo = MamlCommandHelpInfo.Load(doc.DocumentElement, helpCategory);
if (localHelpInfo != null)
{
helpCommentsParser.SetAdditionalData(localHelpInfo);
if (!string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpTargetName)
|| !string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpCategory))
{
if (string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpTargetName))
{
localHelpInfo.ForwardTarget = localHelpInfo.Name;
}
else
{
localHelpInfo.ForwardTarget = helpCommentsParser._sections.ForwardHelpTargetName;
}
if (!string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpCategory))
{
try
{
localHelpInfo.ForwardHelpCategory = (HelpCategory)Enum.Parse(typeof(HelpCategory), helpCommentsParser._sections.ForwardHelpCategory, true);
}
catch (System.ArgumentException)
{
// Ignore conversion errors.
}
}
else
{
localHelpInfo.ForwardHelpCategory = (HelpCategory.Alias |
HelpCategory.Cmdlet |
HelpCategory.ExternalScript |
HelpCategory.Filter |
HelpCategory.Function |
HelpCategory.ScriptCommand);
}
}
// Add HelpUri if necessary
if (localHelpInfo.GetUriForOnlineHelp() == null)
{
DefaultCommandHelpObjectBuilder.AddRelatedLinksProperties(localHelpInfo.FullHelp, commandInfo.CommandMetadata.HelpUri);
}
}
return localHelpInfo;
}
/// <summary>
/// Analyze a block of comments to determine if it is a special help block.
/// </summary>
/// <param name="commentBlock">The block of comments to analyze.</param>
/// <returns>True if the block is our special comment block for help, false otherwise.</returns>
internal static bool IsCommentHelpText(List<Token> commentBlock)
{
if ((commentBlock == null) || (commentBlock.Count == 0))
return false;
HelpCommentsParser generator = new HelpCommentsParser();
return generator.AnalyzeCommentBlock(commentBlock);
}
#region Collect comments from AST
private static List<Language.Token> GetCommentBlock(Language.Token[] tokens, ref int startIndex)
{
var result = new List<Language.Token>();
// Any whitespace between the token and the first comment is allowed.
int nextMaxStartLine = Int32.MaxValue;
for (int i = startIndex; i < tokens.Length; i++)
{
Language.Token current = tokens[i];
// If the current token starts on a line beyond the current "chunk",
// then we're done scanning.
if (current.Extent.StartLineNumber > nextMaxStartLine)
{
startIndex = i;
break;
}
if (current.Kind == TokenKind.Comment)
{
result.Add(current);
// The next comment must be on either the same line as this comment ends, or
// the next line, but nowhere else, otherwise it's not in the same "chunk".
nextMaxStartLine = current.Extent.EndLineNumber + 1;
}
else if (current.Kind != TokenKind.NewLine)
{
// A non-comment, non-position token means we are no longer collecting comments
startIndex = i;
break;
}
}
return result;
}
private static List<Language.Token> GetPrecedingCommentBlock(Language.Token[] tokens, int tokenIndex, int proximity)
{
var result = new List<Language.Token>();
int minEndLine = tokens[tokenIndex].Extent.StartLineNumber - proximity;
for (int i = tokenIndex - 1; i >= 0; i--)
{
Language.Token current = tokens[i];
if (current.Extent.EndLineNumber < minEndLine)
break;
if (current.Kind == TokenKind.Comment)
{
result.Add(current);
minEndLine = current.Extent.StartLineNumber - 1;
}
else if (current.Kind != TokenKind.NewLine)
{
break;
}
}
result.Reverse();
return result;
}
private static int FirstTokenInExtent(Language.Token[] tokens, IScriptExtent extent, int startIndex = 0)
{
int index;
for (index = startIndex; index < tokens.Length; ++index)
{
if (!tokens[index].Extent.IsBefore(extent))
{
break;
}
}
return index;
}
private static int LastTokenInExtent(Language.Token[] tokens, IScriptExtent extent, int startIndex)
{
int index;
for (index = startIndex; index < tokens.Length; ++index)
{
if (tokens[index].Extent.IsAfter(extent))
{
break;
}
}
return index - 1;
}
internal const int CommentBlockProximity = 2;
private static List<string> GetParameterComments(Language.Token[] tokens, IParameterMetadataProvider ipmp, int startIndex)
{
var result = new List<string>();
var parameters = ipmp.Parameters;
if (parameters == null || parameters.Count == 0)
{
return result;
}
foreach (var parameter in parameters)
{
var commentLines = new List<string>();
var firstToken = FirstTokenInExtent(tokens, parameter.Extent, startIndex);
var comments = GetPrecedingCommentBlock(tokens, firstToken, CommentBlockProximity);
if (comments != null)
{
foreach (var comment in comments)
{
CollectCommentText(comment, commentLines);
}
}
var lastToken = LastTokenInExtent(tokens, parameter.Extent, firstToken);
for (int i = firstToken; i < lastToken; ++i)
{
if (tokens[i].Kind == TokenKind.Comment)
{
CollectCommentText(tokens[i], commentLines);
}
}
lastToken += 1;
comments = GetCommentBlock(tokens, ref lastToken);
if (comments != null)
{
foreach (var comment in comments)
{
CollectCommentText(comment, commentLines);
}
}
int n = -1;
result.Add(GetSection(commentLines, ref n));
}
return result;
}
internal static Tuple<List<Language.Token>, List<string>> GetHelpCommentTokens(IParameterMetadataProvider ipmp,
Dictionary<Ast, Token[]> scriptBlockTokenCache)
{
Diagnostics.Assert(scriptBlockTokenCache != null, "scriptBlockTokenCache cannot be null");
var ast = (Ast)ipmp;
var rootAst = ast;
Ast configAst = null;
while (rootAst.Parent != null)
{
rootAst = rootAst.Parent;
if (rootAst is ConfigurationDefinitionAst)
{
configAst = rootAst;
}
}
// tokens saved from reparsing the script.
Language.Token[] tokens = null;
scriptBlockTokenCache.TryGetValue(rootAst, out tokens);
if (tokens == null)
{
ParseError[] errors;
// storing all comment tokens
Language.Parser.ParseInput(rootAst.Extent.Text, out tokens, out errors);
scriptBlockTokenCache[rootAst] = tokens;
}
int savedStartIndex;
int startTokenIndex;
int lastTokenIndex;
var funcDefnAst = ast as FunctionDefinitionAst;
List<Language.Token> commentBlock;
if (funcDefnAst != null || configAst != null)
{
// The first comment block preceding the function or configuration keyword is a candidate help comment block.
var funcOrConfigTokenIndex =
savedStartIndex = FirstTokenInExtent(tokens, configAst == null ? ast.Extent : configAst.Extent);
commentBlock = GetPrecedingCommentBlock(tokens, funcOrConfigTokenIndex, CommentBlockProximity);
if (HelpCommentsParser.IsCommentHelpText(commentBlock))
{
return Tuple.Create(commentBlock, GetParameterComments(tokens, ipmp, savedStartIndex));
}
// comment block is behind function definition
// we don't support it for configuration declaration as this style is rarely used
if (funcDefnAst != null)
{
startTokenIndex =
FirstTokenInExtent(tokens, funcDefnAst.Body.Extent) + 1;
lastTokenIndex = LastTokenInExtent(tokens, ast.Extent, funcOrConfigTokenIndex);
Diagnostics.Assert(tokens[startTokenIndex - 1].Kind == TokenKind.LCurly,
"Unexpected first token in function");
Diagnostics.Assert(tokens[lastTokenIndex].Kind == TokenKind.RCurly,
"Unexpected last token in function");
}
else
{
return null;
}
}
else if (ast == rootAst)
{
startTokenIndex = savedStartIndex = 0;
lastTokenIndex = tokens.Length - 1;
}
else
{
// This case should be rare (but common with implicit remoting).
// We have a script block that was used to generate a function like:
// $sb = { }
// set-item function:foo $sb
// help foo
startTokenIndex = savedStartIndex = FirstTokenInExtent(tokens, ast.Extent) + 1;
lastTokenIndex = LastTokenInExtent(tokens, ast.Extent, startTokenIndex);
Diagnostics.Assert(tokens[startTokenIndex - 1].Kind == TokenKind.LCurly,
"Unexpected first token in script block");
Diagnostics.Assert(tokens[lastTokenIndex].Kind == TokenKind.RCurly,
"Unexpected last token in script block");
}
while (true)
{
commentBlock = GetCommentBlock(tokens, ref startTokenIndex);
if (commentBlock.Count == 0)
break;
if (!HelpCommentsParser.IsCommentHelpText(commentBlock))
continue;
if (ast == rootAst)
{
// One more check - make sure the comment doesn't belong to the first function in the script.
var endBlock = ((ScriptBlockAst)ast).EndBlock;
if (endBlock == null || !endBlock.Unnamed)
{
return Tuple.Create(commentBlock, GetParameterComments(tokens, ipmp, savedStartIndex));
}
var firstStatement = endBlock.Statements.FirstOrDefault();
if (firstStatement is FunctionDefinitionAst)
{
int linesBetween = firstStatement.Extent.StartLineNumber -
commentBlock.Last().Extent.EndLineNumber;
if (linesBetween > CommentBlockProximity)
{
return Tuple.Create(commentBlock, GetParameterComments(tokens, ipmp, savedStartIndex));
}
break;
}
}
return Tuple.Create(commentBlock, GetParameterComments(tokens, ipmp, savedStartIndex));
}
commentBlock = GetPrecedingCommentBlock(tokens, lastTokenIndex, tokens[lastTokenIndex].Extent.StartLineNumber);
if (HelpCommentsParser.IsCommentHelpText(commentBlock))
{
return Tuple.Create(commentBlock, GetParameterComments(tokens, ipmp, savedStartIndex));
}
return null;
}
#endregion Collect comments from AST
}
}