Wynik komendy w zmiennej
Po tej lekcji: kursant zapisuje wynik komendy do zmiennej przez $( ), rozpoznaje stare odwrotne apostrofy i podstawia wynik komendy jako plik przez <( ), na przykład do porównania dwóch list jedną komendą.
Wstęp
skrypt kopii zapasowej, w którym nazwa pliku to kopia.tar.gz, a obok folder z jednym jedynym plikiem kopia.tar.gz
Ktoś napisał skrypt do kopii zapasowych. Codziennie o północy pakuje dane do kopia.tar.gz. Codziennie. Do tego samego pliku. Czyli codziennie nadpisuje wczorajszą kopię. Kiedy w czwartek ktoś zauważy, że we wtorek skasował ważny plik, okaże się, że kopia z wtorku nie istnieje. Jest tylko dzisiejsza, już bez tego pliku.
Potrzebujesz w nazwie dzisiejszej daty. Datę zna komenda date. Pytanie: jak wynik komendy wsadzić do nazwy pliku albo do zmiennej? Dziś się dowiesz i po tej lekcji skrypty zaczną rozmawiać z systemem, zamiast tylko mówić do niego.
$( ), czyli prosimy o odpowiedź na piśmie
Wyobraź sobie, że urzędnik w okienku wypełnia formularz i w jednej rubryce potrzebuje informacji z innego działu. Wysyła gońca: „idź do działu dat i przynieś dzisiejszą datę”. Goniec wraca z karteczką, urzędnik przepisuje to, co na niej jest, w rubrykę i idzie dalej.
Tym gońcem jest $( ). W środku piszesz komendę, a całość zamienia się na to, co ta komenda wypisała.
dzis=$(date +%F)
echo "Kopia z dnia $dzis"Kopia z dnia 2026-11-03date +%F wypisuje datę w formacie rok-miesiąc-dzień. $( ) łapie ten wynik, zamiast puścić go na ekran, i wkłada do pudełka dzis. Teraz skrypt z haka naprawiamy jedną zmianą:
tar czf "kopia-$(date +%F).tar.gz" /daneKażdy dzień to nowy plik. kopia-2026-11-03.tar.gz, kopia-2026-11-04.tar.gz i tak dalej. Format rok-miesiąc-dzień ma też tę zaletę, że sortuje się alfabetycznie w dobrej kolejności. Format dzień-miesiąc-rok tego nie umie i po pół roku masz w folderze bałagan jak w szufladzie na kable.
Możesz też wstawić $( ) prosto w tekst, bez zmiennej:
echo "Jestem w folderze: $(basename "$(pwd)")"Jestem w folderze: e39$( ) można zagnieżdżać. Wewnętrzny goniec przynosi pełną ścieżkę, zewnętrzny wycina z niej ostatni kawałek. Zauważ też cudzysłowy w środku: wewnątrz $( ) zaczyna się nowy, czysty świat i koperty z zewnątrz się z nimi nie gryzą.
Dwa przykłady, które zobaczysz w każdym skrypcie admina:
konta=$(wc -l < /etc/passwd)
echo "Kont w systemie: $konta"
komputer=$(hostname)
echo "Działam na: $komputer"Kont w systemie: 90
Działam na: vmCo goniec przynosi, a czego nie
Goniec ma swoje zwyczaje i dobrze je znać.
Po pierwsze, obcina puste linijki z końca wyniku. Każda komenda kończy wynik znakiem nowej linii. Gdyby goniec go nie obciął, każda data miałaby doczepiony Enter i nazwy plików byłyby połamane. Dzięki temu kopia-$(date +%F).tar.gz wychodzi w jednej linijce.
Po drugie, wynik bez cudzysłowu zostaje pocięty na słowa. Porównaj:
lista=$(printf 'a\nb\nc\n')
echo $lista
echo "$lista"a b c
a
b
cBez koperty trzy linijki skleiły się w jedną. W kopercie zostały, jak były. Zasada, która wraca w tym kursie jak bumerang: zmienne i $( ) zawsze w cudzysłowie, chyba że dokładnie wiesz, dlaczego nie.
Po trzecie, goniec przynosi tylko to, co komenda wypisała pierwszym głośnikiem. Błędy z drugiego głośnika dalej lecą na ekran. Jeśli chcesz je też złapać: wynik=$(komenda 2>&1).
Po czwarte, kod wyjścia komendy nie ginie. Zaraz po przypisaniu leży w $?:
wynik=$(grep -c root /etc/passwd)
echo "kod: $? wynik: $wynik"
wynik=$(grep -c nieistnieje /etc/passwd)
echo "kod: $? wynik: $wynik"kod: 0 wynik: 1
kod: 1 wynik: 0Czyli masz i odpowiedź na piśmie, i kciuk. Możesz nawet napisać if wynik=$(komenda); then, a bramkarz sprawdzi kciuk, a wynik i tak trafi do pudełka.
Stare odwrotne apostrofy
W starych skryptach i na Stack Overflow z dawnych lat zobaczysz to samo zapisane inaczej:
dzis=`date +%F`To są odwrotne apostrofy, ten znak pod Escape, obok jedynki. Robią to samo co $( ). Kiedyś były jedyną drogą, dziś są jak telefon z kręconym kablem: działa, ale nikt rozsądny nie kupuje nowego.
Dlaczego? Bo nie da się ich łatwo zagnieżdżać. Ten sam przykład z folderem w starej wersji wygląda tak:
echo "Jestem w folderze: `basename \`pwd\``"Wewnętrzne apostrofy trzeba poprzedzić ukośnikami, bo inaczej Bash nie wie, który apostrof zamyka który. Przy trzech poziomach robi się z tego szyfr Enigmy. Do tego w środku odwrotnych apostrofów ukośniki zachowują się inaczej niż wszędzie indziej, co opisuje osobny akapit w man bash. A na wielu czcionkach odwrotny apostrof wygląda prawie jak zwykły, więc w tutorialu łatwo pomylić jeden z drugim.
ShellCheck na odwrotne apostrofy mówi krótko: Use $(...) notation instead of legacy backticks. Rozpoznawaj je, kiedy czytasz cudze skrypty, ale sam pisz $( ).
<( ), czyli wynik komendy udający plik
Czasem komenda nie chce tekstu, tylko pliku. Klasyka: diff, który porównuje dwa pliki. Chcesz porównać, jakie pliki leżą w dwóch folderach. Po staremu robisz dwa pliki tymczasowe, porównujesz i sprzątasz. Trzy komendy plus pamiętanie o sprzątaniu, o którym nikt nie pamięta.
Wyobraź sobie, że urzędnik wymaga dwóch dokumentów na papierze. Zamiast drukować, podsuwasz mu dwie tuby, z których tekst wyjeżdża na bieżąco. On myśli, że czyta kartki. Ty nie zużyłeś ani jednej.
diff <(ls stary) <(ls nowy)2d1
< b.conf
3a3
> d.conf<( ) uruchamia komendę w środku, a w jej miejsce wstawia nazwę pliku, z którego można czytać jej wynik. diff dostał dwie „nazwy plików” i nawet nie wie, że to nie są prawdziwe pliki. Wynik czytamy tak: b.conf jest tylko w starym folderze, d.conf tylko w nowym.
Co to za nazwa? Sprawdźmy:
echo <(true)/dev/fd/63To nie plik na dysku, tylko furtka do rury, do której jest podpięta komenda. O tym za chwilę pod spodem.
Prawdziwe zastosowania admina: porównanie listy pakietów na dwóch serwerach, porównanie konfiguracji przed zmianą i po, sprawdzenie, czego brakuje na jednej liście względem drugiej. Za każdym razem jedna linijka zamiast plików tymczasowych.
Jak to działa pod spodem
$( ) to wynajęcie drugiego kucharza. Bash tworzy kopię samego siebie, czyli podpowłokę, każe jej wykonać komendę, a jej pierwszy głośnik podpina do rury, z której sam czyta. Kiedy kucharz skończy, Bash ma cały wynik, obcina końcowe Entery i wstawia go w miejsce $( ).
Kopia jest kopią. Wszystko, co zrobi się w środku, zostaje w środku:
x=1
y=$(x=2; echo "$x")
echo "x=$x y=$y"x=1 y=2W środku x było dwa, ale na zewnątrz dalej jest jeden. Kucharz na zapleczu może przestawić sobie wszystkie garnki, ale Twoja kuchnia zostaje nietknięta. Przez to cd wewnątrz $( ) też nie zmienia folderu skryptu.
<( ) działa podobnie, ale zamiast czekać na koniec i zbierać wynik, Bash od razu uruchamia komendę w tle, jej wyjście podpina do rury i przekazuje nazwę tej rury jako /dev/fd/63 albo podobną. Na Linuksie /dev/fd/ to folder, w którym każdy proces widzi swoje otwarte pliki pod numerami. Numer zero to wejście, jeden to pierwszy głośnik, dwa to drugi. A 63 to rura, którą Bash otworzył specjalnie dla diff. Komenda czyta z niej jak z pliku, tylko nie może się w nim cofać, bo rura płynie w jedną stronę.
Ściąga flag
| Komenda | Flaga / element | Co robi |
|---|---|---|
$( ) | x=$(komenda) | wynik komendy do zmiennej |
$( ) | "$(komenda)" | wynik w kopercie, bez cięcia na słowa |
$( ) | $(komenda 2>&1) | złap też błędy |
$( ) | $(< plik) | zawartość pliku bez cat |
| odwrotne apostrofy | stara postać | to samo co $( ), nie używaj w nowych skryptach |
<( ) | diff <(a) <(b) | wynik komendy jako plik do czytania |
<( ) | done < <(komenda) | wynik komendy na wejście pętli |
>( ) | tee >(komenda) | plik, który przekazuje dane do komendy |
date | +%F | data RRRR-MM-DD |
date | '+%F %T' | data i godzina |
basename | basename ścieżka | ostatni kawałek ścieżki |
⚠ Typowy błąd
Ten błąd kosztował już wielu adminów godzinę życia, bo nie ma żadnego komunikatu. Liczymy konta z powłoką Bash:
#!/bin/bash
# zle.sh — liczy konta z powłoką bash (z błędem)
licznik=0
grep 'bash$' /etc/passwd | cut -d: -f1 | while IFS= read -r linia; do
echo "Znalazłem: $linia"
licznik=$(( licznik + 1 ))
done
echo "Razem: $licznik"Znalazłem: root
Znalazłem: ubuntu
Znalazłem: eryk
...
Razem: 0Pętla znalazła kilkanaście kont, a na końcu „Razem: 0”. Jak to?
Każdy kawałek rury działa w osobnej podpowłoce. Także pętla while za ostatnią kreską. Czyli liczył kucharz na zapleczu, w swojej kopii zmiennej licznik. Kiedy pętla się skończyła, kucharz poszedł do domu razem ze swoim wynikiem, a Ty masz swoje stare zero. ShellCheck to wyłapuje: licznik was modified in a subshell. That change might be lost.
Naprawa: zamiast wpychać dane do pętli rurą, podaj je pętli jak plik przez < <( ):
#!/bin/bash
# dobre.sh — liczy konta z powłoką bash
licznik=0
while IFS= read -r linia; do
echo "Znalazłem: $linia"
licznik=$(( licznik + 1 ))
done < <(grep 'bash$' /etc/passwd | cut -d: -f1)
echo "Razem: $licznik"Znalazłem: root
Znalazłem: ubuntu
Znalazłem: eryk
...
Razem: 16Pierwsza strzałka to zwykłe przekierowanie wejścia, tak jak done < plik. Druga, razem z nawiasem, to <( ), czyli „plik” z wynikiem komendy. Spacja między nimi jest obowiązkowa. Teraz pętla działa w głównej powłoce, a podpowłoka tylko dostarcza dane. Licznik zostaje u Ciebie.
✎ Zadanie
- Napisz skrypt
stan.sh, który zapisze do zmiennych nazwę komputera, dzisiejszą datę z godziną, liczbę procesów w systemie (ps -e --no-headers | wc -l) i procent zajętości dysku/jako samą liczbę. Podpowiedź:df --output=pcent /, potemtail -n 1, potemtr -dc '0-9'. Wypisz to wszystko w jednej linijce, a jeśli dysk jest zajęty w ponad 80 procentach, dopisz ostrzeżenie. - Zrób dwa foldery z kilkoma plikami, częściowo takimi samymi, częściowo różnymi, i jedną komendą
diffz<( )pokaż, czego brakuje w którym.
Pokaż rozwiązanie
#!/bin/bash
# stan.sh — jednolinijkowy raport o stanie serwera
komputer=$(hostname)
teraz=$(date '+%F %T')
procesy=$(ps -e --no-headers | wc -l)
zajete=$(df --output=pcent / | tail -n 1 | tr -dc '0-9')
echo "$teraz | $komputer | procesów: $procesy | dysk /: $zajete%"
if (( zajete > 80 )); then
echo "UWAGA: na / zostało mało miejsca!"
fi2026-11-03 16:35:46 | vm | procesów: 138 | dysk /: 38%Najciekawsza jest linijka z dyskiem. df --output=pcent / wypisuje nagłówek Use% i pod nim 38%, ze spacją na początku. tail -n 1 zostawia ostatnią linijkę, a tr -dc '0-9' usuwa wszystko, co nie jest cyfrą. Zostaje czyste 38, z którym kalkulator (( )) umie już porównywać. A żeby sprawdzić ostrzeżenie bez zapychania dysku, podmień na próbę zmienną ręcznie: zajete=91. Testowanie alarmu przeciwpożarowego zapalniczką, a nie podpalaniem biura.
Druga część:
mkdir -p stary nowy
touch stary/{a,b,c}.conf nowy/{a,c,d}.conf
diff <(ls stary) <(ls nowy)2d1
< b.conf
3a3
> d.confb.conf jest tylko po lewej, d.conf tylko po prawej. Zauważ też, że pliki zrobiłeś nawiasami klamrowymi z modułu pierwszego. Wszystko, czego się uczysz, zaczyna się składać w jedno. Jak IKEA, tylko instrukcja jest czytelna.
Co dalej
Nasze skrypty robią się coraz dłuższe i te same kawałki zaczynają się powtarzać. W następnej lekcji zamkniemy je w funkcjach, czyli przyciskach w windzie: jeden przycisk, cała procedura. I wyjaśnimy wreszcie, czemu return nie zwraca tego, co myślisz.