Przejdź do treści
Lekcja 47 z 117

Kurs › Moduł 4 › Lekcja 47

awk od podstaw

⏱ ok. 9 min czytania· moduł 4: Napisy, listy i przetwarzanie danych

Po tej lekcji: kursant wyciąga awk-iem kolumny z dowolnego tekstu, filtruje linijki warunkami, liczy sumy i zestawienia i składa z tego czytelny raport.

Wstęp

Serwer muli. Pamięć prawie pełna. Pytanie: który użytkownik ją zjada? ps aux wypisuje kilkaset procesów, każdy z właścicielem i pamięcią. Trzeba je pogrupować po właścicielu i zsumować. W Excelu byś zrobił tabelę przestawną. W terminalu robisz to tak:

ps aux | awk 'NR>1 {mem[$1] += $6} END {for (u in mem) printf "%-10s %8.1f MB\n", u, mem[u]/1024}' | sort -k2 -rn
root         1190.3 MB
postfix        37.8 MB
www-data       30.0 MB
kuba           19.5 MB

Jedna linijka, tabela przestawna gotowa. Za kwadrans będziesz takie pisać z pamięci, a ludzie w biurze zaczną mówić, że „też umiesz w Excela, tylko na czarnym tle”.

Księgowa z kalkulatorem

Grep to sito, sed to korektorka. A awk to księgowa. Dostaje tabelkę, patrzy na kolumny, liczy, sumuje, a na koniec kładzie na biurko raport. Do tego ma zeszyt, w którym prowadzi własne notatki.

Nazwa wzięła się od pierwszych liter nazwisk twórców: Aho, Weinberger, Kernighan. Pierwsza wersja jest z 1977 roku. Czyli awk jest starszy niż większość ludzi, którzy na niego narzekają.

Awk to tak naprawdę mały język programowania. Każdy program w awku wygląda tak samo:

warunek { akcja }

Dla każdej linijki: jeśli warunek pasuje, zrób akcję. Bez warunku akcja działa na każdej linijce. Bez akcji domyślnie wypisuje całą linijkę. Jak kartka z poleceniami dla księgowej: „każdą fakturę z Krakowa dopisz do sumy”.

Program zawsze w apostrofach, czyli w kopercie. Zaraz zobaczysz dlaczego.

Kolumny: $1, $2, $NF

Awk sam dzieli każdą linijkę na kolumny, po awkowemu pola. $1 to pierwsza kolumna, $2 druga, $0 cała linijka.

echo 'Ala ma kota' | awk '{print $3, $1}'
echo '  wiele   spacji  tu ' | awk '{print $2}'
kota Ala
spacji

Druga linijka to ważna różnica względem cut. Domyślnie awk traktuje dowolnie dużo spacji i tabulatorów jak jeden separator. cut -d' ' przy podwójnej spacji zwariowałby i zwracał puste kolumny. Dlatego wyniki ps, df czy ls -l, gdzie kolumny są wyrównane spacjami, tniemy awkiem.

Przecinek w print wstawia między kolumny spację. Bez przecinka awk skleja: print $1 $3 da „root0”.

Dwie wbudowane zmienne, które musisz znać:

  • NF to liczba pól w bieżącej linijce. A $NF to ostatnie pole, $(NF-1) przedostatnie.
  • NR to numer linijki, od 1.

W naszym auth.log z lekcji 45 adres IP jest czwarty od końca, niezależnie od tego, czy linijka ma „invalid user”, czy nie:

awk '/Failed/ {print $(NF-3)}' auth.log | sort | uniq -c | sort -rn
      3 203.0.113.45
      2 198.51.100.7
      1 192.0.2.10

Ten sam wynik co w haku z lekcji 45, bez regeksu na adres IP. Liczenie od końca to trik, który ratuje, kiedy początek linijki pływa.

-F: inny separator

