Modules/GetHelp/Pscx.GetHelp.psm1
#requires -Version 3 param([string[]]$PreCacheList) if (!(Test-Path variable:\helpCache) -or $RefreshCache) { $SCRIPT:helpCache = @{} } function Resolve-MemberOwnerType { [CmdletBinding()] param ( [system.management.automation.psmethod]$method ) # TODO: support overloads, support interface definitions $PSCmdlet.WriteVerbose("Resolving $($method.name)'s owning Type.") # hackety-hack - this is prone to breaking in the future $targetType = [system.management.automation.psmethod].getfield("baseObject", "Instance,NonPublic").getvalue($method) # [system.runtimetype] is special-cased in powershell - you can't reference it? if (-not ($targetType.GetType().fullname -eq "System.RuntimeType")) { $targetType = $targetType.GetType() } if ($method.OverloadDefinitions -match "static") { $flags = "Static,Public" } else { $flags = "Instance,Public" } # FIXME: support overloads $methodInfo = $targetType.GetMethods($flags) | ?{$_.Name -eq $method.Name}| select -first 1 if (-not $methodInfo) { # this shouldn't happen. throw "Could not resolve owning type!" } $declaringType = $methodInfo.DeclaringType $PSCmdlet.WriteVerbose("Owning Type is $($targetType.fullname). Method declared on $($declaringType.fullname).") $declaringType } function Get-DocsLocation { [CmdletBinding()] param ( [type]$type, [switch]$Online, [switch]$Members, [switch]$Static ) # get documentation filename, assembly location and assembly codebase $docFilename = [io.path]::changeextension([io.path]::getfilename($type.assembly.location), ".xml") $location = [io.path]::getdirectoryname($type.assembly.location) $codebase = (new-object uri $type.assembly.codebase).localpath $PSCmdlet.WriteVerbose("Documentation file is $docFilename") if (-not $Online.IsPresent) { # try localized location (typically newer than base framework dir) $frameworkDir = "${env:windir}\Microsoft.NET\framework\v2.0.50727" $lang = [system.globalization.cultureinfo]::CurrentUICulture.parent.name # I love looking at this. A Duff's Device for PowerShell.. well, maybe not. switch ( "${frameworkdir}\${lang}\$docFilename", "${frameworkdir}\$docFilename", "$location\$docFilename", "$codebase\$docFilename" ) { { test-path $_ } { $_; return; } default { # try next path continue; } } } # failed to find local docs, is it from MS? if ((Get-ObjectVendor $type) -like "*Microsoft*") { # drop locale - site will redirect to correct variation based on browser accept-lang $suffix = "" if ($Members.IsPresent) { $suffix = "_members" } new-object uri ("http://msdn.microsoft.com/library/{0}{1}.aspx" -f $type.fullname,$suffix) return } $PSCmdlet.WriteWarning("Sorry, I couldn't find any local documentation for ${type}.") } # Dig out something that might lead us to the vendor of this Object function Get-ObjectVendor { [CmdletBinding()] param ( [type]$type, [switch]$CompanyOnly ) $assembly = $type.assembly $attrib = $assembly.GetCustomAttributes([Reflection.AssemblyCompanyAttribute], $false) | select -first 1 if ($attrib.Company) { # try company $attrib.Company return } else { if ($CompanyOnly) { return } # try copyright $attrib = $assembly.GetCustomAttributes([Reflection.AssemblyCopyrightAttribute], $false) | select -first 1 if ($attrib.Copyright) { $attrib.Copyright return } } $PSCmdlet.WriteVerbose("Assembly has no [AssemblyCompany] or [AssemblyCopyright] attributes.") } function Get-HelpSummary { [CmdletBinding()] param ( [string]$file, [reflection.assembly]$assembly, [string]$selector ) if ($helpCache.ContainsKey($assembly)) { $xml = $helpCache[$assembly] $PSCmdlet.WriteVerbose("Docs were found in the cache.") } else { # cache it Write-Progress -id 1 "Caching Help Documentation" $assembly.getname().name # cache this for future lookups. It's a giant pig. Oink. $xml = [xml](gc $file) $helpCache.Add($assembly, $xml) Write-Progress -id 1 "Caching Help Documentation" $assembly.getname().name -completed } $PSCmdlet.WriteVerbose("Selector is $selector") # TODO: support overloads $summary = $xml.doc.members.SelectSingleNode("member[@name='$selector' or starts-with(@name,'$selector(')]").summary $summary } function Show-Help { @" SYNTAX $((get-help get-objecthelp).split([char]13) | % { "$_" }) "@ } function Get-ObjectHelp { [CmdletBinding()] param ( [Parameter(ValueFromPipeline=$true, Mandatory=$true)] [ValidateNotNull()] $Object, [Parameter()] [switch]$Online, [Parameter()] [switch]$Member, [Parameter()] [switch]$Static ) process { if ($Object -is [string]) { $PSCmdlet.WriteVerbose("A string was passed - reparsing as expression.") # they probably meant to pass the string inside '(' and ').' try { # e.g. "[int]::gettype" was passed without being wrapped # in new evaluative parentheses. $Object = invoke-expression $Object } catch { if ($_.fullyqualifiederrorid -eq "TypeNotFound,Microsoft.PowerShell.Commands.InvokeExpressionCommand") { $PSCmdlet.WriteWarning("I don't recognize the Type in ${InputObject}. Are you sure you've typed it correctly?") } else { $PSCmdlet.WriteWarning("A string was passed and was parsed as an expression, and failed. " + "If you really meant to find help on strings, pass [string] instead.") } $PSCmdlet.WriteVerbose($_) return } } $type = $Object.GetType() $PSCmdlet.WriteVerbose("InputObject Type is $($type.Fullname)") $selector = $null # won't work with $type; case statements don't match with type literals? switch ($type.FullName) { "System.RuntimeType" { $PSCmdlet.WriteVerbose("[runtimetype]") $type = $Object $selector = "T:$($type.FullName)" break; } "System.Management.Automation.PSMethod" { $PSCmdlet.WriteVerbose("[psmethod]") $type = Resolve-MemberOwnerType $Object # TODO: support overloaded methods $selector = "M:$($type.FullName).$($Object.Name)" break; } default { $PSCmdlet.WriteVerbose("[object]") $selector = "T:$($type.FullName)" } } # do we have an assembly help xml somewhere? $docs = Get-DocsLocation $type -Online:$Online.IsPresent -Members:$Member.IsPresent -Static:$Static.IsPresent if ($docs) { $PSCmdlet.WriteVerbose("Found $docs") if ($docs -is [uri]) { # Could not find local xml, but object is from Microsoft. Offer to view MSDN. $title = "Microsoft Developer Network" $message = "No local help for $($type.fullname).`n`nDo you want to visit this object's documentation page on MSDN?" $options = [System.Management.Automation.Host.ChoiceDescription[]]("&Yes", "&No") $result = $host.ui.PromptForChoice($title, $message, $options, 0) if ($result -eq 0) { Start-Process $docs -Verb Open > $null } return } # get summary, if possible $summary = Get-HelpSummary $docs $type.assembly $selector if ($summary) { [string]::empty # TODO: parse out <see ...> tags and create a PromptForChoice list to lookup referenced type(s). if ($summary.selectnodes) { $see = $summary.selectnodes("see") } if (($Object -eq 42) -and (!$PSCmdlet.Force)) { "What do you get if you multiply six by nine?" [string]::empty "That's it. That's all there is." } else { $text = & { if ($summary.innerxml) { $summary.innerxml.trim() } else { $summary.trim() } } # strip <see ... /> tags $text -replace [regex]'<see.*?"?:(.*?)"\s/>', '$1' } if ((Test-Path Variable:\see) -and $see) { #Show-References # TODO: list of <see cref="foo" /> types } [string]::empty } else { Write-Host "While some local documentation was found, it was incomplete. Sorry!" } } else { Write-Host "Sorry, I couldn't find any local documentation for ${type}." $vendor = Get-ObjectVendor $type -CompanyOnly if ($vendor) { # needed for urlencode add-type -a system.web write-host "However, it looks like the vendor of this Object is '${vendor}.'" $title = "Bing Search" $message = "Do you want to search for this object's documentation?" $options = [System.Management.Automation.Host.ChoiceDescription[]]("&Yes", "&No") $result = $host.ui.PromptForChoice($title, $message, $options, 0) if ($result -eq 0) { # encode our question $q = [system.web.httputility]::urlencode(("`"{0}`" {1}" -f $vendor, $type)) # fire up the browser [diagnostics.process]::Start("http://www.bing.com/results.aspx?q=$q") } } } } } # cache common assembly help function Preload-Documentation { if ($SCRIPT:helpCache.Keys.Count -eq 0) { # mscorlib $file = Get-DocsLocation ([int]) Get-HelpSummary $file ([int].assembly) "T:System.Int32" > $null # system $file = Get-DocsLocation ([regex]) Get-HelpSummary $file ([regex].assembly) "T:System.Regex" > $null } } <# .ForwardHelpTargetName Get-Help .ForwardHelpCategory Cmdlet #> function Get-PscxHelp { # our proxy command generated from [proxycommand]::create((gcm get-help)) [CmdletBinding(DefaultParameterSetName='AllUsersView', HelpUri='http://go.microsoft.com/fwlink/?LinkID=113316')] param( [Parameter(Position=0, ValueFromPipelineByPropertyName=$true)] [System.String] ${Name}, [System.String] ${Path}, [System.String[]] ${Category}, [System.String[]] ${Component}, [System.String[]] ${Functionality}, [System.String[]] ${Role}, [Parameter(ParameterSetName='DetailedView', Mandatory=$true)] [Switch] ${Detailed}, [Parameter(ParameterSetName='AllUsersView')] [Switch] ${Full}, [Parameter(ParameterSetName='Examples', Mandatory=$true)] [Switch] ${Examples}, [Parameter(ParameterSetName='Parameters', Mandatory=$true)] [System.String] ${Parameter}, [Parameter(ParameterSetName='ObjectHelp', ValueFromPipeline = $true, Mandatory = $true)] [ValidateNotNullOrEmpty()] ${Object}, [Parameter(ParameterSetName='ObjectHelp')] [Switch] ${Member}, [Parameter(ParameterSetName='ObjectHelp')] [Switch] ${Static}, [Parameter(ParameterSetName='ObjectHelp')] [Parameter(ParameterSetName='Online', Mandatory=$true)] [switch] ${Online}, [Parameter(ParameterSetName='ShowWindow', Mandatory=$true)] [switch] ${ShowWindow} ) begin { try { if ($PSCmdlet.ParameterSetName -eq "ObjectHelp") { Preload-Documentation $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-ObjectHelp', [System.Management.Automation.CommandTypes]::Function) $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) } else { # Working around a bug in PowerShell (try man -?) where it passes in the wrong category info for aliases. if ($Name -ne $null) { $commandInfo = try { Microsoft.PowerShell.Core\Get-Command $Name -ErrorAction SilentlyContinue } catch { Write-Warning "Error calling Get-Command on ${Name}: $_" } if ($commandInfo -ne $null) { $isAlias = $commandInfo.CommandType -eq 'Alias' if ($isAlias) { $PSBoundParameters['Category'] = 'Alias' } } } $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer) -and $outBuffer -gt 1024) { $PSBoundParameters['OutBuffer'] = 1024 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Core\Get-Help', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) } $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } } Export-ModuleMember Get-PscxHelp <# NAME ObjectHelp Extensions Module 0.3 for PowerShell 2.0 SYNOPSIS Get-Help -Object allows you to display usage and summary help for .NET Types and Members. DETAILED DESCRIPTION Get-Help -Object allows you to display usage and summary help for .NET Types and Members. If local documentation is not found and the object vendor is Microsoft, you will be directed to MSDN online to the correct page. If the vendor is not Microsoft and vendor information exists on the owning assembly, you will be prompted to search for information using Bing. TODO * localize strings into PSD1 file * Implement caching in hashtables. XMLDocuments are fat pigs. * Support getting property/field help * PowerTab integration? * Test with Strict Parser EXAMPLES # get help on a type PS> Get-PscxHelp -obj [int] # get help against live instances PS> $obj = new-object system.xml.xmldocument PS> Get-PscxHelp -obj `$obj or even: PS> Get-PscxHelp -obj 42 # get help against methods PS> Get-PscxHelp -obj `$obj.Load # explictly try msdn PS> Get-PscxHelp -obj [regex] -online # go to msdn for regex's members PS> Get-PscxHelp -obj [regex] -online -member # pipe support PS> 1,[int],[string]::format | Get-PscxHelp -verbose CREDITS Author: Oisin Grehan (MVP) Blog : http://www.nivot.org/ Have fun! #> |