Szkriptek elemzése - tokenizer

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.



Word To HTML Converter