MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections;5using System.Collections.Generic;6using System.Collections.ObjectModel;7using System.Globalization;8using System.IO;9using System.Linq;10using System.Management.Automation.Help;11using System.Management.Automation.Language;12using System.Management.Automation.Runspaces;13using System.Reflection;14using System.Text;15using System.Text.RegularExpressions;16using System.Xml;17 18namespace System.Management.Automation19{20 /// <summary>21 /// Parses help comments and turns them into HelpInfo objects.22 /// </summary>23 internal sealed class HelpCommentsParser24 {25 private HelpCommentsParser()26 {27 }28 29 private HelpCommentsParser(List<string> parameterDescriptions)30 {31 _parameterDescriptions = parameterDescriptions;32 }33 34 private HelpCommentsParser(CommandInfo commandInfo, List<string> parameterDescriptions)35 {36 FunctionInfo fi = commandInfo as FunctionInfo;37 if (fi != null)38 {39 _scriptBlock = fi.ScriptBlock;40 _commandName = fi.Name;41 }42 else43 {44 ExternalScriptInfo si = commandInfo as ExternalScriptInfo;45 if (si != null)46 {47 _scriptBlock = si.ScriptBlock;48 _commandName = si.Path;49 }50 }51 52 _commandMetadata = commandInfo.CommandMetadata;53 _parameterDescriptions = parameterDescriptions;54 }55 56 private readonly Language.CommentHelpInfo _sections = new Language.CommentHelpInfo();57 private readonly Dictionary<string, string> _parameters = new Dictionary<string, string>();58 private readonly List<string> _examples = new List<string>();59 private readonly List<string> _inputs = new List<string>();60 private readonly List<string> _outputs = new List<string>();61 private readonly List<string> _links = new List<string>();62 internal bool isExternalHelpSet = false;63 64 private readonly ScriptBlock _scriptBlock;65 private readonly CommandMetadata _commandMetadata;66 private readonly string _commandName;67 private readonly List<string> _parameterDescriptions;68 private XmlDocument _doc;69 internal static readonly string mshURI = "http://msh";70 internal static readonly string mamlURI = "http://schemas.microsoft.com/maml/2004/10";71 internal static readonly string commandURI = "http://schemas.microsoft.com/maml/dev/command/2004/10";72 internal static readonly string devURI = "http://schemas.microsoft.com/maml/dev/2004/10";73 74 private const string directive = @"^\s*\.(\w+)(\s+(\S.*))?\s*$";75 private const string blankline = @"^\s*$";76 // Although "http://msh" is the default namespace, it still must be explicitly qualified with non-empty prefix,77 // because XPath 1.0 will associate empty prefix with "null" namespace (not with "default") and query will fail.78 // See: http://www.w3.org/TR/1999/REC-xpath-19991116/#node-tests79 internal static readonly string ProviderHelpCommandXPath =80 "/msh:helpItems/msh:providerHelp/msh:CmdletHelpPaths/msh:CmdletHelpPath{0}/command:command[command:details/command:verb='{1}' and command:details/command:noun='{2}']";81 82 private void DetermineParameterDescriptions()83 {84 int i = 0;85 foreach (string parameterName in _commandMetadata.StaticCommandParameterMetadata.BindableParameters.Keys)86 {87 string description;88 if (!_parameters.TryGetValue(parameterName.ToUpperInvariant(), out description))89 {90 if (i < _parameterDescriptions.Count)91 {92 _parameters.Add(parameterName.ToUpperInvariant(), _parameterDescriptions[i]);93 }94 }95 96 ++i;97 }98 }99 100 private string GetParameterDescription(string parameterName)101 {102 Diagnostics.Assert(!string.IsNullOrEmpty(parameterName), "Parameter name must not be empty");103 104 string description;105 _parameters.TryGetValue(parameterName.ToUpperInvariant(), out description);106 return description;107 }108 109 private XmlElement BuildXmlForParameter(110 string parameterName,111 bool isMandatory,112 bool valueFromPipeline,113 bool valueFromPipelineByPropertyName,114 string position,115 Type type,116 string description,117 bool supportsWildcards,118 string defaultValue,119 bool forSyntax)120 {121 XmlElement command_parameter = _doc.CreateElement("command:parameter", commandURI);122 command_parameter.SetAttribute("required", isMandatory ? "true" : "false");123 // command_parameter.SetAttribute("variableLength", "unknown");124 command_parameter.SetAttribute("globbing", supportsWildcards ? "true" : "false");125 string fromPipeline;126 if (valueFromPipeline && valueFromPipelineByPropertyName)127 {128 fromPipeline = "true (ByValue, ByPropertyName)";129 }130 else if (valueFromPipeline)131 {132 fromPipeline = "true (ByValue)";133 }134 else if (valueFromPipelineByPropertyName)135 {136 fromPipeline = "true (ByPropertyName)";137 }138 else139 {140 fromPipeline = "false";141 }142 143 command_parameter.SetAttribute("pipelineInput", fromPipeline);144 command_parameter.SetAttribute("position", position);145 146 XmlElement name = _doc.CreateElement("maml:name", mamlURI);147 XmlText name_text = _doc.CreateTextNode(parameterName);148 command_parameter.AppendChild(name).AppendChild(name_text);149 if (!string.IsNullOrEmpty(description))150 {151 XmlElement maml_description = _doc.CreateElement("maml:description", mamlURI);152 XmlElement maml_para = _doc.CreateElement("maml:para", mamlURI);153 XmlText maml_para_text = _doc.CreateTextNode(description);154 command_parameter.AppendChild(maml_description).AppendChild(maml_para).AppendChild(maml_para_text);155 }156 157 if (type == null)158 type = typeof(object);159 160 var elementType = type.IsArray ? type.GetElementType() : type;161 162 if (elementType.IsEnum)163 {164 XmlElement parameterValueGroup = _doc.CreateElement("command:parameterValueGroup", commandURI);165 foreach (string valueName in Enum.GetNames(elementType))166 {167 XmlElement parameterValue = _doc.CreateElement("command:parameterValue", commandURI);168 parameterValue.SetAttribute("required", "false");169 XmlText parameterValue_text = _doc.CreateTextNode(valueName);170 parameterValueGroup.AppendChild(parameterValue).AppendChild(parameterValue_text);171 }172 173 command_parameter.AppendChild(parameterValueGroup);174 }175 else176 {177 bool isSwitchParameter = elementType == typeof(SwitchParameter);178 if (!forSyntax || !isSwitchParameter)179 {180 XmlElement parameterValue = _doc.CreateElement("command:parameterValue", commandURI);181 parameterValue.SetAttribute("required", isSwitchParameter ? "false" : "true");182 // parameterValue.SetAttribute("variableLength", "unknown");183 XmlText parameterValue_text = _doc.CreateTextNode(type.Name);184 command_parameter.AppendChild(parameterValue).AppendChild(parameterValue_text);185 }186 }187 188 if (!forSyntax)189 {190 XmlElement devType = _doc.CreateElement("dev:type", devURI);191 XmlElement typeName = _doc.CreateElement("maml:name", mamlURI);192 XmlText typeName_text = _doc.CreateTextNode(type.Name);193 command_parameter.AppendChild(devType).AppendChild(typeName).AppendChild(typeName_text);194 195 XmlElement defaultValueElement = _doc.CreateElement("dev:defaultValue", devURI);196 XmlText defaultValue_text = _doc.CreateTextNode(defaultValue);197 command_parameter.AppendChild(defaultValueElement).AppendChild(defaultValue_text);198 }199 200 return command_parameter;201 }202 203 /// <summary>204 /// Create the maml xml after a successful analysis of the comments.205 /// </summary>206 /// <returns>The xml node for the command constructed.</returns>207 internal XmlDocument BuildXmlFromComments()208 {209 Diagnostics.Assert(!string.IsNullOrEmpty(_commandName), "Name can never be null");210 211 _doc = new XmlDocument();212 XmlElement command = _doc.CreateElement("command:command", commandURI);213 command.SetAttribute("xmlns:maml", mamlURI);214 command.SetAttribute("xmlns:command", commandURI);215 command.SetAttribute("xmlns:dev", devURI);216 _doc.AppendChild(command);217 218 XmlElement details = _doc.CreateElement("command:details", commandURI);219 command.AppendChild(details);220 221 XmlElement name = _doc.CreateElement("command:name", commandURI);222 XmlText name_text = _doc.CreateTextNode(_commandName);223 details.AppendChild(name).AppendChild(name_text);224 225 if (!string.IsNullOrEmpty(_sections.Synopsis))226 {227 XmlElement synopsis = _doc.CreateElement("maml:description", mamlURI);228 XmlElement synopsis_para = _doc.CreateElement("maml:para", mamlURI);229 XmlText synopsis_text = _doc.CreateTextNode(_sections.Synopsis);230 details.AppendChild(synopsis).AppendChild(synopsis_para).AppendChild(synopsis_text);231 }232 233 #region Syntax234 235 // The syntax is automatically generated from parameter metadata236 DetermineParameterDescriptions();237 238 XmlElement syntax = _doc.CreateElement("command:syntax", commandURI);239 MergedCommandParameterMetadata parameterMetadata = _commandMetadata.StaticCommandParameterMetadata;240 if (parameterMetadata.ParameterSetCount > 0)241 {242 for (int i = 0; i < parameterMetadata.ParameterSetCount; ++i)243 {244 BuildSyntaxForParameterSet(command, syntax, parameterMetadata, i);245 }246 }247 else248 {249 BuildSyntaxForParameterSet(command, syntax, parameterMetadata, int.MaxValue);250 }251 252 #endregion Syntax253 254 #region Parameters255 256 XmlElement commandParameters = _doc.CreateElement("command:parameters", commandURI);257 foreach (KeyValuePair<string, MergedCompiledCommandParameter> pair in parameterMetadata.BindableParameters)258 {259 MergedCompiledCommandParameter mergedParameter = pair.Value;260 if (mergedParameter.BinderAssociation == ParameterBinderAssociation.CommonParameters)261 {262 continue;263 }264 265 string parameterName = pair.Key;266 string description = GetParameterDescription(parameterName);267 268 ParameterSetSpecificMetadata parameterSetData;269 bool isMandatory = false;270 bool valueFromPipeline = false;271 bool valueFromPipelineByPropertyName = false;272 string position = "named";273 int i = 0;274 275 CompiledCommandParameter parameter = mergedParameter.Parameter;276 parameter.ParameterSetData.TryGetValue(ParameterAttribute.AllParameterSets, out parameterSetData);277 while (parameterSetData == null && i < 32)278 {279 parameterSetData = parameter.GetParameterSetData(1u << i++);280 }281 282 if (parameterSetData != null)283 {284 isMandatory = parameterSetData.IsMandatory;285 valueFromPipeline = parameterSetData.ValueFromPipeline;286 valueFromPipelineByPropertyName = parameterSetData.ValueFromPipelineByPropertyName;287 position = parameterSetData.IsPositional ? (1 + parameterSetData.Position).ToString(CultureInfo.InvariantCulture) : "named";288 }289 290 var compiledAttributes = parameter.CompiledAttributes;291 bool supportsWildcards = compiledAttributes.OfType<SupportsWildcardsAttribute>().Any();292 293 string defaultValueStr = string.Empty;294 object defaultValue = null;295 var defaultValueAttribute = compiledAttributes.OfType<PSDefaultValueAttribute>().FirstOrDefault();296 if (defaultValueAttribute != null)297 {298 defaultValueStr = defaultValueAttribute.Help;299 if (string.IsNullOrEmpty(defaultValueStr))300 {301 defaultValue = defaultValueAttribute.Value;302 }303 }304 305 if (string.IsNullOrEmpty(defaultValueStr))306 {307 if (defaultValue == null)308 {309 RuntimeDefinedParameter rdp;310 if (_scriptBlock.RuntimeDefinedParameters.TryGetValue(parameterName, out rdp))311 {312 defaultValue = rdp.Value;313 }314 }315 316 var wrapper = defaultValue as Compiler.DefaultValueExpressionWrapper;317 if (wrapper != null)318 {319 defaultValueStr = wrapper.Expression.Extent.Text;320 }321 else if (defaultValue != null)322 {323 defaultValueStr = PSObject.ToStringParser(null, defaultValue);324 }325 }326 327 XmlElement parameterElement = BuildXmlForParameter(parameterName, isMandatory,328 valueFromPipeline, valueFromPipelineByPropertyName, position,329 parameter.Type, description, supportsWildcards, defaultValueStr, forSyntax: false);330 commandParameters.AppendChild(parameterElement);331 }332 333 command.AppendChild(commandParameters);334 335 #endregion Parameters336 337 if (!string.IsNullOrEmpty(_sections.Description))338 {339 XmlElement description = _doc.CreateElement("maml:description", mamlURI);340 XmlElement description_para = _doc.CreateElement("maml:para", mamlURI);341 XmlText description_text = _doc.CreateTextNode(_sections.Description);342 command.AppendChild(description).AppendChild(description_para).AppendChild(description_text);343 }344 345 if (!string.IsNullOrEmpty(_sections.Notes))346 {347 XmlElement alertSet = _doc.CreateElement("maml:alertSet", mamlURI);348 XmlElement alert = _doc.CreateElement("maml:alert", mamlURI);349 XmlElement alert_para = _doc.CreateElement("maml:para", mamlURI);350 XmlText alert_para_text = _doc.CreateTextNode(_sections.Notes);351 command.AppendChild(alertSet).AppendChild(alert).AppendChild(alert_para).AppendChild(alert_para_text);352 }353 354 if (_examples.Count > 0)355 {356 XmlElement examples = _doc.CreateElement("command:examples", commandURI);357 int count = 1;358 foreach (string example in _examples)359 {360 XmlElement example_node = _doc.CreateElement("command:example", commandURI);361 362 // The title is automatically generated363 XmlElement title = _doc.CreateElement("maml:title", mamlURI);364 string titleStr = string.Format(CultureInfo.InvariantCulture,365 "\t\t\t\t-------------------------- {0} {1} --------------------------",366 HelpDisplayStrings.ExampleUpperCase, count++);367 XmlText title_text = _doc.CreateTextNode(titleStr);368 example_node.AppendChild(title).AppendChild(title_text);369 370 string prompt_str;371 string code_str;372 string remarks_str;373 GetExampleSections(example, out prompt_str, out code_str, out remarks_str);374 375 // Introduction (usually the prompt)376 XmlElement introduction = _doc.CreateElement("maml:introduction", mamlURI);377 XmlElement introduction_para = _doc.CreateElement("maml:para", mamlURI);378 XmlText introduction_para_text = _doc.CreateTextNode(prompt_str);379 example_node.AppendChild(introduction).AppendChild(introduction_para).AppendChild(introduction_para_text);380 381 // Example code382 XmlElement code = _doc.CreateElement("dev:code", devURI);383 XmlText code_text = _doc.CreateTextNode(code_str);384 example_node.AppendChild(code).AppendChild(code_text);385 386 // Remarks are comments on the example387 XmlElement remarks = _doc.CreateElement("dev:remarks", devURI);388 XmlElement remarks_para = _doc.CreateElement("maml:para", mamlURI);389 XmlText remarks_para_text = _doc.CreateTextNode(remarks_str);390 example_node.AppendChild(remarks).AppendChild(remarks_para).AppendChild(remarks_para_text);391 // The convention is to have 4 blank paras after the example for spacing392 for (int i = 0; i < 4; i++)393 {394 remarks.AppendChild(_doc.CreateElement("maml:para", mamlURI));395 }396 397 examples.AppendChild(example_node);398 }399 400 command.AppendChild(examples);401 }402 403 if (_inputs.Count > 0)404 {405 XmlElement inputTypes = _doc.CreateElement("command:inputTypes", commandURI);406 foreach (string inputStr in _inputs)407 {408 XmlElement inputType = _doc.CreateElement("command:inputType", commandURI);409 XmlElement type = _doc.CreateElement("dev:type", devURI);410 XmlElement maml_name = _doc.CreateElement("maml:name", mamlURI);411 XmlText maml_name_text = _doc.CreateTextNode(inputStr);412 inputTypes.AppendChild(inputType).AppendChild(type).AppendChild(maml_name).AppendChild(maml_name_text);413 }414 415 command.AppendChild(inputTypes);416 }417 // For outputs, we prefer what was specified in the comments, but if there are no comments418 // and the OutputType attribute was specified, we'll use those instead.419 IEnumerable outputs = null;420 if (_outputs.Count > 0)421 {422 outputs = _outputs;423 }424 else if (_scriptBlock.OutputType.Count > 0)425 {426 outputs = _scriptBlock.OutputType;427 }428 429 if (outputs != null)430 {431 XmlElement returnValues = _doc.CreateElement("command:returnValues", commandURI);432 foreach (object output in outputs)433 {434 XmlElement returnValue = _doc.CreateElement("command:returnValue", commandURI);435 XmlElement type = _doc.CreateElement("dev:type", devURI);436 XmlElement maml_name = _doc.CreateElement("maml:name", mamlURI);437 string returnValueStr = output as string ?? ((PSTypeName)output).Name;438 XmlText maml_name_text = _doc.CreateTextNode(returnValueStr);439 returnValues.AppendChild(returnValue).AppendChild(type).AppendChild(maml_name).AppendChild(maml_name_text);440 }441 442 command.AppendChild(returnValues);443 }444 445 if (_links.Count > 0)446 {447 XmlElement links = _doc.CreateElement("maml:relatedLinks", mamlURI);448 foreach (string link in _links)449 {450 XmlElement navigationLink = _doc.CreateElement("maml:navigationLink", mamlURI);451 bool isOnlineHelp = Uri.IsWellFormedUriString(link, UriKind.Absolute);452 string nodeName = isOnlineHelp ? "maml:uri" : "maml:linkText";453 XmlElement linkText = _doc.CreateElement(nodeName, mamlURI);454 XmlText linkText_text = _doc.CreateTextNode(link);455 links.AppendChild(navigationLink).AppendChild(linkText).AppendChild(linkText_text);456 }457 458 command.AppendChild(links);459 }460 461 return _doc;462 }463 464 private void BuildSyntaxForParameterSet(XmlElement command, XmlElement syntax, MergedCommandParameterMetadata parameterMetadata, int i)465 {466 XmlElement syntaxItem = _doc.CreateElement("command:syntaxItem", commandURI);467 XmlElement syntaxItemName = _doc.CreateElement("maml:name", mamlURI);468 XmlText syntaxItemName_text = _doc.CreateTextNode(_commandName);469 470 syntaxItem.AppendChild(syntaxItemName).AppendChild(syntaxItemName_text);471 472 Collection<MergedCompiledCommandParameter> compiledParameters =473 parameterMetadata.GetParametersInParameterSet(1u << i);474 475 foreach (MergedCompiledCommandParameter mergedParameter in compiledParameters)476 {477 if (mergedParameter.BinderAssociation == ParameterBinderAssociation.CommonParameters)478 {479 continue;480 }481 482 CompiledCommandParameter parameter = mergedParameter.Parameter;483 ParameterSetSpecificMetadata parameterSetData = parameter.GetParameterSetData(1u << i);484 string description = GetParameterDescription(parameter.Name);485 bool supportsWildcards = parameter.CompiledAttributes.Any(static attribute => attribute is SupportsWildcardsAttribute);486 XmlElement parameterElement = BuildXmlForParameter(parameter.Name,487 parameterSetData.IsMandatory, parameterSetData.ValueFromPipeline,488 parameterSetData.ValueFromPipelineByPropertyName,489 parameterSetData.IsPositional ? (1 + parameterSetData.Position).ToString(CultureInfo.InvariantCulture) : "named",490 parameter.Type, description, supportsWildcards, defaultValue: string.Empty, forSyntax: true);491 syntaxItem.AppendChild(parameterElement);492 }493 494 command.AppendChild(syntax).AppendChild(syntaxItem);495 }496 497 private static void GetExampleSections(string content, out string prompt_str, out string code_str, out string remarks_str)498 {499 const string default_prompt_str = "PS > ";500 501 var promptMatch = Regex.Match(content, "^.*?>");502 prompt_str = promptMatch.Success ? promptMatch.Value : default_prompt_str;503 if (promptMatch.Success)504 {505 content = content.Substring(prompt_str.Length);506 }507 508 var codeAndRemarksMatch = Regex.Match(content, "^(?<code>.*?)\r?\n\r?\n(?<remarks>.*)$", RegexOptions.Singleline);509 if (codeAndRemarksMatch.Success)510 {511 code_str = codeAndRemarksMatch.Groups["code"].Value.Trim();512 remarks_str = codeAndRemarksMatch.Groups["remarks"].Value;513 }514 else515 {516 code_str = content.Trim();517 remarks_str = string.Empty;518 }519 }520 521 /// <summary>522 /// Split the text in the comment token into multiple lines, appending commentLines.523 /// </summary>524 /// <param name="comment">A single line or multiline comment token.</param>525 /// <param name="commentLines"></param>526 private static void CollectCommentText(Token comment, List<string> commentLines)527 {528 string text = comment.Text;529 CollectCommentText(text, commentLines);530 }531 532 private static void CollectCommentText(string text, List<string> commentLines)533 {534 int i = 0;535 if (text[0] == '<')536 {537 int start = 2;538 // The full text includes '<#', so start at index 2 to skip those characters,539 // and the full text also includes '#>' at the end, so skip those as well.540 for (i = 2; i < text.Length - 2; i++)541 {542 if (text[i] == '\n')543 {544 commentLines.Add(text.Substring(start, i - start));545 start = i + 1;546 }547 else if (text[i] == '\r')548 {549 commentLines.Add(text.Substring(start, i - start));550 551 // No need to check length here, comment text has at least '#>' at the end.552 if (text[i + 1] == '\n')553 {554 i++;555 }556 557 start = i + 1;558 }559 }560 561 commentLines.Add(text.Substring(start, i - start));562 }563 else564 {565 for (; i < text.Length; i++)566 {567 // Skip all leading '#' characters as it is a common convention568 // to use more than one '#' character.569 if (text[i] != '#')570 {571 break;572 }573 }574 575 commentLines.Add(text.Substring(i));576 }577 }578 579 /// <summary>580 /// Collect the text of a section. Stop collecting the section581 /// when a new directive is found (even if it is an unknown directive).582 /// </summary>583 /// <param name="commentLines">The comment block, as a list of lines.</param>584 /// <param name="i"></param>585 /// <returns>The text of the help section, with 'i' left on the last line collected.</returns>586 private static string GetSection(List<string> commentLines, ref int i)587 {588 bool capturing = false;589 int countLeadingWS = 0;590 StringBuilder sb = new StringBuilder();591 const char nbsp = (char)0xA0;592 593 for (i++; i < commentLines.Count; i++)594 {595 string line = commentLines[i];596 if (!capturing && Regex.IsMatch(line, blankline))597 {598 // Skip blank lines before capturing anything in the section.599 continue;600 }601 602 if (Regex.IsMatch(line, directive))603 {604 // Break on any directive even if we haven't started capturing.605 i--;606 break;607 }608 609 // The first line of a section sets how much whitespace we'll ignore (and hence strip).610 if (!capturing)611 {612 int j = 0;613 while (j < line.Length && (line[j] == ' ' || line[j] == '\t' || line[j] == nbsp))614 {615 countLeadingWS++;616 j++;617 }618 }619 620 capturing = true;621 622 // Skip leading whitespace based on the first line in the section, skipping623 // only as much whitespace as the first line had, no more (and possibly less.)624 int start = 0;625 while (start < line.Length && start < countLeadingWS &&626 (line[start] == ' ' || line[start] == '\t' || line[start] == nbsp))627 {628 start++;629 }630 631 sb.Append(line.AsSpan(start));632 sb.Append('\n');633 }634 635 return sb.ToString();636 }637 638 internal string GetHelpFile(CommandInfo commandInfo)639 {640 if (_sections.MamlHelpFile == null)641 {642 return null;643 }644 645 string helpFileToLoad = _sections.MamlHelpFile;646 Collection<string> searchPaths = new Collection<string>();647 string scriptFile = ((IScriptCommandInfo)commandInfo).ScriptBlock.File;648 if (!string.IsNullOrEmpty(scriptFile))649 {650 helpFileToLoad = Path.Combine(Path.GetDirectoryName(scriptFile), _sections.MamlHelpFile);651 }652 else if (commandInfo.Module != null)653 {654 helpFileToLoad = Path.Combine(Path.GetDirectoryName(commandInfo.Module.Path), _sections.MamlHelpFile);655 }656 657 string location = MUIFileSearcher.LocateFile(helpFileToLoad, searchPaths);658 659 return location;660 }661 662 internal RemoteHelpInfo GetRemoteHelpInfo(ExecutionContext context, CommandInfo commandInfo)663 {664 if (string.IsNullOrEmpty(_sections.ForwardHelpTargetName) || string.IsNullOrEmpty(_sections.RemoteHelpRunspace))665 {666 return null;667 }668 669 // get the PSSession object from the variable specified in the comments670 IScriptCommandInfo scriptCommandInfo = (IScriptCommandInfo)commandInfo;671 SessionState sessionState = scriptCommandInfo.ScriptBlock.SessionState;672 object runspaceInfoAsObject = sessionState.PSVariable.GetValue(_sections.RemoteHelpRunspace);673 PSSession runspaceInfo;674 if (runspaceInfoAsObject == null ||675 !LanguagePrimitives.TryConvertTo(runspaceInfoAsObject, out runspaceInfo))676 {677 string errorMessage = HelpErrors.RemoteRunspaceNotAvailable;678 throw new InvalidOperationException(errorMessage);679 }680 681 return new RemoteHelpInfo(682 context,683 (RemoteRunspace)runspaceInfo.Runspace,684 commandInfo.Name,685 _sections.ForwardHelpTargetName,686 _sections.ForwardHelpCategory,687 commandInfo.HelpCategory);688 }689 690 /// <summary>691 /// Look for special comments indicating the comment block is meant692 /// to be used for help.693 /// </summary>694 /// <param name="comments">The list of comments to process.</param>695 /// <returns>True if any special comments are found, false otherwise.</returns>696 internal bool AnalyzeCommentBlock(List<Token> comments)697 {698 if (comments == null || comments.Count == 0)699 {700 return false;701 }702 703 List<string> commentLines = new List<string>();704 foreach (Token comment in comments)705 {706 CollectCommentText(comment, commentLines);707 }708 709 return AnalyzeCommentBlock(commentLines);710 }711 712 private bool AnalyzeCommentBlock(List<string> commentLines)713 {714 bool directiveFound = false;715 for (int i = 0; i < commentLines.Count; i++)716 {717 Match match = Regex.Match(commentLines[i], directive);718 if (match.Success)719 {720 directiveFound = true;721 722 if (match.Groups[3].Success)723 {724 switch (match.Groups[1].Value.ToUpperInvariant())725 {726 case "PARAMETER":727 {728 string param = match.Groups[3].Value.ToUpperInvariant().Trim();729 string section = GetSection(commentLines, ref i);730 if (!_parameters.ContainsKey(param))731 {732 _parameters.Add(param, section);733 }734 735 break;736 }737 case "FORWARDHELPTARGETNAME":738 _sections.ForwardHelpTargetName = match.Groups[3].Value.Trim();739 break;740 case "FORWARDHELPCATEGORY":741 _sections.ForwardHelpCategory = match.Groups[3].Value.Trim();742 break;743 case "REMOTEHELPRUNSPACE":744 _sections.RemoteHelpRunspace = match.Groups[3].Value.Trim();745 break;746 case "EXTERNALHELP":747 _sections.MamlHelpFile = match.Groups[3].Value.Trim();748 isExternalHelpSet = true;749 break;750 default:751 return false;752 }753 }754 else755 {756 switch (match.Groups[1].Value.ToUpperInvariant())757 {758 case "SYNOPSIS":759 _sections.Synopsis = GetSection(commentLines, ref i);760 break;761 case "DESCRIPTION":762 _sections.Description = GetSection(commentLines, ref i);763 break;764 case "NOTES":765 _sections.Notes = GetSection(commentLines, ref i);766 break;767 case "LINK":768 _links.Add(GetSection(commentLines, ref i).Trim());769 break;770 case "EXAMPLE":771 _examples.Add(GetSection(commentLines, ref i));772 break;773 case "INPUTS":774 _inputs.Add(GetSection(commentLines, ref i));775 break;776 case "OUTPUTS":777 _outputs.Add(GetSection(commentLines, ref i));778 break;779 case "COMPONENT":780 _sections.Component = GetSection(commentLines, ref i).Trim();781 break;782 case "ROLE":783 _sections.Role = GetSection(commentLines, ref i).Trim();784 break;785 case "FUNCTIONALITY":786 _sections.Functionality = GetSection(commentLines, ref i).Trim();787 break;788 default:789 return false;790 }791 }792 }793 else if (!Regex.IsMatch(commentLines[i], blankline))794 {795 return false;796 }797 }798 799 _sections.Examples = new ReadOnlyCollection<string>(_examples);800 _sections.Inputs = new ReadOnlyCollection<string>(_inputs);801 _sections.Outputs = new ReadOnlyCollection<string>(_outputs);802 _sections.Links = new ReadOnlyCollection<string>(_links);803 // TODO, Changing this to an IDictionary because ReadOnlyDictionary is available only in .NET 4.5804 // This is a temporary workaround and will be fixed later. Tracked by Win8: 354135805 _sections.Parameters = new Dictionary<string, string>(_parameters);806 807 return directiveFound;808 }809 810 /// <summary>811 /// The analysis of the comments finds the component, functionality, and role fields, but812 /// those fields aren't added to the xml because they aren't children of the command xml813 /// node, they are under a sibling of the command xml node and apply to all command nodes814 /// in a maml file.815 /// </summary>816 /// <param name="helpInfo">The helpInfo object to set the fields on.</param>817 internal void SetAdditionalData(MamlCommandHelpInfo helpInfo)818 {819 helpInfo.SetAdditionalDataFromHelpComment(820 _sections.Component,821 _sections.Functionality,822 _sections.Role);823 }824 825 internal static CommentHelpInfo GetHelpContents(List<Language.Token> comments, List<string> parameterDescriptions)826 {827 HelpCommentsParser helpCommentsParser = new HelpCommentsParser(parameterDescriptions);828 helpCommentsParser.AnalyzeCommentBlock(comments);829 return helpCommentsParser._sections;830 }831 832 internal static HelpInfo CreateFromComments(ExecutionContext context,833 CommandInfo commandInfo,834 List<Language.Token> comments,835 List<string> parameterDescriptions,836 bool dontSearchOnRemoteComputer,837 out string helpFile, out string helpUriFromDotLink)838 {839 HelpCommentsParser helpCommentsParser = new HelpCommentsParser(commandInfo, parameterDescriptions);840 helpCommentsParser.AnalyzeCommentBlock(comments);841 842 if (helpCommentsParser._sections.Links != null && helpCommentsParser._sections.Links.Count != 0)843 {844 helpUriFromDotLink = helpCommentsParser._sections.Links[0];845 }846 else847 {848 helpUriFromDotLink = null;849 }850 851 helpFile = helpCommentsParser.GetHelpFile(commandInfo);852 853 // If only .ExternalHelp is defined and the help file is not found, then we854 // use the metadata driven help855 if (comments.Count == 1 && helpCommentsParser.isExternalHelpSet && helpFile == null)856 {857 return null;858 }859 860 return CreateFromComments(context, commandInfo, helpCommentsParser, dontSearchOnRemoteComputer);861 }862 863 internal static HelpInfo CreateFromComments(ExecutionContext context, CommandInfo commandInfo, HelpCommentsParser helpCommentsParser,864 bool dontSearchOnRemoteComputer)865 {866 if (!dontSearchOnRemoteComputer)867 {868 RemoteHelpInfo remoteHelpInfo = helpCommentsParser.GetRemoteHelpInfo(context, commandInfo);869 if (remoteHelpInfo != null)870 {871 // Add HelpUri if necessary872 if (remoteHelpInfo.GetUriForOnlineHelp() == null)873 {874 DefaultCommandHelpObjectBuilder.AddRelatedLinksProperties(remoteHelpInfo.FullHelp,875 commandInfo.CommandMetadata.HelpUri);876 }877 878 return remoteHelpInfo;879 }880 }881 882 XmlDocument doc = helpCommentsParser.BuildXmlFromComments();883 HelpCategory helpCategory = commandInfo.HelpCategory;884 MamlCommandHelpInfo localHelpInfo = MamlCommandHelpInfo.Load(doc.DocumentElement, helpCategory);885 if (localHelpInfo != null)886 {887 helpCommentsParser.SetAdditionalData(localHelpInfo);888 889 if (!string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpTargetName)890 || !string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpCategory))891 {892 if (string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpTargetName))893 {894 localHelpInfo.ForwardTarget = localHelpInfo.Name;895 }896 else897 {898 localHelpInfo.ForwardTarget = helpCommentsParser._sections.ForwardHelpTargetName;899 }900 901 if (!string.IsNullOrEmpty(helpCommentsParser._sections.ForwardHelpCategory))902 {903 try904 {905 localHelpInfo.ForwardHelpCategory = (HelpCategory)Enum.Parse(typeof(HelpCategory), helpCommentsParser._sections.ForwardHelpCategory, true);906 }907 catch (System.ArgumentException)908 {909 // Ignore conversion errors.910 }911 }912 else913 {914 localHelpInfo.ForwardHelpCategory = (HelpCategory.Alias |915 HelpCategory.Cmdlet |916 HelpCategory.ExternalScript |917 HelpCategory.Filter |918 HelpCategory.Function |919 HelpCategory.ScriptCommand);920 }921 }922 923 // Add HelpUri if necessary924 if (localHelpInfo.GetUriForOnlineHelp() == null)925 {926 DefaultCommandHelpObjectBuilder.AddRelatedLinksProperties(localHelpInfo.FullHelp, commandInfo.CommandMetadata.HelpUri);927 }928 }929 930 return localHelpInfo;931 }932 933 /// <summary>934 /// Analyze a block of comments to determine if it is a special help block.935 /// </summary>936 /// <param name="commentBlock">The block of comments to analyze.</param>937 /// <returns>True if the block is our special comment block for help, false otherwise.</returns>938 internal static bool IsCommentHelpText(List<Token> commentBlock)939 {940 if ((commentBlock == null) || (commentBlock.Count == 0))941 return false;942 943 HelpCommentsParser generator = new HelpCommentsParser();944 return generator.AnalyzeCommentBlock(commentBlock);945 }946 947 #region Collect comments from AST948 949 private static List<Language.Token> GetCommentBlock(Language.Token[] tokens, ref int startIndex)950 {951 var result = new List<Language.Token>();952 953 // Any whitespace between the token and the first comment is allowed.954 int nextMaxStartLine = Int32.MaxValue;955 956 for (int i = startIndex; i < tokens.Length; i++)957 {958 Language.Token current = tokens[i];959 960 // If the current token starts on a line beyond the current "chunk",961 // then we're done scanning.962 if (current.Extent.StartLineNumber > nextMaxStartLine)963 {964 startIndex = i;965 break;966 }967 968 if (current.Kind == TokenKind.Comment)969 {970 result.Add(current);971 972 // The next comment must be on either the same line as this comment ends, or973 // the next line, but nowhere else, otherwise it's not in the same "chunk".974 nextMaxStartLine = current.Extent.EndLineNumber + 1;975 }976 else if (current.Kind != TokenKind.NewLine)977 {978 // A non-comment, non-position token means we are no longer collecting comments979 startIndex = i;980 break;981 }982 }983 984 return result;985 }986 987 private static List<Language.Token> GetPrecedingCommentBlock(Language.Token[] tokens, int tokenIndex, int proximity)988 {989 var result = new List<Language.Token>();990 int minEndLine = tokens[tokenIndex].Extent.StartLineNumber - proximity;991 992 for (int i = tokenIndex - 1; i >= 0; i--)993 {994 Language.Token current = tokens[i];995 996 if (current.Extent.EndLineNumber < minEndLine)997 break;998 999 if (current.Kind == TokenKind.Comment)1000 {1001 result.Add(current);1002 minEndLine = current.Extent.StartLineNumber - 1;1003 }1004 else if (current.Kind != TokenKind.NewLine)1005 {1006 break;1007 }1008 }1009 1010 result.Reverse();1011 return result;1012 }1013 1014 private static int FirstTokenInExtent(Language.Token[] tokens, IScriptExtent extent, int startIndex = 0)1015 {1016 int index;1017 for (index = startIndex; index < tokens.Length; ++index)1018 {1019 if (!tokens[index].Extent.IsBefore(extent))1020 {1021 break;1022 }1023 }1024 1025 return index;1026 }1027 1028 private static int LastTokenInExtent(Language.Token[] tokens, IScriptExtent extent, int startIndex)1029 {1030 int index;1031 for (index = startIndex; index < tokens.Length; ++index)1032 {1033 if (tokens[index].Extent.IsAfter(extent))1034 {1035 break;1036 }1037 }1038 1039 return index - 1;1040 }1041 1042 internal const int CommentBlockProximity = 2;1043 1044 private static List<string> GetParameterComments(Language.Token[] tokens, IParameterMetadataProvider ipmp, int startIndex)1045 {1046 var result = new List<string>();1047 var parameters = ipmp.Parameters;1048 if (parameters == null || parameters.Count == 0)1049 {1050 return result;1051 }1052 1053 foreach (var parameter in parameters)1054 {1055 var commentLines = new List<string>();1056 1057 var firstToken = FirstTokenInExtent(tokens, parameter.Extent, startIndex);1058 var comments = GetPrecedingCommentBlock(tokens, firstToken, CommentBlockProximity);1059 if (comments != null)1060 {1061 foreach (var comment in comments)1062 {1063 CollectCommentText(comment, commentLines);1064 }1065 }1066 1067 var lastToken = LastTokenInExtent(tokens, parameter.Extent, firstToken);1068 for (int i = firstToken; i < lastToken; ++i)1069 {1070 if (tokens[i].Kind == TokenKind.Comment)1071 {1072 CollectCommentText(tokens[i], commentLines);1073 }1074 }1075 1076 lastToken += 1;1077 comments = GetCommentBlock(tokens, ref lastToken);1078 if (comments != null)1079 {1080 foreach (var comment in comments)1081 {1082 CollectCommentText(comment, commentLines);1083 }1084 }1085 1086 int n = -1;1087 result.Add(GetSection(commentLines, ref n));1088 }1089 1090 return result;1091 }1092 1093 internal static Tuple<List<Language.Token>, List<string>> GetHelpCommentTokens(IParameterMetadataProvider ipmp,1094 Dictionary<Ast, Token[]> scriptBlockTokenCache)1095 {1096 Diagnostics.Assert(scriptBlockTokenCache != null, "scriptBlockTokenCache cannot be null");1097 var ast = (Ast)ipmp;1098 1099 var rootAst = ast;1100 Ast configAst = null;1101 while (rootAst.Parent != null)1102 {1103 rootAst = rootAst.Parent;1104 if (rootAst is ConfigurationDefinitionAst)1105 {1106 configAst = rootAst;1107 }1108 }1109 1110 // tokens saved from reparsing the script.1111 Language.Token[] tokens = null;1112 scriptBlockTokenCache.TryGetValue(rootAst, out tokens);1113 1114 if (tokens == null)1115 {1116 ParseError[] errors;1117 // storing all comment tokens1118 Language.Parser.ParseInput(rootAst.Extent.Text, out tokens, out errors);1119 scriptBlockTokenCache[rootAst] = tokens;1120 }1121 1122 int savedStartIndex;1123 int startTokenIndex;1124 int lastTokenIndex;1125 1126 var funcDefnAst = ast as FunctionDefinitionAst;1127 List<Language.Token> commentBlock;1128 if (funcDefnAst != null || configAst != null)1129 {1130 // The first comment block preceding the function or configuration keyword is a candidate help comment block.1131 var funcOrConfigTokenIndex =1132 savedStartIndex = FirstTokenInExtent(tokens, configAst == null ? ast.Extent : configAst.Extent);1133 1134 commentBlock = GetPrecedingCommentBlock(tokens, funcOrConfigTokenIndex, CommentBlockProximity);1135 1136 if (HelpCommentsParser.IsCommentHelpText(commentBlock))1137 {1138 return Tuple.Create(commentBlock, GetParameterComments(tokens, ipmp, savedStartIndex));1139 }1140 1141 // comment block is behind function definition1142 // we don't support it for configuration declaration as this style is rarely used1143 if (funcDefnAst != null)1144 {1145 startTokenIndex =1146 FirstTokenInExtent(tokens, funcDefnAst.Body.Extent) + 1;1147 lastTokenIndex = LastTokenInExtent(tokens, ast.Extent, funcOrConfigTokenIndex);1148 1149 Diagnostics.Assert(tokens[startTokenIndex - 1].Kind == TokenKind.LCurly,1150 "Unexpected first token in function");1151 Diagnostics.Assert(tokens[lastTokenIndex].Kind == TokenKind.RCurly,1152 "Unexpected last token in function");1153 }1154 else1155 {1156 return null;1157 }1158 }1159 else if (ast == rootAst)1160 {1161 startTokenIndex = savedStartIndex = 0;1162 lastTokenIndex = tokens.Length - 1;1163 }1164 else1165 {1166 // This case should be rare (but common with implicit remoting).1167 // We have a script block that was used to generate a function like:1168 // $sb = { }1169 // set-item function:foo $sb1170 // help foo1171 startTokenIndex = savedStartIndex = FirstTokenInExtent(tokens, ast.Extent) + 1;1172 lastTokenIndex = LastTokenInExtent(tokens, ast.Extent, startTokenIndex);1173 1174 Diagnostics.Assert(tokens[startTokenIndex - 1].Kind == TokenKind.LCurly,1175 "Unexpected first token in script block");1176 Diagnostics.Assert(tokens[lastTokenIndex].Kind == TokenKind.RCurly,1177 "Unexpected last token in script block");1178 }1179 1180 while (true)1181 {1182 commentBlock = GetCommentBlock(tokens, ref startTokenIndex);1183 if (commentBlock.Count == 0)1184 break;1185 1186 if (!HelpCommentsParser.IsCommentHelpText(commentBlock))1187 continue;1188 1189 if (ast == rootAst)1190 {1191 // One more check - make sure the comment doesn't belong to the first function in the script.1192 var endBlock = ((ScriptBlockAst)ast).EndBlock;1193 if (endBlock == null || !endBlock.Unnamed)1194 {1195 return Tuple.Create(commentBlock, GetParameterComments(tokens, ipmp, savedStartIndex));1196 }1197 1198 var firstStatement = endBlock.Statements.FirstOrDefault();1199 if (firstStatement is FunctionDefinitionAst)1200 {