Seriál Windows PowerShell: PowerShell z pohledu programátora (část 19.)

Na úvod se musím zmínit o největší události za posledních pár měsíců, alespoň z mého pohledu: kniha PowerShell In Action (autorem je Bruce Payette) je konečně k dostání i v papírové podobě. Pokud to s PowerShellem myslíte opravdu vážně, tato kniha by už měla ležet ve vašem nákupním košíku.

Při jedné kratší diskuzi na Twitteru jsem se dozvěděl, že někteří PowerShell nepotřebují (nebo si to alespoň myslí :)) a případně, že PowerShell má obskurní syntaxi a napsaný Y combinator je snad nečitelnější, než kdyby to autor napsal v perlu. A další otázka se týkala tutoriálu k PowerShellu.
Proto jsem se rozhodl, že PowerShell zkusím představit očima programátora. Jako inspirace mi posloužilo Augiho povídání o javascriptu. Dnešní článek tedy bude spíše teoretický, ale budu se snažit jej co nejvíce odlehčit příklady. Cílem je ukázat základní podobnosti a rozdíly mezi PowerShellem a běžným programovacím jazykem. Více informací se dá najít buď v helpu PowerShellu, nebo například ve výše uvedené knize.

A zde je obsah dnešního článku:

Framework

PowerShell staví na .NET frameworku. Proto ti, kteří s .NET frameworkem pracují, získávají jednoznačnou výhodu – mohou samozřejmě využít při skriptování svých znalostí. PowerShell je zkompilovaný pro .NET 2.0/3.5. Dá se ale dosáhnout toho, že poběží v .NET 4 runtime. Jak? Otázka na SO obsahuje mnoho tipů. Ve zkratce nejjednodušší způsob je tento:

  1. Otevřete PowerShell config. Config pro ISE se jmenuje powershell_ise.exe.config a otevřete jej příkazem notepad $pshome\powershell_ise.exe.config. Je možné, že zatím neexistuje a bude jej potřebovat vytvořit.

  2. Do configu přidejte tento záznam:

     <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v4.0"/>
        <supportedRuntime version="v2.0.50727" />
    </startup>
    
  3. A restartujte ISE.

Obdobně pro PowerShell konzoli můžete změnit/vytvořit soubor $pshome\powershell.exe.config. Musíme ale počítat s tím, že start PowerShellu je při runtime .NET 4 daleko pomalejší.

Typy

Vzhledem ke svým základům – .NET frameworku – se nedá vymezit pevná sada typů, se kterou PowerShell pracuje. Místo toho můžeme poukázat na nejběžněji používané typy, které se většinou vyskytují také v jiných programovacích jazycích.

  • číslo – reprezentované typy [int] , [int64] , [float] , [decimal] , …
  • bool – tedy hodnota ano/ne. K těmto hodnotám přisluší konstanty $true a $false.
  • řetězec – řetězce je možné zapisovat do apostrofů i do uvozovek a mohou se roztáhnout přes více řádek.
  • scriptblock – je v podstatě anonymní funkce. Zapisuje se do složených závorek. Příklad: { Write-Host toto je test } je anonymní funkce, která nic nevrací, pouze vypíše nějaký text na obrazovku.
  • hashtable – je reprezentovaná starou známou System.Collections.Hashtable.
  • pole – je pole obecných objektů, tj. System.Object[] .

Bool

Typ bool není potřeba popisovat. Proto zde zmíním pouze přetypování na [bool] :

 [bool]1        # true
[bool]0        # false 
[bool]''       # false
[bool]'a'      # true
[bool]1.1      # true
[bool]$null    # false
[bool][bool]   # true
[bool](1..10|Where-Object{$_ -gt 100 }) # false (1)
[bool](@())    # false (2)
[bool](@{})    # true

V praxi je důležité přetypování (1) a (2). Úzce souvisí s pipeline – prázdná sekvence se konvertuje na $false, neprázdná pak na $true:

 if (Get-ChildItem d:\temp) { 
  Write-Host Dir d:\temp is not empty
}

Řetězec

Jaký je rozdíl mezi apostrofy a uvozovkami? Ukážeme na příkladu:

 $today = Get-Date
Write-Host "Dnes je $today" # vypíše Dnes je 05/27/2011 18:30:29
Write-Host 'Dnes je $today' # vypíše Dnes je $today

V případě, že použijeme uvozovky, proměné se vyhodnotí a poté se celý řetězec předá jako parametr cmdletu Write-Host. V případě apostrofů se ale řetězec vezme jako takový a vyhodnocení neprobíhá.

Zde bych rád upozornil na možnou chybu při formátování. Srovnejte:

 $today              # 27. května 2011 18:30:29
Write-Host $today   # 27.5.2011 18:30:29
"$today"            # 05/27/2011 18:30:29

Obdobná pravidla a nesrovnalosti platí například i pro floaty. Naštěstí se s tímto problémem dá dobře žít, pokud o něm víme.

