:

Szerző: Bodnár Ádám

2010. március 30. 12:26

A Windows PowerShell rejtelmei - 2. rész

A Windows PowerShell használatát bemutató cikksorozat második részében szöveges állományok feldolgozásához adunk tippeket.

A cikksorozat első részében egy egyszerű fájlrendszerező példán keresztül kezdtem bemutatni a PowerShellt. A cikkhez adott hozzászólásokra reagálva fontosnak tartom hangsúlyozni, hogy nem akarom a PowerShellt más shellekkel szembeállítani. Egyrészt azért nem, mert nem értek más shellekhez, így nem tartanám ezt korrektnek.

Másrészt egy ilyen összehasonlításnak annyi értelme lenne, mint például a magyar és az angol nyelv összehasonlításának. Melyik nyelv jobb, szebb, praktikusabb? Ez elég szubjektív dolog, természetesen a legtöbb ember az anyanyelvét tartja a legjobbnak, legszebbnek, nem utolsósorban azért, mert azt tudja legjobban, azt tanulta meg legelőször. Másrészt nyilván Angliában praktikusabb az angol, mint a magyar. Ugyanez a helyzet a PowerShellel is: a Windows platformon valószínű jobb a PowerShell a többi shellnél. Ha csak azt nézzük, hogy számos Microsoft termék PowerShellel kezelhető, más shellel nem, akkor szerintem ez elég egyértelmű.

De térjünk vissza a PowerShellhez! Ebben a példában már  próbálok kevésbé szájbarágós lenni, feltételezem immár, hogy az olvasók zöme azért valamilyen programozási ismerettel rendelkezik. A mostani példáimban már a könyvemben is alkalmazott jelölést használom, azaz itt nem a grafikus PowerShell ISE felületen dolgozom, hanem a karakteres konzolon. Kicsit még módosítottam a \"gyári\" prompton, a PS C:\\> elé bebiggyesztettem egy szögletes zárójelek közti sorszámot, hogy jobban lehessen hivatkozni ezekre a sorokra.

Nézzük akkor a következő témakört, a szövegfájlban tárolt adatok feldolgozását! Vegyünk egy képzeletbeli kerékpáros versenyt, amelyen három kategóriában lehet indulni: férfi, nő, ifjúsági, ezek különböző távokat jelentenek. A verseny eredményét egy egyszerű szöveges fájlba írjuk be, legyen a fájl neve bringa.txt:

Név, Nem, Életkor, Táv, Idő
Brin Gáspár, férfi, 15, 45, 1:34:35
Soós Tibor, férfi, 39, 60, 2:10:34
Fájdalom Csilla, nő, 22, 30, 1:21:11
Vegetári János, férfi, 35, 60, 1:59:54
Beléd Márton, férfi, 42, 60, 2:22:10
Jövő Éva, nő, 32, 30, 1:12:05
Brill Antal, férfi, 16, 45, 1:54:22
Kerék Pál, férfi, 17, 45, 1:39:54
Hajt Anna, nő, 26, 30, 1:19:32

Ezeket az adatokat kellene feldolgozni PowerShell segítségével. Ha megfigyeljük a szöveget, látható, hogy ez valójában egy CSV (comma separated values) adatokat tartalmazó fájl, azaz vesszővel elválasztott értékek táblázata, az első sorban az „oszlopok” nevei találhatók. Az ilyen formátumú fájlt a PowerShell nagyon könnyen tudja kezelni:

[4] PS C:\\scripts> Import-Csv .\\scripts\\bringa.txt 
| Format-Table

Név             Nem             Életkor         Táv             Idő
-               -               -              -               -
Brin Gáspár     férfi           15              45              1:34:35
Soós Tibor      férfi           39              60              2:10:34
Fájdalom Csilla nő              22              30              1:21:11
Vegetári János  férfi           35              60              1:59:54
Beléd Márton    férfi           42              60              2:22:10
Jövő Éva        nő              32              30              1:12:05
Brill Antal     férfi           16              45              1:54:22
Kerék Pál       férfi           17              45              1:39:54
Hajt Anna       nő              26              30              1:19:32