-F mówi, czym są oddzielone kolumny. Na przykład dwukropkiem w /etc/passwd. Do ćwiczeń zrób plik konta.txt w tym samym formacie:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
eryk:x:1000:1000:Eryk:/home/eryk:/bin/bash
ania:x:1001:1001:Ania Nowak:/home/ania:/bin/zsh
stazysta:x:1002:1002::/home/stazysta:/bin/bash
awk -F: '{print $1, $7}' konta.txt | head -3
root /bin/bash
daemon /usr/sbin/nologin
sshd /usr/sbin/nologin

A jeśli chcesz zmienić separator na wyjściu, jest zmienna OFS:

awk -F: -v OFS=';' '{print $1, $3, $7}' konta.txt | head -2
root;0;/bin/bash
daemon;1;/usr/sbin/nologin

Warunki: które linijki

Przed klamrą możesz napisać warunek. Porównania jak w matematyce: ==, !=, <, >, >=. Łączysz przez && (i) oraz || (lub).

awk -F: '$3 >= 1000 && $3 != 65534 {print $1}' konta.txt
eryk
ania
stazysta

To są ludzie. Wszystko poniżej UID 1000 to konta systemowe, a 65534 to nobody, który ma UID wyższy niż wszyscy, a prawa niższe niż wszyscy. Jak strażnik z największym numerem na identyfikatorze i bez klucza do czegokolwiek.

[Ubuntu] [RHEL] Na obu rodzinach zwykli użytkownicy dostają UID od 1000. Sprawdzisz to w /etc/login.defs, w linijce UID_MIN.

Warunek może też być wyrażeniem regularnym w ukośnikach, jak w grepie. Albo dopasowaniem jednej kolumny przez ~:

awk -F: '$7 == "/bin/bash" {print NR ": " $1}' konta.txt
1: root
5: eryk
7: stazysta

Napisy w awku zawsze w cudzysłowach, inaczej awk pomyśli, że to nazwa zmiennej. A awk -F: '$7 ~ /bash/' znaczy „siódma kolumna zawiera bash”.

Sumy: zeszyt księgowej

Zmienne w awku nie trzeba tworzyć. Pierwsze użycie i już jest, z wartością zero albo pustym napisem. Do tego dwa specjalne bloki: BEGIN działa raz przed pierwszą linijką, END raz po ostatniej. Księgowa przed pracą rysuje nagłówek tabelki, a po pracy podlicza.

Plik sprzedaz.csv:

data,sklep,produkt,sztuk,cena
2026-11-01,Krakow,kawa,3,24.50
2026-11-01,Warszawa,herbata,5,12.00
2026-11-02,Krakow,herbata,2,12.00
2026-11-02,Gdansk,kawa,4,24.50
2026-11-03,Krakow,kawa,1,24.50
2026-11-03,Warszawa,kawa,6,24.50
awk -F, 'NR>1 {suma += $4*$5} END {printf "Utarg: %.2f zl\n", suma}' sprzedaz.csv
Utarg: 427.00 zl

NR>1 pomija nagłówek. Dla każdej linijki doliczamy sztuki razy cenę. Na końcu printf, który znasz z lekcji 44, tylko że tu w nawiasach nie trzeba, a argumenty oddziela przecinek. I zauważ: awk liczy na ułamkach. Bash z lekcji 38 przy 24.50 by się obraził.

Zestawienia: tablica jako szatnia

A teraz magia z haka. Tablice w awku działają jak słownik z lekcji 43, szatnia z nazwiskami. Kluczem może być dowolny napis.

awk -F, 'NR>1 {utarg[$2] += $4*$5} END {for (s in utarg) printf "%-10s %8.2f\n", s, utarg[s]}' sprzedaz.csv | sort -k2 -rn
Warszawa     207.00
Krakow       122.00
Gdansk        98.00

