ugrás a tartalomhoz

Perl alapjai IV. - Tömbök, hashek, elágazások, ciklusok és szubrutinok

Bártházi András · 2004. Aug. 31. (K), 22.30
Perl alapjai IV. - Tömbök, hashek, elágazások, ciklusok és szubrutinok
A cikksorozat mai cikkében továbbhaladunk a nyelv megismerésével, az elején a tömbökkel, hashekkel végezhető műveleteket vesszük át, majd az elágazások, ciklusszervezési lehetőségek, s legvégül a szubrutinok következnek. Mára már nem jutott tevés kép, egy teve megtekintéséhez egy melegebb éghajlatra való utazást, vagy valamely közeli állatkertbe történő kiugrást tudom javasolni (vagy esetleg a Perl weblap felkeresését?). Ezen alkalommal is bemutatok egy rövid programot, mely előre mutat egy kicsit, de nagyrészt épít az eddig megismert lehetőségekre.

Műveletek tömbökkel, hash-ekkel

A tömbök, hash-ek használatához is találunk beépített parancsokat, s bár személy szerint én hiányolok pár lehetőséget (unió, metszet, komplementer), de így is jóval többet lehetőséget tartalmaz a nyelv, mint például a C. Ebből a szempontból a Perl 6-os nyelvi szinten majd számos lehetőséget kínál az összekapcsolás (junctions) segítségével, de arra még várnunk kell majd egy kicsit. Persze ez a jelenlegi verziókban sem lehetetlen (sőt, teljesen egyszerű), például a mai cikk végén levő kódban az egyedi értékeket válogatjuk ki és számoljuk meg, mikor a hivatkozó oldalakat figyeljük.

Nézzük végig az idevágó parancsokat!

Kivágás, csere (splice)

Már láthattuk, hogy hogyan tudunk hozzáadni egy tömbhöz értékeket, de még nem tudjuk, hogy hogyan lehetne egyszerűen kicserélni, vagy levágni elemeket. Erre szolgál a splice parancs. Segítségével egy tömbből a megadott pozíciótól a megadott hosszig kivágja, vagy ha meg van adva egy lista, akkor kicseréli rá a tömb elemeit:

@tomb1=splice(@tomb,4);
@tomb2=splice(@tomb,4,2);
@tomb3=splice(@tomb,4,2,'ötödik','hatodik');
Az első példa levágja a tömb elemeit az ötödik elemtől kezdve. A második példa két elemet vág le az ötödik elemtől, a harmadik példa pedig kiveszi az ötödik és a hatodik elemet, és a helyükre beírja, hogy 'ötödik' és 'hatodik'. Mind a három tömb azokat az elemeket fogja tartalmazni, amelyek el lettek távolítva a tömbből.

Verem (push, pop)

A verem egy nagyon széleskörűen alkalmazott adatszerkezet, általában a megvalósítása tömb segítségével történik. Röviden leírva a működését: hozzá lehet adni és ki lehet venni belőle elemeket, amit utoljára betettünk, azt tudjuk kivenni elsőre (Last In First Out - LIFO), másképpen fogalmazva, fordított sorrendben kapjuk vissza az elemeket, mint ahogy a szerkezetben elhelyeztük.

Ennek az adatszerkezetnek a használatához a push és a pop parancsokat vehetjük igénybe. A push egy tömbhöz ad hozzá egy skalárt, a pop pedig egy tömbből vesz ki egyet (véglegesen eltávolítva onnan):

@verem=(1..10);
push(@verem,11);
push(@verem,12,13,14,15);
print pop(@verem);

Sor (shift, unshift)

A verem mellett a másik gyakran használt adatstruktúra a sor. Ezt is tömb segítségével valósíthatjuk meg, ennek a lényege, hogy amit elsőnek betettünk, azt vesszük ki elsőnek (First In First Out - FIFO). Használatához a shift és az unshift eljárások állnak rendelkezésünkre. Az unshift beszúr elemeket, a shift pedig kivesz:

@sor=(5..10);
unshift(@sor,4);
unshift(@sor,1,2,3);
print shift(@sor);

Sorrend megváltoztatása (sort, reverse)

