As you create larger and larger PowerShell scripts, developing the habit of organizing them into separate files and modules is important for two reasons. Managing complexity and maintaining the ability to reason about what you’ve built.
One system I wrote contained:
- 105 PowerShell script files
- 151 functions
- Over 4 KLOCs (read Kay-Locks, meaning 4,000 lines of code).
Compared to the PowerShell Pack WPK set of scripts having:
- 73 files
- 722 functions
- More than 100 KLOCs
Maybe you keep your scripts in separate directory and each are for a different task.
Select-String – Finds text in strings and files
While Select-String is a useful and must know cmdlet, if searching for a function name you will find that name when it is contained in a comment or random string. Sometimes you don’t want to see that.
Get-Function Finds and Returns Only Function Names
Get-Function searches all PowerShell files from the current directory and returns only function names. Plus, you can provide a partial name and Get-Function will do a match. The following output is the result of running Get-Function in the $PSHOME\Modules\PSDiagnostics directory.
FunctionName FileName Line
------------ -------- ----
Start-Trace C:\Windows\System32\Wind... 20
Stop-Trace C:\Windows\System32\Wind... 96
Enable-WSManTrace C:\Windows\System32\Wind... 122
Disable-WSManTrace C:\Windows\System32\Wind... 152
Enable-PSWSManCombinedTrace C:\Windows\System32\Wind... 157
Disable-PSWSManCombinedTrace C:\Windows\System32\Wind... 179
Set-LogProperties C:\Windows\System32\Wind... 184
ConvertTo-Bool C:\Windows\System32\Wind... 223
Get-LogProperties C:\Windows\System32\Wind... 235
Enable-PSTrace C:\Windows\System32\Wind... 262
Disable-PSTrace C:\Windows\System32\Wind... 269
The Script
Get-Function recursively searches from the current directory (the default that can be overridden), then uses the PowerShell tokenizer (see line 8 ) to select only script statements that are functions and returns the name of that function (see lines 13 and 14). It creates and emits to the pipeline a PSObject that holds the FileName, FunctionName and Line Number. Finally, the where cmdlet is used to filter only the function names you’d like, if provided.
As with other PowerShell output, the results from Get-Function can be saved to a file, piped to Out-GridView or piped to other cmdlets like Group-Object.
Function Get-Function ($pattern, $path="$pwd") { $parser = [System.Management.Automation.PSParser] Get-ChildItem $path -Recurse -Include *.ps1, *.psm1 | ForEach { $content = [IO.File]::ReadAllText($_.FullName) $tokens = $parser::Tokenize($content, [ref] $null) $count = $tokens.Count $( for($idx=0; $idx -lt $count; $idx += 1) { if($tokens[$idx].Content -eq 'function') { $targetToken = $tokens[$idx+1] New-Object PSObject -Property @{ FileName = $_.FullName FunctionName = $targetToken.Content Line = $targetToken.StartLine } | Select FunctionName, FileName, Line } } ) | Where {$_.FunctionName -match $pattern} } }





{ 12 comments… read them below or add one }
How about this:
Get-Command -CommandType Function -Name “Your-FunctionNamr” | Select-Object -ExpandProperty Module | Format-List
Thanks for the comment Matt.
That solution works great when they are loaded into memory. I intended to use this for finding functions in files that are not running.
For example, sometimes you need to work with/extend legacy scripts you inherit. In those cases I like to figure out the organization of the scripts and modules. This is a type of ‘static’ analysis.
Ah ha! Excellent!
Very nice, Doug.
I modified it to not do a recursive search by default:
Function Get-Function ($pattern, $path=”$pwd\*”, [switch]$Recurse = $false) {
$parser = [System.Management.Automation.PSParser]
Get-ChildItem $path -Recurse:$Recurse -Include *.ps1, *.psm1 | ForEach {
$content = [IO.File]::ReadAllText($_.FullName)
$tokens = $parser::Tokenize($content, [ref] $null)
$count = $tokens.Count
$(
for($idx=0; $idx -lt $count; $idx += 1) {
if($tokens[$idx].Content -eq ‘function’) {
$targetToken = $tokens[$idx+1]
New-Object PSObject -Property @{
FileName = $_.FullName
FunctionName = $targetToken.Content
Line = $targetToken.StartLine
} | Select FunctionName, FileName, Line
}
}
) | Where {$_.FunctionName -match $pattern}
}
}
Karl
Thanks Karl. Good idea to let the parameters flow through to the underlying cmdlet and let the user of the script have more control.
This is very useful. Thanks for this.
My favorite use would be ‘Get-Function | ogv’
Thanks Thorsten.
Here is a better version of the in memory lookup:
Get-Command -CommandType Function | Select-Object Name,@{n=”File”;e={$_.ScriptBlock.File}},@{n=”Line”;e={$_.ScriptBlock.StartPosition.StartLine}}
You could add that as another feature to your function. Make another parameter set for passing a function name.
Thanks Jason. Yes, that will make a nice addition.
Hi Doug,
Pathological case admitedly: this will fail if there’s content between the function keyword and the function name (e.g. a comment). Here’s a version using $Foreach.movenext() that iterates over tokens after the function keyword until a CommandArgument token is found.
Cheers,
Chris Warwick
http://chrisjwarwick.wordpress.com/
@cjwarwickps
Function Get-Function ($Pattern, $Path=”$pwd”) {
Get-ChildItem $path -Recurse -Include *.ps1, *.psm1 |%{
$Content = [IO.File]::ReadAllText($_.FullName)
$Tokens = [Management.Automation.PSParser]::Tokenize($Content,[ref]$Null)
$(Foreach ($Token in $Tokens) {
If ($Token.Type -eq ‘Keyword’ -And $Token.Content -eq ‘function’){
Do {$More=$Foreach.MoveNext()} Until ($Foreach.Current.Type -eq ‘CommandArgument’ -or !$More)
New-Object PSObject -Property @{
Filename=$_.FullName
Line=$Token.Startline
FunctionName=$Foreach.Current.Content
}
}
})|?{$_.FunctionName -Match $Pattern}
}
}
Thanks Chris. Glad you caught an edge condition. Thanks again for posting it.
Doug
I know this is an old post, but I have found that piping a call to Select-String before the Foreach-Object and then only tokenizing the one line results in a spped increase of about 3 times, according to Measure-Command!