Skip to content

Instantly share code, notes, and snippets.

@mklement0
Last active January 29, 2023 20:30
Show Gist options
  • Save mklement0/50a1b101cd53978cd147b4b138fe6ef4 to your computer and use it in GitHub Desktop.
Save mklement0/50a1b101cd53978cd147b4b138fe6ef4 to your computer and use it in GitHub Desktop.
PowerShell function for performing online documentation lookups for built-in .NET types and their members - see https://stackoverflow.com/a/59324892/45375
<#
Prerequisites: PowerShell v3+
License: MIT
Author: Michael Klement <[email protected]>
DOWNLOAD and DEFINITION OF THE FUNCTION:
irm https://gist.github.com/mklement0/50a1b101cd53978cd147b4b138fe6ef4/raw/Show-TypeHelp.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/50a1b101cd53978cd147b4b138fe6ef4/raw > Show-TypeHelp.ps1
Thea above downloads to the specified file, which you then need to dot-source to make the function available
in the current session:
. ./Show-TypeHelp.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 shth Show-TypeHelp
#>
function Show-TypeHelp {
<#
.SYNOPSIS
Shows documentation for built-in .NET types.
.DESCRIPTION
Navigates to the specified .NET type's learn.microsoft.com documentation page
in your default web browser, assuming the type comes with .NET and PowerShell.
Use -WhatIf to preview the URL that would be opened.
There are two basic invocation patterns:
* Provide the full name of a type a type accelerator or a [type] instance
via the -Type parameter.
* Tab-completion works with the (prefixes of) a type's simple name (without
namespace component); e.g., Get-TypeHelp List<tab> cycles through all
loaded types whose name is or starts with 'List'.
* Pipe instance(s) of the type of interest.
.PARAMETER Type
Can be the name of a type (e.g. "string"), or a type literal (e.g. [string]).
If given a name, the name must be one of the following:
* The type's full name; e.g., 'System.Xml.XmlDocument'.
* The full name with the 'System.' prefix omitted; e.g., 'Xml.XmlDocument'
* The name of a PowerShell type accelerator; e.g., 'xml'
.PARAMETER Member
The optional name of a property or method to get specific information on.
If the target type has no such member, a warning is issued, and you're taken
to the type's home page.
.PARAMETER InputObject
Object(s), typically provided via the pipeline, whose type's documentation
page should be opened.
.PARAMETER Platform
The target .NET platform / standard, which must include a specific major.minor
version number; e.g., 'dotnetcore-3.1'.
Currently, the latest 'netframework-*' version is targeted by default, i.e.,
the Windows-only .NET Framework (FullCLR).
Use tab completion to cycle through the available platforms, but note that
you must complete the specific version number yourself.
.PARAMETER CopyLink
Instead of opening the topic page in the default browser, copies a Markdown
link to it to the clipboard; e.g.:
[`System.IO.DirectoryInfo`](https://learn.microsoft.com/en-US/dotnet/api/System.IO.DirectoryInfo)
.PARAMETER CopyUrl
Instead of opening the topic page in the default browser, copies its URL to
the clipboard.
.EXAMPLE
Get-TypeHelp xml
Opens the documentation page for type [xml], i.e., for System.Xml.XmlDocument
.EXAMPLE
Get-TypeHelp string split
Opens the documentation page for the System.String type's Split() method.
.EXAMPLE
Get-Item / | Get-TypeHelp
Opens the documentation page for type System.IO.DirectoryInfo, an instance
of which is output by the Get-Item command.
.EXAMPLE
Get-TypeHelp regex -Platform netcore-3.1
Opens the documenation page for type System.Text.RegularExpressions.Regex
for the .NET Core 3.1 platform.
.NOTES
License: MIT
Author: Michael Klement <[email protected]>
It would be nice if the following worked to get help for the String.Split()
method, for instance:
'foo'.Split | Get-TypeHelp
Unfortunately, however, the resulting [System.Management.Automation.PSMethodInfo]
info instance contains no information about the enclosing type.
#>
[CmdletBinding(DefaultParameterSetName = 'ByType', SupportsShouldProcess = $true)]
[OutputType()] # No output.
param(
[Parameter(ParameterSetName = 'ByType', ValueFromPipeline, Mandatory, Position = 0)]
[ArgumentCompleter( {
param($cmd, $param, $wordToComplete)
# Remove enclosing / opening quote(s), if present.
$wordToComplete = $wordToComplete -replace '^[''"]|[''"]$'
if ($tp = $wordToComplete -as [Type]) {
# Already a full type name or the name of a type accelerator such as [xml]
$tp.FullName
}
else {
# Get the full names of all public types (including nested ones), but exclude dynamic assemblies.
# (Dynamic assemblies can't be expected to have documentation pages anyway; also, not excluding them would break the .GetExportedTypes() call.)
$allLoadedTypes = [System.AppDomain]::CurrentDomain.GetAssemblies().Where( { -not $_.IsDynamic }).GetExportedTypes().FullName
# Prefix-name-only-match against all loaded full type names from non-dynamic assemblies at
# and enclose in embedded '...' if the type name contains a ` char. (generics), then sort.
$(foreach ($match in $allLoadedTypes -match "[+.]$wordToComplete[^.]*$") {
($match, "'$match'")[$match -match '`']
}) | Sort-Object
}
})]
[Type] $Type
,
[Parameter(ParameterSetName = 'ByType', Position = 1)]
[string] $Member
,
[Parameter(ParameterSetName = 'ByInstance', ValueFromPipeline, Mandatory)]
[ValidateNotNullOrEmpty()]
$InputObject
,
[ArgumentCompleter( {
'netcore-', 'netframework-', 'xamarinmac-', 'dotnet-plat-ext-', 'netstandard-', 'dotnet-uwp-', 'xamarinandroid-', 'xamarinios-10.8', 'xamarinmac-' -like "$wordToComplete*"
})]
[string] $Platform
,
[Alias('cp')]
[switch] $CopyLink
,
[Alias('cpu')]
[switch] $CopyUrl
)
begin {
# To avoid parameter-set proliferation, implement mutual exclusion for -CopyUrl and -CopyLink manually.
If ($CopyUrl -and $CopyLink) { throw "Please specify EITHER -CopyUrl OR -CopyLink."}
$copyToClipboard = $CopyUrl -or $CopyLink
$types = [System.Collections.Generic.List[Type]]::new()
$instances = [System.Collections.Generic.List[object]]::new()
if ($Platform -and $Platform -notmatch '^[a-z][a-z-]+-\d+\.\d+$') {
Throw "The -Platform value must be in the form '<platform-id>-<major>.<minor>'; e.g., 'netcore-3.1'; use tab completion to cycle through the supported platforms and add a version number."
}
}
process {
switch ($PSCmdlet.ParameterSetName) {
'ByType' { $types.Add($Type) }
'ByInstance' {
$instances.Add($InputObject)
}
Default { Throw 'What are you doing here?' }
}
}
end {
# If instances were given, determine their types now.
if ($PSCmdlet.ParameterSetName -eq 'ByInstance') {
$types = $instances.ToArray().ForEach({
if ($_ -is [string]) {
Write-Verbose -Verbose "Interpreting [string] input as a type *name*, '$_'."
[type] $_
} else {
$_.GetType()
}
}) | Select-Object -Unique
}
$urls = foreach ($tp in $types) {
# Make sure that the member exists, otherwise a 404 happens.
if ($Member -and -not ([string] $memberCaseCorrect = ($tp.GetMembers().Name -eq $Member)[0])) {
Write-Warning "Ignoring member name '$Member', because type '$tp' has no such member."
}
# Transform the full type name to the format used in the URLs.
# '`1' -> '-1'
# System.Environment+SpecialFolder -> 'System.Environment.SpecialFolder'
$typeNameForUrl = $tp.FullName -replace '`', '-' -replace '\+', '.'
"https://learn.microsoft.com/$PSCulture/dotnet/api/$typeNameForUrl" + ('', ".$memberCaseCorrect")[[bool] $memberCaseCorrect] + ('', "?view=$Platform")[[bool] $Platform]
}
if ($PSCmdlet.ShouldProcess("`n" + ($urls -join "`n") + "`n")) {
if ($copyToClipboard) {
# Convert to Markdown links.
$urlsOrMdLinks = if ($CopyLink) {
$urls.ForEach({
$url = $_
$label = $url -replace '^.*/' -replace '\?.+$' -replace '-', '`'; '[`{0}`]({1})' -f $label, $url
})
} else { # $CopyUrl
$urls
}
Write-Verbose "Copying URL(s) as $(('Markdown links', 'URLs')[$CopyUrl.IsPresent]) to clipboard: $urlsOrMdLinks"
# Copy to the clipboard.
Set-Clipboard $urlsOrMdLinks
}
else {
Write-Verbose "Navigating to: $urls"
Start-Process $urls
}
}
} # end
}
# --------------------------------
# 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 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.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment