After a long period of time, only writing about event-driven topics, I thought it was time for something else. Let’s dive into something that isn’t very well-known by a lot of people, but can be very powerful to get a better understanding of it.

After a lot of coffee and staring at the sky, this topic popped up! “AST”, I already hear you thinking ‘What the @*#@&% is AST?!’. Well, AST is the abbreviation for “PowerShell abstract Syntax tree”; it is basically a structured tree presentation of the PowerShell code when it’s being parsed by the engine.

AST doesn’t treat your PowerShell code as text; it’s giving you a semantic model of the code (cmdlets, variables, expressions, pipelines, loops, methods, etc).

All this makes it extremely useful for doing things like:

  • Code analysis with linting
  • Custom tooling
  • Code generation
  • Static code analysis
  • Security analysis

🎬 See this one? Time to stop relaxing and get your hands dirty!

📒 This one? Well, this one is there for highlighting things to you. Whether I believe extra information is required about a section, I’ll post it here.

Enough smooth talk! Let’s get started!

The first encounter!

When PowerShell runs your code, it doesn’t just execute the text line by line, it first parses the code and builds something called an Abstract Syntax Tree (AST). Think of the AST as a structured, tree-like map that represents the meaning of your code rather than its exact text. Each command, variable, and operator becomes a node in this tree, which PowerShell uses to understand what your script is doing. What’s really interesting is that this AST doesn’t just exist behind the scenes, you can actually access it yourself! Whenever PowerShell parses a script or a function, it keeps the AST in memory as an object, allowing you to explore, analyze, or even manipulate the structure of your code programmatically. This opens up powerful possibilities for building code analyzers, documentation generators, or custom script tools directly in PowerShell.

Lets see it in action!

🎬Run the code below in your Favorite IDE

Get-Process | Where-Object {$_.CPU -gt 100 }

Nothing exiting right? We should see the output below:

Very simple, and effective. Showing all processes on the system where the value of CPU is greater than 100. (Which are quite a lot 😉)

But let’s now spice it up, let’s utilize this same code and see what AST does for us in this case.

🎬 Run the code below

$code = { Get-Process | Where-Object { $_.CPU -gt 100 } }
$code.Invoke()

And we’ll see that the result remains the same

📒 You might experience this as slower. This is because the invoke command needs to create the appropriate PowerShell environment in order to execute the actual ‘variable code’ now.

So you’ve now seen that the code which we have as a variable can also be executed by invoking it. Let’s now get the AST!

🎬 In the same Shell call the variable holding the code like below

$code.ast

You should now see the outcome of the AST;

But don’t be fooled, this single line represents the root node of AST. Every piece of PowerShell code is now visible and can be used for further processing. Consider the output of the AST like opening a book and landing on the index page. You don’t see every chapter in detail, but what you do see is the starting point, and from there you can drill down in different sections!

From a dedicated script

So far we’ve only looked at a single line of code, which is a nice start, but when working in real life scenarios you will often run into big scripts. These scripts often contain methods, variables, classes, parameters and a lot more! So in this section we’ll dive into what AST can do when working with standalone separate scripts.

🎬 Copy the code below and save this to a standalone PowerShell file

$threshold = 50
$logFile   = "processReport.txt"
$services  = Get-Service

function Test-Process {
    param([string]$Name)

    $procs = Get-Process | Where-Object { $_.ProcessName -eq $Name }

    if(-not (Test-Path $logFile)) {
        New-Item $logFile -ItemType File | Out-Null
    }

    if ($procs.Count -eq 0) {
        Write-Warning "Process $Name not found."
    }

    else {
        foreach ($p in $procs) {
            if ($p.CPU -gt $threshold) {
                "$($p.ProcessName) is using more than $threshold CPU!" | Out-File -FilePath $logFile -Append
            }
            else {
                "$($p.ProcessName) looks fine." | Out-File -FilePath $logFile -Append
            }
        }
    }
}
Test-Process -Name "powershell"

🎬 Save the file with a name like ‘example.ps1’

📒 Because this code is now in an external file, we need to get it differently. The code itself doesn’t ‘live’ in the current shell session but lives in its own context.

🎬 Run the code below from a terminal that is already in the directory where you saved the example script

$errors = $null
$tokens = $null
$ast    = [System.Management.Automation.Language.Parser]::ParseFile(
    "{FULL_PATH_TO_AST}",
    [ref]$tokens,
    [ref]$errors
)

⚠️ Make sure you modify the FULL_PATH_TO_AST to have the full path to the PowerShell file you want tot check the AST! E.G; If your file is on C:\Scripts\Example.ps1 include the full path like that.

The ‘[System.Management.Automation.Language.Parser]’ is a .NET implementation. If you want to know more about this you can find more information here;

https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.language.parser.parsefile?view=powershellsdk-7.4.0#system-management-automation-language-parser-parsefile(system-string-system-management-automation-language-token()@-system-management-automation-language-parseerror()@)

