Seriál: Windows Powershell – tipy a triky (část 8.)

Při práci v PowerShellu, stejně jako v jakémkoliv jiném programovacím či skriptovacím jazyku, narazíte čas od času na nějakou zajímavost, nebo obecný vzorec, který vám může usnadnit práci, zpřehlednit ji, nebo vám konečně pomůže pochopit nějaký obecný princip. V tomto článku vám předložím některé z těchto tipů a zajímavostí. Věřte ale, že je to jen malá část.

Navíc se snažím nerozebírat pokročilá témata, ale ukázat především tipy základní. Doufám, že alespoň některé z nich pro vás budou nové.

Konzole

Libovolná jména funkcí

PowerShell se používá dvěma různými způsoby: většinu kódu máme ve svých skriptech a tyto pouštíme podle potřeby. Anebo máme otevřenou konzoli a píšeme ps kód a spouštíme bez ukládání.

V prvním případě je velmi důležitá přehlednost kódu, jeho srozumitelnost a pochopitelnost. Proto se doporučuje používat plná jména cmdletů, ne jen gci ale Get-ChildItem (nehledě na to, že na jiném systému může gci být aliasem pro něco úplně jiného).

Ve druhém případě ovšem používáme hojně aliasů (gci|?{!$_.PSIsContainer}|select -exp Length) a co nejkratších konstrukcí. Důležité je dosáhnout cíle, ale téměř nezáleží na tom jak. Díky tomu, že PowerShell je velmi benevolentní, umožní nám pojmenovat funkce a aliasy velmi zajímavými jmény. Podívejte se na příklady.

 PS> # dobře známé funkce ?? a ?:
PS> function ?? { if ($args[0]) { $args[0] } else { $args[1] } }
PS> function ?: { if (&$args[0]) { $args[1] } else { $args[2] } }

PS> ?? $null 'default value'
default value
PS> ?: {1} 'is 1' 'is not 1'
is 1
PS> ?: {get-process nonexisting -ea 0} 'process exists' 'process doesn''t exist'
process doesn't exist

PS> function \ { 'this is slash' }
PS> function * { '*'*10 }

PS> # definuje funkci, jejíž jméno je CTRL+D
PS> New-Item -Path "function:$([char][int]4)" -ItemType function –Value 
{ write-host 'CTRL+D!' }

I když PowerShell nechává na nás, jak se budou naše funkce jmenovat, dělejme tak s rozmyslem.

Zápis bytů

Nejlépe a okamžitě vše bude vidět na příkladu

 PS> 1kb, 1mb, 1gb, 1tb, 1pb
1024
1048576
1073741824
1099511627776
1125899906842624

Na konec číselné hodnoty můžete přidat jednotku (v bytech). PowerShell toto automaticky vyhodnotí za vás. V důsledku pak zjednodušíte kód např. takto:
gci | ? { !$_.PSIsContainer -and $_.Length -gt 15mb }

Operátory a proměnné

Řetězení operátorů

Tato technika není příliš často používána, ale v mnoha případech dokáže nahradit použití cmdletů a pipeline. Pro demonstraci předpokládejme, že náš adresář obsahuje tyto soubory:

 ch01-2010-03-01.txt
ch01-2010-03-02.txt
ch02-2010-03-01.txt
ch02-2010-03-02.txt
ch03-2010-03-01.txt
ch04-2010-03-01.txt

My z nich chceme vyfiltrovat jen ty, které začínají ch01. Z nich pak chceme získat pouze střední část zkonvertovatelnou na [datetime]. Tradičně bychom toto mohli provést přibližně takto:

 Get-ChildItem c:\temp\aa\ | select -exp Name | ? { $_ -like 'ch01*'} | 
% { $_ -replace 'ch01-|\.txt','' } 

Ale stejně tak můžeme použít i následující způsob. Všimněte si, že část select -exp Name v tomto případě není nutná, protože dojde ke konverzi [FileInfo] na string, při níž se bere jméno souboru.

 (Get-ChildItem c:\temp\aa\ | select -exp Name) -like 'ch01*' 