Formátovat (datum) můžete standardními prostředky, které nabízí .NET, nebo samotný PowerShell. Konkrétně pro datum můžete přímo určit, v jakém formátu se má výstup vrátit takto:

 Get-Date -Format yyyy-MM-dd       # 2011-05-27
(Get-Date).ToString('yyyy-MM-dd') # 2011-05-27

Scriptblock

V některých jazycích jsou považovány funkce za jeden ze základních typů. Stejně tak je tomu i v PowerShellu, kde anonymní funkce je reprezentovaná typem scriptblock. Scriptblock může vyžadovat parametry, nebo být bezparametrický:

 $listC = { Get-ChildItem c:\ }
# anonymní funkci zavoláme & operátorem
& $listc

$listCWithFilter = { param([string]$filter) Get-ChildItem c:\ -filter $filter }
& $listCWithFilter *.txt

Hlavní využití scriptblocku je ovšem ve funkcích/cmdletech. Typickým příkladem jsou cmdlety Where-Object or Foreach-Object, kde scriptblock předáváme jako parametr.

 Get-ChildItem c:\ | Where-Object {$_.PsIsContainer} | Foreach-Object {$_.LastWriteTime}

Přetypováním na [string] získáme kód scriptblocku. Pokud bychom naopak měli kód a potřebovali vytvořit scriptblock, pomůže nám metoda [scriptblock]::Create.

 & ([scriptblock]::Create('Write-Host (get-date)'))

Pokud se při vytváření scriptblocků neobejdete bez uzávěrů, použijte metodu GetNewClosure:

 $scriptblocks = Get-ChildItem | 
    Foreach-Object { 
      $item = $_
      { Write-Host Name is $item.Name } 
    }
$scriptblocks | Foreach-Object { & $_ }
                   
# versus
$scriptblocks = Get-ChildItem | 
  Foreach-Object { 
    $item = $_
    { Write-Host Name is $item.Name }.GetNewClosure() 
  }
$scriptblocks | Foreach-Object { & $_ }

Hashtable

Hashtable pravděpodobně každý .NET vývojář zná hlavně z dob před generikami. Princip je jednoduchý: jde o kolekci, která udržuje dvojice key-value. Vytvoříme ji pomocí literálu @{}. Při přístupu použijeme hranaté závorky, jak jsme zvyklí, nebo tečkové notace (usnadnění PowerShellu):

 $translation = @{monday='po'; tuesday='ut'}
$translation.wednesday = 'st'
$translation['thursday'] = 'ct'
$translation.ContainsKey('monday')     # true
$translation.ContainsKey('MONDAY')     # true

Všimněte si, zápisu monday='po'. Levá strana před rovnítkem je uvedena bez apostrofů/uvozovek. Pokud PowerShell může levou stranu zkonvertovat na nějaký základní typ, udělá to. Jinak považuje výraz za řetězec.
Poslední příklad také připomíná, že PowerShell je obecně case-insensitive a jak vidno, platí to i pro hashtable.

Jedna z ošklivých věcí v PowerShellu je procházení key-value párů. Je potřeba si nejdříve vrátit enumerátor a ten poslat do pipe. Důvodem tohoto neintuitivního chování je zřejmě skutečnost, že většina uživatelů by netušila, s jakým typem vlastně v pipeline budou pracovat (tj. neočekávali by property key a value).

 $translation.GetEnumerator() | % { "{0} - {1}" -f $_.Key, $_.Value }

Čas od času se může také hodit sčítání objektů typu hashtable:

 $prvni = @{1 = 'jedna'; 2='dva' }
$druha = @{3 = 'tri' }
$prvni + $druha

