Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get All Functions In A PowerShell Script

Tags:

powershell

I have a problem similar to this question. I want to get all the functions in a given PowerShell script but the difference being I don't want to execute the contents of the script and I don't want to execute the functions.

The intent is to be able to load all the functions into the runspace to be able to pull the comment-based help from each function for documentation purposes.

Does anyone have any magical tricks to just load the functions from a .ps1 without executing all the other code within that file?

I thought about using [System.Management.Automation.PSParser]::Tokenize() to parse the script file but that's a whole lot more work than I would like to do. If someone has something easier, I'd be delighted.

# I want to load this to get the comment-based help
Function Invoke-Stuff {
    <#
    .SYNOPSIS
       Stuff doer
    .DESCRIPTION
       It does lots of stuff
    .EXAMPLE
       Invoke-Stuff
    #>
    Write-Host "Stuff was done"
}

# But I don't want to execute any of this
$Items = Get-ChildItem
$Items | ForEach-Object {
    Invoke-Stuff
}
like image 442
SomeShinyObject Avatar asked Aug 29 '17 00:08

SomeShinyObject


People also ask

How do I get all PowerShell commands?

Description. The Get-Command cmdlet gets all commands that are installed on the computer, including cmdlets, aliases, functions, filters, scripts, and applications. Get-Command gets the commands from PowerShell modules and commands that were imported from other sessions.

How can you display all the options for a specific command using PowerShell?

Enter the name of one command, such as the name of a cmdlet, function, or CIM command. If you omit this parameter, Show-Command displays a command window that lists all of the PowerShell commands in all modules installed on the computer.

How do I get the contents of a function in PowerShell?

To use the Get-Content cmdlet to display the contents of a function, you enter Get-Content and supply the path to the function. All functions available to the current Windows PowerShell environment are available via the Function Windows PowerShell drive.

How do you call a function in a PowerShell script?

A function in PowerShell is declared with the function keyword followed by the function name and then an open and closing curly brace. The code that the function will execute is contained within those curly braces.


2 Answers

The AST is the way to go for static(ish) analysis. Here's how I would do what you described

$rs = [runspacefactory]::CreateRunspace()
$rs.Open()

# Get the AST of the file
$tokens = $errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
    'MyScript.ps1',
    [ref]$tokens,
    [ref]$errors)

# Get only function definition ASTs
$functionDefinitions = $ast.FindAll({
    param([System.Management.Automation.Language.Ast] $Ast)

    $Ast -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
    # Class methods have a FunctionDefinitionAst under them as well, but we don't want them.
    ($PSVersionTable.PSVersion.Major -lt 5 -or
    $Ast.Parent -isnot [System.Management.Automation.Language.FunctionMemberAst])

}, $true)

# Add the functions into the runspace
$functionDefinitions | ForEach-Object {
    $rs.SessionStateProxy.InvokeProvider.Item.Set(
        'function:\{0}' -f $_.Name,
        $_.Body.GetScriptBlock()) 
}

# Get help within the runspace.
$ps = [powershell]::Create().AddScript('Get-Help MyFunction')
try {
    $ps.Runspace = $rs
    $ps.Invoke()
} finally {
    $ps.Dispose()
}

You could also use the $tokens from near the top if you want to go a purely static route. The comments won't be in the AST but they will be in the tokens.

Edit The method above actually loses comment help somewhere in the process, not because of the runspace but just because of how the function is assigned. Likely due to the comments not really being a part of the AST. In any case there is a more direct and more static way to obtain the help.

Instead of defining the functions, you can use the GetHelpContent method on FunctionDefinitionAst

$helpContent = $functionDefinitions | ForEach-Object { $_.GetHelpContent() }

This will return a CommentHelpInfo object for each function. It's important to note that this is not the same object returned by the Get-Help cmdlet. Most notably it does not distinguish between things like the code and the description in an example block. However, if you need the CBH to be parsed as normal you can get the comment block text and define your own fake version.

$helpContent = $functionDefinitions | ForEach-Object {

    # Get the plain string comment block from the AST.
    $commentBlock = $_.GetHelpContent().GetCommentBlock()

    # Create a scriptblock that defines a blank version of the
    # function with the CBH. You may lose some parameter info
    # here, if you need that replace param() with
    # $_.Body.ParamBlock.Extent.Text
    $scriptBlock = [scriptblock]::Create(('
    function {0} {{
        {1}
        param()
    }}' -f $_.Name, $commentBlock))

    # Dot source the scriptblock in a different scope so we can
    # get the help content but still not pollute the session.
    & {
        . $scriptBlock

        Get-Help $_.Name
    }
}
like image 111
Patrick Meinecke Avatar answered Sep 27 '22 21:09

Patrick Meinecke


Ideally, you'd have your functions in a module (or their own script file), which you could load. Your 'execution' script would then be it's own thing that you only run when you do want to execute the function, or you run the functions manually.

If you had your functions in a module, in one of the paths that PowerShell looks to for them, you'd be able to run this to see the functions:

Get-Command -Module Example -CommandType Function

Most of the time you wouldn't include the CommandType parameter, unless there was extra stuff in the module you didn't care about.

This module approach (or separation of function/execution) would be the only way to get your comment based help working as you'd expect it to work.

If you're happy to just see the names of your functions, you'd have to load the content of the script file, and check for lines starting with the function keyword. There's probably smarter ways to do this, but that's where my mind went to.


Just to be a bit more clear about what I'm getting at around separating the functions from the execution code, it'd look something like:

functions.ps1

# I want to load this to get the comment-based help
Function Invoke-Stuff {
    <#
    .SYNOPSIS
       Stuff doer
    .DESCRIPTION
       It does lots of stuff
    .EXAMPLE
       Invoke-Stuff
    #>
    Write-Host "Stuff was done"
}

You can then freely load the function into your session, making the comment based help accessible.

execute.ps1

. .\path\functions.ps1

# But I don't want to execute any of this
$Items = Get-ChildItem
$Items | ForEach-Object {
    Invoke-Stuff
}
like image 40
Windos Avatar answered Sep 27 '22 22:09

Windos