Home Dokumentacje Efektywne programowanie w AWK - Podręcznik użytkownika GNU awk - Praktyczne programy awk
22 | 08 | 2019
Efektywne programowanie w AWK - Podręcznik użytkownika GNU awk - Praktyczne programy awk Drukuj

Przejdź do pierwszej, poprzedniej, następnej, ostatniej sekcji, spisu treści.

 


 

16. Praktyczne programy 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.

16.1. Wymyślanie prochu dla rozrywki i zysku

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.

16.1.1. Wycinanie pól i kolumn

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
Wykorzystuje listę jako listę znaków do wycięcia. Pozycje listy można rozdzielać, a zakresy znaków oddzielać myślnikami. Lista `1-8,15,22-35' określa znaki od jeden do osiem, 15, i 22 do 35.
-f lista
Wykorzystuje listę jako listę pól do wycięcia.
-d ogranicz
Jako znaku separatora pól, zamiast znaku tabulacji, używa ogranicz.
-s
Pomija wypisywanie wierszy, które nie zawierają ogranicznika pól.

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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.

16.1.2. Wyszukiwanie wyrażeń regularnych w plikach

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
Zamiast pasujących do wzorca wierszy wypisuje tylko ich ilość.
-s
Tryb cichy. Nie jest tworzone żadne wyjście, a kod zakończenia wskazuje czy dopasowano wzorzec, czy też nie.
-v
Odwraca znaczenie testu. egrep wypisuje wiersze, które nie pasują do wzorca, i kończy pomyślnie pracę jeśli nie dopasowano wzorca.
-i
Nie rozróżnia wielkości liter zarówno we wzorcu, jak i w danych wejściowych.
-l
Wypisuje tylko nazwy plików, w których znaleziono dopasowania, a nie pasujące wiersze.
-e wzorzec
Używa wzorca jako wyrażenia regularnego do dopasowania. Celem opcji `-e' jest umożliwienie podania wzorca zaczynającego się od `-'.

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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.

16.1.3. Wypisywanie informacji o użytkowniku

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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 ""
}

16.1.4. Podział dużego pliku na części

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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.

16.1.5. Duplikowanie wyjścia do wielu plików

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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])
}

16.1.6. Wypisywanie nie powtarzających się wierszy tekstu

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
Wypisuje tylko wiersze powtarzające się.
-u
Wypisuje tylko wiersze nie powtarzające się.
-c
Zlicza wiersze. Opcja ta przesłania `-d' i `-u'. Zliczane są zarówno wiersze powtarzające się, jak i nie powtarzające się.
-n
Przed porównaniem wierszy pomija n pól. Definicja pól jest podobna do domyślnej awk: nie-białe znaki oddzielone ciągami spacji i/lub tabulacji.
+n
Przed porównaniem wierszy pomija n znaków. Wcześniej pomijane są ewentualne pola określone za pomocą `-n'.
plik wejściowy
Dane czytane są z pliku wejściowego wymienionego w wierszu poleceń, a nie ze standardowego wejścia.
plik wyjściowy
Tworzone wyjście wysyłane jest do wskazanego pliku wyjściowego, zamiast na standardowe wyjście.

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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
}

16.1.7. Zliczanie rzeczy

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
Zlicza tylko wiersze.
-w
Zlicza tylko wyrazy. "Wyraz" jest nieprzerwanym ciągiem nie-białych znaków, oddzielonym spacjami i/lub tabulacjami. Tak się dobrze składa, że w awk jest to normalna metoda rozdzielania pól w danych wejściowych.
-c
Zlicza tylko znaki.

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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"
    }
}

16.2. Pełen worek programów awk

Ta sekcja to wielki "worek" pełen rozmaitych programów. Mam nadzieję, że uznasz je za interesujące i przyjemne.

16.2.1. Znajdowanie w dokumencie zdublowanych wyrazów

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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
}

16.2.2. Program-budzik

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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
}

16.2.3. Zamiana znaków

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
Lista znaków, które konwertować.
na
Lista znaków, na które konwertować.
cel
Łańcuch, jaki ma podlegać konwersji.

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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ą.

16.2.4. Wypisywanie etykiet adresowych

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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()
}

16.2.5. Tworzenie statystyk użycia wyrazów

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:

  • Słowa wykrywane są za pomocą konwencji 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.
  • Język 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.
  • Wyniki nie pojawiają się w żadnej użytecznym porządku. Prawdopodobnie jesteśmy raczej zainteresowani tym, które słowa pojawiają się najczęściej, lub ułożoną alfabetycznie tabelą częstości poszczególnych słów.

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.

16.2.6. Usuwanie duplikatów z niesortowanego tekstu

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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.

16.2.7. Wydzielanie programów z plików źródłowych Texinfo

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.

  • Symbol "at", `@', w Texinfo ma znaczenie specjalne, całkiem podobnie jak `\' w C czy awk. Dosłowne symbole `@' reprezentowane są w plikach źródłowych Texinfo jako `@@'.
  • Komentarze zaczynają się albo od `@c' albo od `@comment'. Program wydobywający pliki będzie pracował wykorzystując specjalne komentarze rozpoczynające się na samym początku wiersza.
  • Tekst przykładu, który nie powinien być dzielony między strony zgrupowany jest między wierszami zawierającymi polecenia `@group' i `@end group'.

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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)
}

