Seriál Windows PowerShell Přejímání skriptů z internetu (část 46.)

Dnes bych rád navázal na jeden z posledních článků, kde jsem mluvil o používání WhatIf. Byl jsem požádán o pomoc při řešení problému se staženým skriptem – nefungoval. Rovnou říkám, že dotaz byl od běžného IT admina – o PowerShellu má základní povědomost, ale vždy si vystačí s vyhledáním řešení na internetu. To, co najde, poté vloží do konzole nebo ISE, spustí to, a doufá, že se něco (dobrého) stane. Osobně si nedokážu představit, co by se stalo, kdyby mu někdo podstrčil destruktivní kód. Ale to už je jiný příběh.

Původní soubor, ke kterému jsem se dostal, obsahoval následující kód:

# convert an octet string guid to a typical string GUID
function Convert-OctetStringToGuid
{
    param
    (
        [String]$Guid
    );
    if(32 -eq $guid.Length)
    {
        [UInt32]$a = [Convert]::ToUInt32(($guid.Substring(6, 2) +$guid.Substring(4, 2) + $guid.Substring(2, 2) + $guid.Substring(0, 2)), 16)
        [UInt16]$b = [Convert]::ToUInt16(($guid.Substring(10, 2) +$guid.Substring(8, 2)), 16)
        [UInt16]$c = [Convert]::ToUInt16(($guid.Substring(14, 2) +$guid.Substring(12, 2)), 16)
        [Byte]$d = ([Convert]::ToUInt16($guid.Substring(16, 2), 16) -as [byte])
        [Byte]$e = ([Convert]::ToUInt16($guid.Substring(18, 2), 16) -as [byte])
        [Byte]$f = ([Convert]::ToUInt16($guid.Substring(20, 2), 16) -as [byte])
        [Byte]$g = ([Convert]::ToUInt16($guid.Substring(22, 2), 16) -as [byte])
        [Byte]$h = ([Convert]::ToUInt16($guid.Substring(24, 2), 16) -as [byte]) 
        [Byte]$i = ([Convert]::ToUInt16($guid.Substring(26, 2), 16) -as [byte])
        [Byte]$j = ([Convert]::ToUInt16($guid.Substring(28, 2), 16) -as [byte])
        [Byte]$k = ([Convert]::ToUInt16($guid.Substring(30, 2), 16) -as [byte])
        [Guid]$g = New-Object Guid($a$b$c$d$e$f$g$h$i$j,$k)
        return $g.Guid;
    }
    else
    {
        throw Exception('Input string is not a valid octet string GUID')
    }
}

Soubor byl nazván Convert-OctetStringToGuid.ps1a při spuštění samozřejmě neprovedl žádnou akci. Předpokládám, že čtenáři Flashe ví proč. Při spuštění skriptu proběhl vnitřní kód, ale ten pouze vytvořil funkci, která nebyla dále spuštěna. Situace má dvě řešení

  1. Do skriptu vložit i volání funkce s daným parametrem.
  2. Zavolat skript pomocí tzv. dot-sourcingu, kdy funkce zevnitř skriptu přenese do aktuálního prostředí. O dot-sourcingu jsem již psal a i v první PowerShell akademii je o něm zmínka. Skript by se tedy spustil následujícím způsobem:

PS> . ./Convert-OctetStringToGuid.ps1

Ano, první tečka je opravdu správně, to je onen dot-sourcing. Řekněme, že jsme jej použili a funkci máme nyní dostupnou v konzoli.

Vstupem je GUID z Exchange. Ten je standardně ve tvaru: d05db2e2-42de-4998-80cc-90fdc8111c54. Takže jej zkusíme zadat jako parametr naší funkce:

PS C:\> Convert-OctetStringToGuid d05db2e2-42de-4998-80cc-90fdc8111c54

Exception : The term 'Exception' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:25 char:15
+ throw Exception('Input string is not a valid octet string GUID')
+ ~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Exception:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException

Evidentně máme chybu ve slově Exception. Po krátkém pátrání v helpu

PS C:\> help throw
TOPIC
    about_Throw

zjistíme, že syntaxe je jiná oproti té, uvedené ve skriptu:

throw Exception('Input string is not a valid octet string GUID')

vs.

