Przejdź do treści
Lekcja 44 z 117

Kurs › Moduł 4 › Lekcja 44

Ładne wypisywanie i długie teksty

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

Po tej lekcji: kursant wypisuje dane w równych kolumnach przez printf z formatami, wpisuje wielolinijkowe teksty i pliki konfiguracyjne here-dokiem i podaje komendzie tekst ze zmiennej przez here-string.

Wstęp

Ten sam skrypt. Te same dane. Lewy raport wysyłasz szefowi i dostajesz pytanie „co to jest?”. Prawy wysyłasz i dostajesz pytanie „możesz to robić co tydzień?”.

Różnica to dwie rzeczy: printf i here-doc. Dziś obie.

Czemu nie echo

Do tej pory wypisywaliśmy wszystko przez echo. I echo jest fajne, dopóki nie chcesz czegoś więcej. Zobacz:

echo "Linia1\nLinia2"
echo -e "Linia1\nLinia2"
printf 'Linia1\nLinia2\n'
Linia1\nLinia2
Linia1
Linia2
Linia1
Linia2

Zwykłe echo wypisało \n dosłownie. echo -e już zrozumiało. Ale tu jest haczyk: w różnych powłokach echo zachowuje się różnie. Pamiętasz dasha z lekcji 9? Inny urzędnik, inne zwyczaje. printf za to zachowuje się wszędzie tak samo. Dlatego w skryptach, które mają wyglądać porządnie, używa się printf.

printf, czyli formularz z rubrykami

printf działa jak formularz w urzędzie. Masz gotowy wzór z pustymi rubrykami: „Imię: ____, Wiek: ____”. Podajesz dane, a urzędnik wpisuje je w rubryki po kolei.

Wzór to pierwszy argument, nazywa się format. Rubryki to znaczki zaczynające się od procentu. Najważniejszy to %s, czyli „tu wstaw tekst”:

printf 'Witaj, %s!\n' "Eryk"
Witaj, Eryk!

Uwaga, printf sam nie dodaje nowej linii. Jeśli jej nie wpiszesz jako \n, następny tekst przyklei się do tego. Zapomnisz raz, a znak zachęty wyląduje Ci na końcu raportu.

A teraz magia. Jeśli podasz więcej danych niż rubryk, printf bierze formularz jeszcze raz. I jeszcze raz:

printf '%s\n' web01 web02 db01
printf '%s -> %s\n' web01 10.0.0.11 db01 10.0.0.21
web01
web02
db01
web01 -> 10.0.0.11
db01 -> 10.0.0.21

printf '%s\n' "${tablica[@]}" to najkrótszy sposób na wypisanie tablicy, każdy element w osobnej linijce. Używaliśmy tego już przy słownikach, teraz wiesz, czemu działa.

Szerokość rubryk: tu rodzą się tabelki

Między procent a literę możesz wpisać szerokość. To jak rubryka w formularzu, która ma określoną liczbę kratek.

printf '[%10s]\n' "prawo"
printf '[%-10s]\n' "lewo"
[     prawo]
[lewo      ]

%10s to dziesięć kratek, tekst dosunięty do prawej. Z minusem, %-10s, do lewej. Nawiasy kwadratowe są tu tylko po to, żebyś widział spacje. Zasada w tabelkach: teksty do lewej, liczby do prawej. Wtedy jedności stoją pod jednościami, jak w słupku z podstawówki.

Dla liczb są osobne rubryki:

printf '[%5d]\n' 42
printf '[%05d]\n' 42
printf '%.2f\n' 3.14159
printf '%d%%\n' 87
printf '%x\n' 255
[   42]
[00042]
3.14
87%
ff

%d to liczba całkowita. %05d dopełnia zerami, przydaje się do numerowania plików, żeby plik007 sortował się przed plik010. %.2f to liczba z dwoma miejscami po przecinku. %% to sam znak procentu, bo pojedynczy procent zarezerwował sobie formularz. A %x zamienia na szesnastkowy, co się przyda przy uprawnieniach i adresach.

I fajna rzecz dla admina: %d pilnuje, czy dostał liczbę.

printf '%d\n' "12abc"
bash: printf: 12abc: invalid number
12

Krzyknął, wpisał tyle, ile zrozumiał, i zwrócił kod błędu. Darmowy kontroler danych.

Dwie sztuczki Basha: data i zmienna

printf w Bashu umie wstawić datę bez wołania programu date:

