Szövegfeldolgozás régen és most

Az AWK a 70-es évek egy remek találmánya, de vajon megállja a helyét a mai világban is?

1979 nyara. Egy levegőtlen irodában ülsz a terminál mögött. Nézed a kurzor lassú villogását, miközben izzadtságcseppek gördülnek végig a hátadon. A szomszéd szobából áthallatszik a nagyszámítógép tompa zúgása.
Mögötted két nyomozó figyel feszülten. Életek múlhatnak azon, hogy valami nyomot találj a számítógép log fájljaiban. De hát több megabájtnyi adatról van szó! A közeli polcon megakad a szemed a "The C Programming Language" című könyv egy példányán. Rövid mérlegelés után elveted a lehetőséget. Nincs idő napokig pointerekkel fogócskázni.
Gondolataid lassan vánszorognak. Nem a legjobb napot választottad a kávéadagod csökkentésére. Mintha hallottál volna mostanában egy új nyelvről, amit pont szövegfájlok feldolgozására találtak ki. Valami három betűs... hmm... talán AWK? Már írod is a man awk parancsot, a megjelenő dokumentum első pár sorának átfutása után halvány mosoly jelenik meg az arcodon.
- Nos? - kérdezi az egyik nyomozó türelmetlenül.
- Megoldjuk. - mondod magabiztosan.

DEC VT100 terminál, az 1978-as év sztárja
(CC BY 2.0 image by Jason Scott)

Az AWK vitathatatlanul hasznos eszköz volt a maga idejében, de amikor a közelmúltban belefutottam egy AWK oktató cikkbe, azon kezdtem el gondolkodni, hogy érdemes-e még napjainkban is megtanulni vagy a kor gyengén típusos, szövegfeldolgozásban jeleskedő programozási nyelvei teljes mértékben elavulttá tették.

A vizsgálatok során a vetélytársak olyan nyelvek vagy eszközök lesznek, amik jó eséllyel már alapból telepítve vannak egy modern Linux-disztribúcióban.

AWK gyorstalpaló

Az AWK programok blokkokból állnak, egy blokk szűrő { parancsok } formátumú. A parancsok lefutnak minden egyes sorra, amire a szűrő igaz. A sort mezőkre bontja whitespace-ek mentén (változtatható) és az egyes mezők a $1..$n változókban elérhetőek. A szűrő opcionális, lehet például BEGIN, END, /regex/ vagy egy boolean feltétel, mint például $2 > 4.

Van pár beépített változó, amik közül az érdekesebbek az FS, amivel a elválasztó karaktert (vagy regexet) lehet megadni (például a BEGIN blokkban), az NF, ami az aktuális sor mezőinek a száma ($NF az utolsó mező értéke) és az NR, ami azt adja meg, hogy hányadik rekordnál (sornál) járunk.

Parancssori eszköz

Az egyik felhasználási mód, hogy az AWK programot rögtön a parancs paramétereként írjuk meg. Például a cat parancs awk megfelelője:

$ awk '{ print }' /etc/passwd

Ezen a téren az egyik kihívó a Perl nyelv lesz, aminek van awk-szerű működési módja:

$ perl -ae 'print' /etc/passwd

Valamint megnézzük, hogy egyéb Linux-on elérhető eszközökkel, mint a cut vagy a grep hogyan lehet ugyanazokat a problémákat megoldani.

A root felhasználó rekordjának utolsó mezője

Az /etc/passwd esetén ez nem akkora probléma, mivel fix mennyiségű oszlop van, de tegyük fel, hogy egy olyan fájlról van szó, ahol soronként változhat, hogy hányadik mező az utolsó.

$ awk -F: '$1 == "root" { print $NF }' /etc/passwd

$ perl -F: -le 'print $F[-1] if $F[0] eq "root"' /etc/passwd

$ grep '^root:' /etc/passwd | rev | cut -d: -f1 | rev

A Perl változat egy kicsit hosszabb, bár a grep-nél használt regex-es trükk itt is alkalmazható lenne (ahogy az awk esetén is). A shell script-es változatban a rev használata nem túl elegáns, de sajnos a cut nem tud olyat, hogy dinamikusan visszaadja az utolsó mezőt.

Azok a rekordok, ahol a user id nem egyezik a group id-val

$ awk -F: '$3 != $4 { print $1 }' /etc/passwd

$ perl -F: -le 'print $F[0] if $F[2] != $F[3]' /etc/passwd

$ grep -v ':\([0-9]\+\):\1:' /etc/passwd | cut -d: -f1

Az awk és a Perl-es változat itt is egész egyértelmű, a tömb elérések miatt a Perl itt is egy kicsivel hosszabb. A shell script-es változat backreference-es trükkje kevésbé olvasható megoldást eredményez.

