Seriál Windows PowerShell: Operátor formátování (část 26.)

V nedávno skončených Scripting Games se často vyskytoval ve výsledných skriptem jeden „nešvar“ – výstup byl často podáván ve formě textu pomocí cmdletu Write-Host. Obecně proti Write-Host nic nemám, ale v pravidlech Scripting Games (a v diskusích) bylo často zmiňováno, že výstupem by měly být objekty. My dnes ale půjdeme opačnou cestou a podíváme se, jak vytvořit textový výstup pomocí operátoru formátování ( -f).

Pozor: PowerShell je nad objekty postaven a práce s objekty je u něm přirozená. Ovšem občas se nám ale hodí na výstupu textová informace nebo prostě jenom chceme generovat velké množství textu.

Operátor formátování nám říká, jak chceme formátovat a co chceme formátovat. Vezměme si následující příklad:

PS C:\> $proc = Get-Process powershell
PS C:\> $proc

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
    254 7 57780 57672 162 2.55 3856 powershell

PS C:\> "Process {0} ma Id {1}" -f $proc.Name, $proc.Id
Process powershell ma Id 3856

PS C:\> Write-Host "Process $proc.Name ma Id $proc.Id"
Process System.Diagnostics.Process (powershell).Name ma Id System.Diagnostics.Process (powershell).Id

PS C:\> Write-Host "Process $($proc.Name) ma Id $($proc.Id)"
Process powershell ma Id 3856

Do proměnné $proc jsme si uložili aktuální proces PowerShellu a na následujícím řádku jsme vypsali jeho jméno a Id pomocí operátoru formátování (k zápisu se hned vrátím). Vidíte, že na následujících dvou řádkách jsem zkusil vypsat tu samou informaci pomocí cmdletu Write-Host. V prvním jsem získal z mého pohledu naprosto neužitečnou informaci a teprve ve druhém správná data. Musel jsem totiž použít tzv. subexpression, což je příkaz PowerShellu uzavřený v kulatých závorkách uvozených znakem dolaru: $(<sem_patří_nějaký_příkaz>)

Pokud PowerShell vidí subexpression v textovém řetězci, provede nejdříve příkaz v závorce a teprve poté poskládá celý řetězec. V našem příkladu se tedy nejprve převedly $proc.Name a $proc.Id na správné texty a pak se celý řetězec složil do očekávané podoby.

A nyní zpátky k operátoru formátování. Na levé straně máme uvedeno, jak chceme formátovat:

"Process {0} ma Id {1}"

Čísla ve složených závorkách mi určují místa, kam přijdou proměnné z pravé strany operátoru formátování. Číslování je stejně jako u polí, uváděno o nuly, čili první proměnná je zobrazena symbolem {0}. Na pravé straně pak máme ony zmiňované proměnné:

$proc.Name, $proc.Id

které se nám ve výsledném řetězci přeloží na požadované hodnoty. Celý zápis

"Process {0} ma Id {1}" -f $proc.Name, $proc.Id

bychom tedy mohli přečíst jako: Vypiš text „Process“, dále vlož jméno procesu, text „ma Id“a následně Id zmiňovaného procesu. Pokud bychom například chtěli vypsat stejnou informaci pro více procesů, můžeme to provést následujícím způsobem:

PS C:\> Get-Process | Select -First 3 | ForEach { "Process {0} ma Id {1}" -f $_.Name, $_.Id }
Process AESTFltr ma Id 3104
Process ApMsgFwd ma Id 3904
Process ApntEx ma Id 924

Pro první tři procesy na mém počítači jsem vypsal požadovanou informaci. Pořadí vypisovaných informací můžeme samozřejmě libovolně měnit, můj oblíbený příklad je tento:

PS C:\> "{4}{4}{1}{5}{0}{3}{0}{5}{2}" -f 'e','ka','l','p','po','t'

Dáte dohromady výsledek? Ze zápisu je vidět, že jsem některé proměnné (zde na pravé straně zastoupené daným textem) použil vícekrát a rozhodně jsem nedodržoval dané pořadí na levé straně. Nicméně v případě takovéhoto zápisu už bych trochu pochyboval o čitelnosti J Mimochodem, výsledný text je popokatepetl.

