-
-
Save mklement0/880624fd665073bb439dfff5d71da886 to your computer and use it in GitHub Desktop.
<# | |
Prerequisites: PowerShell v5.1 and above (verified; may also work in earlier versions) | |
License: MIT | |
Author: Michael Klement <[email protected]> | |
DOWNLOAD and DEFINITION OF THE FUNCTION: | |
irm https://gist.github.com/mklement0/880624fd665073bb439dfff5d71da886/raw/Show-Help.ps1 | iex | |
The above directly defines the function below in your session and offers guidance for making it available in future | |
sessions too. | |
DOWNLOAD ONLY: | |
irm https://gist.github.com/mklement0/880624fd665073bb439dfff5d71da886/raw > Show-Help.ps1 | |
The above downloads to the specified file, which you then need to dot-source to make the function available | |
in the current session: | |
. ./Show-Help.ps1 | |
To learn what the function does: | |
* see the next comment block | |
* or, once downloaded and defined, invoke the function with -? or pass its name to Get-Help. | |
To define an ALIAS for the function, (also) add something like the following to your $PROFILE: | |
Set-Alias shh Show-Help | |
#> | |
function Show-Help { | |
<# | |
.SYNOPSIS | |
Wrapper command for the Get-Help cmdlet that shows help topics online rather | |
than locally in the console / terminal. | |
.DESCRIPTION | |
about_* topics are also supported, via direct construction of the target URL. | |
-CopyUrl (-cpu) / -CopyLink (-cp) copy the online help topic's URL to the | |
clipboard as-is / as a Markdown link instead of opening it in the browser. | |
For about_CommonParameters and about_Automatic_Variables specifically, you may | |
pass a specific parameter / variable name as the 2nd positional argument or | |
via -Anchor. Short parameter aliases such as 'wi' for 'WhatIf' are supported. | |
Additionally, 'where' and 'foreach' are supported for about_Arrays to look up | |
the .Where() and .ForEach() array methods. | |
As an alternative to online lookup you my specify -Local, in which case | |
local help content, with the default detail level changed to -Full, is captured | |
in a temporary file and displayed via your system's default text editor, | |
Run Get-Help Get-Help for help on the other parameters. | |
.EXAMPLE | |
Show-Help Get-Command | |
Shows the Get-Command cmdlet's online help topic | |
.EXAMPLE | |
Show-Help about_Automatic_Variables HOME | |
Shows the conceptual help topic about PowerShell's automatic variables online | |
and jumpts to the description of the $HOME variable, specifically, | |
.EXAMPLE | |
Show-Help about_Automatic_Variables HOME -CopyLink | |
Copies a Markdwown link to the online version of the given conceptual topic | |
to the clipboard, with the URL pointing to the $HOME variable's description, | |
specifically. -CopyUrl would copy just the URL. | |
#> | |
[CmdletBinding(DefaultParameterSetName = 'AllUsersView', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113316')] | |
param( | |
[Parameter(Position = 0)] | |
[ValidateNotNullOrEmpty()] | |
[ArgumentCompleter( { | |
param($cmd, $param, $wordToComplete) | |
if ($wordToComplete -like 'about*' -or $wordToComplete -like '_[a-z]*') { | |
# As a courtesy, allow '_...' as shorthand for 'about_...' | |
$wordToComplete = $wordToComplete -replace '^(?:about)?_', 'about_' | |
# Note: This is *slow* and invoked every time what the user typed changes. | |
# Also, it prints a blank line below the current one, curiously. | |
@((Get-Help -Category HelpFile).Name) -like "$wordToComplete*" | |
} | |
else { | |
# Get-Help itself completes those for us, unlike the about_* topics, curiously, | |
# even though it does the latter in direct invocation. | |
# (Get-Command -Type Alias, Function, Cmdlet).Name -like "$wordToComplete*" | |
} | |
})] | |
[string] | |
${Name}, | |
# Custom parameter: | |
# To support about_CommonParameters and about_Automatic_Variables with a specific | |
# parameter / variable name. | |
[Parameter(Position = 1)] | |
[string] | |
$Anchor, | |
[string] | |
${Path}, | |
[ValidateSet('Alias', 'Cmdlet', 'Provider', 'General', 'FAQ', 'Glossary', 'HelpFile', 'ScriptCommand', 'Function', 'Filter', 'ExternalScript', 'All', 'DefaultHelp', 'Workflow', 'DscResource', 'Class', 'Configuration')] | |
[string[]] | |
${Category}, | |
[Parameter(ParameterSetName = 'DetailedView', Mandatory = $true)] | |
[switch] | |
${Detailed}, | |
[Parameter(ParameterSetName = 'AllUsersView')] | |
[switch] | |
${Full}, | |
[Parameter(ParameterSetName = 'Examples', Mandatory = $true)] | |
[switch] | |
${Examples}, | |
[Parameter(ParameterSetName = 'Parameters', Mandatory = $true)] | |
[string[]] | |
${Parameter}, | |
[string[]] | |
${Component}, | |
[string[]] | |
${Functionality}, | |
[string[]] | |
${Role}, | |
[Parameter(ParameterSetName = 'Online', Mandatory = $true)] | |
[switch] | |
${Local} # Custom argument - !! inversion of the Get-Help logic | |
, | |
[Parameter(ParameterSetName = 'CopyLink', Mandatory = $true)] # Custom argument - copy URL to clipboard as Markdown link. | |
[Alias('cp')] | |
[switch] | |
$CopyLink | |
, | |
[Parameter(ParameterSetName = 'CopyUrl', Mandatory = $true)] # Custom argument - copy URL to clipboard | |
[Alias('cpu')] | |
[switch] | |
$CopyUrl | |
) | |
Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop' | |
$copyToClipboard = $CopyUrl -or $CopyLink | |
# The online help topic should be navigated to by default; -Local overrides in order | |
# to use the local help content and display it in the default text editor. | |
$Online = -not $Local | |
# Remove all wrapper-specific parameters, so that Get-Help @PSBoundParameters (if it is used), | |
# doesn't break. | |
foreach ($paramName in 'Local', 'CopyLink', 'CopyUrl') { | |
$null = $PSBoundParameters.Remove($paramName) | |
} | |
# Conversely, make sure the -Online switch is set appropriately. | |
if ($Online) { $PSBoundParameters['Online'] = $Online } | |
$isAboutTopic = $Name -like 'about_*' | |
$linkLabel = $Name | |
$topicsSupportedWithAnchor = 'about_CommonParameters', 'about_Automatic_Variables', 'about_Preference_Variables', 'about_Arrays' | |
if ($Anchor) { | |
if (-not (($Online -or $copyToClipboard) -and $Name -in $topicsSupportedWithAnchor)) { | |
throw "An -Anchor argument is only supported for online lookups and link/URL copying when combined with the following topics: $($topicsSupportedWithAnchor -join ', ')" | |
} | |
# Validate the anchor value, based on hard-coded knowledge. | |
# !! We do this, because there's no easy way to validate the presence of an anchor on a page, short of downloading the HTML and anlyzing it. | |
# !! Because of the hard-coded nature, this may have to be updated over time. | |
$validAnchor = switch ($Name) { | |
$topicsSupportedWithAnchor[0] { | |
# about_CommonParameters | |
# Determined with: | |
# (((get-help about_CommonParameters) -split '\r?\n' -match '^\s*-\s+\w+\s+\(.*?\)').Trim('-').Trim() -split '[ ()]' -ne '') -replace '^', "'" -replace '$', "'" -join ', ' | |
# Note: The casing has been manually corrected to be more eye-friendly. | |
$namesAndAliases = 'Debug', 'db', 'ErrorAction', 'ea', 'ErrorVariable', 'ev', 'InformationAction', 'infa', 'InformationVariable', 'iv', 'OutVariable', 'ov', 'OutBuffer', 'ob', 'PipelineVariable', 'pv', 'Verbose', 'vb', 'WarningAction', 'wa', 'WarningVariable', 'wv', 'WhatIf', 'wi', 'Confirm', 'cf' | |
# Find the index. | |
$ndx = [Array]::FindIndex($namesAndAliases, [Predicate[string]] { $Anchor -eq $args[0] }) | |
# If the index is an odd number, a short alias name was specified - the immediately preceding name contains the full name, which must be used as the anchor. | |
if ($ndx % 2) { --$ndx } | |
$Anchor = '-' + $namesAndAliases[$ndx] | |
$linkLabel = 'common `-{0}` parameter' -f $namesAndAliases[$ndx] # Use the proper casing | |
$ndx -ge 0 | |
} | |
$topicsSupportedWithAnchor[1] { | |
# about_Automatic_Variables | |
# Determined with: | |
# (((get-help about_automatic_variables) -split '\r?\n' -match '^\s*\$\w+\s*$').Trim().Trim('$') | Sort-Object -Unique) -replace '^', "'" -replace '$', "'" -join ', ' | |
$names = '?', '_', 'args', 'ConsoleFileName', 'Error', 'Event', 'EventArgs', 'EventSubscriber', 'ExecutionContext', 'false', 'foreach', 'HOME', 'Host', 'input', 'IsCoreCLR', 'IsLinux', 'IsMacOS', 'IsWindows', 'LastExitCode', 'Matches', 'MyInvocation', 'NestedPromptLevel', 'null', 'PID', 'PROFILE', 'PSBoundParameters', 'PSCmdlet', 'PSCommandPath', 'PSCulture', 'PSDebugContext', 'PSHOME', 'PSItem', 'PSScriptRoot', 'PSSenderInfo', 'PSUICulture', 'PSVersionTable', 'PWD', 'Sender', 'ShellId', 'StackTrace', 'switch', 'this', 'true' | |
$ndx = [Array]::FindIndex($names, [Predicate[string]] { $Anchor -eq $args[0] }) | |
$linkLabel = 'automatic `${0}` variable' -f $names[$ndx] # Use the proper casing | |
$irregularAnchor = @{ '$'='section'; '?' = 'section-1'; '^' = 'section-2' }[$Anchor] | |
if ($irregularAnchor) { $Anchor = $irregularAnchor } | |
$ndx -ge 0 | |
} | |
$topicsSupportedWithAnchor[2] { | |
# about_Preference_Variables | |
# Determined with: | |
# ((get-help about_Preference_Variables) -split '\r?\n' -match '^ \$\w+\s+').ForEach({ (-split $_)[0].TrimStart('$') }) -replace '^', "'" -replace '$', "'" -join ', ' | |
$names = 'ConfirmPreference', 'DebugPreference', 'ErrorActionPreference', 'ErrorView', 'FormatEnumerationLimit', 'InformationPreference', 'LogCommandHealthEvent', 'LogCommandLifecycleEvent', 'LogEngineHealthEvent', 'LogEngineLifecycleEvent', 'LogProviderLifecycleEvent', 'LogProviderHealthEvent', 'MaximumHistoryCount', 'OFS', 'OutputEncoding', 'ProgressPreference', 'PSDefaultParameterValues', 'PSEmailServer', 'PSModuleAutoLoadingPreference', 'PSSessionApplicationName', 'PSSessionConfigurationName', 'PSSessionOption', 'Transcript', 'VerbosePreference', 'WarningPreference', 'WhatIfPreference', 'PSNativeCommandArgumentPassing', 'PSNativeCommandUseErrorActionPreference' | |
$ndx = [Array]::FindIndex($names, [Predicate[string]] { $Anchor -eq $args[0] }) | |
$linkLabel = 'preference variable `${0}`' -f $names[$ndx] # Use the proper casing | |
$ndx -ge 0 | |
} | |
$topicsSupportedWithAnchor[3] { | |
# about_Arrays | |
# Just the .Where() and .ForEach() method anchors | |
$names = 'Where', 'ForEach' | |
$ndx = [Array]::FindIndex($names, [Predicate[string]] { $Anchor -eq $args[0] }) | |
$linkLabel = '`.{0}()` array method' -f $names[$ndx] # Use the proper casing | |
$ndx -ge 0 | |
} | |
} | |
if (-not $validAnchor) { | |
throw "Invalid -Anchor argument for topic $Name." | |
} | |
# !! Anchors as URL parts are case-SENSITIVE and must be *all-lowercase* | |
$Anchor = $Anchor.ToLowerInvariant() | |
} | |
# Note: For online help it only makes sense to look for topics for names recognized | |
# as *commands*. | |
# Note: We needn't worry about alias resolution, Get-Help does that automatically. | |
if ($Online -and $Name -and -not $isAboutTopic -and -not (Get-Command -Ea Ignore $Name)) { | |
Throw "No command named '$Name' found." | |
} | |
if ($Online -or $copyToClipboard) { | |
# Open online help topic in default web browser or copy the topic URL to the clipboard. | |
if ($isAboutTopic) { | |
# Sadly, as of 7.0 about_* topics have no online URL information, but it's easy to construct them. | |
$url = "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/$Name" | |
if ($Online -and -not $copyToClipboard) { | |
# To be consistent with how -Online normally works, append the executing engine's PowerShell version as the version-specific view. | |
Start-Process ($url + ('?view=powershell-' + (('{0}.{1}' -f $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor) -replace '\.0$')) + $(if ($Anchor) { "#$Anchor" })) | |
return | |
} | |
} | |
else { | |
if ($Online -and -not $copyToClipboard) { | |
# Pass through to Get-Help -Online | |
Microsoft.PowerShell.Core\Get-Help @PSBoundParameters | |
return | |
} | |
# For -CopyLink and -CopyUrl: Derive the URL from the properties of the target help topic, but strip the version-specific view request, | |
# because we want to copy version-agnostic URLs to the clipboard. | |
[string] $url = (@((Microsoft.PowerShell.Core\Get-Help -Name $Name).relatedlinks.navigationLink.uri) -ne '')[0] # remove | |
if (-not $url) { | |
Throw "No online help-topic URL found for: $Name" | |
} | |
elseif ($url -like '*go.microsoft.com/fwlink*') { | |
# URL is a short version that redirects to the ultimate URL; find that URL and lop off the query-string part. | |
try { $url = [System.Net.HttpWebRequest]::Create($url).GetResponse().ResponseUri.AbsoluteUri -replace '\?.+$' } catch { throw } | |
} | |
# Remove the query-string part, such as '?view=powershell-6&WT.mc_id=ps-gethelp' | |
$url = $url -replace '\?.+$' | |
} | |
if ($Anchor) { | |
$url += "#$Anchor" | |
} | |
# -CopyLink or -CopyUrl | |
# Convert to Markdown links. | |
if ($CopyLink) { | |
$label = if ($isAboutTopic) { | |
$linkLabel | |
} | |
else { | |
$cmd = (Get-Command -Name $Name) # $url -replace '^.*/' -replace '\?.+$' -replace '-', '`' | |
if ($cmd.ResolvedCommand) { $cmd = $cmd.ResolvedCommand } | |
'`{0}`' -f $cmd.Name | |
} | |
$textToCopy = '[{0}]({1})' -f $label, $url | |
} | |
else { | |
# $CopyUrl | |
$textToCopy = $url | |
} | |
Write-Verbose "Copying URL / Markdown link to the clipboard: $textToCopy" | |
Set-Clipboard $textToCopy | |
return | |
} | |
# -Local specified: | |
# Change default detail level to -Full, capture the output in a temporary file, | |
# and display it in the default text editor. | |
if (-not $PSBoundParameters.ContainsKey('Full') -or -not $PSBoundParameters.ContainsKey('Detailed') -or -not $PSBoundParameters.ContainsKey('Examples')) { | |
$PSBoundParameters.Add('Full', $true) | |
} | |
# Use a preexisting Show-Output command to capture the output in a temp. file | |
# and open it in the default text editor. | |
# If no such command can be found, define it now. | |
if (-not ((Get-Command -ErrorAction Ignore Show-Output))) { | |
function Show-Output { | |
$tmpFile = (Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName())) + '.txt' | |
$Input > $tmpFile | |
Invoke-Item -LiteralPath $tmpFile | |
# Quietly try to delete the file after a number of seconds, under the assumption | |
# that the text editor that has the file open won't complain. | |
Start-Process -NoNewWindow -FilePath (Get-Process -Id $PID).Path -Args '-c', "Start-Sleep 5; Remove-Item -ErrorAction Ignore -LiteralPath `"$tmpFile`"" | |
} | |
} | |
Microsoft.PowerShell.Core\Get-Help @PSBoundParameters | Out-String | Show-Output | |
} | |
# -------------------------------- | |
# GENERIC INSTALLATION HELPER CODE | |
# -------------------------------- | |
# Provides guidance for making the function persistently available when | |
# this script is either directly invoked from the originating Gist or | |
# dot-sourced after download. | |
# IMPORTANT: | |
# * DO NOT USE `exit` in the code below, because it would exit | |
# the calling shell when Invoke-Expression is used to directly | |
# execute this script's content from GitHub. | |
# * Because the typical invocation is DOT-SOURCED (via Invoke-Expression), | |
# do not define variables or alter the session state via Set-StrictMode, ... | |
# *except in child scopes*, via & { ... } | |
if ($MyInvocation.Line -eq '') { | |
# Most likely, this code is being executed via Invoke-Expression directly | |
# from gist.github.com | |
# To simulate for testing with a local script, use the following: | |
# Note: Be sure to use a path and to use "/" as the separator. | |
# iex (Get-Content -Raw ./script.ps1) | |
# Derive the function name from the invocation command, via the enclosing | |
# script name presumed to be contained in the URL. | |
# NOTE: Unfortunately, when invoked via Invoke-Expression, $MyInvocation.MyCommand.ScriptBlock | |
# with the actual script content is NOT available, so we cannot extract | |
# the function name this way. | |
& { | |
param($invocationCmdLine) | |
# Try to extract the function name from the URL. | |
$funcName = $invocationCmdLine -replace '^.+/(.+?)(?:\.ps1).*$', '$1' | |
if ($funcName -eq $invocationCmdLine) { | |
# Function name could not be extracted, just provide a generic message. | |
# Note: Hypothetically, we could try to extract the Gist ID from the URL | |
# and use the REST API to determine the first filename. | |
Write-Verbose -Verbose "Function is now defined in this session." | |
} | |
else { | |
# Indicate that the function is now defined and also show how to | |
# add it to the $PROFILE or convert it to a script file. | |
Write-Verbose -Verbose @" | |
Function `"$funcName`" is now defined in this session. | |
* If you want to add this function to your `$PROFILE, run the following: | |
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE | |
* If you want to convert this function into a script file that you can invoke | |
directly, run: | |
"`${function:$funcName}" | Set-Content $funcName.ps1 -Encoding $('utf8' + ('', 'bom')[[bool] (Get-Variable -ErrorAction Ignore IsCoreCLR -ValueOnly)]) | |
"@ | |
} | |
} $MyInvocation.MyCommand.Definition # Pass the original invocation command line to the script block. | |
} | |
else { | |
# Invocation presumably as a local file after manual download, | |
# either dot-sourced (as it should be) or mistakenly directly. | |
& { | |
param($originalInvocation) | |
# Parse this file to reliably extract the name of the embedded function, | |
# irrespective of the name of the script file. | |
$ast = $originalInvocation.MyCommand.ScriptBlock.Ast | |
$funcName = $ast.Find( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false).Name | |
if ($originalInvocation.InvocationName -eq '.') { | |
# Being dot-sourced as a file. | |
# Provide a hint that the function is now loaded and provide | |
# guidance for how to add it to the $PROFILE. | |
Write-Verbose -Verbose @" | |
Function `"$funcName`" is now defined in this session. | |
If you want to add this function to your `$PROFILE, run the following: | |
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE | |
"@ | |
} | |
else { | |
# Mistakenly directly invoked. | |
# Issue a warning that the function definition didn't take effect and | |
# provide guidance for reinvocation and adding to the $PROFILE. | |
Write-Warning @" | |
This script contains a definition for function "$funcName", but this definition | |
only takes effect if you dot-source this script. | |
To define this function for the current session, run: | |
. "$($originalInvocation.MyCommand.Path)" | |
"@ | |
} | |
} $MyInvocation # Pass the original invocation info to the helper script block. | |
} |
Thank you!
You're welcome, @Konfekt; in future Gists I'll add a hint re 4>$null
in the comment at the top.
I should add a slight caveat:
I hadn't really anticipated that users methodically pull down the latest version of my Gists every time.
While I try to maintain backward compatibility between versions, it isn't guaranteed.
While I try to make sure that a Gist is helpful and works as advertised, the choice of a Gist as the publication mechanism means that I don't want to spend the effort to create a full-fledged module with proper tests, explicit version control, semantic versioning, ...
P.S., @Konfekt:
To avoid the risk of a later revision breaking existing code, you can adapt the irm ... | iex
command to use a link (URL) to a specific revision of the Gist that you know to be working, which is guaranteed not to change (you should upgrade to a later revision only after testing first).
You can obtain such a link as follows:
- Click on
Revisions
in the top left corner of the window. - Click on
...
next to the revision of interest, and click onView File
in the dropdown menu. - Click on
Raw
on the right side of the header above the revision's source code. - Copy the URL of the page that opens, which is your permalink to the targeted revision; it is composed as follows:
https://gist.githubusercontent.com/<user>/<gist-ID>/raw/<commit-id>/<filename>
Here's an example URL that locks in the latest revision as of this writing:
To spell out its use in the context of the irm ... | iex
command, usable in a script:
irm https://gist.githubusercontent.com/mklement0/880624fd665073bb439dfff5d71da886/raw/07605b0030258de155982b94837a943a49cb20d7/Show-Help.ps1 | iex 3>$null
Hello @mklement0 ,
thank you very much again!
Glad to hear they're useful, @Konfekt.
So you're looking to download these Gists as
.ps1
files you can invoke directly?If you're dot-sourcing them so as to define the embedded function, you can suppress the verbose output with a redirection; e.g.:
Similarly, you can append
4>$null
directly to theiex
call for direct definition; e.g.:However, if you call the above from a script, you'll get a (false) warning about dot-sourcing instead, which you can suppress with
3>$null
; e.g.:However, please see the caveat below.