\"/
\"/ \"/    

Programovací jazyk textových manipulací: awk (1)

Michal Brandejs, FI MU
Ročník V - číslo 5, květen 1995
Citace: M. Brandejs. Programovací jazyk textových manipulací: awk (1). Zpravodaj ÚVT MU. ISSN 1212-0901, 1995, roč. V, č. 5, s. 3-7.
Tematické zařazení: Programy, nástroje
Článek je součástí seriálu Programovací jazyk textových manipulací: awk
 předchozí článek | následující článek 

Následující kapitolu jsme pro čtenáře Zpravodaje ÚVT MU vybrali z knihy připravované pro nakladatelství Grada Publishing. Nechť poslouží všem, kteří chtějí trošku více proniknout do ovládání operačního systému UNIX, i když awk nacházíme i v jiných systémech.

Awk je programovací jazyk pro práci s textem. V textově orientovaném UNIXu jej používáme také pro automatickou konstrukci příkazů, příkazových souborů a předzpracování vstupních dat. Název Awk pochází ze jmen jeho autorů: Alfred V. Aho, Peter J. Weinberger a Brian W. Kernighan. Interpretem tohoto jazyka je příkaz (program) awk. Jeho verzí se však vyskytuje více. Nejvíce funkcí má zřejmě implementace awk z projektu GNU (gawk) a jí je následující text věnován. Tato implementace má všechny rysy podle POSIX 1003.2 a navíc některá rozšíření.

1  Použití awk

Příkaz se spouští následujícím způsobem:

     awk program [soubory]

Program můžeme také číst ze souboru, potom zadáme:

     awk -f soubor [soubory]

Program awk čte řádky buď ze zadaných souborů nebo ze standardního vstupu. Výstup směřuje na standardní výstup.

2  Struktura programu

Program pro awk se tvoří posloupností příkazů ve tvaru:

     vzorek { akce }
vzorek { akce }
atd.

V každém řádku čteném ze vstupu se hledá určený vzorek. Pokud se najde, provede se s řádkem zadaná akce. Poté, co se použijí všechny vzorky, přečte se ze vstupu další řádek a operace se opakují.

Jak vzorek, tak akce se smí vynechat. Nelze však vynechat obojí současně. Pokud není ke vzorku určena akce, potom se vyhovující řádek zkopíruje na výstup. Pro řádek vyhovující více vzorkům bude akce provedena vícekrát. Řádek nevyhovující žádnému ze zadaných vzorků se ignoruje.

Pokud vynecháme vzorek, potom se akce provede pro každý načtený řádek. Popis akce se musí uzavřít do složených závorek '{ }'. Tím se popis akce rozpozná od popisu vzorku.

3  Záznamy a položky

Vstup, který awk čte, dělíme do záznamů (records) ukončených oddělovačem záznamu. Implicitním oddělovačem záznamu je znak nového řádku. V tomto případě je záznamem jeden řádek. Číslo aktuálního záznamu awk udržuje v proměnné NR.

Každý zpracovávaný záznam se dělí do položek (field). Položky se implicitně oddělují bílým místem (mezera, tabulátor), lze však explicitně nastavit jinou hodnotu oddělovače položek. Na jednotlivé položky se odkazujeme $1, $2 atd. Údaj za znakem dolar je číslo (ne jenom číslice). Identifikátorem $0 se odkazujeme na celý záznam. Počet položek v aktuálním záznamu je uložen v proměnné NF.

Chceme-li změnit implicitní nastavení oddělovačů záznamů a položek, nastavíme novou hodnotu do proměnné: RS pro oddělovač záznamů a FS pro oddělovač položek. Obsah těchto proměnných můžeme změnit obecně na regulární výraz (v jiných verzích awk pouze na libovolný jeden znak). Oddělovač položek můžeme také nastavit na příkazovém řádku při spouštění awk volbou -Fc, kde c je oddělovač položek.

Je-li oddělovač záznamů prázdný, potom se jako oddělovač chápe prázdný řádek na vstupu. Oddělovači položek potom jsou znaky mezera, tabulátor a nový řádek.

V proměnné FILENAME je uloženo jméno aktuálního vstupního souboru ('-' v případě standardního vstupu).

4  Výstup

Nejjednodušší program, který opíše standardní vstup na standardní výstup, je následující:

     ... | awk '{ print }' | ...

Vzorek jsme vynechali, a proto se akce print provede pro všechny vstupující řádky. Akce print bez parametrů opíše celý řádek na výstup. Užitečnější bude vybrat si určité položky a tyto vypsat, např. první dvě položky v opačném pořadí:

     { print $2, $1 }

Takový zápis akce na příkazovém řádku spouštějícím awk musí být nutně uzavřen do dvojice apostrofů, aby nedošlo k expanzi dvojic znaků $1$2 na poziční parametry shellu, ale aby se v nezměněné podobě předaly awk.

Položky oddělené v zápisu akce print čárkou se na výstupu oddělí aktuálně nastavenou hodnotou oddělovače položek. Položky oddělené pouze mezerou se spojí bez oddělovače. Vyzkoušejme si:

     { print $2 $1 }