Gyakran lehet szükségünk egy tömb elemeinek rendezésére. Erre szolgál a sort parancs, mely egy rendezett tömböt ad vissza (tehát nem helyben, magát a tömböt rendezi). A C nyelv qsort algoritmusával rendez, tehát viszonylag gyors. Alapesetben ASCII sorrend szerint hajtja végre a feladatát (tehát nem a számok értéke szerint!, ezért a 13 kisebb lesz, mint az 2, mert az 1 az kisebb, mint a 2), de az összehasonlító feltétel megváltoztatható, akár egy külön programrész, szubrutin írható rá - így elég összetett, több szempont szerinti rendezések valósíthatóak meg egy gyors algoritmusú rendezés segítségével.

@rendezett = sort(@rendezetlen);
@rendezett = sort({lc($a) cmp lc($b)} @rendezetlen);
@rendezett = sort({$a <=> $b} @rendezetlen);
Az első sor az alapeset, legtöbbször így használjuk az eljárást. A második sor mutatja be, hogy hogyan lehet megváltoztatni a feltételt, a harmadikban pedig egy példát látunk arra, hogy hogyan lehet számokat rendezni. A $a és a $b változók tartalmazzák a két összehasonlítandó értéket, ezeknek nem kell értéket adnunk. Értelemszerűen, ha felcseréljük a $a-t és a $b-t, akkor csökkenő sorrendben rendezett tömböt kapunk eredményül. A második és a harmadik példában valójában egy szubrutin foglal helyet az első paraméter helyén, ehelyett egy szubrutin nevét is lehet használni.

Adott tömb esetén, amennyiben szükségünk van a sorrend megfordítására, akkor a reverse eljárást használhatjuk. Ez a paraméteréül megadott tömböt fordított sorrendben adja vissza. Amennyiben rendezünk és utána lenne szükségünk a fordított sorrendre, akkor memóriahasználati szempontból inkább ne ezt használjuk, rendezzünk eleve fordított sorrendben (a két változó cseréjével: 'sort ({$b cmp $a}, @redezetlen)' ugyanaz lesz, mint a reverse sort ({$a cmp $b}, @redezetlen), csak tovább tart).

@vissza = reverse(@elore);
%hash1 = reverse(%hash2);
Az első példa egyértelmű, megfordítja a tömb elemeinek sorrendjét. Kérdés viszont, hogy mit csinál a második, hiszen nincs igazán értelme sorrendről beszélni a hash-ek esetén (a fordító logikája szerinti sorrendben tárolódnak a memóriában)? Az értékadáskor már bemutattam, hogy egy hash-nek egy tömböt is lehet értékül adni, ekkor páronként kerülnek be a hash-be a tömb elemei, az elsők (páratlan sorszámúak) a hash kulcsai, a másodikak (párosak) pedig ezek értékei lesznek. Ha megfordítjuk egy hash sorrendjét, akkor úgy viselkedik, mintha egy ilyen tömbnek a sorrendjét fordítanánk meg, azaz az értékek kulccsá válnak és fordítva. Nagyon hasznos trükk.

Műveletek a tömb összes elemén (chomp, chop, map, grep)

A tömbök minden elemén végrehajtandó feladat elvégzése szintén gyakori feladat különböző programrészek, algoritmusok megvalósításánál. Természetesen számos módszer létezik, az egyik legkézenfekvőbb, hogy valamilyen ciklusszervezési módszerrel (lásd később) végighaladunk az egyes elemeken. Erre azonban vannak a Perl-nek elegánsabb megoldásai is. Például, mikor egy szöveges állományból betöltjük a sorokat egy tömbbe, akkor a sorvég jelek ott maradnak a sorok - azaz most már a tömb elemeinek végén. Ezt a problémát lehet megoldani a chop eljárás segítségével, melynek egyetlen paramétere van: a feldolgozandó tömb. Az utasítás helyben dolgozik, azaz az eredménye a megadott tömbön jelentkezik - levágja a sorok utolsó karakterét. Ennek az eljárásnak a "biztonságosabb" változata a chomp, ez csak akkor vágja le az utolsó elemet, amennyiben az újsor karakter, illetve Windows alatt ha újsor karakterpáros. Ezt a nyelv fejlődése során vezették be a felhasználók tapasztalataira épülő egyik javaslat hatására - hiszen többek között például akkor nem működik a chop, ha a file utolsó sorának végén már nincsen újsor karakter. A következő példák ezeknek a függvényeknek a használatát mutatják be (megjegyzendő, hogy tömb helyett egy skalár is átadható ezeknek a rutinoknak, ilyenkor az adott skaláron végzik tevékenységüket):

