Search-Script
Finding Needles in Haystacks with the AST

PowerShell is a pretty interesting language.
One of the ways it is interesting is that you can access the Abstract Syntax Tree. Another thing that's interesting is that you can convert any [ScriptBlock] into any [func].
Put these two together, and PowerShell can succinctly search itself.
That's the foundation of a simple little module I just updated, SearchScript
Let's learn how to search our scripts
How to Search Scripts
Most languages use an abstract syntax tree (AST) to represent the code you want to run. PowerShell is nice enough to let you easily access it.
Let's imagine we wanted to find out what types a script uses.
We could try to do this with regular expressions. We would not be happy. It's much easier to ask PowerShell.
We can access the Ast of any script block by using the .Ast property.
{"hello world"}.AstWe can get the members of any Ast by piping to Get-Member
{"hello world"}.Ast | Get-MemberThere's a couple of methods Find and FindAll. Find finds the first matching element. FindAll finds all of them (optionally recursively).
I almost always find myself using .FindAll, but they're both there if we need them.
FindAll takes a Func[Management.Automation.Language.Ast,bool] predicate (fancy speak for "condition").
But how do we make a Func?
We don't have to!
PowerShell does it for us. Let's see the nodes in a simple list:
{"hello","goodbye"}.Ast.FindAll({param($ast) return $true}, $true)Let's do it again, but this time only find elements whose .Value is 'hello'
{"hello","goodbye"}.Ast.FindAll({param($ast) return $ast.Value -eq 'hello'}, $true)How do we search scripts? We provide a [ScriptBlock] to find nodes within a [ScriptBlock].
This is quite handy! We can use this to find needles in haystacks.
Search-Script
We all love a useful function, so let's abstract this all a bit.
Search-Script is an eponymous module. It contains only one command, Search-Script (and a bunch of aliases to it).
All it accepts is:
- A ``-Script` to search
- Something to search
-For - An optional
[switch]for-Shallowsearches
-For is a little special. We can accept multiple types of values for -For.
If it's a [ScriptBlock] we just call .FindAll.
If it's not a [ScriptBlock], we can make it into one.
Search-Script -For ([string])
If it's a [string], we'll try an exact match, unless it starts and ends with slashes.
Here's the current code:
# If `-For` is a `[string]`
if ($for -is [string]) {
# the operator is -eq by default.
$operator = '-eq'
# If it takes the form of a regex literal
if ($for -match '^/.+/$') {
# strip the slashes
$for =
$for -replace '^/' -replace '/$'
# and match instead.
$operator = '-match'
}
# Always double single quotes to avoid code injection.
$For = $for -replace "'","''"
# Create a `[Scriptblock]` that finds exactly that string.
$for = [ScriptBlock]::Create("param(`$ast) (`$ast.Extent.ToString() $operator '$(
$For
)') -or (`$ast.Value $operator '$For')")
}Search-Script -For ([regex])
If it's a [Regex], we'll try to match it.
Here's the current code:
# If `-For` is a `[Regex]`
if ($for -is [Regex]) {
$for =
# Create a `[ScriptBlock]` that matches that pattern.
[ScriptBlock]::Create("param(`$ast) `$pattern = [Regex]::new('$(
# Always double single quotes to avoid code injection.
$for -replace "'","''"
)','$($for.Options)'); `$ast -match `$pattern")
}Search-Script -For ([type])
If it's a [type], we'll try to find all instances of that type.
It's that last one that gets a little complicated.
Sure, we could just look for AST types. That would be easy. But we can also ask anything with a .TypeName to give us a type via reflection (and any static references will have a .StaticType). To make matters even more fun, equality comparison doesn't quite cut it for types. We have to check if a type is a subclass of a type. Oh, yeah, then there are interfaces. We have to check that if the type implements the interface.
It's just a bit more complicated than it's kin. Here's the current code:
if ($For -as [type[]]) {
$for =
# Create a `[ScriptBlock]` that looks for that type.
# This one is more complicated, so we will create it in two parts
[ScriptBlock]::Create((
(@(
# dynamically create the list of types
'param($ast)'
"`$types = @("
foreach ($forType in $for) {
$forType = $forType -as [type]
if (-not $forType) { continue }
"[$($forType.FullName)]"
}
")"
) -join [Environment]::NewLine) + {
# Find a reflected type, if there is one.
$reflectedType =
if ($ast.TypeName.GetReflectionType) {
$ast.TypeName.GetReflectionType()
} elseif ($ast.StaticType) {
$ast.StaticType
} else {
$null
}
# Go over each of our potential types
# Several conditions would be a use of our type
foreach ($type in $types) {
# * If the ast is that type, return true
if ($ast -is $type) { return $true }
if (-not $reflectedType) { continue }
# * If the reflected type is exactly that type, return true
if ($reflectedType -eq $type) { return $true }
# * If the reflected type is a subclass of that type, return true
if ($reflectedType.IsSubClassOf($type)) { return $true }
# * If the type is an interface,
# return true if the reflected type implements it
if ($type.IsInterface -and $reflectedType.GetInterface($type)) {
return $true
}
}
# Returning nothing will be falsy, and will not return the element.
}
))The implementation might be a bit brutish, but the execution can be downright glorious.
# Find just the `[double]`
{1,2.0,3} | Search-Script -For ([double])
# Find just the `[int]`
{1,2.0,3} | Search-Script -For ([int])
# Find all the `[IComparable]` objects
{1,2.0,3} | Search-Script -For ([IComparable])Using the Ast, we can find any needle in any scripted haystack. Please try to Search-Script and give feedback if you've got it.
Happy Hunting!
Log in to leave a note.