Dla każdej linijki księgowa idzie do wieszaka z nazwą sklepu i dokłada do niego kwotę. Wieszaka nie ma? Awk sam go stawia. W END pętla for (s in utarg) przechodzi po wszystkich wieszakach. Kolejność jest przypadkowa, dlatego na końcu sort.

Ten sam schemat liczy wystąpienia:

awk -F: '{licz[$7]++} END {for (p in licz) print licz[p], p}' konta.txt | sort -rn
3 /usr/sbin/nologin
3 /bin/bash
1 /bin/zsh

Pamiętasz zadanie z lekcji 7, ranking powłok przez cut | sort | uniq -c | sort -rn? Tu jest to samo, jednym programem.

Mały raport od początku do końca

Wszystko razem: nagłówek w BEGIN, filtr, licznik, podsumowanie w END.

awk -F: '
BEGIN { printf "%-10s %6s\n", "KONTO", "UID" }
$3 >= 1000 && $3 != 65534 { printf "%-10s %6d\n", $1, $3; n++ }
END { print "Razem ludzi:", n }
' konta.txt
KONTO         UID
eryk         1000
ania         1001
stazysta     1002
Razem ludzi: 3

Program może mieć wiele linijek. Kiedy urośnie, przenieś go do pliku raport.awk i uruchamiaj przez awk -F: -f raport.awk konta.txt.

Jak to działa pod spodem

Awk to interpreter, jak Bash, tylko wyspecjalizowany w tabelkach. Cykl wygląda tak:

  1. Wykonaj blok BEGIN.
  2. Wczytaj rekord, domyślnie jedną linijkę. Ustaw $0 i NR.
  3. Pokrój go na pola według separatora FS (to właśnie ustawia -F). Ustaw $1, $2, ... i NF.
  4. Sprawdź po kolei wszystkie pary „warunek { akcja }”.
  5. Wróć do punktu 2. Po ostatniej linijce wykonaj END.

Zmienne są jednocześnie liczbami i napisami. "12" + 3 daje 15, a do $5 z napisem „24.50” można normalnie dodać liczbę. Jak pudełko, które samo zgaduje, czy trzymać w nim śrubki, czy kartkę.

I ważne: „awk” to nie jeden program. To język, który ma kilka implementacji.

  • [RHEL] komenda awk to GNU awk, czyli gawk.
  • [Ubuntu] domyślnie jest mawk, szybki i oszczędny. Jeśli doinstalujesz gawk, przejmuje on komendę awk przez mechanizm alternatyw (sprawdzisz readlink -f /usr/bin/awk).

Wszystko z tej lekcji działa w obu. Różnice wychodzą przy dodatkach. Na przykład przy polskich znakach:

echo 'ćma' | mawk '{print length($0)}'
echo 'ćma' | LC_ALL=C.UTF-8 gawk '{print length($0)}'
4
3

Mawk liczy bajty, a ć w UTF-8 zajmuje dwa. Gawk z ustawionym UTF-8 liczy litery.

[macOS] Na Macu awk to wersja z rodziny BSD, wywodząca się od awka autorów języka (awk --version pokazuje zamiast numeru datę, np. 20200816). Podstawy z tej lekcji działają tak samo. Funkcje typowe dla GNU, na przykład strftime czy sortowanie tablic, nie istnieją w awku z Maca, są w gawk: brew install gawk.

Ściąga flag

KomendaFlaga / elementCo robi
awk-F Xseparator kolumn na wejściu (FS)
awk-v zm=wartprzekazuje wartość z Basha do zmiennej awka
awk-f plik.awkczyta program z pliku
awk$1, $2, $0pierwsza, druga kolumna, cała linijka
awkNF / $NFliczba kolumn / ostatnia kolumna
awkNRnumer linijki
awkOFSseparator kolumn na wyjściu
awkBEGIN {} / END {}przed pierwszą / po ostatniej linijce
awk/wzor/warunek: linijka pasuje do regeksu
awk$3 ~ /wzor/warunek: kolumna 3 pasuje do regeksu
awk&& / pionowe kreski / !i / lub / nie
awksuma += $4sumowanie kolumny
awkt[$1]++licznik w tablicy (zestawienie)
awkfor (k in t)pętla po kluczach tablicy
awkprintfwypisywanie z formatem, jak w Bashu
awklength()długość napisu

