Zachary Loeber

The personal website of Zachary Loeber.

PowerShell: Comment Based Help Generation

2015-10-03 7 min read Powershell Zachary Loeber

Here is a quick couple of functions I put together which will use AST to pull all the functions, their parameters, and associated parameter attributes and create a template comment based help from the results.

I’d been meaning to put this one together for a little while now. If you ever hacked together a bunch of PowerShell code and had to go back later and add comment based help you already know how tedious the process can be. This function should make it slightly less cumbersome. It pulls the ‘HelpMessage’ parameter attributes and automatically populates the parameter sections of a CBH comment block string.

Just pipe your code in raw form into this function to get an array of psobjects containing function names and comment based help strings in return. This only works for fully defined functions right now (my primary use case). It will take some modification to get it to work on script file based parameters. Anyway, the code is in my github repository as well as below.

function New-CommentBasedHelp {
    <#
    .SYNOPSIS
        Create comment based help for a function.
    .DESCRIPTION
        Create comment based help for a function.
    .PARAMETER Code
        Multi-line or piped lines of code to process.
    .PARAMETER Advanced
        The default CBH result is good for most scenarios. Using this switch returns a more advanced CBH string.
    .EXAMPLE
       PS > $testfile = 'C:\temp\test.ps1'
       PS > $test = Get-Content $testfile -raw
       PS > $test | New-CommentBasedHelp | clip
       
       Description
       -----------
       Takes C:\temp\test.ps1 as input, creates basic comment based help and puts the result in the clipboard 
       to be pasted elsewhere for review.
    .EXAMPLE
        PS > $CBH = Get-Content 'C:\EWSModule\Get-EWSContact.ps1' -Raw | New-CommentBasedHelp -Verbose -Advanced
        PS > ($CBH | Where {$FunctionName -eq 'Get-EWSContact'}).CBH
        
        Description
        -----------
        Consumes Get-EWSContact.ps1 and generates advanced CBH templates for all functions found within. Print out to the screen the advanced
        CBH for just the Get-EWSContact function.
    .NOTES
       Author: Zachary Loeber
       
       Requires: Powershell 3.0

       Version History
       1.0.0 - Initial release
    #>
    [CmdletBinding()]
    param(
        [parameter(Position=0, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')]
        [string[]]$Code,
        [parameter(Position=0, HelpMessage='The default CBH result is good for most scenarios. Using this switch returns a more advanced CBH string.')]
        [switch]$Advanced
    )
    begin {
        $FunctionName = $MyInvocation.MyCommand.Name
        Write-Verbose "$($FunctionName): Begin."
        
        function Get-FunctionParameters {
            <#
            .SYNOPSIS
                Return all parameters for each function found in a code block.
            .DESCRIPTION
                Return all parameters for each function found in a code block.
            .PARAMETER Code
                Multi-line or piped lines of code to process.
            .EXAMPLE
               PS > $testfile = 'C:\temp\test.ps1'
               PS > $test = Get-Content $testfile -raw
               PS > $test | Get-FunctionParameters | clip
               
               Description
               -----------
               Takes C:\temp\test.ps1 as input, remove statement separators and puts the result in the clipboard 
               to be pasted elsewhere for review.

            .NOTES
               Author: Zachary Loeber
               
               Requires: Powershell 3.0

               Version History
               1.0.0 - Initial release
            #>
            [CmdletBinding()]
            param(
                [parameter(Position=0, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')]
                [string[]]$Code
            )
            begin {
                #Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
                $FunctionName = $MyInvocation.MyCommand.Name
                Write-Verbose "$($FunctionName): Begin."
                
                $Codeblock = @()
                $ParseError = $null
                $Tokens = $null

                # These are essentially our AST filters
                $functionpredicate = { ($args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]) }
                $parampredicate = { ($args[0] -is [System.Management.Automation.Language.ParameterAst]) }
                $typepredicate = { ($args[0] -is [System.Management.Automation.Language.TypeConstraintAst]) }
                $paramattributes = { ($args[0] -is [System.Management.Automation.Language.NamedAttributeArgumentAst]) }
                $output = @()

            }
            process {
                $Codeblock += $Code
            }
            end {
                $ScriptText = $Codeblock | Out-String
                Write-Verbose "$($FunctionName): Attempting to parse AST."

                $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [ref]$Tokens, [ref]$ParseError) 
         
                if($ParseError) {
                    $ParseError | Write-Error
                    throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry."
                }

                $functions = $ast.FindAll($functionpredicate, $true)
                
                # get the begin and end positions of every for loop
                foreach ($function in $functions) {
                    Write-Verbose "$($FunctionName): Processing function - $($function.Name.ToString())"
                    $Parameters = $function.FindAll($parampredicate, $true)
                    foreach ($p in $Parameters) {
                        $ParamType = $p.FindAll($typepredicate, $true)
                        Write-Verbose "$($FunctionName): Processing Parameter of type [$($ParamType.typeName.FullName)] - $($p.Name.VariablePath.ToString())"
                        $OutProps = @{
                            'FunctionName' = $function.Name.ToString()
                            'ParameterName' = $p.Name.VariablePath.ToString()
                            'ParameterType' = $ParamType[0].typeName.FullName
                        }
                        $p.FindAll($paramattributes, $true) | Foreach {
                            $OutProps.($_.ArgumentName) = $_.Argument.Value
                        }
                        $Output += New-Object -TypeName PSObject -Property $OutProps
                    }
                }

                $Output
                Write-Verbose "$($FunctionName): End."
            }
        }
        $CBH_PARAM = @'
.PARAMETER %%PARAM%%
%%PARAMHELP%%
'@

        $FullCBH = @'
<#
.SYNOPSIS
    A brief description of the function or script.

.DESCRIPTION
    A detailed description of the function or script.

%%PARAMETER%%
.EXAMPLE

.EXAMPLE

.EXAMPLE

.INPUTS
    The Microsoft .NET Framework types of objects that can be piped to the
    function or script. You can also include a description of the input
    objects.

.OUTPUTS
    The .NET Framework type of the objects that the cmdlet returns. You can
    also include a description of the returned objects.

.NOTES
    Additional information about the function or script.

.LINK

.LINK

.COMPONENT
    The technology or feature that the function or script uses, or to which
    it is related.

.ROLE
    The user role for the help topic. This content appears when the Get-Help
    command includes the Role parameter of Get-Help.

.FUNCTIONALITY
    The intended use of the function. This content appears when the Get-Help
    command includes the Functionality parameter of Get-Help.

.FORWARDHELPTARGETNAME 
    Redirects to the help topic for the specified command. You can redirect
    users to any help topic, including help topics for a function, script,
    cmdlet, or provider.

.FORWARDHELPCATEGORY  
    Specifies the help category of the item in ForwardHelpTargetName.
    Valid values are Alias, Cmdlet, HelpFile, Function, Provider, General,
    FAQ, Glossary, ScriptCommand, ExternalScript, Filter, or All. Use this
    keyword to avoid conflicts when there are commands with the same name.

.REMOTEHELPRUNSPACE 
    Specifies a session that contains the help topic. Enter a variable that
    contains a PSSession. This keyword is used by the Export-PSSession
    cmdlet to find the help topics for the exported commands.

.EXTERNALHELP  
    Specifies an XML-based help file for the script or function.

    The ExternalHelp keyword is required when a function or script
    is documented in XML files. Without this keyword, Get-Help cannot
    find the XML-based help file for the function or script.

    The ExternalHelp keyword takes precedence over other comment-based
    help keywords. If ExternalHelp is present, Get-Help does not display
    comment-based help, even if it cannot find a help topic that matches
    the value of the ExternalHelp keyword.

    If the function is exported by a module, set the value of the
    ExternalHelp keyword to a file name without a path. Get-Help looks for
    the specified file name in a language-specific subdirectory of the module
    directory. There are no requirements for the name of the XML-based help
    file for a function, but a best practice is to use the following format:
    -help.xml

    If the function is not included in a module, include a path to the
    XML-based help file. If the value includes a path and the path contains
    UI-culture-specific subdirectories, Get-Help searches the subdirectories
    recursively for an XML file with the name of the script or function in
    accordance with the language fallback standards established for Windows,
    just as it does in a module directory.

    For more information about the cmdlet help XML-based help file format,
    see "How to Create Cmdlet Help" in the MSDN (Microsoft Developer Network)
    library at http://go.microsoft.com/fwlink/?LinkID=123415.
#>
'@

        $BasicCBH = @'
<#
.SYNOPSIS

.DESCRIPTION

%%PARAMETER%%
.EXAMPLE

.EXAMPLE

.EXAMPLE

.INPUTS

.OUTPUTS

.NOTES

.LINK

.LINK

#>
'@

        $Codeblock = @()
        $CBHResults = @()
    }
    process {
        $Codeblock += $Code
    }
    end {
        $ScriptText = $Codeblock | Out-String
        Write-Verbose "$($FunctionName): Attempting to parse parameters."
        $AllParams = Get-FunctionParameters -Code $Codeblock | Sort-Object -Property FunctionName
        $AllFunctions = @($AllParams.FunctionName | Select -unique)
        
        foreach ($f in $AllFunctions) {
            $OutCBH = @{}
            $OutCBH.'FunctionName' = $f
            [string]$OutParams = ''
            $fparams = @($AllParams | Where {$_.FunctionName -eq $f} | Sort-Object -Property Position)
            $fparams | foreach {
                $ParamHelpMessage = if ([string]::IsNullOrEmpty($_.HelpMessage)) {"`n`r"} else {"`t" + $_.HelpMessage + "`n`r`n`r"}
                $OutParams += $CBH_PARAM -replace '%%PARAM%%',$_.ParameterName -replace '%%PARAMHELP%%',$ParamHelpMessage
            }
            if ($Advanced) {
                $OutCBH.'CBH' = $FullCBH -replace '%%PARAMETER%%',$OutParams
            }
            else {
                $OutCBH.'CBH' = $BasicCBH -replace '%%PARAMETER%%',$OutParams
            }
            New-Object PSObject -Property $OutCBH
        }

        Write-Verbose "$($FunctionName): End."
    }
}
comments powered by Disqus