Uwaga: Oryginalna wersja tego artykułu została opublikowana w IBM developerWorks i jest własnością Westtech Information Services. Poniższy dokument jest poprawioną przez zespół GDP wersją oryginalnego tekstu i nie jest już aktualizowany. |
1. Jak jeszcze lepiej wykorzystać edytor tekstowy systemu UNIX
Przyjrzyjmy się jednej z najbardziej przydatnych komend edytora sed, podstawieniu. Przy jej pomocy możemy zastąpić określony ciąg znaków lub dopasowane wyrażenie regularne innym ciągiem znaków. Oto przykład najbardziej elementarnego zastosowania tej komendy:
Listing 1.1: Najbardziej elementarne zastosowanie komendy podstawienia |
$ sed -e 's/cos/cosinnego/' mojplik.txt
|
Powyższe polecenie wypisze zawartość pliku mojplik.txt na standardowe wyjście, zastępując w każdej linii pierwsze wystąpienie napisu "cos" (o ile w ogóle wystąpi) napisem "cosinnego". Zauważmy, że napisałem o pierwszym wystąpieniu w każdej linii, pomimo iż zwykle nie o to nam chodzi. Najczęściej gdy wykonujemy podstawienie napisu, chcemy tego dokonać globalnie. Oznacza to, że chcę zastąpić wszystkie wystąpienia napisu w każdej linii, w ten sposób:
Listing 1.2: Zamiana wszystkich wystąpień w każdej linii |
$ sed -e 's/cos/cosinnego/g' mojplik.txt
|
Dodatkowa opcja 'g' po ostatnim ukośniku mówi sedowi, aby wykonał globalne podstawienie.
Powinniśmy wiedzieć jeszcze kilka rzeczy o komendzie s///. Po pierwsze, mówiliśmy wyłącznie o komendzie; w powyższych przykładach nie podawaliśmy adresów. Oznacza to, że możemy użyć polecenia s/// wraz z adresem, aby kontrolować, do których wierszy będzie ona zastosowana. Robimy to w następujący sposób:
Listing 1.3: Określanie wierszy, do których komenda zostanie zastosowana |
$ sed -e '1,10s/zaczarowanie/zablokowanie/g' mojplik2.txt
|
Powyższy przykład sprawi, że wszystkie wystąpienia wyrazu "zaczarowanie" zostaną zastąpione wyrazem "zablokowanie", ale tylko w wierszach od pierwszego do dziesiątego włącznie.
Listing 1.4: Podawanie dalszych opcji |
$ sed -e '/^$/,/^END/s/wzgórza/góry/g' mojplik3.txt
|
W tym przykładzie wyraz "wzgórza" zostanie zastąpiony wyrazem "góry", ale tylko w tych blokach tekstu, które zaczynają się pustą linią, a kończą linią zaczynającą się od liter "END" włącznie.
Kolejną miłą rzeczą dotyczącą komendy s/// jest mnogość opcji dotyczących separatorów /. Jeśli chcemy dokonać podstawienia napisów i wyrażenie regularnie lub napis, który chcemy zmienić zawiera wiele ukośników, możemy zmienić separator, podając inny znak po literze "s". Zilustrujmy to przykładem, w którym zamienimy wszystkie wystąpienia /usr/local na /usr:
Listing 1.5: Zamiana wszystkich wystąpień napisu innym napisem |
$ sed -e 's:/usr/local:/usr:g' mojalista.txt
|
Uwaga: W powyższym przykładzie użyliśmy dwukropka jako separatora. Jeśli musielibyśmy użyć znaku separatora w wyrażeniu regularnym, należałoby poprzedzić go odwrotnym ukośnikiem. |
Do tej pory wykonywaliśmy jedynie podstawienia zwykłych napisów. Mimo iż często się to przydaje, to jednak możemy zrobić to samo dla wyrażeń regularnych. Poniższa komenda znajdzie frazę rozpoczynającą się znakiem "<" i kończącą się znakiem ">", posiadającą dowolną ilość znaków pomiędzy nimi. Fraza ta zostanie skasowana (zastąpiona pustym napisem):
Listing 1.6: Kasowanie podanej frazy |
$ sed -e 's/<.*>//g' mojplik.html
|
Jest to dobry początek skryptu, który usunie tagi języka HTML z pliku, jednakże nie zadziała dobrze ze względu na pewną właściwość wyrażeń regularnych. Mianowicie gdy sed szuka wyrażenia regularnego w linii, znajduje najdłuższe możliwe dopasowanie w tej linii. Nie było to problemem w poprzednim artykule o sedzie, ponieważ używaliśmy komend d i p, które tak czy inaczej skasują lub wypiszą całą linię. W przypadku komendy s/// nie odpowiada nam to, ponieważ cały dopasowany fragment tekstu zostanie zastąpiony przez docelowy napis, a w tym przypadku skasowany. Oznacza to, że powyższy przykład zamieni następującą linię:
Listing 1.7: Przykładowy kod HTML |
<b>Właśnie</b> o to <b>mi</b> chodziło. |
W tę:
Listing 1.8: Niepożądany wynik |
chodziło. |
Zamiast w poniższą, czyli w to, co chcieliśmy osiągnąć:
Listing 1.9: Pożądany wynik |
Właśnie o to mi chodziło. |
Na szczęście da się temu zaradzić. Zamiast wyrażenia regularnego, które znajdzie "dowolną ilość znaków, poprzedzoną znakiem '<' i zakończoną znakiem '>'" musimy użyć takiego, które odnajdzie "dowolną ilość znaków różnych od znaku '>', poprzedzoną znakiem '<' i zakończoną znakiem '>'". W ten sposób znajdziemy najkrótsze możliwe dopasowanie, zamiast najdłuższego. Nowe polecenie powinno wyglądać tak:
Listing 1.10: |
$ sed -e 's/<[^>]*>//g' mojplik.html
|
W powyższym przykładzie wyrażenie "[^>]" oznacza "znak różny od '>'", a symbol "*" uzupełnia je o znaczenie "zero lub więcej znaków różnych od '>'". Warto wypróbować to polecenie na kilku przykładowych plikach html, przeglądając wyniki za pomocą programu more.
Dopasowanie większej ilości znaków
Składnia wyrażenia regularnego "[ ]" oferuje jeszcze kilka możliwości. Możemy określić zakres znaków za pomocą symbolu "-", o ile nie znajduje się on na pierwszym lub ostatnim miejscu:
Listing 1.11: Podawanie zakresu znaków |
'[a-x]*' |
W ten sposób wyszukamy zero lub więcej znaków, dopóki wszystkie należeć będą do zbioru "a","b","c"..."v","w","x". Oprócz tego dysponujemy klasą znakową "[:space:]" do znajdywania odstępu. Poniżej znajduje się prawie kompletna lista dostępnych klas znakowych.
| Klasa znakowa | Opis |
| [:alnum:] | Znaki alfanumeryczne [a-z A-Z 0-9] |
| [:alpha:] | Znaki alfabetyczne [a-z A-Z] |
| [:blank:] | Spacje lub tabulatory |
| [:cntrl:] | Dowolny znak kontrolny |
| [:digit:] | Cyfry [0-9] |
| [:graph:] | Znaki drukowalne (bez odstępów) |
| [:lower:] | Małe litery [a-z] |
| [:print:] | Znaki drukowalne z odstępami |
| [:punct:] | Znaki drukowalne za wyjątkiem odstępów, liter i cyfr |
| [:space:] | Wszystkie znaki odstępu |
| [:upper:] | Duże litery [A-Z] |
| [:xdigit:] | Cyfry w systemie szesnastkowym [0-9 a-f A-F] |
Używanie klas znakowych wszędzie tam, gdzie to możliwe jest wysoce pożądane, ponieważ znakomicie dopasowują się one do innych niż angielskie zestawów znaków (uwzględniając znaki akcentowane, itd).
Do tej pory przyjrzeliśmy się prostym i w miarę złożonym podstawieniom, ale sed potrafi jeszcze więcej. Możemy odnosić się do dowolnej części lub całości tekstu odnalezionego przez wyrażenie regularne i użyć jej do stworzenia podstawianego napisu. Przykładowo, załóżmy że chcemy odpowiedzieć na wiadomość. Poniższa komenda poprzedzi każdą linię wyrażeniem "ralph powiedział: ":
Listing 1.12: Poprzedzanie każdej linii określonym napisem |
$ sed -e 's/.*/ralph powiedział: &/' oryginalnawiadomosc.txt
|
Oto wynik jej działania:
Listing 1.13: Efekt działania powyższej komendy |
ralph powiedział: Cześć Jim, ralph powiedział: ralph powiedział: Bardzo podoba mi się ta lekcja seda! ralph powiedział: |
W powyższym przykładzie wykorzystaliśmy znak "&" w podstawianym napisie. Mówi on sedowi, aby wstawić w tym miejscu cały tekst znaleziony przez wyrażenie regularne. Tak więc cokolwiek zostało znalezione przez ".*" (największa grupa zera lub więcej znaków w linii lub cała linia) może zostać podstawione w dowolnym miejscu podstawianego tekstu nawet kilka razy. Całkiem nieźle, ale sed potrafi jeszcze więcej.
Dyskretny urok nawiasów poprzedzonych odwrotnymi ukośnikami
Komenda s/// pozwala nam na definiowanie regionów w wyrażeniu regularnym, do których możemy się następnie odwoływać w podstawianym tekście. W ramach przykładu przyjrzyjmy się następującemu tekstowi:
Listing 1.14: Przykładowy tekst |
bla ble bli ele mele dudki ala ma kota kot ma alę |
Załóżmy teraz, że chcemy napisać skrypt w sedzie, który zamieni "ele mele dudki" na "Victor ele-mele Von dudki" i tak dalej. Aby tego dokonać najpierw powinniśmy napisać wyrażenie regularne, które odnajdzie nasze trzy wyrazy oddzielone spacjami:
Listing 1.15: Odpowiednie wyrażenie regularne |
'.* .* .*' |
Właśnie tak. Następnie zdefiniujmy regiony, umieszczając wokół każdego interesującego nas miejsca nawiasy poprzedzone wstecznym ukośnikiem:
Listing 1.16: Definiowanie regionów |
'\(.*\) \(.*\) \(.*\)' |
Powyższe wyrażenie regularne zadziała indentycznie jak pierwsze. Jedyną różnicą będzie fakt, iż definiuje ono trzy logiczne regiony, do których będziemy mogli odnieść się w naszym podstawianym napisie. Oto ostateczny skrypt:
Listing 1.17: Ukończony skrypt |
$ sed -e 's/\(.*\) \(.*\) \(.*\)/Victor \1-\2 Von \3/' mojplik.txt
|
Jak widać, odnosimy się do każdego obszaru ograniczonego nawiasami za pomocą wyrażenia "\x", gdzie x jest numerem regionu, zaczynając od jedynki. Efekt działania skryptu będzie następujący:
Listing 1.18: Wynik działania powyższej komendy |
Victor bla-ble Von bli Victor ele-mele Von dudki Victor ala-ma Von kota Victor kot-ma Von alę |
W miarę jak poznajemy seda coraz bliżej, możliwe staje się dokonywanie skomplikowanych operacji na tekście przy minimum wysiłku. Warto zastanowić się nad rozwiązaniem powyższego problemu za pomocą dowolnego języka skryptowego -- czy udałoby nam się równie łatwo zmieścić rozwiązanie w jednej linijce?
Gdy zaczynamy tworzyć bardziej złożone skrypty, potrzebujemy możliwości wpisywania więcej niż jednej komendy naraz. Można tego dokonać na kilka sposobów. Po pierwsze, możemy rozdzielić komendy średnikami. Na przykład ten zestaw poleceń używa komendy "=", która każe sedowi wypisać numer linii, oraz komendy p, która specjalnie instruuje seda, aby wypisał linię (ponieważ pracujemy w trybie "-n"):
Listing 1.19: Pierwszy sposób, średniki |
$ sed -n -e '=;p' mojplik.txt
|
Gdy podajemy dwie lub więcej komend, każda z nich jest zastosowana (po kolei) do każdej linii pliku. W powyższym przykładzie na pierwszej linii zostanie użyta komenda "=", a następnie p. Sed wówczas przechodzi do drugiej linii i proces powtarza się. Pomimo iż średnik okazał się być przydatny, w niektórych sytuacjach nie zadziała. Alternatywą będzie użycie dwóch parametrów -e w celu podania dwóch osobnych komend:
Listing 1.20: Drugi sposób, wiele parametrów -e |
$ sed -n -e '=' -e 'p' mojplik.txt
|
Jednak gdy tylko dojdziemy do bardziej złożonych poleceń dodawania i wstawiania, nawet wiele parametrów "-e" nam nie pomoże. Wówczas najlepiej zrobimy zapisując wszystkie komendy w osobnym pliku, a następnie wywołując go parametrem -f:
Listing 1.21: Trzeci sposób, zewnętrzny plik zawierający komendy |
$ sed -n -f mojekomendy.sed mojplik.txt
|
Ten sposób, choć mniej wygodny, nigdy nas nie zawiedzie.
Wiele komend wykonanych na jednym adresie
Czasem zajdzie potrzeba wykonania kilku poleceń na jednym przedziale. Szczególnie jest to przydatne gdy chcemy wykonać wiele komend s/// w celu przekształcenia wyrazów lub składni w pliku źródłowym. Aby wykonać wiele komend na jednym adresie zapiszmy je w pliku i użyjmy znaków "{ }" do pogrupowania ich:
Listing 1.22: Podawanie wielu komend dla jednego adresu |
1,20{
s/[Ll]inux/GNU\/Linux/g
s/samba/Samba/g
s/posix/POSIX/g
}
|
Powyższe polecenia zastosują trzy podstawienia w liniach od pierwszej do dwudziestej włącznie. Jako adresów moglibyśmy również użyć wyrażenia regularnego lub połączyć jedno z drugim:
Listing 1.23: Połączenie obu sposobów |
1,/^END/{
s/[Ll]inux/GNU\/Linux/g
s/samba/Samba/g
s/posix/POSIX/g
p
}
|
W powyższym przykładzie wszystkie komendy między znakami "{ }" zostaną użyte na tekście od pierwszej linii aż po linię, która zaczyna się literami "END" lub, w przypadku braku takowej, aż po koniec pliku.
Dołączanie, wstawianie i zmienianie linii
Teraz gdy już piszemy skrypty seda w osobnych plikach, możemy wykorzystać polecenia dołączania, wstawiania i zmiany linii. Polecenia te wstawią wiersz po aktualnej linii, przed aktualną linią lub zastąpią ją. Można ich także używać do wstawiania wielu wierszy jednocześnie. Komendy wstawiania używa się w następujący sposób:
Listing 1.24: Użycie komendy wstawiania wiersza |
i\ Ten wiersz zostanie wstawiony przed każdą linią |
Jeśli nie podamy adresu dla tej komendy, zostanie ona zastosowana do każdej linii, a wynik jej działania będzie podobny do poniższego:
Listing 1.25: Efekt działania powyższego polecenia |
Ten wiersz zostanie wstawiony przed każdą linią oto linia 1 Ten wiersz zostanie wstawiony przed każdą linią oto linia 2 Ten wiersz zostanie wstawiony przed każdą linią oto linia 3 Ten wiersz zostanie wstawiony przed każdą linią oto linia 4 |
Jeśli zechcemy wstawić więcej niż jeden wiersz przed aktualną linią, wystarczy że dopiszmy je, dodając odwrotny ukośnik na końcu każdego poprzedniego wiersza:
Listing 1.26: Wstawianie wielu wierszy przed aktualną linią |
i\ wstaw ten wiersz\ i ten\ i ten\ o, nie zapomnijmy tez o tym. |
Komenda dołączania działa podobnie, tylko dołącza wiersz lub wiersze po aktualnej linii. Oto przykład:
Listing 1.27: Dołączanie wiersza po aktualnej linii |
a\ proszę wstawić ten wiersz po każdej linii. Z góry dziękuję! :) |
Z kolei polecenie "zmień linię" podmieni ją na nową.
Ze względu na to, iż komendy dołączania, wstawiania i zamiany linii muszą być podawane w kilku wierszach, należy wpisywać je w plikach tekstowych i podawać sedowi za pomocą parametru "-f". Pozostałe metody wpisywania komend zaowocują problemami.
W następnym artykule, który będzie już ostatnim na temat seda, ukazane zostaną znakomite przykłady z życia wzięte na zastosowanie seda do wielu różnych zadań. Nie tylko dowiemy się co skrypty robią, ale też w jaki sposób działają. Z pewnością zaowocuje to wieloma pomysłami na zastosowanie seda w rozmaitych projektach. Do zobaczenia w następnym odcinku!