chop(@sorok);  # a "sorok" elemeinek utolsó karakterét levágja
chomp(@sorok); # ez pedig a "biztonságosabb" változat
Igen, ezek a függvények jól használhatóak, de mit tegyünk, ha mi pont a fordítottját szeretnénk tenni a dolognak, például hozzáadni egy újsort vagy pedig egy teljesen más feladatunk akad? Erre van a map parancs. Két paramétert kell neki megadni, az első egy parancs - amit tenni szeretnénk az egyes elemekkel -, a parancs eredménye lesz az adott elem új tartalma, a második a tömb. Az aktuális elem $_ változóba kerül bele. Íme pár példa:

@ujtomb=map('"'.$_. '"',@egytomb);
@ujtomb=map($_**2,@egytomb);
@tomb=map(substr($_,-1) eq "\n"?substr($_,0,-1):$_,@tomb);
Mint látható ez a parancs már nem a tömbön magán dolgozik, hanem az eredménye egy új tömbbe kerül bele. Az első példa idézőjelbe teszi minden elemét a tömbnek, a második pedig a tömb minden elemét négyzetre emeli. Az utolsó példa gyakorlatilag a chomp megvalósítása, persze lehetett volna hatékonyabban is írni, de törekedtem az érthetőségre, hiszen a feltételes utasításokat még nem mutattam be. Röviden annyit csinál, hogyha az utolsó karaktere az adott elemnek újsor, akkor a kérdőjel előtti részt adja vissza, azaz az utolsó karakter levágásra kerül, különben pedig a kérdőjel utánit, azaz az eredeti értéket.

Ebben a blokkban még a grep utasítás maradt hátra. Ezzel egy kiválogatást végezhetünk, azaz egy általunk megadott feltétel segítségével megadhatjuk, hogy a visszaadott tömbben szerepeljen-e az adott elem, vagy nem.

@haromjegyu=grep($_>99 and $_<1000, @szamok);
@nemmegjegyzes=grep(substr($_,0,1) ne '#', @sorok);
Az első példa a 'szamok' tömbből visszaadja a háromjegyű számokat (feltéve, hogy egész számokról beszélünk). A második segítségével, amennyiben a 'sorok' tömbbe egy állomány sorai vannak betöltve, akkor kiszűri belőle azokat, melyek nem '#' karakterrel kezdődnek.

Kulcs meglétének vizsgálata hash-ben (exists)

A végére maradtak a hash-ekkel végezhető műveletek. Hash-eket használva, szükségünk lehet arra, hogy megvizsgáljuk, definiálva van-e egy adott kulcs a hash-ben vagy nem? Erre szolgál az exists operátor, ami igaz értéket ad vissza, ha létezik, s hamisat, ha nem:

exists( $hash{'kulcs'} ); ='', ha nincs, =1, ha van

Hashek kulcsai, értékei (keys, values)

Tipikus feladat lehet, hogy egy hash minden elemére, vagy minden értékével szeretnénk tenni valamit, erre szolgálnak a keys és values operátorok. A keys a hash kulcsait adja vissza egy tömbben, a values pedig az egyes értékeket. A keys-re egy példa (a values ugyanígy működik):

print map( $_."=".$hash{$_}."\n", keys %hash );
A példa kiírja a hash elemeit kulcs=érték formában, külön sorokban.

Kulcs-érték párok (each)

Amennyiben szeretnénk egy hash elemein végigfutni és utána használni a továbbiakban valamire, akkor az each parancsot használhatjuk. Ez egy kételemű listát ad vissza, melyben a soron következő kulcs és érték értékek vannak benne. Ha a hash elemeinek végére értünk, akkor mindkét elem undefined lesz, majd kezdődik előröl.

($kulcs1,$ertek1)=each(%hash);
($kulcs2,$ertek2)=each(%hash);
($kulcs3,$ertek3)=each(%hash);

Elágazások