printf '%(%Y-%m-%d %H:%M)T\n' -1
2026-11-03 22:17

-1 znaczy „teraz”. A w nawiasie te same literki co w date +.

I printf -v, czyli „nie wypisuj, tylko włóż do pudełka”:

printf -v dzis '%(%F)T' -1
echo "Dziś: $dzis"
Dziś: 2026-11-03

Czyli funkcja kopia z lekcji 40 mogłaby wziąć datę bez uruchamiania date. Jeden proces mniej. [macOS] %( )T jest od Basha 4.2, -v działa i na starym 3.2.

Wszystko razem: raport dysków

#!/bin/bash
# raport.sh – krótki raport o dyskach serwera
wiersz() {
    printf '%-20s %8s %8s %8s\n' "$@"
}

cat <<EOF
=============================================
 RAPORT DYSKÓW: $(hostname)
 Data: $(date '+%F %H:%M')
=============================================
EOF

wiersz "PUNKT MONTOWANIA" "ROZMIAR" "WOLNE" "PROCENT"
while read -r rozmiar wolne procent punkt; do
    wiersz "$punkt" "$rozmiar" "$wolne" "$procent"
done < <(df -h --output=size,avail,pcent,target -x tmpfs -x devtmpfs | tail -n +2)
=============================================
 RAPORT DYSKÓW: vm
 Data: 2026-11-03 22:18
=============================================
PUNKT MONTOWANIA      ROZMIAR    WOLNE  PROCENT
/                        252G      25G      42%
/srv/dane                452M     312M      26%

Format jest w jednym miejscu, w funkcji wiersz. Nagłówek i dane idą przez ten sam przycisk w windzie, więc zawsze trzymają się tych samych kolumn. Chcesz szerszą pierwszą kolumnę? Zmieniasz jedną liczbę. df --output wybiera kolumny, -x tmpfs wyrzuca dyski w pamięci, a tail -n +2 zjada nagłówek df, bo mamy własny.

[macOS] df --output to opcja z GNU. Macowy df jej nie zna, tam ten skrypt odpalisz tylko na Linuksie.

A ta górna ramka? To był here-doc. Czas na niego.

Here-doc: list w kopercie

Czasem chcesz wypisać nie jedną linijkę, tylko cały akapit. Ramkę raportu. Instrukcję użycia skryptu. Cały plik konfiguracyjny. Dwadzieścia echo pod rząd to jak pisanie listu, w którym każda linijka ma osobną kopertę.

Here-doc to jedna koperta na cały list. Mówisz komendzie: „czytaj od następnej linijki, aż zobaczysz słowo EOF”.

serwer=web01
cat <<EOF
Raport z serwera: $serwer
Data: $(date +%F)
Koszt: \$100
EOF
Raport z serwera: web01
Data: 2026-11-03
Koszt: $100

<<EOF otwiera kopertę, samo EOF w osobnej linijce ją zamyka. EOF to skrót od „end of file”, ale możesz użyć dowolnego słowa. W środku zmienne i $( ) działają jak w cudzysłowie. A jeśli chcesz dolara dosłownie, dajesz przed nim ukośnik.

A jeśli nie chcesz, żeby cokolwiek się rozwijało? Bierzesz słowo zamykające w apostrofy:

cat <<'EOF'
Raport z serwera: $serwer
Data: $(date +%F)
EOF
Raport z serwera: $serwer
Data: $(date +%F)

To jest koperta zaklejona na głucho. Co włożyłeś, to wyjdzie. Idealne, kiedy here-dokiem tworzysz… inny skrypt, który ma mieć w sobie dolary.

Here-doc można skierować do pliku. Tak się tworzy pliki konfiguracyjne w skryptach instalacyjnych:

cat > motd.txt <<EOF
Witaj na $serwer
Zanim coś zepsujesz, zrób kopię.
EOF
cat motd.txt
Witaj na web01
Zanim coś zepsujesz, zrób kopię.

A plik, do którego pisać może tylko root? Wtedy przez sudo tee:

sudo tee /etc/motd > /dev/null <<EOF
Witaj na $serwer
EOF

tee to rozgałęziacz, pisze do pliku i na ekran. Ekran wyciszamy przez /dev/null. Czemu nie po prostu sudo cat > /etc/motd? Wyjaśnię w błędach. Uwaga: tee bez -a nadpisuje cały plik. Pliki w /etc zmieniaj najpierw na maszynie wirtualnej, a przed zmianą zrób kopię, na przykład funkcją kopia.

