ObjectHelp.psm1

param()

@"
    NAME
     
        ObjectHelp Extensions Module 0.2 for PowerShell 2.0 CTP3
      
    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 Microsoft
        Live Search.
      
    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-help -obj [int]
 
        # get help against live instances
        PS> $obj = new-object system.xml.xmldocument
        PS> get-help -obj `$obj
 
        or even:
         
        PS> get-help -obj 42
         
        # get help against methods
        PS> get-help -obj `$obj.Load
 
        # explictly try msdn
        PS> get-help -obj [regex] -online
 
        # go to msdn for regex's members
        PS> get-help -obj [regex] -online -members
         
        # pipe support
        PS> 1,[int],[string]::format | get-help -verbose
     
    CREDITS
     
        Author: Oisin Grehan (MVP)
        Blog : http://www.nivot.org/
     
        Have fun!
"@

function Get-LocalizedNamespace {
    param(
        $NameSpace
        ,
        [int]$cultureID = (Get-Culture).LCID
    )

    #First, get a list of all localized namespaces under the current namespace
    $localizedNamespaces = Get-WmiObject -NameSpace $NameSpace -Class "__Namespace" | Where-Object {$_.Name -like "ms_*"}
    if ($null -eq $localizedNamespaces) {
        if (-not $quiet) {
            Write-Warning "Could not get a list of localized namespaces"
        }
        return
    }

    return ("$namespace\ms_{0:x}" -f $cultureID)
}
function Get-WmiClassInfo {
    param(
        [Parameter(Position = 0)]
        [string]$Class
        ,
        [string]$Namespace = "ROOT\cimv2"
        ,
        [int]$CultureID = (Get-Culture).LCID
    )

    $LocalizedNamespace = Get-LocalizedNamespace $Namespace $CultureID
    $ClassLocation = $LocalizedNamespace + ':' + $Class

    $Options = New-Object System.Management.ObjectGetOptions
    $Options.UseAmendedQualifiers = $true

    ## Return
    New-Object System.Management.ManagementClass $ClassLocation,$Options
}
function Search-WmiHelp {
    param(
        [ScriptBlock]$DescriptionExpression = {}
        ,
        [ScriptBlock]$MethodExpression = {}
        , 
        [ScriptBlock]$PropertyExpression = {}
        ,
        $Namespaces = "root\cimv2"
        ,
        $CultureID = (Get-Culture).LCID
        ,
        [switch]$List
    )

    $resultWmiClasses = @{}
   
    foreach ($namespace in $Namespaces) {
        #First, get a list of all localized namespaces under the current namespace
    
        $localizedNamespace = Get-LocalizedNamespace $namespace
        if ($localizedNamespace -eq $null) {
            Write-Verbose "Could not get a list of localized namespaces"
            return
        }

        $localizedClasses = Get-WmiObject -NameSpace $localizedNamespace -Query "select * from meta_class"
        $count = 0
        foreach ($WmiClass in $localizedClasses) {
            $count++
            Write-Progress "Searching Wmi Classes" "$count of $($localizedClasses.Count)" -PercentComplete ($count*100/$localizedClasses.Count)
            $classLocation= $localizedNamespace + ':' + $WmiClass.__Class
            $classInfo = Get-WmiClassInfo $classLocation
            [bool]$found = $false
            if ($classInfo -ne $null) {
                if (! $resultWmiClasses.ContainsKey($classLocation)) {
                    $resultWmiClasses.Add($wmiClass.__Class, $classInfo)
                }

                $descriptionMatch = [bool]($classInfo.Description | Where-Object $DescriptionExpression)
                $methodMatch = [bool]($classInfo.Methods.GetEnumerator() | Where-Object $MethodExpression)
                $propertyMatch = [bool]($classInfo.Properties.GetEnumerator() | Where-Object $PropertyExpression)

                $found = $descriptionMatch -or $methodMatch -or $propertyMatch
                
                if (! $found) {
                    $resultWmiClasses.Remove($WmiClass.__Class)
                }
            }
          }              
    }

    if ($List) {
        $resultWmiClasses.Keys | Sort-Object
    } else {
        $resultWmiClasses.GetEnumerator() | Sort-Object Key
    }
}
function Get-HelpLocation {
    [CmdletBinding()]
    param(
        [System.Type]$Type
    )

    # get documentation filename, assembly location and assembly codebase
    $DocFilename = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetFileName($Type.Assembly.Location), ".xml")
    $Location = [System.IO.Path]::GetDirectoryName($Type.Assembly.Location)
    $CodeBase = (New-Object System.Uri $Type.Assembly.CodeBase).LocalPath

    $PSCmdlet.WriteVerbose("Documentation file is '$DocFilename.'")

    ## try localized location (typically newer than base framework dir)
    $FrameworkDir = "${env:windir}\Microsoft.NET\framework\v2.0.50727"
    $Language = [System.Globalization.CultureInfo]::CurrentUICulture.Parent.Name

    foreach ($Path in "$FrameworkDir\$Language\$DocFilename",
                      "$FrameworkDir\$DocFilename",
                      "$Location\$DocFilename",
                      "$CodeBase\$DocFilename") {
        if (Test-Path $Path) {
            return $Path
        }
    }
    
    # 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
    # }
}
function Get-HelpUri {
    [CmdletBinding()]
    param(
        [System.Type]$Type
        ,
        [String]$Member
    )

    ## Needed for UrlEncode()
    Add-Type -AssemblyName System.Web

    $Vendor = Get-ObjectVendor $Type
    if ($Vendor -like "*Microsoft*") {
        ## drop locale - site will redirect to correct variation based on browser accept-lang
        $Suffix = ""
        if ($Member -eq "_members") {
            $Suffix = "_members"
        } elseif ($Member) {
            $Suffix = ".$Member"
        }

        $Query = [System.Web.HttpUtility]::UrlEncode(("{0}{1}" -f $Type.FullName,$Suffix))
        New-Object System.Uri "http://msdn.microsoft.com/library/$Query.aspx"
    } else {
        $Suffix = ""
        if ($Member -eq "_members") {
            $Suffix = " members"
        } elseif ($Member) {
            $Suffix = ".$Member"
        }

        if ($Vendor) {
            $Query = [System.Web.HttpUtility]::UrlEncode(("`"{0}`" {1}{2}" -f $Vendor,$Type.FullName,$Suffix))
        } else {
            $Query = [System.Web.HttpUtility]::UrlEncode(("{0}{1}" -f $Type.FullName,$Suffix))
        }
        New-Object System.Uri "http://www.bing.com/results.aspx?q=$Query"
    }
}
function Get-ObjectVendor {
    [CmdletBinding()]
    param(
        [System.Type]$Type
        ,
        [switch]$CompanyOnly
    )

    $Assembly = $Type.Assembly
    $attrib = $Assembly.GetCustomAttributes([Reflection.AssemblyCompanyAttribute], $false) | Select-Object -First 1        
    
    if ($attrib.Company) {
        return $attrib.Company
    } else {
        if ($CompanyOnly) { return }

        # try copyright
        $attrib = $Assembly.GetCustomAttributes([Reflection.AssemblyCopyrightAttribute], $false) | Select-Object -First 1
        
        if ($attrib.Copyright) {
            return $attrib.Copyright
        }
    }
    $PSCmdlet.WriteVerbose("Assembly has no [AssemblyCompany] or [AssemblyCopyright] attributes.")
}
function Import-LocalNetHelp {
    [CmdletBinding()]
    param(
        [string]$File
        ,
        [string]$Selector
    )

    try {
        $FileStream = New-Object System.IO.FileStream $File, ([System.IO.FileMode]::Open), ([System.IO.FileAccess]::Read)
        $Reader = New-Object System.Xml.XmlTextReader $FileStream
        $Reader.EntityHandling = [System.Xml.EntityHandling]"ExpandEntities"
        $Document = New-Object System.Xml.XPath.XPathDocument $Reader
        $Navigator = $Document.CreateNavigator()

        # TODO: support overloads
        $Navigator.Select("//member[@name='$Selector' or starts-with(@name,'$Selector(')]") | ForEach-Object {[Xml]$_.OuterXml}
    } finally {
        if ($Reader) {
            $Reader.Close()
        }
    }
}
function Resolve-MemberOwnerType {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [System.Management.Automation.PSMethod]$Method
    )

    # TODO: support overloads, support interface definitions

    $PSCmdlet.WriteVerbose("Resolving owning type of '$($Method.Name)'.")

    # hackety-hack - this is prone to breaking in the future
    $TargetType = [System.Management.Automation.PSMethod].GetField("baseObject", "Instance,NonPublic").GetValue($Method)
    if (($TargetType -isnot [System.Type]) -and (-not $TargetType.__CLASS)) {
        $TargetType = $TargetType.GetType()
    }

    if ($TargetType -is [System.Management.ManagementObject]) {
        $DeclaringType = Get-CimClass $TargetType.__CLASS -Namespace $TargetType.__NAMESPACE
    } else {
        if ($Method.OverloadDefinitions -match "static") {
            $Flags = "Static,Public"
        } else {
            $Flags = "Instance,Public"
        }

        # FIXME: support overloads
        $MethodInfo = $TargetType.GetMethods($Flags) | Where-Object {$_.Name -eq $Method.Name} | Select-Object -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-CimHelp {
    [CmdletBinding(DefaultParameterSetName = "Class")]
    param(
        [Parameter(Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$Class
        ,
        [string]$Namespace = "ROOT\cimv2"
        ,
        [Parameter(ParameterSetName = "Class")]
        [switch]$Detailed
        ,
        [Parameter(ParameterSetName = "Property")]
        [string]$Property
        ,
        [Parameter(ParameterSetName = "Method")]
        [string]$Method
    )

    $CimClass = Get-CimClass $Class -Namespace $Namespace
    $LocalizedClass = Get-WmiClassInfo $Class -Namespace $Namespace

    $HelpObject = New-Object PSObject -Property @{
        Details = New-Object PSObject -Property @{
            Name = $CimClass.CimClassName
            Namespace = $CimClass.CimSystemProperties.Namespace
            SuperClass = $CimClass.CimSuperClass.ToString()
            Description = @($LocalizedClass.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
                $Paragraph = New-Object PSObject -Property @{Text=$_.Trim()}
                $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
                $Paragraph
            })
        }
        Properties = @{}
        Methods = @{}
    }
    $HelpObject.Details.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Details")
    $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo")
    $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim")
    if ($Detailed) {
        $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#DetailedView")
    }

    foreach ($CimProperty in $LocalizedClass.Properties) {
        $PropertyObject = New-Object PSObject -Property @{
            Name = $CimProperty.Name
            Type = $CimProperty.Type
            Description = @($CimProperty.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
                $Paragraph = New-Object PSObject -Property @{Text=$_.Trim()}
                $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
                $Paragraph
            })
        }
        $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#Property")
        $HelpObject.Properties.Add($CimProperty.Name, $PropertyObject)
    }

    foreach ($CimMethod in $CimClass.CimClassMethods) {
        $MethodHelp = $LocalizedClass.Methods[$CimMethod.Name]

        $MethodObject = New-Object PSObject -Property @{
            Name = $CimMethod.Name
            Static = $CimMethod.Qualifiers["Static"].Value
            Constructor = $CimMethod.Qualifiers["Constructor"].Value
            Description = $null
            Parameters = @{}
        }
        $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#Method")

        $MethodObject.Description = @($MethodHelp.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
            $Paragraph = New-Object PSObject -Property @{Text=$_.Trim()}
            $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
            $Paragraph
        })

        $CimMethod.Parameters | ForEach-Object {
            if ($_.Qualifiers["In"]) {
                $MethodObject.Parameters[$_.Name] = New-Object PSObject -Property @{
                    Name = $_.Name
                    Type = $_.CimType
                    ID = [int]$_.Qualifiers["ID"].Value
                    Description = $null
                    In = $true
                }
            }
            if ($_.Qualifiers["Out"]) {
                $MethodObject.Parameters[$_.Name] = New-Object PSObject -Property @{
                    Name = $_.Name
                    Type = $_.CimType
                    ID = [int]$_.Qualifiers["ID"].Value
                    Description = $null
                    In = $false
                }
            }
        }
        $HelpObject.Methods.Add($CimMethod.Name, $MethodObject)
    }

    if ($Property) {
        $PropertyObject = $HelpObject.Properties[$Property]

        if ($PropertyObject) {
            Add-Member -InputObject $PropertyObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
            Add-Member -InputObject $PropertyObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
            Add-Member -InputObject $PropertyObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
            $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#PropertyDetail")
            return $PropertyObject
        } else {
            throw "Property named '$Property' not found."
        }
    } elseif ($Method) {
        $MethodObject = $HelpObject.Methods[$Method]

        if ($MethodObject) {
            Write-Progress "Retrieving Parameter Descriptions"
            $i, $total = 0, $MethodObject.Parameters.Values.Count

            $MethodHelp = $LocalizedClass.Methods[$Method]
            $MethodObject.Parameters.Values | Where-Object {$_.In} | ForEach-Object {
                Write-Progress "Retrieving Parameter Descriptions" -PercentComplete ($i/$total*100); $i++

                $ParameterHelp = $MethodHelp.InParameters.Properties | Where-Object Name -eq $_.Name
                $_.Description = @($ParameterHelp.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
                    $Paragraph = New-Object PSObject -Property @{Text=$_.Trim()}
                    $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
                    if ($Paragraph.Text) {$Paragraph}
                })
            }
            $MethodObject.Parameters.Values | Where-Object {-not $_.In} | ForEach-Object {
                Write-Progress "Retrieving Parameter Descriptions" -PercentComplete ($i/$total*100); $i++

                $ParameterHelp = $MethodHelp.OutParameters.Properties | Where-Object Name -eq $_.Name
                $_.Description = @($ParameterHelp.Qualifiers["Description"].Value -split "`n" | ForEach-Object {
                    $Paragraph = New-Object PSObject -Property @{Text=$_.Trim()}
                    $Paragraph.PSObject.TypeNames.Insert(0, "CimParaTextItem")
                    if ($Paragraph.Text) {$Paragraph}
                })
            }
            Add-Member -InputObject $MethodObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
            Add-Member -InputObject $MethodObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
            Add-Member -InputObject $PropertyObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
            $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Cim#MethodDetail")

            Write-Progress "Retrieving Parameter Descriptions" -Completed

            return $MethodObject
        } else {
            throw "Method named '$Method' not found."
        }
    } else {
        return $HelpObject
    }
}
function Get-NetHelp {
    [CmdletBinding(DefaultParameterSetName = "Class")]
    param(
        [Parameter(Position = 0)]
        [ValidateNotNull()]
        [System.Type]$Type
        ,
        [Parameter(ParameterSetName = "Class")]
        [switch]$Detailed
        ,
        [Parameter(ParameterSetName = "Property")]
        [string]$Property
        ,
        [Parameter(ParameterSetName = "Method")]
        [string]$Method
    )

    # if ($Docs = Get-HelpLocation $Type) {
    # $PSCmdlet.WriteVerbose("Found '$Docs'.")

    # $TypeName = $Type.FullName
    # if ($Method) {
    # $Selector = "M:$TypeName.$Method"
    # } else { ## TODO: Property?
    # $Selector = "T:$TypeName"
    # }

    # ## get summary, if possible
    # $Help = Import-LocalNetHelp $Docs $Selector

    # if ($Help) {
    # $Help #| Format-AssemblyHelp
    # } else {
    # Write-Warning "While some local documentation was found, it was incomplete."
    # }
    # }

    $HelpObject = New-Object PSObject -Property @{
        Details = New-Object PSObject -Property @{
            Name = $Type.Name
            Namespace = $Type.Namespace
            SuperClass = $Type.BaseType
        }
        Properties = @{}
        Constructors = @()
        Methods = @{}
        RelatedLinks = @(
            New-Object PSObject -Property @{Title="Online Version"; Link = Get-HelpUri $Type}
        )
    }
    $HelpObject.Details.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Details")
    $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo")
    if ($Detailed) {
        $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#DetailedView")
        # Write-Error "Local detailed help not available for type '$Type'."
    } else {
        $HelpObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net")
    }

    foreach ($NetProperty in $Type.DeclaredProperties) {
        $PropertyObject = New-Object PSObject -Property @{
            Name = $NetProperty.Name
            Type = $NetProperty.PropertyType
        }
        $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#Property")
        $HelpObject.Properties.Add($NetProperty.Name, $PropertyObject)
    }

    foreach ($NetConstructor in $Type.DeclaredConstructors | Where-Object {$_.IsPublic}) {
        $ConstructorObject = New-Object PSObject -Property @{
            Name = $Type.Name
            Namespace = $Type.Namespace
            Parameters = $NetConstructor.GetParameters()
        }
        $ConstructorObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#Constructor")
        
        $HelpObject.Constructors += $ConstructorObject
    }

    foreach ($NetMethod in $Type.DeclaredMethods | Where-Object {$_.IsPublic -and (-not $_.IsSpecialName)} | Group-Object Name) {
        $MethodObject = New-Object PSObject -Property @{
            Name = $NetMethod.Name
            Static = $NetMethod.Group[0].IsStatic
            Constructor = $NetMethod.Group[0].IsConstructor
            ReturnType = $NetMethod.Group[0].ReturnType
            Overloads = @(
                $NetMethod.Group | ForEach-Object {
                    $MethodOverload = New-Object PSObject -Property @{
                        Name = $NetMethod.Name
                        Static = $_.IsStatic
                        ReturnType = $_.ReturnType
                        Parameters = @(
                            $_.GetParameters() | ForEach-Object {
                                New-Object PSObject -Property @{
                                    Name = $_.Name
                                    ParameterType = $_.ParameterType
                                }
                            }
                        )
                        Class = $HelpObject.Details.Name
                        Namespace = $HelpObject.Details.Namespace
                    }
                    $MethodOverload.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#MethodOverload")
                    $MethodOverload
                }
            )
        }
        $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#Method")
        
        $HelpObject.Methods.Add($NetMethod.Name, $MethodObject)
    }

    if ($Property) {
        $PropertyObject = $HelpObject.Properties[$Property]

        if ($PropertyObject) {
            Add-Member -InputObject $PropertyObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
            Add-Member -InputObject $PropertyObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
            Add-Member -InputObject $PropertyObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
            $PropertyObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#PropertyDetail")
            return $PropertyObject
        } else {
            throw "Property named '$Property' not found."
        }
    } elseif ($Method) {
        $MethodObject = $HelpObject.Methods[$Method]

        if ($MethodObject) {
            Add-Member -InputObject $MethodObject -Name Class -Value $HelpObject.Details.Name -MemberType NoteProperty
            Add-Member -InputObject $MethodObject -Name Namespace -Value $HelpObject.Details.Namespace -MemberType NoteProperty
            Add-Member -InputObject $MethodObject -Name SuperClass -Value $HelpObject.Details.SuperClass -MemberType NoteProperty
            $MethodObject.PSObject.TypeNames.Insert(0, "ObjectHelpInfo#Net#MethodDetail")

            return $MethodObject
        } else {
            throw "Method named '$Method' not found."
        }
    } else {
        return $HelpObject
    }
}
function Get-ObjectHelp {
    [CmdletBinding(DefaultParameterSetName = "Class")]
    param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [PSObject]$Object
        ,
        [Parameter(ParameterSetName = "Class")]
        [switch]$Detailed
        ,
        [Parameter(ParameterSetName = "Method")]
        [string]$Method
        ,
        [Parameter(ParameterSetName = "Property")]
        [string]$Property
        ,
        [Parameter()]
        [switch]$Online
    )

    begin {
        $PSCmdlet.WriteVerbose("Begin")
    }

    process {
        $Type = $null
        $TypeName = $null
        # $Selector = $null

        Write-Verbose "Start processing..."
        Write-Verbose ("Input object (Type:" + $Object.GetType() + ", IsType:" + ($Object -is [System.Type]) + ")")
        if ($Object -is [Management.Automation.PSMemberInfo]) {
            if ($Object -is [System.Management.Automation.PSMethod]) {
                $Method = $Object.Name
                $Type = Resolve-MemberOwnerType $Object
            } else {
                Write-Error "Unable to identify owning time of PSMembers."
                return
            }
        } elseif ($Object -is [Microsoft.PowerShell.Commands.MemberDefinition]) {
            if ($Object.MemberType -eq "Method") {
                $Method = $Object.Name
            } else {
                $Property = $Object.Name
            }
            if ($Object.TypeName -match '^System.Management.ManagementObject#(.+)') {
                $Type = $Object.TypeName
            } else {
                $Type = "$($Object.TypeName)" -as [System.Type]
            }
        } elseif ($Object -is [Microsoft.Management.Infrastructure.CimClass]) {
            $Type = $Object
        } elseif ($Object -is [Microsoft.Management.Infrastructure.CimInstance]) {
            $Type = $Object.PSBase.CimClass
        } elseif ($Object -is [System.Management.ManagementObject]) {
            $Type = Get-CimClass $Object.__CLASS -Namespace $Object.__NAMESPACE
        } elseif ($Object -is [System.__ComObject]) {
            $Type = $Object
        } elseif ($Object -is [System.String]) {
            switch -regex ($Object) {
                '^\[[^\[\]]+\]$' {
                    ## .NET Type (ex: [System.String])
                    try {
                        $Type = Invoke-Expression $Object
                    } catch {
                    }
                    break
                }
                '^(Win32|CIM)_[\w]+' {
                    $Type = Get-CimClass $Object
                }
                ## TODO: WMI / CIM
                Default {}
            }
        } elseif ($Object -as [System.Type]) {
            $Type = $Object -as [System.Type]
        }

        if (-not $Type) {
            Write-Error "Could not identify object"
            return
        }

        Write-Verbose ("Object (Type:" + $Object.GetType() + ", IsType:" + ($Object -is [System.Type]) + ")")
        Write-Verbose ("Method is: $Method")
        Write-Verbose ("Property is: $Property")

        $Culture = $Host.CurrentCulture.Name
        ## TODO: Support culture parameter?

        if ($Type -is [Microsoft.Management.Infrastructure.CimClass]) {
            if ($Online) {
                $TypeName = $Type.CimClassName -replace "_","-"
                if ($Method) {
                    # $Page = "$TypeName#methods"
                    $Page = "$Method-method-in-class-$TypeName"
                } elseif ($Property) {
                    $Page = "$TypeName#properties"
                } else {
                    $Page = $TypeName
                }
                $Uri = "https://docs.microsoft.com/$Culture/windows/desktop/CIMWin32Prov/$Page"
                [System.Diagnostics.Process]::Start($uri) | Out-Null
            } else {
                if ($Method) {
                    Get-CimHelp -Class $Type.CimClassName -Namespace $Type.CimSystemProperties.Namespace -Method $Method
                } elseif ($Property) {
                    Get-CimHelp -Class $Type.CimClassName -Namespace $Type.CimSystemProperties.Namespace -Property $Property
                } else {
                    Get-CimHelp -Class $Type.CimClassName -Namespace $Type.CimSystemProperties.Namespace -Detailed:$Detailed
                }
            }
        } elseif ($Type -is [System.Type]) {
            if ($Online) {
                $Member = if ($Method) {
                        $Method
                    } elseif ($Property) {
                        $Property
                    } else {
                        $null
                    }
                if ($Uri = Get-HelpUri $Type -Member $Member) {
                    [System.Diagnostics.Process]::Start($Uri.ToString()) | Out-Null
                }
            } else {
                if ($Method) {
                    Get-NetHelp -Type $Type -Method $Method
                } elseif ($Property) {
                    Get-NetHelp -Type $Type -Property $Property
                } else {
                    Get-NetHelp -Type $Type -Detailed:$Detailed
                }
            }
        } elseif ($Type -is [System.__ComObject]) {
            if ($Online) {
                if ($Type.PSTypeNames[0] -match 'System\.__ComObject#(.*)$') {
                    if (Test-Path "HKLM:\SOFTWARE\Classes\Interface\$($Matches[1])") {
                        $TypeKey = (Get-ItemProperty "HKLM:\SOFTWARE\Classes\Interface\$($Matches[1])").'(default)'
                        if ('_Application' -contains $TypeKey) {
                            $TypeName = (Get-ItemProperty "HKLM:\SOFTWARE\Classes\TypeLib\$TypeLib\$Version").'(default)'
                        } else {
                            $TypeName = $TypeKey
                        }
                    }
                }
                $Uri = "http://social.msdn.microsoft.com/Search/$Culture/?query=$TypeName"
                [System.Diagnostics.Process]::Start($uri) | Out-Null
            } else {
                Write-Error "Local help not supported for COM objects."
                return
            }
        }
    }
}

New-Alias -Name "ohelp" -Value "Get-ObjectHelp"