Mniej znany BASH

W tym artykule chciałbym na konkretnych przykładach pokazać użyteczność niektórych poleceń powłoki BASH, wspartych czasem zewnętrznymi programami. Jest to najpopularniejsza i domyślna na ogół powłoka w systemie Linux. Polskie tłumaczenie jej podręcznika systemowego liczy ponad 5000 linijek (tak przynajmniej w mojej konsoli polecenie 'man bash | wc -l' zliczyło) i na ogół są w nim podane tylko uogólnione metody korzystania z jej poleceń. Użytkownikowi mającemu mało do czynienia z programowaniem trudno raczej będzie widzieć praktyczne zastosowania po jego lekturze. Sam nigdy nie czytałem go od deski do deski, a mało które z moich doświadczeń bierze się wprost z jego lektury. Zazwyczaj najpierw podglądam przykłady, a później ogólne definicje w podręczniku. A jest w czym przebierać. Prócz podobnych artykułów mamy całą masę skryptów w systemie. Chciałbym pokazać tylko to, czego nie znalazłem wytłumaczonego w dobry dla mnie sposób lub rzeczy które chciałbym po swojemu opisać.

Zapewne żaden z takich przykładów nie pokaże całej mocy polecenia w nim użytego. To raczej punkt wyjścia do zrozumienia ogólniejszego działania opisanego w podręczniku.

Kopię polskiej wersji podręcznika można znaleźć również na stronie PTM

Przypuśćmy, że

 ~ $ NIEBO=nimbostratus-09.03.2007.xcf.gz

…i wyświetlmy

 ~ $ echo ${NIEBO%.*} nimbostratus-09.03.2007.xcf

Jak widać, dostaliśmy wartość zmiennej NIEBO pozbawioną ostatniej kropki i dalszych znaków. Ogólniej, konstrukcja z “%” obcięła tej zmiennej najkrótszy łańcuch postaci “.*” pasujący do końca. Przydaje się to zazwyczaj, gdy chcemy uzyskać nazwę pliku bez rozszerzenia (bardziej złożone rozszerzenia jak podane tu .xcf.gz wymagać mogą osobnego potraktowania – o tym kawałek niżej). Zamieńmy teraz “%” na “%%“:

 ~ $ echo ${NIEBO%%.*} nimbostratus-09