Látható, hogy CSV fájlok beolvasására az Import-CSV cmdlet áll rendelkezésünkre. A PowerShell terminológiában az \"igazi\" parancsok neve a cmdlet (ejtsd: kommandlet). Ezek olyan parancsok, amelyeket a PowerShell saját infrastruktúrája biztosít, nem pedig valamilyen külső program. Szigorú szabályok vannak a parancsok elnevezésére, mindannyian ige-kötőjel-főnév szerkezetűek, ráadásul a lehetséges igék is jól behatároltak, így sok meglepetésre nem kell számítanunk, ráadásul elég könnyen kitalálható, akár egy számunkra ismeretlen parancsról is, hogy az mit is csinálhat.

Az Import-CSV például beolvassa egy CSV fájl tartalmát és az ottani információk alapján létrehoz speciális objektumokat olyan tulajdonságnevekkel, mint ami a fájl első sorában levő elnevezések. A fenti kifejezésem végén én még táblázatos nézetben kértem az adatokat a Format-Table cmdlet segítségével. Nézzük, hogy a megjelenített sorok tényleg objektumok, azaz tulajdonságaik külön-külön megszólíthatók:

[6] PS C:\\> $versenyzők = Import-Csv .\\scripts\\bringa.txt
[7] PS C:\\> $versenyzők[1].idő
2:10:34

Az egyszerűség kedvéért az előbb látott objektum-halmazt betöltöm egy $versenyzők változóba [6] és például Soós Tibor időeredményét kiolvastam [7]. A $versenyzők tömbjének egyes elemeire szögletes zárójelbe írt számmal kell hivatkozni, az első elem a 0. elem, így az én adatsorom az 1. elemben van, és ennek az Idő tulajdonsága adta meg az időeredményemet. Ezzel már most sok minden el tudunk végezni, például keressük a kategóriánkénti győzteseket:

[8] PS C:\\> $versenyzők | Group-Object -Property táv | ForEach-Object 
{$_.group | Sort-Object -Property idő | Select-Object -First 1} 
| Format-Table

Név             Nem             Életkor         Táv             Idő
-               -               -               -               -
Brin Gáspár     férfi           15              45              1:34:35
Vegetári János  férfi           35              60              1:59:54
Jövő Éva        nő              32              30              1:12:05

Mi történt itt? Vettem a versenyzői adatokat és rátettem őket a futószalagra. Az első gép, a Group-Object csoportosította ezeket, \"bedobozolta\" a Táv tulajdonság alapján, ez adja meg az egyes kategóriákat. Ennek kimenete olyan csoportobjektumok halmaza, melyek Group nevű tulajdonságán keresztül érhetők el az adott csoportba tartozó versenyzői adatok, azaz a doboz tartalma. Pillantsunk bele, hogy a gépsor ezen pontján mi is van a futószalagon, hogyan is néznek ki az így képzett dobozok:

[9] PS C:\\> $versenyzők | Group-Object -Property táv

