Wie Sie PowerShells abstrakten Syntaxbaum Nutzen können

Januar 18, 2018 Sicherheit und Compliance, MOVEit

Der abstrakte Syntaxbaum (engl. abstract syntax tree, kurz AST) gliedert den Code in einen hierarchischen Baum, bei dem jedes Element einen Teil des Baumes repräsentiert, und dabei das Skript ‘selbsterkennend’ macht.

 

Haben Sie den Film Inception gesehen? Er handelt von einem Dieb, der sich in fremde Träume schleusen und aus dem Unterbewusstsein klauen kann. Nach dem Film werden Sie die Realität um sich herum anzweifeln. Er hat mir das gleiche Gefühl wie The Matrix gegeben – dass die Realität nicht das ist, was man denkt sie ist. Das hat nichts mit PowerShell zu tun, gibt Ihnen aber eine Idee davon, wie der PowerShell abstrakte Syntaxbaum (AST) arbeitet.

Stellen Sie sich ein PowerShell-Skript vor, das ‘selbsterkennend’ ist. Stellen Sie sich ein PowerShell-Skript vor, dass sich selbst lesen kann, oder sogar andere Skripte generieren kann, basierend auf seinem eigenen Inhalt. Stellen Sie sich das wie Meta-Skripting vor. Es ist ein tolles Konzept und hat eine Menge praktische Anwendungen! Genau das kann PowerShell AST machen. Der PowerShell AST zerlegt den Code quasi in einen hierarchischen Baum, bei dem jedes Element einen Teil des Baumes repräsentiert.

In diesem Artikel erkläre ich, wie Sie den PowerShell AST benutzen können und gebe einige Beispiele, wie das Parsen von PowerShell-Code funktioniert.

Zu Anfang müssen Sie sich erst einmal mit der System.Management.Automation.Language.Parser class. vertraut machen. Dies ist eine Klasse, die einige statische Methoden enthält, die benutzt werden können, um Skripte und Code zu lesen. Diese Klasse hat zwei Methoden, die Sie regelmäßig benutzen werden - ParseInput() und ParseFile(), die im Grunde das gleiche machen. ParseInput() liest Code als einen langen String während ParseFile() Ihnen dabei hilft, eine Textdatei, die PowerShell-Code enthält, in einen String zum Parsen zu konvertieren. Beide liefern das gleiche Ergebnis.

Nehmen wir zum Beispiel ein einfaches Skript mit den folgenden Linien:

Write-Host 'I am doing something here'
Write-Verbose 'I am doing something here too'
Write-Host 'Again, doing something.'
$var1 = 'abc'
$var2 = '123'

Aus dem Skript heraus möchte ich alle Bezüge auf jedes Cmdlet erkennen, das ich habe und jede Variable. Dazu muss ich einen Weg finden, um den gesamten Skript-Inhalt als einen einzigen String zu bekommen. Das kann man mit dem Merkmal $MyInvocation.MyCommand.ScriptContents machen. Man fügt es als die letzte Zeile im Skript an und führt es aus.

Wenn man den Skript-Inhalt hat, kann man diesen, wie oben erwähnt, an die Methode ParseInput() weitergeben, um einen „Baum“ von dem Skript zu bilden. Ich ersetze dabei $MyInvocation.MyCommand.ScriptContents durch Folgendes:

[System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.MyCommand.ScriptContents, [ref]$null, [ref]$null)

Das gibt mir folgenden Output:

PS> C:\test.ps1
I am doing something here
Again, doing something.


Attributes         : {}
UsingStatements    : {}
ParamBlock         :
BeginBlock         :
ProcessBlock       :
EndBlock           : Write-Host 'I am doing something here'
                     Write-Verbose 'I am doing something here too'
                     Write-Host 'Again, doing something.'
                     $var1 = 'abc'
                     $var2 = '123'
                     [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.MyCommand.ScriptContents,
                      [ref]$null, [ref]$null)
DynamicParamBlock  :
ScriptRequirements :
Extent             : Write-Host 'I am doing something here'
                     Write-Verbose 'I am doing something here too'
                     Write-Host 'Again, doing something.'
                     $var1 = 'abc'
                     $var2 = '123'
                     [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.MyCommand.ScriptContents,
                      [ref]$null, [ref]$null)

Parent             :

Das hilft aber noch nicht viel. Man braucht einen Weg, um nur die Funktionen und Variablen zu finden, die im Skript enthalten sind. Dazu ordne ich unseren AST einer Variablen zu, die ich $ast nenne.

PS> $ast = C:\test.ps1

Damit erhalte ich ein Objekt, das verschiedene Methoden und Merkmale enthält, die ich nun benutzen kann.

PS> $ast | gm


   TypeName: System.Management.Automation.Language.ScriptBlockAst