-replace 'ch01-|\.txt',''

Zde jsme použili dvou vlastností:
Zaprvé – operátory se dají řetězit. Tedy i operátor replace by někdo kvůli čitelnosti mohli rozdělit na dva. Výsledek by pak vypadal takto: ...-replace 'ch01-','' -replace '\.txt',''. Výsledek předchozího operátoru se uplatní při vyhodnocování následujícího operátoru. Toto jsem neviděl pořádně zdokumentované, tedy se odvolávám pouze na své dosavadní zkušenosti.
Zadruhé – některé operátory pracují nejen nad skalárními hodnotami, ale také nad poli. Proto jsme mohli operátorům like a replace jako levý operand předat pole a vrátila se nám korektní hodnota. V případě, že bychom ale uvedený příklad chtěli dovést až do konce a hodnoty 2010-03-02 a podobné chtěli převést na datetime, pak toto je špatně: ... -replace 'ch01-|\.txt','' -as [datetime]. V tomto případě se operátor snaží převést vstupní objekt (pole) na čas a toto pochopitelně nedopadne dobře. Stačí ale drobná změna a máme funkční kód.

Srovnejte:

 Get-ChildItem c:\temp\aa\ | 
    select -exp Name | 
 ? { $_ -like 'ch01*'} | 
    % { $_ -replace 'ch01-|\.txt','' } |
    % { $_ -as [datetime] } |
   ? { $_ -le '2010-03-01' }
 (Get-ChildItem c:\temp\aa\) `
  -like 'ch01*' `
 -replace 'ch01-|\.txt','' `
 -as [datetime[]] `
  -le '2010-03-01'
Automatické proměnné $$, $^

$^ a $$ jsou automatické proměnné, které mají smysl hlavně při interaktivní práci v shellu. Ne vždy je použijete, ale hodí se je znát. O co jde, je nejlépe vidět na příkladu:

 PS> Get-ChildItem -rec c:\temp\powershelltest\version1
...výpis souborů
PS> $^
Get-ChildItem
PS> $$
c:\temp\powershelltest\version1

Proměnné obsahují první a poslední token na předchozím řádku. Přesněji: Contains the first token in the last line received by the session a Contains the last token in the last line received by the session.
Mohou vám tak ušetři hlavně psaní dlouhých cest do příkazů jako je Get-ChildItem, Get-Item atd. Otázka na StackOverflow dokládá, že se najdou tací, kteří tuto proměnnou skutečně používají.

Objekty

Objekty a typy

Pro mě nejužitečnější tip k práci s objekty a typy je přetypování. Nemám na mysli operátory, ale jednodušší způsob, jak využít jednoparametrický konstruktor. Příklady napoví.

 PS> Add-Type @"
 using System;
 using System.Net;
 public class Test1 {
  public Test1(string s) { Console.WriteLine("Test1 - string ctor: {0}", s); }
  public Test1(int i)  { Console.WriteLine("Test1 - int ctor: {0}", i); }
  public Test1(WebClient c) { Console.WriteLine("Test1 – webclient   ctor: {0}", c); }
    }
    public class Test2 {
        public Test2(string s1, string s2) { Console.WriteLine("Test1 - string ctor: {0}, {1}", s1, s2); }
    }
"@

PS> [Test1] 'a' > $null                  # pouzije 1. konstruktor
PS> [Test1] 1 > $null                    # pouzije 2. konstruktor  
PS> [Test1] (New-Object Net.WebClient) > $null # pouzije 3. konstruktor
PS> [Test1] (date) > $null               # chyba - Multiple ambiguous overloads...
PS> [Test2] @('a','b') > $null           # bohuzel nejde
PS> [system.net.ipaddress[]]'127.0.0.1','192.168.45.1','192.168.45.2','192.168.45.3'

Je tedy možné, pokud máme k dispozici jednoparametrický konstruktor, vyhnout se cmdletu New-Object a pouze použít přetypování.

