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.Diagnostics;8using System.Diagnostics.CodeAnalysis;9using System.Globalization;10using System.IO;11using System.Management.Automation.Internal;12using System.Management.Automation.Runspaces;13using System.Xml;14 15using Dbg = System.Management.Automation.Diagnostics;16using System.Management.Automation.Help;17using System.Reflection;18 19namespace System.Management.Automation20{21 /// <summary>22 /// Class CommandHelpProvider implement the help provider for commands.23 /// </summary>24 /// <remarks>25 /// Command Help information are stored in 'help.xml' files. Location of these files26 /// can be found from through the engine execution context.27 /// </remarks>28 internal class CommandHelpProvider : HelpProviderWithCache29 {30 /// <summary>31 /// Constructor for CommandHelpProvider.32 /// </summary>33 internal CommandHelpProvider(HelpSystem helpSystem) : base(helpSystem)34 {35 _context = helpSystem.ExecutionContext;36 }37 38 /// <summary>39 /// </summary>40 static CommandHelpProvider()41 {42 s_engineModuleHelpFileCache.Add("Microsoft.PowerShell.Diagnostics", "Microsoft.PowerShell.Commands.Diagnostics.dll-Help.xml");43 s_engineModuleHelpFileCache.Add("Microsoft.PowerShell.Core", "System.Management.Automation.dll-Help.xml");44 s_engineModuleHelpFileCache.Add("Microsoft.PowerShell.Utility", "Microsoft.PowerShell.Commands.Utility.dll-Help.xml");45 s_engineModuleHelpFileCache.Add("Microsoft.PowerShell.Host", "Microsoft.PowerShell.ConsoleHost.dll-Help.xml");46 s_engineModuleHelpFileCache.Add("Microsoft.PowerShell.Management", "Microsoft.PowerShell.Commands.Management.dll-Help.xml");47 s_engineModuleHelpFileCache.Add("Microsoft.PowerShell.Security", "Microsoft.PowerShell.Security.dll-Help.xml");48 s_engineModuleHelpFileCache.Add("Microsoft.WSMan.Management", "Microsoft.Wsman.Management.dll-Help.xml");49 }50 51 private static readonly Dictionary<string, string> s_engineModuleHelpFileCache = new Dictionary<string, string>();52 53 private readonly ExecutionContext _context;54 55 #region Common Properties56 57 /// <summary>58 /// Name of this provider.59 /// </summary>60 /// <value>Name of this provider</value>61 internal override string Name62 {63 get64 {65 return "Command Help Provider";66 }67 }68 69 /// <summary>70 /// Help category for this provider, which is a constant: HelpCategory.Command.71 /// </summary>72 /// <value>Help category for this provider</value>73 internal override HelpCategory HelpCategory74 {75 get76 {77 return78 HelpCategory.Alias |79 HelpCategory.Cmdlet;80 }81 }82 83 #endregion84 85 #region Help Provider Interface86 87 private static void GetModulePaths(CommandInfo commandInfo, out string moduleName, out string moduleDir, out string nestedModulePath)88 {89 Dbg.Assert(commandInfo != null, "Caller should verify that commandInfo != null");90 91 CmdletInfo cmdletInfo = commandInfo as CmdletInfo;92 IScriptCommandInfo scriptCommandInfo = commandInfo as IScriptCommandInfo;93 94 string cmdNameWithoutPrefix = null;95 bool testWithoutPrefix = false;96 97 moduleName = null;98 moduleDir = null;99 nestedModulePath = null;100 101 if (commandInfo.Module != null)102 {103 moduleName = commandInfo.Module.Name;104 moduleDir = commandInfo.Module.ModuleBase;105 106 if (!string.IsNullOrEmpty(commandInfo.Prefix))107 {108 testWithoutPrefix = true;109 cmdNameWithoutPrefix = Microsoft.PowerShell.Commands.ModuleCmdletBase.RemovePrefixFromCommandName(commandInfo.Name, commandInfo.Prefix);110 }111 112 if (commandInfo.Module.NestedModules != null)113 {114 foreach (PSModuleInfo nestedModule in commandInfo.Module.NestedModules)115 {116 if (cmdletInfo != null &&117 (nestedModule.ExportedCmdlets.ContainsKey(commandInfo.Name) ||118 (testWithoutPrefix && nestedModule.ExportedCmdlets.ContainsKey(cmdNameWithoutPrefix))))119 {120 nestedModulePath = nestedModule.Path;121 break;122 }123 else if (scriptCommandInfo != null &&124 (nestedModule.ExportedFunctions.ContainsKey(commandInfo.Name) ||125 (testWithoutPrefix && nestedModule.ExportedFunctions.ContainsKey(cmdNameWithoutPrefix))))126 {127 nestedModulePath = nestedModule.Path;128 break;129 }130 }131 }132 }133 }134 135 private static string GetHelpName(CommandInfo commandInfo)136 {137 Dbg.Assert(commandInfo != null, "Caller should verify that commandInfo != null");138 139 CmdletInfo cmdletInfo = commandInfo as CmdletInfo;140 141 if (cmdletInfo != null)142 {143 return cmdletInfo.FullName;144 }145 146 return commandInfo.Name;147 }148 149 private HelpInfo GetHelpInfoFromHelpFile(CommandInfo commandInfo, string helpFileToFind, Collection<string> searchPaths, bool reportErrors, out string helpFile)150 {151 Dbg.Assert(commandInfo != null, "Caller should verify that commandInfo != null");152 Dbg.Assert(helpFileToFind != null, "Caller should verify that helpFileToFind != null");153 154 CmdletInfo cmdletInfo = commandInfo as CmdletInfo;155 IScriptCommandInfo scriptCommandInfo = commandInfo as IScriptCommandInfo;156 157 HelpInfo result = null;158 159 helpFile = MUIFileSearcher.LocateFile(helpFileToFind, searchPaths);160 161 if (!string.IsNullOrEmpty(helpFile))162 {163 if (!_helpFiles.Contains(helpFile))164 {165 if (cmdletInfo != null)166 {167 LoadHelpFile(helpFile, cmdletInfo.ModuleName, cmdletInfo.Name, reportErrors);168 }169 else if (scriptCommandInfo != null)170 {171 LoadHelpFile(helpFile, helpFile, commandInfo.Name, reportErrors);172 }173 }174 175 if (cmdletInfo != null)176 {177 result = GetFromCommandCacheOrCmdletInfo(cmdletInfo);178 }179 else if (scriptCommandInfo != null)180 {181 result = GetFromCommandCache(helpFile, commandInfo);182 }183 }184 185 return result;186 }187 188 [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Justification = "TestUri is created only to check for source helpUriFromDotLink errors.")]189 private HelpInfo GetHelpInfo(CommandInfo commandInfo, bool reportErrors, bool searchOnlyContent)190 {191 Dbg.Assert(commandInfo != null, "Caller should verify that commandInfo != null");192 193 HelpInfo result = null; // The help result194 string helpFile = null; // The file that contains the help info195 string helpUri = null;196 string helpUriFromDotLink = null;197 198 CmdletInfo cmdletInfo = commandInfo as CmdletInfo;199 IScriptCommandInfo scriptCommandInfo = commandInfo as IScriptCommandInfo;200 FunctionInfo functionInfo = commandInfo as FunctionInfo;201 bool isCmdlet = cmdletInfo != null;202 bool isScriptCommand = scriptCommandInfo != null;203 bool isFunction = functionInfo != null;204 205 string moduleName = null;206 string moduleDir = null;207 string nestedModulePath = null;208 209 // When InternalTestHooks.BypassOnlineHelpRetrieval is enable, we force get-help to generate a metadata210 // driven object, which includes a helpUri that points to the fwlink defined in the cmdlet code.211 // This means that we are not going to load the help content from the GetFromCommandCache and212 // we are not going to read the help file.213 214 // Only gets help for Cmdlet or script command215 if (!isCmdlet && !isScriptCommand)216 return null;217 218 // Check if the help of the command is already in the cache.219 // If not, try load the file specified by HelpFile property and retrieve help.220 if (isCmdlet && !InternalTestHooks.BypassOnlineHelpRetrieval)221 {222 result = GetFromCommandCache(cmdletInfo.ModuleName, cmdletInfo.Name, cmdletInfo.HelpCategory);223 224 if (result == null)225 {226 // Try load the help file specified by CmdletInfo.HelpFile property227 helpFile = FindHelpFile(cmdletInfo);228 if (!string.IsNullOrEmpty(helpFile) && !_helpFiles.Contains(helpFile))229 {230 LoadHelpFile(helpFile, cmdletInfo.ModuleName, cmdletInfo.Name, reportErrors);231 }232 233 result = GetFromCommandCacheOrCmdletInfo(cmdletInfo);234 }235 }236 else if (isFunction)237 {238 // Try load the help file specified by FunctionInfo.HelpFile property239 helpFile = functionInfo.HelpFile;240 if (!string.IsNullOrEmpty(helpFile))241 {242 if (!_helpFiles.Contains(helpFile))243 {244 LoadHelpFile(helpFile, helpFile, commandInfo.Name, reportErrors);245 }246 247 result = GetFromCommandCache(helpFile, commandInfo);248 }249 }250 251 // For scripts, try to retrieve the help from the file specified by .ExternalHelp directive252 if (result == null && isScriptCommand)253 {254 ScriptBlock sb = null;255 try256 {257 sb = scriptCommandInfo.ScriptBlock;258 }259 catch (RuntimeException)260 {261 // parsing errors should not block searching for help262 return null;263 }264 265 if (sb != null)266 {267 helpFile = null;268 // searchOnlyContent == true means get-help is looking into the content, in this case we dont269 // want to download the content from the remote machine. Reason: In Exchange scenario there270 // are ~700 proxy commands, downloading help for all the commands and searching in that271 // content takes a lot of time (in the order of 30 minutes) for their scenarios.272 result = sb.GetHelpInfo(_context, commandInfo, searchOnlyContent, HelpSystem.ScriptBlockTokenCache,273 out helpFile, out helpUriFromDotLink);274 275 if (!string.IsNullOrEmpty(helpUriFromDotLink))276 {277 try278 {279 Uri testUri = new Uri(helpUriFromDotLink);280 helpUri = helpUriFromDotLink;281 }282 catch (UriFormatException)283 {284 // Do not add if helpUriFromDotLink is not a URI285 }286 }287 288 if (result != null)289 {290 Uri uri = result.GetUriForOnlineHelp();291 292 if (uri != null)293 {294 helpUri = uri.ToString();295 }296 }297 298 if (!string.IsNullOrEmpty(helpFile) && !InternalTestHooks.BypassOnlineHelpRetrieval)299 {300 if (!_helpFiles.Contains(helpFile))301 {302 LoadHelpFile(helpFile, helpFile, commandInfo.Name, reportErrors);303 }304 305 result = GetFromCommandCache(helpFile, commandInfo) ?? result;306 }307 }308 }309 310 // If the above fails to get help, try search for a file called <ModuleName>-Help.xml311 // in the appropriate UI culture subfolder of ModuleBase, and retrieve help312 // If still not able to get help, try search for a file called <NestedModuleName>-Help.xml313 // under the ModuleBase and the NestedModule's directory, and retrieve help314 if (result == null && !InternalTestHooks.BypassOnlineHelpRetrieval)315 {316 // Get the name and ModuleBase directory of the command's module317 // and the nested module that implements the command318 GetModulePaths(commandInfo, out moduleName, out moduleDir, out nestedModulePath);319 320 var userHomeHelpPath = HelpUtils.GetUserHomeHelpSearchPath();321 322 Collection<string> searchPaths = new Collection<string>() { userHomeHelpPath };323 324 if (!string.IsNullOrEmpty(moduleDir))325 {326 searchPaths.Add(moduleDir);327 }328 329 if (!string.IsNullOrEmpty(userHomeHelpPath) && !string.IsNullOrEmpty(moduleName))330 {331 searchPaths.Add(Path.Combine(userHomeHelpPath, moduleName));332 }333 334 if (!string.IsNullOrEmpty(moduleName) && !string.IsNullOrEmpty(moduleDir))335 {336 // Search for <ModuleName>-Help.xml under ModuleBase folder337 string helpFileToFind = moduleName + "-Help.xml";338 result = GetHelpInfoFromHelpFile(commandInfo, helpFileToFind, searchPaths, reportErrors, out helpFile);339 }340 341 if (result == null && !string.IsNullOrEmpty(nestedModulePath))342 {343 // Search for <NestedModuleName>-Help.xml under both ModuleBase and NestedModule's directory344 searchPaths.Add(Path.GetDirectoryName(nestedModulePath));345 string helpFileToFind = Path.GetFileName(nestedModulePath) + "-Help.xml";346 result = GetHelpInfoFromHelpFile(commandInfo, helpFileToFind, searchPaths, reportErrors, out helpFile);347 }348 }349 350 // Set the HelpFile property to the file that contains the help content351 if (result != null && !string.IsNullOrEmpty(helpFile))352 {353 if (isCmdlet)354 {355 cmdletInfo.HelpFile = helpFile;356 }357 else if (isFunction)358 {359 functionInfo.HelpFile = helpFile;360 }361 }362 363 // If the above fails to get help, construct an HelpInfo object using the syntax and definition of the command364 if (result == null)365 {366 if (commandInfo.CommandType == CommandTypes.ExternalScript ||367 commandInfo.CommandType == CommandTypes.Script)368 {369 result = SyntaxHelpInfo.GetHelpInfo(commandInfo.Name, commandInfo.Syntax, commandInfo.HelpCategory);370 }371 else372 {373 PSObject helpInfo = Help.DefaultCommandHelpObjectBuilder.GetPSObjectFromCmdletInfo(commandInfo);374 375 helpInfo.TypeNames.Clear();376 helpInfo.TypeNames.Add(DefaultCommandHelpObjectBuilder.TypeNameForDefaultHelp);377 helpInfo.TypeNames.Add("CmdletHelpInfo");378 helpInfo.TypeNames.Add("HelpInfo");379 380 result = new MamlCommandHelpInfo(helpInfo, commandInfo.HelpCategory);381 }382 }383 384 if (result != null)385 {386 if (isScriptCommand && result.GetUriForOnlineHelp() == null)387 {388 if (!string.IsNullOrEmpty(commandInfo.CommandMetadata.HelpUri))389 {390 DefaultCommandHelpObjectBuilder.AddRelatedLinksProperties(result.FullHelp, commandInfo.CommandMetadata.HelpUri);391 }392 else if (!string.IsNullOrEmpty(helpUri))393 {394 DefaultCommandHelpObjectBuilder.AddRelatedLinksProperties(result.FullHelp, helpUri);395 }396 }397 398 if (isCmdlet && result.FullHelp.Properties["PSSnapIn"] == null)399 {400 result.FullHelp.Properties.Add(new PSNoteProperty("PSSnapIn", cmdletInfo.PSSnapIn));401 }402 403 if (result.FullHelp.Properties["ModuleName"] == null)404 {405 result.FullHelp.Properties.Add(new PSNoteProperty("ModuleName", commandInfo.ModuleName));406 }407 }408 409 return result;410 }411 412 /// <summary>413 /// ExactMatchHelp implementation for this help provider.414 /// </summary>415 /// <remarks>416 /// ExactMatchHelp is overridden instead of DoExactMatchHelp to make sure417 /// all help item retrieval will go through command discovery. Because each418 /// help file can contain multiple help items for different commands. Directly419 /// retrieve help cache can result in a invalid command to contain valid420 /// help item. Forcing each ExactMatchHelp to go through command discovery421 /// will make sure helpInfo for invalid command will not be returned.422 /// </remarks>423 /// <param name="helpRequest">Help request object.</param>424 /// <returns></returns>425 internal override IEnumerable<HelpInfo> ExactMatchHelp(HelpRequest helpRequest)426 {427 int countHelpInfosFound = 0;428 string target = helpRequest.Target;429 // this is for avoiding duplicate result from help output.430 var allHelpNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);431 432 CommandSearcher searcher = GetCommandSearcherForExactMatch(target, _context);433 434 while (searcher.MoveNext())435 {436 CommandInfo current = ((IEnumerator<CommandInfo>)searcher).Current;437 438 if (!SessionState.IsVisible(helpRequest.CommandOrigin, current))439 {440 // this command is not visible to the user (from CommandOrigin) so441 // dont show help topic for it.442 continue;443 }444 445 HelpInfo helpInfo = GetHelpInfo(current, true, false);446 string helpName = GetHelpName(current);447 448 if (helpInfo != null && !string.IsNullOrEmpty(helpName))449 {450 if (helpInfo.ForwardHelpCategory == helpRequest.HelpCategory &&451 helpInfo.ForwardTarget.Equals(helpRequest.Target, StringComparison.OrdinalIgnoreCase))452 {453 throw new PSInvalidOperationException(HelpErrors.CircularDependencyInHelpForwarding);454 }455 456 if (allHelpNames.Contains(helpName))457 continue;458 459 if (!Match(helpInfo, helpRequest, current))460 {461 continue;462 }463 464 countHelpInfosFound++;465 allHelpNames.Add(helpName);466 yield return helpInfo;467 468 if ((countHelpInfosFound >= helpRequest.MaxResults) && (helpRequest.MaxResults > 0))469 yield break;470 }471 }472 }473 474 private static string GetCmdletAssemblyPath(CmdletInfo cmdletInfo)475 {476 if (cmdletInfo == null)477 return null;478 479 if (cmdletInfo.ImplementingType == null)480 return null;481 482 return Path.GetDirectoryName(cmdletInfo.ImplementingType.Assembly.Location);483 }484 485 /// <summary>486 /// This is a hashtable to track which help files are loaded already.487 ///488 /// This will avoid one help file getting loaded again and again.489 /// (Which should not happen unless some commandlet is pointing490 /// to a help file that actually doesn't contain the help for it).491 /// </summary>492 private readonly Hashtable _helpFiles = new Hashtable();493 494 private string GetHelpFile(string helpFile, CmdletInfo cmdletInfo)495 {496 string helpFileToLoad = helpFile;497 498 // Get the mshsnapinfo object for this cmdlet.499 PSSnapInInfo mshSnapInInfo = cmdletInfo.PSSnapIn;500 501 // Search fallback502 // 1. If cmdletInfo.HelpFile is a full path to an existing file, directly load that file503 // 2. If PSSnapInInfo exists, then always look in the application base of the mshsnapin504 // Otherwise,505 // Look in the default search path and cmdlet assembly path506 Collection<string> searchPaths = new Collection<string>();507 508 if (!File.Exists(helpFileToLoad))509 {510 helpFileToLoad = Path.GetFileName(helpFileToLoad);511 512 if (mshSnapInInfo != null)513 {514 Diagnostics.Assert(!string.IsNullOrEmpty(mshSnapInInfo.ApplicationBase),515 "Application Base is null or empty.");516 // not minishell case..517 // we have to search only in the application base for a mshsnapin...518 // if you create an absolute path for helpfile, then MUIFileSearcher519 // will look only in that path.520 521 searchPaths.Add(HelpUtils.GetUserHomeHelpSearchPath());522 searchPaths.Add(mshSnapInInfo.ApplicationBase);523 }524 else if (cmdletInfo.Module != null && !string.IsNullOrEmpty(cmdletInfo.Module.Path) && !string.IsNullOrEmpty(cmdletInfo.Module.ModuleBase))525 {526 searchPaths.Add(HelpUtils.GetModuleBaseForUserHelp(cmdletInfo.Module.ModuleBase, cmdletInfo.Module.Name));527 searchPaths.Add(cmdletInfo.Module.ModuleBase);528 }529 else530 {531 searchPaths.Add(HelpUtils.GetUserHomeHelpSearchPath());532 searchPaths.Add(GetDefaultShellSearchPath());533 searchPaths.Add(GetCmdletAssemblyPath(cmdletInfo));534 }535 }536 else537 {538 helpFileToLoad = Path.GetFullPath(helpFileToLoad);539 }540 541 string location = MUIFileSearcher.LocateFile(helpFileToLoad, searchPaths);542 543 // let caller take care of getting help info in a different way544 // like "get-command -syntax"545 if (string.IsNullOrEmpty(location))546 {547 s_tracer.WriteLine("Unable to load file {0}", helpFileToLoad);548 }549 550 return location;551 }552 553 /// <summary>554 /// Finds a help file associated with the given cmdlet.555 /// </summary>556 /// <param name="cmdletInfo"></param>557 /// <returns></returns>558 private string FindHelpFile(CmdletInfo cmdletInfo)559 {560 if (InternalTestHooks.BypassOnlineHelpRetrieval)561 {562 // By returning null, we force get-help to generate a metadata driven help object,563 // which includes a helpUri that points to the fwlink defined in the cmdlet code.564 return null;565 }566 567 if (cmdletInfo == null)568 {569 throw PSTraceSource.NewArgumentNullException(nameof(cmdletInfo));570 }571 572 // Get the help file name from the cmdlet metadata573 string helpFile = cmdletInfo.HelpFile;574 575 if (string.IsNullOrEmpty(helpFile))576 {577 if (cmdletInfo.Module != null)578 {579 if (InitialSessionState.IsEngineModule(cmdletInfo.Module.Name))580 {581 return System.IO.Path.Combine(cmdletInfo.Module.ModuleBase, CultureInfo.CurrentCulture.Name, s_engineModuleHelpFileCache[cmdletInfo.Module.Name]);582 }583 }584 585 return helpFile;586 }587 588 // This is the path to the help file.589 string location = null;590 591 if (helpFile.EndsWith(".ni.dll-Help.xml", StringComparison.OrdinalIgnoreCase))592 {593 // For PowerShell on OneCore, we ship Ngen binaries. As a result, the name of the assembly now contains '.ni' on it,594 // e.g., <AssemblyName>.ni.dll as supposed to <AssemblyName>.dll.595 596 // When cmdlet metadata is generated for the 'HelpFile' field, we use the name assembly and we append '-Help.xml' to it.597 // Because of this, if the cmdlet is part of an Ngen assembly, then 'HelpFile' field will be pointing to a help file which does not exist.598 // If this is the case, we remove '.ni' from the help file name and try again.599 // For example:600 // Ngen assembly name: Microsoft.PowerShell.Commands.Management.ni.dll601 // Cmdlet metadata 'HelpFile': Microsoft.PowerShell.Commands.Management.ni.dll-Help.xml602 // Actual help file name: Microsoft.PowerShell.Commands.Management.dll-Help.xml603 604 // Make sure that the assembly name contains more than '.ni.dll'605 string assemblyName = helpFile.Replace(".ni.dll-Help.xml", string.Empty);606 607 if (!string.IsNullOrEmpty(assemblyName))608 {609 // In the first try, we remove '.ni' from the assembly name and we attempt to find the corresponding help file.610 string helpFileName = cmdletInfo.HelpFile.Replace(".ni.dll-Help.xml", ".dll-Help.xml");611 location = GetHelpFile(helpFileName, cmdletInfo);612 613 if (string.IsNullOrEmpty(location))614 {615 // If the help file could not be found, then it is possible that the actual assembly name is something like616 // <Name>.ni.dll, e.g., MyAssembly.ni.dll, so let's try to find the original help file in the cmdlet metadata.617 location = GetHelpFile(helpFile, cmdletInfo);618 }619 }620 else621 {622 // the assembly name is actually '.ni.dll'.623 location = GetHelpFile(helpFile, cmdletInfo);624 }625 }626 else627 {628 location = GetHelpFile(helpFile, cmdletInfo);629 }630 631 return location;632 }633 634 private void LoadHelpFile(string helpFile, string helpFileIdentifier, string commandName, bool reportErrors)635 {636 Exception e = null;637 try638 {639 LoadHelpFile(helpFile, helpFileIdentifier);640 }641 catch (IOException ioException)642 {643 e = ioException;644 }645 catch (System.Security.SecurityException securityException)646 {647 e = securityException;648 }649 catch (XmlException xmlException)650 {651 e = xmlException;652 }653 catch (NotSupportedException notSupportedException)654 {655 e = notSupportedException;656 }657 catch (UnauthorizedAccessException unauthorizedAccessException)658 {659 e = unauthorizedAccessException;660 }661 catch (InvalidOperationException invalidOperationException)662 {663 e = invalidOperationException;664 }665 666 if (reportErrors && (e != null))667 {668 ReportHelpFileError(e, commandName, helpFile);669 }670 }671 672 /// <summary>673 /// Load help file for HelpInfo objects. The HelpInfo objects will be674 /// put into help cache.675 /// </summary>676 /// <remarks>677 /// 1. Needs to pay special attention about error handling in this function.678 /// Common errors include: file not found and invalid xml. None of these error679 /// should cause help search to stop.680 /// 2. a helpfile cache is used to avoid same file got loaded again and again.681 /// </remarks>682 private void LoadHelpFile(string helpFile, string helpFileIdentifier)683 {684 XmlDocument doc = InternalDeserializer.LoadUnsafeXmlDocument(685 new FileInfo(helpFile),686 false, /* ignore whitespace, comments, etc. */687 null); /* default maxCharactersInDocument */688 689 // Add this file into _helpFiles hashtable to prevent it to be loaded again.690 _helpFiles[helpFile] = 0;691 692 XmlNode helpItemsNode = null;693 694 if (doc.HasChildNodes)695 {696 for (int i = 0; i < doc.ChildNodes.Count; i++)697 {698 XmlNode node = doc.ChildNodes[i];699 if (node.NodeType == XmlNodeType.Element && string.Equals(node.LocalName, "helpItems", StringComparison.OrdinalIgnoreCase))700 {701 helpItemsNode = node;702 break;703 }704 }705 }706 707 if (helpItemsNode == null)708 {709 s_tracer.WriteLine("Unable to find 'helpItems' element in file {0}", helpFile);710 return;711 }712 713 bool isMaml = IsMamlHelp(helpFile, helpItemsNode);714 715 using (this.HelpSystem.Trace(helpFile))716 {717 if (helpItemsNode.HasChildNodes)718 {719 for (int i = 0; i < helpItemsNode.ChildNodes.Count; i++)720 {721 XmlNode node = helpItemsNode.ChildNodes[i];722 if (node.NodeType == XmlNodeType.Element && string.Equals(node.LocalName, "command", StringComparison.OrdinalIgnoreCase))723 {724 MamlCommandHelpInfo helpInfo = null;725 726 if (isMaml)727 {728 helpInfo = MamlCommandHelpInfo.Load(node, HelpCategory.Cmdlet);729 }730 731 if (helpInfo != null)732 {733 this.HelpSystem.TraceErrors(helpInfo.Errors);734 AddToCommandCache(helpFileIdentifier, helpInfo.Name, helpInfo);735 }736 }737 738 if (node.NodeType == XmlNodeType.Element && string.Equals(node.Name, "UserDefinedData", StringComparison.OrdinalIgnoreCase))739 {740 UserDefinedHelpData userDefinedHelpData = UserDefinedHelpData.Load(node);741 742 ProcessUserDefinedHelpData(helpFileIdentifier, userDefinedHelpData);743 }744 }745 }746 }747 }748 749 /// <summary>750 /// Process user defined help data by finding the corresponding helpInfo and inserting751 /// necessary helpdata info to command help.752 /// </summary>753 /// <param name="mshSnapInId">PSSnapIn Name for the current help file.</param>754 /// <param name="userDefinedHelpData"></param>755 private void ProcessUserDefinedHelpData(string mshSnapInId, UserDefinedHelpData userDefinedHelpData)756 {757 if (userDefinedHelpData == null)758 return;759 760 if (string.IsNullOrEmpty(userDefinedHelpData.Name))761 return;762 763 HelpInfo helpInfo = GetFromCommandCache(mshSnapInId, userDefinedHelpData.Name, HelpCategory.Cmdlet);764 765 if (helpInfo == null)766 return;767 768 if (!(helpInfo is MamlCommandHelpInfo commandHelpInfo))769 return;770 771 commandHelpInfo.AddUserDefinedData(userDefinedHelpData);772 773 return;774 }775 776 /// <summary>777 /// Gets the HelpInfo object corresponding to the command.778 /// </summary>779 /// <param name="helpFileIdentifier">Help file identifier (either name of PSSnapIn or simply full path to help file).</param>780 /// <param name="commandName">Name of the command.</param>781 /// <param name="helpCategory"></param>782 /// <returns>HelpInfo object.</returns>783 private HelpInfo GetFromCommandCache(string helpFileIdentifier, string commandName, HelpCategory helpCategory)784 {785 Debug.Assert(!string.IsNullOrEmpty(commandName), "Cmdlet Name should not be null or empty.");786 787 string key = commandName;788 if (!string.IsNullOrEmpty(helpFileIdentifier))789 {790 key = helpFileIdentifier + "\\" + key;791 }792 793 HelpInfo result = GetCache(key);794 795 // Win8: Win8:477680: When Function/Workflow Use External Help, Category Property is "Cmdlet"796 if ((result != null) && (result.HelpCategory != helpCategory))797 {798 MamlCommandHelpInfo original = (MamlCommandHelpInfo)result;799 result = original.Copy(helpCategory);800 }801 802 return result;803 }804 805 /// <summary>806 /// Gets the HelpInfo object corresponding to the CommandInfo.807 /// </summary>808 /// <param name="helpFileIdentifier">Help file identifier (simply full path to help file).</param>809 /// <param name="commandInfo"></param>810 /// <returns>HelpInfo object.</returns>811 private HelpInfo GetFromCommandCache(string helpFileIdentifier, CommandInfo commandInfo)812 {813 Debug.Assert(commandInfo != null, "commandInfo cannot be null");814 HelpInfo result = GetFromCommandCache(helpFileIdentifier, commandInfo.Name, commandInfo.HelpCategory);815 if (result == null)816 {817 // check if the command is prefixed and try retrieving help by removing the prefix818 if ((commandInfo.Module != null) && (!string.IsNullOrEmpty(commandInfo.Prefix)))819 {820 MamlCommandHelpInfo newMamlHelpInfo = GetFromCommandCacheByRemovingPrefix(helpFileIdentifier, commandInfo);821 if (newMamlHelpInfo != null)822 {823 // caching the changed help content under the prefixed name for faster824 // retrieval later.825 AddToCommandCache(helpFileIdentifier, commandInfo.Name, newMamlHelpInfo);826 return newMamlHelpInfo;827 }828 }829 }830 831 return result;832 }833 834 /// <summary>835 /// Tries to get the help for the Cmdlet from cache.836 /// </summary>837 /// <param name="cmdletInfo"></param>838 /// <returns>839 /// HelpInfo object representing help for the command.840 /// </returns>841 private HelpInfo GetFromCommandCacheOrCmdletInfo(CmdletInfo cmdletInfo)842 {843 Debug.Assert(cmdletInfo != null, "cmdletInfo cannot be null");844 HelpInfo result = GetFromCommandCache(cmdletInfo.ModuleName, cmdletInfo.Name, cmdletInfo.HelpCategory);845 if (result == null)846 {847 // check if the command is prefixed and try retrieving help by removing the prefix848 if ((cmdletInfo.Module != null) && (!string.IsNullOrEmpty(cmdletInfo.Prefix)))849 {850 MamlCommandHelpInfo newMamlHelpInfo = GetFromCommandCacheByRemovingPrefix(cmdletInfo.ModuleName, cmdletInfo);851 852 if (newMamlHelpInfo != null)853 {854 // Noun exists only for cmdlets...since prefix will change the Noun, updating855 // the help content accordingly856 if (newMamlHelpInfo.FullHelp.Properties["Details"] != null &&857 newMamlHelpInfo.FullHelp.Properties["Details"].Value != null)858 {859 PSObject commandDetails = PSObject.AsPSObject(newMamlHelpInfo.FullHelp.Properties["Details"].Value);860 if (commandDetails.Properties["Noun"] != null)861 {862 commandDetails.Properties.Remove("Noun");863 }864 865 commandDetails.Properties.Add(new PSNoteProperty("Noun", cmdletInfo.Noun));866 }867 868 // caching the changed help content under the prefixed name for faster869 // retrieval later.870 AddToCommandCache(cmdletInfo.ModuleName, cmdletInfo.Name, newMamlHelpInfo);871 return newMamlHelpInfo;872 }873 }874 }875 876 return result;877 }878 879 /// <summary>880 /// Used to retrieve helpinfo by removing the prefix from the noun portion of a command name.881 /// Import-Module and Import-PSSession supports changing the name of a command882 /// by supplying a custom prefix. In those cases, the help content is stored by using the883 /// original command name (without prefix) as the key.884 ///885 /// This method retrieves the help content by suppressing the prefix and then making a copy886 /// of the help content + change the name and then returns the copied help content.887 /// </summary>888 /// <param name="helpIdentifier"></param>889 /// <param name="cmdInfo"></param>890 /// <returns>891 /// Copied help content or null if no help content is found.892 /// </returns>893 private MamlCommandHelpInfo GetFromCommandCacheByRemovingPrefix(string helpIdentifier, CommandInfo cmdInfo)894 {895 Dbg.Assert(cmdInfo != null, "cmdInfo cannot be null");896 897 MamlCommandHelpInfo result = null;898 MamlCommandHelpInfo originalHelpInfo = GetFromCommandCache(helpIdentifier,899 Microsoft.PowerShell.Commands.ModuleCmdletBase.RemovePrefixFromCommandName(cmdInfo.Name, cmdInfo.Prefix),900 cmdInfo.HelpCategory) as MamlCommandHelpInfo;901 902 if (originalHelpInfo != null)903 {904 result = originalHelpInfo.Copy();905 // command's name can be changed using -Prefix while importing module.To give better user experience for906 // get-help (on par with get-command), it was decided to use the prefixed command name907 // for the help content.908 if (result.FullHelp.Properties["Name"] != null)909 {910 result.FullHelp.Properties.Remove("Name");911 }912 913 result.FullHelp.Properties.Add(new PSNoteProperty("Name", cmdInfo.Name));914 915 if (result.FullHelp.Properties["Details"] != null &&916 result.FullHelp.Properties["Details"].Value != null)917 {918 // Note we are making a copy of the original instance and updating919 // it..This is to protect the help content of the original object.920 PSObject commandDetails = PSObject.AsPSObject(921 result.FullHelp.Properties["Details"].Value).Copy();922 923 if (commandDetails.Properties["Name"] != null)924 {925 commandDetails.Properties.Remove("Name");926 }927 928 commandDetails.Properties.Add(new PSNoteProperty("Name", cmdInfo.Name));929 930 // Note we made the change to a copy..so assigning the copy back to931 // the help content that is returned to the user.932 result.FullHelp.Properties["Details"].Value = commandDetails;933 }934 }935 936 return result;937 }938 939 /// <summary>940 /// Prepends mshsnapin id to the cmdlet name and adds the result to help cache.941 /// </summary>942 /// <param name="mshSnapInId">PSSnapIn name that this cmdlet belongs to.</param>943 /// <param name="cmdletName">Name of the cmdlet.</param>944 /// <param name="helpInfo">Help object for the cmdlet.</param>945 private void AddToCommandCache(string mshSnapInId, string cmdletName, MamlCommandHelpInfo helpInfo)946 {947 Debug.Assert(!string.IsNullOrEmpty(cmdletName), "Cmdlet Name should not be null or empty.");948 949 string key = cmdletName;950 951 // Add snapin qualified type name for this command at the top..952 // this will enable customizations of the help object.953 helpInfo.FullHelp.TypeNames.Insert(954 index: 0,955 string.Create(956 CultureInfo.InvariantCulture,957 $"MamlCommandHelpInfo#{mshSnapInId}#{cmdletName}"));958 959 if (!string.IsNullOrEmpty(mshSnapInId))960 {961 key = mshSnapInId + "\\" + key;962 // Add snapin name to the typenames of this object963 helpInfo.FullHelp.TypeNames.Insert(964 index: 1,965 string.Create(966 CultureInfo.InvariantCulture,967 $"MamlCommandHelpInfo#{mshSnapInId}"));968 }969 970 AddCache(key, helpInfo);971 }972 973 /// <summary>974 /// Check whether a HelpItems node indicates that the help content is975 /// authored using maml schema.976 ///977 /// This covers two cases:978 /// a. If the help file has an extension .maml.979 /// b. If HelpItems node (which should be the top node of any command help file)980 /// has an attribute "schema" with value "maml", its content is in maml981 /// schema.982 /// </summary>983 /// <param name="helpFile"></param>984 /// <param name="helpItemsNode"></param>985 /// <returns></returns>986 internal static bool IsMamlHelp(string helpFile, XmlNode helpItemsNode)987 {988 if (helpFile.EndsWith(".maml", StringComparison.OrdinalIgnoreCase))989 return true;990 991 if (helpItemsNode.Attributes == null)992 return false;993 994 foreach (XmlNode attribute in helpItemsNode.Attributes)995 {996 if (attribute.Name.Equals("schema", StringComparison.OrdinalIgnoreCase)997 && attribute.Value.Equals("maml", StringComparison.OrdinalIgnoreCase))998 {999 return true;1000 }1001 }1002 1003 return false;1004 }1005 1006 /// <summary>1007 /// Search help for a specific target.1008 /// </summary>1009 /// <param name="helpRequest">Help request object.</param>1010 /// <param name="searchOnlyContent">1011 /// If true, searches for pattern in the help content of all cmdlets.1012 /// Otherwise, searches for pattern in the cmdlet names.1013 /// </param>1014 /// <returns></returns>1015 internal override IEnumerable<HelpInfo> SearchHelp(HelpRequest helpRequest, bool searchOnlyContent)1016 {1017 string target = helpRequest.Target;1018 Collection<string> patternList = new Collection<string>();1019 // this will be used only when searchOnlyContent == true1020 WildcardPattern wildCardPattern = null;1021 // Decorated Search means that original match target is a target without1022 // wildcard patterns. It come here to because exact match was not found1023 // and search target will be decorated with wildcard character '*' to1024 // search again.1025 bool decoratedSearch = !WildcardPattern.ContainsWildcardCharacters(helpRequest.Target);1026 1027 if (!searchOnlyContent)1028 {1029 if (decoratedSearch)1030 {1031 if (target.Contains(StringLiterals.CommandVerbNounSeparator))1032 {1033 patternList.Add(target + "*");1034 }1035 else1036 {1037 patternList.Add("*" + target + "*");1038 }1039 }1040 else1041 {1042 patternList.Add(target);1043 }1044 }1045 else1046 {1047 // get help for all cmdlets.1048 patternList.Add("*");1049 string searchTarget = helpRequest.Target;1050 if (decoratedSearch)1051 {1052 searchTarget = "*" + helpRequest.Target + "*";1053 }1054 1055 wildCardPattern = WildcardPattern.Get(searchTarget, WildcardOptions.Compiled | WildcardOptions.IgnoreCase);1056 }1057 1058 int countOfHelpInfoObjectsFound = 0;1059 // this is for avoiding duplicate result from help output.1060 var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);1061 var hiddenCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);1062 foreach (string pattern in patternList)1063 {1064 CommandSearcher searcher = GetCommandSearcherForSearch(pattern, _context);1065 1066 while (searcher.MoveNext())1067 {1068 if (_context.CurrentPipelineStopping)1069 {1070 yield break;1071 }1072 1073 CommandInfo current = ((IEnumerator<CommandInfo>)searcher).Current;1074 1075 HelpInfo helpInfo = GetHelpInfo(current, !decoratedSearch, searchOnlyContent);1076 string helpName = GetHelpName(current);1077 1078 if (helpInfo != null && !string.IsNullOrEmpty(helpName))1079 {1080 if (!SessionState.IsVisible(helpRequest.CommandOrigin, current))1081 {1082 // this command is not visible to the user (from CommandOrigin) so1083 // dont show help topic for it.1084 hiddenCommands.Add(helpName);1085 1086 continue;1087 }1088 1089 if (set.Contains(helpName))1090 continue;1091 1092 // filter out the helpInfo object depending on user request1093 if (!Match(helpInfo, helpRequest, current))1094 {1095 continue;1096 }1097 1098 // Search content1099 if (searchOnlyContent && (!helpInfo.MatchPatternInContent(wildCardPattern)))1100 {1101 continue;1102 }1103 1104 set.Add(helpName);1105 countOfHelpInfoObjectsFound++;1106 yield return helpInfo;1107 1108 if (countOfHelpInfoObjectsFound >= helpRequest.MaxResults && helpRequest.MaxResults > 0)1109 yield break;1110 }1111 }1112 1113 if (this.HelpCategory == (HelpCategory.Alias | HelpCategory.Cmdlet))1114 {1115 foreach (CommandInfo current in ModuleUtils.GetMatchingCommands(pattern, _context, helpRequest.CommandOrigin))1116 {1117 if (_context.CurrentPipelineStopping)1118 {1119 yield break;1120 }1121 1122 if (!SessionState.IsVisible(helpRequest.CommandOrigin, current))1123 {1124 // this command is not visible to the user (from CommandOrigin) so1125 // dont show help topic for it.1126 continue;1127 }1128 1129 HelpInfo helpInfo = GetHelpInfo(current, !decoratedSearch, searchOnlyContent);1130 string helpName = GetHelpName(current);1131 1132 if (helpInfo != null && !string.IsNullOrEmpty(helpName))1133 {1134 if (set.Contains(helpName))1135 continue;1136 1137 if (hiddenCommands.Contains(helpName))1138 continue;1139 1140 // filter out the helpInfo object depending on user request1141 if (!Match(helpInfo, helpRequest, current))1142 {1143 continue;1144 }1145 1146 // Search content1147 if (searchOnlyContent && (!helpInfo.MatchPatternInContent(wildCardPattern)))1148 {1149 continue;1150 }1151 1152 set.Add(helpName);1153 countOfHelpInfoObjectsFound++;1154 yield return helpInfo;1155 1156 if (countOfHelpInfoObjectsFound >= helpRequest.MaxResults && helpRequest.MaxResults > 0)1157 yield break;1158 }1159 }1160 }1161 }1162 }1163 1164 /// <summary>1165 /// Check if a helpInfo object matches the component/role/functionality1166 /// criteria from helpRequest.1167 /// </summary>1168 /// <param name="helpInfo"></param>1169 /// <param name="helpRequest"></param>1170 /// <param name="commandInfo"></param>1171 /// <returns></returns>1172 private static bool Match(HelpInfo helpInfo, HelpRequest helpRequest, CommandInfo commandInfo)1173 {1174 if (helpRequest == null)1175 return true;1176 1177 if ((helpRequest.HelpCategory & commandInfo.HelpCategory) == 0)1178 {1179 return false;1180 }1181 1182 if (helpInfo is not BaseCommandHelpInfo)1183 return false;1184 1185 if (!Match(helpInfo.Component, helpRequest.Component))1186 {1187 return false;1188 }1189 1190 if (!Match(helpInfo.Role, helpRequest.Role))1191 {1192 return false;1193 }1194 1195 if (!Match(helpInfo.Functionality, helpRequest.Functionality))1196 {1197 return false;1198 }1199 1200 return true;