Seriál: Windows Powershell – PS pro programátory (část 6.)

Na Technet Flash běží už několikátý díl o skriptovacím nástroji zvaném PowerShell, kterým provází David Moravec. Seznámili jsme se se základy PowerShellu a dotknuli se i pokročilejších témat jako jsou WMI anebo PsDrives.

PowerShell byl navrhnutý primárně pro administrátory, kteří dostali do rukou konečně rozumný nástroj umožňující zautomatizovat jim jejich postupy. Proto jej Microsoft integruje například do Windows Serveru 2008, nebo do SQL Serveru 2008. Vznikají i další knihovny – na správu IIS, Exchange, VmWare, atd.

Proč by ale PowerShell neměl dostat šanci i u programátorů, nebo pokročilých uživatelů Windows? Pokud jste někdy tvořili .bat soubor, nebo se snažili o „skriptování“ v klasické windows command line, bude PowerShell určitě správnou volbou. Jeho syntaxe je daleko příjemnější, více příbuzná běžným programovacím jazykům. Krom toho disponuje i velmi zajímavými příkazy (cmdlety) přímo integrovanými do jádra.

Tento článek má být určen především programátorům. Budu se zde snažit nastínit některé tipy, které jim mohou usnadnit práci.

Automatické zpracování

Jako první asi zdůrazním tu základní věc, proč PowerShell získal takovou oblibu – velmi jednoduše můžeme vyřešit úlohu, kterou bychom museli jinak psát například v C#. Co znamená "jednoduše"? Jediné, co musíte udělat, je vytvořit si ps1 soubor a tento spustit v PowerShell konzoli. (samozřejmě bychom se obešli i bez souboru, ale takto si postupně můžeme tvořit knihovnu užitečných skriptů)

Jak bychom to dělali jinak? Ve Visual Studiu bychom vytvořili C# command line projekt a do přislušné vygenerované Main metody bychom začali psát kód. Parsování předaných argumentů, procházení souborů, adresářů, operace nad nimi, práce s chybovými stavy atd. Přeložit, spustit a máme výsledný exe soubor. Na jednoduchou úlohu je toto příliš zdlouhavé a množství vygenerovaných souborů je velké (i případě, že bychom pouze překládali pomocí csc, počet souborů je dvojnásobný).

C# se hodí na komplexní zpracování, kde využijeme jeho aspektů. Už jen code noise je u C# ve srovnání s PowerShellem obrovský. Jakýkoliv úkol proveditelný z .NETu, se dá zpracovat pomocí PowerShellu. Typickým úkolem může být: projdi všechny soubory v daném adresáři, otevři je a nahraď řetězec XYZ řetězcem ABC.

Samozřejmě se nemusíme omezovat jen na nahrazování. Nedávno jsem viděl člověka, který řešil duplicitu u svých fotek. Na disku měl velmi mnoho fotografií a některé byly v různých složkách vícekrát. Jeho přáním pak bylo najít ty fotografie, které jsou stejné. Přitom bylo potřeba u nich ignorovat i Exif – některé totiž byly s Exifem, jiné bez. Můžete se podívat na daný dotaz.

Jinak pro jednu velmi jednoduchou ukázku si můžeme roztřídit fotografie podle toho, jestli jsou navysoko, nebo naležato. Výsledek si pak seřadíme podle orientace a jména. Fotografie mohou být v adresáři libovolně hluboko zanořené:

 $d = gci D:\temp\ dscn*.jpg -rec | 
    % {
       $b = new-object System.Drawing.Bitmap $_.FullName
       New-Object PsObject -prop @{
        Orientation=if($b.Height -gt $b.Width) { 'v' } else {'h' };
        Path=$_.fullname }
       $b.Dispose()
    } | 
    sort -Property Orientation,Path

Kód by se dal ještě zkrátit nevytvářením objektu, ale už by to bylo případně na úkor čitelnosti.
Podobných úloh pak můžeme najít mnoho, stačí zapojit svou fantazii. Dále už budu mluvit o konkrétních technikách a vlastnostech PowerShellu.

Práce s XML

Jedna z věcí, která se vám zalíbí, je práce s XML. Představte si, že se na celý XML soubor podíváme jako na objekt – z atributů se stanou stringové property a z vnořených elementů se stanou složené property objektu. Leckoho asi napadne (de)serializace. V tomto případě sice o deserializaci nejde, ale výsledek je podobný. Při deserializaci je vytvořen objekt určitého typu, zatímco při načtení xml v PowerShellu dostaneme k dispozici objekt typu XmlDocument, který je všem programátorům v .NET dobře známý.