I jeszcze jedna odmiana, <<- z minusem. Pozwala wciąć tekst i słowo zamykające, żeby ładnie wyglądało w if-ie. Ale wycina tylko tabulatory. Nie spacje. Tylko tabulatory. Wrócimy do tego w błędach, bo to klasyka.

Here-string: kartka zamiast koperty

Here-doc to list. A czasem chcesz podać komendzie tylko jedną linijkę ze zmiennej. Do tego są trzy strzałki, <<<, czyli here-string. Karteczka podana przez okienko.

linia="web01 10.0.0.11 ubuntu"
read -r nazwa adres system <<< "$linia"
echo "$nazwa | $adres | $system"
bc <<< "scale=2; 7/3"
tr '[:lower:]' '[:upper:]' <<< "awaria"
web01 | 10.0.0.11 | ubuntu
2.33
AWARIA

Pierwsza rzecz to rozbicie zmiennej na kawałki, widziałeś to już przy read -a. Druga to kalkulator bc z lekcji 38, bez echo i rury. Trzecia to podanie tekstu komendzie, która czyta tylko z wejścia.

Czemu nie echo "$linia" | read ...? Bo to ten sam problem co z mapfile w rurze: read działałby w podpowłoce i zmienne by przepadły. Here-string nie tworzy rury, więc zmienne zostają.

Jak to działa pod spodem

Here-doc i here-string to przekierowania, tak jak < z pliku. Bash bierze tekst, wkłada go do tymczasowego pliku albo do rury w pamięci, i podpina to komendzie jako wejście standardowe, czyli deskryptor 0. Komenda nawet nie wie, że tekst przyszedł ze skryptu. Myśli, że czyta z pliku. Dlatego here-doc działa z każdą komendą, która umie czytać z wejścia: cat, wc, mysql, ssh, bc.

wc -l <<EOF
jeden
dwa
trzy
EOF
3

A printf? To polecenie wbudowane, urzędnik robi to sam. Szerokość rubryki liczy w bajtach, nie w literach. I tu zaczyna się problem z polskimi znakami:

printf '[%-6s]\n' "lodz" "łódź"
[lodz  ]
[łódź]

„Łódź” ma cztery litery, ale siedem bajtów, więc w sześciu kratkach już się nie mieści i printf nie dodał ani jednej spacji. Kolumna się rozjechała. Na serwerach rzadko masz polskie nazwy kont, ale w raportach dla ludzi już tak.

I druga pułapka, tym razem dla osób z polskim systemem. printf czyta liczby według ustawień regionalnych. W polskich ustawieniach przecinek dziesiętny to przecinek, nie kropka:

LC_ALL=pl_PL.UTF-8 bash -c 'printf "%.2f\n" 3.14159'
LC_ALL=pl_PL.UTF-8 bash -c 'printf "%.2f\n" 3,14159'
bash: line 1: printf: 3.14159: invalid number
3,00
3,14

Na serwerze z angielskimi ustawieniami ten sam skrypt działał, na laptopie z polskimi wywala się. W skryptach, które liczą ułamki, ustaw na górze export LC_NUMERIC=C i śpij spokojnie.

Ściąga flag

KomendaFlagaCo robi
printf 'format' danewypisuje dane według wzoru, bez automatycznego \n
printf%stekst
printf%10s / %-10stekst w 10 kratkach, do prawej / do lewej
printf%d / %5d / %05dliczba całkowita, w 5 kratkach, dopełniona zerami
printf%.2fliczba z 2 miejscami po przecinku (uwaga na locale)
printf%xliczba szesnastkowo
printf%%znak procentu
printf%*sszerokość z argumentu
printf%(%F)Tdata według wzoru jak w date, -1 = teraz (Bash 4.2+)
printf-v zmiennazapisuje wynik do zmiennej zamiast wypisywać
cat <<EOFhere-doc, zmienne i $( ) działają
cat <<'EOF'apostrofyhere-doc dosłowny, nic się nie rozwija
cat <<-EOF-here-doc z wcięciem, wycina tylko tabulatory
cat > plik <<EOF>here-doc prosto do pliku
sudo tee plik > /dev/null <<EOFhere-doc do pliku roota (tee -a dopisuje)
komenda <<< "$v"<<<here-string, jedna linijka na wejście komendy

