Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
January 14, 2025

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
Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.