Spacje w nazwach plików
Po tej lekcji: kursant pisze skrypty i pętle, które nie wykładają się na plikach ze spacją, myślnikiem na początku ani nawet znakiem nowej linii w nazwie, i wie, kiedy użyć cudzysłowu, IFS, find -print0 i nullglob.
Wstęp
Skrypt backupu działał rok. Codziennie o drugiej w nocy, zero błędów, admin spał spokojnie. Aż księgowa założyła folder Faktury 2026. Ze spacją. Bo czemu nie, przecież w Windowsie wszyscy tak robią.
Skrypt zobaczył spację i uznał, że to dwa foldery: Faktury i 2026. Żadnego nie znalazł, wypisał błąd do logu, którego nikt nie czytał, i poszedł dalej. Przez trzy miesiące.
Spacja w nazwie pliku to dla Basha jak „Jan Maria Kowalski” w formularzu z jednym polem na imię. Dziś nauczysz się pisać skrypty, które nie robią z Jana Marii trzech różnych ludzi.
Jak Bash tnie słowa
Przypomnij sobie urzędnika z okienka. Dostaje od ciebie kartkę z poleceniem i zanim cokolwiek zrobi, tnie ją nożyczkami na słowa. Tnie w każdym miejscu, gdzie jest spacja, tabulator albo nowa linia. Pierwsze słowo to komenda, reszta to argumenty.
To cięcie nazywa się dzielenie na słowa, po angielsku word splitting. I tu jest haczyk: urzędnik tnie nie tylko to, co wpisałeś, ale też to, co wyszło ze zmiennej albo z $( ), jeśli nie jest w cudzysłowie.
Mała pokazówka. Folder z dwoma plikami:
touch "raport 2026.txt" notatki.txt
lsnotatki.txt
raport 2026.txtKlasyk z internetu, pętla po wyniku ls:
for f in $(ls); do echo "[$f]"; done[notatki.txt]
[raport]
[2026.txt]Nawiasy kwadratowe dodałem, żeby było widać, gdzie każde „słowo” się zaczyna i kończy. Dwa pliki, trzy kawałki. raport 2026.txt został pocięty na pół, bo wynik $(ls) to po prostu tekst, a tekst urzędnik tnie.
A teraz to samo, tylko gwiazdką:
for f in *; do echo "[$f]"; done[notatki.txt]
[raport 2026.txt]Idealnie. Gwiazdka nie produkuje tekstu do pocięcia, tylko gotową listę nazw. Każda nazwa to osobna, nienaruszalna pozycja. Zapamiętaj: po plikach chodzisz gwiazdką, nigdy po wyniku ls. ls jest dla ludzi, nie dla skryptów.
Cudzysłów, czyli koperta
Kiedy to cięcie robi prawdziwą krzywdę? Kiedy zmienna z nazwą pliku trafia do komendy bez cudzysłowu.
dwa pliki: stare ważne.txt i ważne.txt
Mamy dwa pliki: stare ważne.txt, który chcemy usunąć, i ważne.txt, którego za nic nie chcemy stracić.
f="stare ważne.txt"
rm $f
ls -1rm: cannot remove 'stare': No such file or directory
stare ważne.txtPrzeczytaj to jeszcze raz. Plik, który miał zniknąć, dalej jest. A ważne.txt zniknął. Urzędnik pociął stare ważne.txt na stare i ważne.txt, dostał dwa polecenia usunięcia, pierwszego pliku nie znalazł, a drugi usunął bez mrugnięcia okiem. Błąd na ekranie dotyczy tego, co się NIE stało. O tym, co się stało, nikt cię nie poinformował.
Lekarstwo znasz z lekcji o zmiennych: cudzysłów, czyli koperta. Wszystko, co jest w kopercie, urzędnik bierze w całości, bez nożyczek.
rm "$f"ShellCheck łapie to od ręki:
In zly.sh line 3:
rm $f
^-- SC2086 (info): Double quote to prevent globbing and word splitting.
Did you mean:
rm "$f"Zwróć uwagę na słowo „globbing”. Zmienna bez cudzysłowu ma dwa problemy, nie jeden. Najpierw jest cięta na słowa, a potem każde słowo jest jeszcze sprawdzane, czy nie jest wzorcem z gwiazdką. Jeśli ktoś nazwie plik *, a ty zrobisz rm $f… Tak, Linux pozwala nazwać plik gwiazdką. Linux pozwala na bardzo dużo rzeczy.
Zasada na całe życie: każde $zmienna, $(komenda), $1 i ${tablica[@]} w cudzysłowie, chyba że masz konkretny powód. Dla tablic forma "${pliki[@]}" daje każdy element osobno i nienaruszony, a to dokładnie to, czego chcesz. To samo, co "$@" z lekcji o argumentach.
IFS, czyli którymi nożyczkami
Gdzie Bash trzyma informację, w których miejscach ciąć? W zmiennej o nazwie IFS, od Internal Field Separator, wewnętrzny separator pól. Zajrzyjmy:
printf '%q\n' "$IFS"$' \t\n'Spacja, tabulator, nowa linia. Trzy ostrza nożyczek. printf '%q' pokazuje niewidzialne znaki tak, żeby było je widać.
IFS przydaje się sam w sobie, najczęściej z read. Pamiętasz /etc/passwd? Pola rozdzielone dwukropkiem. Podmieniasz nożyczki tylko na czas jednej komendy:
IFS=: read -r user _ uid _ _ home shell <<< "root:x:0:0:root:/root:/bin/bash"
echo "user=$user uid=$uid home=$home shell=$shell"user=root uid=0 home=/root shell=/bin/bash_ to umowna nazwa na „to pole mnie nie obchodzi”. A IFS=: przed read działa tylko dla tego jednego read. Po nim IFS wraca do normy.
Druga sztuczka: IFS= pusty. Wtedy read nie obcina spacji na początku i końcu linii:
linia=" Jan Kowalski "
read -r a <<< "$linia"; echo "[$a]"
IFS= read -r a <<< "$linia"; echo "[$a]"[Jan Kowalski]
[ Jan Kowalski ]Dlatego od lekcji o pętlach piszę zawsze while IFS= read -r linia. IFS= zostawia spacje w spokoju, -r zostawia w spokoju backslashe. Czytasz linijkę dokładnie taką, jaka jest.
Popularny trik z internetu to IFS=$'\n' na górze skryptu, żeby ciąć tylko na nowej linii. Działa… połowicznie. Po pierwsze, psuje każdą inną pętlę w skrypcie, która liczy na cięcie po spacji: for n in $dane z dane="1 2 3" nagle daje jedno słowo 1 2 3. Po drugie, nie ratuje przed plikiem z nową linią w nazwie. Bo tak, to też jest dozwolone.
Najgorsze nazwy świata
folder zdjecia, a w nim pliki, które wyglądają jak sabotaż
Zróbmy folder testowy z nazwami, które spotkasz prędzej czy później. Zrób go u siebie, przyda się w zadaniu:
mkdir -p "zdjecia/wakacje 2026"
cd zdjecia
touch zwykle.jpg "wakacje 2026/plaża o zachodzie.jpg"
touch $'nowa\nlinia.jpg'
touch -- -dziwne.jpg
ls-dziwne.jpg
'nowa'$'\n''linia.jpg'
'wakacje 2026'
zwykle.jpg$'nowa\nlinia.jpg' to sposób Basha na wpisanie prawdziwego znaku nowej linii. ls w terminalu pokazuje taką nazwę w cudzysłowach, żebyś wiedział, że coś tu jest nie tak. A -- przy touch to znak „koniec opcji, dalej są już tylko nazwy”. Bez tego touch uznałby -dziwne.jpg za zestaw opcji i odmówił współpracy.
Myślnik na początku
Zanim przejdziemy do pętli, ta jedna jest zdradliwa. W folderze leżą a.txt, b.txt i plik o nazwie -l:
ls *-rw-r--r-- 1 root root 2 Nov 3 22:09 a.txt
-rw-r--r-- 1 root root 0 Nov 3 22:09 b.txtGwiazdka rozwinęła się do -l a.txt b.txt, a ls przeczytał -l jako opcję „długi format”. Plik -l zniknął z listy, za to dostaliśmy szczegóły. Tu niewinnie. Teraz wyobraź sobie plik o nazwie -rf i komendę rm *. Nie wyobrażaj sobie zbyt długo.
Dwa lekarstwa: -- albo ./ przed gwiazdką.
ls -- *
ls ./*-l
a.txt
b.txt
./-l
./a.txt
./b.txt./* jest jeszcze lepsze, bo działa z każdym programem, także z takim, który nie zna --. Nazwa zaczyna się od kropki, a nie od myślnika, więc żaden program nie weźmie jej za opcję.
Bezpieczne pętle po plikach
Teraz zdjęcia, także w podfolderach. Pierwsze, co przychodzi do głowy:
for f in $(find . -name '*.jpg'); do echo "[$f]"; done[./zwykle.jpg]
[./nowa]
[linia.jpg]
[./-dziwne.jpg]
[./wakacje]
[2026/plaża]
[o]
[zachodzie.jpg]Cztery pliki, osiem kawałków. Plaża o zachodzie słońca rozpadła się na cztery części jak źle zapakowany talerz.
Poprawny sposób składa się z trzech części, które znasz osobno:
while IFS= read -r -d '' f; do
printf '[%s]\n' "$f"
done < <(find . -name '*.jpg' -print0)[./zwykle.jpg]
[./nowa
linia.jpg]
[./-dziwne.jpg]
[./wakacje 2026/plaża o zachodzie.jpg]Cztery pliki, cztery wyniki, nawet ten z nową linią. Rozbierzmy to:
find -print0kończy każdą nazwę bajtem zerowym zamiast nowej linii. Jedynym znakiem, którego w nazwie być nie może.read -d ''czyta do bajtu zerowego, a nie do końca linii.IFS=i-rzostawiają nazwę nietkniętą.< <( )to podstawianie procesu z lekcji 39. I to nie jest ozdobnik.
Bo zobacz, co się dzieje, kiedy ten sam find podasz rurą i chcesz policzyć pliki:
n=0
find . -name '*.jpg' -print0 | while IFS= read -r -d '' f; do n=$((n+1)); done
echo "znaleziono: $n"znaleziono: 0Zero. Pętla liczyła poprawnie, ale za rurą działa w osobnej podpowłoce. To jak liczenie paczek na taśmie w sąsiedniej hali: naliczyłeś cztery, wychodzisz, a tablica w twojej hali dalej pokazuje zero. Z < <(find …) pętla zostaje w głównej hali i licznik przeżywa.
Są jeszcze dwa bezpieczne sposoby, gdy nie potrzebujesz pętli:
find . -name '*.jpg' -exec ls -l {} +
find . -name '*.jpg' -print0 | xargs -0 ls -l-rw-r--r-- 1 root root 0 Nov 3 22:09 ./-dziwne.jpg
-rw-r--r-- 1 root root 0 Nov 3 22:09 ./nowa
linia.jpg
-rw-r--r-- 1 root root 300 Nov 3 22:09 ./wakacje 2026/plaża o zachodzie.jpg
-rw-r--r-- 1 root root 1200 Nov 3 22:09 ./zwykle.jpgA jeśli chcesz mieć wszystko w tablicy:
mapfile -d '' -t pliki < <(find . -name '*.jpg' -print0)
echo "${#pliki[@]}"4macOS: systemowy Bash 3.2 nie ma mapfile. Pętla z read -r -d '' działa wszędzie.
nullglob, czyli gdy nie ma czego szukać
Ostatnia pułapka. Folder, w którym nie ma żadnego .png:
for f in *.png; do echo "przetwarzam: [$f]"; doneprzetwarzam: [*.png]Kiedy wzorzec niczego nie znajdzie, Bash zostawia go… dosłownie. Skrypt próbuje przetworzyć plik o nazwie *.png. Jak kelner, który na pytanie „co jest na deser?” przynosi kartkę z napisem „deser”.
Włączasz opcję nullglob i pusty wzorzec znika:
shopt -s nullglob
for f in *.png; do echo "przetwarzam: [$f]"; done
pliki=(*.png)
echo "liczba png: ${#pliki[@]}"liczba png: 0Pętla nie wykonała się ani razu i dobrze. A tablica z gwiazdki to najprostszy sposób, żeby sprawdzić, czy jakieś pliki w ogóle są.
Ale uwaga, nullglob też ma ciemną stronę:
shopt -s nullglob
ls *.png-dziwne.jpg
nowa
linia.jpg
wakacje 2026
zwykle.jpgWzorzec zniknął, ls dostał zero argumentów, więc wypisał cały folder. Z ls to śmieszne. Z rm -- *.bak… już mniej, bo rm bez argumentów tylko marudzi, ale inne programy bez argumentów potrafią wziąć „wszystko”. Dlatego nullglob włączaj świadomie, najlepiej tylko na czas pętli, a potem shopt -u nullglob.
Jest też failglob: pusty wzorzec to błąd i komenda się nie wykona.
glob.sh: line 16: no match: *.pngJak to działa pod spodem
Bash rozbiera każdą linijkę w ustalonej kolejności. Najpierw rozwija nawiasy klamrowe, tyldę, zmienne, $( ) i arytmetykę. Potem tnie wyniki tych rozwinięć na słowa według IFS, ale tylko te, które nie są w cudzysłowie. Potem rozwija gwiazdki i znaki zapytania. Na końcu usuwa cudzysłowy i odpala program.
Z tej kolejności wynika wszystko, co dziś widziałeś. Tekst wpisany ręcznie jest cięty przy wczytywaniu linijki, ale wynik zmiennej i $( ) jest cięty jeszcze raz, chyba że siedzi w kopercie. Gwiazdka jest rozwijana po cięciu, więc nazwy z gwiazdki nie są już cięte. Dlatego for f in * jest bezpieczne, a for f in $(ls) nie.
I jeszcze jedno: sam program, rm czy ls, nigdy nie widzi ani cudzysłowów, ani gwiazdki. Dostaje od jądra gotową listę argumentów, każdy jako osobny napis zakończony bajtem zerowym. O wszystkim decyduje urzędnik przy okienku, zanim przekaże sprawę dalej.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
"$zmienna" | — | koperta: bez cięcia na słowa i bez rozwijania gwiazdek |
"${tab[@]}" | — | każdy element tablicy osobno i w całości |
IFS=: read -r a b | IFS= | nożyczki tylko dla tej jednej komendy |
read | -r | nie rusza backslashy |
read | -d '' | czyta do bajtu zerowego zamiast do nowej linii |
find | -print0 | nazwy rozdzielone bajtem zerowym |
find | -exec … {} + | podaje wiele plików naraz, bezpiecznie |
xargs | -0 | czyta nazwy rozdzielone bajtem zerowym |
mapfile | -d '' -t | wczytuje nazwy z -print0 do tablicy |
rm, ls, cp | -- | koniec opcji, dalej tylko nazwy |
shopt | -s nullglob | pusty wzorzec znika |
shopt | -s failglob | pusty wzorzec to błąd |
shopt | -s dotglob | * łapie też pliki z kropką |
printf | %q | pokazuje niewidzialne znaki |
⚠ Typowy błąd
Kopiowanie w pętli bez cudzysłowu:
for f in *.txt; do cp $f /backup/; donecp: cannot stat 'raport': No such file or directory
cp: cannot stat '2026.txt': No such file or directoryPętla jest dobra, bo idzie gwiazdką. Ale w środku $f bez koperty i urzędnik znowu bierze nożyczki. Komunikat cannot stat 'raport' z kawałkiem nazwy to zawsze ten sam objaw: gdzieś brakuje cudzysłowu.
Naprawa: cp -- "$f" /backup/. I od razu przepuść skrypt przez ShellCheck, bo jeśli brakuje cudzysłowu w jednym miejscu, to zwykle brakuje go w pięciu.
✎ Zadanie
Zrób u siebie folder zdjecia z najgorszymi nazwami świata, komendami z lekcji. Wrzuć do dwóch plików trochę danych, na przykład head -c 1200 /dev/urandom > zdjecia/zwykle.jpg. Potem napisz skrypt policz_jpg.sh, który dostaje folder jako argument (domyślnie bieżący), znajduje wszystkie pliki .jpg także w podfolderach, wypisuje każdy w nawiasach kwadratowych z rozmiarem w bajtach, a na końcu podaje liczbę plików i łączny rozmiar. Nie może się pomylić na żadnej nazwie. ShellCheck ma milczeć.
Pokaż rozwiązanie
#!/bin/bash
# policz_jpg.sh FOLDER — liczy pliki .jpg, odporny na dziwne nazwy
folder=${1:-.}
licznik=0
suma=0
while IFS= read -r -d '' plik; do
rozmiar=$(stat -c %s -- "$plik")
printf '[%s] %s B\n' "$plik" "$rozmiar"
licznik=$((licznik + 1))
suma=$((suma + rozmiar))
done < <(find "$folder" -type f -iname '*.jpg' -print0)
echo "Plików: $licznik, razem: $suma B"[zdjecia/zwykle.jpg] 1200 B
[zdjecia/nowa
linia.jpg] 0 B
[zdjecia/-dziwne.jpg] 0 B
[zdjecia/wakacje 2026/plaża o zachodzie.jpg] 300 B
Plików: 4, razem: 1500 BCztery pliki, żaden nie pocięty, ShellCheck milczy. -print0 z read -d '' niesie nazwy, < <( ) ratuje liczniki przed podpowłoką, ${1:-.} daje bieżący folder, gdy nie podasz żadnego. -iname łapie też ZDJECIE.JPG z telefonu, który krzyczy wielkimi literami. stat -c %s podaje rozmiar w bajtach, a -- chroni przed -dziwne.jpg, chociaż tu ścieżka i tak zaczyna się od nazwy folderu.
macOS: stat jest z rodziny BSD i rozmiar daje stat -f %z.
Jeśli twoja wersja wypisała Plików: 0, to pewnie dałeś find … | while. Licznik został w sąsiedniej hali.
Co dalej
Twój skrypt już nie boi się spacji. Ale co zrobi, jeśli w połowie jedna komenda się wysypie? Domyślnie: nic, pojedzie dalej, jakby nigdy nic. W następnej lekcji włączymy tryb „zatrzymaj się przy błędzie”, czyli set -e, set -u i pipefail, i zobaczymy na żywo, gdzie każdy z nich potrafi cię oszukać.