Nauka programowaniu w AWKu


1. Wiadomości ogólne

Nazwa AWK pochodzi od nazwisk autorów tego języka: Alfreda V. Aho, Petera J. Weinbergera i Briana W. Kernighana. AWK jest językiem interpretowanym (a nie kompilowanym), służącym do obróbki plików tekstowych. Bardzo dobrze nadaje się do pisania programów przetwarzających dane porcjami (w AWKu taka porcja jest określana jako rekord, standardowo jest to pojedyncza linia) - np. do analizowania wszelkiego rodzaju logów.
Zasada działania języka jest prosta: wczytywany jest pojedynczy rekord, i dla niego wykonywane są wszystkie instrukcje programu. Potem czytany jest następny, i znowu wykonywane są wszystkie instrukcje.

Istnieje kilka odmian AWKa: np. gawk, mawk. Są one do siebie podobne, jednak obie mają nieco więcej możliwości niż to wymagane od standardowego interpretera AWK (POSIX-AWKa). Dlatego o ile typowe rzeczy działają tak samo na wszystkich interpreterach, to w przypadku niestandardowych rozszerzeń rozwiązania z jednej wersji mogą nie działać w innych

2. Podstawy składni AWKa

Program napisany w AWKu składa się z bloków. Składnia bloku wygląda tak:

<warunek> { <polecenia> }

W momencie kiedy wczytywany jest kolejny rekord, i dla niego w pewnym momencie wykonywany jest ten blok, to AWK sprawdza, czy zachodzi <warunek>. Jeśli tak: to wykonywane są <polecenia>.
Przykładowym warunkiem jest np.:

i==5 { <polecenia> }

(i to jakaś zmienna, o zmiennych będzie później).
Warunkiem może być też wyrażenie regularne, ograniczone przez znaki /. Na przykład:

/a*bc/ { <polecenia> }

Taki blok będzie wykonany dla wszyskich rekordów, w których występuje dowolnie długi ciąg liter a (może być też pusty), a po nim litery "bc". Na przykład dla linii z napisem "aaaaabc", albo "xxxabcxxx", albo "xxxxbbc".
Można użyć dwóch warunków, oddzielonych przecinkiem, co oznacza: "dla każdej linii począwszy od pierwszej spełniającej warunek pierwszy, do następnej spełniającej warunek drugi". Np.:

/^From: /,/^$/ { <instrukcje> }

wykona blok instrukcji dla wszystkich linii począwszy od linii z napisem "From: " na początku, aż do napotkania pustej linii

Dodatkowo są dwa warunki specjalne: BEGIN i END. Pierwszy jest prawdziwy na samym początku, zanim jeszcze zostanie wczytany pierwszy rekord z jakiegokolwiek pliku. Drugi jest prawdziwy po zakończeniu czytania wszystkich plików.
Można pominąć w wyrażeniu albo warunek (wtedy blok będzie wykonywany dla każdego rekordu}, albo część z poleceniami (łącznie ze znakami { i }) - wtedy domyślnym blokiem instrukcji jest wypisanie rekordu na standardowe wyjście.

Polecenia nie są kończone średnikami. Natomiast średnikami możemy je oddzielać, jeśli chcemy użyć więcej niż jednej instrukcji w jednej linijce.

3. Zmienne

W AWKu nie ma deklaracji zmiennych. Wszystkie zmienne są inicjalizowane w momencie użycia, a przy inicjalizacji otrzymują wartość 0 lub "" (pusty napis), w zależności od kontekstu w jakim zmienna została użyta.
Wszystkie tablice zmiennych w AWKu są asocjacyjne, tzn. są indeksowane nie liczbami, a napisami. Można się też odwoływać do tablic przez numerki: wtedy liczba jest zamieniana na napis, i dalej następuje odwołanie do elementu indeksowanego napisem.
Przykłady:

i=3
a=i+3
tab["pierwszy"]=i
tab[2]=a

