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
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.
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.
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")
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
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.
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.
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".
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
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ść>.
Ż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").
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.
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.