Private/Get-PSFunctionDocumentation.ps1

Function Get-PSFunctionDocumentation
{
    <#
        .SYNOPSIS
        Converts .ps1 help into a PowerShell object.
 
        .DESCRIPTION
        Parses .ps1 files using Abstract Syntax Tree (AST) to generate a standardized object format for function help files.
 
        The tool is compatible with multiple functions inside the same script file.
 
        .PARAMETER InputObject
        The path to the file to be analyzed. Relative paths are supported.
 
        Object can be also piped from Get-Command. The function must be loaded into your active PowerShell session for this to function properly.
 
        You can also simply pass name the function. The function must be loaded into your active PowerShell session for this to function properly.
 
        .EXAMPLE
        Get-PSFunctionDocumentation -File C:\Path\To\Function.ps1
 
        .EXAMPLE
        Get-PSFunctionDocumentation C:\Path\To\Function.ps1
 
        .EXAMPLE
        Get-PSFunctionDocumentation MyFunction
 
        .EXAMPLE
        Get-Command MyFunction | Get-PSFunctionDocumentation
 
        .EXAMPLE
        Get-ChildItem *.ps1 -Path C:\Path\To\Module -Recurse | Get-PSFunctionDocumentation
 
        .NOTES
        - This function is explicitly made for documenting functions and does not capture script-level parameters or help.
        - This function is designed to be a helper function in a larger tool.
        - This function does not support external help files at this time.
 
    #>


    [CmdletBinding()]
    PARAM
    (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        $InputObject
    )

    #region BEGIN Block
    BEGIN
    {
        # Locally scope ErrorActionPreference for predictable behavior of Try/Catch blocks inside the function
        $ErrorActionPreference = 'Stop'

        # Create output variable
        $Results = [System.Collections.ArrayList]::new()
    }
    #endregion BEGIN Block

    #region PROCESS Block
    PROCESS
    {
        #region Input Handling
        # Execute search based on type of inputobject received

        SWITCH($InputObject.GetType().FullName)
        {
            "System.Management.Automation.FunctionInfo"
            {
                # Object was input from Get-Command ; transform it to a full definition and see if it can be found in PowerShell
                $Filename = $InputObject.Name
                $DefinitionText = "Function $Filename {`n$(Get-Content Function:$Filename)`n}"

                # Query AST
                $AST = [System.Management.Automation.Language.Parser]::ParseInput($DefinitionText,[ref]$null,[ref]$null)
            }

            "System.IO.FileInfo"
            {
                # Check if object is a path
                $Filename = (Resolve-Path $InputObject.Fullname).Path

                # Query AST
                $AST = [System.Management.Automation.Language.Parser]::ParseFile($Filename,[ref]$null, [ref]$null)
            }

            "System.String"
            {
                TRY
                {
                    # Check if object is a path
                    $Filename = (Resolve-Path $InputObject).Path

                    # Query AST
                    $AST = [System.Management.Automation.Language.Parser]::ParseFile($Filename,[ref]$null, [ref]$null)
                }
                CATCH
                {
                    # Object is likely to be a function name ; transform it to a full definition and see if it can be found in PowerShell
                    $Filename = $InputObject
                    $DefinitionText = "Function $Filename {`n$(Get-Content Function:$Filename)`n}"

                    # Query AST
                    $AST = [System.Management.Automation.Language.Parser]::ParseInput($DefinitionText,[ref]$null,[ref]$null)
                }
            }
        }
        #endregion Input Handling

        # $Filename

        # Declare variables
        $Functions     = [System.Collections.ArrayList]::new()
        $FilenameShort = $Filename.Split('\')[-1]

        # Construct predicate to execute search
        $Predicate = {
            PARAM([System.Management.Automation.Language.Ast] $Ast)
            $AST -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
                (
                    $PSVersionTable.PSVersion.Major -lt 5 -or
                    $Ast.Parent -isnot [System.Management.Automation.Language.FunctionMemberAst]
                )
        }

        # Execute AST search
        $FunctionDefinitions = $Ast.FindAll($Predicate, $true)

        FOREACH ($FunctionDefinition in $FunctionDefinitions)
        {
            Write-Verbose "Processing Function $($FunctionDefinition.Name)"

            # Declare variables
            $Syntax      = ''
            $Synopsis    = ''
            $Description = ''
            $Parameters  = ''
            $Notes       = ''
            $Examples    = [System.Collections.ArrayList]::new()

            #region Parameters and Help Block
            TRY
            {
                # Declare variables
                $ParamBlock   = ''
                $CommentBlock = ''
                $HelpFile     = ''

                # Look up existing PARAM block
                $ParamBlock = $FunctionDefinition.Body.ParamBlock.Extent.Text | Out-String

                # Look up existing help block
                IF ($FunctionDefinition.GetHelpContent())
                {
                    $CommentBlock = $FunctionDefinition.GetHelpContent().GetCommentBlock() | Out-String
                }

                # Create an ephemeral function that only contains the help file and parameters
                $ScriptBlock = [scriptblock]::Create(('
                Function {0} {{
                    {1}
                    {2}
                }}'
 -f $FunctionDefinition.Name, $CommentBlock, $ParamBlock))

                # Dot-source the ephemeral function to pull help file into an acceptable PowerShell object format
                $HelpFile = & {. $ScriptBlock ; Get-Help $FunctionDefinition.Name}
            }

            CATCH
            {
                Write-Warning "$($FilenameShort): Unable to generate comment block / help file for $($FunctionDefinition.Name)."
            }
            #endregion Parameters and Help Block

            #region Syntax
            # Dot-source the ephemeral function to query command syntax
            $Syntax = & {. $ScriptBlock ; (Get-Command $FunctionDefinition.Name -Syntax -ErrorAction SilentlyContinue)}

            # Trim carriage returns
            $Syntax = $Syntax.Split("`r`n") | ?{$_ -ne ""}
            $Syntax = $Syntax -Join "`r`n"

            $EscapedSyntax = $Syntax -replace "\[", "``[" -replace "\]", "``]"
            #endregion Syntax

            #region Synopsis
            # Trim carriage returns
            $Synopsis = $HelpFile.Synopsis.Replace("`n", "").Replace("`r", "")

            # Blank Synopsis field if it is populated by auto-generated syntax
            IF ($Synopsis -like "*$EscapedSyntax*")
            {
                Write-Warning "$($FilenameShort): Field for $($FunctionDefinition.Name) is blank: Synopsis."
                $Synopsis = ""
            }
            ELSE
            {
                $Synopsis = $HelpFile.Synopsis
            }
            #endregion Synopsis

            #region Description
            IF (!$HelpFile.Description)
            {
                Write-Warning "$($FilenameShort): Field is blank: Description. Using synopsis field to auto-populate if it exists."
                $Description = $HelpFile.Synopsis
            }
            ELSE
            {
                $Description = $HelpFile.Description
            }
            #endregion Description

            #region Parameters
            IF(!$HelpFile.Parameters.Parameter.Description)
            {
                Write-Warning "$($FilenameShort): Field for $($FunctionDefinition.Name) is blank: Parameters."
                $Parameters = ''
            }
            ELSE
            {
                $Parameters = $HelpFile.Parameters.Parameter
            }
            #endregion Parameters

            #region Examples
            IF (!$HelpFile.Examples -or !$HelpFile.Examples.Example.Description)
            {
                Write-Warning "$($FilenameShort): Field for $($FunctionDefinition.Name) is blank: Examples."
                $Examples = ''
            }
            ELSE
            {
                # Add properly formatted examples to $Examples object
                FOREACH ($Entry in $HelpFile.Examples.Example)
                {
                    # Add the rest of the help object if there's a multi-line example
                    IF ($Entry.Remarks.Text -match "\S")
                    {
                        $Example = [PSCustomObject]@{
                            Title        = $Entry.Title.TrimStart('-').TrimEnd('-').Trim(' ')
                            Description  = $Entry.Code + "`n" + $Entry.Remarks.Text
                        }
                    }
                    ELSE
                    {
                        $Example = [PSCustomObject]@{
                            Title        = $Entry.Title.TrimStart('-').TrimEnd('-').Trim(' ')
                            Description  = $Entry.Code
                        }
                    }
                    $Examples.Add($Example) | Out-Null
                }
            }
            #endregion Examples

            #region Notes
            IF(!$HelpFile.AlertSet)
            {
                Write-Warning "$($FilenameShort): Field for $($FunctionDefinition.Name) is blank: Notes."
                $Notes = ''
            }
            ELSE
            {
                $Notes = $HelpFile.AlertSet.Alert.Text
            }
            #endregion Notes

            # Construct function object and add to larger functions object
            $Function = [PSCustomObject]@{
                Name        = $FunctionDefinition.Name
                FileName    = $Filename
                Synopsis    = $Synopsis
                Description = $Description
                Parameters  = $Parameters
                Syntax      = $Syntax
                Examples    = $Examples
                Notes       = $Notes
            }

            $Functions.Add($Function) | Out-Null
        }

        $Results.Add($Functions) | Out-Null
    }
    #endregion PROCESS Block

    #region END Block
    END
    {
        Return $Results
    }
    #endregion END Block
}