Operatory są jak w języku C - = to przypisanie, == to porównanie. Dodatkowym operatorem jest ~ (tylda), która oznacza "pasuje do", albo "zawiera wyrażenie". Np: "aaa" ~ /a/ jest prawdziwe, bo litera a występuje w napisie "aaa". A wyrażenie "aaa" ~ /b/ już jest nieprawdziwe.
Do odwoływania się do elementów z tablicy można używać operatora "in" w sytuacjach, gdy chcemy sprawdzić, jakie elementy są w tablicy, albo czy dany element istnieje w tablicy. Przykłady:

if ("xxx" in tablica) { ... }
for (i in tablica) { ... }

W pierwszym wypadku blok poleceń będzie wykonany, jeśli tablica zawiera element o indeksie "xxx", w drugim blok będzie wykonywany dla wszystkich elementów tablicy.


4. Zmienne specjalne

AWK udostępnia liczne zmienne specjalne, które są ustawiane przez sam interpreter, lub mają bezpośredni wpływ na działanie programu.
Podstawowe takie zmienne to zmienne "dolarowe": $0, $1, $2, ... Po wczytaniu dowolnego rekordu jest on zapamiętywany w zmiennej $0. Dodatkowo jest dzielony na pola, oddzielone od siebie separatorami. Przykładowo jeśli mamy linię z napisem "aaa bbb ccc", a separatorem jest spacja, to pola mają kolejno wartości: "aaa", "bbb", "ccc". I takie wartości też są przypisywane kolejno zmiennym: $1, $2, $3.
Ogólnie wyrażenie "$<zmienna>" oznacza: "pole o numerze takim jak wartość zmiennej". Czyli jeśli x==2, to $x oznacza to samo co $2.
Tych zmiennych dotyczą też takie zmienne jak: NF (liczba wczytanych pól), FS (separator pól - tu wpisywane jest pełne wyrażenie regularne, które opisuje separatory (np. "a|b"). Wyjątkiem jest separator " " (spacja), który pasuje do dowolnej liczby spacji, tabulatorów, itp. Początkowe i końcowe separatory (np. jak w " abc abc ") są ignorowane. Jeśli FS jest puste, to rekordy są dzielone na pojedyncze znaki.
Oprócz separatora pól mamy też separator rekordów, trzymany w zmiennej RS. Domyślnie separatorem jest znak nowej linii (\n)
Przy wypisywaniu rekordów na wyjście możemy posługiwać się innymi separatorami niż te, które były używane przy wejściu: analogicznie do FS i RS dostępne są też OFS i ORS (output field/record separator)
Kolejne to: NR (numer wczytanego rekordu) i FNR (numer wczytanego rekordu z aktualnego pliku). Nazwa aktualnie przetwarzanego pliku jest trzymana w zmiennej FILENAME. Następne zmienne: ARGC i ARGV, pierwsza z nich to ilość parametrów podanych do programu, druga to tablica tych parametrów, indeksowana od 0. ARGV[0] to nazwa uruchomionego interpretera (na ogół "awk"), pozostałe to rzeczywiste parametry, odpowiadające nazwom plików z których AWK bierze kolejne rekordy.
Dalej: zmienna ENVIRON. To jest tablica, która trzyma wszystkie zmienne środowiskowe. Przykładowo jeśli chcemy się odwołać do zmiennej "$HOME", to używamy:

ENVIRON["HOME"]

Na koniec dwie zmienne, które występują jedynie w GNU-AWKu (gawku): IGNORECASE, która jeśli jest różna od zera to powoduje ignorowanie różnic między małymi i dużymi literami, oraz CONVFMT która jest zapisem formatu wyświetlania liczb (podobnie jak w funkcji printf w języku C lub w samym AWKu, domyślnie "%.6g")


5. Podstawy składni AWKa

Składnia AWKa jest bardzo podobna do składni C (nic dziwnego - jeden z autorów AWKa, Kernighan, jest też jednym z autorów języka C). Z tego powodu nie będę się rozwodził nad znaczeniem każdej instrukcji, tylko je tu wymienię, razem ze składnią:

for (<inicjalizacja>; <warunek podtrzymujący>; <krok>) { ... }

if (<warunek>) { ... }
if (<warunek>) { ... } else { ... }

while (<warunek>) { ... }
do { ... } while (<warunek>)

W pętlach, tak samo jak w C, można użyć polecenia break żeby zakończyć działanie pętli, oraz continue, który wymusza zakończenie tego przebiegu pętli i rozpoczęcie następnego.
Jeśli w pętli/warunku jest tylko jedna instrukcja, to nie trzeba jej obejmować nawiasami klamrowymi ( {,} ).
Instrukcje w AWKu nie muszą kończyć się średnikiem. Średnik jest potrzebny tylko tam, gdzie chcemy wpisać kilka instrukcji w jednej linii. Czyli:

{
  print
  if ( 1 == 2 ) print
}

jest poprawne, a jeśli chcemy to zrobić w jednej linijce, to musimy to zapisać tak:

print ; if ( 1 == 2 ) print


6. Operacje na zmiennych

Właściwie na ten temat wszystko już powiedziałem przy okazji wprowadzania zmiennych. Mogę tylko dodać informację na temat usuwania elementów tablicy:

delete tab["klucz1"]

Takie polecenie usunie z tablicy o nazwie "tab" element z indeksem "klucz1". W większości implementacji AWKa można też użyć polecenia delete bez podawania indeksów:

delete tab

co usunie wszystkie elementy z tablicy. Aczkolwiek jest to niestandardowe (niezgodne z POSIX-AWKiem) rozszerzenie.


7. Operacje na tekście

Przede wszystkim przydatne są różnego rodzaju operacje korzystające z wyrażeń regularnych.
Zacznę od najprostszego wyszukiwania podciągu w tekście:

index ( <siano>, <igła> )

Takie polecenie zwraca "0", jeśli <igła> nie została znaleziona. Natomiast jeśli została, to zwraca numer znaku, na którym zaczyna się znaleziony fragment

match ( <siano>, <igła> )

Robi to samo co index, tylko że dla wzorców a nie napisów, dodatkowo do zmiennej RLENGTH wstawia długość dopasowanego fragmentu. Zwracana wartość, analogicznie jak w index, jest podawana "na wyjściu", a oprócz tego wstawiana do zmiennej RSTART.
Teraz jeśli chcemy wyciąć z naszego <siana> pasujący fragment, to użyjemy polecenia:

substr ( <siano>, RSTART, RLENGTH )

które wytnie z <siana> fragment tekstu o długości co najwyżej RLENGTH, a zaczynający się na znaku nr RSTART. Ogólnie składnia jest następująca:

substr ( <napis>, <początek> [, <koniec>] )

<koniec> jest parametrem opcjonalnym, jeśli go pominiemy, polecenie zwróci fragment tekstu od podanego miejsca do końca.
Przy okazji jest też funkcja length ( <napis> ), której chyba nie muszę opisywać :)
Dalej operacje podstawienia:

sub ( <co>, <na co> [, <w czym>] )

podstawia pierwsze wystąpienie wyrażenia <co> na wyrażenie <na co>, w zmiennej podanej jako <w czym>, lub w zmiennej $0, jeśli <w czym> jest ominięte. Funkcja zwraca ilość wykonanych podstawień: 1 lub 0. Przykład:

x = "abcabc" ; sub ( "a", "b", x )

po wykonaniu tego kawałka kodu zmienna x będzie miała wartość "bbcabc".
Podobną instrukcją jest gsub, która działa dokładnie tak samo jak sub, ale zamienia wszystkie wystąpienia, a nie tylko pierwsze. Podobnie, zwraca ilość zamienionych fragmentów.
W obu tych instrukcjach można w stringu na który zamieniamy używać wyrażenia specjalnego "&": ten symbol oznacza cały fragment tekstu, który został dopasowany. Czyli jeśli użyjemy polecenia:

sub ( "[abc]", "x&y" )

to w zależności od tego, czy w zmiennej $0 wystąpiła literka a, b lub c otrzymamy w jej miejscu napis "xay", "xby" lub "xcy".
Następną funkcją podstawiającą jest gensub, która jest rozszerzoną wersją sub/gsub. gensub nie należy do standardu AWKa i występuje tylko w GNU-AWKu (gawk). W działaniu jest podobna do poprzedniczek, z kilkoma wyjątkami. Składnia polecenia:

gensub ( <co>, <na co>, <które> [, <w czym> ] )

Pierwsza różnica: gensub może podstawić tylko konkretne wystąpienie danego wyrażenia. Jeśli np. <które> będzie równe 3 (liczbowo, nie napis "3"), to zostanie zamienione tylko trzecie wystąpienie >co>. Jeśli <które> będzie równe 1, to gensub zachowuje się jak sub, a jeśli będzie równe napisowi "g" lub "G", to tak jak gsub
Druga różnica: gensub ma możliwość użycia w stringu na który podstawiamy oprócz znaku "&" wyrażeń specjalnych "\\0", "\\1" itd. Te wyrażenia są odpowiednio rozwijane w zależności od pogrupowania wyrażenia zamienianego nawiasami. "\\0" to to samo co "&". We wzorcu "\([abc]\)a*" pod wyrażenie "\\1" zostanie podstawione to co jest w pierwszej parze nawiasów: czyli a, b lub c. W ten sposób coś takiego:

gensub ( "\([abc]\)\([abc]\)", "x\\1y\\2z", 1, <zmienna> )

zastąpi pierwsze wystąpienie dwóch literek pod rząd z przedziału a-c przez "x<pierwsza literka>y<druga literka>z".
Trzecia różnica: gensub nie zwraca liczby wykonanych podmian, tylko zwraca już podmieniony napis, natomiast nie modyfikuje zmiennej w której dokonywał podstawień.
Tyle o gensub. Następna funkcja:

split ( <napis>, <tablica> [, <separator>] )

Funkcja dzieli <napis> na pola, i umiesza je w <tablicy>. Separatorem pól w tym napisie domyślnie jest ogólny separator pól, wpisany do zmiennej FS, chyba że podamy własny separator.
Tablica kolejnym polom przypisuje indeksy numeryczne, począwszy od 1.
Funkcja sprintf:
działa prawie tak samo jak funkcja printf w języku C. Składnia:

sprintf ( <format>, <argument 1>, ... )

<format> jest analogiczny do formatu printfa z C. Polecenie niczego nie wypisuje, jedynie zwraca otrzymany napis.
I na koniec: funkcje toupper ( >napis> ) i tolower ( <napis> ), które odpowiednio zamieniają wszystkie znaki w <napisie> na duże/małe.


8. Operacje wyjścia

Podstawową instrukcją wyjścia jest print. Samo polecenie print powoduje wypisanie całego rekordu ($0) na standardowe wyjście. print przyjmuje argumenty, np.:

print "abc" "def"

co wypisze napis "abcdef" (spacja oznacza konkatenację). Jeśli chcemy oddzielić te dwa napisy separatorem (OFS), to piszemy:

print "abc","def"

Drugą instrukcją pisania jest printf, które działa tak samo jak printf w języku C:

printf <format>, <arg1>, ...

(bez nawiasów).
Przy wypisywaniu można używać przekierowań: np.

print "aaaaa" > "plik1"

wpisze tekst "aaaaa" do pliku o nazwie "plik1". Chcąc wypisać coś do strumienia standardowego błędu należy użyć pliku specjalnego "/dev/stderr".


9. Operacje matematyczne

Jak w C: dodawanie i odejmowanie (+,-,++,--), mnożenie (*), dzielenie (/ - część całkowita z dzielenia, i % - reszta), potęgowanie (^), funkcje matematyczne (sin, cos, sqrt, log, ...)
Można też używać operatorów skróconych:

x+=3
y*=2


10. Funkcje

W AWKu można samemu definiować funkcje:

function <nazwa> ( <arg1>, <arg2>, ... ) { ... }

Wywołuje się to przez "<nazwa> ( <val1>, ... )". Każda funkcja musi coś zwracać: jeśli nie podamy zwracanej wartości w treści funkcji, to będzie ona zwracała śmieci. Wynik funkcji podajemy za pomocą polecenia return <wartość>.


11. Operacje wejścia

Żeby przerwać operacje na danym rekordzie i wczytać następny wystarczy wywołać polecenie next. Wtedy AWK przerywa obróbkę aktualnie wczytanej pozycji i idzie do następnej.
Do samego wczytania następnego rekordu służy polecenie getline. Uwaga: to polecenie zamazuje aktualny rekord, a dodatkowo powoduje, że po zakończeniu wszystkich instrukcji w tym przebiegu jako następny zostanie wczytany kolejny rekord (tzn. jeszcze następny)!
Podobną funkcję wykonuje polecenie getline Dlatego znacznie częściej używa się polecenia getline <zmienna>, które wczytuje rekord i jego zawartość wstawia do zmiennej <zmienna>.
Można też wczytać linię z innego pliku niż aktualnie przetwarzany:

getline < <nazwa pliku>

AWK pamięta, którą linijkę ostatnio czytał z którego pliku, także ponowne wywołanie takiej instrukcji wczyta nie pierwszą z pliku, ale pierwszą do tej pory nie wczytaną. Podobnie można korzystać z poleceń zewnętrznych, i z ich wyjścia:

"/bin/ls -1" | getline <zmienna>

To oczywiście tylko przykład. Tak samo: dla każdej takiej instrukcji AWK nie wykonuje tego samego polecenia od nowa, tylko bierze następną linijkę z wyjścia pierwszego wywołania tego polecenia.
Jeśli chcemy zamknąć plik i zacząć jego czytanie od początku, używamy polecenia close ( <nazwa pliku> ). To samo dotyczy komend: np. close ("/bin/ls -1").


12. Inne

Komentarze oznaczamy przez wstawienie znaczka "#" na początku linii.
Polecenie systime () zwraca liczbę sekund które upłynęły od dnia 01.01.1970. Polecenie strftime ( <format> [ ,<czas> ] ), o ile <czas> nie został podany, zwraca aktualny czas w formacie takim jak podany (zgodnym z formatem polecenia systemowego "date". Jeśli chcemy podać <czas>, który ma zostać wyświetlony, musimy to zrobić w takiej postaci w jakiej zwraca ten czas polecenie systime.
Do uruchamiania poleceń systemowych (shella) służy instrukcja system ( <polecenie> ), która działa tak jak w C.


13. Wywoływanie AWKa

To już właściwie prawie koniec. Nasz program uruchamiamy poleceniem:

awk -f <plik z programem> <plik wejściowy 1> <plik wejściowy 2> ...

Jeśli chcemy uruchamiać nasz skrypt "z palca", bez ręcznego wywoływania AWKa, musimy wstawić jako pierwszą linijkę coś takiego:

#!/bin/awk -f

i nadać programowi prawa do uruchamiania (chmod u+x)
Można programu nie wpisywać w żaden plik, tylko podać go bezpośrednio w linii poleceń:

awk 'BEGIN { print "aaa" }' <plik1> <plik2> ...

Jeśli nie podamy żadnej nazwy pliku, rekordy będą wczytywane ze standardowego wejścia. Podobnie będzie, jeśli zamiast którejś nazwy pliku podamy "-".

I to tyle. Miłego AWKowania :)
Aha, najpopularniejszym użyciem AWKa jest coś takiego:

awk '{ print $1 }' <plik1> ...

co powoduje wypisanie pierwszego pola z każdego rekordu: np. pierwszego wyrazu.


Powrót do strony głównej