Hashtable se typicky v PowerShellu používá na dvě věci: splatting a vytváření custom objektů:

 $parameters = @{Path='c:\'; Filter='*.txt' }
Get-ChildItem @parameters |             # splatting
Foreach-Object { 
  new-object PsObject -property @{
    Name=$_.FullName; 
    Size=$_.Length }                  # property pro custom objekt
}

Velmi stručně řečeno se splatting hodí v situacích, kdy při psaní kódu ještě nemusíme vědět, s jakými parametry budeme chtít daný cmdlet zavolat. Upozorňuji, že nejde o hodnoty parametrů (argumenty), ale skutečně výčet použitých parametrů.

 function GetItems { param($p) Get-ChildItem @p } $parameters = @{Path='c:\'} if ($env:computername -eq 'qa-test') { $parameters['filter'] = '*.xls' } GetItems $parameters

Proměnná $parameters udržuje seznam parametrů a jejich hodnot pro volání Get-ChildItem. A pouze na testovacím stroji vylistuje jen xls soubory.

Array

Pole hraje jednu z klíčových rolí v PowerShellu. Může obsahovat jakýkoliv objekt, nekontroluje se typ. Pole vytvoříme pomocí literálu @().
V praxi se ještě často používá operátor čárky. Ten nám také vrátí pole. Jen je potřeba myslet na prioritu, s jakou jsou operátory vyhodnocovány.

 $arr1 = @('test')          # jednoprvkové pole
$arr2 = ,'test'            # jednoprvkové pole, stejně jako $arr1
$arr3 = 'test1', 'test2'   # dvouprvkové pole
$fail = 1, 4-2             # chyba; musí být 1, (4-2)

Pro upřesnění: při použití @(...) PowerShell zkontroluje, jestli objekt uvnitř závorek je pole. Pokud ano, vrátí ono pole. Pokud není, zabalí objekt mezi závorkami do pole. Proto vícenásobná aplikace je zbytečná:

 $arr1 = @('test')
$arr2 = @($arr1)    # arr1 a arr2 jsou ekvivalentní
$arr3 = @(@(@('test'))) # opět stejné jako arr1

Pole se dají sčítat. Následující příklad ukazuje, jak při sčítání polí dostat očekávané výsledky:

 $arr1 = @('test')
$arr2 = @('test2', 'test3')

$result1 = $arr1 + $arr2   # co je uvnitř? (1)
$result2 = ,$arr1 + ,$arr2 # a co tady? (2)

# otestujeme
$result1[1][1]  # vrátí e 
$result2[1][1]  # vrátí test3

Je patrné, že při sčítání dvou polí dojde k připojení prvků z druhého pole (1). Pokud bychom chtěli pole nechat izolované, je potřeba je zabalit jako jednoprvké pole (pomocí operátoru čárky) a tato zabalená pole sečíst (2).

Dynamičnost

PowerShell na první pohled působí jako dynamicky typovaný jazyk:

 function Get-Length{ param($object) $object.Length }

Get-Length 'test'
Get-Length (Get-Item $profile)

V obou případech voláme property Length. při prvním volání předáváme řetězec, v druhém FileInfo. Ve skutečnosti je ale PowerShell staticky typovaný.

PowerShell na pozadí každý objekt zabalí do instance typu PSObject. Zde se udržuje seznam dostupných metod a properties objektu. Můžeme se na něj podívat takto:

 $xml = [xml]'<root><test/></root>'
$xml.PsObject # vrátí obalující objekt
$xml.PsBase   # vrátí proxovaný objekt

Díky tomuto obalení PowerShell může přidat další property k danému objektu:

 $xml.root

Property root ve třídě XmlDocument samozřejmě obsažená není. Kvůli zjednodušení práce s XML ji tam PowerShell dodal, aby se dalo do XML dotazovat pomocí tečkové notace. Záznam o této property pak získáme takto:

 $xml.PsObject.Properties | Where-Object {$_.Name -eq 'root'}

Operátory

Aritmetické

Mezi základní aritmetické operátory patří + - * / %. Toto zřejmě nikoho nepřekvapí:

 $a = 2
$b = 3
$a + $b
$a / $b        # pozn. (1)
$a / $a        # pozn. (1)
$a % $b
'test_' * $b   # pozn. (2)
$a + "10"      # pozn. (3)
'test_' * "10" # pozn. (3)

Za povšimnutí stojí několik zjištění:

  • (1) PowerShell inteligentně pozná, jestli již je potřeba pracovat s plovoucí čárkou, nebo ne. Proto vrací pro $a/$b typ [double] a pro $a/$a typ [int] .
  • (2) Pokud použijeme operátor násobení s řetězcem na levé straně a číslem na pravé, PowerShell provede zopakování řetězce.
  • (3) PowerShell používá poměrně mocný systém konverzí. Proto zafunguje nejen výraz $a + "10", ale i 'test_' * "10". PowerShell neumí násobit dva řetězce, proto automaticky zkonvertuje druhý řetězec na číslo a provede násobení řetězce číslem.

Logické

Syntaxe logických operátorů poněkud trpí nečitelností: -and -or -not -xor. Naštěstí alespoň operátor -not můžeme psát jako vykřičník:

 -not $true # je false
! $true    # opět false

U logických operátorů se uplatní zkrácené vyhodnocování, tj. pokud nalevo od -and je $false, zbytek se nevyhodnocuje. Obdobně pro -or a $true.

Bitové operátory nezmiňuji, protože PowerShell nemá podporu pro bitové posuny. Z tohoto pohledu je práce na úrovni bitů nedotažená a nemá smysl se jí zabývat. V případě potřeby se dají najít řešení.

Přiřazení

Většina operátorů přiřazení vychází z aritmetických, tj. += -= *= /= %=. A ten nejdůležitější je samozřejmě reprezentován rovnítkem: =. Zde není důvod se více rozepisovat. Uvedu jen jeden tip, který nebývá v ostatních jazycích běžný:

 $a = 1
$b = 2

$b, $a = $a, $b

Prohození hodnot dvou proměnných je velmi jednoduché. Ve skutečnosti jde o speciální případ obecnějšího konstruktu – dělení pole:

 $array = 1,2,3,4
$first, $rest = $array

# můžeme za jistých okolností použít pro iteraci polem
$array = 'jablko',1,'hruska',2,'rybiz',3
do {
 $ovoce, $index, $array = $array
 Write-Host Ovoce $ovoce ma index $index
} while ($array)

Porovnání

Porovnávací operátory jsou jedním z důvodů, proč je PowerShell na první pohled nečitelný. Nešlo o záměr, ale o racionální rozhodnutí. PowerShell byl primárně zaměřen na administrátory a kvůli tomu byl operátor > rezervován pro přesměrování do souboru. A od toho se pak už odvinuly další operátory. O jaké se jedná? -lt -le -eq -ne -ge -gt. Neboli less than, less or equal, equal, not equal, greater or equal, greater than.

Pokud tyto operátory budeme používat na řetězce, je potřeba mít na mysli, že porovnání neberou do úvahy case sensitivitu. Pokud potřebujeme přesné porovnání, použijeme operátorry -clt -cle -ceq -cne -cge -cgt.

Opět připomínám mocné konverze prováděné na pozadí, tedy:

 1 -eq 01    # žádné přetypování, 01 je 1
1 -eq "1"   # přetypování ze stringu na int
1 -eq "01"  # přetypování ze stringu na int
"1" -eq 01  # přetypování z intu (tj. z 1) na string
"01" -eq 01 # přetypování z intu (tj. z 1) na string ("1")

Řetězcové operátory

Pro porovnávání řetězců se naštěstí nemusíme uchylovat k prostředkům .NET frameworku, nabízí je totiž samotný jazyk: -match -like -notmatch -notlike. Rozdíl mezi -match a -like je možná již na první pohled patrný.

  • -match bere jako pravý operand regulární výraz a vrací $true, pokud levý operand odpovídá regulárnímu výrazu. Jinak vrací $false. V prvním případě pak ještě naplní automatickou proměnnou $matches, která obsahuje groupy regulárního výrazu.
  • -like pracuje s wildcardy
 '123-45-67' -match '\d+-(?<n>(?<n1>\d+)-(?<n2>\d+))' #true
$matches
# dostaneme toto:
#Name              Value
#----              -----
#n1                45
#n                 45-67
#n2                67
#0                 123-45-67

'123' -like '1'   #false
'123' -like '1*3' #true

K dispozici máme ještě další operátory. Ty už se používají na manipulaci s textem:

  • -replace nahrazuje kus textu jiným na základě regulárního výrazu
  • -split rozděluje text na základě regulárního výrazu
  • -join spojí objekty do jednoho řetězce

Při použití replace můžeme jako nahrazující výraz použít jednoduchý řetězec, ale můžeme se i odkázat na nepojmenovanou groupu (viz. $0), nebo pojmenovanou groupu (${grp}).

 'toto je testovaci uryvek.' -replace 'to(?!\b)', '$0X'
'pouziti groupy' -replace 'ou(?<grp>.)','${grp}${grp}'
 'toto je test' -split '\s+'
1,2,3,4 -join "+"

Operátory na polích (kolekcích)

Doposud jsme používali operátory pro jednoduché typy ( [int] , [string] ). Operátory se dají ovšem použít i na kolekce. Zde se ale změní sémantika.

Uvažujme porovnávací operátory. Výsledkem není výraz typu [bool] , ale kolekce prvků, které vyhovují podmínce:

 1..10 + 1..5 -eq 5
1..10 -lt 5

Při použití aritmetických operátorů nedojde k aplikování na prvky kolekce. Zafungují pouze násobení (stejně jako u řetězců) a sčítání (sečte dvě kolekce):

 1..10 / 3   # chyba
1..10 + 15  # k poli připojí 15
1..10 * 2   # pole zduplikuje

Operátory typické pro pole pak jsou -contains -notcontains. Jejich význam je jasný:

 1,2,'3' -contains 3
1,2,'3' -notcontains 3

Řetězcové operátory -match -like vrací pouze ty prvky, pro které je vyhodnocení $true:

 '123', '145', '234', '345' -match '^1'  # vrátí jen ty, které začínají jedničkou
'123', '145', '234', '345' -like '1*'   # obdoba

A manipulační řetězcové operátory můžeme použít také:

 123, '123', '112233' -replace '1', 'X'
'abc', 'aabbcc', 'abbcc' -split 'b'

Všimněte si, že v příkladu na -replace jsem jako první položku v poli použil číslo. Připomínám, že opět dojde ke konverzi na řetězec.

Funkce

Deklarace

Funkce musí být uvozena klíčovým slovem function. Poté následuje jméno. Parametry a tělo funkce pak můžeme zapsat více způsoby:

 function mul($what, [int]$times) {
  $what * $times
}
function mul {
  param($what, [int]$times)
  $what * $times
}

Častěji se používá druhý způsob, který se syntaxí odkazuje na scriptblock a v podstatě jej jen pojmenovává.

Funkci můžeme také vytvořit pomocí cmdletu New-Item, v praxi ovšem většinou není důvod ji využít:

 New-Item -path function: -name mul -value {param($what, [int]$times) $what * $times}

Jako hodnotu zde předáváme parametrizovaný scriptblock. Uvedený příklad využívá toho, že na funkce stejně jako například na souborový systém se můžeme dívat jako na hierarchickou strukturu – drive. Jejich seznam získáme pomocí cmdletu Get-PsDrive.

Výše jsme si uvedli, že se scriptblocky (tedy anonymními funkcemi) se dá pracovat jako s jakýkoliv jiným typem. To stejné platí i pro funkce. Pokud bychom funkci chtěli získat, respektive odkaz na ni, použijeme cmdlet Get-Item:

 function writetest { 
  param($prefix) Write-Host $prefix : test  -fore Green
}
function writetest2 { 
  param($prefix) Write-Host $prefix : test2 -fore Blue
}
function writerCaller { 
  # použití operátoru & na zavolání funkce; na konci předáváme parametry
  param($func) & $func 'this is writerCaller' 
}
writerCaller (Get-Item function:writetest)
writerCaller (Get-Item function:writetest2)

Volání

Právě jsme se dostali k nejdůležitějšímu místu dnešního článku. Funkce (potažmo cmdlety) voláme s parametry oddělenými mezerou a neuzavřené do závorek. Toto je jeden z nejčastějších omylů mezi zrychlenými programátory.

 function mul { 
  param($what, $times)
  Write-Host "Multiplying $what * $times"
  $what * $times
}

mul('test', 5)   # ne! na $what se navázalo pole s dvěma prvky
mul ('test', 5)  # ne! na $what se navázalo pole s dvěma prvky
mul 'test', 5    # ne! na $what se navázalo pole s dvěma prvky
mul 'test' 5     # ano
mul test 5       # také možné; zjednodušeně řečeno se neznámá hodnota považuje za řetězec

Výše je uvedeno volání, které spoléhá na pozicování parametrů. Stejně tak ale můžeme specifikovat, na který parametr se argument naváže:

 mul -times 10 -what 'test'

Parametry mohou mít default hodnotu a při volání ani nemusíme všem parametrům argument předat.

 function mul {
  param($what='test', $times=2)
  $what * $times
}
mul
mul x
mul -times 1

V PowerShell se nedají funkce přetěžovat. Místo toho se používá koncept parametrů sdružených do tzv. parameter set-u.

 function Get-Size {
  param(
    [Parameter(ParameterSetName='dir')]$dir,
    [Parameter(ParameterSetName='file')]$file
  )
  if ($PsCmdlet.ParameterSetName -eq 'dir') {
    (Get-ChildItem $dir -recurse | 
      ? { !$_.PsIsContainer } |
      Select -expand Length |
      Measure-Object -sum).Sum
  } else {
    Get-Item $file | Select -expand Length
  }
}

Více informací může poskytnout přímo PowerShell: help about_functions_advanced_parameters.

Pipeline

Mezi nejvýraznější prvky PowerShellu patří pipeline. Jde o vlastnost jazyka, která se používá na iterování prvky kolekce. Krom toho jazyk obsahuje i další konstrukty jako for, while, do/while. Více lze najít v dokumentaci, nebo na internetu. Protentokrát se zaměříme na pipeline.

V oblasti .NET frameworku podporuje pipeline snad jen F#, u ostatních jazyků se rozšíření neplánuje. V F# je pipeline jen syntaktickým cukrem:

 let col = [1; 2; 5; 10]
col |> List.map (fun i -> printfn "map1: %d" i; i*i)
    |> List.map (fun i -> printfn "map2: %d" i; i*i)
    |> List.iter (printfn "iter: %d")
    
// ekvivalent:
(List.iter 
  (printfn "i3: %d")
  (List.map (fun i -> printfn "i2: %d" i; i*i) 
            (List.map (fun i -> printfn "i1: %d" i; i*i) col)))
            
// výstup:
map1: 1
map1: 2
map1: 5
map1: 10
map2: 1
map2: 4
map2: 25
map2: 100
iter: 1
iter: 16
iter: 625
iter: 10000

Zápis s pomocí pipeline je jistě přehlednější. Je vidět, že se jednotlivé řádky a volání funkcí List.map/iter volají postupně na všech členech kolekce. V případě použití sekvencí se ovšem výstup radikálně změní:

 let col = seq { for i in [1;2;5;10] do printfn "yield: %d" i; yield i }
col |> Seq.map (fun i -> printfn "map1: %d" i; i*i)
    |> Seq.map (fun i -> printfn "map2: %d" i; i*i)
    |> Seq.iter (printfn "iter3: %d")
    
// výsledek
yield: 1
map1: 1
map2: 1
iter3: 1
yield: 2
map1: 2
map2: 4
iter3: 16
yield: 5
map1: 5
map2: 25
iter3: 625
yield: 10
map1: 10
map2: 100
iter3: 10000

Zde je vidět důležitý rozdíl mezi oběma výsledky. U sekvencí dochází k lazy vyhodnocování, tj. vyhodnocují se až při požadavku (viz. yield). Proto se nejdříve zpracuje první prvek, prožene se přes všechna volání map/iter a až poté se pokračuje na další prvek. A proč to tu vlastně píšu? Protože PowerShell funguje podobně jako sekvence. Uvedeme si příklad.

 # vyfiltruje podané objekty a vrátí jen soubory starší než 7 dnů
function FilterFile {
  param([Parameter(ValueFromPipeline=$true)]$item)
  begin { 
    Write-Host end: filtering -fore Green
  }
  process {
    if ($item.PsIsContainer) { return } # directory
    if ($item.LastWriteTime -lt (Get-Date).AddDays(-7)) {
      Write-Host Returning $item.Fullname -fore Blue
      $item
    }
  }
  end { 
    Write-Host beg: filtering -fore Red 
  }
}

# pro každý podaný soubor vrátí jeho první a poslední řádek
function ReturnInterestingLines {
  param([Parameter(ValueFromPipeline=$true)]$file)
  begin { 
    Write-Host beg: return interesting -fore Green 
  }
  process {
    Write-Host Reading $file -fore Blue
    (Get-Content $file.FullName)[0,-1]
  }
  end { 
    Write-Host end: return interesting -fore Red 
  }
}

Get-ChildItem $psHome *.txt |
  FilterFile |
  ReturnInterestingLines

Zkuste si tento kus kódu zkopírovat do PowerShell konzole nebo ISE a spustit.

  • Na výstupu nejdříve vidíme zelený text z begin bloků
  • Následují modré zprávy o zpracování prvního souboru. Tyto zprávy pochází nejdříve z funkce FilterFile a hned poté z ReturnInterestingLines. Na výstup jsou pak vypsány příslušné řádky ze souboru. Tady je patrná analogie se sekvencemi v F#. Také se nejdříve zpracuje první položka a až poté následují další.
  • Na konci se nachází zprávy o end bloku.

OOP

O PowerShellu jistě každý uslyší ve spojení s objekty. V tomto smyslu se o objektově orientované programování jedná. Ale pokud programátor hledá v PowerShellu možnost, jak vytvářet třídu, z ní podědit, mít některé metody virtuální a podobně, bude zklamán. PowerShell se na tento typ programování nehodí.
Na codeplexu sice existuje projekt psclass, který implements Inheritance, Polymorphism, encapsulation, and more!, případně je možné využít cmdletu Add-Type a opravdu si vytvořit svou třídu a zkompilovat ji. Přesto bych doporučil odprostit se od naučených objektových technik a využívat typový systém .NET frameworku.

Generiky

V první verzi PowerShellu jsme se museli bez generik obejít. Ve V2 byla dodána podpora, ale bohužel ne kompletní. Můžeme sice vytvořit například List<string>, ale už nebyla dodána podpora pro volání generických metod. Čemu to vadí? Pokud jsme dostali neznámou assembly a chceme ji prozkoumat a vyzkoušet si práci s objekty této assembly, pak nemožnost zavolat generickou metodu omezuje naše možnosti.

 $list = New-Object System.Collections.Generic.List[string]
$list.Add('i1')
$list.AddRange([string[]]('i2', 'i3'))

Upozorním jen na dvě volání, která s generikami tolik nesouvisí, ale je dobré si je uvědomit:

 $list.AddRange('i2', 'i3')    # chyba
$list.AddRange(('i2', 'i3'))  # chyba

Při prvním volání by se mohlo zdát, že je všechno v pořádku. PowerShell zde ale neinterpretuje čárku jako operátor. Místo toho zde čárka odděluje jednotlivé argumenty.
Druhé volání vyhodí chybu, protože jako argument předáváme [object[]] . Automatická konverze zde evidentně neproběhne.

Metody

Jak jsme si už řekli, volání generických metod v PowerShellu nemá přímou podporu. Přesto je ale možné, pokud využijeme prostředků .NET frameworku. Ukážeme si, jak použít již hotové řešení.

 Add-Type -TypeDefinition @"
  public class TestGenerics {
    public string GetObjectType(T item) {
      return item.ToString() + " - " + typeof(T).FullName;
    }
  }
"@
$t = New-Object testgenerics
d:\Invoke-GenericMethod.ps1 `
  -instance $t `
  -methodName GetObjectType `
  -typeParameters string `
  -methodParameters 'test'

Vytvořili jsme si třídu TestGenerics a poté ji instanciovali a uložili do proměnné $t. Tato třída má jen jednu metodu s generickým parametrem.
Poslední příkaz ukazuje, jakým způsobem je možné generickou metodu zavolat. Využijeme k tomu výše referencovaný skript, který jsme si uložili do souboru Invoke-GenericMethod.ps1.

Scope, platnost proměnných

V PowerShellu rozeznáváme tři rozsahy platnosti proměnných: global, script, local.

  • global označuje scope, který je přístupný pro všechny funkce/cmdlety a vždy je poněkud riskantní v něm něco měnit. Nikdy nevíme, jestli jiný kus kódu neovlivníme.
  • script je obdoba globálního, ale tentokrát už platí pro jednotlivé skripty. Je vytvořen, když běží skript. Stejné platí i pro modul a uvnitř definované funkce.
  • local se explicitně nedeklaruje. Je to ale scope, ve kterém jste právě teď. Pokud vytvoříte proměnnou, pak ji vytváříte v local scope.

Jednotlivé scope se do sebe zanořují s každým odložením na zásobník. Můžeme se podívat na příklad:

 function writevar {
  param($func)
  Write-Host $func :: var is $var
}
function test1 {
  $var = 'test1'       # (5)
  writevar test1       # (6)
}
function test2 {
  writevar test2       # (1)
  $var = 'test2'       # (2)
  writevar test2       # (3)
  test1                # (4), (7)
  writevar test2       # (8)
}
test2

Zkusíme si rozebrat volání a vyhodnocování proměnné. Nadeklarovali jsme tři funkce a jdeme volat funkci test2. Pokud jsme deklaraci prováděli v konzoli, byli jsme ve scope global. V jejím těle se už nacházíme v zanořeném scope, ale global máme stále k dispozici.

(1) Jako první krok volá writevar. Zanoříme se o jeden scope níž. Při vyhodnocování $var se postupuje od aktuálního (local) scope směrem nahoru. Nikde ale zatím nebyla $var definovaná, takže se vypíše prázdná hodnota.

(2) Dalším krokem je přiřazení $var = 'test2'. Toto způsobí, že v lokálním scope se vytvoří proměnná a nastaví jí nějaká hodnota. Tato proměnná bude viditelná i v dalších podřízených scope.

(3) Opět se volá writevar. Vytvoří se zanořený scope. Při vyhodnocování se zjistí, že proměnná v lokálním scope neexistuje a jde se o jedno výš (tj. scope funkce test2). Zde už byla proměnná definovaná, tedy je při vyhodnocování vrácena její hodnota.

(4) Po vrácení se zpět a zavolání funkce test1 nastane zajímavá situace: vytvoří se opět zanořený scope v test1 a v něm se přiřadí do proměnné jiná hodnota (5): $var = 'test1'. V tuto chvíli na stacku existují dvě proměnné: s hodnotou test2 a test1. Přiřazením v novém scope tedy nejsme schopni přepsat hodnotu proměnné v nadřazeném scope, místo toho nová proměnná překryje tu dřívější.
Jde o dynamické vyhodnocování .

Ve skutečnosti můžeme použít cmdlet Set-Variable, který nám proměnnou nastaví v libovolném scope:

 function inner {
    Write-Host "inner: before" $inouter
    Set-Variable inouter 10 -scope 1
    Write-Host "inner: after" $inouter
}
function outer {
  $inouter = -5
  Write-Host "outer: before" $inouter
  inner
  Write-Host "outer: after" $inouter
}
outer
  

Samozřejmě ale nedoporučuji pro reálné použití.

(6) Voláme z test1 funkci writevar, která si opět vytvoří nový scope. V něm při vyhodnocování proměnnou $var nenajde. Pokračuje o jedno výš a prohledává nadřazený scope – to je ten, kde se do proměnné přiřadila hodnota test1. Najde a vypíše.

(7) Opustí se funkce writevar a hned potom i test1. Při tom se zruší scope, kde je definovaná proměnná s hodnotou test1.

(8) Proto další vypisování proměnné narazí v nadřazeném scope už jen na proměnnou s hodnotou test2.

Proměnná se dá i definovat v private scope. Takto definovaná proměnná není viditelná z podřízených scope. V praxi je samozřejmě na překážku vypisovat vždy, že jde o private. Proto se běžně používá defaultní local, u něhož se modifikátor vynechává.

 function inner {
    Write-Host inner: $inouter # proměnná není vidět
}
function outer {
  $private:inouter = 10
  inner  # nedostane se na proměnnou inouter
}
outer

Vyhodnocování výrazů

Výrazem zde rozumím jazykový konstrukt, který vrací nějakou hodnotu. Může to být volání funkce, operátoru na svých operandech a podobně.

 $d = Get-Date

V případě, že PowerShell rozezná, že do nějaké proměnné přiřazujeme hodnotu, pak považuje pravou stranu od rovnítka za výraz, který má vyhodnotit. Proto v proměnné $d bude datum a ne reference na funkci. Odlišná situace ale nastává, pokud funkci použijeme jako argument při volání jiného příkazu:

 Write-Host Get-Date   # vypíše get-date
#vs.
Write-Host (Get-Date) # vypíše skutečný datum

Zde PowerShell neví, že jde o funkci a Get-Date považuje pouze za řetězec (ani ne odkaz na funkci). Proto musíme pomoci použitím závorek a vynutit si vyhodnocení.

Při podobných vyhodnocování interpreter postupuje podle přesně definovaných pravidel, kdy se případný výraz snaží vyhodnotit. Některá z nich si ukážeme na příkladech.

 function testfunc($p) { 
  Write-Host Parameter type: $p.GetType() value: $p
}
testfunc get-date               # string / get-date
testfunc (get-date)             # date / aktuální čas
testfunc 1                      # int / 1
testfunc 1.5                    # double / 1.5
testfunc test                   # string / test  (1)
testfunc 'test'                 # string / test  (2)
testfunc ('test')               # string / test  (3)
testfunc (test)                 # chyba

$d = Get-Item $env:temp         # temporary dir
testfunc $d                     # DirectoryInfo / ..temp
testfunc $d.Name                # string / temp
testfunc $d.FullName.Length     # int / 32 (délka celé cesty)

$f = { Get-Date }               # anonymní fce
testfunc $f                     # scriptblock / {get-date}
testfunc & $f                   # chyba, takto nejde
testfunc (& $f)                 # date / aktuální čas

Ve funkci testfunc jsme neurčili typ parametru $p, proto se neprovádí žádné konverze a na parametr se naváže předaná hodnota bez konverze. Proto si bez obav můžeme vypsat typ a hodnotu parametru.

Při předávání řetězce jako argumentu je dobré si zapamatovat, že uvozovky/apostrofy psát nemusíme, proto (1), (2) a (3) jsou ekvivalentní.

Stručně řečeno platí: pokud PowerShell pozná, že daná navazovaná hodnota může reprezentovat výraz, pak tento výraz vyhodnotí. Pokud ne, pak předpokládá, že daná hodnota je řetězec a zachází s ním dále jako s řetězcem.

Hranice hodnoty, kterou se interpreter snaží vyhodnotit jako výraz, je dána mezerami. Proto je už snad jasné, proč následující příkaz skončí chybou:

 Get-ChildItem C:\Program Files

V případě, že si nejsme jistí, jak byly hodnoty navázány na parametry, můžeme použít cmdlet Trace-Command:

 Trace-Command -pshost -name ParameterBinding { Get-ChildItem c:\Program Files }

Z výpisu je pak jasné, že na parametr -Path byla navázána hodnota c:\Program a na parametr -Filter hodnota Files. Příkaz následně selhal, protože adresář c:\Program prostě neexistuje. Řešením je samozřejmě uzavřít cestu do uvozovek.

Výrazy můžeme vytvořit i z řídících bloků, pokud je zabalíme do $(...). To se někdy může hodit, pokud potřebujeme vytvořit one-liner například kvůli spuštění z cmd.exe.

 # vypíše text modře odpoledne a zeleně jinak
Write-Host modra? zelena? -fore $(if((Get-Date).Hour -ge 12){'Blue'}else{'Green'})

A tím jsme se pomalu dostali k vyhodnocování výrazů v řetězcích. Dopředu prozradím, že problematické situace řeší právě blok uzavřený do $(...). Příklady:

 write-host "get-date"           # get-date
write-host "(get-date)"         # (get-date)
write-host "$(get-date)"        # aktuální datum

$d = Get-Item $env:temp         # temporary dir
write-host "$d"                 # (1) C:\Users\stej\AppData\Local\Temp
write-host "$d.Name"            # (2) C:\Users\stej\AppData\Local\Temp.Name
write-host "$(d.Name)"          # chyba
write-host "$($d.Name)"         # (3) Temp

write-host "$(1; get-date; 'a','b'|%{$_*3})"  # (4)

Je důležité se zapamovat, že při přístupu k property pomocí tečkové notace se nám vyhodnotí jen samotný objekt (proběhne konverze na string) a část od tečky dál se považuje za řetězec. Poslední příklad (4) navíc ukazuje, že výraz v závorkách může vracet více hodnot. Všechny tyto hodnoty se pak spojí do jednoho stringu.

Co přesně znamená "spojí"? Výraz $(1; get-date; 'a','b'|%{$_*3}) vrátí pole prvků. První bude číslo, druhý bude datum a poté dva řetězce. Toto pole je poté konvertováno na řetězec. Při konverzi je použita speciální proměnná $ofs, jejíž výchozí hodnota je mezera. Zkusme ji změnit na cokoliv jiného a uvidíme výsledek:

 $ofs = ']['
write-host "$(1; get-date; 'a','b'|%{$_*3})"

Závěrem

Snažil jsem se pohlédnout na PowerShell očima programátora a vypíchnout ty vlastnosti, které mohou být pro programátora zajímavé a kde může narazit na rozdíly mezi svým jazykem a PowerShellem. Článek tentokrát narostl do délky, přesto věřím, že se najde někdo, kdo ho přečte celý. V příštím díle se už zkusíme podívat na praktičtější použití.

- Josef Štefan