Konsola nie gryzie – operacje na tekście (część druga) 10


Kilka dni temu opublikowaliśmy pierwszą część artykułu o przetwarzaniu tekstu w konsoli. Ponieważ w miarę pisania, jego objętość rosła w zatrważającym tempie, szybko zdaliśmy sobie sprawę, że najsensowniej będzie podzielić go na dwie części — zarówno dla wygody piszących, jak i Czytelników. Oto druga część naszych konsolowych opowieści.

WYRAŻENIA REGULARNE
W poprzedniej odsłonie omówiliśmy podstawowe polecenia i opisaliśmy zastosowanie komendy grep. Co powinniśmy jednak zrobić, jeśli chcemy znaleźć słowa, które trochę się od siebie różnią? Cóż, w takiej sytuacji również możemy wykorzystać grep, po czym… włożyć w wyszukiwanie trochę własnej inicjatywy — przeszukać wynik „na piechotę” i zweryfikować, które linie są poprawne. Inne rozwiązanie,zdecydowanie mniej pracochłonne, to użycie tak zwanych wyrażeń regularnych. Niektórzy spośród naszych Czytelników z pewnością potrafią je stosować, inni przynajmniej wiedzą, co to takiego — ale wśród odbiorców tego artykułu przypuszczalnie znajdą się też osoby, które tylko słyszały o „magicznych” wyrażeniach regularnych i sądzą, że to „hokus-pokus” nie do przeskoczenia dla zwykłych użytkowników. Nic bardziej mylnego…

Wyrażenia regularne (regexp – od angielskiego „regular expressions”) to wzorce opisujące ciąg znaków. Brzmi strasznie niezrozumiale, prawda? Prosty przykład powinien rzucić nieco światła na tę techniczną definicję. Załóżmy, że w pliku opowiadanie.txt mamy jakiś tekst, w którym występują m.in. słowa pompka oraz bombka. Skoro nasz artykuł traktuje o przetwarzaniu tekstu, przyjmijmy, że chcemy w naszym opowiadaniu znaleźć linie zawierające którekolwiek z wspomnianych słów. Oczywiście, możemy dwukrotnie przeszukać tekst, za każdym razem definiując inny obiekt do wyszukiwania. Drugim, lepszym i szybszym, rozwiązaniem będzie wykorzystanie faktu, że interesujące nas słowa różnią się od siebie tylko w dwóch miejscach (p/b-om-p/b-ka). Przy pomocy wyrażeń regularnych możemy tekst przeszukać jedynie raz:

Przykład 1a.
grep "[bp]om[pb]ka" opowiadanie.txt

Nie wygląda to przerażająco, prawda? W późniejszej części, kiedy wyjaśnimy jak używać regexp, stanie się jeszcze prostsze. Żeby jednak ruszyć dalej, musimy zacząć od teoretycznej podbudowy.

Wyrażenia regularne wykorzystują operatory:

  • . — oznacza jeden dowolny znak,
  • * — oznacza dowolną ilość wystąpień poprzedzającego znaku,
  • ^ — jeśli jest to pierwszy znak wyrażenia oznacza początek wiersza,
  • $ — jeśli jest to ostatni znak wyrażenia oznacza koniec wiersza,
  • [lista] — oznacza jeden ze znaków występujących w [ ], może zawierać serię znaków (np. [abcdefgh]), zakres (np. [a-h]) lub oba te przypadki (np. [abcdefghi-z]),
  • [^lista] — symbol ^ działa jak dopełnienie zbioru, zapis ten oznacza dowolny znak, niewymieniony na liście,
  • {A} — oznacza wystąpienie znaku lub znaków poprzedzających operator dokładnie A razy,
  • {A,} — oznacza wystąpienie poprzedzających znaków przynajmniej A razy,
  • {A,Z} — oznacza wystąpienie znaków poprzedzających co najmniej A razy i maksymalnie Z razy,
  • (tekst) — wprawdzie tego wyrażenia nie zastosujemy z grep (nawet z egrep) jednak czujemy się w obowiązku opisać je, umożliwia ono do tekstu zawartego w ( a ) odwołanie się w każdej chwili przez znaki od 1 (pierwsze wyrażenie) do 9,
  • + — oznacza jedno lub więcej wystąpień znaków poprzedzających,
  • ? — oznacza zero lub jedno wystąpienie znaków poprzedzających,
  • | — oznacza alternatywę — znak występujący przed lub po tym symbolu,
  • ( ) — nawias grupuje wyrażenia regularne