Formátování ve formátování

V příkladu s výpisem více procesů jste si všimli, že výsledek není úplně oku lahodící. Někdy by se nám hodilo, aby se jednotlivé položky výstupu zarovnávaly pod sebe. Naštěstí operátor formátování disponuje možností dalšího formátování (proto ten divný nadpis). Zmiňovaný příklad můžeme tedy přepsat například takto:

PS C:\> Get-Process | Select -First 3 | ForEach { "Process '{0,8}' ma Id: {1,4}" -f $_.Name, $_.Id }
Process 'AESTFltr' ma Id: 3104
Process 'ApMsgFwd' ma Id: 3904
Process ' ApntEx' ma Id: 924

Pro lepší představu jsem do výstupu přidal jednoduché uvozovky. Zápis {0,8} říká, že nahrazovaný text bude dlouhý osm znaků a v případě, že bude kratší, bude doplněn zleva mezerami. To samé pravidlo se uplatní i v případě Id daného procesu ( {1,4} ), kde šířka textu bude čtyři znaky. Pokud bych použil větší číslo, výsledek bude následující:

PS C:\> "Process '{0,15}' ma Id: {1,8}" -f $proc.Name, $proc.Id
Process ' powershell' ma Id: 3856

Možná je ještě o trochu lepší způsob zarovnat text (jméno procesu) ve vytvořeném bloku na levou stranu. V tom případě použijeme šířku textu se zápornou hodnotou:

PS C:\> Get-Process | Select -First 3 | ForEach { "Process {0,-10} ma Id: {1,4}" -f $_.Name, $_.Id }
Process AESTFltr ma Id: 3104
Process ApMsgFwd ma Id: 3904
Process ApntEx ma Id: 924

Další možností úpravy je formátování pomocí formátovacího řetězce. Podíváme se na nejjednodušší formátování, například měny a poté se vrátíme k formátování čísel.

PS C:\> $mame=50
PS C:\> $nakup=63.5

PS C:\> "Kdyz budeme mit {0} a utratime {1}, zustane nam {2}." -f $mame, $nakup, ($mame-$nakup)
Kdyz budeme mit 50 a utratime 63.5, zustane nam -13.5.

PS C:\> "Kdyz budeme mit {0:C} a utratime {1:C}, zustane nam {2:C}." -f $mame, $nakup, ($mame-$nakup)
Kdyz budeme mit 50,00 Kč a utratime 63,50 Kč, zustane nam -13,50 Kč.

Do proměnných si uložíme částku, kterou máme na účtu a kterou pak utratíme. V prvním případě výsledný text nijak neformátujeme a dostáváme větu jak z automatického překladače. Ve druhém případě doplníme za znak dvojtečky formátovací řetězec pro zápis měny (Currency). Vidíte, že výsledek se bez jakéhokoli dalšího přispění změnil na vcelku srozumitelnou podobu mých výdajů a příjmů. Formátování hodnot je dáno nastavením prostředí Windows, takže v mém případě takto:

clip_image001

V závěrečném příkladu si ukážeme možnost formátování čísel. Příklad bude trošku složitější, nejdříve si ukážeme výsledek:

ProcessName cpu ws
---------------------------------
TOTALCMD 465.0 11.89
System 260.3 1.26
WINWORD 213.8 77.27
procexp 184.5 26.39
svchost 125.1 99.32
powershell_ise 101.0 164.04
explorer 98.9 30.96
svchost 75.8 29.45
aenrjpro 69.9 8.82
chrome 68.0 23.44

Výsledkem je TOP10 procesů podle využití CPU.

Začneme tedy získáním procesů.

PS C:\> $procesy = Get-Process | Sort-Object CPU -Descending | Select-Object -First 10
PS C:\> $procesy

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
    196 7 6152 12160 62 519.73 4508 TOTALCMD
   1104 0 0 1288 3 264.09 4 System
   1150 21 65180 79224 274 222.66 3512 WINWORD
    513 13 20812 27036 112 189.61 3556 procexp
    706 50 71340 101704 542 125.13 2548 svchost
    432 18 173456 168744 463 108.33 2952 powershell_ise
    518 18 22552 31700 131 100.33 3372 explorer
   2470 146 17576 29688 154 76.73 856 svchost
    199 8 20864 24012 122 70.36 3388 chrome
    276 9 7532 9036 74 69.94 1180 aenrjpro