Egy programban legyen az bármilyen egyszerű is, nagyon valószínű, hogy lehetővé kell tennünk, hogy bizonyos feltételek teljesülése és nem teljesülése esetén más és más történjen. Erre szolgálnak a feltételes utasítások. A feltételes utasítások legalább egy feltételből, s legalább egy utasításblokkból állnak. A feltétel egy kifejezés lehet, melynek értéke vagy igaz, vagy hamis, az utasításblokk pedig egy olyan szerkezet mely (általában) több utasítást fog egybe. Az utasításblokkokat kapcsos-zárójelek közé írjuk, s a kapcsos-zárójelek akkor sem hagyhatóak el, ha egyetlen utasítást fognak közre (ellenben a Pascal begin-end-jével, vagy a C kapcsos-zárójeleivel).

Perl-ben egyetlen lehetőség áll rendelkezésünkre elágazások használatára, ez az if-es szerkezet, azonban ezt olyan sokféleképpen és rugalmasan használhatjuk, hogy nincs is más lehetőségre szükségünk. A legáltalánosabb formája a következő:

if (feltétel1)
  {
  # ha feltétel1 igaz
  utasítások;
  }
elsif (feltétel2)
  {
  # ha feltétel1 nem igaz és feltétel2 igaz
  utasítások;
  }
else
  {
  # ha egyik feltétel sem igaz
  utasítások;
  }
Mind az elsif és az utána következő blokk, mind az else és az utána következő blokk tetszés szerint elhagyható. Elsif blokkból többet is használhatunk. A fenti program működése tehát a következő: ha feltétel1 igaz, akkor a hozzátartozó blokk végrehajtódik, különben ha a feltétel2 igaz, akkor a második blokk végrehajtódik, különben pedig az else-hez tartozó blokk hajtódik végre. Lássunk a használatára példákat:

if ($szam<10) { print "0".$szam } else { print $szam }
if ($parancs eq 'elore')
  {
  print "$ertek1 $ertek2 $ertek3\n";
  }
elsif ($parancs eq 'hatra')
  {
  print "$ertek3 $ertek2 $ertek1\n";
  }
elsif ($parancs eq 'elso')
  {
  print "$ertek1\n";
  }
elsif ($parancs eq 'utolso')
  {
  print "$ertek3\n";
  }
elsif ($parancs eq 'kozepso')
  {
  print "$ertek2\n";
  }
else
  {
  print "Ismeretlen parancs!\n";
  }
A fenti példák remélhetőleg egyértelműek. A feltételeknél a műveleteknél megismert összehasonlításokat alkalmaztam. Az if szó helyett további lehetőség az unless szó használata, amely gyakorlatilag az ellenkezőjét fogja tenni az if-nek, akkor hajtódik végre az if-hez tartozó utasításblokk, ha a kifejezés nem igaz. Elsunless utasítás nem létezik. A használata ezenkívül teljesen megegyezik az if-ével:

unless (feltétel1)
  {
  # ha feltétel1 nem igaz
  utasítások;
  }
elsif (feltétel2)
  {
  # ha feltétel1 igaz és feltétel2 igaz
  utasítások;
  }
else
  {
  # ha feltétel1 igaz volt, a többi feltétel nem
  utasítások;
  }
Amennyiben az utasításblokkunk csak egy utasításból áll, rendelkezésünkre áll egy további lehetőség is:

utasítás if (feltétel);
utasítás unless (feltétel);
Ekkor az utasítás csak akkor fog végrehajtódni, ha a feltétel igaz (if), illetve hamis (unless). Erre a lehetőségre pár példa:

print "Túl nagy szám" if ($szam>9999);
print "Hiba!!!" unless ($hiba==0);
Az első üzenet akkor jelenik meg, hogyha a szam változó értéke nagyobb, mint 9999, a második pedig akkor, ha a hiba nevű változó értéke nem 0. Természetesen ezeknek az utasításoknak az "egymásba ágyazása", azaz feltételes utasítás használata egy feltételes utasítás utasításblokkjában minden további nélkül lehetséges.

Ciklusszervezési lehetőségek

Gyakran lehet szükségünk arra is, hogy a programunk egy részét többször végrehajtsuk, például amíg egy feltétel igaz, vagy amíg egy feltétel hamis. Erre a Perl nyelv számos lehetőséget biztosít. Képzeljük el, hogy mi tennénk, ha ki kellene írnunk az összes négyjegyű egész számot! Biztosan nem leírnánk mindet A példákban ennek megoldási lehetőségeit fogom bemutatni.