Pamiętajmy aby wyrażenia regularne umieszczać w cudzysłowach — podwójnych lub pojedynczych. W przeciwnym wypadku polecenie z regexp może nie zadziałać tak, jak sobie życzymy.

W tym tekście będziemy koncentrować się na wykorzystaniu wyrażeń regularnych w połączeniu z poleceniem grep. Na samym początku musimy wspomnieć o dwóch opcjach tej komendy — -F (równoznaczna z poleceniem fgrep), która nie toleruje żadnych wyrażeń regularnych, a szukany tekst rozumie dosłownie, a także -E (lub analogicznie: egrep), która to wykorzystuje w wyrażeniach regularnych znaki: +, ?, | oraz ( ).

Po solidnej porcji teorii czas zobaczyć, jak to działa w praktyce. Na początku wróćmy jeszcze do przykładowego problemu, od którego zaczęliśmy ten artykuł (ze względów oczywistych nie zamieścimy wyników). Podaliśmy już jedno rozwiązanie:

Przykład 1a.
grep "[bp]om[pb]ka" opowiadanie.txt

Alternatywnie możemy wykorzystać egrep i użyć innych operatorów:

Przykład 1b.
egrep "(p|b)om(b|p)ka" opowiadanie.txt

Skoro już uporaliśmy się z „pokazowym” problemem i wskazaliśmy, jak uniknąć dwukrotnego przeszukiwania tekstu, możemy zaprezentować zastosowania innych wyrażeń regularnych. Gdybyśmy chcieli dowiedzieć się, jacy użytkownicy należą do grupy root, możemy użyć sposobu z Przykładu 2.

Przykład 2.
grep "^root" /etc/group

root::0:root

Może też najść nas ochota, żeby odnaleźć w systemie wszystkich użytkowników korzystających z powłoki bash:

Przykład 3.
grep "bash$" /etc/passwd

root:x:0:0:root:/root:/bin/bash
krzysztof:x:1000:100:,,,:/home/krzysztof:/bin/bash

Ktoś może powiedzieć, że wystarczyłoby tu proste wyszukanie frazy „bash„. Teoretycznie to słuszne stwierdzenie, ale takie wyszukanie zwróciłoby także użytkowników, którzy mają w swoim loginie czy też nazwie użytkownika szukany tekst.

Kolejny przykład trochę rozbudujemy — tym razem szukamy użytkowników, którzy jako swoją grupę domyślną mają ustawioną grupę users (numery identyfikacyjny tej grupy to 100). Już w poprzedniej części artykułu wspominaliśmy, że zawartość poszczególnych linii w pliku /etc/passwd jest podzielona na siedem pół (rozdzielonych znakiem :). W tej chwili interesuje nas czwarte pole, zawierające GID, czyli numer identyfikacyjny grupy. Zatrzymajmy się na chwilkę i popatrzmy, jak wygląda zawartość poszczególnych pól — za chwilę spróbujemy opisać ich zawartość przy pomocy wyrażeń regularnych. Trzecie pole zawiera ID użytkownika, a więc tylko cyfry. Pole numer dwa to hasło użytkownika — w większości systemy korzystają z cieniowania hasła i przetrzymują te dane gdzie indziej, a w /etc/passwd hasło zastąpione jest znakiem x. Pola numer jeden, sześć i siedem (odpowiednio: login, ścieżka katalogu domowego oraz ścieżka do powłoki) muszą zawierać co najmniej jeden znak (w zasadzie nawet trochę więcej…). Pozostaje jeszcze piąte pole – nieobowiązkowa nazwa użytkownika, która może być pustym ciągiem znaków lub zawierać jakiś tekst. Korzystając z tych informacji tworzymy komendę, która pozwoli nam przebrnąć przez każdą możliwą kombinację danych w /etc/passwd:

Przykład 4.
egrep ".+:x:[0-9]+:10{2}:.*:.+:.+$" /etc/passwd

krzysztof:x:1000:100:,,,:/home/krzysztof:/bin/bash

Jak widać w Przykładzie 4 użyliśmy zamiast {A} {A} — użyte narzędzie inaczej trochę interpretuje podane treści. Należy być elastycznym korzystając z różnych narzędzi.