THROWING A STRING
The optional expression in a Throw statement can be a string, as shown in
the following example:
C:\PS> throw "This is an error."
This is an error.
At line:1 char:6
+ throw <<<< "This is an error."
        + CategoryInfo : OperationStopped: (This is an error.:String) [], RuntimeException
        + FullyQualifiedErrorId : This is an error.

Upravíme proto mírně náš skript a spustíme jej znovu

clip_image002

PS C:\> Convert-OctetStringToGuid d05db2e2-42de-4998-80cc-90fdc8111c54

Input string is not a valid octet string GUID
At line:25 char:9
+ throw 'Input string is not a valid octet string GUID'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (Input string is...tet string GUID:String) [], RuntimeException
    + FullyQualifiedErrorId : Input string is not a valid octet string GUID

Nyní již vidíme, že se nám správně zobrazila chyba. Proč jsme ale zachytili výjimku, když máme GUID ve správném tvaru? Odpověď nalezneme na tomto řádku:

if(32 -eq $guid.Length)

Délka vstupního řetězce musí být 32 znaků. Náš řetězec má ovšem znaků 36, včetně pomlček. K této části kódu bych měl dvě výhrady:

  1. Čistě pocitově mi přijde logičtější postavit porovnání následujícím způsobem:
    if($guid.Length –eq 32)
  2. Délku porovnávat buď přes celý řetězec (36 znaků) a v těle skriptu je rozdělit na jednotlivé části nebo dát alespoň krátkou zmínku do nápovědy.

Zkusme tedy odstranit pomlčky a spustit funkci znovu:

PS C:\> Convert-OctetStringToGuid d05db2e242de499880cc90fdc8111c54
e2b25dd0-de42-9849-80cc-90fdc8111c54

Voilá. Vidíte nekonzistenci mezi vstupem a výstupem (zde již s pomlčkami). Jak se vypořádat se vstupem? Máme několik možností jednou z nich je následující konstrukce, kterou můžeme umístit na začátek skriptu:

$Guid = $Guid -replace '-',''

Tím odstraníme ze vstupního textu všechny pomlčky a dále pokračujeme originálním skriptem.

PS C:\> Convert-OctetStringToGuid d05db2e2-42de-4998-80cc-90fdc8111c54
e2b25dd0-de42-9849-80cc-90fdc8111c54

Možná by bylo elegantnější vytvořit funkci, která bude pracovat v rouře a umožní nám elegantní převod více GUIDů najednou. Další úpravy, které bychom mohli použít, jsou například:

  1. Odstranit středník za ukončením bloku parametrů. Jedná se o kosmetickou změnu, ale i na ní je vidět, že autor byl zřejmě programátor.
  2. Dopsal bych alespoň krátkou nápovědu.
  3. Kdybych chtěl mít opravdu kontrolu nad parametry, přidal bych konstrukci [CmdletBinding()] a parametr uvedl v konstrukci [Parameter(….)]
  4. Možná bych uvažoval o pomocné funkci, která by nahradila konstrukci:[Convert]::ToUInt16($guid.Substring(16, 2), 16) -as [byte]
  5. Výslednou hodnotu bych nevracel pomocí klíčového slova return, ale pouze jako $g.Guid.

Je vidět, že i když si stáhnete řešení z internetu, měli byste umět se skripty a funkcemi pracovat. Možná to 1000x vyjde a jednou se stane něco, co jste nepředpokládali. V jednodušším případě se dostanete do právě popsané situace, v tom složitějším budete možná muset něco obnovovat ze zálohy.

Chtěl jsem dnes ukázat opět něco ze života. Vždy byste měli postupovat opatrně při přejímání cizích kódů. Máte nějaký příklad z vlastní praxe, kdy se skript choval jinak, než jste očekávali? Reakce uvítám v komentářích.

Závěrem humorná vsuvka. Víte, jak se baví PowerShell MVP? Malá ukázka toho, jak můžete pojmenovat proměnnou v PowerShellu je na GitHubu: https://gist.github.com/klumsy/11266285 V konzoli vypadá jméno proměnné lépe než na webu – vyzkoušejte si to J

Další ukázka je poněkud zajímavější:

PS C:\> ${ } = 1; ${ } = 2; ${ } = 3; ${ } = 4; ${ } = 5
PS C:\> Get-Variable -Name ' *'
Name Value
---- -----
     1
     2
     3
     4
     5

No, občas může mít člověk těžkou hlavu i z maličkostí