Przejdź do treści
Lekcja 41 z 117

Kurs › Moduł 4 › Lekcja 41

Nożyczki do tekstu

⏱ ok. 10 min czytania· moduł 4: Napisy, listy i przetwarzanie danych

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}"
8

Po 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"
fi
Za krótkie hasło: 8 znaków

Zauważ: 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
03

Rok 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}"
03

Obcinanie 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.gz

Po 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.pl

Kró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_odpowiada

Ten 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 odpowiada

Są 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.csv

A 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
ls
kot.jpg
moja plaża.jpg
wakacje.jpg

Pę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ę."
fi

Wpisuję 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 ela
Cześć, 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/backup

A jak ktoś poda swoje?

BACKUP_DIR=/mnt/nas ./skrypt.sh /etc
Sprawdzam: /etc
Kopie lecą do: /mnt/nas

Ten 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ŻKA

Skrypt 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.gz
Folder:       /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.GZ

Zero zewnętrznych programów. ShellCheck czysty. A teraz test, który oblewa połowa skryptów w internecie:

./rozbierz.sh raport.txt
Folder:       raport.txt
Nazwa:        raport.txt
...
Kopia:        raport.txt/raport_KOPIA.txt

Nie 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.003s

Pół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ź 7

Na 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

KomendaFlagaCo robi
${#v}# przed nazwądługość tekstu w znakach
${v:2:3}:start:ilewycina 3 znaki od pozycji 2 (liczymy od 0)
${v: -2}spacja + minusostatnie 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
03

Drugi 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

  1. 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.

  1. 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-41

Protokół 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.