Wyjaśnijmy trochę „chińszczyznę” z Przykładu 4. W przypadku pierwszego, szóstego i siódmego pola /etc/passwd kryterium wyszukiwania stanowiło .+, a więc dowolny znak powtórzony jeden lub więcej razy, dodatkowo pole siódme musi kończyć się znakiem końca linii. Drugie pole — to po prostu x. Pole piąte ma w naszym przykładzie zawierać dowolny znak, powtórzony dowolną ilość razy (.*). Coś takiego zabezpiecza nas przed sytuacją, kiedy pole jest całkowicie puste i nie zawiera ani jednego znaku — do wyrażenia .* pasuje i taka treść. Pole numer trzy — kryterium wyszukiwania [0-9]+ — to po prostu jedno lub więcej powtórzeń dowolnej cyfry. Pozostałe jeszcze do omówienia pole numer cztery (tutaj 10{2}), to nic innego jak jedynka i dwukrotnie powtórzone zero — w praktyce napisalibyśmy 100 (krócej, łatwiej, szybciej), jednak chcieliśmy w tym przykładzie pokazać jak najwięcej możliwości regexp.

Nic nie stoi na przeszkodzie, by grep i wyrażenia regularne wykorzystywać w połączeniu z wynikami innych poleceń i przetwarzaniem potokowym. Dla przykładu, aby uzyskać informacje o wykrywanych przez system kartach sieciowych, możemy skorzystać z polecenia jak w Przykładzie 5.

Przykład 5.
lspci | egrep "(Network|Ethernet)"

00:19.0 Ethernet controller: Intel Corporation 82566MC Gigabit Network Connection (rev 03)
03:00.0 Network controller: Intel Corporation PRO/Wireless 4965 AG or AGN [Kedron] Network Connection (rev 61)

OPERATORY PRZEADRESOWANIA — ZAPIS DO PLIKU I ODCZYT Z PLIKU
Kolejną rzeczą jaką chcieliśmy omówić są operatory przeadresowania. Narzędzia, które przetwarzają dane (czyli wszystkie, które dotychczas opisywaliśmy) posiadają „kanały informacyjne”, którymi dostarczane są dane wejściowe i po przetworzeniu wysyłane dane wyjściowe. Istnieją trzy takie kanały zwane standardowymi strumieniami danych: standardowe wejście (stdin, służy do pobierania danych wejściowych), standardowe wyjście (stdout, służy do odbierania wyjścia polecenia) oraz standardowe wyjście błędów (stderr, służy do odbierania komunikatów o błędach). Przeważnie standardowym wejściem jest klawiatura, wyjściem oraz wyjściem błędów jest terminal.

Większość opisanych już przez nas komend (a także tych, których nie wymieniliśmy), w przypadku wywołania bez parametrów, np.

sort

oczekuje danych wejściowych z standardowego wejścia — z klawiatury. Ale kto chciałby sortować dane podawane z klawiatury? Zazwyczaj sortujemy zawartość pliku. Większość programów umożliwia zmodyfikowanie standardowego wejścia, a czasem także standardowego wyjścia (w przypadku sort jest to opcja -o plik wyjściowy). Co jednak zrobić, jeśli używane przez nas narzędzie nie umożliwia takiego rozwiązania (np. tr)? W takiej sytuacji możemy skorzystać z operatorów przeadresowania.

Gdyby ktoś jednak chciał sortować (czy wykonać inną operacje) na danych z standardowego wejścia (klawiatury) to musi po potwierdzeniu polecenia wypisać dane wejściowe i potwierdzić je kombinacją klawiszy CTRL D.

Omówmy najpierw operator > — przekierowuje on strumień wyjściowy do zadanego pliku, a jego ewentualna dotychczasowa zawartość zostaje nadpisana. Poniżej zamieszczamy kilka przykładów działania tego operatora (polecamy po każdym wykonaniu polecenia odczytać zawartość pliku plik).

Przykład 7a.
cat /etc/fstab > plik

Przykład 7b.
cat /etc/passwd /etc/group > plik

Przykład 7c.
lspci > plik

Plik plik został za każdym razem nadpisany, co czasami (chociaż lepiej byłoby napisać „najczęściej”) nie jest pożądanym efektem. Aby uniknąć kasowania wcześniejszej zawartości plików, wykorzystujemy operator >>, który dane wyjściowe zapisuje na końcu danego pliku. Oto przykłady działania tego operatora:

Przykład 8a.
grep ^$ /etc/fstab >> plik

Przykład 8b.
wc -l /etc/fstab >> plik

Teraz plik powinien zawierać pokaźną porcję danych.

