Variációk egy témára

Van az úgy néha, hogy elindulunk egy gondolat mentén és nem várt helyeken lyukadunk ki. Amikor nem csak azt tudod meg, hogy milyen mély a nyúl ürege, hanem azon kapod magad, hogy újabb járatokat ásol.

Hasonlóképpen jártam, amikor szembe jött velem a következő probléma. Adott egy fájl, amiben tabokkal elválasztott értékek vannak és az első sora az oszlopok nevei. Valami ilyesmi:

id	name	status	date	type
1	n1	s1	d1	t1
2	n2	s2	d2	t2
3	n3	s3	d3	t3

Amiből az első oszlop értékeit szeretnéd megkapni, szóközzel elválasztva: 1 2 3. Annyi még a nehezítés, hogy parancssorban vagyunk és nem szeretnénk külön programot írni. Érkezett már hozzá egy megoldás is:

cat file.tsv | awk '(NR>1) { print $1 }'  |  tr '\n'  ' '

Bár magam is nagy awk rajongó vagyok, mégsem ez a megoldás jutott eszembe elsőre. Amivel semmi probléma nincsen, sőt. Ez indított el az úton, hogy vajon milyen alternatív lehetőségek vannak még.

A probléma boncolása

Először is vizsgáljuk meg egy kicsit jobban a problémát. Alapvetően három részre bonthatjuk fel:

  1. szabaduljunk meg az első sortól
  2. vegyük minden sor első oszlopát
  3. a több sorból csináljunk egy sort (szóközzel elválasztva)

Így, hogy kisebbek a problémák, megnézhetjük, hogy az egyes lépésekre milyen megoldásokat találunk.

Az input első sorának eldobása

awk '(NR>1) { print }'

Kicsit erőltetett példa az eredeti megoldásból, mert ha már idáig eljutottunk awk-ban, akkor mehetnénk tovább is, de erről majd egy kicsit később.

sed 1d

Ezt már a nyomozásaim közben fedeztem fel, nagyon elegáns kis megoldás, törli a bemenet első sorát, a maradékot pedig változtatás nélkül megkapjuk.

tail +2

Ahogy a klasszikus tail -1 az input utolsó sorától a végéig adja vissza az adatokat, úgy ez a második sortól a végéig.

Az első oszlop megtartása

awk '{ print $1 }'

A klasszikus awk-s megoldás.

cut -f1

Személyes kedvencem, nem annyira okos, mint az awk (például ha több elválasztó karakter van egymás után), de sok esetben jól jön.

sed 's/^\([^\t]\+\).*/\1/'

A sed-del mindent is meg lehet oldani. De minek? Nagy segítségünkre van itt, hogy az első oszlopra van szükségünk, extra könnyítés lehet még, ha tudjuk, hogy az id csak szám lehet.

Az eredmény egysorosítása

paste -s -d' ' -

Ezt az eszközt pont erre a célra találták ki, remek választás lehet.

sed -n 'H;${g;y/\n/ /;p}'

Nem egy barátságos megoldás, ráadásul van egy extra szóköz a sor elején. De a sed lelki világáról és arról, hogy mit is jelent a fenti bitkolbász a későbbiekben még beszélünk.

tr '\n'  ' '

Remekül működik, az egyetlen hátránya, hogy a sor végi új sor karaktert is lecseréli.

xargs

Na, ez egy fura választás, a sorokból parancs paramétereket csinál és meghív vele egy parancsot, ami alapból az echo, úgyhogy pont azt csinálja, ami nekünk kell. Nem működik, ha szóköztől különböző karakterrel akarjuk elválasztani a sorokat.

Komplex megoldások

Máris van 36 különböző megoldásunk a problémára. Persze egy awk '(NR>1) { print }' | awk '{ print $1 }' nem túl életszerű, előfordul ugyanis, hogy egy eszköz több részproblémát is meg tud oldani egy lépésben:

awk '(NR>1) { print $1 }'
sed -n '1!s/^\([^ ]\+\).*\n/\1/p'
perl -F'\t' -e 'print "$F[0] " if $i > 0; $i++'

De vajon van-e olyan eszköz, ami egy lépésben megoldja az egészet? Biztos van egy olyan egyszerű kis sed parancs, ami pont ezt csinálja, nem? Itt kezdődött az ásás. Belevetettem magam a sed dokumentációjának sűrűjébe. A fejem is belefájdult látva, hogy milyen borzalmakra képes ez az eszköz. Mintha az awk és a vi szerelemgyereke lenne. Erőfeszítéseim nem voltak hiábavalóak, végül egy parancs társaságában másztam ki az üregből:

sed -n '1!{s/^\([^\t]\+\).*/\1/;H};${g;y/\n/ /;s/^ //;p}'

Nem is értem miért nem ez jutott az eszembe első megoldásnak. Bár nem akarok senkit magammal rántai a sed bugyrainak mélyére, azt hiszem ez azért némi magyarázatra szorul.

sed alapok

A sed parancsokat vár, minden parancs <szűrő><parancs><paraméterek> formátumú, a parancs lefut minden sorra, amire a szűrő feltétel igaz (eddig gyanúsan hasonlít az awk-ra). Megadhatunk egymás után több parancsot is, ha pontosvesszővel választjuk el őket. Egy szűrőhöz kapcsolva is megadhatunk több parancsot, ha a parancsok { és } között vannak.

Ezen kívül annyit érdemes még tudni, hogy a sed-ben van egy úgynevezett "pattern space", amiben az aktuális sor található és amibe a parancsok visszaírják az eredményüket, valamint létezik egy "hold space" is, amibe elrakhatjuk a "pattern space" tartalmát majd később vissza is hozhatjuk onnan.

A megoldás részletei

Nincs más hátra, mint felboncolni az eredeti parancsunkat:

sed -n '1!{s/^\([^\t]\+\).*/\1/;H};${g;y/\n/ /;s/^ //;p}'

A -n kapcsoló azt mondja neki, hogy ne írjon ki alapból semmit. Az utána következő részt rögtön két parancsra bonthatjuk, van egy 1!{...} és egy ${...} blokkunk. Az első lefut minden sorra, ami nem az első sor, a második pedig csak az utolsó sorra.

Az első blokkra két parancs fut le, az s és a H, az s lecseréli a teljes sort az első tab előtti részre, a H pedig elrakja ezt az adatot egy új sor után fűzve a "hold space"-be.

A második blokkban négy parancsot futtatunk, egy g-t, egy y-t, egy s-t és végül egy p-t. A g előszedi a "hold space" tartalmát (ami az első oszlopa az eredeti adatnak, új sor karakterekkel elválasztva), az y lecseréli az új sor karaktereket szóközre, az s pedig eltűnteti az így kapott eredmény elején található extra szóközt. A p pedig kiírja nekünk a végeredményt és kész is vagyunk.

Érdekes módon hasonló eredményre jutunk, ha más eszközt használunk (például awk vagy egy Perl one-liner), mivel ezek alapvetően sorokkal dolgoznak, kell valami, amiben a részeredményeket el tudjuk tárolni és később tovább dolgozni vele.

Ezzel a végére is értünk rövid kis sed-földi kirándulásunknak. Tanúlság? Talán annyi, hogy hatékonynak tűnik a részfeladatokra céleszközöket használni ahelyett, hogy a teljes problémát próbálnánk megoldani egyszerre. További kellemes szövegfeldolgozást.

This post is also available in english: Variations for a theme

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre vagy irány a bejegyzéshez tartozó tweet.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha a régi jó dolgokat kedveled, de követheted a blogot Twitteren is.