Vezměme si příklad. Nejdřív si vytvoříme testovací xml soubor.

 PS> @"
<root>
   <article date="2010-12-01">
    <name>Discover new dimensions</name>
    <body>Discover them now. Go!</body>
   </article>
   <article date="2000-01-01">
    <name>Future</name>
    <body>what will be with us in ten years?</body>
   </article>
</root>
"@ | Set-Content c:\temp\flashtest.xml

Zkusíme soubor načíst a přetypovat na xml (jde o použití tzv. accelerátoru) a podívat se, co se nám vlastně vrátilo.

 PS>$x = [xml](gc c:\temp\flashtest.xml)
PS>$node = $x.root.article | ? { [datetime]$_.date -lt [datetime]'2005-01-01' }
PS>$node
date         name                     body
----         ----                     ----
2000-01-01   Future                   what will be with us in ten years?
PS>$node.name = 'Near ' + $node.name
PS>$x.root.article[0].date = (get-date).ToString('yyyy-MM-dd')

Pomocí tečkové notace známé snad všem programátorům přistupujeme k jednotlivým elementům. Proměnná $x je typu XmlDocument, zatímco $node je typu XmlElement. Nastavení nové hodnoty atributu, nebo testového elementu probíhá jednoduchým přiřazením. Přidávání elementů je již plně v režii .NET.

 PS>$note = $x.CreateElement('note')
PS>$note.InnerText = 'poor content'
PS>$node.AppendChild($note)
PS>$x.root.article[1]
date        name             body                                note
----        ----             ----                                ----
2000-01-01  Near Future      what will be with us in ten years?  poor content

Vybírat elementy pomocí XPath šlo dříve opět jen pomocí .NETích prostředků, od verze 2 máme k dispozici cmdlet Select-Xml.

 PS>Select-Xml -Xml $x -XPath '//article[contains(body/text(),"ten")]'
PS>#použití namespace
PS>$ns = @{ e = "https://www.w3.org/1999/xhtml" } 
PS>Select-Xml -Path $somePath -Xpath //e:div[@id] -names $ns

Jako zdroj pro Select-Xml můžeme použít nejen xml, ale i seznam cest k xml souborům, nebo xml ve formě řetězce (string).

Na uložení xml nemá PowerShell žádné speciální prostředky. Použijeme opět .NET volání:

 PS>$x.Save('c:\temp\res.xml')

Regex tester

V PowerShellu si můžete velmi rychle vyzkoušet své regulární výrazy. Nebudu zde popisovat, co je to regulární výraz, jak se tvoří a k čemu slouží. Více informací k regulárním výrazům nabízí například Regular-expressions.info.
Místo vysvětlování základů se podíváme, jak můžeme rychle otestovat, zda regulární výraz "dělá to, co má".

Jestliže máte po ruce konzoli PowerShellu, je toto snad nejrychlejší způsob (pozn. pokud se vám konzole špatně hledá mezi ostatními okny, možná vám pomůže AutoHotkey). Spouštět programy určené pro testování regulárních výrazů nebo si je zkoušet online vyžaduje režii, která programátora zpomalí. Jak uvidíme, PowerShell touto brzdou nebude. Podívejme se tedy, jak na to – pro testování regulárních výrazů je zde totiž několik přístupů.

Operátor -match

Nejjednodušším z nich je použití operátoru -match, kterému jako pravý operand předáme regulární výraz.

 # hloupý regex jen pro účel demonstrace
PS>'jeho email je karel@novak.cz' -match ' \w+@[a-zA-Z_]+\.(?<d>[a-zA-Z]{2,3})' 
True
PS>$matches
Name                           Value
----                           -----
d                              cz
0                              karel@novak.cz

V kolekci $matches nám PowerShell udržuje groupy z posledního vyhodnocení operátorem -match. Groupa 0 obsahuje celý řetězec, který regulárnímu výrazu odpovídá. Regulární výraz není case sensitive a v podstatě odpovídá regulárnímu výrazu bez jakýchkoliv speciálních options.

Pro případ, že potřebujeme mít test case-sensitive, použijeme operátor -cmatch.

Operátor -match také umí filtrovat pole řetězců. Vyzkoušejte si následující příklad a hned pochopíte:

 PS>'1a','2b','1c' -match '1\w'
PS>'1a' -match '1\w'

Akcelerátor [regex]