Operator przekierowania standardowego wyjścia może nam posłużyć do „dopisania” wierszy do pliku (bądź też nadpisania jego zawartości). Przykład 9 pokazuje dwie metody dopisania tekstu do pliku, bez jego „normalnej” edycji.

Przykład 9a.
echo "To są nowe
linie dopisane do pliku" >> plik

Przykład 9b.
cat >> plik
To też są kolejne
linie dopisywane do tego samego pliku.
Wyjście z tej powłoki tekstu dopisywanego możliwe jest po wciśnięciu kombinacji klawiszy CTRL D

Skoro opisujemy przekierowanie standardowego wyjścia czas na standardowe wyjście błędu. Przekierowujemy je operatorami 2> oraz 2>>, które działają analogicznie jak ich „odpowiedniki bez dwójki”.

Bardzo często Standardowe wyjście błędów przekierowuje się do pliku /dev/null, co powoduje zignorowanie wszelkich komunikatów o błędach — „wysłanie ich do nicości”. Oczywiście, jeśli sytuacja tego wymaga, możemy zamiast błędów przekierować standardowe wyjście do /dev/null.

Mamy standardowe wyjście i standardowe wyjście błędów. Po co je rozdzielać? Powodów może być kilka:

  • czasem zależy nam na samym wyniku, a komunikaty o błędach (czasem nawet oczekiwanych) nie są dla nas niespodzianką i nie mają dla nas żadnej wartości,
  • czasem zdarza się, że interesują nas tylko i wyłącznie komunikaty o błędach (stanowią informację, czy operacja się powiodła, czy też nie),
  • czasem łatwiej jest nam oddzielnie przeanalizować wyniku i komunikaty o błędach — wynik można dalej obrabiać, natomiast osobny plik informujący o błędach pozwoli na wyeliminowanie ich w późniejszym czasie,
  • powodów może być jeszcze wiele — wszystko zależy od naszych potrzeb

Gdyby jednak zaistniała potrzeba przekierowania standardowego wyjścia oraz standardowego wyjścia błędów do jednego pliku możemy posłużyć się operatorem 2>&1. Co on robi? Mówiąc mało fachowo — przekierowuje standardowego wyjście błędów do tego samego pliku co standardowe wyjście.

Można oczywiście w takim wypadku użyć: > wynik 2> wynik lub jednego operatora >& wynik. Popularniejszym jednak rozwiązaniem jest > wynik 2>&1. Mogą istnieć wersje powłoki Bash (lub inne), w których wspomniane tu operatory mogą nie zadziałać.

Przykład 10a.
cat /etc/fstab= 2> error

Przykład 10b.
cat /etc/fstab /etc/fstab= > wynik 2> error

Przykład 10c.
cat /etc/fstab /etc/fstab= > wynik 2>&1

Ponieważ przekierowanie z użyciem operatora > czy 2> może mieć destrukcyjne działanie (wystarczy, że przypadkiem użyjemy > zamiast >>), wypada tutaj wspomnieć o zmiennej noclobber. Możemy ją uaktywnić na stałe, dodając w ~/.bashrc linijkę:

set -o noclobber

…a operatory > i 2> utracą możliwość nadpisywania istniejących plików. Gdyby zdarzyła się sytuacja, w której naprawdę będziemy chcieli zastąpić w taki sposób zawartość jakiegoś pliku, możemy noclobbera na chwilę wyłączyć:

set +o noclobber

Albo wymusić „normalne” działanie operatorów przy pomocy zapisu >| / 2>|

Pora na operator przekierowania standardowego wejścia. Podobnie jak we wcześniejszych przypadkach, istnieją dwa: < oraz <<, ale ich działanie nie jest już analogiczne do operatorów standardowego wyjścia.
Pierwszy z naszych „bohaterów” (<) odczytuje dane wejściowe z pliku i przekierowuje je na wejście polecenia. Przykładowo jeżeli w pliku opowiadanie.txt mamy tekst, w którym chcemy zmienić litery wielkie na małe skorzystamy z przykładu 11a. Dodatkowo, jeśli zależy nam na zapisaniu wyniku do kolejnego pliku skorzystać należy z przykładu 11b. Ten ostatni — pomimo faktu, iż na pierwszy rzut oka może wyglądać troszkę dziwnie — działa jak tego oczekujemy.

Przykład 11a.
tr 'A-Z' 'a-z' < opowiadanie.txt

Przykład 11b.
tr 'A-Z' 'a-z' < opowiadanie.txt > wynik.txt

