Creating AST-Based PowerShell Tools
Over the last few articles, we've been exploring the world of Abstract Syntax Trees (ASTs) in PowerShell. We've seen how to parse PowerShell scripts into ASTs and how to leverage the information in the AST to discover how PowerShell code is structured. I want to return to a scripting perspective and do a little more with a function I showed in an earlier article that will get script requirement information.
#requires -version 5.1
Function Get-RequiredMetadata {
[cmdletbinding()]
[OutputType('ScriptRequirementMetadata')]
Param(
[Parameter(
Position = 0,
Mandatory,
ValueFromPipeline,
HelpMessage = 'The path to the .PS1 script file'
)]
[ValidatePattern('.*\.ps1$')]
[ValidateScript({ Test-Path $_ })]
[string]$Path
)
Begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
New-Variable astTokens -Force
New-Variable astErr -Force
$progSplat = @{
Activity = $MyInvocation.MyCommand
Status = 'Starting'
}
Write-Progress @progSplat
}
Process {
Write-Verbose "Processing $Path"
$progSplat.Status = "Processing $Path"
Write-Progress @progSplat
$AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)
$hash = [ordered]@{
PSTypeName = 'ScriptRequirementMetadata'
Path = $Path
RequiredVersion = $Null
RequiresAdmin = $False
IsCore = $False
LastWriteTime = (Get-Item $Path).LastWriteTime
}
If ($AST.ScriptRequirements) {
#save requiredVersion as a string if found
if ($AST.ScriptRequirements.RequiredPSVersion -match '\d') {
$hash.RequiredVersion = $AST.ScriptRequirements.RequiredPSVersion -as [string]
}
$hash.RequiresAdmin = $AST.ScriptRequirements.IsElevationRequired
if ($ast.ScriptRequirements.RequiredPSVersion -gt '5.1') {
$hash.IsCore = $True
}
}
#write a custom object to the pipeline
[PSCustomObject]$hash
}
End {
$progSplat['Completed'] = $True
Write-Progress @progSplat
Write-Verbose "Ending $($MyInvocation.MyCommand)"
}
}
This function uses the AST to create custom object that contains information about the script file's requirements.
PS C:\> Get-RequiredMetadata C:\scripts\SharedProfileDefault.ps1
Path : C:\scripts\SharedProfileDefault.ps1
RequiredVersion : 5.1
RequiresAdmin : False
IsCore : False
LastWriteTime : 12/12/2024 3:05:16 PM
Let's take this idea and run with it. You could use the concepts and techniques with other scripting projects, not just AST-related commands.
Defining a Class
My function writes an object to the pipeline. I am going to convert this to a PowerShell class.
Class ScriptRequirementMetadata {
[string]$Path
[string]$RequiredVersion
[bool]$RequiresAdmin
[string[]]$RequiredModules
[bool]$IsCore
[bool]$IsEmpty
[datetime]$LastWriteTime
[string]$ComputerName = [System.Environment]::MachineName
ScriptRequirementMetadata($Path) {
$this.Path = $Path
$this.LastWriteTime = (Get-Item $Path).LastWriteTime
$ast = _getAST -Path $Path
$this.RequiredVersion = $ast.RequiredVersion
$this.RequiresAdmin = $ast.RequiresAdmin
$this.RequiredModules = $ast.RequiredModules
$this.IsEmpty = ($this.RequiredModules.Count -eq 0) -AND ($null -eq $this.RequiredVersion) -AND (-not $this.RequiresAdmin)
$this.IsCore = [version]$ast.RequiredVersion -gt [version]'5.1'
}
} #class
I've added a few more properties to the class, such as required modules, the computername, and if there are any requirements at all. I can validate the properties with the Get-TypeMember
command.
PS C:\> Get-TypeMember ScriptRequirementMetadata
Type: ScriptRequirementMetadata
Name MemberType ResultType IsStatic IsEnum
---- ---------- ---------- -------- ------
GetType Method Type
ComputerName Property String
IsCore Property Boolean
IsEmpty Property Boolean
LastWriteTime Property DateTime
Path Property String
RequiredModules Property String[]
RequiredVersion Property String
RequiresAdmin Property Boolean