Előfordulhat olyan feladat, amelyben szkripteket szeretnénk elemezni. Ez egyszerű szövegelemzéssel általában nem biztos, hogy könnyű. Nézzük a következő értelem nélküli, ámde szintaktikailag helyes szkriptet:
# Kezdés $i lesz mindjárt
$i = 0
$hostinfo = [net.dns]::Resolve("www.microsoft.com")
dir | %{
Add-Member -MemberType NoteProperty -Name ExtensionName -InputObject $_ -Value ($_.extension -replace "^\.","") -PassThru
}
$i++
# Vége
Ha csak annyi a feladat, hogy listázzuk ki a használt változókat, már ez sem lenne annyira triviális, hiszen látható, hogy a $i karakterkombináció az első sor kommentjében is megtalálható. Ez még itt egyszerű, hiszen ez csak egy egysoros megjegyzés, de ha a többsorosokra is gondolunk, akkor már bonyolultabb lenne nyilvántartani, hogy hol a komment blokk eleje és vége.
De minek is akarnánk egy ilyen bonyolult elemzőt mi magunk készíteni, ha a PowerShellnek úgyis elemeznie kell a szkriptjeinket, ha helyesen akarja azokat futtatni? Hátha ezt a képességét mi is igénybe tudjuk venni!
És szerencsére így is van, a [system.management.automation.psparser] osztály Tokenize statikus metódusa végzi el a szkriptek elemzését:
PS C:\> [ref] $errors = $null
PS C:\> [system.management.automation.psparser]::Tokenize((Get-Content "C:\PSKö
nyv\ToTokenize.ps1"), $errors) | ft -AutoSize
Content Type Start Length StartLine StartColumn EndLine
------- ---- ----- ------ --------- ----------- -------
# Kezdés $i-vel Comment 0 15 1 1 1
... NewLine 15 2 1 16 2
i Variable 17 2 2 1 2
= Operator 20 1 2 4 2
0 Number 22 1 2 6 2
... NewLine 23 2 2 7 3
hostinfo Variable 25 9 3 1 3
= Operator 35 1 3 11 3
[net.dns] Type 37 9 3 13 3
:: Operator 46 2 3 22 3
Resolve Member 48 7 3 24 3
( GroupStart 55 1 3 31 3
www.microsoft.com String 56 19 3 32 3
) GroupEnd 75 1 3 51 3
... NewLine 76 2 3 52 4
dir Command 78 3 4 1 4
| Operator 82 1 4 5 4
% Command 84 1 4 7 4
{ GroupStart 85 1 4 8 4
... NewLine 86 2 4 9 5
Add-Member Command 92 10 5 5 5
-MemberType CommandParameter 103 11 5 16 5
NoteProperty CommandArgument 115 12 5 28 5
-Name CommandParameter 128 5 5 41 5
ExtensionName CommandArgument 134 13 5 47 5
-InputObject CommandParameter 148 12 5 61 5
_ Variable 161 2 5 74 5
-Value CommandParameter 164 6 5 77 5
( GroupStart 171 1 5 84 5
_ Variable 172 2 5 85 5
. Operator 174 1 5 87 5
extension Member 175 9 5 88 5
-replace Operator 185 8 5 98 5
^\. String 194 5 5 107 5
, Operator 199 1 5 112 5
String 200 2 5 113 5
) GroupEnd 202 1 5 115 5
-PassThru CommandParameter 204 9 5 117 5
... NewLine 213 2 5 126 6
} GroupEnd 215 1 6 1 6
... NewLine 217 2 6 3 7
i Variable 219 2 7 1 7
++ Operator 221 2 7 3 7
... NewLine 223 2 7 5 8
# Vége Comment 225 6 8 1 8
... NewLine 231 2 8 7 9
Látható, hogy elég alapos elemzést végzett ez a metódus. Minden szintaktikai elem szerepel a kimeneten, a Start oszlopban karakterpozícióban, a StartLine, StartColumn oszlopokban pedig a sortörések figyelembevételével mutatja az adott elem helyzetét. (A fenti kimeneten a könyv keskeny formátuma miatt az EndColumn oszlop már nem fért ki.) A Type oszlopban a szintaktikai elem típusa látható, ezt felhasználva szűrhetünk a minket érdeklő elemekre, például a változókra:
PS C:\> [system.management.automation.psparser]::Tokenize((Get-Content "C:\PSKö
nyv\ToTokenize.ps1"), $errors) | Where-Object {$_.type -eq "Variable"} | ft -Au
toSize
Content Type Start Length StartLine StartColumn EndLine EndColumn
------- ---- ----- ------ --------- ----------- ------- ---------
i Variable 17 2 2 1 2 3
hostinfo Variable 25 9 3 1 3 10
_ Variable 161 2 5 74 5 76
_ Variable 172 2 5 85 5 87
i Variable 219 2 7 1 7 3
A $errors változót referenciaként adtuk át a metódusnak, így lehetősége van, hogy az elsődleges kimenet mellett oda is adjon át értékeket. Jelen esetben a szintaktikai hibákra vonatkozó adatokat teszi le oda. Nézzünk erre is egy példát:
PS C:\> $sb = "1 + get-date"
PS C:\> Invoke-Expression $sb
Invoke-Expression : At line:1 char:4
+ 1 + get-date
+ ~
You must provide a value expression following the '+' operator.
At line:1 char:5
+ 1 + get-date
+ ~~~~~~~~
Unexpected token 'get-date' in expression or statement.
At line:1 char:1
+ Invoke-Expression $sb
+ ~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ParserError: (:) [Invoke-Expression], ParseExce
ption
+ FullyQualifiedErrorId : ExpectedValueExpression,Microsoft.PowerShell.Co
mmands.InvokeExpressionCommand
Az első sorban, az idézőjelben levő kifejezés abban a formában nem helyes, a get-date-et zárójelbe kellene tenni (persze nem sok értelme lenne a kifejezésnek úgy sem). A hibajelzés többszörös. Egyrészt az összeadás után reklamál, hogy valami érték kellene oda, másrészt a get-date, mint felismert, végrehajtható elem jön számára váratlanul.
Nézzük, hogyan bánik el ezzel a tokenizer:
PS C:\> [ref] $errors = $null
PS C:\> [system.management.automation.psparser]::Tokenize($sb, $errors) | ft -A
utoSize
Content Type Start Length StartLine StartColumn EndLine EndColumn
------- ---- ----- ------ --------- ----------- ------- ---------
1 Number 0 1 1 1 1 2
+ Operator 2 1 1 3 1 4
get-date Command 4 8 1 5 1 13
Idáig semmi meglepő. Nézzük, mi van az $errors-ban:
PS C:\> $errors
Value
-----
{System.Management.Automation.PSParseError, System.Management.Automation.PS...
PS C:\> $errors.Value
Token Message
----- -------
System.Management.Automation.PSToken You must provide a value expression...
System.Management.Automation.PSToken Unexpected token 'get-date' in expr...
A Value tulajdonság Message tulajdonságában már felsejlenek a korábban látott hibajelzések. A Token tulajdonság meg megadja a hiba pozícióját:
PS C:\> $errors.Value[0].Token
Content :
Type : Position
Start : 3
Length : 0
StartLine : 1
StartColumn : 4
EndLine : 1
EndColumn : 4
Visszatérve a Tokenizer felhasználásához, összeraktam egy kis függvény-kezdeményt, ami kicseréli egy szkriptben az összes változónevet egy másikra:
function Replace-Variable {
param(
$scriptpath,
$fromvariable,
$tovariable
)
[ref] $errors = $null
$escapedfrom = '\$' + [regex]::Escape($fromvariable)
$scriptcontent = Get-Content $scriptpath
$prevvariablecommand = $false
$előfordulások = [system.management.automation.psparser]::Tokenize($scriptcontent, $errors) |
Where-Object {
$feltétel1 = "Variable", "Comment" -eq $_.type
$feltétel2 = $prevvariablecommand -and $_.Type -eq "CommandArgument" -and $_.Content -eq $fromvariable
if($feltétel1){
($_.Type -eq "Variable" -and $_.Content -eq $fromvariable) -or
($_.Type -eq "Comment" -and $_.Content -match "$escapedfrom\b")
}
else{$feltétel2}
if($_.Type -eq "Command" -and $_.content -match "-Variable$"){
$prevvariablecommand = $true
}
else{
$prevvariablecommand = $false
}
}
$előfordulások[-1..-$előfordulások.length] | ForEach-Object {
$sor = $_.StartLine - 1
if("variable", "commandargument" -eq $_.Type){
$scriptcontent[$sor] = $scriptcontent[$sor].Substring(0, $_.StartColumn-1) +
$(if($_.Type -eq "variable"){"$"}) + $tovariable +
$scriptcontent[$sor].Substring($_.EndColumn - 1 )
}
elseif($_.Type -eq "comment"){
$scriptcontent[$sor] = $scriptcontent[$sor] -replace "$escapedfrom\b", "`$$tovariable"
}
}
$scriptcontent
}
A függvény két lényegi része:
A változó előfordulásainak felderítése
A változó nevének a cseréje az előfordulási helyeken
Az első részben a tokenizer kimentében mind a változó típusú elemek, mind a kommentek potenciálisan vizsgálandó. Ezen kívül még a Variable főnevű cmdletek is vizsgálandók, hiszen azokkal is dolgozhatunk változókkal. Itt az a nehézség, hogy a változó a $ jel nélkül szerepek paraméterként.
A második részben, a tényleges cserénél visszafele kell haladni, hiszen ha előröl cserélnénk, akkor ha nem ugyanolyan hosszú a változó új neve, akkor a későbbi pozíciók elcsúsznak az elemzéskori állapothoz képest, így a cserék elrontanák a teljes szkriptet.
A Replace-Variable függvény ebben a formában félkész, hiszen a változásokat nem menti el, csak kiírja a konzolra, de innen már nem olyan nehéz folytatni.