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
}
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.
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.
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.
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.
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
}
}
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With