Seriál: Windows Powershell - Pasti, chytáky a nechtěná překvapení (část 10.)

Seriál: Windows Powershell - Pasti, chytáky a nechtěná překvapení (část 10.)

  • Comments 3
  • Likes

Uživatelé, kteří začínají pracovat s PowerShellem se většinou rychle naučí základní příkazy a postupy. Po čase začnou vytvářet složitější skripty, ve kterých už mohou narazit na záludnosti a chytáky. Pojďme si odhalit některé z nich, abychom se jim už příště vyhnuli a nemuseli dumat nad tím, jestli je to bug, nebo ne.

Automatické odrolování (unrolling/flattening) kolekce

Ve složitějších skriptech budete volat funkce/cmdlety, jejich výsledek uchovávat v proměnné a následně s proměnnou budete pracovat. Potíž může nastat, pokud předpokládáte, že v proměnné máte vždy uloženou kolekci.
Přestavme si následující kód:

PS> function GetCollection {
  # vrátí jednoprvkové pole
  @(new-object PsObject -property @{Date=get-date })
}
PS> $a = GetCollection

Jak byste očekávali, že se dostanete na property Date?

"Správně" by to mělo být takto: $a[0].Date
Ale PowerShell nám jednoprvkovou kolekci vybalil (unrolled) a do proměnné $a přiřadil samotný objekt. Na datum se tedy dostaneme takto:

PS> $a.Date

Stejný problém se netýká jen vámi vytvořených funkcí, ale i vestavěných cmdletů:

PS> $a = Get-ChildItem c:\ *windows* # najde adresář s nainstalovanými windows, zřejmě bude jen jeden
PS> $a.CreationTime                  # vypíše, kdy byl vytvořen

Na této vlastnosti PowerShellu je nepříjemné to, že po volání funkce/cmdletu můžete mít v proměnné uloženou kolekci, nebo vybalený objekt (tj. ne jednoprvkovou kolekci). Ve skriptu pak musíte testovat jestli je proměnná typu pole, nebo ne.

Naštěstí zde existuje jedna velmi jednoduchá technika – zabalení volání funkce/cmdletu do pole:

PS> $a = @(GetCollection)
PS> $a[0].Date
 
8. června 2010 14:14:27
PS> $a = @(Get-ChildItem c:\ *windows*)
PS> $a[0].CreationTime
 
2. listopadu 2006 12:18:34

Na odrolování můžete narazit nejen u polí, ale i u dalších objektů, DataSety nevyjímaje.

Práce s $null místo kolekce

Tento případ tak trochu souvisí s předchozím. Předpokládejte tento kód:

PS> function GetRandomNumbers {
  param([int]$min, [int]$max)
  if ($min -lt $max) {
    0..10 | % { Get-Random -min $min -max $max }
  }
}
PS> $numbers = GetRandomNumbers 0 -10
PS> $numbers | % { write-host Number: $_ }
 
Number:

Co se stane, když zkusíte zadat minimum větší než maximum? Vypíše se pouze "Number: " a žádné číslo. Důvodem je, že z funkce GetRandomNumbers se nám nic nevrátilo. Toto nic pak bylo při přiřazení do proměnné $numbers interpretováno jako $null. A protože je naprosto legální poslat do pipeline $null, místo čísla se nám vypsal $null, jehož reprezentace je prázdný string.

Náprava je opět velmi jednoduchá:

PS> $numbers = @(GetRandomNumbers 0 -10)
PS> $numbers | % { write-host Number: $_ }

Do proměnné $numbers se nám tentokrát uložilo prázdné pole, což je přesně to, co jsme chtěli.

Jen podotýkám, že z výrazu @($null) nám prázdné pole nevznikne!

Jak si více zamotat hlavu

Pokud máte pocit, že už víte, jak na to, možná teprve teď se vám rozsvítí. Opět začněme příkladem.

PS> function ReturnNothing { }
PS> ReturnNothing | % { "some value passed" }   # nevypíše nic

Tento kus kódu se chová tak, jak byste intuitivně čekali. Když funkce ReturnNothing nic nevrací, tak se do pipeliny nic nepošle.

PS> function ReturnNull { $null }
PS> ReturnNull | % { "some value passed" }   # vypíše řetězec 1x

A tady, když se zamyslíme, kód funguje opět podle očekávání. Funkce něco vrací (i když je to jen $null), tedy do pipeline něco posílá. Proto se řetězec vypíše.

PS> $nothing = ReturnNothing
PS> $rnull = ReturnNull
PS> $nothing | % { "some value passed" }  # vypíše řetězec 1x
PS> $rnull   | % { "some value passed" }  # vypíše řetězec 1x

Po přiřazení do proměnné, se výsledek nic z ReturnNothing transformuje na $null. Chová se tedy stejně jako u ReturnNull.

