Using the AST to Find Module Dependencies in PowerShell Functions and Scripts
Earlier this week, Chris Gardner presented a session on Managing dependencies in PowerShell for the Mississippi PowerShell User Group. I mentioned that I had written a function to retrieve PowerShell module dependencies that's part of my ModuleBuildTools module.
Get-MrAST
is one of the primary functions that numerous other functions in the module are built
on.
1#Requires -Version 3.0
2function Get-MrAst {
3
4<#
5.SYNOPSIS
6 Explores the Abstract Syntax Tree (AST).
7
8.DESCRIPTION
9 Get-MrAST is an advanced function that provides a mechanism for exploring the Abstract Syntax Tree (AST).
10
11 .PARAMETER Path
12 Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory.
13
14.PARAMETER Code
15 The code to view the AST for. If Get-Content is being used to obtain the code, use its -Raw parameter otherwise
16 the formating of the code will be lost.
17
18.PARAMETER ScriptBlock
19 An instance of System.Management.Automation.ScriptBlock Microsoft .NET Framework type to view the AST for.
20
21.PARAMETER AstType
22 The type of object to view the AST for. If this parameter is ommited, only the top level ScriptBlockAst is returned.
23
24.EXAMPLE
25 Get-MrAST -Path 'C:\Scripts' -AstType FunctionDefinition
26
27.EXAMPLE
28 Get-MrAST -Code 'function Get-PowerShellProcess {Get-Process -Name PowerShell}'
29
30.EXAMPLE
31 Get-MrAST -ScriptBlock ([scriptblock]::Create('function Get-PowerShellProcess {Get-Process -Name PowerShell}'))
32
33.NOTES
34 Author: Mike F Robbins
35 Website: http://mikefrobbins.com
36 Twitter: @mikefrobbins
37#>
38
39 [CmdletBinding(DefaultParameterSetName='Path')]
40 param(
41 [Parameter(ValueFromPipeline,
42 ValueFromPipelineByPropertyName,
43 ValueFromRemainingArguments,
44 ParameterSetName = 'Path',
45 Position = 1)]
46 [ValidateNotNull()]
47 [Alias('FilePath')]
48 [string[]]$Path = ('.\*.ps1', '.\*.psm1'),
49
50 [Parameter(Mandatory,
51 ValueFromPipelineByPropertyName,
52 ValueFromRemainingArguments,
53 ParameterSetName = 'Code')]
54 [string[]]$Code,
55
56 [Parameter(Mandatory,
57 ValueFromPipelineByPropertyName,
58 ValueFromRemainingArguments,
59 ParameterSetName = 'ScriptBlock')]
60 [scriptblock[]]$ScriptBlock
61 )
62
63 DynamicParam {
64 $ParameterAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute
65 $ParameterAttribute.Position = 0
66
67 $ValidationValues = Get-MrAstType
68 $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($ValidationValues)
69
70 $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
71 $AttributeCollection.Add($ParameterAttribute)
72 $AttributeCollection.Add($ValidateSetAttribute)
73
74 $ParameterName = 'AstType'
75 $RuntimeParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
76
77 $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
78 $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
79 $RuntimeParameterDictionary
80 }
81
82 BEGIN {
83 $AstType = $PsBoundParameters[$ParameterName]
84 }
85
86 PROCESS {
87 switch ($PSCmdlet.ParameterSetName) {
88 'Path' {
89 Write-Verbose -Message 'Path Parameter Set Selected'
90 Write-Verbose "Path contains $Path"
91
92 $Files = Get-ChildItem -Path $Path -Exclude *tests.ps1, *profile.ps1 |
93 Select-Object -ExpandProperty FullName
94
95 if (-not ($Files)) {
96 Write-Warning -Message 'No valid files found.'
97 Return
98 }
99
100 $AST = foreach ($File in $Files) {
101 [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$null, [ref]$null)
102 }
103
104 break
105 }
106 'Code' {
107 Write-Verbose -Message 'Code Parameter Set Selected'
108
109 $AST = foreach ($c in $Code) {
110 [System.Management.Automation.Language.Parser]::ParseInput($c, [ref]$null, [ref]$null)
111 }
112
113 break
114 }
115 'ScriptBlock' {
116 Write-Verbose -Message 'ScriptBlock Parameter Set Selected'
117
118 $AST = $ScriptBlock.Ast
119
120 break
121 }
122 default {
123 Write-Warning -Message 'An unexpected error has occurred'
124 }
125 }
126
127 if ($PsBoundParameters.AstType) {
128 Write-Verbose -Message 'AstType Parameter Entered'
129
130 $AST = $AST.FindAll({$args[0].GetType().Name -like "$($ASTType)Ast"}, $true)
131 }
132
133 Write-Output $AST
134 }
135
136}
It retrieves the AST from one or more PS1 and/or PSM1 files, script blocks, or random arbitrary code.
1Get-MrAst -Path .\Hyper-V\MrHyperV\public\
It can also be used to only retrieve specific AST types such as the definition of one or more functions.
1Get-MrAst -Path .\Hyper-V\MrHyperV\public\ -AstType FunctionDefinition
Get-MrPrivateCommand
is another function in the module which is used to show private functions in
a module.
1#Requires -Version 3.0
2function Get-MrPrivateCommand {
3
4<#
5.SYNOPSIS
6 Returns a list of private (unexported) commands from the specified module or snap-in.
7
8.DESCRIPTION
9 Get-MrPrivateFunction is an advanced function that returns a list of private commands
10 that are not exported from the specified module or snap-in.
11
12.PARAMETER Module
13 Specify the name of a module. Enter the name of a module or snap-in, or a snap-in or module
14 object. This parameter takes string values, but the value of this parameter can also be a
15 PSModuleInfo or PSSnapinInfo object, such as the objects that the Get-Module, Get-PSSnapin,
16 and Import-PSSession cmdlets return.
17
18.EXAMPLE
19 Get-MrPrivateCommand -Module Pester
20
21.NOTES
22 Author: Mike F Robbins
23 Website: http://mikefrobbins.com
24 Twitter: @mikefrobbins
25#>
26
27 [CmdletBinding()]
28 param (
29 [Parameter(Mandatory)]
30 [Alias('PSSnapin')]
31 [string]$Module
32 )
33
34 if (-not((Get-Module -Name $Module -OutVariable ModuleInfo))){
35 try {
36 $ModuleInfo = Import-Module -Name $Module -Force -PassThru -ErrorAction Stop
37 }
38 catch {
39 Write-Warning -Message "$_.Exception.Message"
40 Break
41 }
42 }
43
44 $Global:ModuleName = $Module
45
46 $All = $ModuleInfo.Invoke({Get-Command -Module $ModuleName -All})
47
48 $Exported = (Get-Module -Name $Module -All).ExportedCommands |
49 Select-Object -ExpandProperty Values
50
51 if ($All -and $Exported) {
52 Compare-Object -ReferenceObject $All -DifferenceObject $Exported |
53 Select-Object -ExpandProperty InputObject |
54 Add-Member -MemberType NoteProperty -Name Visibility -Value Private -Force -PassThru
55 }
56}
Notice the private function named Get-MrFunctionRequirment
.
1Get-MrPrivateCommand -Module MrModuleBuildTools
Get-MrFunctionRequirment
is a helper function that's sandwiched between Get-MrAST
and Get-MrRequiredModule
.
1function Get-MrFunctionRequirement {
2 [CmdletBinding(DefaultParameterSetName='File')]
3 param(
4 [Parameter(ValueFromPipeline,
5 ValueFromPipelineByPropertyName,
6 ValueFromRemainingArguments,
7 ParameterSetName = 'File',
8 Position = 0)]
9 [ValidateNotNullOrEmpty()]
10 [Alias('FilePath')]
11 [string[]]$Path = ('.\*.ps1', '.\*.psm1'),
12
13 [Parameter(ValueFromPipelineByPropertyName,
14 ValueFromRemainingArguments,
15 ParameterSetName = 'Code',
16 Position = 0)]
17 [ValidateNotNull()]
18 [Alias('ScriptBlock')]
19 [string[]]$Code
20 )
21
22 PROCESS {
23 if ($PSBoundParameters.Path) {
24 Write-Verbose 'Path'
25 $Results = Get-MrAST -Path $Path
26 }
27 elseif ($PSBoundParameters.Code) {
28 Write-Verbose 'Code'
29 $Results = Get-MrAST -Code $Code
30 }
31 else {
32 Write-Verbose -Message 'Valid input not received.'
33 }
34
35 $Results | Select-Object -ExpandProperty ScriptRequirements | Sort-Object -Property * -Unique
36 }
37
38}
I'll use a little known trick in this example to run the Get-MrFunctionRequirment
private function
from outside of the module by invoking it within module scope. This is the same trick I use to
determine the private functions in a module by comparing the exported commands to the ones that
exist in module scope.
1(Get-Module -Name MrModuleBuildTools).Invoke({Get-MrFunctionRequirement -Path .\Hyper-V\MrHyperV\public\})
The Get-MrRequiredModule
function is used to determine the required modules for one or more files
or blocks of code.
1#Requires -Version 3.0
2function Get-MrRequiredModule {
3
4<#
5.SYNOPSIS
6 Gets a list of the required modules.
7
8.DESCRIPTION
9 Get-MrRequiredModule is an advanced function that returns a list of the required module dependencies from one or more
10 PS1 and/or PSM1 files.
11
12 .PARAMETER Path
13 Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory.
14
15.PARAMETER Code
16 The code to get the required modules for. If Get-Content is being used to obtain the code, use its -Raw parameter
17 otherwise the formating of the code will be lost.
18
19.PARAMETER Detailed
20 Return a detailed list of all of the modules including built-in modules that are required. This option does not
21 reply on a Requires statement.
22
23.EXAMPLE
24 Get-MrRequiredModule -Path 'C:\Scripts'
25
26.EXAMPLE
27 Get-MrRequiredModule -Code 'function Get-PowerShellProcess {Get-Process -Name PowerShell}' -Detailed
28
29.NOTES
30 Author: Mike F Robbins
31 Website: http://mikefrobbins.com
32 Twitter: @mikefrobbins
33#>
34
35 [CmdletBinding(DefaultParameterSetName='File')]
36 param(
37 [Parameter(ValueFromPipeline,
38 ValueFromPipelineByPropertyName,
39 ValueFromRemainingArguments,
40 ParameterSetName = 'File',
41 Position = 0)]
42 [ValidateNotNullOrEmpty()]
43 [Alias('FilePath')]
44 [string[]]$Path = ('.\*.ps1', '.\*.psm1'),
45
46 [Parameter(ValueFromPipelineByPropertyName,
47 ValueFromRemainingArguments,
48 ParameterSetName = 'Code',
49 Position = 0)]
50 [ValidateNotNull()]
51 [Alias('ScriptBlock')]
52 [string[]]$Code,
53
54 [switch]$Detailed
55 )
56
57 PROCESS{
58 if (-not($PSBoundParameters.Detailed)) {
59 (Get-MrFunctionRequirement -Path $Path |
60 Select-Object -ExpandProperty RequiredModules -Unique).Name
61 }
62 else {
63 $PSBoundParameters.Remove('Detailed') | Out-Null
64 $AllAST = Get-MrAst @PSBoundParameters
65
66 foreach ($AST in $AllAST){
67 $FunctionDefinition = $AST.FindAll({$args[0].GetType().Name -like 'FunctionDefinitionAst'}, $true)
68 $Commands = $AST.FindAll({$args[0].GetType().Name -like 'CommandAst'}, $true) | ForEach-Object {$_.CommandElements[0].Value} | Select-Object -Unique
69
70 foreach ($Command in $Commands){
71 [pscustomobject]@{
72 Function = $FunctionDefinition.Name
73 Dependency = $Command
74 Module = (Get-Command -Name $Command -ErrorAction SilentlyContinue).Source
75 }
76 }
77
78 }
79
80 }
81 }
82}
When the Get-MrRequiredModule
is used without its Detailed
parameter, it simply returns what's
listed in the Requires
statement of the individual PS1 and/or PSM1 files.
1Get-MrRequiredModule -Path .\Hyper-V\MrHyperV\public\
When its Detailed
parameter is specified, it retrieves a list of all the commands used in the
files and then returns the name of the function along with the module that the command is part of
including built-in modules. This does require the module to exist on your system, but it's great for
figuring out what to add to the Requires
statement. Of course, there's no reason to add built-in
modules to the requires statement.
1Get-MrRequiredModule -Path .\Hyper-V\MrHyperV\public\ -Detailed
As you can see, the previous example also shows how easy it would be to retrieve a list of all the commands used in a function or script with the AST for auditing purposes. Want to know if someone wrote a function that uses Write-Host? That would be no problem whatsoever for an entire module or hundreds of functions or scripts.
The MrModuleBuildTools PowerShell module along with all code found in this blog article can be found in my ModuleBuildTools repository on GitHub.
µ