Az egyik legegyszerűbb ciklusszervezési lehetőség a while ciklus. A szintaktikája a következő:

while (feltétel)
  {
  utasítások;
  }
Az utasításblokk amíg a feltétel igaz, addig ismétlődik (másik oldalól megközelítve, amíg hamissá nem válik). A példa (ami a négyjegyű egészeket írja ki):

$szam=1000;
while ($szam<10000)
  {
  print $szam++."\n";
  }
A while-hoz hasonló ciklusszervezési lehetőségünk az until, ami ugyanúgy működik, mint a while, kivéve, hogy a "ciklusmagot", azaz az utasításblokkban szereplő utasításokat addig ismétli, amíg feltétel igazzá nem válik, azaz ameddig hamis. Ezzel megoldva a példánk a következőképpen alakul:

$szam=1000;
until ($szam>9999)
  {
  print $szam++."\n";
  }
Az if és az unless szerkezetekhez hasonlóan mind a while, mind az until szerkezet lehetőséget ad arra, hogy a feltételt egy utasítás után írjuk:

$szam=1000; print $szam++."\n" while ($szam<10000);
$szam=1000; print $szam++."\n" until ($szam>9999);
A do függvény segítségével utasításblokkot is használhatunk az egyetlen utasítás helyett. Így egyetlen különbség lesz a "sima" while és until ciklusokhoz képest: a ciklusmag akkor is végre fog hajtódni egyszer, ha a feltétel nem igaz (while), illetve hamis (until) (ez az előző példára is igaz!). Ilyenkor a ciklusmag elé a do szót kell írnunk. A példa do-while-al:

$szam=1000;
do {
   print $szam++."\n";
   } while ($szam<10000);
Illetve do-until-lal:

$szam=1000;
do {
   print $szam++."\n";
   } until ($szam>9999);
A következő ciklusszervezési lehetőségünk a for ciklus. Nagyon rugalmas szerkezet, rendkívül széleskörűen felhasználható. A következőképpen lehet használni:

for (utasítás1; feltétel; utasítás2)
  {
  ciklusmag;
  }
Az utasítás1 utasítás az egész ciklus elején hajtódik végre, majd végrehajtódnak az úgynevezett ciklusmag helyén szereplő utasítások, s utána az utasítás2 utasítás, majd megint a ciklusmag és megint az utasítás2, és így tovább, amíg a feltétel igaz, és hamissá nem lesz. Ez így elsőre biztosan bonyolultan hangzik, lássuk, hogy hogyan oldható meg a feladatunk ezzel a ciklusszervezési módszerrel:

for ($szam=1000; $szam<10000; $szam++)
  {

  print $szam."\n";
  }
Első alkalommal értéket adunk a szam változónak, majd amíg nem érjük el a 10000-et, addig kiírjuk a szam változó értékét és növeljük eggyel. A zárójelben lévő részek tetszés szerint elhagyhatóak, vagy akár (a feltétel kivételével) több is írható belőlük, vesszővel elválasztva:

for (;;) {print "Végtelen ciklus!";}
for ($x=0, $y=1; $x<10; $x++, $y+=2) { print $x.' '.$y."\n";}
A ciklusok használatakor felmerülhet az az igény, hogy a ciklusmag közepén valamely feltétel esetén ki szeretnénk lépni a ciklusmagból. Erre háromfajta lehetőségünk van, a next, a last és a redo utasítás. A next segítségével az utasításblokk végére ugorhatunk, a redo segítségével az elejére (így ugyanazokkal az értékekkel mégegyszer végrehajtódik a ciklusmag), a last pedig befejezi a ciklus végrehajtását és kilép belőle. A következő példák mutatják be ezeknek az utasításoknak a használatát:

while ($line=EgyUjSor)
  {
  if ($line eq 'kilép') { last }
  if ($line eq 'üdv') { print "Hello!\n"; next }
  if ($line eq 'hello') { $line='üdv'; redo }
  print "Nem értem.\n";
  }