A nyní asi možná tušíte, že když obě volání funkcí uzavřete do operátoru @(..), bude se kód chovat opět intuitivně.

PS> $nothing = @(ReturnNothing)
PS> $rnull = @(ReturnNull)
PS> $nothing | % { "some value passed" }  # nevypíše nic
PS> $rnull   | % { "some value passed" }  # vypíše řetězec 1x

Co to znamená? Pokud explicitně vrátíte $null, musíte s tím počítat, protože i tento $null bude v pipeline zpracován.

Pokud byste si o tomto rysu PowerShellu chtěli přečíst ještě něco víc, v článku Effective PowerShell Item 8: Output Cardinality - Scalars, Collections and Empty Sets - Oh My! jej najdete přehledně popsaný.

Automatické vracení hodnot z funkce

Na začátek opět jedna krátká ukázka:

PS> function GetArrayList {
  $a = new-object Collections.ArrayList
  $a.Add(10)
  $a.Add('test')
  $a
}
PS> GetArrayList
0
1
10
test

Víte, co se stalo? Možná byste očekávali, že se vypíše pouze 10 a "test". Jenže i samotná funkce Add něco vrací – index nově přidané položky. A protože jsme výstup z Add nezachytili, dostal se mezi návratové hodnoty.

Korektně bychom tyto položky mohli vyřadit (minimálně) třemi způsoby:

$a.Add(10) | out-null
$null = $a.Add(10)
[void]$a.Add(10)

Update: čtvrtý způsob využívá přesměrování.

$a.Add(10) > $null

Záleží na vás, který z nich je vám sympatičtější.

PowerShellí funkce se volá jinak než .NETí

Tento problém překvapí spíše jen začátečníky. Ale pořád se s ním dá setkat.
Spočívá ve způsobu, jak se do funkce (a samozřejmě i cmdletu) předávají parametry. V mnoha programovacích jazycích se uzavřou do závorek a oddělí čárkou. Jenže v PowerShellu se pomocí čárky vytváří pole.

PS> function Test {
  param($a1, $a2)
  write-host A1: $a1
  write-host A2: $a2
}
PS> Test (1, 2)        # špatně
PS> Test 1, 2          # špatně; chová ste stejně jako předchozí volání
PS> Test 1 2           # spravná varianta 

V prvním a druhém případě jsme vlastně předali hodnoty pouze prvnímu parametru; do proměnné reprezentované druhým parametrem byl přiřazen $null.

Předání $null do .NET metody

V případě, že používáme .NET a potřebujeme do metody, která bere string parametr předat $null, máme smůlu. Na toto téma byl otevřen bug a snad se ho podaří pořešit do příští verze.

Na onom reportu je k nalezení také workaround. Tj. je to možné, ale … nepěkné.

Priorita operátorů

Z času na čas vás může zaskočit, proč se váš výraz vyhodnocuje jinak, než jste zamýšleli. V tom případě je možná na vině priorita operátorů.

PS> $x = 2
PS> $y = 3
PS> $a,$b = $x,$y*5

Co byste jako programátoři očekávali v proměnných $a a $b? Po mé otázce už to zřejmě nebude 2 a 15. Operátor čárky má totiž větší prioritu než násobení.

Posholic kdysi experimentálně vytvořil tabulku s prioritou operátorů. Možná mezi nimi najdete i nějaké doposud neznámé kousky.

Operátor -f

S operátorem -f pracujte jen tehdy, když víte, že s ním pracujete správně :) Podle všeho byla do jeho implementace zanesen bug.

Ve zkratce řečeno … operátor špatně vyhodnocuje, pokud mu podstrčíme jako pravý operand pole. Vyzkoušet si to můžete na tomto příkladě:

PS> $a = 1,2,3
PS> "{0} a {1}" -f $a
1 a 2
PS> $a.Length
3
PS> "{0} a {1}" -f $a
Error formatting a string: Index (od nuly) musí být větší nebo roven 
nule a menší než velikost seznamu argumentů..
At line:1 char:13 …

Vtip je v tom, že $a není při prvním formátování obalena do PsObject instance a proto se vyhodnotí stejně, jakobychom zapsali "{0} a {1}" -f $a[0], $a[1], $a[2]. Možná vám to připomíná splatting.

Tím, že přistoupíme k property objektu (!), se tento objekt obalí do PsObject. Při vyhodnocení formátování pak je tento objekt konvertován na string a přiřazen do {0}. Do {1} už není co přiřadit a tedy skončíme s chybou.

Více informací najdete na StackOverflow.

Specifikujte typy u parametrů funkcí

PowerShell je dost chytrý a provádí plno konverzí na pozadí, o kterých ani nevíme, ale ne vždy je schopen si poradit. Příkladem může být toto:

PS> Function Test {
    param($n, $k)            
 
    if($n -lt 0 -Or $k -lt 0) {
        throw "Negative argument in Test"
    }            
 
    "Processing"
}            
PS> Test 1 –2
Processing 

Přirozeně byste očekávali, že funkce vyhodí vyjímku. Bohužel se tak nestane. Parametry funkce nemají specifikován typ a tedy PowerShell z nějakých důvodů konvertoval argumenty na string.

Upravte hlavičku:

PS> Function Test {
    param([int]$n, [int]$k)
    ...
PS> Test 1 -2
Negative argument in Test
At line:4 char:14 … 

A dostanete očekávaný výsledek.

Generické typy

Vývojáři rychle zjistí, že PowerShell má omezenou podporu generických typů. Můžeme sice vytvořit například List<string> (syntaxe ze C#), ale:

  • Už nemůžeme volat generickou metodu v negenerické třídě.
  • Někdy je potřeba specifikovat fullname typu, viz. Powershell Generic Collections na StackOverflow.

Díky tomu nemůžeme plnohodnotně prozkoumávat neznámé assembly a musíme si pomáhat berličkami jako Invoking Generic Methods on Non-Generic Classes in PowerShell.

Vyhodnocování proměnných v řetězcích

Čas od času se najde někdo, kdo se potýká s vyhodnocováním proměnných v řetězci:

PS> $date = get-date
PS> "Teď je $date.Hour hodin"   # špatně
PS> "Teď je $($date.Hour) hodin" # správně

Pozor na proměnnou $args

Proměnná $args se používá hlavně u jednoduchých funkcí. Musíme ale vědět, kdy ji použít.

PS> function Test {
    Write-Host args: $args
    1..3 | Foreach-Object { write-host args je $args }
}
PS> Test 1 2 3
args: 1 2 3
args je 
args je 
args je 

Jaktože proměnná nebyla vyhodnocena v rámci cmdletu Foreach-Object?
Důvod je velmi prostý. Proměnná $args je automatická a inicializuje se v rámci každého volání funkce, skriptu, nebo scriptblocku. V tomto případě byl proveden scriptblock. V rámci toho byla vytvořena automatická proměnná $args, navázaná defaultně na $null.

Způsobů, jak toto obejít je více. Nejjednodušší z nich je odložit si $args do speciální proměnné a tu pak používat, tj. $a = $args … write-host args je $a.

Předávání argumentů externím programům

Z PowerShellu můžete přímo spouštět ostatní programy, má to ovšem jedno Ale. Někdy je velmi těžké skloubit způsoby, jakým funguje PowerShellí parser a požadavky na syntaxi argumentů pro daný program. Mohou vám chybět uvozovky, může vám dělat problém středník, mezera.

Pokud potřebujete vědět, jak přesně jsou argumenty předány vašemu programu, použijte program EchoArgs z PSCX modulu. Jak na to?

PS> import-Module pscx
PS> echoargs "argument" second 3rd 'before;after' -like-switch outside"inside"'inside2'"'inside3'"
Arg 0 is <argument>
Arg 1 is <second>
Arg 2 is <3rd>
Arg 3 is <before;after>
Arg 4 is <-like-switch>
Arg 5 is <outsideinsideinside2'inside3'>

Přesměrování do souboru a kódování

Možná jste zvyklí přesměrovat výstup z nějakého programu do souboru pomocí >, případně >>. Pak byste měli vědět, že výstup bude uložen do souboru v UNICODE. Je to ekvivalent … | out-file soubor -encoding unicode.

Obsah tedy můžete přesměrovat ručně pomocí Out-File a při tom specifikovat kódování.

Závěrem

Snažil jsem se vám tu nastínit problémy, které vás mohou potkat při práci s PowerShellem. Pokud nechcete číst článek celý, doporučuji alespoň prozkoumat Automatické odrolování (unrolling/flattening) kolekce. Je velmi pravděpodobné, že se s ním jednou setkáte.

- Josef Štefan

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Ahoj,

    musim pochvalit, velmi odborne a pekne napisany clanok... K 1, este co sa pouziva (ale s $null to nepomoze) je [array]$ = ...

    Dalej k "leakovaniu" vystupu z funkcii a sa pouzit nasledovne:

    function GetArrayList {

    {

     $a = new-object Collections.ArrayList

     $a.Add(10)

     $a.Add('test')

    } | Out-Null

     $a

    }

    Tym sa da oznacit cely blok, ktory nema vracat vystup.

    Martin

  • Díky ;)

    První (proměnná typu [array]) může pomoct, ale většinou mi funkce vrací 0, 1 nebo více objektů. Proto používám @(..). Ale někomu by se mohlo hodit. Je pravda, že většina asi netuší, že proměnné můžou být typové, mohl jsem to uvést do předchozího článku.

    Druhou techniku jsem doposud neviděl, díky za info.

    Pepa