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.