Na vytvoření regulárního výrazu můžete použít akcelerátor [regex]:

 PS>$r = [regex]'^\w+@[a-zA-Z_]+\.(?<d>[a-zA-Z]{2,3})$'
PS>$r.GetType().FullName
System.Text.RegularExpressions.Regex

Vytvoří se klasický .NET regulární výraz a uloží do proměnné $r. Opět zde nemá žádné zvláštní options, o čemž se můžeme přesvědčit takto:

 PS>$r | fl

S regulárním výrazem pak pracujeme tak, jak známe – pokud bychom nevěděli, pak nám pomůže výpis metod a properties:

 PS>$r | gm
   TypeName: System.Text.RegularExpressions.Regex
Name                MemberType Definition
----                ---------- ----------
...
GetGroupNames       Method     string[] GetGroupNames()
GetGroupNumbers     Method     int[] GetGroupNumbers()
…
IsMatch             Method     bool IsMatch(string input), bool IsMatch(strin
Match               Method     System.Text.RegularExpressions.Match Match(st
Matches             Method     System.Text.RegularExpressions.MatchCollection
Replace             Method     string Replace(string input, string replacemen
...

Vytvoření objektu

Poslední možností, která ale úzce souvisí s druhou, je vytvoření regulárního výrazu pomocí cmdletu new-object. Zde pak můžeme specifikovat volby jako multiline, singleline apod.

 PS>$opts = [System.Text.RegularExpressions.RegexOptions]'MultiLine,
SingleLine'
 PS>$r = new-object Text.RegularExpressions '^\w+@[a-zA-Z_]+?\.(?<d>
[a-zA-Z]{2,3})$',$opts

Povšimněte si zajímavé syntaxe, jak specifikovat options u regulárního výrazu – nenašel jsem ji pořádně v nápovědě popsanou. Pomohla mi pouze zmínku na blogu Using Enumerated types (Enums) in PowerShell.

Obecně se s enumy v PowerShellu pracuje hezky. Není nutné uvádět typ enumu, stačí pouze hodota ve stringové podobě.
Navíc – zkuste si například toto:

 PS>[string]::Compare('a','a',[Globalization.CultureInfo]::CurrentCulture, 
'test') 
Cannot convert argument "3", with value: "test"..... The possible 
enumeration values are "None, IgnoreCase, IgnoreNonSpace, ... Ordinal"."
PS>[string]::Compare('a','a',[Globalization.CultureInfo]::CurrentCulture,
'IgnoreCase')

PowerShell vám napoví, jaké hodnoty může enum nabývat – není tedy nutné hledat v dokumentaci. Výrazně se tím zrychlí vývoj.

Pozn.: Samozřejmě můžeme použít i statické metody třídy [regex], které se volají pomocí dvojtečky:

 PS>[regex]::IsMatch('karel@novak.cz', '^\w+@[a-zA-Z_]+\.(?<d>[a-zA-Z]{2,3})$')

Pozn. 2: Krom operátoru -match je vhodné znát i operátor -replace, který slouží k nahrazování textu. Velmi jednoduché a účinné nahrazení textu v souboru můžeme dosáhnout takto:

 PS>(Get-Content c:\test.txt) -replace 'abc','def' | Set-Content c:\test.txt
Metoda FindR

Později v sekci o clipboardu využívám metodu FindR. Jde o funkci, která filtruje vstupní data podle zadaného regulárního výrazu.

 filter FindR($regex) {
 [Regex]::Matches($_, $regex, 'IgnoreCase') | % { $_.Value }
}

Jako data můžeme poslat prakticky cokoliv. PowerShell se poté snažit vstup konvertovat na string, protože metoda Matches očekává string. Pokud do pipeline posíláme objekty, PowerShell runtime se rozhodne sám, jak objekt zkonvertuje na string.

Tak například dir | findr '.*gs.*' nám bude fungovat, ale bude vracet pouze jména souborů/adresářů.

Ale Get-WinEvent -LogName Application -MaxEvents 10 | findr '.*instal.*' nezafunguje, protože jako vstup do metody Matches PowerShell vloží řetězec 'System.Diagnostics.Eventing.Reader.EventLogRecord'. Tuto hodnotu zřejmě získal prostým zavoláním ToString() na objektech z Get-WinEvent.

Reálné použití si sami jistě dokážete vymyslet. Já jej sem tam používám na práci s logy od zákazníků. Naposledy jsem filtr použil, když jsem zkoumal log, který se týkal importu souborů. V logu byly zapsány jejich velikosti a já jsem potřeboval zjistit jejich průměrnou velikost a celkový součet. Na vyparsování konkrétních velikostí je použit look behind .

 gc c:\dev\WO\\wpdataimport.log | 
  findr -r '(?<=Job content file size: )\d+' | 
  Measure-Object -Sum -Average

První příkaz načte soubor, druhý profiltruje jeho řádky a vrátí řetězce (číslice), před kterými je "Job content file size: " a poslední příkaz sečte čísla (zkonvertuje řetězce na čísla) a spočítá jejich průměr.

Kódování, dekódování, konverze, ...

Obzvláště weboví vývojáři někdy potřebují pracovat s base64 stringy, případně kódovat a dekódovat url. Samozřejmě na konverzi existují dostupné nástroje. Jednodušší ale je přímo do PowerShell profilu přidat příslušné funkce, takže budou vždy velmi rychle dostupné.

 # načte assembly potřebnou pro práci s url (pokud ještě načtená nebyla)
Add-Type -AssemblyName System.Web

function FromBase64([string]$str) {
  [text.encoding]::utf8.getstring([convert]::FromBase64String($str))
}
function ToBase64([string]$str) {
  [convert]::ToBase64String([text.encoding]::utf8.getBytes($str))
}
function UrlDecode([string]$url) {
  [Web.Httputility]::UrlDecode($url)
}
function UrlEncode([string]$url) {
  [Web.Httputility]::UrlEncode($url)
}
function HtmlDecode([string]$url) {
  [Web.Httputility]::HtmlDecode($url)
}
function HtmlEncode([string]$url) {
  [Web.Httputility]::HtmlEncode($url)
}

Ti kteří často pracují v PowerShellu pak mohou ocenit například takovou funkci:

 PS> function Run-GoogleQuery {
  param([string]$word)
  Start-Process ('https://www.google.cz/search?q=' + (UrlEncode $word))
}
PS> Set-Alias qg Run-GoogleQuery
PS> qg 'toto je test' # spustí defaultní browser s dotazem na google

Konverzi HtmlEncode jsem například využil při psaní tohoto článku, když jsem potřeboval vložit PowerShell kód a zakódovat korektně HTML znaky.
S využitím funkce clip (kterou uvedu později v sekci o clipboardu) to bylo vcelku jednoduché:

 HtmlEncode (clip) | clip

(znamená to: vezmi obsah schránky, zakóduj a vlož do schránky)

Převod Html2Xml

Někdy se hodí i převod html na xml. V tomto případě využijeme volně dostupnou assembly SgmlReader.

 function Convert-Html2Xml {
  param(
    [Parameter(ValueFromPipeline=$true)][object[]]$html
  )
  begin   { $sb = new-object Text.StringBuilder(20kb) }
  process { $html | % { $null = $sb.AppendLine($_) } }
  end {
    # udelame si zivot jednodussi...
    $str = $sb.ToString().Replace(' xmlns="https://www.w3.org/1999/xhtml"', '')
    Add-Type -Path G:\bin\SgmlReaderDll.dll 
    
    $sr = new-object io.stringreader $str

    $sgml = new-object Sgml.SgmlReader
    $sgml.DocType = 'HTML';
    $sgml.WhitespaceHandling = 'All';
    $sgml.CaseFolding = 'ToLower';
    $sgml.InputStream = $sr;

    $xml = new-object Xml.XmlDocument;
    $xml.PreserveWhitespace = $true;
    $xml.XmlResolver = $null;
    $xml.Load($sgml);

    $sgml.Close()
    $sr.Close()
    
    $xml
  }
}

Máme pak dva způsoby, jak volat:

 PS>$x1 = gc c:\temp\testhtmlsource.delete.html | Convert-Html2Xml
PS>$x1.Save('c:\temp\test1.xml')
PS>$x3 = Convert-Html2Xml (gc c:\temp\testhtmlsource.html)
PS>$x3.Save('c:\temp\test2.xml')

Tuto funkci můžeme například použít, pokud daná webová služba nemá přístupné API, ale uživatel s ní pracuje pouze přes její webové UI. Příkladem může být https://www.slovnik.cz. Kompletní řešení ve stylu quick&dirty zahrnuje i použití XPath pomocí cmdletu Select-Xml.
Jiné řešení jsem rychle stvořil pro kolegu, který vytvářel seznam filům s linky na CSFD. nehledejte krásu, ale automatizaci :)

A ještě jeden rychlý příklad: narazili jste na blog, který má velmi mnoho příspěvků a do stránky se vypisují celé příspěvky. Může vypadat například takto. Rádi byste věděli názvy jednotlivých článků, protože vás zajímá třeba o čem ten člověk píše? Samozřejmě můžete scrollovat, ale protože je stránka velmi dlouhá, hrozí nebezpečí, že něco zajímavého přeskočíte. Jaké je řešení?

 $xml = Convert-Html2Xml (download-page 'https://www.nivot.org/CategoryView,
category,PowerShell.aspx' )
Select-Xml -Xml $xml -XPath '//div[@class="itemTitle"]/a/text()' | % 
{ $_.Node.Value }

PowerShell 2.0 - About Dynamic Parameters
PowerShell 2.0 – Introducing the PModem File Transfer Protocol
PowerShell 2.0 - Enabling Remoting with Virtual XP Mode on Windows 7
PowerShell 2.0 goes RTM for ALL Platforms
PowerShell 2.0 - Module Initializers
PowerShell 2.0 – Getting and setting text to and from the clipboard
PowerShell 2.0 – Asynchronous Callbacks from .NET
PowerShell – Function Parameters &amp; .NET Attributes
PowerShell 2.0: A Configurable and Flexible Script Logger Module
....

Clipboard

Na první pohled si možná řeknete: "K čemu mi bude clipboard?" . Velmi často jej používám jako transportní mechanismus z nějaké aplikace do PowerShellu. Už jste v článku několikrát viděli použití funkce clip, ale tedy ještě pro zopakování ještě poslední příklad:

Mám problém s deadlocky na sql serveru. Spustím si jej tedy z konzole s přepínači kvůli detekci deadlocků. Poté, co se mi podaří nasimulovat deadlock, označím si obsah konzole sqlserveru a jdu do PowerShellu. Chci například zjistit jednotlivá SPID. Jak na to?
(clip) | FindR -r 'SPID: \d+'
Toto mi vypíše ID všech zúčastněných procesů (pro vynechání duplicit bychom použili select -unique).

Vím, že v SQL analyzeru je možné SPID také vidět, berte to jen jako příklad.

Podobných případů jistě naleznete dost. Funkce na práci s clipboardem máme tedy tyto:

 Add-Type –a system.windows.forms
function Set-ClipBoard { 
  param(
    [Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=0)][object]$s
  )
  begin { $sb = new-object Text.StringBuilder }
  process { 
    $s | % { 
      if ($sb.Length -gt 0) { $null = $sb.AppendLine(); }
      $null = $sb.Append($_) 
    }
  }
  end { [windows.forms.clipboard]::SetText($sb.Tostring()) }
}
function Get-ClipBoard { 
  [windows.forms.clipboard]::GetText() 
}
# funkce clip zastupuje Get-Clipboard i Set-Clipboard podle kontextu:
# gc c:\test.txt | clip 
# clip | sc c:\test.txt
function clip {
    param([Parameter(Mandatory=$false,ValueFromPipeline=$true)][object]$s)
  begin { $sb = new-object Text.StringBuilder }
   process {
    $s | % { 
      if ($sb.Length -gt 0) { $null = $sb.AppendLine(); }
      $null = $sb.Append($_) 
    }
   }
   end {
    if ($sb.Length -gt 0) { $sb.Tostring() | Set-ClipBoard} 
    else                  { Get-ClipBoard  }
  }
}

K tomu, aby přístup ke schránce fungoval, je nutné, aby PowerShell běžel s přepínáčem -sta, nebo abyste tento kód pouštěli v ISE prostředí. Pokud byste přesto chtěli mít přístup ke schránce i v MTA režimu, podívejte se na řešení .

Je to všechno?

Co by si zasloužilo alespoň zmínit, je cmdlet New-WebServiceProxy, dále práce s sql serverem (pomocí .NET prostředků), vytváření jednodušších GUI aplikací (ať už WinForms, nebo WPF). Tato témata se už do přehledu nevešla vzhledem k jejich rozsahu. Na internetu ovšem je k nalezení plno zdrojů, které zájemcům pomohou.

Za sebou mám základní věci, které vás jako vývojáře mohou uchvátit, nebo nechat chladným. Stále platí, že nejdůležitější je první bod – rychlé a efektivní řešení běžných úkolů a v tom se PowerShellu těžko něco vyrovná.

- Josef Štefan

Další díly seriálu:
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.)