If you now check the content of the $ast variable you should see something like below;

What we can do now?

Alright, we’ve got ourselves a full AST from the script. If you look closely, it’s like peeking inside a toolbox: there are functions, commands, pipelines… and of course, variables sprinkled all over the place.

Grabbing variables

Variables are the lifeblood of any script, they hold your data, they carry values around, and they’re often the first thing you’ll want to inspect when analyzing someone else’s code. Imagine opening up a recipe book: before cooking, you’d first scan the list of ingredients. That’s exactly what we’ll do here, gather all the variable names from our AST.

🎬 Run the code below from the same session as where the AST is loaded

$variables = $ast.FindAll({$args[0] -is [System.Management.Automation.Language.VariableExpressionAst]}, $true)

And you should get the result as shown below;

Grabbing functions

The same we can do for grabbing functions. They contain the logic of your script and can expose valuable information!

🎬 Run the code below from the same session as where the AST is loaded

$functions = $ast.FindAll({$args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]}, $true)

And you should get the result as shown below

A use case

Now lets build a very small use case around what we’ve just learned. Let’s make a mini code analyzer and analyze the script to see if it contains things we don’t want 😉

🎬 Store the code below which is a basic analyzer in a file so we can run it with powershell (analyzer.ps1)

function Test-Script {
    param(
        [string]$ScriptContent,
        [string[]]$VariableNames,
        [string[]]$NonApprovedCmdlets = @('New-Item')
    )
    
    # Parse the script content into AST
    $tokens = $null
    $errors = $null
    $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptContent, [ref]$tokens, [ref]$errors)
    
    # Check for variables
    $foundVariables = @()
    if ($VariableNames) {
        $allVariables = $ast.FindAll({$args[0] -is [System.Management.Automation.Language.VariableExpressionAst]}, $true)
        $foundVariables = $allVariables | Where-Object { $_.VariablePath.UserPath -in $VariableNames }
    }
    
    # Check for non-approved cmdlets
    $foundCmdlets = @()
    if ($NonApprovedCmdlets) {
        $allCmdlets = $ast.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)
        $foundCmdlets = $allCmdlets | Where-Object { $_.CommandElements[0].Value -in $NonApprovedCmdlets }
    }
    
    return @{
        FoundVariables = $foundVariables
        FoundCmdlets = $foundCmdlets
        HasIssues = ($foundVariables.Count -gt 0 -or $foundCmdlets.Count -gt 0)
    }
}

# Example usage:
$scriptContent = Get-Content "example.ps1" -Raw
$result = Test-Script -ScriptContent $scriptContent -VariableNames @('threshold', 'logFile') -NonApprovedCmdlets @('New-Item', 'Where-Object')

Write-Host "Found variables: $($result.FoundVariables.Count)"
Write-Host "Found non-approved cmdlets: $($result.FoundCmdlets.Count)"
Write-Host "Has issues: $($result.HasIssues)"

🎬 Now execute it like below

./minianalyzer.ps1

And you will get the output as shown below;

😉 cool right? We have now created our own mini code analyzer!

Real-World Applications 💡

At this point you might be thinking: “Cool, but what’s this good for in the real world?”
Here are a few things you could actually build with AST:

  • Script auditing: Find all uses of dangerous commands like Invoke-Expression or New-Object Net.WebClient.
  • Refactoring tools: Replace aliases with full cmdlet names, or enforce naming conventions.
  • Documentation generators: Extract all function names and their parameters from a script.
  • Custom linters: Flag things like Write-Host, missing error handling, or other style issues.

📒 In fact, tools like PSScriptAnalyzer (the official PowerShell linter) rely heavily on AST behind the scenes.

Summary

In this post we dove into the world of PowerShell AST (Abstract Syntax Tree) 🌳

Instead of just treating scripts as plain text, AST gives us a structured, semantic view of the code, letting us inspect cmdlets, variables, pipelines, loops, and more.

From there:

  • We explored how to grab the AST from a simple inline command and from full-fledged scripts.
  • Variables and functions could be extracted effortlessly.
  • Scripts could be analyzed for non-approved cmdlets or suspicious patterns.
  • Potential issues or dangerous commands were flagged automatically.

And by parsing the AST, we essentially opened the script like a toolbox, seeing all its inner parts without executing them. Super handy for:

  • Script auditing and security checks
  • Custom linters and style enforcement
  • Documentation generators
  • Refactoring tools

This turns your PowerShell session into a code detective, inspecting scripts safely and intelligently. Perfect for:

  • Spotting risky commands or patterns
  • Auditing and understanding large scripts
  • Automating code quality checks

No guesswork, no manual reading… just pure AST-powered insight ✨

Now you’ve got the foundation for your own PowerShell code analyzer. Go ahead, explore, extract, and analyze!

And remember, have some fun doing so 😉

Leave a Reply

Your email address will not be published. Required fields are marked *