Při vytváření instancí nějakého .NET typu je potřeba vždy uvádět plnou cestu. Například New-Object System.Text.StringBuilder. Co nám může pomoct?

Předně není nutné psát System. Kratší zápis vypadá takto:

 PS> $sb = new-object Text.StringBuilder

Pokud máme vytvářet více instancí z jednoho namespace a nechceme pořád daný namespace opisovat, můžeme si jej uložit do proměnné:

 PS> $g = 'System.Collections.Generic.'
PS> $list = New-Object ($g + 'List[int]')

Skládání stringů možná nikoho nepřekvapilo a možná to každému přišlo naprosto přirozené. Méně přirozeně může vypadat poslední tip k typům.
Odkaz na třídu si můžeme uložit do proměnné a později použít hlavně při volání statických metod.

 PS> Add-Type –assembly system.windows.forms #nacteme windows.forms
PS> $forms = [System.Windows.Forms.MessageBox]
PS> $forms::Show('Hello')
Přidávání properties k PSObject

Za časů PowerShell V1 se k instancím obecného objektu přídávaly property pomocí cmdletu Add-Member.

 $info = new-object PSObject |
  Add-Member Noteproperty App 'ap' -pass |
  Add-Member Noteproperty Account 'account' -pass |
  Add-Member Noteproperty StatusId 1000 -pass

Někteří si život usnadňovali trikem pomocí Select-Object.

 $info = '' | Select-Object App,Account,StatusId
$info.App,$info.Account,$info.StatusId = 'app','account',1000

V PowerShellu V2 ale máme k dispozici nový parametr -property, který zápis velmi usnadní a zpřehlední.
Už nikdy víc Add-Member a Select-Object jen kvůli přidání property!

 $info = new-object PSObject -property @{App='app'; Account='account'; StatusId=1000 }
Práce s hashtable

Pro nejjednodušší případy, kdy i vyrábění PSObjectu je pro nás zdlouhavé, můžeme použít hashtable. Přistupovat do ní totiž můžeme nejen pomocí hranatých závorek, ale i pomocí tečkové notace.

 PS> $h = @{Height=180; Weigh=80; Name='El'; Surname='Hombre' }
PS> $h.Name
El

Někdy může být zajímavé i použití více hodnot do indexu:

 PS> $h['Height', 'Name']
180
El

Tento přístup má ale i své nevýhody: Pří přístupu přes tečkovou notaci nefunguje doplňování pomocí [TAB] . Navíc se tyto "objekty" nedají třídit podle dané "property".

 1..10 | % { New-Object PSObject -property @{Num=$_} } | Sort Num -desc #funguje