⚠ Typowy błąd

Piszesz porządnie, z wcięciami, żeby ładnie wyglądało:

#!/bin/bash
if true; then
    cat <<EOF
    Witaj na serwerze
    EOF
fi
echo "koniec"
zly.sh: line 7: warning: here-document at line 3 delimited by end-of-file (wanted `EOF')
zly.sh: line 8: syntax error: unexpected end of file

Bash szuka linijki, w której jest dokładnie EOF i nic więcej. Ty masz cztery spacje i EOF. Dla niego to zupełnie inne słowo. Więc czyta dalej, przez fi, przez echo, do końca pliku. Zjadł Ci resztę skryptu. A potem się dziwi, że if nie ma końca. To jak list, w którym zapomniałeś podpisu, więc listonosz wsadził do koperty jeszcze Twoją lodówkę.

ShellCheck powie Ci dokładnie, co zrobić:

SC1039 (error): Remove indentation before end token (or use <<- and indent with tabs).

Naprawa: EOF zawsze od pierwszej kolumny. Albo <<-EOF i wcięcia tabulatorami. Uwaga, wiele edytorów zamienia Tab na spacje, więc <<- zdradza w najmniej spodziewanym momencie. Bezpieczniej trzymać EOF przy lewej krawędzi i nie kombinować.

I obiecany drugi błąd, z sudo:

sudo cat > /etc/motd <<EOF
Witaj
EOF
bash: /etc/motd: Permission denied

sudo działa tylko na cat. Ale przekierowanie > robi Twoja powłoka, jeszcze zanim sudo w ogóle ruszy. A Twoja powłoka to zwykły użytkownik, który do /etc nie ma klucza. To jak dać świadkowi przepustkę VIP, a samemu stanąć przed bramką bez niej. Dlatego sudo tee: wtedy do pliku pisze tee, który ma uprawnienia roota.

✎ Zadanie

Wypisz printf-em tabelkę wszystkich kont z /etc/passwd, które mają powłokę /bin/bash: nazwa, UID i folder domowy, w równych kolumnach. Nagłówek tabelki zrób here-dokiem.

Rozwiązanie znajdziesz pod spodem.

Pokaż rozwiązanie

Zadanie: tabelka printf-em ze wszystkich kont z /etc/passwd, które mają powłokę /bin/bash. Nazwa, UID i folder domowy w równych kolumnach, a nagłówek here-dokiem.

#!/bin/bash
# Tabelka kont z powłoką /bin/bash
cat <<NAGLOWEK
Konta z powłoką /bin/bash na: $(hostname)
NAGLOWEK
printf '%-12s %6s  %s\n' "NAZWA" "UID" "FOLDER DOMOWY"
printf '%-12s %6s  %s\n' "-----" "---" "-------------"
while IFS=: read -r nazwa _ uid _ _ dom powloka; do
    if [[ $powloka == /bin/bash ]]; then
        printf '%-12s %6d  %s\n' "$nazwa" "$uid" "$dom"
    fi
done < /etc/passwd
Konta z powłoką /bin/bash na: vm
NAZWA           UID  FOLDER DOMOWY
-----           ---  -------------
root              0  /root
eryk           1000  /home/eryk

Co tu się dzieje. Here-doc nie ma apostrofów wokół słowa NAGLOWEK, więc Bash rozwija w nim $(hostname). IFS=: mówi read, że kolumny oddziela dwukropek, a podkreślnik _ to pudełko na śmieci: kolumny, których nie potrzebujemy, wrzucamy tam i zapominamy. %-12s to napis dosunięty do lewej na dwanaście znaków, %6d to liczba dosunięta do prawej. Liczby do prawej, napisy do lewej, i tabelka wygląda jak z banku, a nie jak lista zakupów pisana w tramwaju.

Jeśli masz zainstalowanego PostgreSQL-a, wyskoczy też postgres. Tak, baza danych ma Basha. Czasem też chce pogadać z urzędnikiem.

Co dalej

Nożyczki z lekcji 41 tną według prostych wzorców z gwiazdką. A co, jeśli musisz znaleźć w logu wszystkie adresy IP albo sprawdzić, czy ktoś podał poprawny adres e-mail? W następnej lekcji wyrażenia regularne: grep -E, grep -o i [[ =~ ]]. Język, który wygląda jak kot przeszedł po klawiaturze, a jest najpotężniejszym narzędziem do tekstu, jakie znasz.