A ciklus addig ismétlődik, míg tud új sort beolvasni. Ha beolvasott egy sort, akkor a tartalma szerint a következők történhetnek: 'kilép' esetén kilép a ciklusból, 'üdv' és 'hello' esetén visszaír egy 'Hello!'-t, a többi esetben pedig azt írja ki, hogy 'Nem értem.'. A 'Hello!' kiírása után nem íródik ki a 'Nem értem.', mivel a next utasítást használtuk, az 'üdv' pedig megváltoztatja a line változó értékét és a redo-val újra feldolgoztatja azt, ezért kapjuk a 'Hello!' üzenetet.

Ennek a résznek a végére a rendkívül jól használható foreach utasítás maradt. Ez veszi egy tömb összes elemét és mindegyikhez végrehajtja a hozzá tartozó utasításblokkot. Egy példa a használatára:

@tomb=('első','második','satöbbi','utolsó');
$i=1;
foreach (@tomb)
  {
  print "A $i. elem: $_\n"; $i++;
  }
Mint látható, az aktuális elem az '_' változóban áll rendelkezésünkre. Ezt már használtuk korábban (grep-nél, map-nél) és rejtett változónak hívjuk. A fenti ciklusszervezési módszerekkel rendkívül változatos és elegánsan használható ciklusszervezési eszközökhöz juttat minket a Perl, melyek kevés nyelvben használhatóak ilyen kényelmesen és változatosan.

Szubrutinok (Eljárások és függvények)

Mint minden más fejlett nyelv, a Perl is biztosít lehetőséget a gyakran használt programrészek állandó a kódban ismételgetése helyett metódusok használatára, azonban nem tesz különbséget az eljárások, illetve függvények között (eljárás, amit meghívunk, de nem tér vissza értékkel - ilyen a print -, függvény, amit meghívunk, és visszatér egy értékkel - ilyen a szinusz-t megvalósító sin). A deklarációja mindkét metódustípusnak megegyezik, s az, hogy egy metódust mire használunk, csakis tőlünk függ. Egy példán keresztül megpróbálom bemutatni, hogy mit nyerünk akkor, ha metódusokat használunk:

$szam=1;
print "A szám értéke: $szam. Eggyel kisebb: ".($szam-1)."\n";
$szam=5;
print "A szám értéke: $szam. Eggyel kisebb: ".($szam-1)."\n";
$szam=8;
print "A szám értéke: $szam. Eggyel kisebb: ".($szam-1)."\n";
$szam=9;
print "A szám értéke: $szam. Eggyel kisebb: ".($szam-1)."\n";
Ezt a következőkre tudjuk leegyszerűsíteni:

$szam=1; kiir();
$szam=5; kiir();
$szam=8; kiir();
$szam=9; kiir();

sub kiir
  {
  print "A szám értéke: $szam. Eggyel kisebb: ".($szam-1)."\n";
  }
Ez azon az előnyön kívül, hogy nem kell 4-szer beírni a kiíró sort, azzal az előnnyel is jár, hogy ha a későbbiekben módosítani kell a programot, hogy az eggyel nagyobb számokat írja ki, akkor nem négy helyen kell azt megtennünk. Amennyiben elképzeljük, hogy 20-30 soros az ismétlendő rész (ennél sokkal-sokkal hosszabb megoldások is vannak), még érthetőbb lesz a probléma.

De nézzük végig, hogy mit is csinál a fenti program, mit is jelentenek az egyes sorai. A kiir() utasítások hatására a program átugrik a 'sub' részhez, végrehajtja az ott leírtakat, majd annak végén visszaugrik oda, ahol volt. A 'sub' szó azt jelenti, hogy egy eljárást (szubrutint) szeretnénk most leírni, majd kapcsos zárójelek között a tartalma jön.

Mint látható volt, az eljárásunk a 'szam' értékét használja. Felmerülhet bennünk a kérdés, hogy nem lehetne-e még egyszerűbben megoldani a problémánkat? Természetesen lehet, paraméterátadással. Ilyen paraméterátadást használunk akkor is, mikor leírjuk, hogy sin(30). Ekkor felsoroljuk a metódus neve után, hogy milyen változókat, konstansokat szeretnénk átadni a számára, majd ezeket az eljáráson belül használni fogjuk tudni. Nézzük, ezt hogyan lehet megoldani:

kiir(1);
kiir(5);
kiir(8);
kiir(9);