1..10 | % { @{Num=$_} | Sort Num -desc #nedava spravne vysledky

Cmdlety

Splatting

V PowerShellu V2 máme k dispozici splatting operátor @. Používá se při práci s parametry funkcí a cmdletů. Za běhu si můžeme určit, které parametry budeme chtít předat.

 PS> function Write-Parameters {
  param([string]$p1, [int]$p2, [switch]$p3, [string]$p4)
  Write-Host P1: $p1
  Write-Host P2: $p2 
  Write-Host P3: $p3 
  Write-Host P4: $p4
}

PS> $parameters1 = @('2000')              # pole
PS> $parameters2 = @('2. polozka v poli') # pole
PS> $switch      = @{p3=$true; p1='P1' }  # hashtable
PS> Write-Parameters @parameters1 @parameters2 @switch
PS> Write-Parameters 2000 '2.polozka v poli' -p3 -p1 P1

Všimněte si, že výstup z volání Write-Parameters je stejný v obou případech. Za jméno funkce/cmdletu můžeme předat pole, nebo hashtable, které určují vstupní parametry.
Pokud předáme pole, účinek je podobný, jako bychom zapisovali pouze argumenty a spoléhali se na navázání na parametry pomocí pozice.
Pokud předáme hashtable, argumenty jsou navázány stejně, jakobychom před každým z nich specifikovali jméno parametru.

Pozn.: vyhodnocování parametrů je relativně složitý proces. Více je popsáno v knize Windows PowerShell in Action. Rád bych jen upozornil, že v našem případě se nejdříve navážou argumenty určené explicitně, tj. před nimi je jméno parametru (např. -p1 P1). A až teprve poté se navazují zbylé argumenty pomocí pozice na dosud nenavázané parametry.
V našem případě se tedy nejdříve naváže parametr p1 a p3. Teprve potom se začíná navazovat argument 2000. Parametr p1 už je navázaný, tedy je přeskočen. První volný je parametr p2, proto se hodnota naváže na něj. To stejné platí pro hodnotu 2. polozka v poli.

Splatting použijeme tehdy, pokud chceme využít už existujících cmdletů, jako je např. Get-ChildItem.

 PS> function Get-MyItems {
    param([switch]$all, [string]$extension, [string]$directory)
    $p = @{}
    if ($all) { $p['recurse'] = $true }
    if ($extension) { $p['filter'] = "*.$extension" }
    if ($directory) { $p['literalPath'] = $directory }
    
    Get-ChildItem @p
}
PS> Get-MyItems -extension txt
PS> Get-MyItems -all -directory G:\temp\blog
Alias = Get-Alias

Narazili jste někdy na použití např. alias gm a divili se, co je to vlastně ten alias zač? V seznamu funkcí se nenachází, stejně tak v seznamu aliasů a cmdletů.
Podobně funguje date, job, childitem, item, verb, service, atd.

Pokud PowerShell zjistí, že neexistuje daný příkaz (např. můžeme mít definovanou funkci service), zjistí, zda existuje alias Get-…. Pokud alias existuje, zavolá jej. Pokud neexistuje, zkouší podobně existenci funkce a cmdletu a případně existující příkaz zavolá.

Vyhodnocení proměnné/výrazu v řetězci.

To, že se proměnné v řetězci vyhodnocují, ví asi každý, kdo s PowerShellem pracuje.

Pokud jde o proměnnou, která obsahuje pole hodnot, pak tyto hodnoty jsou spojeny pomocí speciální proměnné $ofs. Pokud bychom chtěli hodnoty oddělit pomocí svislítek, můžeme použít toto:

 PS> $ofs = "|"; $pole = 1,2,3,4; "$pole"
1|2|3|4

Mnohem pěknější je ovšem v tomto případě použít operátor -join než nějakou magickou proměnnou.

 PS> $pole = 1,2,3,4; $pole -join "|"
1|2|3|4

Pokud chceme přistupovat k property objektu uložené v proměnné, musíme už použít závorek:

 PS> $pole = 1,2,3,4; "velikost: $($pole.Length)"
velikost: 4

Při vyhodnocování výrazu nejsme omezeni, tj. můžeme použít např. indexaci, můžeme vytvořit objekt, atd. Tato praktika se ovšem nedá obecně doporučit. Výraz lze téměř jistě vždy vyhodnotit dříve než až při použití v řetězci:

 PS> "$((new-object net.webclient).DownloadString('https://google.com').Substring(0, 50))"
<!doctype html><html><head><meta http-equiv="conte

Pokud se ovšem seznam vnořených property dozvíme až za běhu, můžeme použít chytré metody ExpandString:

 PS> $x=[xml]'<a><b><c>some value</c><c>some second value</c></b></a>'
PS> $prop='a.b.c[1]' #tento řetězec zjistime až za běhu
PS> $stringToExpand = "`$(`$x.$prop)"
PS> $ExecutionContext.InvokeCommand.ExpandString($stringToExpand)
Parametr -OutputVariable

-OutputVariable je parametr společný všem cmdletům. Používá se v situacích, kdy cmdlet vrací nějaké objekty (tj. u Write-Host nemá smysl) a chceme zachytit výstup z daného cmdletu do proměnné. Může se nám např. hodit při debugování nebo na zjednodušení skriptů.
V příkladech budu používat alias OV.

 PS> Get-Process svchost -OV svchosts | ? { $_.Handles -gt 500 } -OV svchosts2
PS> $svchosts.Count
15
PS> $svchosts2.Count
4

Tento parametr funguje podobně jako cmdlet Tee-Object, který má mimojiné také možnost uložit obsah do proměnné, ale jednak je použití parametru jednodušší, druhak je možné do proměnné přidávat předřazením znaménka +.

 PS> Get-Process svchost -OV procs; Get-Process firefox -OV +procs; Get-Process powershell -OV +procs
PS> $procs | select -exp ProcessName -unique
svchost
firefox
powershell

Ostatní Krátce

  • Start-Transcript spustí "logování" toho, co se děje ve vaší konzoli. Hodí se především tehdy, když experimentujete s neznámým API – dodatečně se dají příkazy a výstupy prohlédnout a jednoduše najít, která cesta vedla k cíli. Na ukončení logování pak použijete cmdlet Stop-Transcript.

  • ii adresář otevře adresář ve Windows Exploreru. ii soubor spustí default akci nad souborem.

  • gci env: vylistuje systémové proměnné.

  • Jestli vám někdy chyběl příkaz Stop-Pipeline, podívejte se na článek Cancelling a Pipeline.

  • Pokud potřebujete z kolekce i odebírat, můžet použít ArrayList, nebo za určitých okolností využít i přiřazování do více proměnných. Příklad napoví: $pole = 1..10; while($pole) { $p,$pole = $pole; write-host $p }.

  • Některé cmdlety mají dynamické parametry. Pokud se chcete na tyto parametry podívat podrobněji, doporučuji About Dynamic Parameters.

  • Zkuste v konzoli napsat na nový řádek # a mačkat TAB. PowerShell vám bude vypisovat příkazy z historie, počínaje posledním. Je to lepší varianta pohybu pomocí šipek. V tomto případě se totiž nepohybuje po řádcích, ale po celých příkazech, které se roztáhnou přes více řádků. Když pak zkusíte napsat #<řetězec obsažený v nějakém předchozím příkazu>[TAB], bude nápověda cyklit jen po příkazech, které obsahují daný řetězec.

  • A ještě jeden tip na doplňování pomocí TAB. V některých situacích si nepamatujete přesně jméno souboru, ale víte nějaký řetězec, který jeho jméno obsahuje, případně příponu. Pak stačí výraz zapsat pomocí wildcards c:\temp\log??iis* a zmáčknout [TAB] . Konzole bude nabízet jen soubory odpovídající wildcards.

  • Windows API

    Možná je to všem programátorům zřejmé, možná ne, ale – v PowerShellu můžete pomocí Add-Type používat i Windows API. Joel Bennet má na PoshCode.org krásnou ukázku. Jeho modul umožní skrýt a zobrazit okna, nebo nastavit průhlednost oka. Použití je velmi jednoduché.

     PS> $n = Select-Window | ? {$_.title -match 'untitled' } #vybere instanci Notepadu
    PS> $n | Set-GhostWindow -Percent 50 #nastav průhlednost na 50%
    PS> $n | Remove-GhostWindow # odstraň průhlednost
    

Pokud máte své vlastní tipy, které vám přijdou zajímavé, neváhejte a využijte komentářů. Ostatní čtenáři je jistě ocení!

- Josef Štefan

Další díly seriálu:
Seriál: Windows PowerShell – PS a Active Directory (část 7.)
Seriál: Windows Powershell – PS pro programátory (část 6.)
Seriál: Windows Powershell - souborový systém a registry (část 5.)
Seriál: Windows Powershell – dolujeme data aneb jak na WMI (část 4.)
Seriál: Windows Powershell – roury a aliasy (část 3.)
Seriál: Windows Powershell – objekty a roury (část 2.)
Seriál: Windows Powershell – úvod (část 1.)