Akce print umí vypisovat i obsahy proměnných a textové řetězce, např.:

     { print "Číslo záznamu=" NR, "Počet položek=" NF, $0 }

Takto zadaný program před kompletním záznamem vypíše číslo záznamu a počet položek v aktuálním záznamu.

Výstup můžeme rozdělit i do více výstupních souborů. Např. program:

     { print $1 >"soubor1"; print $2 >"soubor2" }

zapíše první položku do souboru soubor1 a druhou položku do souboru soubor2. Lze použít i zápis >>. Potom se do souboru přidává za konec. Jméno souboru může být proměnná, obsah zapisovaný do souboru může být konstanta. Můžeme tedy napsat např.:

     { print "nesmysl" >>$2 }

Jako jméno souboru se použije obsah druhé položky (pozor, nesmí být prázdná). V tomto příkladě bude počet řádků v jednotlivých souborech znamenat četnost slov ve druhém poli.

Podobně lze výstup z akce print předat rourou procesu. Např. poslat poštou na adresu 'zaznamenej':

     { print | "mail zaznamenej" }

Pokud si tento příklad vyzkoušíme, zjistíme, že se procesu předá celý vstup naráz, tj. nepředává se po jednotlivých záznamech a navíc se předá až po načtení konce vstupu.

Pomocí proměnných OFSORS můžeme samostatně nastavit oddělovače pouze pro výstup. Obsahem OFS se oddělí vypisované položky a obsahem ORS se oddělí vypisované záznamy.

Jazyk awk také poskytuje možnost formátovaných výstupů pomocí akce printf. Tato akce se zapisuje následujícím způsobem:

     printf formát, výraz, výraz, ...

Popisovačem formát sdělíme strukturu výstupu a prostřednictvím nadefinované struktury vypíšeme jednotlivé výrazy. V popisovači formát se používá stejná syntaxe jako v jazyce C. Uveďme si ve stručnosti možnosti:

Do řetězce formát vkládáme text, který se má vypsat. Na místa, kde se má vypsat výsledek výrazu, vložíme popisovač ve tvaru:

     %příznaky šířka přesnost typ konverze

Prvnímu popisovači (každý popisovač začíná vždy znakem %) se přiřadí výsledek prvního výrazu, druhému popisovači výsledek druhého výrazu atd. Jednotlivá pole popisovače mají tento význam (všechna pole vyjma konverze jsou nepovinná):

příznaky
se uvádějí žádný nebo více. Možné příznaky jsou:
-  Výstup bude zarovnán vlevo. Bez uvedení tohoto příznaku bude výstup zarovnán vpravo.
+ Výstup čísla se znaménkem bude znaménko vždy obsahovat. Bez uvedení tohoto příznaku se uvede pouze případné záporné znaménko.
mezera Stejné jako příznak +, jenom se místo kladného znaménka vytiskne mezera. Pokud se uvede + i mezera, potom + vyhraje.
# Výstup se převede do alternativní podoby. Podrobnosti jsou uvedeny u popisu každé konverze.
šířka
Význam pole záleží na typu použité konverze.
přesnost
Přesnost uvádí, kolik číslic se má vypsat vpravo od desetinné tečky. Číslu odpovídající přesnosti musí předcházet tečka. Pokud se uvede tečka bez čísla, použije se hodnota 0. Přesnost lze zadat pouze s konverzemi e, E, f, gG.
typ
Pole může obsahovat znaky h, l nebo L. Symbol h sděluje, že se argument před výstupem převede na formát short, typ l převede na formát long int a typ L na formát long double.
konverze
Konverze obsahuje jeden znak, který sděluje, jak se má výraz vytisknout.

Na místě konverze se smějí použít tyto symboly:

i nebo d
Předpokládá se celočíselný argument (int), který se interpretuje jako číslo se znaménkem. Pole šířka může obsahovat minimální počet znaků, na který se číslo vypíše. Implicitně je šířka 1. Význam příznaku # není definován.
o   Celočíselný argument bez znaménka se vytiskne osmičkově. Význam pole šířka je stejný jako u konverze i. Příznak # zvýší šířku tak, aby první číslice byla nula.
u Celočíselný argument bez znaménka se vytiskne desítkově. Význam pole šířka je stejný jako u konverze i. Význam příznaku # není definován.
x Celočíselný argument bez znaménka se vytiskne šestnáctkově. Na místě číslic se použijí i znaky abcdef. Význam pole šířka je stejný jako u konverze i. Příznak # zvýší šířku tak, aby první číslice byla nula.
X Celočíselný argument bez znaménka se vytiskne šestnáctkově. Na místě číslic se použijí i znaky ABCDEF. Význam pole šířka je stejný jako u konverze i. Příznak # zvýší šířku tak, aby první číslice byla nula.
f Argument ve dvojnásobné přesnosti double se převede do tvaru '[-]ddd.ddd'. Pole šířka uvádí minimální počet vytisknutých znaků. Pole přesnost může za tečkou specifikovat počet desetinných míst. Po uvedení příznaku # se vypíše desetinná tečka, i když nenásleduje žádná desetinná číslice.
e Argument ve dvojnásobné přesnosti double se převede do tvaru '[-]d.dddedd'. Exponent budou vždy alespoň dvě číslice. Zobrazuje-li se hodnota 0, potom je i exponent nulový. Význam pole přesnost a příznaku # je stejný jako u konverze f.
E Stejné jako e s tím rozdílem, že se místo e vypíše E.
g Stejné jako f nebo e. Konverze e se použije tehdy, pokud exponent je menší než -4 nebo je větší než přesnost.
G Stejné jako g s tím rozdílem, že se místo e vypíše E.
c Celočíselný argument se převede na typ unsigned char a výsledný znak se vypíše (tj. ordinální hodnotu převede na ASCII znak). Význam pole přesnost a příznaku # je nedefinován.
s Předpokládá se, že argumentem je řetězec znaků (char *). Vypíší se znaky řetězce až po (ale bez) ukončovacího null znaku. Pole šířka představuje maximální počet znaků, které se vypíší. Význam příznaku # je nedefinován.

