awk
W tym rozdziale przedstawiamy mieszankę programów awk dla
przyjemności czytania.
Rozdział ten składa się z dwu sekcji. Pierwsza pokazuje zrobione w awk
wersje kilku popularnych narzędzi POSIX-owych. Druga jest
workiem pełnym interesujących programów.
Wiele z tych programów wykorzystuje funkcje biblioteczne przedstawione
w 15. Biblioteczka funkcji awk.
Ta sekcja przedstawia kilka narzędzi POSIX, które zostały zaimplementowane
w awk. Ponowne wymyślanie tych programów w awk jest często
dobrą zabawą, gdyż odpowiednie algorytmy można wyrazić przejrzyście, jasno,
a kod jest zwykle zwięzły i prosty. Jest to możliwe dzięki temu, że
awk robi tak wiele za nas.
Należy zauważyć, że przeznaczeniem opisywanych programów nie jest
zastąpienie wersji zainstalowanych w twoim systemie. Ich celem jest
natomiast zilustrowanie programowania w języku awk rzeczywistych,
"z życia wziętych" zadań.
Programy przedstawiono w kolejności alfabetycznej.
Narzędzie cut wybiera, lub "wycina" ("cut"), znaki albo pola
ze swego standardowego wejścia i wysyła je na standardowe wyjście.
cut potrafi wyciąć albo listę znaków, albo listę pól. Domyślnie,
pola oddzielane są tabulacjami, ale można podać opcję wiersza poleceń, by
zmienić ogranicznik pól, tj. znak separatora pól. Definicja pól
w cut jest mniej ogólna niż w awk.
Typowym zastosowaniem cut może być wyciągnięcie z wyjścia programu
who, pokazującego zalogowanych użytkowników, tylko nazw kont.
Na przykład, poniższy potok tworzy posortowaną, nie zawierającą powtórzeń
listę zalogowanych użytkowników:
who | cut -c1-8 | sort | uniq
cut ma następujące opcje:
-c lista
-f lista
-d ogranicz
-s
Implementacja cut wykonana w awk korzysta z funkcji
bibliotecznej getopt
(zob. 15.10. Przetwarzanie opcji wiersza poleceń), oraz funkcji
bibliotecznej join
(zob. 15.6. Scalanie tablicy w łańcuch).
Program rozpoczyna się od komentarza opisującego opcje i funkcji usage
(sposób użycia), która wypisuje komunikat o sposobie użycia i kończy pracę
programu. usage wywoływana jest gdy podano nieprawidłowe argumenty.
# cut.awk --- implementacja cut w awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# Opcje:
# -f list wycina pola (fields)
# -d c znak separatora (delimiter) pól
# -c list wycina znaki (characters)
#
# -s pomija wiersze bez znaku separatora
function usage( e1, e2)
{
e1 = "składnia: cut [-f lista] [-d c] [-s] [pliki...]"
e2 = "składnia: cut [-c lista] [pliki...]"
print e1 > "/dev/stderr"
print e2 > "/dev/stderr"
exit 1
}
Zmienne e1 i e2 zastosowano, by funkcja ładnie
mieściła się na
stronie.
Następnie mamy regułę BEGIN, która wykonuje analizę składniową opcji
wiersza poleceń. Przypisuje FS pojedynczy znak tabulacji, gdyż taki
jest domyślny separator pól cut. Ustalany jest też separator pól
wyjściowych, by był taki sam jak separator pól wejściowych. Następnie
do przechodzenia przez kolejne opcje wiersza poleceń wykorzystywana jest
getopt. Jedna ze zmiennych wg_pol i wg_znakow staje
się prawdziwa, wskazując, że przetwarzanie powinno być wykonane, odpowiednio,
według pól lub według znaków. Przy wycinaniu według znaków, separator pól
wyjściowych staje się łańcuchem pustym.
BEGIN \
{
FS = "\t" # domyślne
OFS = FS
while ((c = getopt(ARGC, ARGV, "sf:c:d:")) != -1) {
if (c == "f") {
by_fields = 1
fieldlist = Optarg
} else if (c == "c") {
by_chars = 1
fieldlist = Optarg
OFS = ""
} else if (c == "d") {
if (length(Optarg) > 1) {
printf("Korzystam z pierwszego znaku %s" \
" jako separatora\n", Optarg) > "/dev/stderr"
Optarg = substr(Optarg, 1, 1)
}
FS = Optarg
OFS = FS
if (FS == " ") # obrona semantyki awk
FS = "[ ]"
} else if (c == "s")
suppress++
else
usage()
}
for (i = 1; i < Optind; i++)
ARGV[i] = ""
Gdy separator pól jest spacją, podejmowane są specjalne środki ostrożności.
Stosowanie " " (pojedynczej spacji) jako wartości FS jest
niepoprawne -- awk rozdzielałby pola ciągami spacji, tabulacji i/lub
znaków nowej linii, a chcemy, by były oddzielane pojedynczymi spacjami.
Zwróć też uwagę, że po ukończeniu getopt musimy wyczyścić wszystkie
elementy ARGV, od jeden do Optind, by awk nie próbował
przetwarzać opcji wiersza poleceń jako nazw plików.
Po uporaniu się z opcjami wiersza poleceń, program upewnia się, czy mają one
sens. Powinna być użyta tylko jedna z opcji `-c' i `-f', i obie z
nich wymagają listy pól. Następnie, do rozbioru na części listy pól czy
znaków, wywoływana jest albo set_fieldlist albo set_charlist.
if (by_fields && by_chars)
usage()
if (by_fields == 0 && by_chars == 0)
by_fields = 1 # domyślne
if (fieldlist == "") {
print "cut: potrzebna lista dla -c lub -f" > "/dev/stderr"
exit 1
}
if (by_fields)
set_fieldlist()
else
set_charlist()
}
Oto funkcja set_fieldlist. Najpierw łamie na przecinkach listę pól
na części, umieszczając wyniki w tablicy. Następnie, dla każdego
elementu tablicy, patrzy czy jest on może w rzeczywistości zakresem, a jeśli
tak, to dzieli go na składowe. Podany zakres jest sprawdzany, by upewnić
się, że pierwsza z liczb jest mniejsza od drugiej. Każda liczba z listy
dodawana jest do tablicy flist, będącej po prostu wykazem pól do
wypisania. Wykorzystywany jest zwykły podział na pola. Program pozwala,
by awk sam zajął się rozdzielaniem na pola.
function set_fieldlist( n, m, i, j, k, f, g)
{
n = split(fieldlist, f, ",")
j = 1 # indeks w flist
for (i = 1; i <= n; i++) {
if (index(f[i], "-") != 0) { # zakres
m = split(f[i], g, "-")
if (m != 2 || g[1] >= g[2]) {
printf("błędna lista pól: %s\n",
f[i]) > "/dev/stderr"
exit 1
}
for (k = g[1]; k <= g[2]; k++)
flist[j++] = k
} else
flist[j++] = f[i]
}
nfields = j - 1
}
Funkcja set_charlist jest bardziej skomplikowana niż
set_fieldlist. Pomysł polega tu na zastosowaniu występującej
w gawk zmiennej FIELDWIDTHS
(zob. 5.6. Czytanie danych o stałej szerokości), opisującej
wejście o stałej szerokości. Gdy używamy listy znaków, jest to dokładnie
to, czego potrzebujemy.
Przygotowanie FIELDWIDTHS jest bardziej skomplikowane, niż zwykłe
wykazanie pól, jakie powinny być wypisane. Musimy pamiętać pola,
które będą wypisane oraz wtrącone znaki, które mają być pominięte.
Na przykład, załóżmy, że potrzebujemy znaków od jeden do osiem, 15,
i 22 do 35. Użylibyśmy wówczas `-c 1-8,15,22-35'. Potrzebną wartością
FIELDWIDTHS byłoby "8 6 1 6 14". Daje nam to pięć pól,
z których powinny zostać wypisane $1, $3 i $5.
Wtrącone pola są "wypełniaczem" ("filler"), materiałem pomiędzy
pożądanymi danymi.
flist zawiera listę pól do wypisania, a t pamięta pełną listę
pól, łącznie z polami wypełniacza.
function set_charlist( field, i, j, f, g, t,
filler, last, len)
{
field = 1 # zlicza pola razem
n = split(fieldlist, f, ",")
j = 1 # indeks w flist
for (i = 1; i <= n; i++) {
if (index(f[i], "-") != 0) { # zakres
m = split(f[i], g, "-")
if (m != 2 || g[1] >= g[2]) {
printf("błędna lista znaków: %s\n",
f[i]) > "/dev/stderr"
exit 1
}
len = g[2] - g[1] + 1
if (g[1] > 1) # oblicza długość filler
filler = g[1] - last - 1
else
filler = 0
if (filler)
t[field++] = filler
t[field++] = len # długość pola
last = g[2]
flist[j++] = field - 1
} else {
if (f[i] > 1)
filler = f[i] - last - 1
else
filler = 0
if (filler)
t[field++] = filler
t[field++] = 1
last = f[i]
flist[j++] = field - 1
}
}
FIELDWIDTHS = join(t, 1, field - 1)
nfields = j - 1
}
Oto reguła, która faktycznie przetwarza dane. Jeżeli podano opcję
`-s', to suppress będzie prawdziwe. Pierwsza instrukcja
if zapewnia, że rekord wejściowy nie zawiera separatora pól.
Jeśli cut w danym przebiegu przetwarza pola, suppress jest
prawdziwe, a w rekordzie nie ma znaku separatora rekordów, to rekord ten
jest pomijany.
Jeżeli rekord jest poprawny, to w tym miejscu gawk rozdzielił już
dane na pola, albo za pomocą znaku w FS, albo używając pól o stałej
długości i FIELDWIDTHS. Występująca tu pętla przechodzi przez
listę pól, jakie mają być wypisane. Jeśli odpowiednie pole zawiera dane,
to jest wypisywane. Jeżeli następne pole także zawiera dane, to pomiędzy
tymi polami wypisywany jest znak separatora.
{
if (by_fields && suppress && $0 !~ FS)
next
for (i = 1; i <= nfields; i++) {
if ($flist[i] != "") {
printf "%s", $flist[i]
if (i < nfields && $flist[i+1] != "")
printf "%s", OFS
}
}
print ""
}
Ta wersja cut w wykonywaniu wycinania według znaków opiera się na
używanej w gawk zmiennej FIELDWIDTHS. Mimo, iż
w innych implementacjach awk byłoby możliwe skorzystanie
z substr
(zob. 12.3. Funkcje wbudowane działające na łańcuchach),
metoda taka byłaby równocześnie niezwykle bolesna. Zmienna
FIELDWIDTHS zapewnia eleganckie rozwiązanie problemu podziału wiersza
wejściowego na poszczególne znaki.
Narzędzie egrep szuka wzorców w plikach. Korzysta z wyrażeń
regularnych, które są prawie identyczne, jak dostępne w awk
(zob. 7.1.2. Stałe regexp). Używane jest w ten sposób:
egrep [ opcje ] 'wzorzec' pliki ...
wzorzec jest wyrażeniem regularnym.
W typowym zastosowaniu, wyrażenie regularne jest cytowane, by zapobiec
rozwijaniu przez powłokę znaków specjalnych jako masek nazw plików.
Normalnie, egrep wypisuje pasujące wiersze. Jeśli w wierszu poleceń
podano kilka nazw plików, to każdy wiersz wyjściowy poprzedzony jest nazwą
pliku i dwukropkiem.
Opcjami są:
-c
-s
-v
egrep wypisuje wiersze, które nie
pasują do wzorca, i kończy pomyślnie pracę jeśli nie dopasowano wzorca.
-i
-l
-e wzorzec
Nasza wersja wykorzystuje funkcję biblioteczną getopt
(zob. 15.10. Przetwarzanie opcji wiersza poleceń) i program
biblioteczny do obsługi przejścia między plikami
(zob. 15.9. Obsługa przejść między plikami).
Program zaczyna się od komentarza opisowego, a następnie reguły BEGIN,
przetwarzającej za pomocą getopt argumenty wiersza poleceń. Opcja
`-i' (ignoruj wielkość liter) jest szczególnie łatwa w gawk;
wykorzystujemy po prostu zmienną wbudowaną IGNORECASE
(zob. 10. Zmienne wbudowane).
# egrep.awk --- symulacja egrep w awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# Opcje:
# -c zlicza (count) wiersze
# -s cicho (silent): używa kodu zakończenia
# -v odwraca (invert) test:
# powodzenie, gdy nie pasuje
# -i ignoruje wielkość liter
# -l wypisuje (list) tylko nazwy plików
# -e argument jest wzorcem
BEGIN {
while ((c = getopt(ARGC, ARGV, "ce:svil")) != -1) {
if (c == "c")
count_only++
else if (c == "s")
no_print++
else if (c == "v")
invert++
else if (c == "i")
IGNORECASE = 1
else if (c == "l")
filenames_only++
else if (c == "e")
pattern = Optarg
else
usage()
}
Dalej mamy kod, który obsługuje charakterystyczne zachowanie egrep.
Jeżeli nie podano wzorca za pomocą `-e', to jako wzorzec wykorzystywany
jest pierwszy nie będący opcją argument wiersza poleceń. Argumenty wiersza
poleceń awk aż do ARGV[Optind] są czyszczone, by awk
nie usiłował przetworzyć ich jako pliki. Jeżeli nie podano żadnych plików,
to używane jest standardowe wejście, a jeśli podano kilka, to odnotowujemy
to, by później w wydruku poprzedzić dopasowane wiersze nazwami plików.
Ostatnie dwa wiersze są zakomentowane, gdyż nie są potrzebne w gawk.
Powinny zostać odkomentowane jeśli będziemy musieli korzystać z innej wersji
awk.
if (pattern == "")
pattern = ARGV[Optind++]
for (i = 1; i < Optind; i++)
ARGV[i] = ""
if (Optind >= ARGC) {
ARGV[1] = "-"
ARGC = 2
} else if (ARGC - Optind > 1)
do_filenames++
# if (IGNORECASE)
# pattern = tolower(pattern)
}
Kolejna grupa wierszy również powinna być odkomentowana jeśli nie
korzystasz z gawk. Reguła ta, jeśli podano opcję `-i', zamienia
wszystkie litery wiersza wejściowego na małe. Jest ona zakomentowana gdyż
nie jest konieczna w gawk.
#{
# if (IGNORECASE)
# $0 = tolower($0)
#}
Funkcja beginfile wywoływana jest przez regułę z `ftrans.awk'
za każdym razem, gdy rozpoczyna się przetwarzanie nowego pliku. W tym
przypadku jest ona bardzo prosta: wykonuje tylko inicjowanie zmiennej
fcount zerem. fcount pamięta, ile wierszy bieżącego pliku
pasowało do wzorca.
function beginfile(smiec)
{
fcount = 0
}
Funkcja endfile wywoływana jest po przetworzeniu każdego pliku.
Stosowana jest tylko wtedy, gdy użytkownik chce przeliczyć pasujące wiersze.
no_print będzie prawdziwa tylko jeśli wymagany jest kod zakończenia.
count_only będzie prawdziwe jeśli wymagane są liczby wierszy.
Zgodnie z tym, egrep będzie wypisywał liczby wierszy tylko jeśli
włączone są wypisywanie i zliczanie. Format wyjściowy musi być dostosowany
do liczby plików, jakie będą przetwarzane. Na koniec, fcount jest
dodawane do total, byśmy wiedzieli, ile ogółem wierszy pasowało do
wzorca.
function endfile(file)
{
if (! no_print && count_only)
if (do_filenames)
print file ":" fcount
else
print fcount
total += fcount
}
Poniższa reguła wykonuje większość pracy związanej z dopasowywaniem wierszy.
Zmienna matches będzie prawdziwa jeśli wiersz pasuje do wzorca.
Jeżeli użytkownik chce wierszy, które nie pasują, znaczenie matches
jest odwracane za pomocą operatora `!'. fcount zwiększane jest
o wartość matches, która będzie albo jedynką albo zerem, zależnie od
tego czy dopasowanie było pomyślne czy nie. Jeżeli wiersz nie pasuje, to
instrukcja next po prostu przechodzi do następnego.
W poniższych kilku wierszach kodu występuje parę optymalizacji. Jeżeli
użytkownik chce tylko kodu zakończenia (no_print jest prawdziwe),
i nie musimy zliczać wierszy, to wystarczy wiedzieć, że pasuje jeden wiersz
danego pliku, i możemy przeskoczyć do następnego pliku za pomocą
nextfile. Dalej, w podobnych wierszach, jeśli wypisujemy tylko nazwy
plików, a nie musimy zliczać wierszy, to możemy wypisać nazwę pliku,
a następnie dzięki nextfile przejść do następnego pliku.
Wreszcie, wypisywany jest każdy wiersz, z poprzedzającą go nazwą pliku i dwukropkiem, jeśli to konieczne.
{
matches = ($0 ~ pattern)
if (invert)
matches = ! matches
fcount += matches # 1 lub 0
if (! matches)
next
if (no_print && ! count_only)
nextfile
if (filenames_only && ! count_only) {
print FILENAME
nextfile
}
if (do_filenames && ! count_only)
print FILENAME ":" $0
else if (! count_only)
print
}
Reguła END czuwa nad utworzeniem poprawnego kodu zakończenia.
Jeżeli nie było żadnych dopasowań, kod zakończenia wynosi jeden,
w przeciwnym razie -- zero.
END \
{
if (total == 0)
exit 1
exit 0
}
W przypadku wystąpienia niepoprawnych opcji funkcja usage wypisuje
komunikat o sposobie wywoływania i kończy pracę programu.
function usage( e)
{
e = "Składnia: egrep [-csvil] [-e wzorzec] [pliki ...]"
print e > "/dev/stderr"
exit 1
}
Zmienną e zastosowano by funkcja ładnie mieściła się na drukowanej
stronie.
Mała uwaga o stylu programowania. Spostrzegłeś być może, że reguła
END wykorzystuje kontynuację odwrotnym ukośnikiem, z samotnym
nawiasem otwierającym w wierszu. Zrobiono to tak, by bardziej przypominało
sposób, w jaki zapisywane są funkcje. Wiele naszych przykładów
umieszczonych w tym rozdziale
stosuje ten styl. Sam zdecyduj, czy podoba ci się taki sposób zapisywania
własnych reguł BEGIN i END, czy nie.
Narzędzie id wypisuje dla danego użytkownika rzeczywisty
i efektywny identyfikator użytkownika, rzeczywisty i efektywny
identyfikator grupy, i zbiór grup użytkownika, jeśli są takie.
id wypisze efektywny identyfikator użytkownika i efektywny
identyfikator grupy tylko wtedy jeśli są one różne od rzeczywistych.
Jeśli to możliwe, poda także odpowiednie nazwy użytkowników i grup.
Wynik może wyglądać tak:
$ id -| uid=2076(arnold) gid=10(staff) groups=10(staff),4(tty)
Dane te są dokładnie takie same, jak te, które zapewnia występujący
w gawk plik specjalny `/dev/user'
(zob. 6.7. Specjalne nazwy plików w gawk).
Narzędzie id daje jednak przyjemniejsze wyjście niż sam tylko łańcuch
liczb.
Oto prosta wersja id napisana w awk.
Korzysta z funkcji bibliotecznych obsługujących bazę użytkowników
(zob. 15.11. Czytanie bazy użytkowników),
oraz z funkcji bibliotecznych do obsługi bazy grup
(zob. 15.12. Czytanie bazy grup).
Program jest całkiem prosty. Cała robota wykonywana jest
w regule BEGIN. Numery identyfikatorów użytkownika i grupy
uzyskiwane są z `/dev/user'. Jeżeli nie ma obsługi `/dev/user',
program poddaje się.
Kod powtarza się. Pozycja w bazie użytkowników dotycząca rzeczywistego identyfikatora użytkownika dzielona jest na dwukropkach `:' na części składowe. Nazwa jest pierwszym polem. Podobny kod wykorzystywany jest dla efektywnego identyfikatora użytkownika i identyfikatorów grupy.
# id.awk --- implementacja id w awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# wyjściem jest:
# uid=12(foo) euid=34(bar) gid=3(baz) \
# egid=5(blat) groups=9(nine),2(two),1(one)
BEGIN \
{
if ((getline < "/dev/user") < 0) {
err = "id: brak obsługi /dev/user - nie mogę działać"
print err > "/dev/stderr"
exit 1
}
close("/dev/user")
uid = $1
euid = $2
gid = $3
egid = $4
printf("uid=%d", uid)
pw = getpwuid(uid)
if (pw != "") {
split(pw, a, ":")
printf("(%s)", a[1])
}
if (euid != uid) {
printf(" euid=%d", euid)
pw = getpwuid(euid)
if (pw != "") {
split(pw, a, ":")
printf("(%s)", a[1])
}
}
printf(" gid=%d", gid)
pw = getgrgid(gid)
if (pw != "") {
split(pw, a, ":")
printf("(%s)", a[1])
}
if (egid != gid) {
printf(" egid=%d", egid)
pw = getgrgid(egid)
if (pw != "") {
split(pw, a, ":")
printf("(%s)", a[1])
}
}
if (NF > 4) {
printf(" groups=");
for (i = 5; i <= NF; i++) {
printf("%d", $i)
pw = getgrgid($i)
if (pw != "") {
split(pw, a, ":")
printf("(%s)", a[1])
}
if (i < NF)
printf(",")
}
}
print ""
}
Program split dzieli duże pliki tekstowe na mniejsze kawałki.
Domyślnie pliki wyjściowe nazywane są `xaa', `xab', i tak dalej.
Każdy z nich zawiera 1000 wierszy, z wyjątkiem, na ogół, ostatniego pliku.
Ilość wierszy w każdym pliku zmieniamy podając w wierszu poleceń liczbę
poprzedzoną znakiem minus, np. `-500' dla plików o 500 wierszach
zamiast 1000. Chcąc zmienić nazwę plików wyjściowych na coś w rodzaju
`mojplikaa', `mojplikab', i tak dalej, podajemy dodatkowy argument
określający zadaną nazwę.
Oto wersja split wykonana w awk. Wykorzystuje ona funkcje
ord i chr przedstawione
w 15.5. Konwersja między znakami a liczbami.
Program ustala najpierw wartości domyślne, a następnie sprawdza, czy nie ma zbyt wielu argumentów. Następnie po kolei przygląda się każdemu argumentowi. Pierwszy może być minusem, po którym występuje liczba. Jeśli tak jest, to wygląda on jak liczba ujemna, więc robimy z niej dodatnią i to jest liczba wierszy. Nazwa pliku danych jest pomijana, a ostatni argument jest wykorzystywany jako przedrostek nazw plików wynikowych.
# split.awk --- robi split w awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# składnia: split [-num] [plik] [nazwawyj]
BEGIN {
outfile = "x" # domyślne
count = 1000
if (ARGC > 4)
usage()
i = 1
if (ARGV[i] ~ /^-[0-9]+$/) {
count = -ARGV[i]
ARGV[i] = ""
i++
}
# sprawdźmy argv na wypadek czytania z stdin zamiast z pliku
if (i in ARGV)
i++ # pomiń nazwę pliku danych
if (i in ARGV) {
outfile = ARGV[i]
ARGV[i] = ""
}
s1 = s2 = "a"
out = (outfile s1 s2)
}
Następna reguła wykonuje większość pracy. tcount (temporary count -
licznik tymczasowy) zapamiętuje, ile wierszy do tej pory wypisano do pliku
wynikowego. Jeśli jest większa niż count, to pora zamknąć bieżący
plik i rozpocząć nowy. s1 i s2 pamiętają aktualne przyrostki
nazwy pliku. Jeżeli oba mają wartość `z', to plik danych jest
po prostu zbyt duży. W przeciwnym razie, s1 zmiania się na następną
na literę alfabetu a s2 zaczyna znów od `a'.
{
if (++tcount > count) {
close(out)
if (s2 == "z") {
if (s1 == "z") {
printf("split: %s jest za duży do podziału\n", \
FILENAME) > "/dev/stderr"
exit 1
}
s1 = chr(ord(s1) + 1)
s2 = "a"
} else
s2 = chr(ord(s2) + 1)
out = (outfile s1 s2)
tcount = 1
}
print > out
}
Funkcja usage po prostu wypisuje komunikat o błędzie i kończy pracę
programu.
function usage( e)
{
e = "składnia: split [-num] [plik] [nazwawyj]"
print e > "/dev/stderr"
exit 1
}
Zmienną e zastosowano by funkcja ładnie mieściła się na
stronie.
Program jest troszkę niechlujny; zdaje się na awk, by zamknął
za niego automatycznie ostatni plik, zamiast samemu zrobić to
w regule END.
Program tee znany jest jako "pipe fitting".(24)
tee kopiuje swoje standardowe wejście na standardowe
wyjście, a równocześnie powiela je do plików wymienionych w wierszu poleceń.
Ma taką składnię:
tee [-a] plik ...
Opcja `-a' mówi tee, by wykonywał dopisywanie do wskazanych
plików, zamiast obcinać je i zaczynać od nowa.
Reguła BEGIN najpierw w tablicy o nazwie copy tworzy kopię
wszystkich argumentów wiersza poleceń. ARGV[0] nie jest kopiowane,
gdyż nie jest potrzebne. tee nie może korzystać bezpośrednio
z ARGV, ponieważ awk będzie usiłował każdy plik wymieniony
w ARGV przetworzyć jako dane wejściowe.
Jeżeli pierwszym argumentem jest `-a', to zmienna znacznikowa
append otrzymuje wartość prawdziwą, i usuwane są ARGV[1]
i copy[1]. Jeżeli ARGC jest mniejsze niż dwa, to nie podano
nazw plików, i tee wypisuje komunikat o sposobie użycia i kończy
pracę. Na koniec, dzięki nadaniu ARGV[1] wartości "-",
a ARGC wartości dwa, na awk wymuszany jest odczyt standardowego
wyjścia.
# tee.awk --- tee w awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# Revised December 1995
BEGIN \
{
for (i = 1; i < ARGC; i++)
copy[i] = ARGV[i]
if (ARGV[1] == "-a") {
append = 1
delete ARGV[1]
delete copy[1]
ARGC--
}
if (ARGC < 2) {
print "składnia: tee [-a] plik ..." > "/dev/stderr"
exit 1
}
ARGV[1] = "-"
ARGC = 2
}
Pojedyncza reguła wykonuje całą pracę. Ponieważ nie ma tu wzorca, wykonywana jest dla każdego wiersza wejścia. Ciało reguły po prostu wypisuje dany wiersz do każdego pliku z wiersza poleceń, a następnie na standardowe wyjście.
{
# przesunięcie if poza pętlę przyspiesza ją
if (append)
for (i in copy)
print >> copy[i]
else
for (i in copy)
print > copy[i]
print
}
Można by było zakodować pętlę w ten sposób:
for (i in copy)
if (append)
print >> copy[i]
else
print > copy[i]
jest to bardziej zwięzłe, ale równocześnie mniej efektywne. Warunek `if'
jest sprawdzany dla każdego rekordu i każdego pliku wyjściowego. Dzięki
powieleniu ciała pętli, `if' sprawdzany jest tylko raz dla każdego
rekordu wejściowego. Jeżeli mamy N rekordów wejściowych i M
plików wejściowych, to pierwsza metoda wykonuje N instrukcji
`if', podczas gdy druga wykonałaby N*M instrukcji
`if'.
Na koniec, reguła END robi porządki, zamykając wszystkie pliki
wyjściowe.
END \
{
for (i in copy)
close(copy[i])
}
Narzędzie uniq czyta posortowane wiersze danych ze swego
standardowego wejścia, i (domyślnie) usuwa dublujące się wiersze.
Inaczej mówiąc, wypisywane są tylko niepowtarzalne, unikatowe wiersze,
stąd nazwa. uniq ma kilka opcji. Wywoływany jest tak:
uniq [-udc [-n]] [+n] [ plik wejściowy [ plik wyjściowy ]]
Opcje te oznaczają:
-d
-u
-c
-n
awk: nie-białe znaki oddzielone ciągami spacji i/lub
tabulacji.
+n
plik wejściowy
plik wyjściowy
Normalnie uniq zachowuje się tak, jakby podano równocześnie opcje
`-d' i `-u'.
Oto realizacja uniq w awk. Wykorzystuje funkcje biblioteczne
getopt (zob. 15.10. Przetwarzanie opcji wiersza poleceń),
i join (zob. 15.6. Scalanie tablicy w łańcuch).
Program zaczyna się od funkcji usage a następnie komentarza
zawierającego krótki zarys opcji i ich znaczenia.
Reguła BEGIN zajmuje się argumentami i opcjami wiersza poleceń.
Korzysta z pewnej sztuczki, by uzyskać od getopt obsługę opcji
postaci `-25'. Traktuje mianowicie taką opcję jako literę opcji
`2' z argumentem `5'. Jeżeli faktycznie podano dwie lub więcej
cyfr (Optarg wygląda jak liczba), to Optarg jest sklejane
z cyfrą opcji, a następnie do wyniku jest dodawane zero, by zrobić z niego
liczbę. Jeśli opcja składa się tylko z jednej cyfry, to Optarg nie
jest potrzebne, a Optind musi zostać zmniejszone, by getopt
przetworzyła je następnym razem. Ten kod jest niewątpliwie nieco zawiły.
Jeżeli nie podano opcji, to brane są domyślne, wypisywanie zarówno
powtarzających się jak i unikatowych wierszy. Plik wyjściowy, jeśli go
podano, jest przypisywany do outputfile. Wcześniej outputfile
było inicjowane jako standardowe wyjście, `/dev/stdout'.
# uniq.awk --- robi uniq w awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
function usage( e)
{
e = "składnia: uniq [-udc [-n]] [+n] [ wej [ wyj ]]"
print e > "/dev/stderr"
exit 1
}
# -c zlicza (count) wiersze. przesłania -d i -u
# -d tylko zdublowane wiersze
# -u tylko unikatowe wiersze
# -n pomija n pól
# +n pomija n znaków, najpierw pomija pola
BEGIN \
{
count = 1
outputfile = "/dev/stdout"
opts = "udc0:1:2:3:4:5:6:7:8:9:"
while ((c = getopt(ARGC, ARGV, opts)) != -1) {
if (c == "u")
non_repeated_only++
else if (c == "d")
repeated_only++
else if (c == "c")
do_count++
else if (index("0123456789", c) != 0) {
# getopt wymaga argumentów opcji, co
# gmatwa sprawę dla rzeczy typu -5
if (Optarg ~ /^[0-9]+$/)
fcount = (c Optarg) + 0
else {
fcount = c + 0
Optind--
}
} else
usage()
}
if (ARGV[Optind] ~ /^\+[0-9]+$/) {
charcount = substr(ARGV[Optind], 2) + 0
Optind++
}
for (i = 1; i < Optind; i++)
ARGV[i] = ""
if (repeated_only == 0 && non_repeated_only == 0)
repeated_only = non_repeated_only = 1
if (ARGC - Optind == 2) {
outputfile = ARGV[ARGC - 1]
ARGV[ARGC - 1] = ""
}
}
Poniższa funkcja, are_equal, porównuje bieżący wiersz, $0,
z poprzednim, last. Obsługuje pomijanie pól i znaków.
Jeżeli nie podano ani liczby pól ani znaków, are_equal zwraca po
prostu jeden lub zero, w zależności od wyniku zwykłego porównania łańcuchów
last i $0. W przeciwnym przypadku, sprawy się komplikują.
Jeżeli mają zostać pominięte pola, to każdy wiersz rozbijany jest w tablicę
za pomocą split
(zob. 12.3. Funkcje wbudowane działające na łańcuchach),
a następnie żądane pola za pomocą join są ponownie łączone w wiersz.
Złączone wiersze przechowywane są w clast i cline.
Jeżeli nie pomija się pól, to clast i cline otrzymują
wartości, odpowiednio, last i $0.
Wreszcie, jeżeli pomijane są znaki, to do usunięcia początkowych
charcount znaków z clast i cline wykorzystywana jest
funkcja substr. Oba łańcuchy są następnie porównywane,
a are_equal zwraca wynik porównania.
function are_equal( n, m, clast, cline, alast, aline)
{
if (fcount == 0 && charcount == 0)
return (last == $0)
if (fcount > 0) {
n = split(last, alast)
m = split($0, aline)
clast = join(alast, fcount+1, n)
cline = join(aline, fcount+1, m)
} else {
clast = last
cline = $0
}
if (charcount) {
clast = substr(clast, charcount + 1)
cline = substr(cline, charcount + 1)
}
return (clast == cline)
}
Poniższe dwie reguły stanowią ciało programu. Pierwsza z nich jest
wykonywana wyłącznie dla pierwszego wiersza danych. Nadaje last
wartość $0, by kolejne wierszy tekstu mogły być z czymś porównane.
Druga reguła realizuje nasze zadanie. Zmienna equal będzie jedynką
lub zerem w zależności od wyniku porównania wykonanego przez are_equal.
Jeżeli uniq zlicza powtarzające się wiersze, to jeśli wiersze są
równe zwiększana jest zmienna count. W przeciwnym razie wypisuje się
wiersz, a zmienna jest sprowadzana do stanu początkowego, gdyż dane dwa
wiersze nie są równe.
Jeżeli uniq nie zlicza, to count zwiększane jest jeśli
wiersze są równe. W przeciwnym razie, jeśli uniq zlicza powtarzające
się wiersze, a spostrzeżono więcej niż jeden wiersz taki jak bieżący, lub
też jeśli uniq zlicza unikatowe wiersze, a zauważono tylko jeden
wiersz, to wiersz ten jest wypisywany, a count jest zerowane.
Na koniec, podobne rozwiązanie użyte jest w regule END do wypisania
ostatniego wiersza danych wejściowych.
NR == 1 {
last = $0
next
}
{
equal = are_equal()
if (do_count) { # przesłania -d i -u
if (equal)
count++
else {
printf("%4d %s\n", count, last) > outputfile
last = $0
count = 1 # stan wyjściowy
}
next
}
if (equal)
count++
else {
if ((repeated_only && count > 1) ||
(non_repeated_only && count == 1))
print last > outputfile
last = $0
count = 1
}
}
END {
if (do_count)
printf("%4d %s\n", count, last) > outputfile
else if ((repeated_only && count > 1) ||
(non_repeated_only && count == 1))
print last > outputfile
}
Narzędzie wc (word count - zliczanie wyrazów) zlicza wiersze,
wyrazy i znaki z jednego lub więcej plików wejściowych. Ma taką składnię:
wc [-lwc] [ pliki ... ]
Jeżeli w wierszu poleceń nie podano plików, wc czyta swoje
standardowe wejście. Jeśli jest więcej plików, to wypisze także
całkowite ilości dla wszystkich plików. Ma następujące opcje:
-l
-w
awk jest to
normalna metoda rozdzielania pól w danych wejściowych.
-c
Realizacja wc w awk jest szczególnie elegancka, gdyż awk
wykonuje za nas mnóstwo pracy: dzieli wiersze na wyrazy (tj. pola) i zlicza
je, zlicza wiersze (tj. rekordy), i łatwo możemy się od niego dowiedzieć,
jak długi jest wiersz.
Ta wersja korzysta z funkcji bibliotecznej getopt
(zob. 15.10. Przetwarzanie opcji wiersza poleceń) oraz
funkcji obsługi przejść między plikami
(zob. 15.9. Obsługa przejść między plikami).
Wykazuje ona zasadniczą różnicę w stosunku do tradycyjnych wersji wc.
Nasza wersja zawsze wypisuje uzyskane liczby w kolejności: wiersze, wyrazy
i znaki. Wersje tradycyjne zwracają uwagę na kolejność występowania opcji
`-l', `-w' i `-c' w wierszu poleceń, i wypisują liczby w tej
kolejności.
Reguła BEGIN wykonuje przetwarzanie argumentów.
Zmienna print_total będzie prawdziwa jeśli w wierszu poleceń
wymieniono więcej niż jeden plik.
# wc.awk --- zlicza wiersze, wyrazy, znaki
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# Options:
# -l zlicza tylko wiersze
# -w zlicza tylko wyrazy
# -c zlicza tylko znaki
#
# domyślnie zliczane są wiersze, wyrazy, znaki
BEGIN {
# niech getopt wypisze komunikat o nieprawidłowych
# opcjach. my je ignorujemy
while ((c = getopt(ARGC, ARGV, "lwc")) != -1) {
if (c == "l")
do_lines = 1
else if (c == "w")
do_words = 1
else if (c == "c")
do_chars = 1
}
for (i = 1; i < Optind; i++)
ARGV[i] = ""
# jeśli bez opcji, rób wszystkie
if (! do_lines && ! do_words && ! do_chars)
do_lines = do_words = do_chars = 1
print_total = (ARGC - i > 2)
}
Funkcja beginfile jest prosta: zeruje tylko liczniki wierszy, wyrazów
i znaków i zapamiętuje nazwę bieżącego pliku w fname.
Funkcja endfile dodaje liczby dotyczące bieżącego pliku do
narastających sum wierszy, wyrazów i znaków. Następnie wypisuje liczniki
dotyczące właśnie przeczytanego pliku. Zakłada, że zerowanie liczb dla
kolejnego pliku danych wykona beginfile.
function beginfile(file) {
chars = lines = words = 0
fname = FILENAME
}
function endfile(file)
{
tchars += chars
tlines += lines
twords += words
if (do_lines)
printf "\t%d", lines
if (do_words)
printf "\t%d", words
if (do_chars)
printf "\t%d", chars
printf "\t%s\n", fname
}
Mamy tu jedną regułę, wykonywaną dla każdego wiersza. Dodaje ona długość
rekordu do chars. Musi jeszcze dodać jeden, gdyż znak nowej linii
rozdzielający rekordy (wartość RS) sam nie jest częścią rekordu.
lines z każdym przeczytanym wierszem zwiększa się o jeden, a
words o NF, liczbę "wyrazów"
w tym wierszu.(25)
Na koniec, reguła END po prostu wypisuje sumy całkowite dla
wszystkich plików.
# robi po jednym wierszu
{
chars += length($0) + 1 # weź znak newline
lines++
words += NF
}
END {
if (print_total) {
if (do_lines)
printf "\t%d", tlines
if (do_words)
printf "\t%d", twords
if (do_chars)
printf "\t%d", tchars
print "\ttotal"
}
}
awkTa sekcja to wielki "worek" pełen rozmaitych programów. Mam nadzieję, że uznasz je za interesujące i przyjemne.
Powszechnym błędem przy pisaniu dużej ilości prozy jest przypadkowe powtórzenie słów. Często można to zauważyć w tekście jako coś w rodzaju "Ten ten program robi następujące ...". Jeżeli tekst jest w postaci elektronicznej, często zdublowane wyrazy występują na końcu jednego wiersza i na początku następnego, co powoduje, że są bardzo trudne do zauważenia.
Opisywany program, `dupword.awk', przegląda plik po jednym wierszu,
szukając sąsiadujących wystąpień tego samego słowa. Zapamiętuje także
ostatni wyraz w wierszu (w zmiennej prev), by móc go porównać
z pierwszym wyrazem następnego wiersza.
Pierwsze dwie instrukcje zapewniają, że wiersz będzie w całości małymi literami, więc, na przykład, "Ten" i "ten" przy porównywaniu będą takie same. Druga instrukcja usuwa z wiersza wszystkie znaki, które nie są ani alfanumeryczne ani białymi znakami, by interpunkcja również nie wpływała na porównywanie. Prowadzi to czasem do meldunków o powtórzonych słowach, które w rzeczywistości są różne, ale zdarza się to rzadko.
# dupword --- znajduje w tekście zdublowane słowa
# Arnold Robbins, arnold@gnu.org, Public Domain
# December 1991
{
$0 = tolower($0)
gsub(/[^A-Za-z0-9 \t]/, "");
if ($1 == prev)
printf("%s:%d: zdublowane %s\n",
FILENAME, FNR, $1)
for (i = 2; i <= NF; i++)
if ($i == $(i-1))
printf("%s:%d: zdublowane %s\n",
FILENAME, FNR, $i)
prev = $NF
}
Poniższy program jest prostym "budzikiem". Podajemy mu godzinę i opcjonalny komunikat. O zadanej porze wypisuje komunikat na standardowym wyjściu. Dodatkowo, można podać mu, ile razy ma być powtórzony komunikat, a także jaka ma być zwłoka między powtórzeniami.
Program wykorzystuje funkcję gettimeofday z
15.8. Obsługa daty i czasu.
Cała praca wykonywana jest w regule BEGIN. Pierwszą częścią jest
sprawdzenie argumentów i nadanie ustawień domyślnych: opóźnienia, liczby
powtórzeń i komunikatu do wypisania. Jeżeli użytkownik podał komunikat, ale
nie zawiera on znaku ASCII BEL (znanego jako znak "dzwonka", alarmu,
`\a'), to jest on dodawany do komunikatu. (W wielu systemach wypisanie
ASCII BEL daje jakiś rodzaj sygnału dźwiękowego. Zatem, wysyłając sygnał
ostrzegawczy, system zwraca na siebie uwagę, na wypadek gdyby użytkownik nie
patrzył na komputer czy terminal.)
# alarm --- ustawia alarm
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# składnia: alarm czas [ "komunikat" [ ile_razy [ zwłoka ] ] ]
BEGIN \
{
# wstępne sprawdzenie poprawności argumentów
skladn1 = "składnia: alarm czas ['komunikat' [ile_razy [zwłoka]]]"
skladn2 = sprintf("\t(%s) czas ::= hh:mm", ARGV[1])
if (ARGC < 2) {
print skladn1 > "/dev/stderr"
exit 1
} else if (ARGC == 5) {
zwloka = ARGV[4] + 0
ile_razy = ARGV[3] + 0
komunikat = ARGV[2]
} else if (ARGC == 4) {
ile_razy = ARGV[3] + 0
komunikat = ARGV[2]
} else if (ARGC == 3) {
komunikat = ARGV[2]
} else if (ARGV[1] !~ /[0-9]?[0-9]:[0-9][0-9]/) {
print skladn1 > "/dev/stderr"
print skladn2 > "/dev/stderr"
exit 1
}
# ustawienia domyślne akcji
# po osiągnięciu zadanego czasu
if (zwloka == 0)
zwloka = 180 # 3 minuty
if (ile_razy == 0)
ile_razy = 5
if (komunikat == "")
komunikat = sprintf("\aTeraz jest %s!\a", ARGV[1])
else if (index(komunikat, "\a") == 0)
komunikat = "\a" komunikat "\a"
Kolejny fragment kodu zamienia czas alarmu na godziny i minuty, i, jeśli to konieczne, przekształca go na czas 24-godzinny. Potem zamienia go na liczbę sekund od północy. Następnie na liczbę sekund od północy zamienia czas bieżący. Różnica pomiędzy tymi dwoma liczbami wskazuje, jak długo należy odczekać przed wywołaniem alarmu.
# podział czasu docelowego
split(ARGV[1], aczas, ":")
godz = aczas[1] + 0 # wymuś numeryczne
min = aczas[2] + 0 # wymuś numeryczne
# pobranie bieżącego czasu podzielonego na składowe
gettimeofday(teraz)
# jeśli podano czas w formacie 12-godzinnym i jest
# już po tej godzinie, np. `alarm 5:30' o 9 rano
# znaczy 5:30 po południu, to dodajmy 12 do faktycznej
# godziny
if (godz < 12 && teraz["godzina"] > godz)
godz += 12
# ustal czas docelowy w sekundach od północy
cel = (godz * 60 * 60) + (min * 60)
# weź czas bieżący w sekundach od północy
biezacy = (teraz["godzina"] * 60 * 60) + \
(teraz["minuta"] * 60) + teraz["sekunda"]
# jak długo odczekać
czasdrzemki = cel - biezacy
if (czasdrzemki <= 0) {
print "czas w przeszłości!" > "/dev/stderr"
exit 1
}
Na koniec, program wykorzystuje funkcję system
(zob. 12.4. Wbudowane funkcje wejścia/wyjścia) do wywołania
narzędzia sleep. Narzędzie to odczekuje po prostu zadaną liczbę
sekund. Jeśli kodem zakończenia nie jest zero, to program przyjmuje, że
przerwano sleep, i kończy pracę. Jeżeli sleep zakończyło
działanie ze statusem OK (zero), to program wypisuje komunikat w pętli,
ponownie stosując sleep do odczekania tylu sekund, ile to konieczne.
# chrrrr.... idź precz jeśli ci przerwą
if (system(sprintf("sleep %d", czasdrzemki)) != 0)
exit 1
# czas na powiadomienie!
polecenie = sprintf("sleep %d", zwloka)
for (i = 1; i <= ile_razy; i++) {
print komunikat
# jeśli przerwano polecenie sleep, idź sobie
if (system(polecenie) != 0)
break
}
exit 0
}
Systemowe narzędzie tr transliteruje znaki [tłum: zamienia znaki
pewnego zestawu na znaki innego zestawu]. Na przykład, często używane jest
do przekształcenia dużych liter na małe, w celu dalszego przetwarzania.
tworzenie danych | tr '[A-Z]' '[a-z]' | przetwarzanie danych ...
tr podajemy dwie listy znaków objęte nawiasami kwadratowymi.
Zazwyczaj, listy są ujmowane w znaki cytowania, by powstrzymać powłokę przed
próbą wykonania rozwinięcia nazw plików.(26)
Przy przetwarzaniu wejścia, pierwszy znak pierwszej listy zastępowany jest
pierwszym znakiem drugiej listy, drugi znak pierwszej drugim znakiem
drugiej, i tak dalej. Jeżeli lista "z" ma więcej znaków niż lista "na",
to dla pozostałych znaków listy "z" jest używany ostatni znak listy
"na".
Jakiś czas temu,
pewien użytkownik zaproponował nam, byśmy dodali funkcję transliteracji do
gawk. Będąc przeciwnym "efektowi choinki" [tłum.: przeładowanie
programu funkcjami], napisałem poniższy program, by udowodnić, że
transliterację znaków można zrobić za pomocą funkcji użytkownika.
Program ten nie jest tak kompletny, jak systemowe narzędzie tr,
ale generalnie spełnia zadanie.
Program translate ukazuje jedną z kilku słabości standardowego
awk: zajmowanie się pojedynczymi znakami jest bardzo uciążliwe,
wymagając powtarzanego stosowania funkcji wbudowanych substr,
index i gsub
(zob. 12.3. Funkcje wbudowane działające na łańcuchach).(27)
Mamy tu dwie funkcje. Pierwsza stranslate, pobiera trzy argumenty.
z
na
cel
Tablice asocjacyjne powodują, że część wykonująca konwersję znaków jest
całkiem łatwa. t_ar przechowuje znaki "na", indeksowane znakami
"z". Następnie przez z przechodzi zwykłą pętla, po jednym znaku
naraz. Dla każdego znaku w z, jeśli znak ten pojawia się
w cel, wykorzystywana jest gsub, zmieniająca go na odpowiedni
znak na.
Funkcja translate po prostu wywołuje stranslate używając jako
celu $0. Program główny inicjuje dwie zmienne globalne, Z
i NA, według wiersza poleceń, a następnie zmienia ARGV, tak
że awk będzie czytał ze standardowego wejścia.
Wreszcie, reguła przetwarzania po prostu dla każdego rekordu wywołuje
translate.
# translate --- robi rzeczy podobne do tr
# Arnold Robbins, arnold@gnu.org, Public Domain
# August 1989
# błędy: nie obsługuje rzeczy typu: tr A-Z a-z, muszą
# być przeliterowane. Jeśli jednak `na' jest krótsze od `z',
# to dla reszty `z' jest stosowany ostatni znak z `na'.
function stranslate(z, na, cel, dl_z, dl_na, t_tr, i, c)
{
dl_z = length(z)
dl_na = length(na)
for (i = 1; i <= dl_na; i++)
t_tr[substr(z, i, 1)] = substr(na, i, 1)
if (dl_na < dl_z)
for (; i <= dl_z; i++)
t_tr[substr(z, i, 1)] = substr(na, dl_na, 1)
for (i = 1; i <= dl_z; i++) {
c = substr(z, i, 1)
if (index(cel, c) > 0)
gsub(c, t_tr[c], cel)
}
return cel
}
function translate(z, na)
{
return $0 = stranslate(z, na, $0)
}
# program główny
BEGIN {
if (ARGC < 3) {
print "składnia: translate z na" > "/dev/stderr"
exit
}
Z = ARGV[1]
NA = ARGV[2]
ARGC = 2
ARGV[1] = "-"
}
{
translate(Z, NA)
print
}
Mimo że możliwe jest wykonanie transliteracji znaków w funkcji
zdefiniowanej na poziomie użytkownika, niekoniecznie jest to efektywne,
i zaczęliśmy się zastanawiać nad dodaniem funkcji wbudowanej. Jednak,
wkrótce po napisaniu tego programu, dowiedzieliśmy się, że w awk
z System V Release 4 dodano funkcje toupper i tolower.
Funkcje te obsługują przeważającą większość przypadków, w których
transliteracja jest niezbędna, zatem woleliśmy po prostu dołożyć te
funkcje również do gawk, a zostawić w spokoju całą resztę.
Oczywistym udoskonaleniem tego programu byłoby inicjowanie tablicy
t_tr tylko raz, w regule BEGIN. Zakłada to jednak, że
listy "z" i "na" w trakcie działania programu nigdy się nie zmienią.
Oto przykład "rzeczywistego"(28) programu. Ten skrypt czyta listę nazw i adresów, i tworzy etykiety adresowe. Na każdej stronie jest 20 etykiet, dwie w poziomie i dziesięć w pionie. Adresy na pewno nie będą większe niż pięć wierszy danych. Każdy z adresów oddzielony jest od następnego pustym wierszem.
Podstawowym pomysłem jest odczytanie danych o 20 etykietach. Każdy
wiersz każdej etykiety przechowywany jest w tablicy line.
Jedyna reguła główna zajmuje się wypełnianiem tej tablicy i wypisywaniem
strony po przeczytaniu 20 etykiet.
Reguła BEGIN po prostu przypisuje RS łańcuch pusty, tak by
awk dzielił rekordy w miejscu pustych wierszy
(zob. 5.1. Jak wejście dzielone jest na rekordy). Nadaje
MAXLINES wartość 100, gdyż MAXLINE jest maksymalną liczbą
wierszy na stronie (20 * 5 = 100).
Większość pracy wykonywane jest w funkcji printpage. Wiersze
etykiet składowane są kolejno w tablicy line. Muszą one jednak
zostać wydrukowane poziomo: line[1] obok line[6],
line[2] obok line[7], i tak dalej. Aby to osiągnąć
wykorzystano dwie pętle. Zewnętrzna, sterowana przez i, przechodzi
przez co dziesiąty wiersz danych, to jest każdy rząd etykiet.
Wewnętrzna pętla, kontrolowana przez j, przechodzi przez wiersze tego
rzędu. Ponieważ j zmienia się od zera do czterech, `i+j' jest
j-tym wierszem w i-tym rzędzie etykiet, a `i+j+5'
jest wpisem obok niej. Wynik wygląda ostatecznie mniej więcej tak:
wiersz 1 wiersz 6 wiersz 2 wiersz 7 wiersz 3 wiersz 8 wiersz 4 wiersz 9 wiersz 5 wiersz 10
Zauważmy na koniec, że przy wierszach numer 21 i 61, wypisywany jest dodatkowy pusty wiersz, by utrzymać wyrównane etykiety na wyjściu. Zależy to od konkretnego rodzaju etykiet wykorzystywanych w czasie, gdy był pisany program. Zwróć też uwagę, że są tu dwa puste wiersze na górze strony i dwa puste na dole.
Reguła END organizuje opróżnienie bufora ostatniej strony etykiet:
w danych mogła nie wystąpić pełna wielokrotność 20 etykiet.
# labels.awk
# Arnold Robbins, arnold@gnu.org, Public Domain
# June 1992
# Program do wydruku etykiet. Każda etykieta to 5 wierszy
# danych. Mogą one zawierać puste wiersze. Arkusze
# etykiet mają po 2 puste wiersze na górze i 2 na dole.
BEGIN { RS = "" ; MAXLINES = 100 }
function printpage( i, j)
{
if (Nlines <= 0)
return
printf "\n\n" # nagłówek
for (i = 1; i <= Nlines; i += 10) {
if (i == 21 || i == 61)
print ""
for (j = 0; j < 5; j++) {
if (i + j > MAXLINES)
break
printf " %-41s %s\n", line[i+j], line[i+j+5]
}
print ""
}
printf "\n\n" # stopka
for (i in line)
line[i] = ""
}
# reguła główna
{
if (Count >= 20) {
printpage()
Count = 0
Nlines = 0
}
n = split($0, a, "\n")
for (i = 1; i <= n; i++)
line[++Nlines] = a[i]
for (; i <= 5; i++)
line[++Nlines] = ""
Count++
}
END \
{
printpage()
}
Poniższy program awk wypisuje liczbę wystąpień każdego słowa ze
swojego wejścia. Przez wykorzystanie łańcuchów jako indeksów
ilustruje skojarzeniową naturę tablic awk. Demonstruje także
konstrukcję `for x in tablica'. Wreszcie,
pokazuje, w jaki sposób można wykorzystać awk w połączeniu z innymi
programami narzędziowymi do wykonania stosunkowo złożonych, użytecznych
zadań przy minimum wysiłku.
Po listingu programu zamieszczono nieco wyjaśnień.
awk '
# wypisuje częstości słów
{
for (i = 1; i <= NF; i++)
czest[$i]++
}
END {
for (slowo in czest)
printf "%s\t%d\n", slowo, czest[slowo]
}'
Pierwsza rzecz, na jaką warto zwrócić uwagę w tym programie to to, że ma on
dwie reguły. Pierwsza, ponieważ ma pusty wzorzec, wykonywana jest na każdym
wierszu wejścia. Wykorzystuje dostępny w awk mechanizm dostępu do
pól (zob. 5.2. Badanie pól) do wyłapania z wiersza poszczególnych
słów, a zmienną wbudowaną NF (zob. 10. Zmienne wbudowane) do
rozpoznania, ile jest dostępnych pól.
Dla każdego pola wejściowego zwiększany jest element tablicy czest,
by odzwierciedlał, że wyraz ten widziano kolejny raz.
Druga reguła, ponieważ ma wzorzec END, nie jest wykonywana aż
do momentu wyczerpania wejścia. Wypisuje ona zawartość tablicy
czest, która została skonstruowana wewnątrz pierwszej akcji.
Program ma kilka niedociągnięć, które nie pozwalają, by mógł być przydatny z rzeczywistymi plikami tekstowymi:
awk mówiącej, że pola są
oddzielone białym znakiem, i że inne znaki w wejściu (za wyjątkiem znaków
nowej linii) nie mają żadnego specjalnego znaczenia dla awk. Znaczy
to, że znaki interpunkcyjne uważane są za części wyrazów.
awk uważa duże i małe litery za różne. Stąd też,
`bartender' i `Bartender' nie są traktowane jak to samo słowo.
Jest to niepożądane, gdyż w zwykłym tekście słowa rozpoczynające zdanie
pisane są z dużej litery i analizator częstości nie powinien być wrażliwy
na wielkość liter.
Metodą rozwiązania tego problemu jest wykorzystanie pewnych bardziej
zaawansowanych cech języka awk. Najpierw, skorzystamy
z tolower by usunąć różnice w wielkości liter. Następnie,
wykorzystamy gsub do usunięcia znaków interpunkcyjnych. Na koniec,
użyjemy systemowego narzędzia sort by przetworzyć wyjście
naszego skryptu awk. Oto nowa wersja programu:
# Wypisuje częstości słów
{
$0 = tolower($0) # usuwa różnice między
# dużymi a małymi literami
gsub(/[^a-z0-9_ \t]/, "", $0) # usuwa interpunkcję
for (i = 1; i <= NF; i++)
czest[$i]++
}
END {
for (slowo in czest)
printf "%s\t%d\n", slowo, czest[slowo]
}
Zakładając, że zapisaliśmy ten program w pliku o nazwie `wordfreq.awk', a dane są w `plik1', poniższy potok
awk -f wordfreq.awk plik1 | sort +1 -nr
tworzy tabelę słów pojawiających się w `plik1' uporządkowanych w kolejności malejącej częstości.
Program awk odpowiednio gromadzi dane i tworzy tabelę częstości
wyrazów, która nie jest uporządkowana.
Wyjście skryptu awk jest następnie sortowane przez narzędzie
sort i wypisywane na terminalu. Opcje podane sort w tym
przykładzie mówią, że sortowanie powinno być według drugiego pola każdego
wiersza wejściowego (pominięcie jednego pola), klucze sortowania powinny być
traktowane jak wielkości numeryczne (inaczej `15' byłoby przed
`5'), i że sortowanie ma być wykonane w porządku malejącym
(reverse, odwrotnym).
Moglibyśmy nawet zrobić sort z wnętrza programu, zmieniając akcję
END na:
END {
sort = "sort +1 -nr"
for (slowo in czest)
printf "%s\t%d\n", slowo, czest[slowo] | sort
close(sort)
}
Musielibyśmy użyć tej metody na systemach, które nie mają prawdziwych potoków.
Więcej o sposobie korzystania z programu sort można znaleźć w ogólnej
dokumentacji systemu operacyjnego.
Program uniq
(zob. 16.1.6. Wypisywanie nie powtarzających się wierszy tekstu),
usuwa zdublowane wiersze z posortowanych danych.
Załóżmy jednak, że potrzebujemy usunąć powtarzające się wiersze z pliku danych, ale zachowując kolejność wierszy? Dobrym przykładem może tu być plik historii poleceń powłoki. Plik historii przechowuje kopię każdego wprowadzonego polecenia, a nie jest niczym nietypowym kilkakrotne powtarzanie tego samego polecenia. Chcemy od czasu do czasu kondensować plik historii przez usunięcie powielonych pozycji. Nadal jednak pożądane jest zachowanie pierwotnej kolejności poleceń.
Zadanie takie wykonuje poniższy prosty program. Wykorzystuje dwie tablice.
Tablica dane indeksowana jest tekstem każdego wiersza. Dla każdego
wiersza inkrementowane jest dane[$0].
Jeśli jakiegoś konkretnego wiersza nie napotkano wcześniej, to
dane[$0] będzie zerem. W tym przypadku, jego tekst zapamiętywany
jest w wiersze[ile]. Każdy element w tablicy wiersze jest
niepowtarzalnym poleceniem, a indeksy tej tablicy wskazują na kolejność w
jakiej napotkano te wiersze. Reguła wypisuje po prostu wiersze, po kolei.
# histsort.awk --- upakowanie pliku historii powłoki
# Arnold Robbins, arnold@gnu.org, Public Domain
# May 1993
# Dzięki Byronowi Rakitzis za ogólny pomysł
{
if (dane[$0]++ == 0)
wiersze[++ile] = $0
}
END {
for (i = 1; i <= ile; i++)
print wiersze[i]
}
Program stanowi też podstawę do generowania innych przydatnych danych.
Na przykład, zastosowanie poniższej instrukcji print w regule
END wskazałoby, jak często używane były poszczególne polecenia.
print dane[lines[i]], wiersze[i]
Fragment ten działa prawidłowo, ponieważ dane[$0] inkrementowano
przy każdym wystąpieniu wiersza.
Ten i poprzedni rozdział
( 15. Biblioteczka funkcji awk),
przedstawiają wiele programów awk.
Jeżeli chcielibyśmy poeksperymentować z tymi programami, konieczność
wpisywania ich ręcznie byłaby nudna. Pokażemy tu program, który potrafi
wydzielić części pliku wejściowego Texinfo do oddzielnych plików.
Niniejszą książkę napisano w Texinfo, języku formatowania dokumentów projektu GNU. Pojedynczy plik źródłowy można wykorzystać do utworzenia zarówno dokumentacji drukowanej, jak i elektronicznej.
Texinfo opisano w pełni w Texinfo--The GNU Documentation Format, dostępnym z Free Software Foundation.
Do naszych celów wystarczy wiedzieć trzy rzeczy o plikach wejściowych Texinfo.
awk. Dosłowne symbole `@' reprezentowane
są w plikach źródłowych Texinfo jako `@@'.
Poniższy program, `extract.awk', czyta plik źródłowy Texinfo i,
w oparciu o wspomniane specjalne komentarze, robi dwie rzeczy.
W momencie zauważenia `@c system ...' wykonuje polecenie,
wyodrębniając tekst polecenia z wiersza sterującego i przesyłając go
do funkcji
system (zob. 12.4. Wbudowane funkcje wejścia/wyjścia).
Po zauważeniu `@c file nazwapliku', każdy następny wiersz
wysyłany jest do pliku nazwapliku, do momentu
napotkania `@c endfile'.
Reguły w `extract.awk' dopasowują `@c' lub `@comment',
dzięki użyciu części `omment' jako opcjonalnej. Wiersze zawierające
`@group' i `@end group' są po prostu usuwane.
`extract.awk' korzysta z funkcji bibliotecznej join
(zob. 15.6. Scalanie tablicy w łańcuch).
Wszystkie programy przykładowe w źródle Texinfo książki Efektywne programowanie w AWK
(`gawk.texi') zostały ujęte między wiersze `file'
i `endfile'. Dystrybucja gawk wykorzystuje kopię
`extract.awk' do wydzielenia programów przykładowych i instalacji
wielu z nich w standardowym katalogu, gdzie może je znaleźć gawk,
Plik Texinfo wygląda podobnie jak to:
...
Ten program ma blok @code{BEGIN},
wypisujący miły komunikat:
@example
@c file examples/messages.awk
BEGIN @{ print "Nie panikuj!" @}
@c end file
@end example
Wypisuje też pewną końcową radę:
@example
@c file examples/messages.awk
END @{ print "Zawsze unikaj znudzonych archeologów!" @}
@c end file
@end example
...
`extract.awk' zaczyna od nadania IGNORECASE wartości jeden, co
powoduje, że mieszanie dużych i małych liter w dyrektywach nie będzie mieć
znaczenia.
Pierwsza reguła obsługuje wywołanie instrukcji system, sprawdzając
czy podano polecenie (NF równe co najmniej trzy). Sprawdza też
czy polecenie to zakończyło pracę z kodem zerowym, znaczącym OK.
# extract.awk --- wydziela pliki i uruchamia programy
# z plików texinfo
# Arnold Robbins, arnold@gnu.org, Public Domain, May 1993
BEGIN { IGNORECASE = 1 }
/^@c(omment)?[ \t]+system/ \
{
if (NF < 3) {
e = (FILENAME ":" FNR)
e = (e ": źle zbudowany wiersz `system'")
print e > "/dev/stderr"
next
}
$1 = ""
$2 = ""
stat = system($0)
if (stat != 0) {
e = (FILENAME ":" FNR)
e = (e ": ostrzeżenie: system zwrócił " stat)
print e > "/dev/stderr"
}
}
Zmienną e zastosowano by funkcja ładnie mieściła się na
stronie.
Druga reguła obsługuje przenoszenie danych do plików. Upewnia się, czy w dyrektywie podano nazwę pliku. Jeżeli wymieniony plik nie jest aktualnie tworzonym plikiem, to bieżący plik jest zamykany. Oznacza to, że nie podano dla niego `@c endfile'. (Powinniśmy zapewne wypisać w tym przypadku komunikat diagnostyczny, choć teraz tego nie robimy.)
Zasadniczą część zadania realizuje pętla `for'. Czyta wiersze
za pomocą
getline (zob. 5.8. Odczyt bezpośredni przez getline).
W przypadku napotkania niespodziewanego końca pliku wywołuje funkcję
unexpected_eof. Jeżeli wiersz jest wierszem "endfile", to
przerywa pętlę. Jeżeli wiersz jest typu `@group' lub
`@end group', to jest ignorowany, a program przechodzi do następnego.
(Te wiersze sterujące w Texinfo trzymają bloki kodu razem na jednej
stronie. Niestety, TeX nie zawsze jest dość sprytny, by zrobić swoje
całkiem dobrze, i musimy mu trochę podpowiadać.)
Większość pracy wykonuje poniższych kilka linijek kodu. Jeżeli w wierszu nie ma symboli `@', to można go wypisać wprost. W przeciwnym razie muszą zostać usunięte wszystkie początkowe `@'.
W celu usunięcia symboli `@', wiersz dzielony jest, za pomocą
funkcji split
(zob. 12.3. Funkcje wbudowane działające na łańcuchach),
na odrębne elementy tablicy a. Każdy pusty element a
wskazuje na dwa kolejne symbole `@' w pierwotnym wierszu.
Dla każdych dwu pustych elementów (`@@' w pliku pierwotnym) musimy
dodać z powrotem pojedynczy symbol `@'.
Po zakończeniu przetwarzania tablicy, do ponownego złączenia kawałków
w pojedynczy wiersz wywoływana jest join z wartością SUBSEP.
Wiersz ten jest następnie wypisywany do pliku wyjściowego.
/^@c(omment)?[ \t]+file/ \
{
if (NF != 3) {
e = (FILENAME ":" FNR ": źle zbudowany wiersz `file'")
print e > "/dev/stderr"
next
}
if ($3 != curfile) {
if (curfile != "")
close(curfile)
curfile = $3
}
for (;;) {
if ((getline line) <= 0)
unexpected_eof()
if (line ~ /^@c(omment)?[ \t]+endfile/)
break
else if (line ~ /^@(end[ \t]+)?group/)
continue
if (index(line, "@") == 0) {
print line > curfile
continue
}
n = split(line, a, "@")
# jeśli a[1] == "", tzn. początkowe @,
# nie oddawaj jednego.
for (i = 2; i <= n; i++) {
if (a[i] == "") { # było @@
a[i] = "@"
if (a[i+1] == "")
i++
}
}
print join(a, 1, n, SUBSEP) > curfile
}
}
Ważne jest zwrócenie uwagi na użycie przekierowania `>'.
Wyjście wykonane za pomocą `>' otwiera dany plik tylko raz. Pozostaje
on otwarty a kolejne elementy wyjścia są do niego dopisywane.
(zob. 6.6. Przekierowanie wyjścia print i printf).
Daje nam to możliwość łatwego przeplatania tekstu programu i objaśnień
dotyczących tego samego pliku źródłowego (tak jak to zrobiono tutaj!) bez
żadnych kłopotów.
Plik zamykany jest tylko wtedy, gdy napotkana zostanie nowa nazwa pliku albo
koniec pliku wejściowego.
Na koniec, funkcja unexpected_eof wypisuje odpowiedni komunikat
o błędzie i kończy pracę programu.
Reguła END obsługuje końcowe porządkowanie, zamykając otwarty plik.
function unexpected_eof()
{
printf("%s:%d: niespodziewany EOF lub błąd\n", \
FILENAME, FNR) > "/dev/stderr"
exit 1
}
END {
if (curfile)
close(curfile)
}
Narzędzie sed to "edytor strumieniowy" (stream editor), program,
który czyta strumień danych, dokonuje na nim zmian, i przekazuje zmienione
dane dalej. Jest często wykorzystywany do robienia zmian w dużych plikach
lub w strumieniach danych tworzonych przez potoki poleceń.
Chociaż trzeba przyznać, że sed jest skomplikowanym programem,
najczęstszym jego wykorzystaniem jest wykonywanie globalnych podstawień
w środku potoku:
polecenie1 < dane.pocz | sed 's/stare/nowe/g' | polecenie2 > wynik
Tu, `s/stare/nowe/g' nakazuje sed wyszukanie wyrażenia
regularnego `stare' w każdym wierszu wejściowym i zastąpienie go
tekstem `nowe', globalnie (tj. wszystkie wystąpienia w wierszu).
Przypomina to funkcję gsub
(zob. 12.3. Funkcje wbudowane działające na łańcuchach)
z awk.
Poniższy program, `awksed.awk', przyjmuje co najmniej dwa argumenty wiersza poleceń: wzorzec, jakiego szukać, i tekst, jaki ma go zastąpić. Dodatkowe argumenty traktowane są jak nazwy plików danych do przetworzenia. Jeżeli nie podano żadnych, to używane jest standardowe wejście.
# awksed.awk --- robi s/foo/bar/g za pomocą samego print
# Dzięki Michaelowi Brennanowi za pomysł
# Arnold Robbins, arnold@gnu.org, Public Domain
# August 1995
function usage()
{
print "składnia: awksed wzr zast [pliki...]" > "/dev/stderr"
exit 1
}
BEGIN {
# kontrola poprawności argumentów
if (ARGC < 3)
usage()
RS = ARGV[1]
ORS = ARGV[2]
# nie używaj argumentów jako plików
ARGV[1] = ARGV[2] = ""
}
# patrzaj, bez trzymanki!
{
if (RT == "")
printf "%s", $0
else
print
}
Program opiera się na zdolności gawk do obsługi RS jako
wyrażenia regularnego, oraz na przypisywaniu zmiennej RT tekstu,
jaki faktycznie
zakończył rekord (zob. 5.1. Jak wejście dzielone jest na rekordy).
Pomysł polega na tym, by w RS był wzorzec do wyszukania. gawk
automatycznie przypisze do $0 tekst pomiędzy dopasowaniami wzorca.
To tekst, który chcemy pozostawić bez zmian. Następnie, dzięki przypisaniu
tekst zastępującego do ORS, pojedyncza instrukcja print
wypisze tekst, jaki chcemy zostawić, a po nim tekst zastąpienia.
W tym schemacie jest pewna zagwozdka: co zrobić, jeśli ostatni rekord nie
kończy się tekstem pasującym do RS? Skorzystanie z instrukcji
print powoduje bezwarunkowe wypisanie tekstu zastąpienia, co nie jest
poprawne.
Jeśli jednak plik nie kończy się tekstem pasującym do RS, to RT
zostanie przypisany łańcuch pusty. W tym przypadku możemy wypisać $0
za pomocą printf
(zob. 6.5. Wymyślne wyjście dzięki instrukcji printf).
Reguła BEGIN obsługuje konfigurację, kontrolę poprawnej liczby
argumentów i w przypadku problemów wywołanie usage. Następnie
inicjuje RS i ORS argumentami wiersza poleceń i przypisuje
ARGV[1] i ARGV[2] łańcuch pusty, by nie zostały potraktowane
jak nazwy plików (zob. 10.3. Używanie ARGC i ARGV).
Funkcja usage wypisuje komunikat o błędzie i kończy pracę programu.
Wreszcie, jedyna reguła główna obsługuje nakreślony powyżej schemat
tworzenia wyjścia za pomocą odpowiednio print lub printf,
w zależności od wartości RT.
Korzystanie z funkcji bibliotecznych w awk może być bardzo korzystne.
Zachęca do wielokrotnego używania kodu i pisania ogólnych funkcji. Programy
są mniejsze, zatem czytelniejsze. Jednak stosowanie funkcji bibliotecznych
jest łatwe tylko przy pisaniu programów awk. Jest bolesne przy ich
uruchamianiu, gdyż wymaga wielu opcji `-f'. Jeśli nie jest dostępny
gawk, to niedostępna jest też zmienna środowiska AWKPATH
i możliwość umieszczenia funkcji awk w katalogu
bibliotek (zob. 14.1. Opcje wiersza poleceń).
Byłoby miło, gdybyśmy mogli napisać program tak:
# funkcje biblioteczne
@include getopt.awk
@include join.awk
...
# program główny
BEGIN {
while ((c = getopt(ARGC, ARGV, "a:b:cde")) != -1)
...
...
}
Poniższy program, `igawk.sh', udostępnia taką obsługę. Symuluje
wyszukiwanie przez gawk zmiennej AWKPATH, pozwala też na
zagnieżdżone dołączenia, tj. plik, który został dołączony za pomocą
`@include' może zawierać dalsze instrukcje `@include'.
igawk będzie usiłował dołączać pliki tylko raz, by zagnieżdżone
dołączenia nie spowodowały przypadkowo dwukrotnego dołączenia funkcji
bibliotecznej.
igawk zewnętrznie powinien zachowywać się tak jak gawk.
To znaczy, że powinien przyjmować wszystkie argumenty wiersza poleceń
gawk, łącznie z możliwością podania wielu nazw plików źródłowych
poprzez `-f', i możliwością przeplatania plików źródłowych z wiersza
poleceń i bibliotecznych.
Program napisano za pomocą języka poleceń Powłoki POSIX (POSIX Shell,
sh). Działa w następujący sposób:
awk, na później, gdy zostanie uruchomiony rozwinięty
program.
awk
w pliku tymczasowym, który zostanie rozwinięty. Mamy dwa przypadki.
echo,
który samoczynnie zapewni końcowy znak nowej linii.
gawk,
spowoduje to włączenie tekstu pliku do programu w odpowiednim miejscu.
awk (naturalnie) z utworzonym
plikiem tymczasowym jako plikiem danych, by rozwinąć instrukcje
`@include'. Rozwinięty program umieszczany jest w drugim pliku
tymczasowym.
gawk, z pozostałymi
początkowymi argumentami wiersza poleceń podanymi przez użytkownika (jak
nazwy plików danych).
Początkowa część programu włącza śledzenie w powłoce jeśli pierwszym
argumentem było `debug'. W przeciwnym razie, instrukcja trap
powłoki organizuje sprzątanie plików tymczasowych przy zakończeniu lub
przerwaniu pracy programu.
Następna część jest pętlą po wszystkich argumentach wiersza poleceń. Mamy kilka interesujących nas kilka przypadków.
--
igawk. Cała reszta powinna bez dalszej
analizy zostać przekazana do programu awk użytkownika.
-W
gawk. W celu
ułatwienia przetwarzania argumentów, przed pozostałymi argumentami
dopisywane jest `-W' a pętla kontynuuje działanie.
(To trik programowania w sh. Nie przejmuj się nim, jeśli nie znasz
sh.)
-v
-F
gawk.
-f
--file
--file=
-Wfile=
sed.
--source
--source=
-Wsource=
--version
-Wversion
igawk wypisuje swój numer wersji, uruchamia `gawk --version',
by uzyskać informację o wersji gawk, a następnie kończy pracę.
Jeśli nie podano żadnej z opcji `-f', `--file', `-Wfile',
`--source', ani `-Wsource', to pierwszy nie będący opcją
argument powinien być programem awk. Jeżeli nie pozostały już
żadne argumenty wiersza poleceń, igawk wypisze komunikat o
błędzie i zakończy pracę. W przeciwnym razie, pierwszy argument powtarzany
jest echem do `/tmp/ig.s.$$'.
W każdym z przypadków, po przetworzeniu argumentów plik `/tmp/ig.s.$$'
zawiera pełny tekst pierwotnego programu awk.
Zapis `$$' w sh reprezentuje numeryczny identyfikator bieżącego
procesu. W programach powłoki jest często wykorzystywany do tworzenia
niepowtarzalnych nazw plików tymczasowych. Pozwala to na równoczesne
uruchamianie igawk przez wielu użytkowników bez obawy o konflikt nazw
plików tymczasowych.
#! /bin/sh
# igawk --- jak gawk, ale przetwarza @include
# Arnold Robbins, arnold@gnu.org, Public Domain
# July 1993
if [ "$1" = debug ]
then
set -x
shift
else
# sprzątanie przy zakończeniu i otrzymaniu
# hangup, interrupt, quit, termination
trap 'rm -f /tmp/ig.[se].$$' 0 1 2 3 15
fi
while [ $# -ne 0 ] # pętla po argumentach
do
case $1 in
--) shift; break;;
-W) shift
set -- -W"$@"
continue;;
-[vF]) opts="$opts $1 '$2'"
shift;;
-[vF]*) opts="$opts '$1'" ;;
-f) echo @include "$2" >> /tmp/ig.s.$$
shift;;
-f*) f=`echo "$1" | sed 's/-f//'`
echo @include "$f" >> /tmp/ig.s.$$ ;;
-?file=*) # -Wfile lub --file
f=`echo "$1" | sed 's/-.file=//'`
echo @include "$f" >> /tmp/ig.s.$$ ;;
-?file) # get arg, $2
echo @include "$2" >> /tmp/ig.s.$$
shift;;
-?source=*) # -Wsource lub --source
t=`echo "$1" | sed 's/-.source=//'`
echo "$t" >> /tmp/ig.s.$$ ;;
-?source) # pobierz argument, $2
echo "$2" >> /tmp/ig.s.$$
shift;;
-?version)
echo igawk: version 1.0 1>&2
gawk --version
exit 0 ;;
-[W-]*) opts="$opts '$1'" ;;
*) break;;
esac
shift
done
if [ ! -s /tmp/ig.s.$$ ]
then
if [ -z "$1" ]
then
echo igawk: brak programu! 1>&2
exit 1
else
echo "$1" > /tmp/ig.s.$$
shift
fi
fi
# w tym momencie w /tmp/ig.s.$$ jest końcowy program
Program awk do przetwarzania dyrektyw `@include' czyta
po jednym wierszu utworzony program, używając getline
(zob. 5.8. Odczyt bezpośredni przez getline). Nazwy plików
wejściowych i instrukcje `@include' zarządzane są z wykorzystaniem
stosu. Za każdym razem, gdy napotkane zostanie `@include', nazwa
bieżącego pliku umieszczana jest na stosie, a plik wymieniony w dyrektywie
`@include' staje się bieżącym plikiem wejściowym. Po zakończeniu
każdego pliku, ze stosu zdejmowana jest nazwa poprzedniego pliku
wejściowego, który staje się ponownie bieżącym plikiem. Proces rozpoczyna
się od umieszczenia pierwotnego pliku jako pierwszego na stosie.
Funkcja pathto zajmuje się znajdowaniem pełnej ścieżki do pliku.
Symuluje zachowanie się gawk przy przeszukiwaniu zmiennej
AWKPATH
(zob. 14.3. Zmienna środowiska AWKPATH).
Jeśli nazwa pliku zawiera `/', to nie jest wykonywane przeszukiwanie
ścieżki. W przeciwnym razie, nazwa pliku sklejana jest z nazwą każdego
z katalogów ścieżki i wykonywana jest próba otwarcia pliku o tak utworzonej
nazwie. Jedyną metodą sprawdzenia w awk czy można odczytać plik jest
spróbowanie i podjęcie odczytania go za pomocą getline: to właśnie
robi pathto (29) Jeżeli można przeczytać plik, to jest on zamykany i zwracana
jest jego nazwa.
gawk -- '
# przetwarza dyrektywy @include
function pathto(file, i, t, smiec)
{
if (index(file, "/") != 0)
return file
for (i = 1; i <= ndirs; i++) {
t = (pathlist[i] "/" file)
if ((getline smiec < t) > 0) {
# mamy go
close(t)
return t
}
}
return ""
}
Program główny zawiera się w jednej regule BEGIN. Pierwszą
rzeczą, jaką robi, jest zainicjowanie tablicy pathlist, której
używa pathto. Po podziale ścieżki w miejscach `:', elementy
puste zastępowane są przez ".", co oznacza katalog bieżący.
BEGIN {
path = ENVIRON["AWKPATH"]
ndirs = split(path, pathlist, ":")
for (i = 1; i <= ndirs; i++) {
if (pathlist[i] == "")
pathlist[i] = "."
}
Stos inicjowany jest wartością ARGV[1], którą będzie
`/tmp/ig.s.$$'. Następnie mamy główną pętlę. Kolejno czytane są
wiersze wejściowe. Wiersze, które nie rozpoczynają się od `@include'
wypisywane są dosłownie.
Jeżeli dany wiersz zaczyna się od `@include', to nazwa pliku jest
w $2. Do utworzenia pełnej ścieżki wywoływana jest pathto.
Jeśli się to nie powiodło, wypisujemy komunikat o błędzie i kontynuujemy.
Następną rzeczą do sprawdzenia jest to, czy plik został już przez nas
dołączony. Tablica processed zaindeksowana jest pełnymi nazwami
każdego dołączonego pliku i zapamiętuje dla nas tę informację. Jeżeli plik
już obsługiwano, to wypisywany jest komunikat ostrzegawczy. W przeciwnym
razie, nazwa nowego pliku jest umieszczana na stosie a przetwarzanie
kontynuowane.
Na koniec, gdy getline napotka koniec pliku wejściowego, plik ten
jest zamykany a ze stosu jest zdejmowana nazwa poprzedniego.
Gdy stackptr jest mniejsze od zera, to program jest zakończony.
stackptr = 0
input[stackptr] = ARGV[1] # ARGV[1] jest pierwszym plikiem
for (; stackptr >= 0; stackptr--) {
while ((getline < input[stackptr]) > 0) {
if (tolower($1) != "@include") {
print
continue
}
fpath = pathto($2)
if (fpath == "") {
printf("igawk:%s:%d: nie można znaleźć %s\n", \
input[stackptr], FNR, $2) > "/dev/stderr"
continue
}
if (! (fpath in processed)) {
processed[fpath] = input[stackptr]
input[++stackptr] = fpath
} else
print $2, "dołączony w", input[stackptr], \
"już dołączony w", \
processed[fpath] > "/dev/stderr"
}
close(input[stackptr])
}
}' /tmp/ig.s.$$ > /tmp/ig.e.$$
Ostatnim krokiem jest wywołanie gawk z rozwiniętym programem i
pierwotnymi, podanymi przez użytkownika, opcjami i argumentami wiersza
poleceń. Kod zakończenia zwrócony przez gawk odsyłany jest do
programu wywołującego igawk.
eval gawk -f /tmp/ig.e.$$ $opts -- "$@" exit $?
Pokazana wersja igawk jest moim trzecim podejściem do tego programu.
Oto trzy kluczowe uproszczenia, które spowodowały, że program działa lepiej.
awk.
Całość przetwarzania `@include' można wykonać jednokrotnie.
pathto nie usiłuje zapamiętać wiersza odczytanego za pomocą
getline przy sprawdzaniu dostępności pliku. Próba zapamiętania tego
wiersza do wykorzystania w programie głównym znacznie komplikuje sprawę.
getline w regule BEGIN robi wszystko w jednym
miejscu. Nie jest konieczne tworzenie osobnej pętli
do przetwarzania zagnieżdżonych instrukcji `@include'.
Program ten pokazuje także, że często warto połączyć programowanie
w sh i awk. Zwykle można sporo osiągnąć, bez potrzeby
uciekania się do niskopoziomowego programowania w C czy C++, a często
łatwiej wykonać pewne rodzaje operacji na łańcuchach czy argumentach
korzystając z powłoki niż z awk.
Wreszcie, igawk pokazuje, że nie zawsze konieczne jest dodawanie
do programu nowych funkcji: można je często umieścić w wyższej warstwie.
Z igawk nie ma faktycznego powodu wbudowywania przetwarzania
`@include' w sam gawk.
Jako dodatkowy przykład tego, rozważmy pomysł umieszczenia dwu plików w katalogu ze ścieżki wyszukiwania.
getopt i assert.
gawk,
bez potrzeby każdorazowej aktualizacji go przez administratora systemu przez
dodawanie funkcji lokalnych.
Pewien użytkownik
zasugerował zmodyfikowanie gawk tak, by przy uruchomieniu
automatycznie czytał te pliki. Zamiast tego, bardzo łatwo byłoby zmienić
igawk, by to robił. Ponieważ igawk potrafi przetwarzać
zagnieżdżone dyrektywy `@include', plik `default.awk' mógłby po
prostu zawierać instrukcje `@include' dla żądanych funkcji
bibliotecznych.
Przejdź do pierwszej, poprzedniej, następnej, ostatniej sekcji, spisu treści.