sub kiir
  {
  ($szam)=@_;
  print "A szám értéke: $szam. Eggyel kisebb: ".($szam-1)."\n";
  }
A változás annyi, hogy az eljárásunk kibővült egy sorral, ahol a 'szam' változónak értéket adunk. Az eljárás számára átadott változók az '_' nevű tömbbe (@_) kerülnek bele (már megint egy rejtett változó), s abból vesszük ki őket ebben a sorban.

Bővítsük tovább tudásunkat a függvényekkel, azaz az olyan metódusokkal, ahol egy értéket kapunk vissza. Íme egy példa ennek megvalósítására:

print kob(4);
print kob(3);

sub kob
  {
  return $_[0]**3;
  }
A példában a köbre emelő függvényt valósítottuk meg. A függvény visszatérési értékét a 'return' utasítás segítségével határozhatjuk meg, az eljárás egyszerűen veszi az első paramétert (az '_' tömb első eleme: $_[0]), és visszaadja annak a köbét. A függvény visszatérési értéke ha nem adunk meg 'return'-nel semmit, akkor az utolsó kifejezés értékét veszi fel, azaz írhattuk volna a következőket is a függvényünkben:

sub kob
  {
  $_[0]**3;
  }
Hogyan oldhatjuk meg, ha több paramétert szeretnénk visszaadni? Kétféle módszerrel. Az egyik, amely talán magától értetődik: a visszatérési értékünk legyen egy tömb, melynek elemei az általunk visszaadandó értékek:

@tomb=paratlanszamok(5,9);

sub paratlanszamok
  {
  ($szam1,$szam2)=@_;
  for($szam1++ if ($szam1%2==0); $szam1<$szam2; $szam1+=2)
    {
    push(@ret,$szam1);
    }
  return @ret;
  }
A függvény a páratlanszámokat adja vissza a két paraméter között (paraméterként egész számokat kell megadni, hogy helyesen működjön a függvény). A ret tömbbe kerülnek bele a számok, s ahogy az előbb is, itt is a return paranccsal térünk vissza az függvény végén.

A másik megoldás a cím szerint átadott paraméterek használatával történik. Perlben is kétféleképpen használhatjuk a paramétereket, az egyik, mikor létrehozunk egy új változót és azt használjuk, módosítjuk (ahogy a köbös példa kivételével eddig tettük). Ez az érték szerinti átadás megfelelője. A másik módszer, mikor az '_' tömb elemeire közvetlenül hivatkozunk, ha megváltoztatjuk ezek értékét, akkor a "külső" változó értéke is módosul (tehát Perlben valójában csak cím szerinti átadás történik). Lássunk erre is egy példát:

@tomb=(4,7,2,8,2,4,7,9);
rendez(@tomb);
print "A tömb elemei: ".join(',',@tomb)."\n";

sub rendez
  {
  @_= sort({lc($a) cmp lc($b)}, @_);
  }

Apache log vizsgálata

A mai anyag elég tartalmasra sikerült, de még mindig nincs vége. Egy rövid programot következik, mely egy Apache naplófájl egyszerű elemzésére képes (de könnyen kiegészíthető kívánalmaink szerint).

#!/usr/bin/perl

open(LOG, "access.log");