Count Name                      Group
- -                      -
  3 45                        {@{Név=Brin Gáspár; Nem=férfi; Életkor=15; .
  3 60                        {@{Név=Soós Tibor; Nem=férfi; Életkor=39; T.
  3 30                        {@{Név=Fájdalom Csilla; Nem=nő; Életkor=22;.

Látható, hogy három doboz készült, egy-egy dobozon, azaz csoporton belül az oda tartozó adatok érkezési sorrendben vannak, nekem pedig időre kellene rendezni ezeket. Így a [8]-as sorban látható kifejezésben veszem a csoport-objektumokat egyesével a ForEach-Object cmdlettel, és azt adom neki utasításba, hogy vegye az aktuális csoport elemeit, amelyek tehát a Group tulajdonságon keresztül érhetők el. Ezeket helyezze egy belső, újabb futószalagra, amin rendezze idő szerint az adatokat a Sort-Object cmdlet segítségével, majd egy-egy csoportból vegye csak az első elemet, azaz a legkisebb időeredménnyel rendelkező versenyzőt a Select-Object cmdlettel. A legvégén a táblázatos nézetet kiadó Format-Table van ismét.

Fokozzuk ezt! Mi van akkor, ha arra vagyok kíváncsi, hogy vajon mi volt a versenyzők átlagsebessége? Ezt a legelegánsabban úgy tudjuk megtudni, ha ezt egy új tulajdonságként hozzátesszük az egyes versenyzői adatsorokhoz. És ha már alakítgatunk, akkor legyünk még precízebbek! Valójában itt az időadatokat szövegként olvasta be a PowerShell, készítsünk ezekből igazi, idő típusú adatokat. Ezt már a grafikus szkript-szerkesztőben raktam össze, hogy szebb tagolással tudjam ide másolni. Ezeket a formázott, tördelt sorokat egy az egyben ki lehet innen másolni, és be lehet illeszteni közvetlenül a PowerShell konzolba, a sortöréseket és az átlógó sorokat teljesen korrektül kezeli a rendszer:

$versenyzők = @()
Import-Csv c:\\scripts\\bringa.txt |
   ForEach-Object {
      $_.Idő = [timespan] ($_.idő+\".0\")
      $_ | Add-Member -Name Átlagsebesség -MemberType NoteProperty -Value `
	($_.táv/($_.idő.totalhours))
      $versenyzők += $_
   }

Mi történt itt? Előre előkészítettem egy $versenyzők nevű üres tömböt (@() – ez a jelölése egy üres tömbnek), majd a már látott módon elkezdem beimportálni az adatokat. Minden egyes adatsorra átalakítom az Idő tulajdonságot \"igazi\" idővé, ehhez a [timespan] típusjelölőt használtam, de mivel ez számítana még tizedmásodpercekre is, ezért azt még hozzábiggyesztem a beolvasott adathoz. Ezután az aktuális elemet ($_) kibővítettem egy újabb tulajdonsággal, aminek neve Átlagsebesség, típusa un. Noteproperty, ami gyakorlatilag azt jelenti, hogy ez egy statikus adat, értéke pedig a Táv elosztva az előbb kiszámolt Idő órákban kifejezett (totalhours) értékével.

Itt mutatkozik meg az időre alakítás haszna és a PowerShell típusossága, mert látható, hogy milyen egyszerűen lehet ezzel az időtartamok különböző mértékegységekben vett értékét képezni. A legvégén az adott adatsort beraktam a $versenyzők tömb következő elemeként. Nézzük, hogy tényleg benne vannak-e az átlagsebességek, sőt, rendezzük mindjárt eszerint csökkenő sorrendbe:

[26] PS C:\\> $versenyzők | Sort-Object -Property Átlagsebesség -Descending 
| Format-Table név, táv, idő, átlagsebesség

Név                Táv                Idő                    Átlagsebesség
-                  -                  -                      -
Vegetári János     60                 01:59:54              30,02502085070
Brin Gáspár        45                 01:34:35              28,54625550660
Soós Tibor         60                 02:10:34              27,57212152157
Kerék Pál          45                 01:39:54               27,0270270270
Beléd Márton       60                 02:22:10              25,32239155920
Jövő Éva           30                 01:12:05               24,9710982658
Brill Antal        45                 01:54:22              23,60827747012
Hajt Anna          30                 01:19:32              22,63202011735
Fájdalom Csilla    30                 01:21:11              22,17203859577

Hurrá! Benne van. A szebb megjelenítés miatt és most itt csak négy tulajdonságot választottam ki a táblázatba. És még mindig lehet fokozni! Hogyan lehetne például kiszámolni, hogy a versenyzők összesen hány kilométert tekertek, mi volt az átlagos hossz, legkisebb és legnagyobb táv és hány versenyző volt:

[30] PS C:\\> $versenyzők | Measure-Object -Sum -Average -Maximum 
-Minimum  Property táv


Count    : 9
Average  : 45
Sum      : 405
Maximum  : 60
Minimum  : 30
Property : Táv

Hát ez nem volt túl bonyolult! Miután ilyen jellegű egyszerű statisztikák nagyon gyakoriak, ezért a PowerShell alkotói adtak egy beépített cmdletet, a Measure-Object-et, ami megadott tulajdonságra ezeket a statisztikai adatokat kiszámolja. Látható például, hogy 9 versenyzőm volt, összesen 405 km-t tekertek és az átlagtáv pont 45 km volt.

Továbbra is várom a visszajelzéseket, javaslatokat, kérdéseket. A következő részben várhatóan a még egy picit a szövegek feldolgozásával fogok foglalkozni, de a szöveget már nem én fogom begépelni, hanem a webről fogom \"ellopni\".

Soós Tibor (PowerShell MVP, MCT), IQSOFT – John Bryce Oktatóközpont

a címlapról