Ma a kissé szerencsétlen nevű awk parancsról szeretnék írni (és arról is fogok), azaz folytatnám az "Elfelejtett világ" sorozat első részét. Hogy miért szerencsétlen a név? Az awkward ugyebár ügyetlent, szerencsétlent jelent angolul. Ráadásul az awk-t vezérló kód nagyban hasonlít egy véletlenszerű karaktersorozatra, aki rátekint annak biztosan citrommá szűkül a feje. Mégis, az awk-t minimális tudással is lehet kezelni és egész sok apró-cseprő napi teendőben képes másodpercek alatt segíteni.
Kis keretes bekezdésemben néhány alapvető dolog: Az awk a neki megadott fájlokon soronként halad végig, és az általunk most használt kis egysoros parancsokat minden egyes sorra érvényesíti. Minden egyes parancsblokkot kapcsos zárójelek közé kell tenni (ez C programozóknak nyilván ismerős), és ha csak egy print parancsot adunk ki, az is egy blokk, tehát { print } -et kell írnunk.
Az első okosság, hogy az awk feldarabolja, pontosabban fel tudja darabolni az adott sort szakaszokra. Egy olyan szövegben tehát, amire valamilyen szabály érvényes (pl. Excel/CSV fájl tartalma, ahol az oszlopszám ugyanaz minden sorban), nagyszerűen tudunk manipulálni. A feldarabolt szakaszokat a $1, $2, $3 ... stb változókba pakolja, a $0 pedig a teljes sort jelenti. Azt, hogy mi mentén darabolja a sort, mi adjuk meg neki; tehát például a /etc/passwd fájlban kettőspontokkal vannak elválasztva a felhasználói adatok. (Ha valaki nem tudja, hogy néz ez ki, egy cat /etc/passwd paranccsal mindenekelőtt nézze meg!) Ha szeretnénk listázni a felhasználók login id-jét és hozzá a nevét, akkor tudjuk ezt tenni:
$ awk -F ":" '{print $1 $5}' /etc/passwd
rootroot
daemondaemon
...
(A sor elején lévő dollárjelet nehogy begépelje valaki!!! Azzal csak egy új sort jelzek a terminálban. Ha esetleg valakinek ez új lenne.) A -F paramétere természetesen az elválasztójelet jelenti, alapban a szóközt veszi annak. Fel lehet egymás mögé sorolni többet is. Nade ez csúnya, egybeírja, meg minden...
$ awk -F ":" '{print $1 "," $5}' /etc/passwd
root,root
daemon,daemon
...
bagoj,Bagoj úr
Mindjárt jobb. Még szebb lenne, ha tudnánk feltételeket írni és ahol megegyezik a két mező (tehát pl. mindkettő "root"), azt a sort nem írjuk ki.
$ awk -F ":" '( $1 != $5 ) {print $1 "," $5}' /etc/passwd
list,Mailing List Manager
irc,ircd
...
bagoj,Bagoj úr
Hoppá, ez ilyen egyszerű? Mi lenne, ha csak azokat a sorokat írnánk ki, amelyek nem technikai felhasználók, hanem valódi userek? Megtehetjük, hiszen az Ubuntu a technikai felhasználók uid-ját, azaz számszerű egyedi azonosítóját 0-999-ig osztja ki, az emberi felhasználóké pedig 1000 vagy afeletti. A uid a harmadik oszlop a /etc/passwd fájlban. Akkor ezt most próbáljuk is ki:
$ awk -F ":" '( $3 >= 1000 ) {print $1 "," $5}' /etc/passwd
nobody,nobody
bagoj,Bagoj úr
Nocsak, máris megdőlt az állításom: hiszen a nobody user azonosítója 65534! De az biztos, hogy remekül működik a parancs. :) A játék lényege, hogy ha mondjuk abbahagyjuk szerencsétlen passwd fájl abajgatását és mondjuk egy CSV-t kezdünk bántani, máris látszik hogy alapvető szűréseket milyen egyszerű elvégezni. Elkészítettem a következő fájlt, proba.csv néven:
Fizetés,2009.01.12,+,71500
Benzin,2009.01.12,-,7255
Kaja a boltbol,2009.01.15,-,8290
Ebedpenz,2009.01.16,-,3500
Béla megadta a kölcsönt,2009.01.20,+,20000
Kocsma,2009.01.20,-,12500
Asszem elég egyértelmű: A kiadások mínusszal, a bevételek plusszal számítanak.
$ awk -F "," '( $3 == "+" ) {print $1}' proba.txt
Fizetés
Béla megadta a kölcsönt
$ awk -F "," '( $3 == "+" ) && ( $4 > 50000 ) {print $1}' proba.txt
Fizetés
$ awk -F "," '( $4 >= 5000 ) {print $3 $4 "\t" $1}' proba.txt
+71500 Fizetés
-7255 Benzin
-8290 Kaja a boltbol
+20000 Béla megadta a kölcsönt
-12500 Kocsma
Azt hiszem, eddig érthető a játék. Ezek mellett az awk képes regexp, azaz reguláris kifejezésre is illeszteni; magyarul csak azokkal a sorokkal fog foglalkozni, amelyek illeszkednek. Ha Béla viselt dolgaira vagyunk kiváncsiak:
$ awk -F "," '/Béla/ {print $3 $4 "\t" $1}' proba.txt
+20000 Béla megadta a kölcsönt
Az awk remekül tud számolni is:
$ awk -F "," '{print $3 ($4+300) "\t" $1}' proba.txt
+71800 Fizetés
-7555 Benzin
-8590 Kaja a boltbol
-3800 Ebédpénz
+20300 Béla megadta a kölcsönt
-12800 Kocsma
Hozzáadott minden összeghez 300 Ft-ot. Persze ha négyzetre akarjuk emelni, az is megy, akkor $4^2-t kellene írnunk. Mielőtt teljesen bedurvulnánk, elmagyarázom, hogy a fő kódblokk (ami most a kapcsos zárójelek között van) mellett az awk tud kezelni egy úgynevezett BEGIN és egy END blokkot. Ezek arra szolgálnak, hogy a fő blokk előtt és után inicializálásokat, illetve végső kalkulációkat tudjunk végrehajtani. Ennek demonstrálására készítünk egy görgetett összeget, azaz kimutatjuk, hogy hány forint volt a zsebünkben az adott kiadás vagy bevétel után. Ehhez már kell egy inicializálás blokk, ahol nullára állítjuk az összegző változónk értékét. Mivel így már nem tudom szépen és átláthatóan megírni egy sorba, ezért elárulom, hogy az awk parancsok szöveges fájlba is írhatók, és -f paraméterrel végrehajtathatók az awk-val, pl. így: awk -f szumma.awk proba.txt. A következő kódblokkot a szumma.awk fájlba írom:
BEGIN {
szumma=0
FS = ","
}
{
if ($3 == "+") { szumma=szumma+$4 }
else { szumma=szumma-$4 }
print $3 $4 "\t" szumma "\t" $1
}
Aki picit is tanult programozni, annak egyértelmű: Az elején a szumma nevű változót nullázzuk, és mivel itt is lehet, beállítottam a mezőhatárolót vesszőre (hiszen vesszővel elválasztott értékeink vannak). Így szükségtelen lesz a -F paraméter futtatáskor. Ezután, ha az előjel negatív, akkor kivonunk, ha pozitív, akkor hozzáadunk, majd kiírjuk a sort.
Aki házi feladatként egy END nevű blokkot is szeretne a végére rakni, az összesítse az összes kiadást és bevételt, ez alapján mennie kell a dolognak. :-) A futtatás így néz ki:$ awk -f szumma.awk proba.txt
+71500 71500 Fizetés
-7255 64245 Benzin
-8290 55955 Kaja a boltbol
-3500 52455 Ebédpénz
+20000 72455 Béla megadta a kölcsönt
-12500 59955 Kocsma
Ennél jobban nem szeretnék durvulni, pedig az awk ennél sokkal keményebb. :-) Mindössze annyit még, hogy igen hasznos változó a NF, amiben a mezők száma található; illetve a NR, amiben a rekordok száma van - mivel alapértelmezetten egy rekord egy sor a fájlban, ez tartalmazza, hogy hány sorból áll a fájl. Erre a két paraméterre építve szeretnék két példát még elsütni, mert mindkettő iszonyat hasznos tud lenni. De előtte egy kicsit lazítsunk:
1. Feladat
Van egy nagy rakás mp3-unk egy könyvtárban, mindegyik fájl elnevezése következetes, mégpedig "Sorszám.Szám címe-Előadó.mp3", azaz egy példa: "003.This is the sound-Feel Good Production.mp3". Mi szeretnénk, ha az előadó és a szám címe helyet cserélne. Valószínűleg erre még ne mírtak grafikus programot, de nem esünk kétségbe, csak használjuk eddigi tudásunkat:
- A fájlokat egyesével átadjuk az awk-nak
- Az awk megcseréli a kettes és hármas oszlopot, mező szeparátor a pont és a kötőjel
- Ha a szám címében vagy az előadó nevében van pont vagy kötőjel, azzal most nem foglalkozunk, de gondolunk rá - a mezőszám alapján tudjuk ezt megállapítani. Ha
háromnál négynél nagyobb, nem próbáljuk átnevezni a fájlt, mert jelenlegi képességeinket meghaladja. - Ne felejtsük el, hogy a végén a .mp3 is egy új mezőnek számít, hiszen a pont szeparátor!
Még egy kis magyarázat a -F paraméter után következőkhöz: A pont (.) mindenre illeszkedik a reguláris kifejezésekben. Ha ténylegesen a pontra akarunk illeszteni, akkor "\."-t kell írni. Az utána jövő pipe az itt a vagylagosságot jelenti, azaz a "\.|-" jelentése: egy darab pont vagy egy darab kötőjel. Itt a kis egysoros, amely csak kiírja a megfordított neveket:
for i in *; do echo $i | awk -F "\.|-" '(NF == 4) {print $1 "." $3 "-" $2 "." $4}'; done
Magától értetődő, hogy a szeparátort (azaz a pontot és kötőjelet) nekünk kell a print esetén pótolni, hiszen az elválasztójel nem része egyik mezőnek sem. Lássuk ugyanezt, átnevezéssel:
for i in *; do UJFILENEV=$(echo $i | awk -F "\.|-" '(NF == 4) {print $1 "." $3 "-" $2 "." $4}'); mv "$i" "$UJFILENEV"; done
Ezt az UJFILENEV változót azért kellett bevezetnem, hogy idézőjelek közé tudjam írni, hiszen ha szóközök is vannak a fájlnévben, azt az mv parancs több fájlként próbálná meg értelmezni. Hát, remélem, érthető. Az UJFILENEV-be betöltjük, amit kiír az awk, és ezután az eredeti fájlnév (ami a $i-ben van) és az új név segítségével megcsináljuk az átnevezést az mv paranccsal. Feltételeztem, hogy az mp3-akon kívül semmi nincs az aktuális könyvtárban - ellenkező esetben a "for i in *" helyére mehet mondjuk "for i in *.mp3"
2. Feladat
Van egy CSV fájlunk, előre nem tudjuk, hogy hány oszlopot (mezőt) tartalmaz. Feladatunk, hogy megfordítsuk az oszlopok sorrendjét. Szinte adódik ez a megoldás (visszatértem megint a proba.txt fájlhoz):
awk -F "," '{for ( x = NF; x > 0; x-- ) { print $x"," } }' proba.txt
71500,
+,
2009.01.12,
Fizetés,
7255,
...
Látszik, hogy jó az igyekezet, de sajnos külön sorba írja a mezőket, azaz valami miatt mindegyikből külön rekordot csinál. Viszont van nekünk egy ORS (Output Record Separator) nevű változónk is, a BEGIN részben adjunk neki üres sztringet és kész! Persze, hogy egy-egy rekord végén eztán ténylegesen legyen is sorvége jel, erről nekünk magunknak kell gondoskodnunk, de ez nem probléma:
awk -F "," 'BEGIN { ORS="" } {for ( x = NF; x > 0; x-- ) { print $x"," } print "\n" }' proba.txt
71500,+,2009.01.12,Fizetés,
7255,-,2009.01.12,Benzin,
8290,-,2009.01.15,Kaja a boltbol,
3500,-,2009.01.16,Ebédpénz,
20000,+,2009.01.20,Béla megadta a kölcsönt,
12500,-,2009.01.20,Kocsma,
Bocs az egysorosért, ez szépen így nézne ki:
BEGIN {
ORS=""
}
{
for ( x = NF; x > 0; x-- ) {
print $x"," } print "\n"
}
Ez a misszió könnyedén teljesítve. :-)
3. Feladat
Van egy fájlunk, amelyben 3 sor alkot egyetlen adatot. Ezekből kellene egy CSV fájlt alakítani. Sőt, mondok egy jobb példát, van egy .VCF, azaz kontakt lista fájlunk, abból szeretnénk kiszedni a nevet és az email címet, és egy CSV fájlt csinálunk belőle. Ez ugyebár mindenféle programnak elég nehéz feladat, hiszen a fájlok struktúrája teljesen más: Az egyikben több sor írja le azt, amit nekünk egy sor több oszlopába kell rendeznünk.
Az a jó, hogy a bemeneti oldalon is meg tudjuk adni azt a bizonyos record separator-t. A VCF felépítése tehát ez:
BEGIN:VCARD
VERSION:3.0
EMAIL:elso@domain.hu
FN:Első név
....
END:VCARD
BEGIN:VCARD
VERSION:3.0
....
END:VCARD
(A kipontozott részek helyén egy rakás egyéb mező van. Aki nem hiszi, gyártson magának valami levelező programból.)
vcf.awk:
BEGIN {
FS=":"
ORS=""
}
/^EMAIL/ {sub(/\r$/,"");print $2 ", "}
/^FN/ {print $2 "\n"}
Bocs, nem szokásom váratlanul új függvényeket bevezetni, de az a sub() függvény, ami egyszerű cserét csinál, muszáj volt. Ugyanis az Evolution olyan .vcf-et ment, ami DOS-os formátumú, és ezen úrrá kellett lenni. Ha a .vcf fájl UNIXos, akkor nincs szükség a sub()-ra. A lényeg tehát, hogy azokkal a sorokkal foglalkozunk, amelyekben szerepel a sor elején az EMAIL vagy a FN sztring. Az elválasztójel a fájlon belül a kettőspont, ezt a fájl elején az FS paraméterben megadtuk. Az ORS biztosítja, hogy ne legyen akadály, hogy együvé tartozó adatok külön sorban vannak. Még annyit, hogy ha egy rekordból hiányzik az EMAIL vagy az FN mező, illetve ezek sorrendje megváltozik, akkor az egész csúnyán fejreáll - így éjfél felé nincs esésem egy fullos példával előrukkolni. De azt hiszem, a lényeg, a hihetetlen rugalmasság valamint a teljesen elcseszett szintaxis érzete átjött nektek is! :-))