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