Nyní si dáme dohromady hlavičku, kterou použijeme ve výpisu.

PS C:\> $header = "{0,-20}{1,6}{2,7}" -f 'ProcessName','cpu','ws'
PS C:\> $header = "{0}`n{1}" -f $header, ('-'*$header.Length)
PS C:\> $header
ProcessName cpu ws
---------------------------------

Již dopředu jsme si rozmysleli, jména a šířku jednotlivých sloupců. První řádka určuje jména sloupců a druhá řádka vytváří „čáru“ pro oddělení hlavičky od vlastních hodnot.

Poznámka: Všimněte si jedné zajímavosti – pokud násobíte dvě proměnné a první z nich je typu string (řetězec), bude výsledkem opakování tohoto řetězce, tedy

PS C:\> $a='xxxyyy'
PS C:\> $a*3
xxxyyyxxxyyyxxxyyy

PS C:\> $b='123'
PS C:\> $b*3
123123123
PS C:\> 3*$b
369

V posledním příkladu jsem jako první uvedl číslo a proto se celý výraz provádí jako násobení dvou čísel. Proto jsem v mé definované hlavičce mohl využít zápisu ('-'*$header.Length) pro opakované vypsání znaku -. Tím bychom měli definovanou hlavičku našeho výpisu. Nyní budeme formátovat vlastní data, podívejme se na následující zápis:

PS C:\> $c = 1.12345
PS C:\> "{0:###.0}`n{0:###.00}" -f $c
1.1
1.12

Počet desetinných míst dané proměnné je zobrazen v závislosti na počtu desetinných míst ve formátu. Toho můžeme využít při zarovnávání ve výsledné tabulce. Připravíme si proto jednotlivé části a vyzkoušíme je na naší proměnné z našeho dnešního prvního příkladu.

PS C:\> $proc

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
    470 7 56152 57788 163 4.95 3856 powershell

PS C:\> $p = "{0,-20}" -f $proc.ProcessName
PS C:\> $cpu = "{0,6:###.0}" -f $proc.CPU
PS C:\> $ws = "{0,7:###.00}" -f ($proc.WS/1MB)
PS C:\> "{0}{1}{2}" -f $p, $cpu, $ws
powershell 4.8 56.43

První formátování je nám již známé – určujeme šířku sloupce pro jméno procesu. Ve druhém a třetím kombinujeme šířku sloupce se zobrazením na určitý počet desetinných míst. Pro ověření můžeme samozřejmě vyzkoušet toto:

PS C:\> $cpu
   4.8
PS C:\> $ws
   56.43
PS C:\> $proc
powershell
PS C:\> $proc.Length
20

A vidíte, že jednotlivé položky jsou opravdu „předformátované“ podle naší potřeby. Nyní vše spojíme dohromady.

$procesy = Get-Process | Sort-Object CPU -Descending | Select-Object -First 10
$header = "{0,-20}{1,6}{2,7}" -f 'ProcessName','cpu','ws'
$header = "{0}`n{1}" -f $header, ('-'*$header.Length)
$header
foreach ($p in $procesy) {
    $proc = "{0,-20}" -f $p.ProcessName
    $cpu = "{0,6:###.0}" -f $p.CPU
    $ws = "{0,7:###.00}" -f ($p.WS/1MB)
    "{0}{1}{2}" -f $proc, $cpu, $ws
}

A výsledkem bude náš očekávaný výstup:

ProcessName cpu ws
---------------------------------
TOTALCMD 465.0 11.89
System 260.3 1.26
WINWORD 213.8 77.27
procexp 184.5 26.39
svchost 125.1 99.32
powershell_ise 101.0 164.04
explorer 98.9 30.96
svchost 75.8 29.45
aenrjpro 69.9 8.82
chrome 68.0 23.44

Formátovací operátor se dá využít například pro generování textových souborů v určitém formátu. Připravíte si jednotlivé „šablony“ jako v předchozím případě a pak výsledný text pouze přesměrujete na potřebný výstup.

- David Moravec