Az összes user id összege

$ awk -F: '{ s += $3 } END { print s }' /etc/passwd

$ perl -F: -le '$s += $F[2]; END { print $s }' /etc/passwd

$ cut -d: -f3 /etc/passwd | paste -sd+ | bc

A Perl jó munkát végzett az awk másolásakor, nem is nagyon lehet mit hozzáfűzni. A shell script-es itt se túl egyértelmű, a paste a sorokból egy sort csinál, amit a + karakterrel fűz össze, a bc pedig kiértékeli a matematikai műveleteket.

A használt shell-ek népszerűségük szerint

$ awk -F: '{ x[$7]++ } END { PROCINFO["sorted_in"] = "@val_num_desc"; for (v in x) { print x[v], v } }' /etc/passwd

$ perl -F: -le '$x{$F[6]}++; END { print $x{$_}, " ", $_ for sort { $x{$b} <=> $x{$a} } keys %x }' /etc/passwd

$ cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn

Az awk-s megoldás tipikusan olyan, amit fejből nem valószínű, hogy csak úgy begépel az ember, ráadásul csak gawk-ban működik. A Perl-es se sokkal szebb, az asszociatív tömbök rendezése érték alapján egyik nyelvnek se kedvezett. A shell script viszont ez esetben egész kiemelkedően szerepelt.

Programozási nyelv

Egyszer csak eljutunk arra a pontra, hogy olyan programunk van, ami nem fér ki kényelmesen a parancssorban, többször is futtatnunk kell és ezért egy fájlba írjuk. Ezen a ponton úgy érzem, hogy az awk-nak már nincs helye.

Karbantarthatósági és tesztelhetőségi szempontokból elmarad a modern programozási nyelvektől, egy hosszabb életű script esetében pedig ezek már nem elhanyagolhatóak.

Nézzünk azért egy egyszerűbb példát:

BEGIN {
    FS = ":"
    count = 0
}

$3 != $4 {
    count += 1
    print "uid does not equal gid for user " $1 " (" $3 " vs " $4 ")"
}

END {
    print count " matching users"
}

Ez valahogy így nézhetne ki Python nyelven:

import fileinput

count = 0

for line in fileinput.input():
    fields = line.strip().split(':')

    if fields[2] != fields[3]:
        count += 1
        print(f'uid does not equal gid for user {fields[0]} ({fields[2]} vs {fields[3]})')

print(f'{count} matching users')

Nem mondható szörnyűnek, de ha valami awk-hoz hasonlót szeretnénk, akkor sem kell a Python-t hátrahagynunk. Írhatunk egy kis modult, hogy leutánozzuk a szerkezetét:

from my_awesome_file_processing_module import run

def begin(ctx):
    ctx.FS = ':'
    ctx.count = 0

def uid_does_not_equal_gid(ctx, fields):
    if fields[2] != fields[3]:
        ctx.count += 0
        print(f'uid does not equal gid for user {fields[0]} ({fields[2]} vs {fields[3]})')

def end(ctx)
    print(f'{ctx.count} matching users')

run(begin, end, [uid_does_not_equal_gid])

Vagy egy kicsivel több munkával még a tömbös eléréstől is megszabadulhatunk:

from my_awesome_file_processing_module import begin, end, line, run

@begin()
def begin(ctx):
    ctx.FS = ':'
    ctx.count = 0

@line(lambda _3, _4: _3 != _4)
def uid_does_not_equal_gid(ctx, _1, _3, _4):
    ctx.count += 0
    print(f'uid does not equal gid for user {_1} ({_3} vs {_4})')

@end()
def end(ctx)
    print(f'{ctx.count} matching users')

run()

Összegzés

Ha Perl mágus vagy, nagy valószínűséggel nincsen szükséged az awk-ra, sem mint parancssori eszközre, sem mint programozási nyelvre.

Ha shell script-ek írásával töltöd időd jó részét, akkor is egész jól el tudsz boldogulni awk nélkül, de már egy minimális awk tudás is hasznos tagja lehet az eszközkészletednek, mivel egyszerűbb, jobban érthető scripteket eredményezhet.

Ha járatos vagy bármilyen más script nyelvben, valószínűleg jobban jársz ha awk helyett abban írsz hosszabb lélegzetvételű programokat.

Ezektől függetlenül egy érdekes eszközről van szó, amit a gyakorlatban való alkalmazhatóságától függetlenül érdemes lehet kicsit tanulmányozni. Minden nyelvből lehet valamit tanulni, ha mást nem, elrettentő példákat.

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.