Komunikaty, logi i kolory
Po tej lekcji: kursant pisze skrypty, które oddzielają dane (stdout) od komunikatów (stderr), zapisują ważne zdarzenia do dziennika systemowego przez logger, kolorują komunikaty tylko wtedy, gdy ma to sens, i kończą się kodem wyjścia, z którego inny program coś zrozumie.
Wstęp
plik z wynikiem nocnego skryptu, w środku liczby przemieszane z napisami „Sprawdzam…”, „OK!”, „Hmm, coś dziwnego” i krzaczkami ^[[32m
Kolega napisał skrypt, który zbiera zajętość dysków. Drugi skrypt bierze jego wynik i robi z niego wykres. Wykres od tygodnia pokazuje zero. Dlaczego? Bo w wyniku poza liczbami jest „Sprawdzam dysk…”, „Wszystko gra!” i zielony kolor, który w pliku wygląda, jakby ktoś przeszedł kotem po klawiaturze.
Skrypt, który gada do człowieka i do maszyny tym samym kanałem, to jak kelner, który przynosi zupę i od razu wylewa do niej swoje przemyślenia o życiu. Dziś rozdzielamy te dwie rzeczy. Do tego dziennik systemowy, kolory, które wiedzą, kiedy się wyłączyć, i kody wyjścia, z którymi da się coś zrobić.
Dwa głośniki, dwie role
Pamiętasz dwa głośniki z modułu pierwszego? Głośnik 1, stdout, i głośnik 2, stderr. Do tej pory traktowaliśmy je jako „wynik” i „błędy”. W skryptach przyjmij trochę szerszą zasadę:
- stdout to dane. To, co ktoś może chcieć przekazać rurą dalej albo zapisać do pliku: liczby, listy, raport.
- stderr to rozmowa z człowiekiem. Błędy, ostrzeżenia, ale też „sprawdzam…”, „gotowe”, postęp pracy.
Tak robią porządne programy. find wypisuje znalezione pliki na stdout, a „Permission denied” na stderr. Dzięki temu find / -name x 2>/dev/null daje czystą listę.
W skrypcie wysyłasz komunikat na drugi głośnik przez >&2:
echo "Sprawdzam dyski..." >&2
echo "/ 40"Pierwsza linijka to rozmowa, druga to dane. Najwygodniej zrobić sobie funkcje, żeby nie pisać >&2 w stu miejscach:
info() { echo "$*" >&2; }
blad() { echo "BŁĄD: $*" >&2; }Funkcja to przycisk w windzie. Naciskasz blad, a winda sama wie, że jedzie na drugie piętro, czyli do głośnika 2.
logger: wpis do dziennika budowy
Komunikat na stderr widzisz, kiedy siedzisz przy terminalu. A o trzeciej w nocy, kiedy skrypt odpala budzik, czyli cron, nikt nie siedzi. Komunikat leci w próżnię albo do maila, którego nikt nie czyta.
Dlatego ważne zdarzenia zapisujemy do dziennika systemowego. Wyobraź sobie dziennik budowy: każda ekipa, która coś zrobiła albo coś zepsuła, wpisuje się z godziną i nazwą firmy. Kierownik rano otwiera jeden zeszyt i widzi wszystko. Wpis do tego dziennika robi komenda logger.
logger "Kopia zrobiona"
logger -t kopia -p user.warning "Mało miejsca na /srv"
logger -s -t kopia "Kopia zrobiona: 120 plików"-t kopia to podpis, tak zwany tag. Bez niego wpis podpisze się Twoją nazwą użytkownika i za miesiąc nie będziesz wiedział, który skrypt to napisał. -p user.warning to waga wpisu. -s dodatkowo wypisuje to samo na stderr, czyli i do dziennika, i na ekran.
Gdzie to potem znaleźć?
journalctl -t kopia # [Ubuntu] i [RHEL], wszędzie z systemd
journalctl -t kopia -p warning # tylko ostrzeżenia i gorsze
journalctl -t kopia -f # podgląd na żywo
sudo grep kopia /var/log/syslog # [Ubuntu], gdy działa rsyslog
sudo grep kopia /var/log/messages # [RHEL], gdy działa rsyslog2026-11-03T16:28:29.160951+01:00 vm kopia: Mało miejsca na /srv
2026-11-03T16:28:29.163857+01:00 vm kopia: Kopia zrobiona: 120 plikówData, nazwa komputera, podpis, treść. Kierownik budowy byłby dumny.
Wagi wpisów mają ustaloną kolejność, od najgorszej: emerg (0), alert (1), crit (2), err (3), warning (4), notice (5), info (6), debug (7). W skryptach admina wystarczą trzy: err dla błędów, warning dla ostrzeżeń, info dla zwykłych zdarzeń. emerg zostaw na sytuacje typu „serwerownia płonie”. Jeśli Twój skrypt kopii zapasowej wysyła emerg, bo plik ma o jeden bajt za dużo, to koledzy szybko nauczą się ignorować wszystko, co wysyłasz.
Kolory: farba, która nie zawsze wysycha
Kolory w terminalu to nie magia, tylko specjalne ciągi znaków. Terminal dostaje bajt ESC, potem [31m i rozumie: „od teraz pisz na czerwono”. [0m znaczy „wróć do normalnych kolorów”.
printf '\e[31mBŁĄD\e[0m: brak pliku\n'
printf '\e[33mUWAGA\e[0m: mało miejsca\n'
printf '\e[32mOK\e[0m: kopia zrobiona\n'\e to właśnie bajt ESC. 31 to czerwony, 32 zielony, 33 żółty. Na ekranie wygląda świetnie. Problem zaczyna się, kiedy przekierujesz to do pliku:
$ cat -v naiwny.log
^[[31mBŁĄD: brak pliku^[[0mcat -v pokazuje niewidoczne znaki. Plik zawiera surowe kody, bo plik nie jest terminalem i nie ma komu ich zinterpretować. W logu, w mailu z crona, w drugim skrypcie zostaje sama farba rozlana po kartce.
Stąd złota zasada: koloruj tylko wtedy, gdy piszesz do terminala. Bash ma na to test -t:
if [[ -t 2 && -z ${NO_COLOR-} ]]; then
czerwony=$'\e[31m'; zolty=$'\e[33m'; zielony=$'\e[32m'; reset=$'\e[0m'
else
czerwony=''; zolty=''; zielony=''; reset=''
fi[[ -t 2 ]] pyta: „czy głośnik 2 jest podłączony do terminala?”. Jeśli stderr idzie do pliku albo do crona, odpowiedź brzmi nie i zmienne kolorów zostają puste. Skrypt dalej je wkleja, tylko że wkleja nic.
A NO_COLOR to umowa między programistami: jeśli zmienna środowiskowa NO_COLOR jest ustawiona i niepusta, program nie powinien dodawać kolorów. Ktoś ma problem ze wzrokiem, ktoś ma jasny motyw, na którym żółty jest niewidoczny, ktoś po prostu nie lubi. Szanujmy to, kosztuje jeden warunek.
Zamiast kodów można użyć tput, które pyta bazę terminali, jakie kody działają na tym konkretnym ekranie:
czerwony=$(tput setaf 1)
reset=$(tput sgr0)Czytelniej, ale uwaga: bez zmiennej TERM, na przykład w cronie, tput się wykłada z tput: No value for $TERM and no -T specified i kodem 2. Dlatego wywołuj je tylko wewnątrz warunku z -t.
Kody wyjścia: kciuk w górę, kciuk w dół i liczby pomiędzy
Kod wyjścia to kciuk, który skrypt pokazuje na koniec. Zero to kciuk w górę. Wszystko inne to kciuk w dół. Ale kciuk w dół ma 255 odcieni i warto ich używać świadomie, bo inny skrypt, cron czy systemd podejmuje na ich podstawie decyzje.
Konwencje, które warto znać:
0 wszystko dobrze
1 ogólny błąd, "coś poszło nie tak"
2 złe użycie komendy wbudowanej Basha, w wielu programach poważniejszy błąd
126 komenda znaleziona, ale nie da się jej uruchomić (brak prawa wykonania)
127 komenda nie znaleziona
128+N proces zabity sygnałem N: 130 to Ctrl+C, 137 to kill -9, 143 to zwykłe killSprawdzone w praktyce:
$ ./nieexec.sh; echo "kod: $?"
bash: ./nieexec.sh: Permission denied
kod: 126
$ nieistniejaca_komenda; echo "kod: $?"
bash: nieistniejaca_komenda: command not found
kod: 127
$ sleep 100 & kill $!; wait $!; echo "kod: $?"
kod: 143grep to wzór programu, który mądrze używa kodów: 0 znalazł, 1 nie znalazł, 2 wystąpił błąd, na przykład brak pliku. Dzięki temu „nic nie znalazłem” to coś innego niż „nie mogłem szukać”. Twój skrypt powinien robić tak samo.
Jest też starszy zestaw z systemów BSD, plik sysexits.h, który nadaje znaczenie numerom od 64 do 78. Najprzydatniejsze:
64 EX_USAGE złe użycie (złe opcje, brak argumentów)
66 EX_NOINPUT nie ma pliku wejściowego
73 EX_CANTCREAT nie da się utworzyć pliku wynikowego
75 EX_TEMPFAIL chwilowy problem, spróbuj później
77 EX_NOPERM brak uprawnień
78 EX_CONFIG błąd w konfiguracjiTo nie jest obowiązkowe prawo, raczej dobry obyczaj. Najważniejsze: wybierz swoje kody, wypisz je w komentarzu na górze skryptu i trzymaj się ich. Kod wyjścia, który zmienia znaczenie co wersję, to kciuk, który czasem znaczy „super”, a czasem „uciekaj”.
Uwaga na pułapkę: kod wyjścia to liczba od 0 do 255. exit 300 da w praktyce 44, bo liczy się reszta z dzielenia przez 256. A exit -1 da 255. Arytmetyka jak na zegarze, na którym po 23 jest 0.
Wszystko razem: miejsce.sh
#!/bin/bash
# miejsce.sh — sprawdza zajętość systemów plików
# Kody wyjścia: 0 wszystko OK, 1 przekroczony próg, 2 błąd, 64 złe użycie
prog=${0##*/}
# Kolory tylko wtedy, gdy stderr to terminal i nikt nie prosi o brak kolorów
if [[ -t 2 && -z ${NO_COLOR-} ]]; then
czerwony=$'\e[31m'; zolty=$'\e[33m'; zielony=$'\e[32m'; reset=$'\e[0m'
else
czerwony=''; zolty=''; zielony=''; reset=''
fi
info() { printf '%s[ OK ]%s %s\n' "$zielony" "$reset" "$*" >&2; }
uwaga() { printf '%s[UWAGA]%s %s\n' "$zolty" "$reset" "$*" >&2
logger -t "$prog" -p user.warning -- "$*"; }
blad() { printf '%s[BŁĄD]%s %s\n' "$czerwony" "$reset" "$*" >&2
logger -t "$prog" -p user.err -- "$*"; }
if [[ $# -lt 2 ]]; then
echo "Użycie: $prog PRÓG_PROCENT PUNKT_MONTOWANIA..." >&2
exit 64
fi
prog_proc=$1
shift
if [[ ! $prog_proc =~ ^[0-9]+$ ]]; then
blad "Próg musi być liczbą, a dostałem: $prog_proc"
exit 64
fi
wynik=0
for punkt in "$@"; do
zajete=$(df --output=pcent -- "$punkt" 2>/dev/null | tail -n 1 | tr -dc '0-9')
if [[ -z $zajete ]]; then
blad "Nie mogę sprawdzić: $punkt"
wynik=2
continue
fi
if (( zajete >= prog_proc )); then
uwaga "$punkt zajęty w $zajete% (próg $prog_proc%)"
(( wynik == 0 )) && wynik=1
else
info "$punkt zajęty w $zajete%"
fi
echo "$punkt $zajete" # czyste dane na stdout
done
exit "$wynik"${0##*/} to nazwa skryptu bez ścieżki, czyli miejsce.sh, a nie /home/eryk/bin/miejsce.sh. Używamy jej jako podpisu w dzienniku. df --output=pcent wypisuje samą kolumnę z procentem zajętości, a tr -dc '0-9' wyrzuca wszystko, co nie jest cyfrą. -- w logger chroni przed komunikatem, który zaczyna się od minusa.
Sprawdzamy w terminalu:
$ ./miejsce.sh 30 / /nie/ma
[UWAGA] / zajęty w 40% (próg 30%)
/ 40
[BŁĄD] Nie mogę sprawdzić: /nie/ma
$ echo $?
2Na ekranie wszystko razem, z kolorami. A teraz rozdzielamy głośniki:
$ ./miejsce.sh 30 / /nie/ma > dane.txt
[UWAGA] / zajęty w 40% (próg 30%)
[BŁĄD] Nie mogę sprawdzić: /nie/ma
$ cat dane.txt
/ 40
$ ./miejsce.sh 30 / 2> komunikaty.txt > /dev/null
$ cat -v komunikaty.txt
[UWAGA] / zajM-DM-^Yty w 40% (prM-CM-3g 30%)W dane.txt czyste dane, które drugi skrypt może łyknąć bez dławienia. W komunikaty.txt nie ma ani jednego kodu koloru, bo stderr szedł do pliku, więc skrypt sam wyłączył farbę. Te dziwne M-DM-^Y to tylko sposób, w jaki cat -v pokazuje polskie litery, bo też są dla niego „niezwykłymi” bajtami. W edytorze zobaczysz zwykłe „zajęty”.
A w dzienniku:
$ sudo grep miejsce.sh /var/log/syslog
2026-11-03T16:37:36.265441+01:00 vm miejsce.sh: Nie mogę sprawdzić: /nie/ma
2026-11-03T16:37:36.306652+01:00 vm miejsce.sh: / zajęty w 40% (próg 30%)Do dziennika trafiły tylko ostrzeżenia i błędy. „Wszystko OK” nie musi być zapisywane co pięć minut przez rok. Dziennik budowy to nie pamiętnik.
Jak to działa pod spodem
Każdy proces ma tabelkę otwartych plików, w której pozycja 1 to stdout, a pozycja 2 to stderr. Kiedy siedzisz w terminalu, obie pozycje wskazują na to samo urządzenie terminala, dlatego widzisz wszystko razem. Przekierowanie > czy 2> podmienia tylko jedną pozycję w tabelce. >&2 przy echo znaczy: „na czas tej jednej komendy podłącz głośnik 1 tam, gdzie jest głośnik 2”.
Test [[ -t 2 ]] zadaje jądru pytanie: „czy to, co jest pod pozycją 2, jest terminalem?”. Programy w C robią to samo funkcją isatty. Dlatego ls w terminalu wypisuje kolorowe nazwy w kolumnach, a ls | cat jedną nazwę na linijkę i bez kolorów. To nie przypadek, tylko ta sama sztuczka.
logger nie pisze do pliku. Wysyła krótką wiadomość do gniazda /dev/log, czyli do skrzynki pocztowej, którą opróżnia usługa dziennika. W systemach z systemd odbiera ją systemd-journald i zapisuje w swoim dzienniku, który czytasz przez journalctl. Jeśli dodatkowo działa rsyslog, dostaje kopię i zapisuje ją do zwykłego pliku tekstowego, /var/log/syslog na Ubuntu albo /var/log/messages na Red Hacie. Gdy nie ma kto odebrać, logger zgłasza socket /dev/log: No such file or directory.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
echo / printf | >&2 | wysyła komunikat na stderr |
logger | -t TAG | podpis wpisu w dzienniku |
logger | -p grupa.waga | waga wpisu, np. user.err, user.warning |
logger | -s | dodatkowo wypisuje na stderr |
logger | -- | koniec opcji, dalej sama treść |
journalctl | -t TAG | wpisy z danym podpisem |
journalctl | -p warning | tylko ta waga i gorsze |
journalctl | -f | podgląd na żywo |
| [[ ]] | -t 1 / -t 2 | czy stdout / stderr to terminal |
tput | setaf N / sgr0 | kolor tekstu N / powrót do normalnych |
cat | -v | pokazuje niewidoczne znaki, np. kody kolorów |
df | --output=pcent | tylko kolumna z procentem zajętości |
exit | 0–255 | kod wyjścia; większe liczby zawijają się modulo 256 |
⚠ Typowy błąd
#!/bin/bash
echo "Coś nie tak" >2Uruchamiasz i na ekranie cisza. Żadnego błędu. A po chwili w folderze pojawia się plik o nazwie 2.
$ ls -l 2
-rw-r--r-- 1 eryk eryk 13 Nov 3 16:29 2
$ cat 2
Coś nie takBez znaku & Bash rozumie >2 jako „przekieruj do pliku o nazwie 2”. Komunikat o błędzie trafia do pliku, którego nikt nie szuka, a w folderze ze skryptami rośnie kolekcja plików z cyframi w nazwach. Jak list wysłany na adres „Dwa”, bez ulicy i miasta. Poczta coś z nim zrobi, ale nie to, co chciałeś.
Poprawnie: >&2. Ampersand mówi „to nie nazwa pliku, tylko numer głośnika”. ShellCheck to łapie:
In z2.sh line 2:
echo "Coś nie tak" >2
^-- SC2210 (warning): This is a file redirection. Was it supposed to be a comparison or fd operation?Kolejny powód, żeby każdy skrypt przed wdrożeniem przeszedł przez sąsiada zza płotu.
✎ Zadanie
Weź poprawiony policz.sh z poprzedniego zadania i zrób z niego porządnego obywatela:
- Na stdout ma iść tylko sama liczba plików. Wszystkie inne napisy idą na stderr przez funkcje
infoiblad. - Bez argumentu skrypt wypisuje, jak go używać, i kończy się kodem 64. Gdy folder nie istnieje, wypisuje błąd i kończy się kodem 66.
- Każdy błąd trafia też do dziennika przez
loggerz podpisempolicz. - Słowo „BŁĄD” jest czerwone, ale tylko gdy stderr to terminal i nie ustawiono
NO_COLOR.
Sprawdź: ./policz.sh /etc > wynik.txt (w pliku ma być sama liczba), ./policz.sh /nie/ma; echo $? (ma być 66) i journalctl -t policz albo grep policz w pliku dziennika.
Pokaż rozwiązanie
#!/bin/bash
# policz.sh — liczy pliki w folderze; na stdout tylko liczba
# Kody: 0 OK, 64 złe użycie, 66 brak folderu
if [[ -t 2 && -z ${NO_COLOR-} ]]; then
czerwony=$(tput setaf 1); reset=$(tput sgr0)
else
czerwony=''; reset=''
fi
info() { echo "$*" >&2; }
blad() {
echo "${czerwony}BŁĄD:${reset} $*" >&2
logger -t policz -p user.err -- "$*"
}
if [[ $# -ne 1 ]]; then
blad "Użycie: ${0##*/} FOLDER"
exit 64
fi
katalog=$1
if [[ ! -d $katalog ]]; then
blad "Nie ma folderu: $katalog"
exit 66
fi
ile=0
for f in "$katalog"/*; do
if [[ -f $f ]]; then
ile=$((ile + 1))
fi
done
info "Liczę pliki w: $katalog"
echo "$ile"$ ./policz.sh "Moje dokumenty" > wynik.txt
Liczę pliki w: Moje dokumenty
$ cat wynik.txt
6
$ ./policz.sh /nie/ma; echo "kod: $?"
BŁĄD: Nie ma folderu: /nie/ma
kod: 66
$ sudo grep policz /var/log/syslog
2026-11-03T16:37:31.737545+01:00 vm policz: Nie ma folderu: /nie/maNa stdout sama szóstka, cała reszta na drugim głośniku. tput wywołujemy tylko w środku warunku z -t 2, więc w cronie, gdzie nie ma terminala ani zmiennej TERM, nikt go nawet nie zawoła. I zauważ, że skoro jesteśmy już w Bashu, przeszliśmy z [ ] na [[ ]]. W podwójnych nawiasach Bash nie rozsypuje zmiennych na kawałki, więc [[ -f $f ]] jest bezpieczne nawet bez cudzysłowów. Bramkarz w lepszych butach.
Co dalej
Nasze skrypty rosną. Funkcje do komunikatów kopiujemy już z pliku do pliku, a to pierwszy krok do bałaganu. W następnej lekcji robimy duży skrypt jak należy: wspólne funkcje w osobnej bibliotece wczytywanej przez source, porządna struktura z funkcją main, styl, który da się czytać po roku, i blokada flock, żeby skrypt nie wystartował dwa razy naraz.