Nożyczki do tekstu
Po tej lekcji: kursant tnie, skraca, podmienia i zmienia wielkość liter w zmiennych samym Bashem, bez sed, cut i basename, i umie dać zmiennej wartość domyślną.
Wstęp
skrypt, w którym jedna linijka to nazwa=$(echo "$plik" | sed 's/.*\///' | cut -d. -f1)
Widziałeś kiedyś takie coś w skrypcie? Trzy programy, rura, ukośniki jak płot w ogródku. A wszystko po to, żeby z /var/log/nginx/access.log wyciągnąć słowo access.
To jak zamawianie firmy przeprowadzkowej, żeby przesunąć krzesło o metr.
Bash ma w sobie nożyczki. Wbudowane, darmowe, szybkie. Dziś je poznasz i od jutra połowa Twoich rur zniknie. Mówię „połowa”, bo drugą połową zajmiemy się przy sed i awk.
Długość: ile liter siedzi w pudełku
Zmienna to pudełko z napisem. Do tej pory umieliśmy włożyć coś do pudełka i wyjąć całość. Dziś będziemy z tego pudełka wyjmować kawałki.
Najprostsze pytanie: ile tego jest w środku? Stawiasz przed nazwą krzyżyk #.
haslo="tajne123"
echo "${#haslo}"8Po co adminowi długość? Choćby do sprawdzenia hasła, zanim wyślesz je do systemu:
if (( ${#haslo} < 12 )); then
echo "Za krótkie hasło: ${#haslo} znaków"
fiZa krótkie hasło: 8 znakówZauważ: wszystko, co dziś robimy, ma klamry ${ }. Same klamry poznałeś przy zmiennych, ${nazwa}. Teraz wkładamy do nich narzędzia.
Wycinanie: od której litery i ile
Wyobraź sobie taśmę mierniczą. Zaczyna się od zera. Mówisz: „utnij mi od czwartego centymetra, pięć centymetrów”. Tak działa ${zmienna:start:ile}.
data="2026-11-03"
echo "${data:0:4}"
echo "${data:5:2}"
echo "${data:8}"2026
11
03Rok to cztery znaki od zera. Miesiąc to dwa znaki od piątego. Dzień to wszystko od ósmego do końca, bo jak nie podasz długości, Bash tnie do końca.
A od końca? Liczba ujemna. Ale uwaga, tu jest pułapka, do której jeszcze wrócimy. Między dwukropkiem a minusem musi być spacja:
echo "${data: -2}"03Obcinanie z przodu i z tyłu
To jest najważniejsza część lekcji. Wyobraź sobie bagietkę. Możesz uciąć piętkę z lewej, możesz z prawej. I możesz uciąć skromnie, kawałeczek, albo łapczywie, tyle, ile się da.
#obcina z lewej, skromnie, najkrótszy pasujący kawałek,##obcina z lewej, łapczywie, najdłuższy,%obcina z prawej, skromnie,%%obcina z prawej, łapczywie.
A jak zapamiętać, który jest który? Spójrz na klawiaturę. # to Shift+3, % to Shift+5. A dolar, $, to Shift+4, czyli siedzi pośrodku. Krzyżyk stoi przed dolarem, więc tnie z przodu. Procent stoi za dolarem, więc tnie z tyłu. [na ekranie: zbliżenie na klawisze 3, 4, 5]
To, co obcinamy, opisujemy wzorcem, takim samym jak przy plikach: gwiazdka znaczy „cokolwiek”. Patrz:
plik="/var/log/nginx/access.log.2.gz"
echo "${plik##*/}"
echo "${plik%/*}"
echo "${plik%.*}"
echo "${plik%%.*}"
echo "${plik#*/}"access.log.2.gz
/var/log/nginx
/var/log/nginx/access.log.2
/var/log/nginx/access
var/log/nginx/access.log.2.gzPo kolei. ##*/ znaczy: z lewej utnij łapczywie wszystko aż do ukośnika. Łapczywie, czyli do ostatniego ukośnika. Zostaje sama nazwa pliku. To robi komenda basename.
%/* znaczy: z prawej utnij skromnie ukośnik i wszystko za nim. Zostaje folder. To robi dirname.
%.* ucina ostatnie rozszerzenie, .gz. A %%.* ucina od pierwszej kropki, czyli zostaje sam rdzeń, bez .log.2.gz.
I ostatni przykład, #*/, czyli skromnie z lewej. Najkrótsze „cokolwiek, potem ukośnik” to… sam pierwszy ukośnik. Uciął jeden znak. To pokazuje różnicę między skromnym a łapczywym lepiej niż tysiąc slajdów.
Zastosowanie z życia admina, nazwy serwerów:
host="web01.prod.firma.pl"
echo "${host%%.*}"
echo "${host#*.}"web01
prod.firma.plKrótka nazwa i domena, jednym ruchem nożyczek.
Zamiana: znajdź i zamień
To jest Ctrl+H z Worda, tylko w zmiennej. ${zmienna/co/na_co} zamienia pierwsze wystąpienie, a z dwoma ukośnikami, ${zmienna//co/na_co}, wszystkie.
w="web01 padł, web01 nie odpowiada"
echo "${w/web01/web02}"
echo "${w//web01/web02}"
echo "${w// /_}"web02 padł, web01 nie odpowiada
web02 padł, web02 nie odpowiada
web01_padł,_web01_nie_odpowiadaTen trzeci to klasyk: spacje na podkreślniki, zanim zrobisz z tekstu nazwę pliku. A jak nie podasz, na co zamienić, Bash po prostu usuwa:
echo "${w//web01}" padł, nie odpowiadaSą jeszcze dwa warianty z kotwicą. /# zamienia tylko, jeśli tekst pasuje na samym początku, a /% tylko na samym końcu:
plik="raport.txt"
echo "${plik/#raport/RAPORT}"
echo "${plik/%txt/csv}"RAPORT.txt
raport.csvA teraz rzecz, za którą mi podziękujesz przy pierwszym zrzucie zdjęć z telefonu. Zmiana rozszerzenia w całym folderze:
for f in *.jpeg; do
mv -- "$f" "${f%.jpeg}.jpg"
done
lskot.jpg
moja plaża.jpg
wakacje.jpgPętla to taśma przy kasie, każdy plik przejeżdża po kolei. ${f%.jpeg} odcina stare rozszerzenie z tyłu, a .jpg dokleja nowe. I działa nawet z plażą ze spacją w nazwie, bo wszystko jest w kopertach, czyli w cudzysłowach. [dla zaawansowanych: tu celowo %, a nie /. ${f/.jpeg/.jpg} zamieniłby pierwsze .jpeg w nazwie, a plik stary.jpeg.kopia.jpeg dostałby nową nazwę stary.jpg.kopia.jpeg. Kotwica na końcu, czyli %, nie ma tego problemu.]
Wielkie i małe litery
Znasz to: pytasz użytkownika „tak czy nie?”, a on odpowiada „TAK”, „Tak”, „tAk”. Zamiast sprawdzać wszystkie kombinacje, sprowadzasz odpowiedź do małych liter.
${v^^}wszystko wielkimi,${v,,}wszystko małymi,${v^}tylko pierwsza litera wielka,${v,}tylko pierwsza mała.
Daszek do góry jak strzałka w górę, przecinek w dół jak ogonek. Tak to zapamiętałem i działa.
#!/bin/bash
read -r -p "Restartować serwer? [t/n] " odp
if [[ ${odp,,} == t ]]; then
echo "Restartuję..."
else
echo "Nic nie robię."
fiWpisuję T, wielkie:
Restartować serwer? [t/n] T
Restartuję...I nazwa hosta do nagłówka raportu:
echo "${host^^}"WEB01.PROD.FIRMA.PL[macOS] Uwaga dla użytkowników Maca: zmiana wielkości liter weszła do Basha w wersji 4.0, w 2009 roku. Systemowy Bash na Macu to 3.2. Dostaniesz tam błąd bad substitution. Rozwiązanie to samo co zawsze: nowy Bash z Homebrew albo Linux w maszynie wirtualnej. Cała reszta z tej lekcji działa i na starym.
Wartości domyślne: a jak pudełko jest puste?
Pamiętasz funkcję kopia wywołaną bez argumentu? To jest ten problem. Pudełko puste, a skrypt i tak sięga do środka.
Wyobraź sobie bar mleczny. Pytasz: „Co dziś do picia?”. Pani mówi: „Jak pan nic nie wybierze, będzie kompot”. To jest ${v:-domyślnie}.
#!/bin/bash
kto=${1:-nieznajomy}
echo "Cześć, ${kto^}!"./witaj.sh
./witaj.sh elaCześć, Nieznajomy!
Cześć, Ela!Całe rodzeństwo wygląda tak:
${v:-kompot}— jak puste, podaj kompot, ale do pudełka nic nie wkładaj,${v:=kompot}— jak puste, podaj kompot i od razu włóż go do pudełka na stałe,${v:?komunikat}— jak puste, krzyknij komunikat i przerwij skrypt,${v:+coś}— odwrotność: jak NIE jest puste, podaj „coś”, a jak puste, nic.
W skryptach admina wygląda to tak:
katalog=${1:-/var/log}
echo "Sprawdzam: $katalog"
: "${BACKUP_DIR:=/srv/backup}"
echo "Kopie lecą do: $BACKUP_DIR"Sprawdzam: /var/log
Kopie lecą do: /srv/backupA jak ktoś poda swoje?
BACKUP_DIR=/mnt/nas ./skrypt.sh /etcSprawdzam: /etc
Kopie lecą do: /mnt/nasTen dwukropek na początku linijki to komenda, która nic nie robi. Serio, nazywa się „null”. Potrzebujemy jej tylko po to, żeby Bash rozwinął ${BACKUP_DIR:=...} i przy okazji wpisał wartość do pudełka.
A :? to strażnik przy wejściu:
#!/bin/bash
sciezka=${1:?Użycie: rozbierz.sh ŚCIEŻKA}rozbierz.sh: line 2: 1: Użycie: rozbierz.sh ŚCIEŻKASkrypt kończy się kodem 1 i nie robi ani kroku dalej. Jedna linijka zamiast całego if-a.
Wszystko razem: skrypt rozbierz.sh
#!/bin/bash
# rozbierz.sh – rozkłada ścieżkę pliku na części
sciezka=${1:?Użycie: rozbierz.sh ŚCIEŻKA}
folder=${sciezka%/*}
nazwa=${sciezka##*/}
rdzen=${nazwa%%.*}
rozszerzenie=${nazwa#*.}
echo "Folder: $folder"
echo "Nazwa: $nazwa"
echo "Rdzeń: $rdzen"
echo "Rozszerzenie: $rozszerzenie"
echo "Kopia: ${folder}/${rdzen}_KOPIA.${rozszerzenie}"
echo "Wielkimi: ${nazwa^^}"./rozbierz.sh /var/log/nginx/access.log.2.gzFolder: /var/log/nginx
Nazwa: access.log.2.gz
Rdzeń: access
Rozszerzenie: log.2.gz
Kopia: /var/log/nginx/access_KOPIA.log.2.gz
Wielkimi: ACCESS.LOG.2.GZZero zewnętrznych programów. ShellCheck czysty. A teraz test, który oblewa połowa skryptów w internecie:
./rozbierz.sh raport.txtFolder: raport.txt
Nazwa: raport.txt
...
Kopia: raport.txt/raport_KOPIA.txtNie ma ukośnika, więc %/* nie miał czego uciąć i oddał całość. Nożyczki są szybkie, ale głupie: tną dokładnie tam, gdzie każesz. Jeśli plik może leżeć „tu, w bieżącym folderze”, musisz to sprawdzić sam, na przykład bramkarzem if [[ $sciezka == */* ]]. Albo w takim jednym miejscu świadomie użyć dirname, które wie, że wtedy folder to kropka.
Jak to działa pod spodem
Wszystko z tej lekcji robi sam urzędnik, czyli Bash. Nie woła nikogo z zaplecza. A basename, sed czy cut to osobne programy. Każde ich wywołanie w $( ) to nowy proces: Bash się klonuje, klon zamienia się w program, program startuje, robi swoje i umiera. Jak zatrudnianie kucharza do pokrojenia jednej cebuli.
Zmierzyłem to. Tysiąc razy wyciągam nazwę pliku:
time (for i in {1..1000}; do x=$(basename "$plik"); done)
time (for i in {1..1000}; do x=${plik##*/}; done)real 0m1.716s
real 0m0.003sPółtorej sekundy kontra trzy milisekundy. Kilkaset razy szybciej. Przy jednym pliku nie ma różnicy, przy pętli po stu tysiącach linii logu to różnica między „chwilę” a „idę na kawę”.
Druga rzecz: wzorce w nożyczkach to wzorce plikowe, te same co w ls *.txt. Gwiazdka, znak zapytania, nawiasy kwadratowe. To NIE są wyrażenia regularne. Te poznasz za cztery lekcje i wtedy zobaczysz, że to zupełnie inny język.
Trzecia: Bash liczy znaki według ustawień językowych, czyli locale. W C.UTF-8 słowo „Łódź” ma 4 znaki. Uruchomione z LC_ALL=C ma 7, bo Bash liczy wtedy bajty, a polskie litery zajmują po dwa. I ^^ nie umie wtedy zrobić wielkiego „ó”.
C.UTF-8: ŁÓDŹ 4
C: ŁóDź 7Na serwerze z dziwnie ustawionym locale polskie znaki potrafią się rozjechać właśnie tak. [dla zaawansowanych: ShellCheck zna te nożyczki. Jak napiszesz $(echo "$v" | sed "s/a/b/"), dostaniesz ostrzeżenie SC2001: „See if you can use ${variable//search/replace} instead”.]
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
${#v} | # przed nazwą | długość tekstu w znakach |
${v:2:3} | :start:ile | wycina 3 znaki od pozycji 2 (liczymy od 0) |
${v: -2} | spacja + minus | ostatnie 2 znaki |
${v#wzór} | # | utnij z lewej najkrótszy pasujący kawałek |
${v##wzór} | ## | utnij z lewej najdłuższy (np. ##*/ = nazwa pliku) |
${v%wzór} | % | utnij z prawej najkrótszy (np. %.* = bez rozszerzenia) |
${v%%wzór} | %% | utnij z prawej najdłuższy (np. %%.* = rdzeń nazwy) |
${v/a/b} | / | zamień pierwsze a na b |
${v//a/b} | // | zamień wszystkie |
${v/#a/b} | /# | zamień tylko na początku |
${v/%a/b} | /% | zamień tylko na końcu |
${v^^} / ${v,,} | ^^ / ,, | wszystko wielkimi / małymi (Bash 4+) |
${v^} / ${v,} | ^ / , | tylko pierwsza litera |
${v:-x} | :- | jak puste, użyj x |
${v:=x} | := | jak puste, użyj x i zapisz do zmiennej |
${v:?tekst} | :? | jak puste, wypisz błąd i przerwij |
${v:+x} | :+ | jak NIE puste, użyj x |
⚠ Typowy błąd
Chcesz dwa ostatnie znaki daty i piszesz:
data="2026-11-03"
echo "${data:-2}"2026-11-03Żadnego błędu. Po prostu cała data. Dlaczego? Bo :- to dla Basha wartość domyślna z dzisiejszej sekcji o kompocie. Przeczytał to jako „jak data jest pusta, daj 2”. A nie jest pusta, więc oddał całość. Brak błędu to tu najgorsze, bo skrypt jedzie dalej z niewłaściwą wartością.
Naprawa: spacja albo nawias.
echo "${data: -2}"
echo "${data:(-2)}"03
03Drugi częsty błąd to pomylenie skromnego z łapczywym. Chcesz zdjąć rozszerzenie z access.log.2.gz, piszesz %%.* i zostaje Ci access, a chciałeś access.log.2. Zasada: jedna litera, # albo %, tnie mało. Podwójna tnie dużo.
✎ Zadanie
- Masz zmienną:
url="https://www.chmuroholik.pl/blog/odcinek-41.html"Samymi nożyczkami, bez sed, cut i basename, wyciągnij z niej: protokół (https), nazwę hosta, wypisaną WIELKIMI literami, i nazwę strony bez .html.
- Napisz skrypt
witaj.sh, który wita osobę podaną w pierwszym argumencie, z wielkiej litery. Bez argumentu ma witać „Nieznajomego”.
Rozwiązanie znajdziesz pod spodem.
Pokaż rozwiązanie
Pierwsza część: rozebrać adres strony samymi nożyczkami.
#!/bin/bash
url="https://www.chmuroholik.pl/blog/odcinek-41.html"
protokol=${url%%:*}
reszta=${url#*://}
host=${reszta%%/*}
nazwa=${url##*/}
nazwa=${nazwa%.html}
echo "Protokół: $protokol"
echo "Host: ${host^^}"
echo "Strona: $nazwa"Protokół: https
Host: WWW.CHMUROHOLIK.PL
Strona: odcinek-41Protokół to wszystko przed pierwszym dwukropkiem, więc z prawej tniemy łapczywie: %%:*. Host jest trudniejszy, bo siedzi w środku. Dlatego dwa cięcia: najpierw z lewej zdejmujemy https://, potem z prawej wszystko od pierwszego ukośnika. Kto próbował zrobić to jednym cięciem, ten wie, dlaczego chirurdzy mają więcej niż jeden skalpel.
Druga część, witaj.sh:
#!/bin/bash
kto=${1:-nieznajomy}
echo "Cześć, ${kto^}!"$ ./witaj.sh
Cześć, Nieznajomy!
$ ./witaj.sh ela
Cześć, Ela!Wartość domyślna plus wielka pierwsza litera. Dwie linijki. Kiedyś na to był if, tr i trzy minuty z dokumentacją.
Co dalej
Do tej pory w jednym pudełku trzymaliśmy jedną rzecz. W następnej lekcji dostajemy szufladę z przegródkami: tablice. Listę serwerów, listę plików, całe pliki wczytane linijka po linijce przez mapfile.