while(<LOG>) { ++$linenum;

  undef @elements;
  while (m{
        ( [^\[\" ][^\" ]* )
        |
        \"
         (
          (?: [^\"] | "" )*
         )
        \"
        |
        (\[
          [^\]]+
        \])
        }gx) {
    if (defined $1) { $elem=$1; }
    elsif (defined $2) { $elem=$2; $elem=~s/""/\"/g; }
    else { $elem=$3; }
    push @elements,$elem;
  }
  @values=('ip','rname','ruser','datetime','query','status','size','referrer','agent');
  for($i=0; $i<@values; $i++) {
    $l{$values[$i]}=$elements[$i];
  }

  ($refurl)= $l{referrer}=~/^https?:\/\/([^\/]+)\/?.*$/;
  $referrers{$refurl}++;
  $date = substr($l{datetime},1,11);

  $count++;
  $ips{$l{ip}}++;

  $lastdate = $date if $lastdate eq '';

  if ($lastdate ne $date) {
    printinfo();
  }

}

printinfo();

sub printinfo {
  print "$ldate: $count\n";
  print "  külön IP-k: ".(keys(%ips)+0)."\n";
  foreach (sort {$referrers{$b}<=>$referrers{$a}} keys %referrers) {
    print "$_ ($referrers{$_}, $referrers_ip{$_})  ";
  }
  print "\n";
  $count=0;
  $lastdate=$thistime;
  undef %ips; undef %referrers;
  print "--[ $linenum ]-------------------------------------\n";
}
Ahogy múltkor sem, most sem mutatom be részletekbe menően a programot, ezt az olvasóra bízom: próbálgassa, egészítse ki, írja át - ezzel lehet tanulni a legtöbbet. Röviden annyit, hogy a program elején megnyitjuk a naplófájlt, majd egy ciklusban végigmegyünk a sorain. Az a ronda dolog ott az elején egy reguláris (avagy mintaillesztő) kifejezés, erről remélhetőleg majd egyszer később még lesz szó a Weblabor hasábjain. Ezután egy hash-be pakoljuk a kifejezés által talált elemeket a sorból, majd feldolgozzuk. A program végül napokra bontott statisztikát fog nyújtani a találatokról, a különböző IP címekről, és a hivatkozó oldalakról (és a hivatkozások számáról).

Nos, már ennyi jutott a Perlből, legközelebb folytatjuk az ismerkedést.
 
Bártházi András arcképe
Bártházi András
Az Emarsys-nál dolgozik vezető fejlesztőként, és az API-ért, integrációkért felelős termékmenedzserként. Szeret profi csapatban profit alkotni.
1

javíás

Anonymous · 2004. Dec. 2. (Cs), 12.13
A return $[0]**3;
helyesen $_[0]**3;
igy van asszem
2

Jól tudod

Bártházi András · 2004. Dec. 2. (Cs), 12.36
Javítottam is, köszi a hibajelzést.

-boogie-
3

GONDD...

Anonymous · 2005. Dec. 5. (H), 18.03
print "A szám értéke: $szam. Eggyel kisebb: ".$szam-1."

Ezzel itt gondd van Bandikám!
4

Igaz

Bártházi András · 2005. Dec. 5. (H), 18.13
Helyesen:
print "...".($szam-1)."...";
Bár remélem, hogy ettől még érthető volt a mondanivaló. Javítottam a cikket, köszönöm a hibajelzést.

-boogie-
5

Hiba

Anonymous · 2005. Dec. 6. (K), 00.56
Apuci sztem itt is olrontottal valamit!

Hiba:

sub kob
  {
  $[0]**3;
  }
helyett:(sztem)

sub kob
  {
  $_[0]**3;
  }
6

<Nincs cím>

Anonymous · 2005. Dec. 6. (K), 16.07
@tomb=paratlanszamok(5,9);

sub paratlanszamok
  {
  ($szam1,$szam2)=@_;
  for($szam1++ if ($szam1%2==0); $szam1<$szam2; $szam1+=2)
    {
    push(@ret,$szam1);
    }
  return @ret;
  }
Ezt a részt nem nagyon ertem !a for es a push szintaktika mit jelent? es mert csináljuk ezeket a muveeteket a $szam1 el es $szam2 vel.megezeka muveletek mit jelentenek?
7

for és push

Bártházi András · 2005. Dec. 6. (K), 21.01
A függvény ugye nem csinál mást, mint az adott intervallumban levő páratlanszámokat visszaadja. A feladat tehát kettesével végigmenni a kezdőértéktől kezdődően ($szam1), addig, amíg el nem jutunk a végértékig ($szam2).

A for ciklus három részből áll. Az első részben inicializáció, a második részben feltételvizsgálat, a harmadik részben pedig egy művelet van. Az inicializáció a for ciklus elején fut le, a ciklus addig fut, amíg a feltétel igaz, s minden ciklus végén lefut a művelet.

A $szam1++ if ($szam1%2)==0, vagyis az inicializáció nem csinál mást, mint növeli eggyel a kezdőértéket, ha az páros (kettővel történő maradékos osztás végeredménye 0). Ez azt biztosítja, hogy az első szám, amit el fogunk tárolni, az páratlan legyen.

A push két paramétert vár, egy tömböt és egy értéket, a tömb végére beszúrja a kapott értéket. Végül ennek a tömbnek az értékével tér vissza a függvényünk.

-boogie-