Name               MemberType Definition
----               ---------- ----------
Copy               Method     System.Management.Automation.Language.Ast Copy()
Equals             Method     bool Equals(System.Object obj)
Find               Method     System.Management.Automation.Language.Ast Find(System.Func[System.Management.Automatio...
FindAll            Method     System.Collections.Generic.IEnumerable[System.Management.Automation.Language.Ast] Find...
GetHashCode        Method     int GetHashCode()
GetHelpContent     Method     System.Management.Automation.Language.CommentHelpInfo GetHelpContent()
GetScriptBlock     Method     scriptblock GetScriptBlock()
GetType            Method     type GetType()
SafeGetValue       Method     System.Object SafeGetValue()
ToString           Method     string ToString()
Visit              Method     System.Object Visit(System.Management.Automation.Language.ICustomAstVisitor astVisitor...
Attributes         Property   System.Collections.ObjectModel.ReadOnlyCollection[System.Management.Automation.Languag...
BeginBlock         Property   System.Management.Automation.Language.NamedBlockAst BeginBlock {get;}
DynamicParamBlock  Property   System.Management.Automation.Language.NamedBlockAst DynamicParamBlock {get;}
EndBlock           Property   System.Management.Automation.Language.NamedBlockAst EndBlock {get;}
Extent             Property   System.Management.Automation.Language.IScriptExtent Extent {get;}
ParamBlock         Property   System.Management.Automation.Language.ParamBlockAst ParamBlock {get;}
Parent             Property   System.Management.Automation.Language.Ast Parent {get;}
ProcessBlock       Property   System.Management.Automation.Language.NamedBlockAst ProcessBlock {get;}
ScriptRequirements Property   System.Management.Automation.Language.ScriptRequirements ScriptRequirements {get;}
UsingStatements    Property   System.Collections.ObjectModel.ReadOnlyCollection[System.Management.Automation.Languag...

Die nützlichste Methode ist FindAll(). Mit dieser Methode können Sie den AST selbst abfragen, um nach bestimmten Arten von Sprachkonstrukten zu suchen. In unserem Fall suchen wir nach Funktionsaufrufen und Variablenzuweisungen.

Um nur die Sprachkonstrukte zu finden, die wir suchen, müssen wir erst einmal herausfinden, welche Klasse von jedem repräsentiert wird. In unserem Beispiel sind die Klassen CommandAst für Funktionsaufrufe und VariableExpression für Variabelzuweisungen. Sie können alle verschiedenen Klassenarten hier sehen MSDN System.Management.Automation.Language namespace page.

So kann man alle Funktionsreferenzen finden.

PS> $ast.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)

CommandElements    : {Write-Host, 'I am doing something here'}
InvocationOperator : Unknown
DefiningKeyword    :
Redirections       : {}
Extent             : Write-Host 'I am doing something here'
Parent             : Write-Host 'I am doing something here'

CommandElements    : {Write-Verbose, 'I am doing something here too'}
InvocationOperator : Unknown
DefiningKeyword    :
Redirections       : {}
Extent             : Write-Verbose 'I am doing something here too'
Parent             : Write-Verbose 'I am doing something here too'

CommandElements    : {Write-Host, 'Again, doing something.'}
InvocationOperator : Unknown
DefiningKeyword    :
Redirections       : {}
Extent             : Write-Host 'Again, doing something.'
Parent             : Write-Host 'Again, doing something.'

Und auch alle Variabelzuweisungen.

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

ariablePath : var1
Splatted     : False
StaticType   : System.Object
Extent       : $var1
Parent       : $var1 = 'abc'

VariablePath : var2
Splatted     : False
StaticType   : System.Object
Extent       : $var2
Parent       : $var2 = '123'

VariablePath : MyInvocation
Splatted     : False
StaticType   : System.Object
Extent       : $MyInvocation
Parent       : $MyInvocation.MyCommand

VariablePath : null
Splatted     : False
StaticType   : System.Object
Extent       : $null
Parent       : [ref]$null

VariablePath : null
Splatted     : False
StaticType   : System.Object
Extent       : $null
Parent       : [ref]$null

Sie sehen, dass jedes Konstrukt nun ein Objekt wird, mit dem Sie arbeiten können. Sie haben nun die erforderlichen Kenntnisse, um Ihr Skript in fast jeder erdenklichen Weisen auseinander zu brechen. Durch den Einsatz von AST, können Ihre PowerShell-Skripte so ‘selbsterkennend’ werden.

Tipp: Lesen Sie dazu auch das Whitepaper Automatisieren mit PowerShell oder laden Sie sich kostenfrei eine Testversion von MOVEit Automation, Software für die Automatisierung der Dateiübertragung, herunter.

Adam Bertram

Adam Bertram is a 20-year veteran of IT. He’s currently an automation engineer, blogger, independent consultant, freelance writer, author, and trainer. Adam focuses on DevOps, system management, and automation technologies as well as various cloud platforms. He is a Microsoft Cloud and Datacenter Management MVP and efficiency nerd that enjoys teaching others a better way to leverage automation.

Read next Benutzung der neuen MOVEit 2018 REST API mit PowerShell