/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Management.Automation.Runspaces; using System.Collections.ObjectModel; using System.Collections.Generic; using System.Diagnostics; using System.Management.Automation; using System.Globalization; using System.Management.Automation.Internal; using System.Management.Automation.Help; using System.Runtime.InteropServices; namespace Microsoft.PowerShell.Commands { /// /// This class implements get-help command /// [Cmdlet(VerbsCommon.Get, "Help", DefaultParameterSetName = "AllUsersView", HelpUri = "http://go.microsoft.com/fwlink/?LinkID=113316")] public sealed class GetHelpCommand : PSCmdlet { /// /// Help Views /// internal enum HelpView { Default = 0x00, // Default View DetailedView = 0x01, FullView = 0x02, ExamplesView = 0x03 } /// /// Default constructor for the GetHelpCommand class /// public GetHelpCommand() { } #region Cmdlet Parameters /// /// Target to search for help /// [Parameter(Position = 0, ValueFromPipelineByPropertyName = true)] public string Name { get; set; } = ""; /// /// Path to provider location that user is curious about. /// [Parameter] public string Path { get; set; } /// /// List of help categories to search for help /// [Parameter] [ValidateSet( "Alias", "Cmdlet", "Provider", "General", "FAQ", "Glossary", "HelpFile", "ScriptCommand", "Function", "Filter", "ExternalScript", "All", "DefaultHelp", "Workflow", "DscResource", "Class", "Configuration", IgnoreCase = true)] public string[] Category { get; set; } /// /// List of Component's to search on. /// /// [Parameter] public string[] Component { get; set; } = null; /// /// List of Functionality's to search on. /// /// [Parameter] public string[] Functionality { get; set; } = null; /// /// List of Role's to search on. /// /// [Parameter] public string[] Role { get; set; } = null; private string _provider = ""; /// /// Changes the view of HelpObject returned /// /// /// Currently we support following views: /// /// 1. Reminder (Default - Experienced User) /// 2. Detailed (Beginner - Beginning User) /// 3. Full (All Users) /// 4. Examples /// 5. Parameters /// /// Currently we support these views only for Cmdlets. /// A SnapIn developer can however change these views. /// [Parameter(ParameterSetName = "DetailedView", Mandatory = true)] public SwitchParameter Detailed { set { if (value.ToBool()) { _viewTokenToAdd = HelpView.DetailedView; } } } /// /// Changes the view of HelpObject returned /// /// /// Currently we support following views: /// /// 1. Reminder (Default - Experienced User) /// 2. Detailed (Beginner - Beginning User) /// 3. Full (All Users) /// 4. Examples /// 5. Parameters /// /// Currently we support these views only for Cmdlets. /// A SnapIn developer can however change these views. /// [Parameter(ParameterSetName = "AllUsersView")] public SwitchParameter Full { set { if (value.ToBool()) { _viewTokenToAdd = HelpView.FullView; } } } /// /// Changes the view of HelpObject returned /// /// /// Currently we support following views: /// /// 1. Reminder (Default - Experienced User) /// 2. Detailed (Beginner - Beginning User) /// 3. Full (All Users) /// 4. Examples /// /// Currently we support these views only for Cmdlets. /// A SnapIn developer can however change these views. /// [Parameter(ParameterSetName = "Examples", Mandatory = true)] public SwitchParameter Examples { set { if (value.ToBool()) { _viewTokenToAdd = HelpView.ExamplesView; } } } /// /// Parameter name. /// /// /// Support WildCard strings as supported by WildcardPattern class. /// [Parameter(ParameterSetName = "Parameters", Mandatory = true)] public string Parameter { set; get; } /// /// This parameter,if true, will direct get-help cmdlet to /// navigate to a URL (stored in the command MAML file under /// the uri node). /// [Parameter(ParameterSetName = "Online", Mandatory = true)] public SwitchParameter Online { set { _showOnlineHelp = value; if (_showOnlineHelp) { VerifyParameterForbiddenInRemoteRunspace(this, "Online"); } } get { return _showOnlineHelp; } } private bool _showOnlineHelp; private bool _showWindow; /// /// Gets and sets a value indicatuing whether the help should be displayed in a separate window /// [Parameter(ParameterSetName = "ShowWindow", Mandatory = true)] public SwitchParameter ShowWindow { get { return _showWindow; } set { _showWindow = value; if (_showWindow) { VerifyParameterForbiddenInRemoteRunspace(this, "ShowWindow"); } } } // The following variable controls the view. private HelpView _viewTokenToAdd = HelpView.Default; #if !CORECLR private GraphicalHostReflectionWrapper graphicalHostReflectionWrapper; #endif private readonly Stopwatch _timer = new Stopwatch(); private bool _updatedHelp; #endregion #region Cmdlet API implementation /// /// Implements the BeginProcessing() method for get-help command /// protected override void BeginProcessing() { _timer.Start(); if (!Online.IsPresent && UpdatableHelpSystem.ShouldPromptToUpdateHelp() && HostUtilities.IsProcessInteractive(MyInvocation) && HasInternetConnection()) { if (ShouldContinue(HelpDisplayStrings.UpdateHelpPromptBody, HelpDisplayStrings.UpdateHelpPromptTitle)) { System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace).AddCommand("Update-Help").Invoke(); _updatedHelp = true; } UpdatableHelpSystem.SetDisablePromptToUpdateHelp(); } } /// /// Implements the ProcessRecord() method for get-help command /// protected override void ProcessRecord() { try { #if !CORECLR if (this.ShowWindow) { this.graphicalHostReflectionWrapper = GraphicalHostReflectionWrapper.GetGraphicalHostReflectionWrapper(this, "Microsoft.PowerShell.Commands.Internal.HelpWindowHelper"); } #endif this.Context.HelpSystem.OnProgress += new HelpSystem.HelpProgressHandler(HelpSystem_OnProgress); bool failed = false; HelpCategory helpCategory = ToHelpCategory(Category, ref failed); if (failed) return; // Validate input parameters ValidateAndThrowIfError(helpCategory); HelpRequest helpRequest = new HelpRequest(this.Name, helpCategory); helpRequest.Provider = _provider; helpRequest.Component = Component; helpRequest.Role = Role; helpRequest.Functionality = Functionality; helpRequest.ProviderContext = new ProviderContext( this.Path, this.Context.Engine.Context, this.SessionState.Path); helpRequest.CommandOrigin = this.MyInvocation.CommandOrigin; // the idea is to use yield statement in the help lookup to speed up // perceived user experience....So HelpSystem.GetHelp returns an // IEnumerable.. IEnumerable helpInfos = this.Context.HelpSystem.GetHelp(helpRequest); // HelpCommand acts differently when there is just one help object and when // there are more than one object...so handling this behavior through // some variables. HelpInfo firstHelpInfoObject = null; int countOfHelpInfos = 0; foreach (HelpInfo helpInfo in helpInfos) { // honor Ctrl-C from user. if (IsStopping) { return; } if (0 == countOfHelpInfos) { firstHelpInfoObject = helpInfo; } else { // write first help object only once. if (null != firstHelpInfoObject) { WriteObjectsOrShowOnlineHelp(firstHelpInfoObject, false); firstHelpInfoObject = null; } WriteObjectsOrShowOnlineHelp(helpInfo, false); } countOfHelpInfos++; } _timer.Stop(); if (!string.IsNullOrEmpty(Name)) Microsoft.PowerShell.Telemetry.Internal.TelemetryAPI.ReportGetHelpTelemetry(Name, countOfHelpInfos, _timer.ElapsedMilliseconds, _updatedHelp); // Write full help as there is only one help info object if (1 == countOfHelpInfos) { WriteObjectsOrShowOnlineHelp(firstHelpInfoObject, true); } else if (_showOnlineHelp && (countOfHelpInfos > 1)) { throw PSTraceSource.NewInvalidOperationException(HelpErrors.MultipleOnlineTopicsNotSupported, "Online"); } // show errors only if there is no wildcard search or VerboseHelpErrors is true. if (((countOfHelpInfos == 0) && (!WildcardPattern.ContainsWildcardCharacters(helpRequest.Target))) || this.Context.HelpSystem.VerboseHelpErrors) { // Check if there is any error happened. If yes, // pipe out errors. if (this.Context.HelpSystem.LastErrors.Count > 0) { foreach (ErrorRecord errorRecord in this.Context.HelpSystem.LastErrors) { WriteError(errorRecord); } } } } finally { this.Context.HelpSystem.OnProgress -= new HelpSystem.HelpProgressHandler(HelpSystem_OnProgress); // finally clear the ScriptBlockAst -> Token[] cache this.Context.HelpSystem.ClearScriptBlockTokenCache(); } } private HelpCategory ToHelpCategory(string[] category, ref bool failed) { if (category == null || category.Length == 0) return HelpCategory.None; HelpCategory helpCategory = HelpCategory.None; failed = false; for (int i = 0; i < category.Length; i++) { try { HelpCategory temp = (HelpCategory)Enum.Parse(typeof(HelpCategory), category[i], true); helpCategory |= temp; } catch (ArgumentException argumentException) { Exception e = new HelpCategoryInvalidException(category[i], argumentException); ErrorRecord errorRecord = new ErrorRecord(e, "InvalidHelpCategory", ErrorCategory.InvalidArgument, null); this.WriteError(errorRecord); failed = true; } } return helpCategory; } /// /// Change as per user request. /// /// This method creates a new type to the existing typenames /// depending on Detailed,Full,Example parameteres and adds this /// new type(s) to the top of the list. /// /// Full help object to transform. /// Transformed help object with new TypeNames. /// If Detailed and Full are not specified, nothing is changed. private PSObject TransformView(PSObject originalHelpObject) { Diagnostics.Assert(originalHelpObject != null, "HelpObject should not be null"); if (_viewTokenToAdd == HelpView.Default) { s_tracer.WriteLine("Detailed, Full, Examples are not selected. Constructing default view."); return originalHelpObject; } string tokenToAdd = _viewTokenToAdd.ToString(); // We are changing the types without modifying the original object. // The contract between help command and helpsystem does not // allow us to modify returned help objects. PSObject objectToReturn = originalHelpObject.Copy(); objectToReturn.TypeNames.Clear(); if (originalHelpObject.TypeNames.Count == 0) { string typeToAdd = string.Format(CultureInfo.InvariantCulture, "HelpInfo#{0}", tokenToAdd); objectToReturn.TypeNames.Add(typeToAdd); } else { // User request at the top.. foreach (string typeName in originalHelpObject.TypeNames) { // dont add new types for System.String and System.Object.. // as they are handled differently for F&0..(bug935095) if (typeName.ToLowerInvariant().Equals("system.string") || typeName.ToLowerInvariant().Equals("system.object")) { continue; } string typeToAdd = string.Format(CultureInfo.InvariantCulture, "{0}#{1}", typeName, tokenToAdd); s_tracer.WriteLine("Adding type {0}", typeToAdd); objectToReturn.TypeNames.Add(typeToAdd); } // Existing typenames at the bottom.. foreach (string typeName in originalHelpObject.TypeNames) { s_tracer.WriteLine("Adding type {0}", typeName); objectToReturn.TypeNames.Add(typeName); } } return objectToReturn; } /// /// Gets the paramater info for patterns identified Parameter property. /// Writes the parameter info(s) to the output stream. An error is thrown /// if a parameter with a given pattern is not found. /// /// HelpInfo Object to look for the parameter. private void GetAndWriteParameterInfo(HelpInfo helpInfo) { s_tracer.WriteLine("Searching parameters for {0}", helpInfo.Name); PSObject[] pInfos = helpInfo.GetParameter(Parameter); if ((pInfos == null) || (pInfos.Length == 0)) { Exception innerException = PSTraceSource.NewArgumentException("Parameter", HelpErrors.NoParmsFound, Parameter); WriteError(new ErrorRecord(innerException, "NoParmsFound", ErrorCategory.InvalidArgument, helpInfo)); } else { foreach (PSObject pInfo in pInfos) { WriteObject(pInfo); } } } /// /// Validates input parameters /// /// Category specified by the user /// /// If the request cant be serviced. /// private void ValidateAndThrowIfError(HelpCategory cat) { if (cat == HelpCategory.None) { return; } // categories that support -Parameter, -Role, -Functionality, -Component parameters HelpCategory supportedCategories = HelpCategory.Alias | HelpCategory.Cmdlet | HelpCategory.ExternalScript | HelpCategory.Filter | HelpCategory.Function | HelpCategory.ScriptCommand | HelpCategory.Workflow; if ((cat & supportedCategories) == 0) { if (!string.IsNullOrEmpty(Parameter)) { throw PSTraceSource.NewArgumentException("Parameter", HelpErrors.ParamNotSupported, "-Parameter"); } if (Component != null) { throw PSTraceSource.NewArgumentException("Component", HelpErrors.ParamNotSupported, "-Component"); } if (Role != null) { throw PSTraceSource.NewArgumentException("Role", HelpErrors.ParamNotSupported, "-Role"); } if (Functionality != null) { throw PSTraceSource.NewArgumentException("Functionality", HelpErrors.ParamNotSupported, "-Functionality"); } } } /// /// Helper method used to Write the help object onto the output /// stream or show online help (URI extracted from the HelpInfo) /// object. /// private void WriteObjectsOrShowOnlineHelp(HelpInfo helpInfo, bool showFullHelp) { if (helpInfo != null) { // online help can be showed only if showFullHelp is true.. // showFullHelp will be false when the help tries to display multiple help topics.. // -Online should not work when multiple help topics are displayed. if (showFullHelp && _showOnlineHelp) { bool onlineUriFound = false; // show online help s_tracer.WriteLine("Preparing to show help online."); Uri onlineUri = helpInfo.GetUriForOnlineHelp(); if (null != onlineUri) { onlineUriFound = true; LaunchOnlineHelp(onlineUri); return; } if (!onlineUriFound) { throw PSTraceSource.NewInvalidOperationException(HelpErrors.NoURIFound); } } else if (showFullHelp && ShowWindow) { #if !CORECLR graphicalHostReflectionWrapper.CallStaticMethod("ShowHelpWindow", helpInfo.FullHelp, this); #endif } else { // show inline help if (showFullHelp) { if (!string.IsNullOrEmpty(Parameter)) { GetAndWriteParameterInfo(helpInfo); } else { PSObject objectToReturn = TransformView(helpInfo.FullHelp); objectToReturn.IsHelpObject = true; WriteObject(objectToReturn); } } else { if (!string.IsNullOrEmpty(Parameter)) { PSObject[] pInfos = helpInfo.GetParameter(Parameter); if ((pInfos == null) || (pInfos.Length == 0)) { return; } } WriteObject(helpInfo.ShortHelp); } } } } /// /// Opens the Uri. System's default application will be used /// to show the uri. /// /// private void LaunchOnlineHelp(Uri uriToLaunch) { Diagnostics.Assert(null != uriToLaunch, "uriToLaunch should not be null"); if (!uriToLaunch.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && !uriToLaunch.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { throw PSTraceSource.NewInvalidOperationException(HelpErrors.ProtocolNotSupported, uriToLaunch.ToString(), "http", "https"); } Exception exception = null; try { this.WriteVerbose(string.Format(CultureInfo.InvariantCulture, HelpDisplayStrings.OnlineHelpUri, uriToLaunch.OriginalString)); System.Diagnostics.Process browserProcess = new System.Diagnostics.Process(); #if UNIX browserProcess.StartInfo.FileName = Platform.IsLinux ? "xdg-open" : /* OS X */ "open"; browserProcess.StartInfo.Arguments = uriToLaunch.OriginalString; browserProcess.Start(); #elif CORECLR throw new PlatformNotSupportedException(); #else browserProcess.StartInfo.FileName = uriToLaunch.OriginalString; browserProcess.Start(); #endif } catch (InvalidOperationException ioe) { exception = ioe; } catch (System.ComponentModel.Win32Exception we) { exception = we; } if (null != exception) { throw PSTraceSource.NewInvalidOperationException(exception, HelpErrors.CannotLaunchURI, uriToLaunch.OriginalString); } } #endregion private void HelpSystem_OnProgress(object sender, HelpProgressInfo arg) { ProgressRecord record = new ProgressRecord(0, this.CommandInfo.Name, arg.Activity); record.PercentComplete = arg.PercentComplete; WriteProgress(record); } #if !CORECLR [DllImport("wininet.dll")] private static extern bool InternetGetConnectedState(out int desc, int reserved); #endif /// /// Checks if we can connect to the internet /// /// private bool HasInternetConnection() { #if CORECLR return true; // TODO:CORECLR wininet.dll is not present on NanoServer #else int unused; return InternetGetConnectedState(out unused, 0); #endif } #region Helper methods for verification of parameters against NoLanguage mode internal static void VerifyParameterForbiddenInRemoteRunspace(Cmdlet cmdlet, string parameterName) { if (NativeCommandProcessor.IsServerSide) { string message = StringUtil.Format(CommandBaseStrings.ParameterNotValidInRemoteRunspace, cmdlet.MyInvocation.InvocationName, parameterName); Exception e = new InvalidOperationException(message); ErrorRecord errorRecord = new ErrorRecord(e, "ParameterNotValidInRemoteRunspace", ErrorCategory.InvalidArgument, null); cmdlet.ThrowTerminatingError(errorRecord); } } #endregion #region trace [TraceSourceAttribute("GetHelpCommand ", "GetHelpCommand ")] private static PSTraceSource s_tracer = PSTraceSource.GetTracer("GetHelpCommand ", "GetHelpCommand "); #endregion } /// /// Helper methods used as powershell extension from a types file. /// public static class GetHelpCodeMethods { /// /// Verifies if the InitialSessionState of the current process /// /// private static bool DoesCurrentRunspaceIncludeCoreHelpCmdlet() { InitialSessionState iss = System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InitialSessionState; if (iss != null) { IEnumerable publicGetHelpEntries = iss .Commands["Get-Help"] .Where(entry => entry.Visibility == SessionStateEntryVisibility.Public); if (publicGetHelpEntries.Count() != 1) { return false; } foreach (SessionStateCommandEntry getHelpEntry in publicGetHelpEntries) { SessionStateCmdletEntry getHelpCmdlet = getHelpEntry as SessionStateCmdletEntry; if ((null != getHelpCmdlet) && (getHelpCmdlet.ImplementingType.Equals(typeof(GetHelpCommand)))) { return true; } } } return false; } /// /// Retrieves the HelpUri given a CommandInfo instance. /// /// /// CommandInfo instance wrapped as PSObject /// /// /// null if is not a CommandInfo type. /// null if HelpUri could not be retrieved either from CommandMetadata or /// help content. /// [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings")] public static string GetHelpUri(PSObject commandInfoPSObject) { if (null == commandInfoPSObject) { return string.Empty; } CommandInfo cmdInfo = PSObject.Base(commandInfoPSObject) as CommandInfo; // GetHelpUri helper method is expected to be used only by System.Management.Automation.CommandInfo // objects from types.ps1xml if ((null == cmdInfo) || (string.IsNullOrEmpty(cmdInfo.Name))) { return string.Empty; } // The type checking is needed to avoid a try..catch exception block as // the CommandInfo.CommandMetadata throws an InvalidOperationException // instead of returning null. if ((cmdInfo is CmdletInfo) || (cmdInfo is FunctionInfo) || (cmdInfo is ExternalScriptInfo) || (cmdInfo is ScriptInfo)) { if (!string.IsNullOrEmpty(cmdInfo.CommandMetadata.HelpUri)) { return cmdInfo.CommandMetadata.HelpUri; } } AliasInfo aliasInfo = cmdInfo as AliasInfo; if ((null != aliasInfo) && (null != aliasInfo.ExternalCommandMetadata) && (!string.IsNullOrEmpty(aliasInfo.ExternalCommandMetadata.HelpUri))) { return aliasInfo.ExternalCommandMetadata.HelpUri; } // if everything else fails..depend on Get-Help infrastructure to get us the Uri. string cmdName = cmdInfo.Name; if (!string.IsNullOrEmpty(cmdInfo.ModuleName)) { cmdName = string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", cmdInfo.ModuleName, cmdInfo.Name); } if (DoesCurrentRunspaceIncludeCoreHelpCmdlet()) { // Win8: 651300 if core get-help is present in the runspace (and it is the only get-help command), use // help system directly and avoid perf penalty. var currentContext = System.Management.Automation.Runspaces.LocalPipeline.GetExecutionContextFromTLS(); if ((null != currentContext) && (null != currentContext.HelpSystem)) { HelpRequest helpRequest = new HelpRequest(cmdName, cmdInfo.HelpCategory); helpRequest.ProviderContext = new ProviderContext( string.Empty, currentContext, currentContext.SessionState.Path); helpRequest.CommandOrigin = CommandOrigin.Runspace; foreach ( Uri result in currentContext.HelpSystem.ExactMatchHelp(helpRequest).Select( helpInfo => helpInfo.GetUriForOnlineHelp()).Where(result => null != result)) { return result.OriginalString; } } } else { // win8: 546025. Using Get-Help as command, instead of calling HelpSystem.ExactMatchHelp // for the following reasons: // 1. Exchange creates proxies for Get-Command and Get-Help in their scenario // 2. This method is primarily used to get uri faster while serializing the CommandInfo objects (from Get-Command) // 3. Exchange uses Get-Help proxy to not call Get-Help cmdlet at-all while serializing CommandInfo objects // 4. Using HelpSystem directly will not allow Get-Help proxy to do its job. System.Management.Automation.PowerShell getHelpPS = System.Management.Automation.PowerShell.Create( RunspaceMode.CurrentRunspace).AddCommand("get-help"). AddParameter("Name", cmdName).AddParameter("Category", cmdInfo.HelpCategory.ToString()); try { Collection helpInfos = getHelpPS.Invoke(); if (null != helpInfos) { for (int index = 0; index < helpInfos.Count; index++) { HelpInfo helpInfo; if (LanguagePrimitives.TryConvertTo(helpInfos[index], out helpInfo)) { Uri result = helpInfo.GetUriForOnlineHelp(); if (null != result) { return result.OriginalString; } } else { Uri result = BaseCommandHelpInfo.GetUriFromCommandPSObject(helpInfos[index]); return (result != null) ? result.OriginalString : string.Empty; } } } } finally { getHelpPS.Dispose(); } } return string.Empty; } } }