Copyright © 2000--2007 T. Przechlewski
Spis treści
System UNIX wyposażony jest w wiele narzędzi wspomagających pracę użytkowników. AWK jest jednym ze standardowych narzędzi tego systemu, choć implementacje AWK znaleźć można niemal na każdej platformie systemowej. Nazwa AWK pochodzi od inicjałów jego twórców: Alfreda V. Aho, Petera J. Weinbergera i Briana W. Kernighana.
W jednym zdaniu można powiedzieć, że AWK służy do transformacji danych tekstowych. Istotą działania AWK jest przetwarzanie pliku lub plików wejściowych według zadanego zbioru reguł, generując strumień danych wyjściowych, czy też plików wyjściowych.
Każdy program języka AWK składa się z dowolnej
liczby par (w przykładach programów fragmenty,
które oznaczają pewne pojęcia, a nie konkretne konstrukcje języka
oznaczono kursywą, np.:
oznacza każdą instrukcję AWK):
instrukcja
wzorzec{akcja}
jest wyrażeniem logicznym, które może być prawdziwe (wówczas
wykonywana jest Wzorzec)
lub fałszywe (akcja nie
jest wykonywana). Akcja jest zawsze zawarta
pomiędzy parą nawiasów akcja{ i }.
AWK może być wywołany na wiele sposobów. Jeżeli program jest krótki,
to najprościej jest umieścić go pomiędzy znakami
pojedynczego cudzysłowa w linii poleceń,
w następujący sposób (w systemach DOS/MS Windows zamiast cudzysłowa pojedynczego
należy użyć cudzysłowa maszynowego "):
awk '
program' plik1 plik2 ...
Kiedy program jest długi wygodniejsze jest jego umieszczenie
w oddzielnym pliku; w tym wypadku uruchomienie programu wygląda
następująco (
oznacza nazwę pliku zawierającego program):
program
awk -f
program plik1 plik2 ...
Program w języku AWK może zawierać wiele par
. AWK czyta po kolei
wiersze z wzorzec{
akcja },
pliku1 itd. dla
wszystkich plików, których nazwy podano w linii poleceń. Pliki te są
modyfikowane według programu z pliku
pliku2, tj. dla
każdego wiersza z każdego z plików wejściowych obliczane są kolejne
wzorce (w kolejności ich występowania w programie) i wykonywane
akcje. Przykład 1 (Usuwanie pustych wierszy) pokazuje program
wykorzystujący 2 wzorce. Uwaga: Przykładowe programy mogą zawierać
konstrukcje w danym momencie jeszcze nie omówione.
Jeżeli coś jest niezrozumiałe, czytaj dalej, a po lekturze całego
tekstu wróć do tego miejsca -- wszystko powinno być jasne.
Uwaga 2: Przedstawione przykłady programów są gotowe do uruchomienia
w systemach Unix/Linux natomiast w systemach DOS/MS Windows wymagają
czasami modyfikacji, np. zamiany znaków program' na
" czy zastąpienia skryptów shellowych
odpowiednimi plikami .bat.
Przed przedstawieniem bardziej szczegółowych informacji o języku wymienimy kilka podstawowych reguł składni AWK:
kolejne pary ,,'' muszą być oddzielone
średnikami lub znakami nowego wiersza;
wzorzec {
akcja }
akcje mogą składać się z wielu poleceń, które muszą być oddzielone średnikami lub znakami nowego wiersza;
wzorzec lub akcja może zostać pominięty. W wypadku braku
wzorca akcja zostaje wykonana dla każdego wiersza
pliku wejściowego. Jeżeli pominiemy akcję, to AWK zastosuje akcję
domyślną, jaką jest wydrukowanie wiersza z pliku wejściowego (czyli
{ print $0 } w składni AWK).
Przykład 1. Usuwanie pustych wierszy
Poniższy program przepisuje plik wejściowy zastępując kolejne puste wiersze, jednym pustym wierszem (autor: Nelson H. F. Beebe).
NF == 0 { nb++ }
NF > 0 { if (nb > 0) print ""; nb = 0; print $0; }
Ponieważ zarówno
jak i wzorzec są opcjonalne,
to stosowanie następującego stylu programowania:
akcja
wzorzec
{akcja }
jest błędem, gdyż powyższy zapis jest interpretowany jako dwie pary
wzorzec-akcja. Pierwszy wiersz jest interpretowany jako
nie posiadający
jawnie wyspecyfikowanej
wzorzec, co oznacza
wydrukowanie wszystkich wierszy z pliku wejściowego, dla których
wartością logiczną
akcji jest prawda.
Trzy następne wiersze są natomiast interpretowane jako
wzorca bez jawnie
podanego wzorca, co powoduje, że będzie ona wykonywana dla
każdego kolejnego wiersza z pliku wejściowego. Jeżeli ktoś
lubi tego typu styl pisania programów, to musi umieścić otwierający
nawias akcja{ w jednym wierszu z odpowiadającym mu
.
wzorcem
Jest wiele interpretatorów AWK. W systemach uniksopodobnych są dostarczane razem z systemem. Istnieją też wersje ogólnodostępne, takie jak: gawk -- firmowany przez Free Software Foundation czy mawk Michaela Brennana. Różne implementacje AWK nie są w 100% kompatybilne ze sobą a ponadto wiele z nich posiada rozszerzenia w stosunku do standardu (za standard przyjmujemy opis z [AhoetAll]). W niniejszym tekście przedstawiono standard AWK i rozszerzenia interpretatora gawk w wersji 3.*.
Doświadczenia autora wskazują, że komercyjne implementacje AWK są gorsze niż gawk. Przykładowo maksymalna liczba pól w rekordzie albo maksymalna długość rekordu w znakach może być śmiesznie mała (np. 99 pól w rekordzie). Z tego względu zachęcam do zainstalowania i posługiwania się gawk-iem także w systemach, w których znajduje się inna implementacja AWK. Zainstalowanie gawk-a ze źródeł jest w większości systemów uniksowych bardzo proste (naprawdę!). Oczywiście w skład systemu GNU/Linux standardowo wchodzi gawk zatem problem z głowy. Użytkownicy systemów DOS czy Microsoft Windows, których producent oczywiście nie dołącza AWK, muszą samodzielnie skopiować ,,z sieci'' i zaistalować gawk-a (lub mawka). Kopiujemy gotowe pliki wykonywalne ponieważ ich samodzielne kompilowanie nie jest prostą rzeczą w systemie DOS/MS Windows.
Dla AWK dane wejściowe składają się
z rekordów, które rozdzielone są separatorami
RS. Standardowo
rekordem jest cały wiersz, czyli separatorem jest
znak końca wiersza.
Rekordy podzielone są na pola, które rozdzielone
są separatorami pól FS. Domyślnie
separatorami pól są odstępy, tj. znaki spacji i/lub tabulacji.
Poniżej zamieszczono zawartość pliku wina.txt,
którego każdy wiersz zawiera: nazwę wina, symbol kraju-producenta,
kolor, smak, cenę w złotych oraz liczbę butelek sprzedanych, na
przykład w ostatnim miesiącu:
Chardonnay Lyngrove RPA białe wytrawne 95.00 131
Cabernet Sauvignon Merlot Lyngrove RPA czerwone wytrawne 95.00 58
Baron De France Fra białe wytrawne 25.00 289
Anjou Blanc Chenin Fra białe wytrawne 33.00 392
Anjou D'rose Fra różowe półwytrawne 33.00 207
Anjou Rouge Cabernet Fra czerwone półwytrawne 33.00 266
Bordeaux Graveschateau Saint Galier Fra białe wytrawne 89.00 144
Marquis De Chasse 1996 Fra czerwone wytrawne 55.00 229
Don Kichot Tinto Spa czerwone półwytrawne 25.00 360
Rioja Miralcampo Spa czerwone wytrawne 62.00 210
Rioja Miralcampo Spa białe wytrawne 62.00 179
Sherry Rich Cream Spa czerwone słodkie 109.00 55
Sole D'italia Ita czerwone wytrawne 25.00 666
Valpolicella Ita czerwone wytrawne 45.00 370
Chianti Villa Bellafonte Ita czerwone wytrawne 68.00 131
Asti Spumante Docg Ita białe półsłodkie 51.00 207
Moscato Spumante Vsq Ita białe słodkie 26.00 629
Ponieważ nazwa wina składa się z wielu wyrazów, liczba pól
w poszczególnych rekordach jest zmienna, np. pierwszy wiersz zawiera 7
pól, drugi 9 pól, trzeci 8 pól, itd.
RS
i FS są zmiennymi,
a więc można im nadać wartość. Przykładowo, jeśli zmiennej
FS nadamy wartość `;', to
separatorami pól będą znaki `;' a nie spacje
i tabulatory. Wartością zmiennej FS może być
dowolne wyrażenie regularne.
W akcjach i wzorcach do wartości pól można się odwoływać za pomocą
zmiennych postaci
$. Tak więc
nr-pola$1 to pierwsze pole rekordu, $2
drugie itd. $0 oznacza cały rekord. Wbudowana
zmienna NF przechowywuje
liczbę pól bieżącego rekordu, stąd $NF to zmienna
zawierająca zawartość ostatniego pola (w każdym rekordzie).
Przykładowo w pliku wina.txt zmienna
$NF zawiera wielkość sprzedaży każdego gatunku
wina. Dla pierwszego z win $1 zawiera napis
"Chardonnay" a $2 napis
"Lyngrove". Dla drugiego wiersza wartością
$1 będzie "Cabernet"
itd. Ponieważ liczba wyrazów w nazwie wina jest nieustalona, wydawać
by się mogło, że dotarcie do odpowiednich informacji w każdym wierszu
może być skomplikowane, ale tak nie jest, bo pola można także liczyć
od końca. Zapis $(NF-1) oznacza pole
przedostatnie, $(NF-2) drugie
od końca itd. Nawiasy okrągłe są tutaj obowiązkowe, a ich brak zmienia
znaczenie takiej konstrukcji, więcej szczegółów jest
w punkcie „Zmienne przechowujące zawartości pól”.
Plik wina.txt był jednorodny w tym sensie, że
każdy rekord (wiersz) zawierał informację o jednym gatunku wina.
Często można mieć do czynienia z plikami, które zawierają różne
rekordy, np. poniżej przedstawiono plik
tdf2000.txt zawierający zestawienie wszystkich
podjazdów o nachyleniu większym od 5% na alpejskich etapach
wyścigu Tour de France '2000:
**** Tour de France *** 1/07/2000 -- 23/07/2000 *** 3630km *****
Nazwa góry/przełęczy Dystans Początek Szczyt Różnica Długość
----------------------------------------------------------------
Etap 14: Draguignan--Briancon
Col d'Allos 127.5 1432 2250 818 13.4
Col de Vars 177.5 1401 2109 708 10.4
Col d'Izoard 249.5 1345 2361 1016 14.1
Etap 15: Briancon--Courchevel
Col du Galibier 33.0 2065 2645 580 8.4
Col de la Madeleine 110.5 456 2000 1544 19.3
Monte de Courchevel 173.5 602 2004 1402 17.3
Etap 16: Courchevel--Morzine
Col des Saisies 80.0 668 1650 982 15.1
Col des Aravis 106.5 973 1498 525 8.2
Col de la Colombiere 131.0 922 1618 696 11.6
Col de Chatillon 158.0 478 733 255 4.9
Col de Joux-Plane 196.5 692 1700 1008 12.0
W tym przypadku w pliku znajdują się następujące grupy rekordów: nagłówek (wiersze 1--3), określające etap, zawierające dane o każdej górze (nazwa, dystans od startu w kilometrach, wysokość w metrach n.p.m. podnóża i szczytu, różnica poziomów oraz długość podjazdu w kilometrach) i puste wiersze separujące.
Do plików wina.txt oraz tdf2000.txt
będziemy często wracać w przykładach zamieszczonych w dalszej
części tekstu.
Za pomocą mechanizmu znaków #! umieszczonych jako
dwa pierwsze znaki w pliku możliwe jest uruchamianie programów
AWK-owych, tak jakby były programami wykonywalnymi (w systemach
uniksowych, nie w DOS/MS Windows!). Jeżeli przykładowo umieścimy
w pliku pr10 następujący kod:
#!/usr/bin/awk -f NR <= 10
to, po nadaniu plikowi prawa do wykonywania (za pomocą
chmod), możemy uruchamiać program pisząc
po prostu:
pr10 .
(Program drukuje pierwsze 10 wierszy
plik.)
pliku
Wzorce służą do wyznaczenia tych wierszy tekstu, dla których wykonane
mają być odpowiednie akcje. W ogólnym wypadku wzorzec może być
kombinacją wyrażeń logicznych i wyrażeń regularnych. Ponieważ wzorce
są wyrażeniami logicznymi, dozwolone są operatory logiczne:
&&, ||,
! oraz nawiasy. Istnieją dwa specjalne wzorce
o nazwie BEGIN i END. Oto
ogólna specyfikacja wzorców:
Wzorce
BEGIN{akcja}
END{akcja}
jest wykonywana po zamknięciu pliku (plików) wejściowego.akcja
wyrażenie{akcja}
jest
wykonywana za każdym razem gdy wartość
akcja jest
równa prawda, tj. jest niezerowa (dla wyrażeń
numerycznych) lub niepusta (dla napisów).wyrażenia
/wyrażenie-regularne/{akcja}
jest
wykonywana za każdym razem gdy wiersz z pliku wejściowego zawiera
ciąg znaków pasujący do
akcja.wyrażenia-regularnego
wzorzec-złożony{akcja}
wzorzec złożony to kombinacja
logiczna dowolnych warunków. Można stosować operatory
&& (koniunkcja), ||
(alternatywa), ! (negacja) oraz
nawiasy. Por. punkt „Wzorzec złożony”.
wzorzec1,
wzorzec2{akcja}
jest wykonywana dla wszystkich wierszy od wiersza
zawierającego
akcja do wiersza
zawierającego
wzorzec1 (łącznie
z tymi wierszami).
wzorzec2 oznacza
Wzorzec bądź
wyrażenie.
wyrażenie-regularne
Wzorce BEGIN i END nie mogą
być częścią wzorca złożonego. Podobnie częścią wzorca złożonego
nie może być wzorzec z przecinkiem.
Wzorzec BEGIN nie
pasuje do żadnego wiersza z pliku wejściowego, a odpowiadająca mu
akcja jest wykonywana przed przeczytaniem przez AWK pierwszego
znaku z tego pliku. Podobnie instrukcje wzorca
END wykonywane
są po przeczytaniu wszystkich znaków pliku wejściowego. Możliwe
jest umieszczenie wielu wzorców BEGIN
i END w programie AWK-owym; są one wtedy
wykonywane po kolei. Zwyczajowo wzorce BEGIN
są umieszczane na początku a END na końcu
pliku.
Jednym z najczęstszych sposobów użycia BEGIN jest
zmiana domyślnego sposobu w jaki AWK dzieli wiersze z czytanego
pliku na pola. Wbudowana zmienna FS definiuje
napis-separator pól w rekordzie. Domyślnie pola oddzielone są znakami
spacji lub/i tabulacji (FS=" "). Przy uruchomieniu
programu zawierającego tylko wzorce BEGIN AWK nie
oczekuje w linii poleceń nazwy żadnego pliku wejściowego,
por. przykład 17 (Wyświetlanie dużych plików).
Wzorcami mogą być wyrażenia arytmetyczne lub napisowe. Odpowiednia
jest wykonywana
za każdym razem gdy takie
akcja ma wartość
różną od zera lub od napisu pustego. Przykładowo:
wyrażenie
$(NF-4) == "Fra" { print $0 } # Wydrukuj wina francuskie
w powyższym wierszu, który jest kompletnym programem AWK-owym,
wyrażeniem we wzorcu jest porównanie czwartego pola od końca wiersza
z napisem "Fra". Jeżeli napisy są identyczne to
wartością wyrażenia jest prawda i AWK wykonuje akcję -- drukuje cały
wiersz. Zgodnie z tym co już powiedziano, tego typu akcja jest
wykonywana domyśnie -- jeżeli nie podano innej, zatem program
można zapisać jeszcze krócej:
$(NF-4) == "Fra" # Wydrukuj wina francuskie
Przykładem wzorca zawierającego wyrażenie arytmetyczne może być wydrukowanie tych gatunków win, na których obrót był większy od 10 tys. zł:
$NF * $(NF-1) > 10000 # Wydrukuj najczęściej kupowane
Wzorzec regularny to wyrażenie
regularne ujęte w parę znaków /. Podstawowe
sposoby użycia wzorca regularnego to:
/r/
Pasuje do bieżącego wiersza z pliku wejściowego
jeżeli zawiera ona podnapis pasujący do wyrażenia regularnego
.
r
wyrażenie ~ /r/
Pasuje do napisu będącego wartością
jeżeli
zawiera on podnapis pasujący do wyrażenia regularnego
wyrażenia. Zapis
r/ jest równoważny
formie r/$0 ~ /.
r/
wyrażenie !~
/r/
Pasuje do napisu będącego wartością
jeżeli
nie zawiera on podnapisu pasującego do
wyrażenia regularnego
wyrażenia.
r
Wyrażenia regularne mogą być pomocne w rozwiązaniu problemu wydrukowania win wytrawnych i półwytrawnych:
$(NF-2) ~ /wytrawne/ # wydrukuj coś wytrawnego
AWK wydrukuje każdy wiersz, którego trzecie od końca pole zawiera
napis wytrawne. W tym przykładzie zysk jest
niewielki, zamiast $(NF-2) ~/wytrawne/ można
zapisać:
$(NF-2) == "wytrawne" # wydrukuj wytrawne $(NF-2) == "półwytrawne" # ... i półwytrawne
ale w ogólnym przypadku wyrażenia regularne potrafią znakomicie ułatwić pracę.
Wzorzec złożony to wyrażenie złożone
z wzorców i operatorów logicznych ||,
&&, !. Wzorzec złożony
pasuje do bieżącego wiersza z pliku wejściowego jeżeli wartością
wyrażenia jest prawda (czyli jest niezerowa lub
niepusta). Poniższy przykład:
$(NF-2) == "wytrawne" || $(NF-2) == "półwytrawne" # coś wytrawnego
pokazuje wzorzec złożony i jednocześnie potwierdza, że najlepiej do rozwiązania problemu drukowania win wytrawnych korzystać z wyrażeń regularnych.
Inny przykład wzorca złożonego pozwoli nam rozwiązać problem wydrukowania win francuskich, na których obrót był większy od 10 tys. zł:
$(NF-4) == "Fra" && $(NF-1) * $NF > 10000
Pasuje do wszystkich wierszy, od wiersza pasującego do
do wiersza
pasującego do wzorca1
(łącznie z tymi wierszami). Jeżeli w pliku po raz kolejny pojawi się
wzorca2, to znowu
pasują wszystkie wiersze aż do napotkania
wzorzec1. Jeżeli AWK
nie znajdzie wzorca2, to
pasują wszystkie wiersze aż do końca pliku. Jeżeli w pliku nie ma
wzorca2, to nie pasuje
żaden wiersz z tego pliku. Przykładowo wykonanie poniższego programu
spowoduje wydrukowanie wierszy o numerach od 4 do 14 z pliku
wzorca1tdf2000.txt:
$3 ~ /Briancon/, $3 ~ /Morzine/
Jeżeli wykonamy następujący program:
$3 ~ /Draguignan/, $3 ~ /Briancon/
to powstaje pytanie czy na wydruku otrzymamy tylko
jeden wiersz (zawiera
"Draguignan" i jednocześnie zawiera
"Briancon") czy też sześć
wierszy (od 4 do 9; dziewiąty też zawiera słowo
"Briancon") z pliku
tdf2000.txt? Otóż AWK po sprawdzeniu, że
wiersz pasuje do
sprawdza ten sam wiersz, czy aby nie pasuje on do
wzorca1, co powoduje, że
w takim wypadku drukowany jest tylko ten wiersz. Gdyby AWK działał
tak jak program sed, tj. po dopasowaniu
wiersza do wzorca2,
dopasowywał wzorca1 do
następnego i kolejnych wierszy wtedy na wydruku pojawiłoby się
6 wierszy.
wzorzec2
Wyrażenia regularne to wyrażenia umożliwiające specyfikowanie
klas napisów. O napisie należącym do tej klasy
mówimy, że pasuje do wyrażenia regularnego.
Wyrażenia regularne są konstruowane z następujących elementów:
,,normalnych znaków'' (wszystkie litery, cyfry, większość pozostałych
znaków) oraz metaznaków \, ^,
$, ., [,
], |, (,
), *, +,
?. Poniższa tabela przedstawia poszczególne
elementy wyrażeń regularnych, według malejącej kolejności wykonywania:
| Wyrażenie | Znaczenie |
|---|---|
( |
(nawiasy służą do grupowania wyrażeń) |
c |
znak nie będący metaznakiem |
\c |
znak sterujący albo znak/metaznak c |
^ |
początek napisu |
$ |
koniec napisu |
. |
dowolny znak |
[ab...]
|
dowolny ze znaków a,
b... |
[^ab...] |
dowolny ze znaków oprócz a,
b... |
|
zero lub więcej powtórzeń |
|
jedno lub więcej powtórzenie |
|
zero lub jedno powtórzenie
|
|
lub
( oznacza wyrażenie regularne)
|
Do grupy znaków wewnątrz nawiasów klamrowych pasuje jeden dowolny znak
z tej grupy. Wewnątrz nawiasów klamrowych
wszystkie znaki oprócz \,
- i ^ tracą swoje
metaznaczenie. Przykładowo: [...] oznacza trzy
kropki a nie trzy dowolne znaki.
Zapis [a-z] oznacza zakres czyli
jeden znak od a do z, przy czym
obowiązuje kolejności kodów ASCII. Zatem specyfikacja
[0-9] jest równoważna
[0123456789], zaś [A-Da-d]
oznacza [ABCDabcd]. Jeżeli znak
- jest pierwszym znakiem w grupie, wtedy jest
traktowany literalnie, tj. [-+] oznacza albo minus
albo plus podczas gdy [+-] jest błędem -- AWK
oczekuje końca zakresu znaków.
Dopełnieniem grupy lub zakresu znaków jest grupa lub zakres
poprzedzona znakiem ^ (bezpośrednio po otwierającym
nawiasie [). Przykładowo specyfikacja
[^0-9] oznacza jeden dowolny znak ale
nie cyfrę; [^A-ZĄĆĘŁŃÓŚŹŻ] dowolny znak
nie będący dużą literą. Znak ^ jest traktowany
literalnie jeżeli nie rozpoczyna grupy. Na przykład
^[^^] pasuje do każdego znaku
oprócz znaku ^ na początku
napisu.
Nawiasy okrągłe służą do grupowania i -- podobnie jak w wyrażeniach arytmetycznych -- posiadają najwyższy priorytet wykonania. Przykładowo:
/(Ali|ali)(baba|gator)/
pasuje do następujących napisów:
"Alibaba", "Aligator",
"alibaba", oraz "aligator".
Zwróćmy uwagę, że ponieważ operator | ma najniższy
priorytet wykonywania możemy pisać (Ali|ali) a nie
((Ali)|(ali)).
Znaki sterujące, zapisujemy w konwencji języka C. Są to:
\a (dzwonek, alarm),
\b (znak cofnięcia,
backspace), \f (znak końca
strony, form feed),
\n (przejście do nowego wiersza, new
line), \r (carriage
return), \t (znak tabulacji). Ponadto
znak \\ oznacza \, zaś każdy
znak możemy zapisać przy pomocy kodu ósemkowego używając konwencji
\.
cyfracyfracyfra
Przykład 2. Przykłady wyrażeń regularnych
Do wyrażenia regularnego /^[ \t]*$/ pasują
wszystkie napisy składające się tylko ze znaków spacji, tabulacji
i napisu pustego. Do /^[^ \t]*$/ pasują wszystkie
napisy oprócz składających się ze spacji, znaków tabulacji i pustych.
Z kolei do /[+-]?[0-9]+[.]?[0-9]*/ pasują wszystkie
liczby rzeczywiste ze znakiem.
AWK zawsze dopasowuje do wyrażenia regularnego
najdłuższy z możliwych napisów, rozpoczynając dopasowywanie od lewej
strony, tak szybko jak to jest możliwe. Poszczególne operatory
powtórzeń (+, *,
?) dopasowują napis tak długo jak to jest możliwe.
Przykładowo niech plik zawiera następujący krótki tekst:
<tr align="left"><td>Chardonnay Lyngrove</td><td>RPA</td></tr>
Jaki napis zostanie dopasowany do wyrażenia regularnego
/<.+>/? Na pierwszy rzut oka mogłoby się
wydawać, że <tr align="left">, ale nie jest
to prawdą: dopasowany zostanie cały wiersz, gdyż jest to najdłuższy
z możliwych pasujących napisów zaczynających się od
< a kończących się na >.
Rozpatrzmy kolejny przykład. Jaki napis zostanie dopasowany do
wyrażenia: /C*/? Odpowiedź, że napis
rozpoczynający się od C w słowie
Chardonnay do końca wiersza jest błędna.
AWK dopasuje się jedynie do napisu o zerowej długości na początku wiersza.
Tak się stanie ponieważ 0 powtórzeń C jest
poprawnym punktem startu a kolejny znak nie jest już dużą literą
C -- dopasowywanie jest kończone
z wynikiem 0 C.
Przykład 3. Wysokie góry
Rozważmy z kolei jak można z pliku tdf2000.txt
wydrukować szczyty o różnicy poziomów większej od 1 km. Nie można po
prostu napisać $(NF-1) > 1000 ponieważ nie
wszystkie rekordy zawierają w przedostatnim polu różnicę poziomów. Na
przykład w pierwszym wierszu przez zupełny przypadek
$(NF-1) jest równe 3630. Zresztą napotkanie
pustego wiersza spowoduje błąd fatalny i przerwanie wykonywania
programu ponieważ w AWK nie wolno
odwoływać się do ujemnych numerów pól. Oczywistym rozwiązaniem jest
wyspecyfikowanie akcji dla każdego rodzaju rekordu: nagłówka, nagłówka
etapu, góry i pustego:
NR==1, /^-+[ \t]*$/ { next } # Pomiń nagłówek,
NF < 1 { next } # puste wiersze
/^Etap[ \t]+[0-9]+:/ { next } # oraz nagłówki etapów
$(NF-1) > 1000 # Wydrukuj podjazd o różnicy > 1000
Wykorzystana w powyższym programie, jeszcze nie omawiana, instrukcja
next
(por. punkt „Instrukcje sterujące”) powoduje: przerwanie
wykonywania programu, wczytaniu następnego rekordu ze strumienia
danych wejściowych a następnie rozpoczęcie wykonywania programu od
początku, tj. od pierwszej pary wzorzec-akcja.
Kluczowe znaczenie w powyższym programie ma kolejność poszczególnych
par wzorzec-akcja, dzięki której wzorzec
$(NF-1) > 1000 ,,widzi'' tylko wiersze z danymi
o górach, pozostałe zaś są po drodze ,,odcedzane''.
Zwykle wyrażenia regularne zapisywane są jako ciągi znaków
umieszczone pomiędzy znakami ciachów. Możliwe jest też używanie
napisów jako wyrażeń regularnych. Wszędzie tam gdzie AWK
oczekuje pojawienia się wyrażenia regularnego (jak, np. po prawej
stronie operatorów ~ i !~)
umieszczone tam wyrażenie zostanie przekształcone a następnie
zamienione na napis, który będzie interpretowany jako
wyrażenie regularne. Przykładowo, poniższy program:
BEGIN {cyfry="^[0-9]+$" }; $0 ~cyfry
wydrukuje wszystkie wiersze, które zawierają wyłącznie liczbę całkowitą (bez znaku).
Ponieważ wyrażenia napisowe mogą być łączone (por. punkt „Operatory napisowe”), wyrażenie regularne może być konstruowane dynamicznie z części składowych. Przykładowo poniższy program:
BEGIN {znak ="[-+]?"; cyfra="[0-9]+"; liczba = "^" znak cyfra "$"}
$0 ~ liczba
może służyć do wydrukowane wszystkich wierszy z pliku, które zawierają wyłącznie liczbę całkowitą ze znakiem:
Podstawą składni wyrażeń AWK jest składnia wyrażeń języka C wzbogacona o operacje tekstowe. Elementami wyrażeń są: stałe, zmienne, operatory, funkcje wbudowane i definiowane przez użytkownika oraz elementy tablic asocjacyjnych.
W AWK istnieją tylko dwa typy danych:
liczbowy i napisowy. Stałe
liczbowe zapisujemy jak w C, tj. 3.1415 lub
1.333e-5, stałe napisowe otacza się
znakami ". Stałe napisowe mogą zawierać znaki
sterujące takie jak \n czy
\f.
W składni AWK wyróżniamy zmienne: wbudowane, zdefiniowane przez
użytkownika i przechowujące zawartości pól. Nazwy zmiennych
definiowanych przez użytkownika mogą składać się z liter, cyfr i znaku
podkreślenia. Pierwszym znakiem nazwy nie może
być cyfra. Nazwy zmiennych wbudowanych składają się
wyłącznie z dużych liter alfabetu. Nazwy zmiennych przechowujących
zawartości pól zaczynają się od znaku $, po którym
występuje liczba (ogólnie: wyrażenie).
Zmienne liczbowe przechowują wartości zmiennopozycyjne, przy czym ich
dokładność zależna jest od implementacji. Zmienne napisowe
przechowują ciągi znaków (napisy). Zmiennych nie deklaruje się. Typ
zmiennej określony jest przez kontekst; w razie potrzeby zawsze
dokonywana jest odpowiednia konwersja. Zmienna nie zainicjowana ma
wartość zero lub "" (napis pusty).
Interpretacja wyrażeń numerycznych i tekstowych w operacjach
logicznych jest następująca: fałsz odpowiada
liczbie 0 i napisowi pustemu "", zaś
prawda odpowiada wszystkim innym liczbom
i napisom.
Zmienne wbudowane są dokładnie opisane przy okazji omawiania tych aspektów AWK, których dotyczą:
| Zmienna | Opis znaczenia |
|---|---|
ARGC |
liczba argumentów wywołania programu |
ARGV |
tablica argumentów wywołania programu |
ARGIND |
indeks w ARGV odpowiadający
bieżącemu plikowi |
ENVIRON |
tablica zmiennych środowiskowych |
ERRNO |
napis z systemowym opisem błędu |
FIELDWIDTHS
|
specyfikacja długości pól, por. punkt „Pola o ustalonej długości” |
FILENAME |
nazwa bieżącego pliku wejściowego |
FNR |
numer bieżącego rekordu w bieżącym pliku |
FS |
separator pól |
IGNORECASE |
przełącznik rozróżniania wysokości liter |
NF
|
liczba pól w bieżącym rekordzie |
NR |
liczba przeczytanych rekordów |
OFMT |
format wydruku argumentów numerycznych funkcji print |
OFS |
separator pól na wyjściu, por. punkt „Instrukcja print” |
ORS |
separator rekordów na wyjściu, por. punkt „Instrukcja print” |
RLENGTH |
por. opis funkcji match
w punkcie „Napisowe funkcje wbudowane” |
RS |
separator rekordów |
RT |
napis pasujący do wyrażenia RS,
por. punkt „Rekordy” |
RSTART |
por. opis funkcji match
w punkcie „Napisowe funkcje wbudowane” |
SUBSEP |
separator indeksów tablic, por. punkt „Tablice wielowymiarowe” |
ENVIRON jest
tablicą zawierającą wartości zmiennych środowiskowych przy czym
indeksami są nazwy zmiennych. Przykładowo:
gawk 'BEGIN {print ENVIRON["HOME"] }'
zawiera np. /home/tomek.
ERRNO zawiera napis
z systemowym komunikatem o błędzie, jeżeli przy wykonaniu
funkcji getline lub
close wystąpi błąd.
IGNORECASE określa czy
AWK rozróżnia duże i małe litery przy porównywaniu napisów i wyrażeń
regularnych. Jeżeli IGNORECASE jest
niezerowe lub niepuste, wtedy operatory ~,
!~ i funkcje gensub,
gsub,
index,
match,
split oraz
sub nie
rozróżniają dużych i małych liter. Dotyczy to także wartości zmiennych
RS
i FS. Począwszy od
wersji 3.0, gawk obsługuje normę
ISO-8859-1
(Latin-1). Standard ten nie zawiera jednak
większości polskich znaków diakrytycznych.
ARGIND przechowuje
indeks, pod którym w tablicy ARGV znajduje się
nazwa przeglądanego pliku. Zawsze jest prawdziwa równość
FILENAME == ARGV[ARGIND].
Zmienna FILENAME zawiera nazwę bieżącego pliku
wejściowego. Oznacza to, że w obrębie wzorców
BEGIN i END wartość
FILENAME jest
nieokreślona.
Zmienne przechowujące zawartości pól mogą być wykorzystane wewnątrz
wyrażeń, można także im nadawać wartości. Jeżeli wartość zmiennej
$0 została zmodyfikowana przez podstawienie lub
zamianę (np. funkcją sub) to wartości
zmiennych $1, $2, ... oraz
zmiennej NF są powtórnie
wyznaczane. Podobnie jeżeli zmodyfikowano którąkolwiek ze zmiennych
$1, $2, ..., wtedy wartość
zmiennej $0 jest odtwarzana (przy wykorzystaniu
wartości zmiennej OFS jako separatora pól).
Numery pól mogą być wyznaczane jako wartości wyrażeń. Przykładowo
$(NF-1) oznacza przedostatnie pole w bieżącym
rekordzie (nawiasy są istotne, ponieważ $NF-1
oznacza wartość pola $NF pomniejszoną o 1). Możliwe
jest także przypisanie wartości zmiennym odpowiadającym polom nie
istniejącym w czytanym pliku. W takim wypadku pole zostanie
utworzone i przypisana zostanie odpowiednia wartość. Odwołanie się do
pola o numerze ujemnym jest błędem kończącym działanie programu.
Poniższa tabela zawiera zestawienie wszystkich operatorów arytmetycznych.
Operator modulo to reszta z dzielenia całkowitego,
tj. 5.1 % 3 == 2.1 ma wartość prawda.
Napisy i zmienne napisowe można łączyć (konkatenować) przy pomocy ,,niewidocznego'' operatora -- po prostu należy umieścić napisy obok siebie. Przykładowo po wykonaniu:
y = "Ali"; z = "gator"; x1 = y "ba" "ba"; x2= y z;
zmienna x1 ma wartość
,,Alibaba''; zmienna x2 ma wartość
,,Aligator''. Oprócz operacji konkatenacji
AWK nie ma żadnych innych operatorów napisowych.
Zapis i działanie operatorów w AWK w wypadku zmiennych typu liczbowego jest identyczny jak w języku C. Nowością AWK jest to, że mogą być także stosowane do napisów.
Tabela 4. Operatory porównywania
| Operator | Opis znaczenia |
|---|---|
==
|
równe |
!= |
różne |
< |
mniejsze |
<= |
mniejsze lub równe |
> |
większe |
>= |
większe lub równe |
Napisy są porównywane według kodów ASCII w taki sposób, że najpierw
porównywane są pierwsze znaki, potem drugie itd. Przykładowo:
"10" jest mniejsze
od "9". Jeżeli jeden napis jest przedrostkiem
drugiego to krótszy napis jest mniejszy od dłuższego,
np. "Ali" jest mniejsze od
"Alibaba".
Przykład 4. Wyznaczanie roku przestępnego
Zaimplementujmy funkcję, zwracającą 1 jeżeli rok jest przestępny, lub 0 dla lat nieprzestępnych. Algorytm cytujemy za [KR88], s. 121.
function leapyear(year) {
return year %4 == 0 && year % 100 \
!= 0 || year%400 ==0; }
BEGIN {print leapyear(1996), leapyear(1806),
leapyear(1066)}
Wzorzec BEGIN jest potrzebny tylko dla testowania
funkcji. Jeżeli powyższy kod umieścimy, np. w pliku
lyear.awk to pisząc awk -f
lyear.awk otrzymamy na ekranie: 1 0 0.
Bardzo długa instrukcja może zostać podzielona i zapisana w kilku
wierszach. Znakiem kontynuacji jest \,
bezpośrednio przed znakiem końca wiersza (por. drugi wiersz
przykładu). Jeżeli wiersz kończy się przecinkiem (por. wiersz
przedostatni) to znak kontynuacji jest opcjonalny.
W składni AWK są dwa takie operatory ~ oraz
!~. Umożliwają one dopasowanie zmiennej do
wyrażenia regularnego. Przykładowo $1 ~/Chardonnay/
jest prawdziwe, gdy pierwsze pole zawiera napis
Chardonnay. Samotnie pojawiające się
wyrażenie /Chardonnay/ jest równoważne
$0 ~ /Chardonnay/. Operator
!~ pasuje do dopełnienia wyrażenia
regularnego, tj. $1 !~/Chardonnay/ jest prawdziwe
gdy $1 nie zawiera napisu
Chardonnay.
Przypisanie oznaczane jest w AWK pojedynczym znakiem
równości =. Podobnie jak w języku C operator ten
nadaje zmiennej wartość i zwraca przypisaną wartość, stąd dozwolone
są wyrażenia postaci x = y = 1 lub
(x = y) <= 1.
Z operatorem przypisania związane są operatory modyfikacji:
+=, -=, *=,
/=, %=, /=
i ^=. Przykładowo wyrażenie x +=
y jest tożsame z x = x + y, wyrażenie
x -= y jest tożsame
z x = x - y itd.
Przykład 5. Wykorzystanie AWK do obliczeń
Dla danych z pliku wina.txt, obliczyć obrót dla win
białych i czerwonych oraz obrót łączny. Zadanie to rozwiązuje
następujący program:
#!/usr/bin/awk -f
{obrot = $NF * $(NF-1); oo += obrot }
$(NF - 3) ~ /białe/ {biale += obrot }
$(NF - 3) ~ /czerwone/ {czerwone += obrot }
END { print "Obrót razem:", oo ", w tym: czerwone:",
czerwone ", białe:", biale "."; }
Zwróćmy uwagę na instrukcję print. Część argumentów jest oddzielona przecinkiem a część nie. Jest to dopuszczalne ponieważ argumenty oddzielone odstępami są konkatenowane (liczby przed konkatenacją są konwertowane do napisów) i ,,z punktu widzenia'' instrukcji print stanowią jeden napis. Natomiast argumenty oddzielone przecinkami są na wydruku oddzielone odstępem.
Operator warunkowy ?: posiada następującą składnię:
wyrażenie1?wyrażenie2:wyrażenie3
Najpierw obliczane jest
. Jeśli jest
ono prawdziwe obliczane jest
wyrażenie1,
w przeciwnym wypadku
wyrażenie2.
wyrażenie3
Poniższy program oblicza i drukuje odwrotność pierwszych pól
wszystkich rekordów, sprawdzając czy $1 nie jest
równe zeru:
{print $1!=0 ? 1/$1 : "Zero w wierszu", NR;}
AWK posiada inny zestaw funkcji wbudowanych niż język C. Funkcje
wbudowane mogą być, bez żadnych ograniczeń, elementami wyrażeń. Oto
lista takich funkcji (niech
,
x będą pewnymi
wyrażeniami):
y
Tabela 6. Funkcje arytmetyczne
Używając powyższych funkcji można uzyskać użyteczne liczby, na przykład
pi lub e: atan2(0,-1) = pi
oraz exp(1) = e.
Również uzyskanie
logarytmu dziesiętnego nie jest problemem, jeśli zastosujemy wzór
log(x)/log(10).
Natomiast poprzez podstawienie
randint = int(n * rand()) + 1
nadajemy zmiennej randint
wartość pseudolosową z przedziału
<1,x>.
Poniższe zestawienie zawiera funkcje AWK umożliwiające
manipulowanie napisami. W zestawieniu
oznacza wyrażenie
regularne, r
i s napis.
t
Funkcje napisowe
gsub(r, ,s, ,t)
Zamienia wszystkie napisy pasujące do wyrażenia regularnego
na napis
r w napisie
s.
Zwracana jest liczba zamian.
Parametry t
i r mogą być w ogólności
wyrażeniami; paramter s
musi być zmienną lub elementem tablicy, tak aby AWK mógł
gdzieś przypisać zmodyfikowaną wartość.
Jeżeli tgsub wywołamy tylko z dwoma
pierwszymi parametrami, to zmiany dokonywane są w napisie
$0 (tj. gsub(
jest równoważne
r, ,s)gsub().
Znak r, ,s,$0)& w
oznacza napis, który został dopasowany do
s.
Przykładowo po uruchomieniu:
r
awk 'BEGIN {$0 = "baba bababa"; gsub(/(ba)+/,"\\\\&\\&&"); print }'
zostanie wydrukowany napis
"\baba&baba \bababa&bababa".
Ponieważ wewnątrz napisów znak
\ musi być zdublowany żeby AWK traktował
go literalnie, dlatego "\\&" oznacza
&. Zaś wstawienie \
osiągniemy za
pomocą "\\\\" (cztery \!).
gensub(r, ,s, ,a, ,t)
Uogólniona funkcja gsub. Zwraca zmieniony
napis (nie modyfikuje oryginalnego napisu
!). Zamienia napisy
pasujące do wyrażenia regularnego
t na
r w oparciu
o s,
w a (jeżeli nie ma
t, domyślnym argumentem
jest t$0). Argumenty
,
s oraz
a są napisami. Argument
t określa,
który z kolei podnapis pasujący do wyrażenia
a ma być
wymieniony. Jeżeli r
jest napisem rozpoczynającym się od a"g" (lub
"G") to wymieniane są
wszystkie napisy pasujące do
. Funkcja rgensub umożliwia wstawienie
do fragmentów napisu
dopasowanego do s.
Jeżeli wyrażenie r
podzielimy za pomocą nawiasów, r(
i ) na części składowe to te składowe mogą później
pojawić się w
(oznaczamy je jako s\,
gdzie n jest cyfrą od
1 do 9). Znaczenie tego jest takie, że napis dopasowany do
n-tego fragmentu jest
kopiowany do napisu zwracanego przez funkcję. W efekcie możliwe są
wszelkiego rodzaju zmiany kontekstowe,
por. przykład 8 (Funkcja ngensub). Symbol
\0 oznacza napis dopasowany do całego wyrażenia
regularnego (to samo
znaczenie ma r&). Przykładowo
wykonanie programu:
awk 'BEGIN {$0 = "12, 122, 1,901, 20,500, 102,153,000";
$0 = gensub(/([0-9]),([0-9])/, "\\1\\2","g"); print }'
spowoduje wydrukowanie napisu
"12, 122, 1901, 20500, 102153000".
Znaki &
i \ są wewnątrz
specjalne; ich
znaczenie podano w opisie funkcji sgsub.
W szczególności należy używać sekwencji \\ do
wstawienia w napisie znaku \ literalnie.
Poniższy przykład wyjaśnia znaczenie argumentu
:
a
BEGIN{ t = "Alibababa"; print gensub(/ba/, "BA", 2, t) }
otrzymamy: AlibaBAba
index(s, ,t)
Zwraca numer pierwszego znaku napisu
w napisie
t. Jeżeli
s nie zawiera
s zwracana jest
wartość zero. Pierwszy znak w napisie ma numer 1. Przykładowo:
tindex("Alibaba", "baba") zwraca 4;
length(s)
Podaje długość napisu
.s
match(s, ,r)
Jeżeli
zawiera podnapis
pasujący do s, to zwraca
numer pierwszego znaku tego podnapisu; w przeciwnym razie zwracane
jest r0. Ponadto nadawane są wartości zmiennym
RSTART oraz RLENGTH.
RSTART jest równe wartości
zwracanej przez funkcję, RLENGTH jest równe
długości podnapisu pasującego do
.
r
split(s, ,a, ,fs)
Z napisu tworzy
tablicę napisów s
w oparciu o a.
Argument fs jest
wyrażeniem regularnym. Jeżeli fssplit wywołamy
tylko z dwoma parametrami to napisem separujący jest wartość zmiennej
FS, czyli separator pól w rekordzie.
sprintf(format, ,lista-wyrażeń)
Zwraca napis, sformatowany według napisu
, por. funkcja
printf, w punkcie „Instrukcje wyjścia --
print/printf”.
format
sub(r, ,s, ,t)
Funkcja działająca jak gsub, ale wymieniająca
tylko pierwsze wystąpienie napisu pasującego do
na napis
r.
Zwracana jest liczba zamian.
s
substr(s, ,p, ,n)
Zwraca napis wycięty z
począwszy od pozycji s
o długości p znaków (lub
do końca n, jeżeli
ostatni argument jest pominięty). Przykładowo wykonanie instrukcji:
s
print substr("Alibaba",4);
spowoduje wydrukowanie napisu "baba".
tolower(s)
Zwraca napis, w którym duże litery zostały zamienione na małe.
W wypadku polskich tekstów funkcja ta ma ograniczone
zastosowanie, nie zamieni bowiem liter z górnej połówki tabeli
ASCII, gdzie znajdują się Ą,
Ć, Ę, itd.
toupper(s)
Zwraca napis, w którym małe litery zostały zamienione na
duże. Z ,,polskiego'' punktu widzenia ma tę samą wadę co
tolower.
Przykład 6. Zamiana końców wiersza
Problemem przy wymianie danych pomiędzy DOS-em a Uniksem jest stosowanie innych sekwencji znaków do oznaczania końca wiersza. Poniższy program dokonuje odpowiedniej konwersji:
#!/bin/bash
cat $* | awk '{gsub(/\r/,""); print $0}'
Wiele uniksowych implementacji AWK źle interpretuje poprawne
programy w przypadku gdy przetwarzany plik ma DOS-owe końce
wiersza. Na taką okoliczność warto zapamiętać powyższą funkcję
gsub.
Przykład 7. Zamiana kontekstowa
Poniższa funkcja (porównaj punkt Funkcje dalej w tekście) realizuje kontekstową zamianę frazy na frazę. Jest namiastką tego czego AWK-owi do tej pory brakowało (por. następny przykład) -- zamiany wyrażenia regularnego na wyrażenie regularne.
# wymień wscow kontekściebefaftnanafunction exch (bef, co, aft, na, s) { while (match(s, bef co aft) > 0) { match(s, bef co aft); s = substr(s, 1, RSTART) na substr(s, RSTART+RLENGTH-1); } return s; }
Zakładamy, że
jest jednym znakiem i poprzedza
bef.
Następujące po
coco też jest
jednym znakiem.
Przykładowo uruchomienie programu:
aft
{ s = exch("[0-9]","-","[0-9]", "--", $0); print s }
z podaniem jako argumentu pliku kloss.txt:
Działalność agenta J-23 (Hans-Peter Kloss) w latach 1941-1945
opisano na stronach 1234-1239.
wymieni w nim wszystkie frazy
na
cyfra-cyfra,
pozostałe znaki ,,cyfra--cyfra-'' zostaną niezmienione.
Przykład 8. Funkcja gensub
Poniższy program wykorzystujący funkcję gensub:
{ $0=gensub(/([0-9])-([0-9])/,"\\1--\\2","g",$0); print }
działa identycznie jak funkcja exch, czyli zamienia w całym tekście
wszystkie frazy
na
cyfra-cyfra,
np. cyfra--cyfra1234-1239 zmieni na
1234--1239.
Przykład 9. Zamiana małych liter na duże
Poniższa funkcja jest odpowiednikiem funkcji
toupper; ma tę zaletę, że ,,rozpoznaje''
polskie znaki. Łatwo też daje się modyfikować dla różnych
wariantów kodowania polskich znaków.
function upper(string, i, j){
newstring = ""
for (i = 1; i <= length(string); i++){
char = substr(string, i, 1)
for (j = 1; j <= ALPHABET; j++){
if (char == little[j]){
char = big[j]; j = ALPHABET + 1; }
}
newstring = newstring char;
}
return newstring
}
BEGIN{ LOWER = "abcdefghijklmnopqrstuvwxyząćęłńóśźż";
UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZĄĆĘŁŃÓŚŹŻ";
ALPHABET = length(LOWER);
for (i = 1; i <= ALPHABET; i++){
little[i] = substr(LOWER,i,1)
big[i] = substr(UPPER,i,1) } }
Dodajmy jeszcze następujący wzorzec BEGIN:
BEGIN { print upper("Pod źdźbŁEm Żółw spał śnięTY."); }
Po uruchomieniu otrzymamy na ekranie:
POD ŹDŹBŁEM ŻÓŁW SPAŁ ŚNIĘTY.
Interpretator gawk od wersji 3.0 posiada dwie funkcje dotyczące daty
i czasu. Są to systime
i strftime:
systime()
Zwraca bieżący czas w sekundach jakie upłynęły od początku epoki. W standardzie POSIX jest to liczba sekund od 1 Stycznia 1970 r.
strftime(format,czas)
Zwraca napis
zawierający (liczba
w takim samym formacie jak wartość zwracana przez czassystime) sformatowany według specyfikacji
z napisu . Forma
krótka formatstrftime() oznacza użycie
formatu "%a %b %d %H:%M:%S %Z %Y" oraz bieżącego
czasu. Forma
strftime(
wypisuje bieżący czas według specyfikacji
z format).
formatu
Specyfikacje przekształceń funkcji strftime są
zgodne ze standardem ANSI C. Każda specyfikacja składa się ze znaku
,,%'' oraz następującego po nim znaku
określającego typ konwersji (por. także instrukcja
printf, w punkcie „Instrukcje wyjścia --
print/printf”).
Nie będziemy podawać pełnej listy znaków konwersji
(por. [Robbins], s. 149--151),
ograniczymy się do najczęściej stosowanych:
Tabela 7. Znaki konwersji
| Znak | Typ przekształcenia |
|---|---|
d |
dzień miesiąca (01--31) |
H
|
godzina w zapisie 00--23 |
I
|
godzina w zapisie 01--12 |
j
|
dzień roku (001--366) |
m
|
miesiąc (01-12) |
M
|
minuta (00--59) |
S
|
sekundy (00--61) |
y
|
rok w zapisie dwucyfrowym (00-99) |
Y |
rok w zapisie czterocyfrowym (np. 1066) |
Przykładowo w wyniku wykonania poniższego programu:
awk 'BEGIN {print "dzisiaj jest:", strftime ("%m:%d:%Y")}'
na ekranie zostanie wydrukowany napis
"dzisiaj jest: 08:15:2000" (ale oczywiście
tylko wtedy gdy program uruchomimy 15 Sierpnia 2000 r.)
AWK pozwala grupować instrukcje, podejmować decyzje (konstrukcja if-else) oraz tworzyć pętle (instrukcje for, while). Składnia tych instrukcji pochodzi bezpośrednio z języka C.
Pojedyncza instrukcja może być zawsze zastąpiona listą instrukcji ujętych w nawiasy grupujące. Na liście instrukcje separowane są znakami końca wiersza lub średnikami. Znaki końca wiersza mogą pojawić się po dowolnym lewym i przed dowolnym prawym nawiasem grupującym.
Spójrzmy przykładowo na składnię instrukcji if-else
if (wyrażenie)instrukcja1elseinstrukcja2
część else
jest opcjonalna.
instrukcja2
W celu uniknięcia dwuznaczności przyjęto, że każdy
else jest w parze z bezpośrednio poprzedzającym go
if-em; przykładowo w następującym fragmencie programu:
if (e1) if (e2) s=1; else s=2
else jest w parze z drugim
if-em. (Średnik po s=1 jest
wymagany, gdyż else znalazł się w jednym wierszu
z if-em.)
Instrukcje sterujące
{ instrukcje }
grupowanie instrukcji.
if (w)
instrukcja
jeśli wyrażenie
jest prawdziwe
wykonaj
w.instrukcję
if (w)
instrukcja1 else
instrukcja2
wykonaj
jeśli
wyrażenie instrukcję1 jest
prawdziwe, w wypadku przeciwnym
w.instrukcję2
while (w)
instrukcja
jeśli wyrażenie
jest prawdziwe
wykonaj w
i powtórz.instrukcję
for (w1;
w2; w3)
instrukcja
równoważne instrukcji:
.
w1; while
(w2)
{instrukcja;
w3}
for (zmienna in
tablica)
instrukcja
jest wykonywana dla
instrukcja
przyjmującej kolejno wartości każdego elementu
zmiennej.
tablicy
do instrukcja while
(w)
wykonaj
i jeżeli
wyrażenie instrukcję jest
prawdziwe powtórz.w
break
natychmiastowe wyjście z pętli while,
for, do.
continue
rozpocznij następną iterację w pętlach while,
for, do.
next
rozpoczęcie następnej iteracji głównej pętli wejściowej. (Przez główną pętlę wejściową rozumiemy mechanizm AWK do analizowania rekord po rekordzie pliku wejściowego.)
exit
wyrażenie
sterowanie jest przekazywane bezpośrednio do akcji
END. Jeśli
polecenia exit użyto w akcji
END, program kończy działanie. Opcjonalne
zwracane jest
jako status zakończenia programu.wyrażenie
nextfile
zakończenie przeglądania bieżącego pliku i przejście do
przeglądania następnego z podanych w linii poleceń, lub (dla
ostatniego pliku) przejście do wzorca
END. W wyniku wykonania
nextfile zmienia się wartość zmiennej
FILENAME,
wartością FNR staje się
jeden a wartość ARGIND jest
zwiększana o jeden.
Przykład 10. Wyznaczenie maksymalnego obrotu
Wydrukować z pliku wina.txt wino, którego sprzedaż
przyniosła największy obrót. Tak postawione zadanie rozwiązuje
następujący program:
NR == 1 { omax = $NF * $(NF -1); } # inicjalizacja omax
{ o = $NF * $(NF -1);
if (omax < o ) { omax = o; f = $0; }
else { if (omax == o) {f = f "\n" $0 } }
}
END {print omax; print f }
Ogólny schemat działania programu jest taki, że dla bieżącego
wiersza obliczamy obrót i przyrównujemy do dotychczas największego,
pamiętanego w zmiennej omax.
Jeżeli obrót dla bieżącego wiersza jest większy od
wartości omax, to przypisujemy go jako nową
wartość tej zmiennej; zapamiętujemy także wiersz, zawierający
tą wartość (f=$0).
Drugie polecenie if jest potrzebne na wypadek gdyby
maksymalny obrót był identyczny dla kilku win. Jest to wprawdzie
mało prawdopodobne, ale możliwe. W takim przypadku, kolejne wina
są dołączane (konkatenowane) do dotychczasowej wartości zmiennej
f.
Akcja odpowiadając wzorcowi NR == 1 jest
wykonywana tylko dla pierwszego wiersza i ma na celu inicjalizację
zmiennej omax. Uważny czytelnik zapewne zauważy,
że ponieważ obrót jest zawsze nieujemy, to w tym przypadku
nie ma potrzeby jawnej inicjalizacji. Program i tak dawałby
poprawne wyniki, bo pierwsze wystąpienie omax
spowodowałoby przypisanie jej wartości 0. Gdybyśmy jednak
chcieli obliczyć zamiast obrotu maksymalnego minimalny, to ten
sposób inicjalizacji zmiennej może się przydać.
Jeśli w wierszu programu AWK-owego umieścimy samotny znak
`;', to otrzymamy instrukcję pustą. Spójrzmy na
poniższy program wykorzystujący taką instrukcję w pętli
for; program drukuje wszystkie wiersze, które
zawierają puste pole.
BEGIN { FS = "\t" } # Pola oddziela znak tabulacji!
{ for (i=1; i <= NF && $i!=""; i++)
;
if (i <= NF) { print }
}
Tablice asocjacyjne to jedyny rodzaj tablic dostępny w AWK. Tablic nie
trzeba deklarować, określać ich wymiarów czy typu elementów
składowych. Utworzenie elementu tablicy następuje w chwili
wykonania podstawienia, np. a[44] = 3.14 lub
a[1]="Alibaba", albo innego odwołania do niego.
Element nie zainicjowany jest równy zero lub równy napisowi
pustemu. Indeksy nie są liczbami ale
napisami. Użycie w kontekście indeksu liczby spowoduje
jej konwersję do odpowiedniego napisu. Trzeba o tym pamiętać;
np. print a["01"] nie spowoduje wydrukowania
słowa Alibaba (tylko przypuszczalnie napis
pusty) -- napisy "1" oraz
"01" są oczywiście różne, podczas gdy liczby
nie.
Poniższy program zapamiętuje wszystkie słowa pliku wejściowego oraz liczbę ich wystąpień.
{for (i=1; i <= NF; i++) {ls[$i]++}}
Zwróćmy uwagę, że za każdym razem, gdy pojawia się nowy wyraz, AWK tworzy nowy element tablicy z wartością początkową 0. Następnie operator inkrementacji nadaje mu wartość 1. Każde następne pojawienie się tego wyrazu powoduje zwiększenie wartości już istniejącego elementu.
Powstaje problem jak dobrać się do elementów tablicy ls, skoro nie znamy indeksów (wyrazów
tekstu). Do tego celu służy specjalna forma pętli
for:
for (zmiennaintablica)instrukcja
przyjmuje
iteracyjnie wszystkie wartości indeksów
zmienna. Kolejność
przeglądania tablicy nie jest ustalona i jest zależna od konkretnej
implementacji AWK. Działanie pętli jest
nieokreślone jeżeli wewnątrz pętli zostaną dodane
kolejne elementy do
tablicy. Chcąc
wydrukować listę słów z omawianego przykładu możemy posłużyć się
następującą akcją z wzorcem tablicyEND:
END {for (word in ls) print word, ls[word] }
Wyrażenie:
indeksintablica
pozwala ustalić czy określony
występuje
w indeks. Jeżeli
występuje, to wartością wyrażenia jest 1, w wypadku
przeciwnym 0. Przykładowo, poniższa instrukcja sprawdza czy
w tablicy tablicyls wystąpiło słowo
Alibaba:
if ("Alibaba" in ls) {print "OK!"}
else {print "KO!"}
Element tablicy możemy usunąć za pomocą instrukcji delete:
deletetablica[indeks]
przykładowo delete ls["Alibaba"] usuwa z tablicy
ls element odpowiadający indeksowi
"Alibaba".
Przykład 11. Zestawienia
Za pomocą tablic można bardzo sprawnie dokonywać różnego rodzaju obliczeń i zestawień. Poprzednio przekonaliśmy się jak łatwo można policzyć łączny obrót dla win białych albo czerwonych. Równie proste jest policzenie obrotów dla innych kategorii win. Stosowane wtedy rozwiązanie problemu jest jednak kłopotliwe w sytuacji gdy zamiast zestawienia dla jakiegoś smaku czy smaków potrzebne jest zestawienie obrotów z rozbiciem na wszystkie kategorie smakowe. Po pierwsze nie wiemy ile jest kategorii i możemy nie znać ich wartości. Po drugie kategorii może być dużo. Wykorzystując tablice asocjacyjne nie musimy posiadać tych wszystkich informacji, popatrzmy na zadziwiająco krótki program rozwiązujący problem:
{ obrot[$(NF-2)] += $NF * $(NF-1) }
END { for (wino in obrot) { print wino, obrot[wino] }}
Tablice wielowymiarowe są symulowane przez AWK za pomocą tablic jednowymiarowych. Z punktu widzenia użytkownika nie ma to wielkiego znaczenia. Przykładowo w wyniku działania poniższego fragmentu programu:
for (i=1; i <= 10; i++)
for (j=1; j <= 10; j++)
r[i,j] = rand();
zostanie utworzona tablica 100 elementów, do których możemy się
odwoływać za pomocą par zmiennych indeksowanych postaci
i,j. Wewnętrznie jednakże poszczególne
elementy tablic są indeksowane za pomocą napisów postacji
i SUBSEP j. Zmienna wbudowana
SUBSEP
przechowuje znak używany do oddzielenia indeksów składowych;
standardową wartością tej zmiennej nie jest przecinek ale znak
"\034". Sposób testowania czy element
i,j należy do tablicy nie zmienia się:
for ((i,j) in r) {...}
zaś w wypadku pętli for piszemy:
for (k in r) {print r[k]}
i, jeżeli jest to potrzebne, wykorzystujemy konstrukcję
split(k,x,SUBSEP) do dostępu do wartości zmiennych
indeksowych.
Przykład 12. Zestawienia według wielu kryteriów
Wykorzystując tablice wielowymiarowe nie jest trudną rzeczą wykonanie zestawienia obrotów dla win według dwu kryteriów podziału: smaku i koloru. Poniżej przykładowe rozwiązanie tak postawionego problemu:
{ o = $NF * $(NF-1); # obliczamy obrót
smaki[$(NF-2)] += o; # obrót wg. smaków
kolory[$(NF-3)] += o; # obrót wg. kolorów
obrot[$(NF-2), $(NF-3)] += o # obrót wg. smaków i kolorów
}
END { for (smak in smaki) {
print smak, "...", smaki[smak];
razem +=smaki[smak];
for (kolor in kolory) {
if ((smak, kolor) in obrot)
print "--", kolor, "...", obrot[smak, kolor];
} }
print "Razem ...", razem;
}
Na wydruku otrzymamy zestawienie obrotów według smaków i kolorów, łączne obroty według smaków i obroty razem. Program jest króciótki ma jednak poważną wadę: porządek drukowania zestawienia jest dowolny. Poprawieniem tego feleru zajmiemy się w przykładzie 20 (Zestawienia posortowane) po omówieniu polecenia printf.
AWK umożliwia definiowanie własnych funkcji. Definicja funkcji może być umieszczona w dowolnym miejscu programu, pomiędzy kolejnymi parami wzorzec-akcja. Funkcje są definiowane następująco:
functionnazwa(lista-argumentów) {lista-instrukcji}
to
ciąg oddzielonych przecinkami argumentów funkcji. Podczas wywołania
funkcji, argumentom nadawane są odpowiednie wartości. Nazwy
argumentów są lokalne dla funkcji; są one przekazywane przez
wartość, z wyjątkiem tablic, które są przekazywane ,,przez
referencję''. Argumenty funkcji są jedynymi
zmiennymi lokalnymi w AWK.
lista-argumentów
może
zawierać instrukcję Lista-instrukcjireturn
. Wykonanie
wyrażeniereturn polega na obliczeniu wartości
, a następnie
przekazaniu tej wartości w miejsce wywołania funkcji (tzw. wartość
zwracana przez
funkcję). wyrażenia jest
opcjonalne -- jeżeli go nie ma, instrukcja Wyrażeniereturn
jedynie przekazuje sterowanie do miejsca wywołania. Jeżeli wśród
nie ma
listy-instrukcjireturn to po wykonaniu ostatniej instrukcji (przed
zamykającym nawiasem klamrowym) sterowanie jest przekazywane do
miejsca wywołania, a wartość zwracana jest nieokreślona. Zilustrujmy
to prostym przykładem funkcji max
zwracającej większy ze swoich dwu
argumentów ([AhoetAll], s. 53):
function max(x, y) { return x > y ? x: y }
Funkcje zdefiniowane za pomocą polecenia
function mogą być użyte w dowolnym wyrażeniu,
a także wewnątrz innych funkcji; dozwolona jest także
rekursja
(por. przykład 13 (Odwracanie napisu)). Przy wywołaniu
funkcji nie można umieszczać odstępu pomiędzy
jej nazwą a rozpoczynającym listę argumentów
nawiasem (.
Jak już mówiliśmy tylko argumenty funcji są zmiennymi lokalnymi. Wszystkie inne zmienne są globalne. Jeżeli chcemy aby AWK ,,widział'' jakąś zmienną tylko lokalnie to jedyną metodą jest jej umieszczenie na liście parametrów przy definiowaniu funkcji. Po prostu nadmiarowe parametry umieszczamy na końcu listy. Nie będą one wykorzystywane do przekazywania wartości lecz będą stanowić dodatkowe zmienne lokalne. Wywołanie funkcji z mniejszą od deklarowanej liczbą parametrów jest w AWK poprawne -- wszystkie nadmiarowe parametry przyjmują wartość równą zero lub napisowi pustemu w zależności od kontekstu.
Przykład 13. Odwracanie napisu
Następująca funkcja rekurencyjna odwraca napis poczynając od
znaku s ([Robbins], s. 155).
function rev(str, s) {
if (s == 0) return ""
return (substr(str, s, 1) rev(str, s-1))
}
Dla wypróbowania dopiszmy następujący prosty program:
BEGIN {print rev("Karuzela",length("Karuzela"))}
AWK może czytać dane wejściowe na kilka sposobów. Najprostszym jest uruchomienie go w standardowy sposób czyli, np:
awk -fprogramplik
W takim kontekście, zgodnie z tym co już napisano wcześniej, AWK
czyta
wiersz po wierszu.
Jeżeli nie
podamy plik
to AWK będzie
czekał na strumień danych ze standardowego wejścia
(klawiatury). Często jest to działanie niezamierzone --
pliku^C, ^D,
^break czy ^Z kończą działanie
programu w takiej sytuacji.
Standardową wartością wbudowanej zmiennej
FS jest
" " (spacja -- odstęp).
W takiej sytuacji poszczególne pola
są rozdzielone odstępami lub znakami tabulacji. Sposób rozdzielania
pól można zmienić przypisując zmiennej FS
odpowiedni napis. Jeżeli napis ten jest dłuższy niż jeden
znak, to AWK traktuje go jako wyrażenie
regularne. Najdłuższe (leftmost
longest) ciągi znaków, nie zachodzące na siebie
(non overlapping),
pasujące do tego wyrażenia
regularnego będą oddzielać poszczególne pola w bieżącym
wierszu. Przykładowo deklaracja:
BEGIN {FS ="[,;:]"}
powoduje, że pola będą rozdzielane przecinkiem, średnikiem lub
dwukropkiem. Kiedy wartością FS jest pojedynczy
znak (inny od odstępu), to ten znak jest używany do rozdzielania pól.
Wartość zmiennej FS może zostać nadana także
z poziomu uruchomienia AWK za pomocą przełącznika
-F. Przykładowo zamiast powyższego wzorca
BEGIN moglibyśmy napisać w linii poleceń:
awk -F'[,;:]' -fprogramplik
Wartość zmiennej RS przechowuje
napis używany przez AWK do oddzielania poszczególnych
rekordów. Standardowo rekordy są oddzielane znakami końca wiersza
(co odpowiada przypisaniu RS="\n").
W ograniczonym zakresie możemy zmienić sposób w jaki AWK wyróżnia
poszczególne rekordy nadając odpowiednią wartość
zmiennej RS. W opisie
[AhoetAll] separatorem rekordu może być tylko
napis jednoznakowy lub napis pusty. Jeżeli
RS="" (napis pusty), wtedy separatorami
rekordów są puste wiersze (jeden lub więcej).
Przykład 14. Kartotekowa baza danych
Nie należy do rzadkości sytuacja, w której rekordy zajmują więcej niż jeden wiersz; przykładem może być baza adresowa czy też inny tego typu odpowiednik tradycyjnej kartoteki papierowej. Jeżeli wielkość bazy nie jest zbyt duża, to zamiast specjalizowanych systemów bazodanowych z powodzeniem możemy do jej zarządzania (wyszukiwanie rekordów, drukowanie zestawień itp.) wykorzystywać AWK.
Załóżmy, że plik zawiera informacje adresowe o znajomych osobach: imię i nazwisko, adres i numer telefonu, według schematu, który ilustruje poniższy fragment:
Jan Wacław Gdański
ul. Bolesława Krzywoustego 123/3
Gdynia 80-745
+4856 620-75-21
Wanda Kazimiera Matysek
ul. Bohaterów Monte Cassino 2/2
Sopot 81-825
+4858 551-06-50
Wojciech Strzelecki
Aleja Zwycięstwa 5/2
Gdańsk 80-952
+4858 501-26-11
Ponieważ poszczególne rekordy są oddzielone pustymi wierszami, wystarczy odpowiednio zmienić separator rekordów, żeby było można manipulować nimi bezpośrednio; przykładowo program:
BEGIN {RS=""; ORS="\n\n" }; /Aleja Zwycięstwa/
wydrukuje wszystkie rekordy zawierające napis Aleja
Zwycięstwa. Zwróćmy uwagę na przypisanie
ORS="\n\n"; bez niego
zabrakłoby pustego wiersza pomiędzy kolejnymi drukowanymi rekordami.
W niektórych implementacjach AWK separator rekordu może być wyrażeniem regularnym. W takiej sytuacji, każdy napis pasujący do tego wyrażenia wyznacza koniec kolejnego rekordu (z tym, że napis ten nie jest częścią tego rekordu, podobnie jak w wypadku gdy separatorem jest pojedynczy znak).
Jeżeli RS jest
wyrażeniem regularnym wtedy zmienna RT przechowuje dla
bieżącego rekordu, napis będący jego separatorem od rekordu
następnego.
Przykład 15. Edytor potokowy
Następujący program ([Robbins], s. 243--244) jest AWK-ową implementacją edytora potokowego, tj. takiego programu, który czyta strumień danych, modyfikuje go i wysyła dalej. Sposób użycia jest następujący:
gawk -f awksed.awk co naco plik1 plik2 ...
Spowoduje zastąpienie frazy (wyrażenia regularnego)
na napis
co (oba argumenty
są wymagalne), w plikach
naco. Jeżeli nie podamy
listy plików dane będą czytane ze standardowego wejścia.
plik1
plik2...
function usage() {
print "awksed co naco pliki..." > "/dev/stderr"
exit 1 }
BEGIN { # sprawdź argumenty wywołania
if (ARGC < 3) { usage() }
RS = ARGV[1]; ORS = ARGV[2]
# nie używaj argumentów jako nazw plików
ARGV[1] = ARGV[2] = ""
}
{ if (RT == ""){ printf "%s", $0 }
else { print } }
Idea działania jest prosta: separatorem rekordów jest
a separatorem
rekordów na wyjściu
co. Problemem
jest jedynie sytuacja, w której ostatni rekord nie kończy się
napisem pasującym do nacoRS. Jeżeli plik nie
kończy się napisem pasującym do RS to zmienna
RT będzie równa
napisowi pustemu. Stąd, warunek if (RT=="")...
gwarantuje wydrukowanie całej zawartości pliku wejściowego.
Drugi ciekawy fragment tego przykładu to przypisanie
ARGV[1] = ARGV[2] = "". Chodzi o to, żeby
AWK nie traktował napisów
i co jako nazw
plików wejściowych. Dokładne wyjaśnienie znaczenia tego wiersza
znajduje się w punkcie „Argumenty wywołania programu”.
naco
Przykład 16. Kartotekowa baza danych
Uważny czytelnik zauważył, że przeglądanie bazy z przykładu 14 (Kartotekowa baza danych) nie jest wolne od błędów, np. podając:
BEGIN {RS=""; ORS="\n\n" }; /Gdańsk/
nie tylko zostaną wydrukowane osoby z Gdańska, ale także Jan
Wacław Gdański. Żeby uniknąć podobnych błędów musimy
w jakiś sposób określić znaczenie poszczególnych fragmentów
rekordu. Możemy ustalić, np. że imię i nazwisko zajmuje pierwszy
wiersz, adres drugi, telefon trzeci itd. Można też zastosować schemat
klucz-wartość, według poniższego przykładu:
kto Jan Wacław Gdański
tel +4856 620-75-21
email js.gdanski@rugger.gdynia.pl
kto Wanda Kazimiera Matysek
tel +4858 551-06-50
kto Wojciech Strzelecki
miasto Gdańsk 80-952
adres Aleja Zwycięstwa 5/2
tel +4858 501-26-11
email w.strzelecki@ws.com.pl
W ten sposób rekordy mogą zawierać różne pola a ich porządek jest dowolny. Pozwala to na łatwe modyfikowanie bazy za pomocą zwykłego edytora tekstowego; nie musimy pamiętać struktury bazy -- jest ona samoidentyfikująca się. Łatwo także dodawać, w miarę potrzeb, nowe pola. A jak przeglądać takie pliki? Jednym ze sposobów jest wykorzystanie tablic asocjacyjnych:
#!/bin/bash
cat $3 | awk -vCO=$1 -vCOCO=$2 '
BEGIN {FS="\t" } # Pierwsze pole od reszty oddziela tabulator
NF > 0 {dane[$1] = $2 }
NF < 1 && dane[CO] ~ COCO { drukuj(); usun() }
END { if (dane[CO]~ COCO) {drukuj() }} # ostatni rekord!
function usun(i){ for (i in dane) {delete dane[i] } }
function drukuj(){ print dane["kto"]; print dane["tel"] }'
Działanie programu jest następujące: kolejne wiersze zapamiętujemy
w tablicy dane według schematu:
dane[. Aby uprościć czytanie
plików umawiamy się, że pierwsze pole jest oddzielone od następnych
znakiem tabulacji. Po napotkaniu pustego wiersza
(klucz] =
wartośćNF<1) sprawdzamy czy odpowiedni element tablicy
pasuje do wzorca poszukiwań (dane[CO] ~
COCO). Jeżeli tak, to wykonywana jest procedura drukowania
rekordu (funkcja drukuj) a następnie usuwamy
wszystkie elementy z tablicy dane (funkcja
usun). Po ostatnim rekordzie może nie być
pustego wiersza, stąd konieczność dodania wzorca
END. Zmiennym CO (nazwa pola)
i COCO (wartość pola) przypisujemy wartości
wykorzystując opcję -v wywołania, por. punkt „Uruchamianie AWK”. Dla wygody program został umieszczony
w skrypcie shellowym, więc jego uruchomienie wygląda następująco
(szukaj jest nazwą skryptu):
szukaj miasto Gdańsk plik-adresowy
W rezultacie zostanie wydrukowany wyłącznie Wojciech
Strzelecki.
Polecenie getline
umożliwia czytanie danych z bieżącego lub/i z innego pliku
tekstowego albo z potoku generowanego przez inny program. Poniżej
zestawiono różne formy użycia getline
(
i plik to zmienna
lub stała napisowa zawierająca odpowiednio nazwę pliku lub
programu, z którego AWK ma czytać strumień danych).
program
Dwie pierwsze formy dotyczą czytania danych z bieżącego pliku, dwie
następne z . Dwie
ostatnie to wczytywanie danych ze strumienia generowanego przez inny
pliku. Polecenie
getline zwraca wartość 1 jeżeli wczytany został
następny wiersz tekstu (rekord), 0 jeżeli napotkano koniec pliku
(strumienia) danych oraz -1 w wypadku napotkania błędu (np. otwarcia
pliku). Jeżeli po słowie getline występuje
zmienna program to wczytany
wiersz jest dostępny jako wartość tej zmiennej, w innym wypadku jest
dostępny jako wartość zmiennej z$0.
Przykładowo pętla:
while (getline < plik > 0) {...}
Jest ,,tradycyjną'' techniką umożliwiającą przejrzenie całego
. Poszczególne
wiersze dostępne są w każdej iteracji pętli jako wartości
zmiennej pliku$0.
Podobnie wygląda czytanie danych z potoku. Przykładowo chcąc przekazać do AWK zawartość bieżącego katalogu możemy się posłużyć następującą pętlą:
while ("ls -l" | getline > 0) {...}
Pliki i potoki otwarte przez AWK są automatycznie zamykane
z chwilą zakończenia działania programu. Jeżeli jednak musimy
przeglądać wielokrotnie zawartość jakiegoś pliku w obrębie jednego
programu AWK-owego to za każdym razem należy zamknąć czytany
plik używając funkcji close, np.:
close (plik); close("ls -lrt")
Przykład 17. Wyświetlanie dużych plików
Poniższy program wyświetli listę wszystkich plików
większych od DUZY.
BEGIN { niema = 1; DUZY = 1000000
while ("ls -l" | getline)
if (NF == 9 && $5 > DUZY) {print $0; niema =0 }
if (niema) {print "Nie ma plików większych od", DUZY; }
}
Instrukcja if najpierw testuje
czy wczytany wiersz zawiera
dokładnie 9 pól co pozwala ,,odcedzić'' pola nie zawierające
informacji o plikach (nagłówek listy). Następnie sprawdzany jest
warunek czy wielkość pola jest większa
od DUZY.
Przykład 18. Pobranie daty
Poniższy funkcja pobiera bieżącą datę z komputera i udostępnia ją
w trzyelementowej tablicy DAT,
której element "year" zawiera rok, element
"mon" -- miesiąc
a element "day" -- dzień.
function getdate(x, date) {
"date" | getline x; split(x, date, " ");
DAT["year"]= date[6]; DAT["mon"]= date[2]; DAT["day"]= date[3];
return;
}
# Przykład wykorzystania funkcji getdate:
BEGIN {getdate(); print DAT["mon"], DAT["day"] }
Argumenty x, i date nie służą do
przekazywania wartości, ale do uczynienia obu zmiennych lokalnymi
(por. uwagi na ten temat w punkcie „Funkcje”) -- funkcję
wywołujemy po prostu getdate(). Zwracamy uwagę,
że program jest ,,nieodporny'' na zmianę formatu drukowanej daty.
Rekord może być także dzielony na pola o ustalonej długości. W tym
celu zmiennej wbudowanej FIELDWIDTHS przypisujemy
napis zawierający ciąg oddzielonych odstępami liczb. Każda
liczba oznacza długość odpowiedniego pola w znakach. Jeżeli wartością
zmiennej FIELDWIDTHS nie jest napis
pusty, pola wyznaczane są w oparciu o specyfikację podaną w tej
zmiennej, a nie w oparciu wartość separatora pól (czyli
zmienną FS). Przypisanie wartości zmiennej
FS (np. FS=FS) przywraca
standardowy sposób wyznaczania pól.
Przykład 19. Pola o stałej długości
Rekordy dotyczące podjazdów w pliku tdf2000.txt
mają pola o ustalonej długości, zatem do ich podzielenia można
wykorzystać zmienną FIELDWIDTHS:
# Drukuj nazwę góry i jej długość
BEGIN { FIELDWIDTHS = "1 25 5 5 4 3 4 4 4 4 4" }
NR==1, /^-+[ \t]*$/ { next }
/^Etap[ \t]+[0-9]+:/ { next }
NF > 0 { print $2, $11 }
W rezultacie wykonania programu zostaną wydrukowane nazwy gór i długości podjazdów.
Zwróćmy uwagę, że gawk nie dokonuje żadnego sprawdzenia
poprawności specyfikacji podanej w napisie
FIELDWIDTHS.
Instrukcje print oraz printf służą do drukowania. Pierwsza z nich drukuje swoje argumenty zawsze według tego samego formatu, druga umożliwia precyzyjniejsze sterowanie postacią wypisywanych danych. Dane mogą być drukowane na ekran, do pliku lub w potoku.
Składnia instrukcji printf jest niemalże identyczna z odpowiednią funkcją języka C. Ogólna postać tej instrukcji jest następująca:
printf (format,arg1,arg2,...)
Nawiasy ( oraz ) są opcjonalne.
Napis lub zmienna napisowa
określa sposób
przekształcania i formatowania argumentów. Zawiera on dwa rodzaje
obiektów: zwykłe znaki, kopiowane po prostu przy drukowaniu oraz
specyfikacje przekształceń, z których każda określa sposób
przekształcenia i wypisania kolejnego argumentu funkcji
printf.
Specyfikacja ta rozpoczyna się od znaku
format% a kończy znakiem określającym typ
konwersji. Pomiędzy nimi możemy użyć ponadto
następujących znaków modyfikujących:
-, zawartość pola jest justowana do lewego
krańca pola;
,
określający minimalny rozmiar pola (w znakach). Przekształcony
argument będzie wpisany do pola o długości co
najmniej równej
ciąg-cyfr. Jeżeli
argument składa się z mniejszej liczby znaków, to będzie ono
uzupełnione do długości minimalnej odstępami. Jeżeli
specyfikacja długości rozpoczyna się
cyfrą ciąg-cyfr0, to pole będzie wypełniane nie znaczącymi
zerami;
.,
maksymalna drukowana długość napisu lub liczba
cyfr po kropce dziesiętnej.
ciąg-cyfr
Oto lista znaków przekształceń i ich znaczenie (zapis
[-] oznacza, że
znak - jest opcjonalny):
Tabela 9. Znaki konwersji
| Znak | Typ przekształcenia |
|---|---|
c |
znak |
d |
liczba całkowita |
e |
liczba postaci
[-]d.ddddddE[-]dd
|
f |
liczba postaci
[-]ddd.dddddd |
g |
e lub f, w zależności od
tego, które jest krótsze bez nieznaczących zer |
o |
liczba ósemkowa bez znaku |
s |
napis |
x |
liczba szestnastkowa bez znaku |
Jeżeli znak występujący po % nie jest znakiem
przekształcenia to jest on po prostu wypisany; zatem
%% spowoduje wypisanie znaku %.
Poniższe zestawienie ilustruje działanie różnych specyfikacji. Aby
można ocenić długości pól otoczono je znakami |,
zaś spacje oznaczono znakiem _.
Tabela 10. Przykłady specyfikacji
| Specyfikacja | Argument | Wynik |
|---|---|---|
%5d%%
|
33.33
|
|___33%| |
%c |
33.33 |
|!| |
%d |
33.33 |
|33| |
%5d |
33.33 |
|___33| |
%e |
3.1415 |
|3.141500e+000| |
%f |
3.1415 |
|3.141500| |
%8.3f |
3.1415 |
|___3.141| |
%08.3f |
3.1415 |
|0003.141| |
%s |
Alibaba |
|Alibaba| |
%9s |
Alibaba |
|__Alibaba| |
%-9s |
Alibaba |
|Alibaba__| |
%-.3s |
Alibaba |
|Ali| |
%-9.3s |
Alibaba |
|Ali______| |
Instrukcja print jest
uproszczoną formą printf a jej działanie jest
następujące: drukowane argumenty są oddzielane
wartością zmiennej wbudowanej
OFS
(standardowo OFS=" " -- argumenty oddzielone są
znakiem odstępu) a na końcu listy jest drukowana
wartość zmiennej wbudowanej ORS
(standardowo ORS="\n" -- każde kolejne
print drukuje od nowego wiersza). Wszystkie
argumenty numeryczne są drukowane w oparciu o tę samą specyfikację,
określoną poprzez wartość zmiennej OFMT
(standardowo OFMT="%.6g"). Stąd, uruchamiając
poniższy przykład:
awk 'BEGIN {OFS=":";ORS="->"; print log(2), log(3); print log(5); }'
otrzymamy:
0.693147:1.09861->1.60944->
print to skrót od print $0
(a nie, jak można by się spodziewać, print ORS).
Zamiast na ekran (standardowe wyjście) instrukcje
printf/print mogą przesłać dane
do pliku. Służą do tego operatory > oraz
>>, zaś instrukcje mają wówczas postać:
printfformat,arg1,... >plikprintarg1,... >plik
lub
printfformat,arg1,... >>plikprintarg1,... >>plik
jest napisem lub
zmienną typu napisowego zawierającą legalną (z punktu widzenia
systemu operacyjnego) nazwę pliku. Przykładowo program:
plik
{ printf "%s\n", $0 > $1 }
będzie działał doputy, dopóki wartością $0 w pliku
wejściowym będzie napis mogący być legalną nazwą pliku (oraz dopóki
liczba otwartych jednocześnie plików nie przekroczy dopuszczalnego
maksimum, porównaj uwagi z przykładu 22 (Pisanie do wielu plików)).
Instrukcja printf "%d %d\n", $1, $2 > $3
spowoduje wydrukowanie $1
i $2 do pliku określonego jako wartość pola
$3, a nie $1 i wyniku
porówania wartości drugiego oraz trzeciego pola. Jeżeli chcemy
osiągnąć to drugie to powinniśmy napisać:
printf "%d %d\n", $1, ($2 > $3) albo
printf ("%d %d\n", $1, $2 > $3).
Operator > otwiera plik tylko raz (niszcząc
poprzednią zawartość); kolejne instrukcje printf
dodają tekst do tego pliku. Operator >> różni się
od > tym, że przy otwarciu pliku poprzednia
zawartość nie jest niszczona.
Przykład 20. Zestawienia posortowane
Przykład 12 (Zestawienia według wielu kryteriów) zawiera program drukujący zestawienie obrotów dla win z podziałem według smaku i koloru. Poważną wadą programu jest niemożność określenia porządku, w jakim ma on drukować poszczególne pozycje zestawienia. Poniżej poprawiona wersja drukująca poszczególne rubryki w porządku alfabetycznym:
#!/bin/bash
if test $# -eq 0; then
echo "`basename $0`: plik..." >&2; exit 0; fi
cat $* | awk ' { o = $NF * $(NF-1);
smaki[$(NF-2)] += o; kolory[$(NF-3)] += o;
obrot[$(NF-2), $(NF-3)] += o
}
END { for (smak in smaki)
for (kolor in kolory)
if ((smak, kolor) in obrot)
print smak, kolor, obrot[smak, kolor];
}' | sort | awk ' BEGIN { kreska = "+-----------------------+"
KRESKA = "+=======================+"; print KRESKA; }
{ if (kolor != $1) {
if (NR !=1) {
printf "| Razem: | %10.2f |\n", razem;
print KRESKA; razem = 0}
printf "| Wina %-16.16s |\n", $1;
print kreska;
}
razem += $3; rrazem += $3; kolor = $1;
printf "| %-8.8s | %10.2f |\n", $2, $3
}
END { printf "| Razem: | %10.2f |\n", razem;
print KRESKA;
printf "| OGÓŁEM: | %10.2f |\n", rrazem;
print KRESKA; } '
Problem został rozwiązany z wykorzystaniem dwóch
skryptów AWK-owych oraz standardowego programu
sort. Pierwszy skrypt dokonuje obliczeń
a wyniki drukuje w formacie odpowiednim dla programu
sort, który jest uruchamiany w kolejnym
kroku. Wydrukowaniem posortowanych danych w formie eleganckiej tabelki
zajmuje się drugi skrypt AWK-owy. Całość została ,,zintegrowana''
poprzez połączenie skryptów oraz
programu sort w potok i umieszczenie
wszystkiego w jednym skrypcie shellowym. (Skrypt ten oczekuje nazw
plików do przeczytania jako argumentów wywołania. Instrukcja:
if test $# -eq 0;... fi sprawdza czy podano jakieś
argumenty; jeżeli nie to skrypt kończy działanie. Polecenie
cat $* wysyła strumień danych na wejście pierwszego
programu AWK-owego. Przypominamy, że $* w języku
bash oznacza listę wszystkich argumentów,
z którymi uruchomiono skrypt.)
Możliwe jest także drukowanie w potoku za pomocą instrukcji printf (print) o postaci:
printfformat,arg1,... |programprintarg1,... |program
jest napisem lub zmienną napisową
zawierającą nazwę programu-odbiorcy strumienia danych
drukowanych przez printf (print).
program
Przykład 21. Zestawienia posortowane
Jako ilustrację zmodyfikujemy dwuwierszowy program z przykładu 11 (Zestawienia), tak żeby poszczególne kategorie były drukowane według malejących obrotów:
{ obrot[$(NF-2)] += $NF * $(NF-1) }
END {for (wino in obrot) {print wino, obrot[wino] | "sort -nr +1" }}
Dla przypomnienia sort -nr +1 oznacza sortowanie
numeryczne, w odwrotnym porządku poczynając od drugiego pola.
Funkcja
close zamyka plik otwarty za pomocą operatorów
>, >>
i |. Jej składnia jest następująca:
close(napis)
gdzie jest
identyczny z napisem
napis lub
plik, za pomocą
których uprzednio otwarto plik/potok. Funkcja ta jest niezbędna
w sytuacji gdy np. najpierw piszemy do pliku a następnie chcemy
(w obrębie tego samego programu) czytać
z niego dane.
program
Napis zamykający plik lub potok musi być identyczny z dokładnością do znaków odstępu, z tym, za pomocą którego plik/potok został otworzony. Z tego względu zalecamy używanie do otwierania i zamykania plików i potoków uprzednio zadeklarowanych zmiennych a nie posługiwanie się stałymi napisowymi.
Przykład 22. Pisanie do wielu plików
Należy przepisać zawartości pliku tdf2000.txt
w taki sposób aby każdy etap został zapisany w odzielnym pliku.
Poniższy program jest jednym z możliwych sposobów rozwiązania
problemu:
#!/usr/bin/awk -f
NR==1, /^-+[ \t]*$/ { next } # Pomiń nagłówek,
/^Etap[ \t]+[0-9]+:/ { close(File);
sub(/:/,"",$2); File="etap" $2;
next }
NF > 0 { print $0 > File}
Instrukcja File="etap" sub(/:/,"",$2) tworzy nazwę
pliku według schematu
etap.
Kolejne niepuste wiersze (numer-etapuNF > 0) będą wysyłane
do pliku
etap.
Polecenie numer-etapuclose(File) zamyka plik związany
z poprzednim etapem. W tym przykładzie close nie
jest potrzebna bo będziemy pisać raptem do trzech plików. Gdyby
jednak etapów było więcej to mogłoby się okazać, że próbujemy otworzyć
więcej plików niż jest to dopuszczalne (dopuszczalna maksymalna liczba
otwartych plików jest zależna od ustawień systemowych) i program
zakończyłby się błędem.
Ważna jest kolejność: najpierw zamykamy plik (poprzedni) a dopiero w kolejnej iteracji otwieramy następny. Dla pierwszego etapu nie ma wprawdzie czego zamykać -- ale w AWK próba zamknięcia pliku, który nie jest otwarty nie jest błędem.
Przykład 23. Wielokrotne czytanie pliku
Aby obliczyć procentowy udział obrotu każdego wina w obrocie ogółem (plik
wina.txt) należy przeczytać
plik dwa razy. (Można też próbować wczytać cały plik do pamięci,
ale takie rozwiązanie jest bardziej skomplikowane a także, dla
bardzo długiej listy win, może zabraknąć pamięci.)
#!/bin/bash
if [ ! -f "$1" ]; then
echo "Wykorzystanie: `basename $0`: plik" >&2; exit 0; fi
awk 'FNR==1 { pass++ }
pass == 1 {oo += $NF * $(NF-1) }
pass == 2 {print $0, 100 * $NF * $(NF-1)/oo }' $1 $1
Trik polega na dwukrotnym umieszczeniu tego samego pliku jako
argumentów wywołania AWK. Przypominamy, że zmienna
FNR zawiera numer
bieżącego rekordu, licząc w każdym pliku wejściowym od początku.
Licznik pass jest zwiększany o 1 po napotkaniu
pierwszego wiersza każdego pliku zatem ma on wartość 1 przy pierwszym
i 2 przy drugim czytaniu pliku. W konsekwencji druga akcja będzie
wykonywana dla każdego wiersza podczas jego pierwszego czytania zaś
trzecia podczas drugiego. Takie rozwiązanie dwukrotnego czytania tego
samego pliku pozwala znacznie uprościć program: nie musimy jawnie
czytać pliku w pętli while za pomocą
getline, korzystać z funkcji
split i zamykać plik za pomocą
close.
fflush(napis)
opróżnia bufor związany z plikiem lub potokiem poprzez
, identyczny
z napisem napis lub
plik, za pomocą
których uprzednio otwarto plik/potok. Dopuszczalne są także dwie
następujące postacie funkcji: programfflush() oraz
fflush(""). Pierwsza opróżnia bufor związany
ze standardowym wyjściem, druga bufory związane
z wszystkimi otwartymi w danej chwili
plikami/potokami.
Wartości argumentów wywołania programu są, podobnie jak w wypadku
języka C, przechowywane we wbudowanej zmiennej tablicowej
ARGV.
Zmienna ARGC zaś zawiera
liczbę argumentów. Przykładowo:
gawk -v v=1 -f 1.awk test.txt v=1 b
ARGC ma wartość 4, ARGV[0]
="gawk", ARGV[1] ="test.txt",
ARGV[2] ="v=1", ARGV[3]
="b". Opcje i ich wartości nie są liczone
(w przykładzie są to -f i -v)
a ARGV[0] jest równa nazwie uruchomionego programu
(tu gawk). Poniższy program wypisuje wartość
wszystkich argumentów wywołania:
BEGIN {for (i=0; i< ARGC; i++) {print ARGV[i]} }
Zmienne ARGC i ARGV można zmieniać
wewnątrz programu. Po napotkaniu końca bieżącego pliku wejściowego
AWK pobiera następny element tablicy ARGV jako
nazwę następnego pliku wejściowego. Przykładowo:
BEGIN{ ARGV[1]="test.txt"; if (ARGC < 2) {ARGC = 2} }
spowoduje, że AWK, zawsze będzie przeszukiwał
jako pierwszy plik ,,test.txt'' bez względu na to
jaki (i czy w ogóle) podamy w linii poleceń.
Aby usunąć nazwę pliku z tablicy ARGV możemy albo
nadać odpowiedniemu elementowi tablicy wartość napisu pustego, albo
posłużyć się poleceniem delete. Napis
,,-'' oznacza standardowe wejście. Tego typu
manipulacje z reguły umieszczamy wewnątrz wzorca
BEGIN,
por. przykład 15 (Edytor potokowy).
Przykład 24. Wyświetlanie dużych plików
W przykładzie 17 (Wyświetlanie dużych plików) wielkość pliku była zadeklarowana na stałe co jest pewną niedogodnością; poniższa modyfikacja pozbawiona jest już tej wady:
BEGIN { flag = 1;
if (ARGC > 1) {DUZY = ARGV[1] * 1000} else { DUZY = 0; };
while ("ls -l" | getline > 0) {
if (NF == 9 && $5 > DUZY) {print; flag =0 }}
if (flag) {print "Nie ma plików większych od", DUZY; }
}
Chcąc uzyskać listę plików np. większych od 500kb, wystarczy teraz
napisać awk -f chkbig.awk 500 (gdzie
chkbig.awk zawiera powyższy kod).
AWK można wywołać z kilkunastoma
opcjami.
Najważniejsze z nich to
-f, -F oraz
-v. Dwie pierwsze już omówiliśmy, ostatnia
umożliwia nadanie zmiennej (ogólnie zmiennym, ponieważ możemy ją
używać wielokrotnie) wartości w momencie uruchomienia programu
(z linii poleceń). Przykład 25 (Duże pliki)
ilustruje sposób wykorzystania opcji -v.
Z czasem gdy dorobimy się biblioteki własnych funkcji AWK-owych,
może powstać problem, jak w elegancki sposób dołączać ją do różnych
plików, w taki sposób, jak umożliwia to, np. instrukcja
#include w C? Okazuje się, że opcja
-f nie musi wcale występować tylko raz:
awk -f mylib.awk -fprogramplik
gdzie plik mylib.awk zawiera bibliotekę naszych
funkcji. Nie jest to rozwiązanie idealne ale -- według
nas -- najlepsze z możliwych.
Zmienna środowiskowa AWKPATH zawiera
katalogi, w których AWK szuka plików źródłowych podanych za
pomocą opcji -f.
Przykład 25. Duże pliki
Problem z przykładu 17 (Wyświetlanie dużych plików) można rozwiązać w inny sposób niż ten zaprezentowany w przykładzie 24 (Wyświetlanie dużych plików) umieszczając zmodyfikowany program AWK-owy (oczywiste zmiany pozostawiamy czytelnikom) w skrypcie shellowym:
#!/bin/bash
awk -vDUZY=$1 'BEGIN { ... }'
Żeby otrzymać listę plików większych od np. 600kb wystarczy teraz
napisać: chkbig 600 (gdzie
chkbig jest nazwą skryptu).
[AhoetAll] Alfred V. Aho, Brian W. Kernighan, Peter J. Weinberger, The AWK Programming Language, Addison-Wesley, 1988.
[AWKintro] Alfred V. Aho, Brian W. Kernighan, Peter J. Weinberger, AWK -- A Pattern Scanning and Processing Language, 1978. Dostępny m.in. w http://www.softlab.ntua.gr/unix/docs/awk.ps jako plik postscriptowy.
[AwkFAQ] comp.lang.awk FAQ. Dokument dostępny m.in. w http://www.faqs.org/faqs/computer-lang/awk/faq.
[BLTP] Bogusław Lichoński, Tomasz Przechlewski, AWK -- opis języka z przykładami, Biuletyn GUST, 7/1996, s. 10--25.
[Robbins] Arnold D. Robbins, Effective AWK Programming. A User's Guide for GNU AWK, version 1.0.4, April 1999, Podręcznik rozpowszechniany w pakiecie z programem gawk, dostępny m.in. w ftp://sunrise.pg.gda.pl/pub/gnu/gawk.