Ostatni dziś omawiany operator (<<) przekazuje na standardowe wejście wszystkie dane wczytane z klawiatury – aż do napotkania ogranicznika, który musi wystąpić po operatorze podczas podawania komendy. Taki ogranicznik działa więc niczym swoisty sygnał końca pliku. Przykład 12 powinien rozjaśnić trochę jego działanie.

Przykład 12.
tr 'A-Z' 'a-z' << stop
Ubuntu to stare afrykańskie słowo oznaczające "człowieczeństwo dla wszystkich".
Kierując się tym przesłaniem firma Canonical Ltd.
opracowała i udostępniła opartą na Debianie dystrybucję Linuksa Ubuntu.
stop

Ważne jest by ogranicznik operatora << w tekście wejścia był jedynym wyrażeniem ostatniej linii.

I to już koniec drugiej części artykułu o operacjach na tekście. Tym razem wyszło nieco krócej, ale i omawiane zagadnienia są trochę bardziej zawiłe. Mamy nadzieję, że nikt nie usnął nad klawiaturą, a poruszone tutaj zagadnienia przydadzą się w użytkowaniu Linuksa – ba, może nawet skłonią bardziej dociekliwych Czytelników do samodzielnego poszukiwania metod na oswajanie konsoli?


Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

10 komentarzy do “Konsola nie gryzie – operacje na tekście (część druga)

  • fajne

    Bardzo fajny kurs, autorowi gratuluję.

    Z mojej strony dodam, że fajnie by było przeczytać podobny kurs dotyczący konfiguracji Conky.

    Pozdrawiam

  • k2cl

    @ up
    Jako współautor – dziękuję za ciepłe słowa… Jeśli chodzi o informacje dotyczące Conky – na polskim forum Debiana jest pokaźny tekst na ten temat. Nie wiem, czy jest sens dublować poradniki, poza tym – takie omówienie nie miałoby zbyt wiele wspólnego z konsolą, a to o niej staramy się w tej chwili pisać w naszych artykułach…

  • thalcave

    Mamy nadzieję nie dopuścić do śmierci serwisu :))
    A przy tym mamy nadzieję, że informacje zawarte w artykułach przydadzą się każdemu userowi.

  • Bzyk

    RegExp dla początkujących… Faktycznie „nie wygląda to przerażająco (…)”. Gorzej, jak trzeba coś naprawdę przepuścić przez RegExp. Praktyczne zastosowania pokazują, że ów genialny mechanizm wcale, ale to wcale nie jest prosty… Prosty test, czy oznaczenie pasuje do wzorca: ([a-z]{1,4}[0-9]{1,4}-?[^-$])+

    No i polecam: http://regexpal.com/

  • Gr4jp3r

    Bardzo przydatna seria którą na dodatek miło się czyta. I do tego w profesjonalny sposób 😉
    Mam nadzieję na kolejne odcinki „Konsola nie gryzie”!

  • mlody969

    To i tak nie sprawi że ludzie którzy konsoli nie trawią zaczną ją lubić. Konsola nie jest intuicyjna wymaga znajomosci konkretnych poleceń.

  • kris

    [quote comment=”41442″]Dziękuję. Stronę powoli uważałem za umierającą. Mam nadzieję, że pomożesz jej przetrwać :)[/quote]

    zdecydowanie nie umierająca :). Zaczynam korzystać z konsoli na serwerze ubuntu bez żadnego wcześniejszego doświadczenia które pomogło by mi ugryźć temat. Ta strona jest dla mnie najprzyjaźniejsza.

    Miło jest spotkać tak rzetelnie i przystępnie podane informacje.

    Dla bardzo początkujących to jest o tyle ważne, że zadawanie jakiegokolwiek pytania na jakimkolwiek forum powoduje niestety zawsze oburzenie bardziej doświadczonych troli-użytkowników brakiem profesjonalizmu ze strony pytającego (!sic).

    Pozdrawiam i jeszcze raz dziękuję autorowi za naświetlenie sprawy.

  • majksl

    [quote post=”11046″]Bardzo często Standardowe wyjście błędów przekierowuje się do pliku /dev/null, co powoduje zignorowanie wszelkich komunikatów o błędach — „wysłanie ich do nicości”. Oczywiście, jeśli sytuacja tego wymaga, możemy zamiast błędów przekierować standardowe wyjście do /dev/null.[/quote]

    Chyba autorowi chodziło o przekierowanie na konsole a nie z powrotem do /dev/null