16.2.8. Prosty edytor strumieniowy

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, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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.

16.2.9. Łatwa metoda korzystania z funkcji bibliotecznych

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:

  1. Pętla po argumentach, zachowująca wszystko, co nie reprezentuje kodu źródłowego awk, na później, gdy zostanie uruchomiony rozwinięty program.
  2. Zachowanie ewentualnych argumentów nie reprezentujących tekstu awk w pliku tymczasowym, który zostanie rozwinięty. Mamy dwa przypadki.
    1. Tekst dosłowny, dostarczony przez `--source' lub `--source='. Ten tekst jest po prostu bezpośrednio powtarzany przez program echo, który samoczynnie zapewni końcowy znak nowej linii.
    2. Nazwy plików dostarczone przez `-f'. Stosujemy ładną sztuczkę, i powtarzamy echem `@include nazwapliku' do pliku tymczasowego. Ponieważ program dołączania będzie działał w ten sam sposób, co gawk, spowoduje to włączenie tekstu pliku do programu w odpowiednim miejscu.
  3. Uruchomienie programu napisanego w awk (naturalnie) z utworzonym plikiem tymczasowym jako plikiem danych, by rozwinąć instrukcje `@include'. Rozwinięty program umieszczany jest w drugim pliku tymczasowym.
  4. Uruchomienie rozwiniętego programu za pomocą 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.

--
Ta opcja kończy argumenty igawk. Cała reszta powinna bez dalszej analizy zostać przekazana do programu awk użytkownika.
-W
Wskazuje, że następna opcja jest specyficzna dla 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
Są zapamiętywane i przekazywane do gawk.
-f
--file
--file=
-Wfile=
Nazwa pliku zapamiętywana jest w pliku tymczasowym `/tmp/ig.s.$$' za pomocą instrukcji `@include'. Do usunięcia początkowej, opcyjnej części argumentu (np., `--file=') wykorzystywane jest narzędzie sed.
--source
--source=
-Wsource=
Tekst źródłowy jest zapisywany do `/tmp/ig.s.$$'.
--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.

Oto i nasz program:

#! /bin/sh

# igawk --- jak gawk, ale przetwarza @include
# Arnold Robbins, 
 Adres poczty elektronicznej jest chroniony przed robotami spamującymi. W przeglądarce musi być włączona obsługa JavaScript, żeby go zobaczyć.
 , 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.

  1. Zastosowanie `@include' nawet do plików wskazanych przez `-f' znacznie upraszcza zbudowanie wstępnie poskładanego programu awk. Całość przetwarzania `@include' można wykonać jednokrotnie.
  2. Funkcja 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ę.
  3. Pętla 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.

`default.awk'
Plik ten zawierałby zbiór domyślnych funkcji bibliotecznych, jak getopt i assert.
`site.awk'
Ten plik zawierałby funkcje biblioteczne swoiste dla danego miejsca czy instalacji, tj. funkcji opracowanych lokalnie. Posiadanie osobnego pliku pozwala na zmianę `default.awk' przy nowych edycjach 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.

 
Linki sponsorowane

W celu realizacji usług i funkcji na witrynach internetowych ZUI "ELPRO" stosujemy pliki cookies. Korzystanie z witryny bez zmiany ustawień dotyczących plików cookies oznacza, że będą one zapisywane w urządzeniu wyświetlającym stronę internetową. Więcej szczegółów w Polityce plików cookies.

Akceptuję pliki cookies z tej witryny.