Készítsünk olyan függvényt, ami kiszámolja a neki átadott fájlok hosszainak összegét!
[19] PS C:\old>function fájlhossz ([System.IO.FileInfo] $f)
>> {
>> $hossz = 0
>> $hossz += $f.length
>> $hossz
>> }
>>
[20] PS C:\old>fájlhossz C:\old\alice.txt
709
Ez tökéletes, de ehhez nem nagyon kellett volna függvény, hiszen a fájlok Length tulajdonsága pont ezt az értéket adja vissza. Én azt szeretném, hogy egy fájlgyűjteményt is átadhassak a függvénynek, így több fájlnak az együttes hosszát is megkaphassam. Ehhez alakítsuk át a függvényt:
[33] PS C:\old>function fájlhossz ([System.IO.FileInfo[]] $f)
>> {
>> $hossz = 0
>> foreach ($file in $f)
>> {
>> $hossz += $file.length
>> }
>> $hossz
>> }
>>
[34] PS C:\old>fájlhossz (get-childitem)
395962
A függvény paraméterdefiníciós részében felkészülök fájltömb fogadására, majd a függvény törzsében egy foreach ciklussal végig szaladok az elemeken és összeadogatom a hosszokat. Ez már jó, csak nagyon nem PowerShell-szerű a függvény meghívása. Sokkal elegánsabb lenne, ha a get‑childitem kimenetét lehetne becsövezni a függvényembe. Nézzük meg, hogy alkalmas-e erre a függvényem átalakítás nélkül?
[35] PS C:\old>get-childitem | fájlhossz
0
Nem igazán... Merthogy ilyen csövezős esetben a PowerShell nem ad át értéket a „normál” paramétereknek, hanem egy automatikusan generálódó $input változónak adja ezt át:
[36] PS C:\old>function fájlhossz
>> {
>> $hossz = 0
>> foreach ($file in $input)
>> {
>> $hossz += $file.length
>> }
>> $hossz
>> }
>>
[37] PS C:\old>get-childitem | fájlhossz
395962
Megjegyzés
Elég speciális viselkedésű ez az $input! Nézzünk erre egy példát:
[38] PS C:\> 1,2,3 | &{foreach($elem in $input){"elem: $elem";break} ; "input:
$input"}
elem: 1
input: 2 3
A fenti példában három elemet küldök tovább a csövön, ahol egy foreach ciklussal kezdeném kiszedegetni az elemeket a $input változóból, de rögtön az első elem után meg is szakítom a ciklust. Ezután kiírom a $input tartalmát. A kimeneten az a furcsaság állt elő, hogy az első elem „kikerült” a $input változóból, és csak a maradékot kaptuk meg.
Milyen adattípus ez a $input akkor?
[39] PS C:\> 1,2,3 | &{Get-Member -InputObject $input}
TypeName: System.Collections.ArrayList+ArrayListEnumeratorSimple
Name MemberType Definition
---- ---------- ----------
Clone Method System.Object Clone()
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
MoveNext Method bool MoveNext()
Reset Method System.Void Reset()
ToString Method string ToString()
Current Property System.Object Current {get;}
Látható, hogy ez egy furcsa ArrayList típus, aminek van pár hasznos metódusa is, ezek közül a legfontosabb a Reset(), mert ezzel lehet visszatérni az összes csőelem-lista elejére:
[40] PS C:\> 1,2,3 | &{foreach($elem in $input){"elem: $elem";break} ; $input.R
eset(); "input: $input"}
elem: 1
input: 1 2 3
Ezzel a kis trükkel végül is meg tudjuk kapni az összes elemet.
Visszatérve a fájlhossz függvényemhez, ami így már majdnem tökéletes volt, de lehet ezt még szebbé tenni! Mi ezzel a gond? Az, hogy ha egy nagyon mély, sok fájlt tartalmazó könyvtárstruktúrára alkalmazom, akkor azt fogjuk tapasztalni, hogy a PowerShell.exe memória-felhasználása jó alaposan felmegy, mire összeáll az $input változóban a teljes fájlobjektum lista, és csak utána tud lefutni a foreach ciklus. Sokkal optimálisabb lenne, ha már az első fájl átadásával elkezdődhetne a számolás, a memóriában így mindig csak egy fájllal kellene foglalkozni. Erre is van lehetőség, bár - érdekes módon – a help erről nem ír! Merthogy egy függvénynek valójában lehet három elkülönülő végrehajtási fázisú része:
function <név> ( <paraméter lista> )
{
begin {
<parancsok>
}
process {
<parancsok>
}
end {
<parancsok>
}
}
Lehet tehát egy függvénynek egy „begin” része, ami egyszer fut le, a függvény meghívásakor. Lehet egy „process” része, ami minden egyes csőelem megérkezésekor lefut, és lehet egy „end” része, ami az utolsó csőelem érkezése után fut le.
Alakítsuk át úgy a fájlhossz függvényemet, hogy a process szekcióba kerüljön a feldolgozás:
[44] PS C:\old>function fájlhossz
>> {
>> begin {$hossz = 0}
>> process {$hossz += $_.length}
>> end {$hossz}
>> }
>>
[45] PS C:\old>dir | fájlhossz
395962
Mik a főbb változások? Egyrészt nem kell nekünk ciklust szervezni, mert a csőelemek amúgy is egyesével érkeznek. Viszont a process szekcióban nem a $input változóval kell foglalkoznunk, hanem a $_ változóval, az tartalmazza az aktuális csőelemet. Olyannyira, hogy ha így begin/process/end szekciókra bontjuk a függvényt, és a $_ változót használjuk, akkor nem is generálódik $input!
Ez olyan fontos, és annyira nincs benne a helpben, hogy kiemelem újra:
Fontos!
Ha a függvényemben külön begin/process/end szekciót használok, akkor nem képződik $input változó, de a process szekcióban a $_ változón keresztül érhetem el a bejövő csőelemeket!
Ha egy függvénynek csak process szekciója lenne, akkor az ilyen függvényt egy külön kulcsszóval, a filter -rel is definiálhatjuk, és akkor egyszerűbb a szintaxis is. Például, ha egy függvényem csak annyit csinálna, hogy a csőben belé érkező számokat megduplázza, akkor az így nézne ki:
[46] PS C:\ filter dupláz
>> {
>> $_*2
>> }
>>
[47] PS C:\ 1,2,5,9 | dupláz
2
4
10
18
Látszik, hogy nincs szükség process kulcsszóra, nincs felesleges bajuszpár. Viszont nem lehetséges sem begin, sem end szekció definiálása, így ha ilyenre van szükségünk (valamilyen függvényváltozót kellene inicializálni, vagy az egész folyamat végén kellene még valamit kitenni az outputra), akkor azt nem ússzuk meg a function használata nélkül.
Megjegyzés
Fontos tudni, hogy hogyan viselkednek a gyűjtemények a csővezetékben! Láttuk, hogy ha egy egyszerű tömböt adunk át a csővezetéknek, akkor a tömb elemei egymás után kerülnek feldolgozásra. Nézzük, mi történik, ha összetett tömböt adunk át:
[22] PS C:\munka> 1,(2,3) | dupláz
2
2
3
2
3
[23] PS C:\munka> (1,(2,3) | dupláz).count
5
Az előbb látott dupláz filternek egy kételemű tömböt adok át a [22]-es sorban, amely tömbnek a második eleme egy kételemű tömb. Ennek a kimenete az, amire számítottunk, azaz a megkaptam az 1 dupláját, majd a (2,3) tömböt kétszer. De vajon az eredmény hány elemű? A [23]-as sorban látható, hogy a kimenet 5 elemű. Azaz a csőfeldolgozás szétszedi a tömböket elemekre. Vajon hogyan lehetne elérni, hogy az eredmény háromelemű legyen: 2, (2,3), (2,3)? (Szerintem ez is lehetett volna az alapértelmezett, de valószínű a PowerShell alkotói gyakoribbnak a mostani kimenetet tartották, azaz 2,2,3,2,3. A megoldás már egy korábban már látotthoz hasonló trükkel érhető el:
[24] PS C:\munka> 1,,(2,3) | dupláz
2
2
3
2
3
[25] PS C:\munka> (1,,(2,3) | dupláz).count
3
Azaz a második elem előtt egy extra vesszőt raktam, így már dupla csőelem kifejtés nem történt.