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

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