Tak się jakoś w moim życiu zawodowym poukładało w ostatnich miesiącach, że niby jestem bliżej baz danych niźli kiedykolwiek wcześniej – przynajmniej po nazwie stanowiska sądząc – ale tak nie do końca, a przynajmniej okazuje się, że w moim obecnym miejscu pracy skryptowanie w bash może być równie ważne jak budowanie różnych struktur przy pomocy SQL. Napiszę więcej nawet: jest to właściwie jedyne narzędzie w dostępnym mi instrumentarium, by osiągnąć jakąkolwiek automatyzację na poziomie moich interakcji z bazą danych.
Czy to dobrze czy wręcz przeciwnie, nie ma to w tym miejscu znaczenia, choć opinię w tym zakresie mam dość jednoznaczną. Faktem jednak pozostaje, że na tę okoliczność musiałem sobie odświeżyć co nieco wiedzę z zakresu pracy w powłoce uniksowej, co do pewnego stopnia mnie nawet cieszy. I właśnie w tym obszarze stanąłem w zeszłym tygodniu przed pewnym wyzwaniem, w przypadku którego musiałem trochę popracować z tekstem, choć na moje nieszczęście tylko umiarkowanie ustrukturyzowanym, więc przy pomocy kilku prostych “grep-ów” oraz “cut-ów” nie udało mi się osiągnąć zamierzonego rezultatu.
Oczywiście w dobie internetów czy inszych GPT każdy dyletancki klepacz kodu – jak piszący te słowa – przy odrobinie wytrwałości znajdzie rozwiązanie niemal każdego problemu. Tak też stało się w moim przypadku i temat, który na pierwszy rzut oka wydawał się dość złożony, udało się rozwiązać przy pomocy jednej linijki kodu, gdzie dwa razy wykorzystałem polecenie awk połączone za pomocą pipe’a Nie byłbym jednak sobą, gdyby przy tej okazji wreszcie nie zmierzył się z tym tematem.
Napisałem “wreszcie”, ponieważ rzecz jasna miałem świadomość istnienia awk. Ba, nie raz i nie dwa miałem okazję z niego skorzystać – przy okazji posługiwania się cudzymi skryptami czy nawet kopiując z nich całe fragmenty do własnych rozwiązań. Tym niemniej jak dotąd los był na tyle dla mnie łaskawy, że nie zmusił mnie do konieczności pochylania się nad tym zagadnieniem. To czego jednak nie wiedziałem jeszcze do wczoraj, gdy zacząłem szukać informacji na temat awk, to nie wyobrażałem sobie jak potężne jest to narzędzie (do tej pory wyobrażałem sobie, że jest to nieco bardziej wyszukana wersja polecenia grep). Z tego “uczucia” zdziwienia zrodziła się potrzeba popełnienia tego wpisu.
Oczywiście w zgodzie z duchem tego bloga nie będzie to wyczerpujące wprowadzenie do awk, ale raczej dość pobieżne – a przy tym raczej łagodne – przybliżenie tematu. Tym bardziej, że za mną zaledwie kilka godzin treningu w tej materii.
Czym jest awk?
Polecenie awk jest jednym z najstarszych narzędzi dostępnych w systemach UNIX-owych. Jego geneza sięga jeszcze lat 70-tych ubiegłego stulecia, kiedy to zostało opracowane w kazamatach Bell Labs przez Alfreda Aho, Petera Weinbergera i Briana Kernighana (wyjątkowo bystry czytelnik być może zauważył, że pierwsze litery nazwisk tych panów składają się na nazwę “bohatera” tego tekstu). Rzecz jasna od tego czasu narzędzie to było cały czas rozwijane i doczekało się co najmniej kilku różnych implementacji. Tak naprawdę w przypadku większości (jeśli nie wszystkich) popularnych dystrybucji Linuksa de facto korzysta się z GAWK (od GNU awk).
Tyle jeśli chodzi o podbudowę historyczną, natomiast od strony praktycznej polecenie awk ma następującą składnię:
awk [opcje] [program] [lista-plików]
W miejsce programu równie dobrze może być podstawiona ścieżka do pliku, gdzie ten kod się znajduje, choć wymaga to wówczas posłużenia się odpowiednią opcją (“-f”). Rzecz jasna na potrzeby niniejszego wpisu nie będzie potrzeby korzystania z takiej możliwości, ponieważ będą to raczej proste wprawki w tym narzędziu, należy mieć jednak świadomość, że awk jest równocześnie pełnoprawnym językiem skryptowym, gdzie można się posługiwać typowymi strukturami programistycznymi jak zmienne, pętle, instrukcje warunkowe, tablice asocjacyjne czy wreszcie istnieje możliwość pisania własnych funkcji. Jeśli chodzi o te ostatnie, to awk zawiera również szereg wbudowanych funkcji, którymi jednak zajmować się w tym wpisie nie zamierzam – tym niemniej ich liczba robi całkiem niezłe wrażenie.
Sam program niezależnie od tego czy napiszemy go bezpośrednio w wierszu poleceń czy umieścimy w pliku składa się z co najmniej jednego wiersza zawierającego wzorzec oraz akcję. Zadaniem wzorca jest wybranie – według pewnego klucza – pasujących wierszy z danych wejściowych. Następnie na każdym z nich przeprowadzana jest stosowna akacja, którą sobie zaplanowaliśmy. Dla odróżnienia jednego od drugiego akcje są ujęte w nawiasy klamrowe, zaś całość ujęta w apostrofy lub – jak ktoś woli – w pojedyncze cudzysłów. W związku z tym schemat programu można zapisać w następujący sposób:
‘wzorzec{akcja}’
W przypadku programu awk wzorzec nie jest “zestawem obowiązkowym” i jeśli zostanie pominięty, wówczas na warsztat zostaną pobrane wszystkie wiersze z danych wejściowych. Gdy jednocześnie nasza akcja sprowadza się do wyświetlenia wszystkiego jak leci (do tego celu wykorzystamy instrukcję print), tym sposobem otrzymujemy ekwiwalent polecenia cat:
awk ‘{print}’ /etc/passwd #cat /etc/passwd
Jeśli do powyższego polecenia dołożymy jednak jakiś wzorzec, wówczas dostaniemy nieco fikuśny zastępnik dla polecenia grep, ponieważ w konsoli ujrzymy wszystkie wiersze, które zostały odfiltrowane (w przykładzie poniższym zwracam uwagę na ukośniki, w których umieszczony został wzorzec):
awk ‘/bash/{print}’ /etc/passwd #grep bash /etc/passwd
Nic też nie stoi na przeszkodzie by używać awk (przynajmniej do pewnego stopnia) jako ekwiwalentu programu head (przykłady poniżej odpowiadają poleceniu head -n 5 /etc/passwd)
awk ‘NR < 6 {print}’ /etc/passwd
awk ‘NR==1, NR==5 {print}’ /etc/passwd
Oba zapisy są równoważne i podałem je nieprzypadkowo “w tandemie”, ponieważ pozwalają mi na wprowadzenie dwóch kolejnych kwestii. Pierwsza z nich dotyczy zmiennych. Rzecz jasna w awk można deklarować własne zmienne (tym zajmować się nie będziemy), ale w samym programie znajduje się również kilka wbudowanych, właśnie między innymi “NR”, której wartość odpowiada numerowi linii dla danego wiersza/linii. Z tej perspektywy pierwsze przykładowe polecenie wydaje się dość oczywiste, ponieważ w jego warunku określamy, że numer szukanych wierszy musi być mniejszy niż 6, czyli interesują nas linie od 1 do 5. Nieco bardziej zagadkowo prezentuje się przykład drugi, natomiast wystarczy w tym miejscu wspomnieć, że przecinek w przypadku wzorca spełnia rolę operatorem zakresu i jeżeli dwa wzorce (tutaj NR==1 oraz NR==2) rozdzielisz przecinkiem, awk wybierze zakres wierszy, począwszy od pierwszego dopasowania na drugim kończąc. Jeżeli żaden wiersz nie zostanie dopasowany do drugiego “filtra” pobrane zostaną wszystkie rekordy do końca pliku/źródła danych. Co ciekawe kiedy w wyniku wykonywania polecenia zostanie znaleziony drugi wzorzec, wówczas cała zabawa zaczyna się od początku, czyli awk kontynuuje przetwarzanie dokumentu w poszukiwaniu kolejnego dopasowania zaczynając od pierwszej wartości.
Rzecz jasna zmiennych można używać nie tylko we wzorcu, ale również w obrębie akcji. Niżej widać dość banalny przykład takiego zastosowania, w których przy pomocy zmiennej “NR” urozmaicony nieco został sposób prezentowania wybranych wierszy (w tym wypadku zawierających łańcuch znaków “bash”, ponieważ tak stanowi wzorzec).
awk ‘/bash/{ print “linia nr ” NR “: ” $0 }’ /etc/passwd
Przy okazji tego przykładu chciałbym zwrócić uwagę na dwie kwestie. Pierwsza być może oczywista, ale warto ją podkreślić: jak widać numer linii odpowiada faktycznemu położeniu danego wiersza w dokumencie, a nie temu co finalnie zostało przekazane do konsoli. Stąd w moim przypadku mamy 1, 48, 54 oraz 55 a nie 1,2,3,4.
Druga rzecz dotyczy faktu posłużenia się w powyższym poleceniu kolejną zmienną wbudowaną, czyli w tym konkretnym scenariusz “$0”. Ta ostatnia odpowiada czy też reprezentuje zawartość całego wiersza. Być może na ten moment niewiele to wyjaśnia, ale stanie się to jaśniejsze, kiedy omówimy następujące polecenie:
awk -F: ‘/^b/{ print $1 ” || ” $6 }’ /etc/passwd
W tym wypadku efekt jest znacząco odmienny od tego co dotychczas mogliśmy obserwować, ponieważ zamiast całego wiersza wyświetlone zostały tylko wybrane wartości, czyli odpowiadające 1 i 6 kolumnie (jeśli tak to można ująć) przedzielone dwoma kreskami pionowymi, które wstawiliśmy do naszej akcji. Właśnie do tego właśnie służą zmienne numerowane od $1 do $n.
Tym niemniej, żeby to było możliwe konieczne było uruchomienie polecania awk z opcją -F (duże F w odróżnieniu od małego, który dotyczy – jak pamiętamy – wskazania ścieżki do pliku z programem). Jej użycie pozwala na wskazanie separatora poszczególnych wartości w ramach wiersza. Domyślnie w przypadku awk jest nim spacja, natomiast w przypadku pliku “passwd” rolę takiego ogranicznika spełnia dwukropek, stąd konieczność posłużenia się tą opcją w naszym przykładzie. Oczywiście w roli separatora może występować nie tylko pojedynczy znak, ale również dłuższy lub krótszy łańcuch tekstowy, co akurat bardzo mi się przydało w ramach zadania, o którym wspomniałem gdzieś na początku tekstu.
Coby całość zebrać do kupy wykonajmy pewne praktyczne zadanie. Tym samym załóżmy, że dysponujemy plikiem “menu.xml”, który zawiera informacje o jadłospisie dla naszej ulubionej kawiarenki. Niestety całość jest w formacie nieszczególnie przyjaznym do czytania dla zwykłego śmiertelnika, ponieważ plik – jak wskazuje jego rozszerzenie – jest w formacie xml. Całość wygląda jak niżej (przykład został pobrany ze strony w3schools.com).
<?xml version="1.0" encoding="UTF-8"?>
<breakfast_menu>
<food>
<name>Belgian Waffles</name>
<price>$5.95</price>
<description>
Two of our famous Belgian Waffles with plenty of real maple syrup
</description>
<calories>650</calories>
</food>
<food>
<name>Strawberry Belgian Waffles</name>
<price>$7.95</price>
<description>
Light Belgian waffles covered with strawberries and whipped cream
</description>
<calories>900</calories>
</food>
<food>
<name>Berry-Berry Belgian Waffles</name>
<price>$8.95</price>
<description>
Belgian waffles covered with assorted fresh berries and whipped cream
</description>
<calories>900</calories>
</food>
<food>
<name>French Toast</name>
<price>$4.50</price>
<description>
Thick slices made from our homemade sourdough bread
</description>
<calories>600</calories>
</food>
<food>
<name>Homestyle Breakfast</name>
<price>$6.95</price>
<description>
Two eggs, bacon or sausage, toast, and our ever-popular hash browns
</description>
<calories>950</calories>
</food>
</breakfast_menu>
Powiedzmy, że chcielibyśmy w prosty sposób wyświetlić informacje o dostępnych w menu pozycjach wraz z przypisaną do nich ceną. Wówczas nasze polecenie mogłoby wyglądać w następujący sposób:
awk -F”[>|<]” ‘/name/ {print “Nazwa: ” $3} /price/{print “Cena: ” $3}’ test.xml
W efekcie otrzymaliśmy taką listę: Nazwa: Belgian Waffles Cena: $5.95 Nazwa: Strawberry Belgian Waffles Cena: $7.95 Nazwa: Berry-Berry Belgian Waffles Cena: $8.95 Nazwa: French Toast Cena: $4.50 Nazwa: Homestyle Breakfast Cena: $6.95 |
W przypadku powyższego polecenia chciałbym zwrócić uwagę na dwie kwestie. Po pierwsze pojawia się tutaj nowy sposób zapisu separatora. Otóż taka forma – znana z wyrażeń regularnych – pozwala nam na wskazanie więcej niż jednego ogranicznika, dlatego też w naszym poleceniu w roli separatora występuje zarówno znak większości jak i mniejszości. Stąd też interesująca nas wartość występuje dopiero na 3 pozycji (pierwsza to spacja, druga to “name” oraz “price”, zaś 4 to “/name” i “/price”). Ponadto w ramach jednego polecenia – to jest ta druga kwestia – zostały wywołane dwie pary wzorzec/akcja, tak aby odszukać wszystkie interesujące nas pozycje.
Rzecz jasna całość można byłoby zapisać jeszcze “czytelniej”, czyli na ten przykład zamiast rozbijać każdą pozycję w menu na dwie linie, moglibyśmy wyświetlić je w jednym wierszu. Tym niemniej wymagałoby to wprowadzenia zmiennych do naszego programu, a tym zajmować się – jak pisałem wcześniej – nie planowałem w tym wpisie. Tak samo zresztą jak wieloma innymi zagadnieniami typu pętle czy tablice asocjacyjne, których występowanie sygnalizowałem parę akapitów wyżej. Przede wszystkim kilka popołudniowych godzin spędzonych z awk nie uzbroiło mnie w takie kompetencje, a też póki co nie widzę praktycznego ich zastosowanie w mojej pracy. Jedno i drugie być może się zmieni w bliższej lub dalszej przyszłości i być może wówczas pokuszę się o kolejny poświęcony awk.
Od blokach dwa zdania
Na sam koniec już tylko bardzo krótko wspomnę o dwóch specjalnych blokach, które dostępne są w awk oprócz “właściwej” części programu. Chodzi mianowicie o blok BEGIN oraz END, które wykonują pewne zdania odpowiednio przed i po przetwarzaniu danych wejściowych przez “właściwą” część programu. Pierwszy z elementów zwyczajowo służy do wyświetlania nagłówka, choć za jego pomocą można osiągnąć znacznie więcej, zaś drugi na ogół stanowi podsumowanie wcześniejszych działań. Kto będzie chciał to sobie doczyta lub “doogląda” w Internetach.