HTML

Bagoj úr blogja

Kíváncsi Bagoj befigyel a Linux belsejébe, illetve különféle Linux terjesztéseket próbál ki. Ha jó napja van, scriptet ír Neked.

Friss topikok

Ronda, de hatékony - awk

2009.01.21. 23:32 bagoj ur

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:

  1. A fájlokat egyesével átadjuk az awk-nak
  2. Az awk megcseréli a kettes és hármas oszlopot, mező szeparátor a pont és a kötőjel
  3. 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.
  4. 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! :-))

8 komment

Címkék: linux alkalmazások awk parancssor

A bejegyzés trackback címe:

https://bagojur.blog.hu/api/trackback/id/tr46894041

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

csarlee 2009.01.26. 10:39:17

Bagoj Ur!

Emelem kalapom elotted! Ez egy nagyon jo munka, gratulalok!

Csarlee

bagoj ur 2009.02.02. 08:09:21

Köszönöm szépen! :-)

Psycho Dad 2009.02.14. 01:36:58

Először is köszi az írást!

Lenne egy olyan kérdésem, hogy adott egy avidemux-os job file+egy sorozat, ami mondjuk 20 részes, a fájlnevek következetesen valami01.avi, valami02.avi stb., a job fájlban az első rész átkonvertálásához szükséges adatok vannak, ugyanezekkel akarom megcsinálni a többi 19 részt is.
Hogyan oldható meg(esetleg épp awk-val), hogy a job fájlban átírja az avi(valami02.avi) és a kimeneti fájl nevét(akármi02.avi)nevét , növelve eggyel a számot, majd ezt elmenti egy új job fájlba, más néven, ezt is lehet egyszerűen számozással változtatni.
nano-val sem tart sokból átírogatni+menteni, de scripttel csak kényelmesebb lenne. :)

bagoj ur 2009.02.14. 11:41:11

@NoBe: Szia! Nem tűnik nehéznek a feladat, sokféleképp megoldható. Egyszerűen beolvasod egy változóba a job fájl tartalmát

#!/bin/bash
JOBFILE=$(cat jobfile.txt)

, majd mindig cserélgeted a fájlneveket - ezt nem tudom most leírni mert függ attól, mi van a fájlban. Ha elküldöd vagy kiteszed valahová a job fájlt, akkor szívesen foglalkozom vele, megér egy postot.

Utánanéztem ennek az avidemux-nek és azt írják, hogy önmagában is szkriptelhető. Ezen az oldalon a példa avi fájlokat dolgoz fel. Nem mélyedtem bele, de azt látom hogy a "complete script" végén a processFile függvényben lehet a feldolgozási paramétereket állítgatni.

avidemux.org/admWiki/index.php?title=Scripting_tutorial

Ha Te sem szeretnél ennyire beleugrani, akkor ha tudom pontosan hogy néz ki a job file, megoldjuk pillanatok alatt.

Psycho Dad 2009.02.14. 12:25:01

A scriptelési lehetőséget ismerem, csak ahhoz ritkán van rá szükségem, hogy ennyire belemenjek, így egyszerűbb volt manuálisan megoldani.

Valóban nem lenne nehéz feladat, csak az ilyen awk,sed programokkal soha nem voltam jóban, lua-ban még szerintem menne is, ott jobban tudom hogyan kell az ilyen típusú feladatokat megoldani, de bash-el nem túl sokat foglalkoztam.

[url=pastebin.com/m28e46160]Ide[/url] felraktam a job file-t, a hosszú sorokat sajna automatikusan tördelte, de azért talán így is átlátható.

Előre is köszönöm a segítséged!

Psycho Dad 2009.02.14. 12:28:01

Sorry a linkért, nem tudtam, hogy a blog.hu milyen formában eszi meg, de akkor így tuti nem. :)

bagoj ur 2009.02.17. 15:59:52

@NoBe: megvan a link, semmi gond, csak nem tudom, mikor tudok ránézni. Iszonyat nagy a hajtás most a melóhelyen. Ha mindenképp awk-val akarod csinálni, az nehéz mert az awk egyszerre egy fájllal tud csak foglalkozni. De azt megteheted, hogy a BEGIN blokkban az ORF-et ""-re veszed, az FS-t "/"-re, aztán a fő blokkban csinálsz egy regex match-et az app.load és app.save sorokra és ott cseréled ki az utolsó elem értékét sub() függvénnyel (az utolsó elem mindig a $NF-ben van). És még csinálsz egy harmadik regexpet ami az összes többi sorra illeszkedik és csak egy print van benne.

Namost ez szerintem elég bonyolult ahhoz képest, hogy csak egyetlen cserét tudsz végrehajtani. Inkább bash-t kellene használni, és végigmenni egy adott sorozaton és úgy generálni egy alap fájlból. Ezt megcsinálom szívesen, ha lesz legalább annyi időm hogy feladjam a csekkjeimet... :-)

Psycho Dad 2009.02.17. 18:13:23

Na ez nekem kínai volt. :)

Hagyd a fenébe, ha nagyon fontos lenne akkor valahogy összehoznám magamnak, de tényleg nem túl sűrűn használnám, csak kíváncsi voltam mennyire lenne bonyolult megcsinálni, ez viszont már kiderült az eddigiekből. :)

Felejtsd el, inkább írj új cikkeket mindenki örömére. ;)

Köszi a válaszokat!