Akce printf negeneruje žádné výstupní oddělovače. Všechny se musejí specifikovat ve formátu. Uveďme si příklad použití:

     { printf "Průměr=%8.2f, počet pokusů=%10ld\n", $1, $2 }

První položka se vytiskne jako číslo v pohyblivé řádové čárce celkem na 8 znaků se dvěma číslicemi za desetinnou tečkou. Druhá položka se vytiskne jako long integer na 10 znaků. Znak \n představuje nový řádek.

5  BEGIN a END

BEGINEND jsou speciálními případy vzorků. Vzorek BEGIN specifikuje akci, která se má provést dříve, než se přečte první záznam vstupu. Naopak vzorek END popisuje akci, která se provede po zpracování posledního čteného záznamu. Tímto způsobem můžeme řídit zpracování před a po čtení záznamů.

Jako příklad uveďme nastavení specifického oddělovače položek a vytisknutí počtu načtených záznamů:

     BEGIN { FS = ":" }
...zbytek programu...
END { print NR }

BEGIN musí být jako první vzorek (je-li uveden), END musí být posledním vzorkem.

6  Akce

Akce programu awk je posloupnost příkazů vzájemně oddělených novým řádkem nebo středníkem.

7  Proměnné, výrazy a přiřazení

Jazyk awk proměnné zpracovává podle kontextu: buď jako numerické hodnoty (v pohyblivé řádové čárce), nebo jako řetězce znaků. Řetězce se na numerické hodnoty převádějí podle potřeby. Potom např.

     x = 1

je typicky numerický přiřazovací příkaz, ale v příkazu

     x = "3" + "4"

se řetězce převedou na numerické hodnoty a proměnné x se přiřadí numerická hodnota 7. Řetězce, ze kterých nelze získat numerickou hodnotu, mají hodnotu 0.

Proměnná, které dříve nebyla přiřazena hodnota, má hodnotu nula. Interním proměnným awk se přiřazují hodnoty automaticky (viz dále). Proto např. program

         { s1 += $1; s2 += $2 }
END { print s1, s2 }

může k proměnné s1 přičítat. Proměnná interpretující se jako řetězec bez přiřazené hodnoty obsahuje prázdný řetězec.

Potřebujeme-li se ujistit, že proměnná bude chápána jako numerická, přičteme k ní hodnotu 0. Potřebujeme-li naopak proměnnou interpretovat jako řetězec, připojme k ní prázdný řetězec, např.

     b = 12 ""

I když se numerické proměnné zpracovávají a ukládají v pohyblivé řádové čárce, desetinná tečka a číslice za ní se vypisují jenom tehdy, pokud je desetinná část nenulová. Číslo se na řetězec konvertuje podle obsahu proměnné CONVFMT voláním sprintf. Řetězec se na číslo konvertuje voláním atof.

Příklad

Na závěr první části pojednání o awk uveďme ilustrační příklad: Chceme awk použít na převod výstupu příkazu ls -l do klasického DOSovského tvaru výpisu adresáře, tj. ze tvaru:

     -rw-r--r-- 1 brandejs staff
             173741 May 16 21:10 text2.tex

do tvaru:

     text2.tex              173741 May 16 21:10

Sestavíme-li kolonu:

     ls -l | grep -v ^total | awk -f dirp

potom pro awk1 potřebujeme následující program v souboru dirp:

     BEGIN {print "User is " ENVIRON["LOGNAME"]; print }
  {printf "%-18s %10i %3s %2i %4s\n", $9, $5, $6, $7, $8;
   suma = suma + $5 }
END {printf "      %4i file(s)%11i bytes\n", NR, suma }

Příkazem grep -v ^total zrušíme nevýznamný řádek začínající slovem total. Pole ENVIRON popíšeme v pokračování.

(... pokračování)
setting
1 V Systému V (např. Solaris, IRIX) použijte místo awk program nawk, pokud nemáte popisované gawk.
... zpět do textu
Zpět na začátek
ÚVT MU, poslední změna 14.11.2011