Tym razem pozbawiliśmy NIEBO najdłuższej końcówki postaci “.*“. Podobnie użyty znak “#” zamiast “%” pozwala nam usunąć początkowy (najkrótszy lub najdłuższy odpowiednio) kawałek zmiennej:

 ~ $ echo ${NIEBO#*.} 03.2007.xcf.gz  ~ $ echo ${NIEBO##*.} gz

Zwróćmy przy okazji uwagę na postać wycinanego łańcucha: “*.” (czyli cokolwiek lub nic, a potem kropka), a nie “.*“. Oczywiście, zamiast wzorca opisującego jedną z tych postaci możemy użyć inny – jak przy rozwijaniu nazw plików w powłoce. Zrobimy to zresztą w następnym przykładzie.

Stwórzmy nowy katalog “chmury“, a w nim trzy puste (jako standardowe wyjścia pustych poleceń) pliki nimbostratus-09.03.2007.xcf.gz, altostratus-02.10.2000.xcf.bz2 oraz altocumulus_floccus-09.06.1996.tiff:

 tmp $ mkdir chmury  tmp $ >chmury/$NIEBO  tmp $ >chmury/altostratus-02.10.2000.xcf.bz2  tmp $ >chmury/altocumulus_floccus-09.06.1996.tiff  tmp $ cd chmury

Dwa z nich zawierają rozszerzenia z kropką, jeden – bez. Chciałbym masowo okroić je z tych rozszerzeń. Zakładam przy tym, że rozszerzeń zawierających kropkę (jak “xcf.gz“) jest tyle, że mogę je łatwo określić. Tu są dwa, choć można by wymyślić bardziej złożony przykład.

 chmury $ for plik in *; do echo "${plik%%.@(+([^.])|xcf.@(bz2|gz))}"; done altocumulus_floccus altostratus-02.10.2000 nimbostratus-09.03.2007

Użyłem tu tzw. rozszerzonych operatorów dopasowania wzorców (szczegóły w podręczniku). Po kropce (podanej po “%%“) ma wystąpić dokładnie raz (stąd @()) łańcuch który jest: albo niepustym ciągiem znaków nie będących kropką (+([^.])) , albo łańcuchem “xcf.gz” lub “xcf.bz2” (co opisuje alternatywa xcf.@(bz2|gz)). Jeśli w tym kroku pętli for plik=altostratus-02.10.2000.xcf.bz2, to “+([^.])” przybiera postać “bz2“, a “xcf.@(bz2|gz)” – “xcf.bz2“. Znaki “%%” nakazują wybrać najdłuższe z otrzymanych dwóch dopasowań (o kropce nie zapomnijmy na początku), czyli “.xcf.bz2“, i wyciąć ze zmiennej plik, o ile tylko pasuje do jej końca.

Uwaga: użycie rozszerzonych operatorów dopasowania wzorców wymaga włączenia opcji extglob w powłoce. Zawsze można w tym celu wklepać shopt -s extglob w linii komend lub ~/.bashrc, a sprawdzić stan poleceniem shopt bez parametrów (podręcznik…).

Teraz, gdyby stworzone pliki były obrazkami, moglibyśmy użyć podanej formy do zmiany rozszerzenia przy okazji konwersji ich do JPEG i ewentualnego zmniejszenia przy zachowaniu proporcji tak, by weszły w kwadrat 500x500px. Tym razem wpakowałem polecenia do skryptu, a przy okazji okazało się, że działający extglob w powłoce nie oznacza, że i w skrypcie zadziała.

#!/bin/bash shopt -s extglob mkdir 500-tki for plik in *; do 	convert -geometry '500x500>' -quality 75 \ 	 "$plik" 500-tki/"${plik%%.@(+([^.])|xcf.@(bz2|gz))}".jpg done

Na marginesie: convert i inne programy z ImageMagick nie zawsze prawidłowo odczytują pliki Gimpa XCF, prawdopodobnie ze względu na warstwy czy kanały. Na razie nie znalazłem parametrów, które by ten problem rozwiązały. Gdy taki XCF wyświetlę za pomocą display, to jest widziany jako kilka obrazków, przy czym któryś jest jak trzeba.

Spójrzmy na inne cięcie:

 chmury $ OBSKURA=pinh0001099.raw  chmury $ echo ${OBSKURA:4:7}  0001099

Ze zmiennej OBSKURA wyciągnąłem 7-znakowy łańcuch kolejnych jej znaków, zaczynając od pozycji z indeksem 4 (pierwsza ma 0).

  chmury $ echo ${OBSKURA/#pinh/otworek} otworek0001099.raw

Zastąpiłem (jednoznaczny tutaj) wzorzec pinh pasujący do początku (#) zmiennej na otworek. Tu można już (po zajrzeniu do podręcznika) dopatrzyć się pewnej analogii używania z poprzednimi przykładami, więc nie będę powielał opisu.

Taki format numeracji plików z zerami na początku dopełniającymi do stałej długości liczby dobry jest choćby dlatego, że gdy posortujemy je alfabetycznie, to kolejność zgadza się z numeracją. Spójrzmy np. na listę "202.jpg 222.jpg 22.jpg 2.jpg". Jest ona już uporządkowana alfabetycznie, ale chyba nie o taką kolejność chodzi.

Z drugiej strony dopełniony format sprawiał mi problem, gdy tworzyłem pętlę indeksowaną liczbami, które trzeba było uzupełniać z lewej strony pasującą liczbą zer. Budowałem ciągi warunków – demonstracyjną wersję jako funkcję możemy zobaczyć poniżej:

kkk() { i=$1; [ $i -ge 1000 ] && j=$i || \ { [ $i -ge 100 ] && j='0'"$i" || { [ $i -ge 10 ] && j='00'"$i"; } || j='000'"$i"; }; \ echo $j; }

Nie jest ona zbyt użyteczna jej choćby dlatego, że im więcej zer do dopełnienia, tym więcej warunków trzeba pisać… Ale jest jeszcze jeden błąd: wpiszmy kkk 12; kkk 012. Niby te same liczby, ale dostaniemy dwa różne wyniki: 0012 (OK) i 00012. Ale szkoda się nad tym rozwodzić. Przedstawię rozwiązanie prostsze i bardziej uniwersalne.

Załóżmy, że nasze liczby mają być dopełniane do siedmiu znaków. Definiujemy zmienną ZERA='0000000' i zastępujemy ostatnie jej cyfry żądaną liczbą. Niech (jw.) zmienna i będzie wejściową liczbą, a j – uzupełnioną.

j=${ZERA:0:$[${#ZERA}-${#i}]}"$i"

Jak poprzednio w zmiennej OBSKURA, tu pozostawiłem kawałek zmiennej ZERA i dokleiłem na końcu liczbę ze zmiennej i. Jaki kawałek? Ciąg długości $[${#ZERA}-${#i}] począwszy od znaku na miejscu 0. Długość ta jest po prostu liczbą brakujących zer przedstawioną jako różnica długości ciągu ZERA (w BASH ${#ZERA}) i długości liczby i.

Proste, co? A może da się jeszcze prościej?

Można jeszcze zabezpieczyć się przed sytuacją, gdy liczba i będzie o 1 dłuższa niż ZERA, np. dając wtedy j="$i":

ZERA='00' for ((i=1;i<=120;i++)); do 	[ ${#i} -gt ${#ZERA} ] && j="$i" || j=${ZERA:0:$[${#ZERA}-${#i}]}"$i" 	echo "$j".jpg done

Lepiej jest jednak wydłużyć ciąg ZERA, by sortowanie alfabetyczne nie robiło problemów.

Opiszę jeszcze jedną podobną sytuację. Czasem mam okazję korzystać z pewnego aparatu cyfrowego i przychodzi mi robić masowe skalowanie czy inną edycję. Aparat ten numeruje jednak zdjęcia w dziwny sposób: na czwarte od końca miejsce liczby w nazwie pliku zawsze wchodzi zero, wypychając następne krok w lewo. Dla przykładu, mamy 1170998, potem 1170999, ale dalej już 1180000, a nie 1171000. Nie mam pojęcia, na co to zero. Jeśli nasza pętla ma biec między plikami 1170998 a 1180312, to mamy kilka możliwości.

Pętla for ((i=1170998;i<1180312;i++)) przeleci nam niepotrzebnie zakres od 1171000 do 1179999 (9 tysięcy iteracji) i przy okazji za każdym razem będzie próbowała wykonać listę poleceń na nieistniejących plikach. Można dla uniknięcia tego przerobić zakres na i=117998;i<118312, a potem przerobić zmienną do postaci zgodnej z formatem plików.

for ((i=117998;i<118312;i++)) do 	j=${i:0:3}0${i:3:3}; PLIK='pinh'"$j".jpg; [ -f $PLIK ] && identify $PLIK done

Czasem wygodniej jest po prostu przekopiować gdzieś na chwilę wybrane pliki i gwiazdką je potraktować, ale to już poza eksperymentami z bashem.

Poprzednio do operowania na łańcuchach używałem awk, sed lub cut nie zdając sobie sprawy, że niektóre z zadań można załatwić wewnętrznymi poleceniami powłoki. Skrypt nie korzystający z zewnętrznego polecenia jest szybszy i mniej pamięciożerny. Nie wiem czy to ma jakieś znaczenie przy drobiazgach, ale pomyślmy o skryptach na starcie systemu.

Niech Ctrl-S szuka do przodu

Kombinacja Ctrl-R w bashu uruchamia zachętę do wpisania łańcucha, który chcemy wyszukać i od momentu wpisania pierwszego znaku szuka w tył (czyli zaczynając od ostatnio wpisanego polecenia). Jeśli wyszukany wynik to jeszcze nie ten, którego szukamy, to wciskamy Ctrl-R aż do znalezienia lub wyczerpania historii.

Mówiąc dokładniej: bash, dostając Ctrl-R, wykonuje polecenie biblioteki readline o nazwie reverse-search-history.

Jeśli rozpędzimy się w tym wciskaniu i przeleci nam wynik, to warto mieć możliwość cofnięcia zamiast wyjścia (ESC) i szukania od nowa. Do tego w bashu służy polecenie z readline forward-search-history, które, według man bash, domyślnie uruchamiane jest po wciśnięciu Ctrl-S.

Ale na nieszczęście dla tej sprawy, kombinacja Ctrl-S jest przejęta przez terminal. Wciśnięcie jej sprawia, że nie widać wpisywanego tekstu i nie jest on interpretowany aż do momentu wklepania Ctrl-Q. Spróbujmy dla testu dać Ctrl-S, potem na ślepo jakieś nieszkodliwe polecenie potwierdzone enterem i dopiero Ctrl-Q.

Wyboru można dokonać to na różne sposoby. Jeśli do czegoś potrzebna jest nam kombinacja z terminala, możemy przypisać inną dla forward-search-history w readline (jak to zrobić: man bash). Jeśli jednak do niczego nam to, możemy wyłączyć działanie Ctrl-S na bieżącym (pseudo)terminalu poleceniem

stty stop undef </dev/plik_urządzenia_terminala_na_którym_działa_bieżąca_powłoka

To polecenie wypatrzyłem w https://bugs.launchpad.net/ubuntu/+source/gnome-terminal/+bug/48880 . Nawiasem, jest tam też podana kontrolna wersja polecenia stty.

stty -a | fgrep 'stop = '

A ponieważ ten plik urządzenia bywa różny, zajrzałem wpierw do man bash, by zobaczyć czy istnieje jakaś zmienna podająca go wprost, a ponieważ na szybko nie znalazłem, to do man ps. W sumie polecenie może wyglądać tak:

stty stop undef </dev/$(ps --no-headers -o tty $$)

gdzie $$ to PID procesu bieżącej powłoki (man bash: Parametry specjalne).

Teraz Ctrl-S powinno już dać zachętę do szukania w przód. Jeśli jesteśmy na końcu historii, to nic oczywiście nie znajdziemy. Można sobie gdzieś do ~/.bashrc dopisać to polecenie.

Mam nadzieję, że istnieje prostsze rozwiązanie.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Poprzedni post

Age of Empires 3 na Linuksie

Następny post

Linuksowy savoir-vivre, czyli jak najlepiej otrzymać pomoc

Powiązane posty

Start z Javą i Eclipse, część druga.

Poprawne dane

Tak jak obiecałem w pierwszej części artykułu, w części drugiej na początek opiszę jak należy postępować, aby uniemożliwić wprowadzanie nieprawidłowych danych przez użytkownika. Taki mechanizm zabezpieczający jak łatwo można sobie wyobrazić jest konieczny a wręcz niezbędny do poprawnego działania aplikacji. Aby zabezpieczyć program przed wprowadzaniem niewłaściwych danych do obliczeń, posłużymy się mechanizmem wyjątków – i nie tylko!

Więcej...