⚠ Typowy błąd

awk -F: "{print $1}" konta.txt | head -2
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin

Miała być pierwsza kolumna, a jest cała linijka. Program był w cudzysłowach, a nie w apostrofach. Bash zajrzał do koperty, zobaczył $1 i podstawił swoją zmienną $1, czyli pierwszy argument skryptu. A tu była pusta. Awk dostał {print }, czyli „wypisz wszystko”. Dwa programy, ten sam znaczek dolara, a każdy myśli, że jest jego. Jak dwóch Kowalskich wołanych w poczekalni.

Naprawa: program awka zawsze w apostrofach. A jeśli chcesz przekazać zmienną z Basha, nie sklejaj cudzysłowów, tylko użyj -v:

powloka=/bin/bash
awk -F: -v p="$powloka" '$7 == p {print $1}' konta.txt

I drugi, cichy grzech: cut -d' ' na wyniku ps albo df. Kolumny wyrównane spacjami, liczba spacji się zmienia i nagle dostajesz puste pola. Tu zawsze awk.

✎ Zadanie

  1. Z /etc/passwd wypisz awk-iem tabelkę kont ludzi (UID od 1000 w górę, bez nobody): nazwa, UID i powłoka w równych kolumnach, z nagłówkiem w BEGIN i liczbą kont w END.
  2. Z df -P wypisz systemy plików zajęte w ponad 50%: punkt montowania i procent. Podpowiedź: znak procentu przeszkadza w porównaniu.
  3. Z sprzedaz.csv policz, ile sztuk każdego produktu sprzedano łącznie, od największej liczby.
Pokaż rozwiązanie

Część pierwsza, tabelka ludzi z /etc/passwd. Pokazuję na pliku konta.txt z poprzedniej lekcji, na prawdziwym /etc/passwd działa tak samo:

awk -F: '
BEGIN { printf "%-12s %6s  %s\n", "KONTO", "UID", "POWLOKA" }
$3 >= 1000 && $1 != "nobody" { printf "%-12s %6d  %s\n", $1, $3, $7; n++ }
END { print "Kont ludzi:", n }
' konta.txt
KONTO           UID  POWLOKA
eryk           1000  /bin/bash
ania           1001  /bin/zsh
stazysta       1002  /bin/bash
Kont ludzi: 3

Część druga, zajęte systemy plików:

df -P | awk 'NR>1 {zaj = $5; sub(/%/, "", zaj); if (zaj+0 > 50) print $6, $5}'
/ 63%
/boot 57%

Czemu nie po prostu $5 > 50? Bo „63%” to dla awka napis, a nie liczba. A napisy porównuje znak po znaku, jak słownik. Wtedy „8%” jest większe od „50”, bo ósemka stoi dalej niż piątka. Dysk zajęty w ośmiu procentach dostałby alarm, a ty telefon o trzeciej w nocy. sub wycina procent, a +0 mówi: „to jest liczba, licz jak liczbę”. -P przy df daje stały format bez łamania długich nazw na dwie linijki.

Część trzecia, sztuki na produkt:

awk -F, 'NR>1 {szt[$3] += $4} END {for (p in szt) print szt[p], p}' sprzedaz.csv | sort -rn
14 kawa
7 herbata

Kawa wygrywa. Jak w każdym dziale IT na świecie.

Co dalej

Umiesz już przerabiać to, co jest w plikach. Tylko że najpierw trzeba te pliki znaleźć. W następnej lekcji find: szukanie po nazwie, rozmiarze, dacie, właścicielu i uprawnieniach, a do tego xargs i locate.