Pasti, chytáky a nechtěná překvapení

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.