Egy másik szkriptelemzési lehetőség az Abstract Syntax Tree (AST) használata. Ez egy számítógéptudomány által régebb óta használt ábrázolási módszer, amellyel a programkódot egy gráf formájában ábrázolhatjuk. Ezzel a módszerrel nem kapjuk vissza a teljes forráskódot, mert az némi elemzésen esik keresztül és csak egy absztrakt leképezését kapjuk meg. De pont ez lehet az előnye is, hiszen a sallangoktól mentes, a kód logikáját megragadó eredményt kapunk.
Nézzünk erre egy egyszerű példát:
PS C:\> $sb = {$x = dir | Where-Object {$_.psiscontainer} <#komment#>}
Van ebben egy értékadás, csővezeték, szkripblokk, változó, tulajdonság és egy kis megjegyzés a végén. PowerShell 3.0 felett ennek a szkrtiptblooknak van egy AST tulajdonsága, aminek további tulajdonsága is vannak. Ha csak az értékadást vizsgáljuk a legfelsőbb logikai szinten, akkor azt így kapjuk meg:
PS C:\> $sb.Ast.EndBlock.Statements[0].Left.Extent.Text
$x
PS C:\> $sb.Ast.EndBlock.Statements[0].Right.Extent.Text
dir | Where-Object {$_.psiscontainer}
Az AST-n belül egy kifejezésünk van (Statements[0]), ez az egész kifejezés valójában egy end block. A bal (Left) részben van az $x, az egyenlőség jobb (Right) oldakán a csővezeték.
Ha a jobb oldalt részletesebben vizsgáljuk, akkor láthatjuk, hogy ott van egy PipelineElements tulajdonság is, ami kiadja a csővezetékem első (dir) és második elemét (Where-Object ...).
PS C:\> $sb.Ast.EndBlock.Statements[0].Right.PipelineElements
CommandElements : {dir}
InvocationOperator : Unknown
DefiningKeyword :
DefinedKeywords :
Redirections : {}
Extent : dir
Parent : dir | Where-Object {$_.psiscontainer}
CommandElements : {Where-Object, {$_.psiscontainer}}
InvocationOperator : Unknown
DefiningKeyword :
DefinedKeywords :
Redirections : {}
Extent : Where-Object {$_.psiscontainer}
Parent : dir | Where-Object {$_.psiscontainer}
Ezt még mélyebben is lehet vizsgálni, a CommandElements ezt adja meg, a második eleme például egy szóból áll (Where-Object) és a szkriptblokkból ({$_.psiscontainer}):
PS C:\> $sb.Ast.EndBlock.Statements[0].Right.PipelineElements[1].CommandElement
s
StringConstantType : BareWord
Value : Where-Object
StaticType : System.String
Extent : Where-Object
Parent : Where-Object {$_.psiscontainer}
ScriptBlock : {$_.psiscontainer}
StaticType : System.Management.Automation.ScriptBlock
Extent : {$_.psiscontainer}
Parent : Where-Object {$_.psiscontainer}
Ha nem akarunk lépésről-lépésre beleásni a tulajdonságok hierarchiájába, akkor az AST FindAll metódusát érdemes használni:
PS C:\> $sb.AST.FindAll({$true},$true)
Type : ScriptBlock
ParamBlock :
BeginBlock :
ProcessBlock :
EndBlock : $x = dir | Where-Object {$_.psiscontainer}
DynamicParamBlock :
ScriptRequirements :
Extent : {$x = dir | Where-Object {$_.psiscontainer} <#komment#>}
Parent : {$x = dir | Where-Object {$_.psiscontainer} <#komment#>}
Type : NamedBlock
Unnamed : True
BlockKind : End
Statements : {$x = dir | Where-Object {$_.psiscontainer}}
Traps :
Extent : $x = dir | Where-Object {$_.psiscontainer}
Parent : {$x = dir | Where-Object {$_.psiscontainer} <#komment#>}
Type : AssignmentStatement
Left : $x
Operator : Equals
Right : dir | Where-Object {$_.psiscontainer}
ErrorPosition : =
Extent : $x = dir | Where-Object {$_.psiscontainer}
Parent : $x = dir | Where-Object {$_.psiscontainer}
Type : VariableExpression
VariablePath : x
Splatted : False
StaticType : System.Object
Extent : $x
Parent : $x = dir | Where-Object {$_.psiscontainer}
Type : Pipeline
PipelineElements : {dir, Where-Object {$_.psiscontainer}}
Extent : dir | Where-Object {$_.psiscontainer}
Parent : $x = dir | Where-Object {$_.psiscontainer}
Type : Command
CommandElements : {dir}
InvocationOperator : Unknown
DefiningKeyword :
DefinedKeywords :
Redirections : {}
Extent : dir
Parent : dir | Where-Object {$_.psiscontainer}
...
Type : VariableExpression
VariablePath : _
Splatted : False
StaticType : System.Object
Extent : $_
Parent : $_.psiscontainer
Type : StringConstantExpression
StringConstantType : BareWord
Value : psiscontainer
StaticType : System.String
Extent : psiscontainer
Parent : $_.psiscontainer
Kicsit megvágtam a fenti kimenetet a könyvben, mert még 2 oldalt megtöltött volna. Mindenesetre látható ennyiből is, hogy a kód logikájának minden lényeges eleme ábrázolva van. A zárójelek például nincsenek külön kiemelve, mert azt maga az AST elemeinek elrendezése ábrázolja. Természetesen a megjegyzésnek sincs külön eleme.
Mit lehet mindezzel kezdeni? Például kikereshetjük a változókat:
$sb.AST.FindAll({$true},$true) |
Add-Member -MemberType ScriptProperty -Name Type -Value {
$this.gettype().fullname -replace "System.Management.Automation.Language.(\w+)AST$",'$1'
} -PassThru -force | ?{$_.type -eq "VariableExpression"}
Egy kicsit alakítottam a kimeneten, generáltam egy Type tulajdonságot, ami az elem típusát adja meg. Ezzel a kimenet:
Type : VariableExpression
VariablePath : x
Splatted : False
StaticType : System.Object
Extent : $x
Parent : $x = dir | Where-Object {$_.psiscontainer}
Type : VariableExpression
VariablePath : _
Splatted : False
StaticType : System.Object
Extent : $_
Parent : $_.psiscontainer
És itt kanyarodjuk el a FindAll rejtélyes két paraméteréhez. Az első egy szkriptblokk, ami tulajdonképpen egy szűrőfeltétel. Azokat az elemeket fogja csak megadni a metódus, amelyekre a feltétel igaz lesz. Azaz az előző kifejezést sokkal egyszerűbben is össze tudjuk rakni:
$sb.AST.FindAll(
{$args[0] -is [System.Management.Automation.Language.VariableExpressionAst]},
$true
)
A második paraméter azt jelzi, hogy a beágyazott kifejezésekbe is beleásson-e, vagy csak a legfelső szinten levőket.