1Czym jest powłoka (shell)?

Powłoka jako interfejs użytkownika

Zanim zagłębimy się w bash, musimy zrozumieć, czym jest powłoka systemowa (shell). Jest to program, który tworzy interfejs pomiędzy użytkownikiem a jądrem systemu operacyjnego (sercem systemu).

Powłoka pozwala na wykonywanie poleceń, uruchamianie programów i zarządzanie systemem. Wyróżniamy dwa główne typy interfejsów:

  • CLI (Command-Line Interface): Interfejs wiersza poleceń, gdzie użytkownik wpisuje tekstowe polecenia (np. bash, PowerShell, cmd).
  • GUI (Graphical User Interface): Interfejs graficzny, z którym interakcja odbywa się za pomocą myszy, okien i ikon (np. Windows Explorer, GNOME).

Skrypty piszemy w powłokach typu CLI, aby zautomatyzować zadania, które normalnie wykonywalibyśmy, wpisując polecenia ręcznie.

  +------------------+
  |    Użytkownik    |
  +------------------+
          ^
          | Interakcja
          v
  +------------------+
  |   Powłoka (bash) |  <-- Tu działają skrypty
  +------------------+
          ^
          | Wywołania systemowe
          v
  +------------------+
  | Jądro Systemu OS |
  +------------------+
          ^
          | Sterowanie
          v
  +------------------+
  |    Sprzęt (CPU)  |
  +------------------+
            

Powłoka systemowa to fundamentalny element architektury każdego systemu operacyjnego, odpowiedzialny za komunikację między użytkownikiem a jądrem. W codziennej pracy administratorzy systemów Linux korzystają z powłoki do wykonywania rutynowych zadań, takich jak zarządzanie plikami, monitorowanie procesów czy konfiguracja sieci. Wybór interfejsu CLI zamiast GUI niesie za sobą wiele korzyści, przede wszystkim możliwość automatyzacji i skryptowania powtarzalnych operacji. Powłoka interpretuje wpisywane polecenia, przetwarza je i przekazuje do jądra systemu za pomocą wywołań systemowych. W systemach uniksowych istnieje wiele różnych powłok, różniących się dostępnymi funkcjami i składnią, ale bash pozostaje najpopularniejszym wyborem. Zrozumienie roli powłoki w systemie jest pierwszym krokiem do zostania biegłym administratorem lub programistą w środowisku Linux.

Interfejs tekstowy może na pierwszy rzut oka wydawać się mniej przyjazny od graficznego, jednak po opanowaniu podstaw okazuje się nieporównywalnie bardziej efektywny. Wiele zaawansowanych operacji systemowych w ogóle nie ma odpowiednika w interfejsie graficznym i wymaga znajomości poleceń powłoki.

2Wprowadzenie do basha

Czym jest bash?

bash (Bourne Again Shell) to interpreter poleceń oraz język skryptowy, będący standardową powłoką w większości systemów uniksowych, w tym Linux i macOS. Jest następcą oryginalnej powłoki Bourne'a (sh).

  • Główne cele: Automatyzacja zadań administracyjnych, zarządzanie systemem, operacje na plikach i przetwarzanie tekstu.
  • Kontekst historyczny: Stworzony przez Briana Foxa dla projektu GNU w 1989 roku jako wolny zamiennik dla sh. Nazwa jest grą słów (ang. born again – „narodzony na nowo").
  • Interpreter: Skrypty bash są interpretowane linia po linii. Plik skryptu musi mieć uprawnienie do wykonywania (chmod +x nazwa_skryptu.sh) i zazwyczaj rozpoczyna się od specjalnej linii zwanej shebang.
# Sprawdzenie, jakiej powłoki aktualnie używamy
echo $SHELL
# Wynik w większości systemów Linux: /bin/bash

# Sprawdzenie wersji basha
bash --version

Bash wywodzi się z projektu GNU i stanowi rozwinięcie oryginalnej powłoki Bourne'a, zachowując z nią pełną zgodność wsteczną. Jego popularność wynika z bogatego zestawu funkcji, obejmującego między innymi mechanizmy uzupełniania poleceń, historię wpisywanych poleceń oraz zaawansowane możliwości skryptowe. Współczesne dystrybucje Linuksa domyślnie instalują basha jako podstawową powłokę systemową. Pliki skryptów bash mają zazwyczaj rozszerzenie .sh, choć nie jest ono wymagane do poprawnego działania. Ważną cechą basha jest możliwość interaktywnego korzystania z niego na bieżąco, co ułatwia testowanie poleceń przed umieszczeniem ich w skrypcie. Język skryptowy basha oferuje zmienne, instrukcje warunkowe, pętle, funkcje oraz obsługę wyrażeń regularnych.

Interpreter basha czyta skrypt linia po linii, więc błąd w jednej linii nie zawsze przerywa całe działanie skryptu. To od programisty zależy, czy skrypt będzie odpowiednio zabezpieczony przed nieprzewidzianymi sytuacjami. Znajomość basha jest niezbędna w pracy każdego specjalisty DevOps i administratora systemów.

3Nasz pierwszy skrypt: "Hello, World!"

Struktura pliku skryptu

Stwórzmy pierwszy skrypt. Otwórz edytor tekstu (np. `nano`, `vim`, `vscode`) i utwórz plik o nazwie `hello.sh`.

  1. Shebang (#!): Pierwsza linia, #!/bin/bash, informuje system operacyjny, że ten plik powinien być wykonany za pomocą interpretera bash znajdującego się w ścieżce /bin/bash. Jest to kluczowe dla przenośności i prawidłowego działania skryptów.
  2. Komentarze: Linie zaczynające się od # są ignorowane przez interpreter. Służą do dokumentowania kodu.
  3. Polecenia: Każda kolejna linia to polecenie, które powłoka ma wykonać. echo to podstawowe polecenie do wyświetlania tekstu.
#!/bin/bash

# To jest mój pierwszy skrypt basha.
# Jego celem jest wyświetlenie komunikatu powitalnego.

echo "Hello, World!"

Shebang to specjalna konstrukcja składniowa, która informuje system operacyjny o tym, jakiego interpretera należy użyć do wykonania danego pliku. W przypadku basha poprawny shebang to #!/bin/bash, chociaż często spotyka się również #!/usr/bin/env bash, co zapewnia większą przenośność między różnymi dystrybucjami. Komentarze w skryptach są niezwykle ważne z perspektywy utrzymania kodu, ponieważ pozwalają zrozumieć intencje autora osobom, które będą później modyfikować skrypt. Polecenie echo jest jednym z najprostszych, a zarazem najczęściej używanych poleceń w bashu, służącym do wyświetlania tekstu na standardowym wyjściu. Struktura pliku skryptu powinna być przemyślana, z czytelnym podziałem na sekcje i logicznym układem poleceń.

Tworzenie pierwszego skryptu to przełomowy moment w nauce programowania w bashu. Warto od razu wyrobić sobie dobre nawyki, takie jak umieszczanie shebangu, dokumentowanie kodu komentarzami i stosowanie konsekwentnej konwencji nazewnictwa. Nawet najprostszy skrypt uruchomiony po raz pierwszy daje ogromną satysfakcję i motywację do dalszej nauki.

4Uruchamianie skryptu

Nadawanie uprawnień i wykonanie

Domyślnie nowo utworzony plik tekstowy nie ma uprawnień do uruchamiania. Musimy je nadać jawnie za pomocą polecenia chmod (change mode).

  • chmod +x nazwa_pliku: Dodaje (+) uprawnienie do wykonywania (execute) dla wszystkich użytkowników.

Po nadaniu uprawnień skrypt można uruchomić na dwa sposoby:

  1. Podając ścieżkę względną: ./hello.sh. Kropka i ukośnik (./) oznaczają "w bieżącym katalogu". Jest to standardowy i bezpieczny sposób uruchamiania skryptów.
  2. Wywołując interpreter jawnie: bash hello.sh. W tym przypadku uprawnienia do wykonywania nie są wymagane, a shebang jest ignorowany.
# Krok 1: Sprawdź aktualne uprawnienia (brak 'x')
# > ls -l hello.sh
# Wynik: -rw-r--r-- 1 użytkownik grupa 85 paź 8 22:10 hello.sh

# Krok 2: Nadaj uprawnienia do wykonywania
chmod +x hello.sh

# Krok 3: Sprawdź ponownie uprawnienia (pojawiło się 'x')
# > ls -l hello.sh
# Wynik: -rwxr-xr-x 1 użytkownik grupa 85 paź 8 22:11 hello.sh

# Krok 4: Uruchom skrypt
./hello.sh

# Oczekiwany wynik na konsoli:
# Hello, World!

Uprawnienia do plików w systemie Linux dzielą się na trzy kategorie: odczyt, zapis i wykonanie. Polecenie chmod pozwala modyfikować te uprawnienia zarówno dla właściciela pliku, grupy, jak i pozostałych użytkowników. Ważne jest, aby zrozumieć, że dodanie uprawnienia do wykonania nie czyni pliku automatycznie bezpieczniejszym lub mniej bezpiecznym, a jedynie pozwala na jego uruchomienie. Uruchamianie skryptów za pomocą ./nazwa_skryptu wymaga zarówno uprawnienia do wykonania, jak i uprawnienia do odczytu, ponieważ interpreter musi najpierw przeczytać zawartość pliku. Alternatywna metoda uruchamiania przez jawne wywołanie interpretera (bash nazwa_skryptu) jest przydatna, gdy plik nie ma uprawnień do wykonania lub gdy chcemy użyć innego interpretera niż wskazany w shebangu.

Warto zapamiętać, że znak . w ścieżce ./ oznacza bieżący katalog roboczy. Jest to istotne, ponieważ katalog bieżący z reguły nie znajduje się w zmiennej środowiskowej PATH, więc bez jawnego wskazania ścieżki system zgłosi błąd "command not found". Zrozumienie mechanizmu uprawnień i uruchamiania skryptów jest kluczowe dla bezpiecznej i efektywnej pracy w systemie Linux.

5Zmienne: Wprowadzenie

Czym są zmienne?

Zmienne to "pojemniki" na dane. Pozwalają przechowywać informacje (tekst, liczby) pod określoną nazwą, aby móc się do nich później odwoływać i je modyfikować. W bash zmienne nie mają ściśle określonego typu - domyślnie wszystko jest traktowane jako ciąg znaków.

Deklaracja i przypisanie wartości

Składnia jest bardzo prosta, ale ma jedną ważną zasadę: wokół znaku = nie może być spacji.

  • NAZWA="Wartość" - Poprawnie
  • NAZWA = "Wartość" - Błędnie! bash zinterpretuje `NAZWA` jako polecenie.

Konwencją jest używanie wielkich liter dla nazw zmiennych, aby odróżnić je od poleceń systemowych, choć nie jest to wymóg.

#!/bin/bash

# Poprawna deklaracja zmiennej
GREETING="Witaj w bashu"

# Błędna deklaracja - to spowoduje błąd "command not found"
# BAD_VAR = "To nie zadziała"

# Zmienna przechowująca liczbę (nadal jako tekst)
USER_COUNT=10

# Zmienna bez cudzysłowu (możliwe, jeśli wartość nie ma spacji)
HOSTNAME=serwer01

W bashu zmienne nie mają typu w tradycyjnym rozumieniu, co oznacza, że ta sama zmienna może przechowywać raz tekst, a innym razem liczbę. Mimo to warto zachować dyscyplinę i używać zmiennych zgodnie z ich przeznaczeniem. Konwencja zapisu nazw zmiennych wielkimi literami nie jest przypadkowa, ponieważ pomaga odróżnić zmienne własne od poleceń systemowych i zmiennych środowiskowych. Zasada braku spacji wokół znaku równości przy przypisaniu często stanowi źródło błędów początkujących programistów. Każda spacja w bashu ma znaczenie, ponieważ powłoka traktuje słowa oddzielone spacjami jako osobne argumenty. Zmienne można deklarować w dowolnym miejscu skryptu, ale najlepiej robić to na początku, aby kod był czytelny.

Przypisanie wartości do zmiennej bez cudzysłowu jest dozwolone tylko wtedy, gdy wartość nie zawiera spacji ani znaków specjalnych. W przeciwnym razie użycie cudzysłowu jest obowiązkowe. Zrozumienie tych podstawowych zasad pozwoli uniknąć wielu frustrujących błędów w trakcie pisania skryptów.

6Zmienne: Odwoływanie się do wartości

Używanie wartości zmiennych

Aby uzyskać dostęp do wartości przechowywanej w zmiennej, należy poprzedzić jej nazwę znakiem dolara $.

Istnieją dwie formy odwołania:

  • $NAZWA: Prosta forma, wystarczająca w większości przypadków.
  • ${NAZWA}: Forma z nawiasami klamrowymi. Jest ona niezbędna, gdy chcemy oddzielić nazwę zmiennej od otaczającego ją tekstu, który mógłby zostać błędnie zinterpretowany jako część nazwy. Jest to bezpieczniejsza i często zalecana praktyka.

W przykładzie obok, echo "$GREETING" wyświetli zawartość zmiennej, ale echo "GREETING" wyświetli po prostu tekst "GREETING".

#!/bin/bash

IMIE="Alicja"
STANOWISKO="programista"

# Proste odwołanie
echo "Użytkownik: $IMIE"
echo "Stanowisko: $STANOWISKO"

# Przykład, gdzie nawiasy klamrowe są konieczne:
# Chcemy utworzyć tekst "Alicja_programista.log"
# Błędne podejście:
echo "Plik logu: $IMIE_$STANOWISKO.log" 
# bash szuka zmiennej o nazwie IMIE_ (która nie istnieje)

# Poprawne podejście z nawiasami klamrowymi:
echo "Plik logu: ${IMIE}_${STANOWISKO}.log"

Znak dolara przed nazwą zmiennej informuje powłokę, że chcemy odczytać jej wartość, a nie operować na samej nazwie. Nawiasy klamrowe wokół nazwy zmiennej pełnią funkcję separatora, który jednoznacznie określa granice nazwy zmiennej. Jest to szczególnie przydatne, gdy po zmiennej występują litery, cyfry lub znaki podkreślenia, które mogłyby zostać błędnie dopasowane do jej nazwy. Używanie nawiasów klamrowych jest uznawane za dobrą praktykę programistyczną, ponieważ zwiększa czytelność kodu. W przypadku prostych odwołań można stosować formę skróconą bez nawiasów. Błąd polegający na pomyleniu nazwy zmiennej z jej wartością jest jednym z najczęstszych błędów popełnianych przez początkujących.

Przykład z tworzeniem nazwy pliku złożonej z wartości dwóch zmiennych dobrze ilustruje, dlaczego nawiasy klamrowe są potrzebne. Bez nich bash mógłby zinterpretować fragment tekstu jako nazwę nieistniejącej zmiennej, co skutkowałoby błędem lub niezamierzonym zachowaniem skryptu.

7Zmienne: Rola cudzysłowów

Znaczenie cudzysłowów (quoting)

Sposób, w jaki używamy cudzysłowów, ma fundamentalne znaczenie dla działania skryptów. bash interpretuje je na trzy sposoby:

  • Podwójny cudzysłów "...":
    • Zachowuje dosłowne znaczenie większości znaków.
    • Pozwala na interpretację (ekspansję) zmiennych ($NAZWA) i poleceń ($(polecenie)).
    • Spacje wewnątrz są traktowane jako część tekstu.
  • Pojedynczy cudzysłów '...':
    • Traktuje każdy znak dosłownie (literalnie).
    • Żadne zmienne ani polecenia nie są interpretowane. Znak $ jest zwykłym znakiem.
  • Brak cudzysłowu:
    • Powoduje, że bash dzieli tekst na słowa wg spacji, tabulacji i nowych linii (word splitting). Może to prowadzić do nieoczekiwanych błędów, zwłaszcza przy nazwach plików zawierających spacje.

Zasadniczo, najlepszą praktyką jest umieszczanie odwołań do zmiennych w podwójnych cudzysłowach, aby uniknąć problemów.

#!/bin/bash

USER="Jan Kowalski"
FILES_COUNT=$(ls -1 | wc -l) # Podstawienie polecenia, omówimy później

# 1. Podwójny cudzysłów (zalecane)
echo "Użytkownik: $USER. Liczba plików: $FILES_COUNT."
# Wynik: Użytkownik: Jan Kowalski. Liczba plików: 15.
# Zmienne zostały zinterpretowane.

# 2. Pojedynczy cudzysłów (wszystko dosłownie)
echo 'Użytkownik: $USER. Liczba plików: $FILES_COUNT.'
# Wynik: Użytkownik: $USER. Liczba plików: $FILES_COUNT.
# Znaki $ i () potraktowano jak zwykły tekst.

# 3. Brak cudzysłowu (niebezpieczne!)
echo Użytkownik: $USER. Liczba plików: $FILES_COUNT.
# Wynik: Użytkownik: Jan Kowalski. Liczba plików: 15.
# W tym prostym przypadku działa, ale jest to zła praktyka.
# Polecenie 'echo' otrzymało 6 osobnych argumentów.

Cudzysłowy w bashu pełnią znacznie ważniejszą rolę niż w innych językach programowania. Podwójny cudzysłów pozwala na interpretację zmiennych, co jest kluczowe przy tworzeniu komunikatów z dynamicznymi danymi. Pojedynczy cudzysłów zachowuje dosłowną wartość każdego znaku, co jest przydatne, gdy chcemy uniknąć przypadkowej interpretacji znaków specjalnych. Brak cudzysłowu powoduje, że bash wykonuje tak zwane dzielenie na słowa, co może prowadzić do nieoczekiwanych rezultatów przy wartościach zawierających spacje. Dla bezpieczeństwa zaleca się stosowanie podwójnych cudzysłowów za każdym razem, gdy odwołujemy się do zmiennej, chyba że mamy konkretny powód, by tego nie robić. Zasada ta dotyczy również używania zmiennych w warunkach i przekazywania ich jako argumentów do poleceń.

Prawidłowe stosowanie cudzysłowów to jedna z tych umiejętności, która dzieli początkujących od zaawansowanych programistów basha. Warto poświęcić czas na zrozumienie tego mechanizmu, ponieważ błędy związane z cytowaniem są niezwykle trudne do wyśledzenia w większych skryptach.

8Zmienne: Wczytywanie danych od użytkownika

Polecenie read

Aby skrypty były interaktywne, muszą umieć przyjmować dane od użytkownika. Służy do tego wbudowane polecenie read. Czeka ono na wprowadzenie tekstu przez użytkownika i naciśnięcie klawisza Enter.

Najczęściej używane opcje polecenia read:

  • read NAZWA_ZMIENNEJ: Wczytuje linię tekstu i zapisuje ją w zmiennej NAZWA_ZMIENNEJ.
  • read -p "Tekst zachęty: " NAZWA_ZMIENNEJ: Opcja -p (prompt) pozwala wyświetlić tekst zachęty w tej samej linii, w której użytkownik wprowadza dane. Jest to bardziej eleganckie niż używanie echo wcześniej.
  • read -s NAZWA_ZMIENNEJ: Opcja -s (silent) sprawia, że wprowadzany tekst nie jest wyświetlany na ekranie. Idealne do wczytywania haseł.
#!/bin/bash

# Użycie opcji -p do wyświetlenia zachęty
read -p "Wpisz swoje imię: " IMIE

# Użycie opcji -s do wczytania hasła
read -s -p "Wpisz swoje hasło: " HASLO
echo "" # Dodajemy nową linię, bo read -s jej nie dodaje

echo "-----------------------------------"
echo "Witaj, $IMIE!"
# Oczywiście, w realnym skrypcie NIGDY nie wyświetlamy hasła!
# To tylko demonstracja, że zmienna je przechowała.
echo "Twoje hasło zostało wczytane i ma ${#HASLO} znaków."

Polecenie read jest wbudowanym poleceniem basha, co oznacza, że nie wymaga uruchamiania zewnętrznego programu. Dzięki temu działa bardzo szybko i jest dostępne zawsze, gdy używamy powłoki bash. Opcja -p znacząco podnosi użyteczność skryptów, ponieważ pozwala na wyświetlenie czytelnej zachęty dla użytkownika. Opcja -s jest niezbędna przy wczytywaniu haseł, ponieważ zapobiega wyświetlaniu poufnych danych na ekranie. Warto pamiętać, że po użyciu read -s należy samodzielnie dodać znak nowej linii, aby zachować czytelność interfejsu. Polecenie read może wczytać wiele zmiennych jednocześnie, dzieląc wprowadzony tekst na pola według białych znaków. Możliwe jest również dostosowanie separatora pól za pomocą zmiennej IFS.

Interaktywne skrypty z użyciem read są znacznie bardziej przyjazne dla użytkownika końcowego niż skrypty wymagające ręcznej edycji plików konfiguracyjnych. Jednak w środowiskach produkcyjnych częściej stosuje się przekazywanie danych przez argumenty wywołania lub pliki konfiguracyjne, co pozwala na w pełni automatyczne działanie bez nadzoru człowieka.

9Zmienne: Podstawienie polecenia

Zapisywanie wyniku polecenia do zmiennej

Bardzo użyteczną cechą powłok jest możliwość wykonania polecenia i przechwycenia jego standardowego wyjścia (tego, co normalnie pojawiłoby się na ekranie) do zmiennej. Nazywa się to podstawieniem polecenia (command substitution).

Istnieją dwie składnie, które robią dokładnie to samo:

  • NAZWA=$(polecenie): Nowoczesna, zalecana składnia z użyciem $(). Jest czytelniejsza i można ją zagnieżdżać (np. VAR=$(polecenie1 $(polecenie2))).
  • NAZWA=`polecenie`: Starsza składnia z użyciem lewych odwrotnych apostrofów. Należy jej unikać w nowym kodzie, ponieważ jest mniej czytelna i trudniej ją zagnieżdżać.

W przykładzie obok, wynik polecenia date jest przechwytywany do zmiennej, co pozwala na późniejsze użycie go w logach lub nazwach plików.

#!/bin/bash

# Nowoczesna, zalecana składnia
AKTUALNA_DATA=$(date "+%Y-%m-%d %H:%M:%S")
LISTA_PLIKOW=$(ls)
BIEŻĄCY_KATALOG=$(pwd)

echo "Log wygenerowany dnia: $AKTUALNA_DATA"
echo "Jesteś w katalogu: $BIEŻĄCY_KATALOG"
echo "Pliki w tym katalogu to:"
echo "$LISTA_PLIKOW"

# Starsza składnia (działa, ale unikaj)
KERNEL_VERSION=`uname -r`
echo "Wersja jądra systemu: $KERNEL_VERSION"

Podstawienie polecenia to jeden z najważniejszych mechanizmów basha, umożliwiający wykorzystanie wyniku działania jednego polecenia jako danych dla innego polecenia. Nowoczesna składnia $() jest preferowana ze względu na czytelność i możliwość zagnieżdżania bez konieczności uciekania znaków. Starsza składnia z apostrofami, choć wciąż spotykana w starszych skryptach, jest trudniejsza w utrzymaniu i bardziej podatna na błędy. Wynik polecenia może być wykorzystany w dowolnym miejscu skryptu, nie tylko przy przypisaniu do zmiennej. Złożone konstrukcje z wieloma zagnieżdżonymi podstawieniami pozwalają na tworzenie zaawansowanych transformacji danych. Trzeba jednak zachować umiar, ponieważ nadmierne zagnieżdżanie może obniżyć czytelność kodu.

W praktyce podstawienie polecenia jest używane na przykład do przechwytywania aktualnej daty, listy plików, wyników zapytań do baz danych czy stanu systemu. Mechanizm ten świetnie współpracuje z potokami i przekierowaniami, tworząc elastyczne łańcuchy przetwarzania danych. Warto przećwiczyć różne kombinacje, aby w pełni opanować tę umiejętność.

10Zmienne: Zmienne wbudowane i środowiskowe

Predefiniowane zmienne

bash udostępnia wiele wbudowanych zmiennych, które dostarczają informacji o środowisku pracy, systemie i samym skrypcie. Są one niezwykle przydatne.

Najważniejsze zmienne środowiskowe:

  • $USER: Nazwa zalogowanego użytkownika.
  • $HOME: Ścieżka do katalogu domowego użytkownika.
  • $PWD: Bieżący katalog roboczy (Print Working Directory).
  • $HOSTNAME: Nazwa hosta (komputera).
  • $PATH: Lista katalogów, w których powłoka szuka programów do uruchomienia.

Zmienne specjalne bash:

  • $$: Identyfikator procesu (PID) bieżącej powłoki.
  • $?: Kod wyjścia (exit code) ostatnio wykonanego polecenia (0 = sukces, >0 = błąd).
#!/bin/bash

echo "Witaj, $USER!"
echo "Twój katalog domowy to: $HOME"
echo "Aktualnie znajdujesz się w: $PWD"
echo "Pracujesz na maszynie o nazwie: $HOSTNAME"
echo ""
echo "Identyfikator tego procesu to: $$"

# Sprawdzenie kodu wyjścia
ls /etc/passwd > /dev/null # To polecenie się powiedzie
echo "Kod wyjścia po udanym 'ls': $?"

ls /nieistniejacy/plik > /dev/null 2>&1 # To polecenie się nie powiedzie
echo "Kod wyjścia po nieudanym 'ls': $?"

Zmienne środowiskowe są dziedziczone przez procesy potomne, co pozwala na przekazywanie konfiguracji między programami bez używania plików. Zmienne wbudowane, takie jak $? czy $$, są automatycznie dostępne w każdym skrypcie bash i dostarczają cennych informacji o stanie wykonania. Zmienna $PATH jest szczególnie ważna, ponieważ określa, w których katalogach powłoka szuka programów do uruchomienia. Rozszerzenie zmiennej PATH o własne katalogi z programami to częsty zabieg w środowiskach deweloperskich. Zmienna $? pozwala na sprawdzenie, czy poprzednie polecenie zakończyło się sukcesem, co jest fundamentem obsługi błędów w skryptach. Zmienna $$ umożliwia identyfikację procesu, co jest przydatne przy tworzeniu unikalnych nazw plików tymczasowych lub logów.

Warto zapamiętać, że zmienne środowiskowe można definiować samodzielnie za pomocą polecenia export, co czyni je dostępnymi dla wszystkich procesów potomnych. Bez exportu zmienna ma zasięg lokalny i jest widoczna tylko w bieżącej powłoce.

11Zmienne: Zmienne tylko do odczytu (stałe)

Polecenie readonly

Chociaż bash nie ma formalnego konceptu stałych znanego z innych języków programowania, można zadeklarować zmienną jako "tylko do odczytu" za pomocą polecenia readonly. Po takiej deklaracji każda próba zmiany wartości tej zmiennej zakończy się błędem, ale skrypt będzie kontynuował działanie, chyba że włączono opcję set -e.

Jest to przydatne do definiowania stałych wartości konfiguracyjnych w skrypcie, które nie powinny być przypadkowo zmienione w dalszej części kodu.

Można najpierw zadeklarować zmienną, a potem uczynić ją tylko do odczytu, lub zrobić to w jednej linii.

#!/bin/bash

# Sposób 1: Najpierw deklaracja, potem 'readonly'
LOG_DIR="/var/log/myapp"
readonly LOG_DIR

# Sposób 2: Deklaracja i 'readonly' w jednej linii
readonly DB_HOST="192.168.1.100"

echo "Katalog logów: $LOG_DIR"
echo "Host bazy danych: $DB_HOST"

# Próba zmiany wartości zmiennej readonly
echo "Próbuję zmienić LOG_DIR..."
LOG_DIR="/tmp/logs" 

# Powyższa linia spowoduje błąd w trakcie wykonania skryptu:
# ./script.sh: line 17: LOG_DIR: readonly variable
# Skrypt kontynuuje działanie (chyba że włączono set -e).

echo "Ta linia wyświetli się mimo błędu (bez set -e)."

Mimo że bash nie wspiera stałych na poziomie języka, polecenie readonly skutecznie zabezpiecza zmienne przed przypadkową modyfikacją. Jest to szczególnie przydatne w przypadku wartości konfiguracyjnych, takich jak ścieżki do katalogów, adresy serwerów czy progi liczbowe. Próba zmiany zmiennej readonly skutkuje komunikatem błędu i domyślnie nie przerywa działania skryptu, chyba że włączono opcję set -e. Używanie readonly stanowi formę dokumentacji kodu, informując innych programistów, które wartości są niezmienne. W dłuższych skryptach zabezpieczenie kluczowych zmiennych przed nadpisaniem znacząco ułatwia debugowanie i utrzymanie kodu. Deklaracja readonly może nastąpić w dowolnym momencie wykonania skryptu, ale najlepiej robić to zaraz po przypisaniu wartości.

W niektórych skryptach spotyka się również polecenie declare -r jako alternatywny sposób deklarowania zmiennych tylko do odczytu. Efekt działania jest identyczny jak w przypadku readonly, a wybór składni zależy głównie od preferencji programisty i przyjętych w zespole konwencji.

12Wyrażenia arytmetyczne

Operacje na liczbach

Ponieważ bash domyślnie traktuje wszystko jako tekst, do wykonywania operacji matematycznych potrzebujemy specjalnej składni. Istnieje kilka sposobów, ale dwa są najpopularniejsze:

  • Składnia $((...)) (Arithmetic Expansion):
    • Nowoczesna, zalecana i najbardziej czytelna metoda.
    • Wewnątrz podwójnych nawiasów okrągłych bash traktuje zmienne jako liczby całkowite. Nie trzeba nawet używać znaku $ przed nazwami zmiennych.
    • Dostępne operatory: +, -, *, / (dzielenie całkowite), % (reszta z dzielenia, modulo), ** (potęgowanie).
  • Polecenie let:
    • Starsza metoda, która modyfikuje wartość zmiennej.
    • Składnia: let "WYNIK = a + b". Cudzysłów jest potrzebny, jeśli w wyrażeniu są spacje.

Uwaga: bash domyślnie operuje tylko na liczbach całkowitych! Do obliczeń zmiennoprzecinkowych potrzebne są zewnętrzne narzędzia, takie jak bc.

#!/bin/bash

A=10
B=3

# Użycie składni $((...))
SUMA=$((A + B))
ROZNICA=$((A - B))
ILOCZYN=$((A * B))
ILORAZ=$((A / B))   # Wynik to 3, bo to dzielenie całkowite
RESZTA=$((A % B))   # Wynik to 1
POTEGA=$((A ** B))  # 10 do potęgi 3

echo "$A + $B = $SUMA"
echo "$A / $B = $ILORAZ (iloraz) i $RESZTA (reszta)"
echo "$A do potęgi $B = $POTEGA"

# Inkrementacja w stylu C
LICZNIK=0
LICZNIK=$((LICZNIK + 1))
echo "Licznik: $LICZNIK"

# Użycie 'let'
let LICZNIK=LICZNIK+1
echo "Licznik po 'let': $LICZNIK"

Domyślne traktowanie wszystkich wartości jako ciągów znaków jest jedną z cech charakterystycznych basha, która odróżnia go od tradycyjnych języków programowania. Składnia $((...)) została wprowadzona, aby umożliwić wygodne wykonywanie operacji arytmetycznych bez uciekania się do zewnętrznych narzędzi. Wewnątrz podwójnych nawiasów okrągłych bash automatycznie rozpoznaje zmienne liczbowe i stosuje odpowiednie operatory. Należy pamiętać, że bash obsługuje wyłącznie liczby całkowite, a wynik dzielenia jest zawsze zaokrąglany w dół. Operator modulo (%) jest szczególnie przydatny przy sprawdzaniu parzystości liczb czy tworzeniu cyklicznych wzorców. Potęgowanie za pomocą ** działa zgodnie z oczekiwaniami, ale przy dużych wykładnikach może prowadzić do przekroczenia zakresu liczb całkowitych.

Polecenie let, choć starsze, wciąż bywa używane w istniejących skryptach. Nowe skrypty lepiej pisać z użyciem składni $((...)), która jest czytelniejsza i mniej podatna na błędy składniowe. Warto również znać składnię z inkrementacją w stylu C, ponieważ jest często spotykana w pętlach.

13Operacje na liczbach zmiennoprzecinkowych (bc)

Ograniczenia arytmetyki bash

Jak wspomniano, bash nie obsługuje liczb zmiennoprzecinkowych. Próba wykonania takiej operacji zakończy się błędem składniowym.

Narzędzie bc (Basic Calculator)

Aby obejść to ograniczenie, używamy zewnętrznego programu kalkulatora linii poleceń o nazwie bc. Możemy przekazać mu wyrażenie do obliczenia za pomocą potoku (`|`) i polecenia `echo`.

  • echo "wyrażenie" | bc: Przekazuje wyrażenie do bc.
  • Opcja -l dla bc włącza bibliotekę matematyczną, która zapewnia obsługę funkcji trygonometrycznych i ustawia domyślną precyzję (liczbę miejsc po przecinku).
#!/bin/bash

# Próba operacji zmiennoprzecinkowej w bashu (spowoduje błąd)
# WYNIK=$((5 / 2.5)) 
# -> bash: 5 / 2.5: syntax error: invalid arithmetic operator

# Poprawne użycie `bc`
LICZBA1=10.5
LICZBA2=2

WYNIK=$(echo "$LICZBA1 * $LICZBA2" | bc)
echo "$LICZBA1 * $LICZBA2 = $WYNIK"

# Dzielenie z większą precyzją
# 'scale' ustawia liczbę miejsc po przecinku
PRECYZYJNY_WYNIK=$(echo "scale=4; 10 / 3" | bc)
echo "10 / 3 z precyzją do 4 miejsc: $PRECYZYJNY_WYNIK"

# Użycie biblioteki matematycznej (-l) do obliczenia sinusa
SINUS_1=$(echo "s(1)" | bc -l)
echo "Sinus(1) = $SINUS_1"

Ograniczenie basha do liczb całkowitych wynika z jego konstrukcji jako powłoki systemowej, a nie pełnoprawnego języka programowania. Program bc, który jest preprocesorem i kalkulatorem, wypełnia tę lukę, oferując obsługę liczb zmiennoprzecinkowych z dowolną precyzją. Ustawienie zmiennej scale przed wykonaniem obliczeń pozwala kontrolować liczbę miejsc po przecinku w wyniku. Opcja -l w bc nie tylko włącza bibliotekę matematyczną, ale także ustawia domyślną precyzję na 20 miejsc po przecinku. Funkcje matematyczne, takie jak sinus, cosinus czy logarytm, są dostępne wyłącznie po włączeniu tej biblioteki. W praktyce bc jest często używany w skryptach do obliczeń związanych z konwersją jednostek, obliczaniem czasu trwania operacji czy dostosowywaniem parametrów systemowych.

Warto pamiętać, że bc to osobny program, dlatego każde wywołanie wiąże się z uruchomieniem nowego procesu. W przypadku wykonywania wielu obliczeń w pętli może to znacząco spowolnić działanie skryptu, dlatego warto rozważyć grupowanie wyrażeń w jednym wywołaniu bc.

14Operatory porównania: Liczby

Porównywanie liczb całkowitych

Do porównywania liczb w instrukcjach warunkowych (które omówimy szczegółowo za chwilę) używamy specjalnych operatorów, a nie symboli < czy >. Te symbole mają w bash inne znaczenie (przekierowanie strumieni).

Operatory te działają wewnątrz konstrukcji [ ... ] (polecenie `test`) lub, co jest zalecane, [[ ... ]] (słowo kluczowe bash).

Główne operatory liczbowe:

  • -eq : (equal) - równy
  • -ne : (not equal) - nierówny
  • -gt : (greater than) - większy niż
  • -ge : (greater or equal) - większy lub równy
  • -lt : (less than) - mniejszy niż
  • -le : (less or equal) - mniejszy lub równy
#!/bin/bash

read -p "Podaj swój wiek: " WIEK

# Sprawdzenie, czy wiek jest liczbą (prosty przykład z wyrażeniem regularnym)
if ! [[ "$WIEK" =~ ^[0-9]+$ ]]; then
    echo "Błąd: Wprowadzona wartość nie jest liczbą."
    exit 1
fi

# Porównania liczbowe
if [[ "$WIEK" -ge 18 ]]; then
    echo "Jesteś osobą pełnoletnią."
else
    echo "Nie jesteś jeszcze osobą pełnoletnią."
fi

if [[ "$WIEK" -eq 42 ]]; then
    echo "To jest odpowiedź na wielkie pytanie o życie, wszechświat i całą resztę."
fi

Operatory porównania liczb w bashu różnią się od tych znanych z języków takich jak C czy Java, co często bywa źródłem nieporozumień. Symbole < i > mają w bashu specjalne znaczenie jako operatory przekierowania strumieni, dlatego do porównań używa się literowych odpowiedników. Konstrukcja [[ ... ]] jest słowem kluczowym basha, a nie poleceniem zewnętrznym, co czyni ją szybszą i bardziej elastyczną od starszej konstrukcji [ ... ]. Wewnątrz [[ ... ]] można używać operatorów takich jak == dla dopasowania wzorców. Porównania liczbowe są często używane w pętlach while do kontrolowania liczby iteracji. Operator -eq różni się od == tym, że porównuje wartości liczbowe, podczas gdy == porównuje ciągi znaków, co przy liczbach daje ten sam efekt.

Wybór między konstrukcją test [ ... ] a [[ ... ]] ma znaczenie nie tylko dla czytelności, ale także dla bezpieczeństwa. Wewnątrz [[ ... ]] można bezpiecznie używać zmiennych, nawet jeśli zawierają spacje lub są puste, bez ryzyka błędów składniowych.

15Operatory porównania: Ciągi znaków

Porównywanie tekstu

Do porównywania ciągów znaków używamy innego zestawu operatorów. Ważne jest, aby podczas porównań umieszczać zmienne w cudzysłowach, aby uniknąć problemów z pustymi zmiennymi lub spacjami.

Główne operatory tekstowe (najlepiej używać wewnątrz [[ ... ]]):

  • == : równy (= jest synonimem wewnątrz [[...]], ale == jest czytelniejsze).
  • != : nierówny.
  • < : mniejszy (leksykograficznie, czyli wg kolejności alfabetycznej).
  • > : większy (leksykograficznie).
  • -z "ciąg" : prawda, jeśli ciąg jest pusty.
  • -n "ciąg" : prawda, jeśli ciąg nie jest pusty.
#!/bin/bash

read -p "Wpisz 'tak' lub 'nie': " ODPOWIEDZ

# Porównanie ciągu znaków
if [[ "$ODPOWIEDZ" == "tak" ]]; then
    echo "Wybrano opcję twierdzącą."
elif [[ "$ODPOWIEDZ" == "nie" ]]; then
    echo "Wybrano opcję przeczącą."
else
    echo "Nieznana odpowiedź."
fi

# Sprawdzenie, czy zmienna jest pusta
read -p "Wpisz swoje nazwisko (lub zostaw puste): " NAZWISKO

if [[ -z "$NAZWISKO" ]]; then
    echo "Nie podano nazwiska."
else
    echo "Podane nazwisko to: $NAZWISKO"
fi

Porównywanie ciągów znaków w bashu wymaga szczególnej uwagi, ponieważ niektóre operatory mogą zachowywać się inaczej niż w innych językach. Operator == wewnątrz [[ ... ]] umożliwia dopasowanie wzorca z użyciem wyrażeń globalnych, takich jak *, co jest niezwykle przydatne przy przetwarzaniu nazw plików. Operator < i > dla ciągów znaków wykonuje porównanie leksykograficzne, czyli sprawdza kolejność alfabetyczną zgodną z ustawieniami lokalnymi systemu. Sprawdzanie, czy zmienna jest pusta za pomocą -z, jest jednym z najczęściej wykonywanych testów w skryptach bash, zwłaszcza przy walidacji danych wejściowych. Operator -n jest przydatny do upewnienia się, że zmienna zawiera jakąś wartość przed jej użyciem. Należy pamiętać o umieszczaniu zmiennych w cudzysłowach podczas porównań, aby uniknąć błędów przy pustych zmiennych.

W przypadku porównań ciągów wewnątrz [ ... ] zamiast [[ ... ]] operator == nie jest dostępny i trzeba używać pojedynczego znaku =. To kolejny argument za używaniem nowoczesnej składni [[ ... ]] w nowych skryptach.

16Operatory logiczne

Łączenie warunków

Często musimy sprawdzić więcej niż jeden warunek naraz. Do tego służą operatory logiczne.

W nowoczesnej składni [[ ... ]] używamy operatorów znanych z innych języków:

  • && - AND (koniunkcja): Całe wyrażenie jest prawdziwe tylko wtedy, gdy oba warunki są prawdziwe.
  • || - OR (alternatywa): Całe wyrażenie jest prawdziwe, jeśli przynajmniej jeden z warunków jest prawdziwy.
  • ! - NOT (negacja): Odwraca wynik warunku (prawda staje się fałszem, a fałsz prawdą).

W starszej składni [ ... ] używa się opcji -a (AND) i -o (OR), ale jest ona mniej czytelna i podatna na błędy, dlatego należy preferować [[ ... ]].

#!/bin/bash

read -p "Podaj swój wiek: " WIEK
read -p "Czy posiadasz prawo jazdy? (tak/nie): " PRAWKO

# Operator AND (&&)
if [[ "$WIEK" -ge 18 && "$PRAWKO" == "tak" ]]; then
    echo "Możesz prowadzić samochód."
else
    echo "Nie spełniasz warunków do prowadzenia samochodu."
fi

# Operator OR (||)
read -p "Podaj nazwę użytkownika: " USERNAME

if [[ "$USERNAME" == "admin" || "$USERNAME" == "root" ]]; then
    echo "Witaj, administratorze!"
fi

# Operator NOT (!)
read -p "Czy chcesz zakończyć? (tak/nie): " KONIEC

if ! [[ "$KONIEC" == "tak" ]]; then
    echo "Kontynuujemy pracę..."
fi

Operatory logiczne w bashu pozwalają na budowanie złożonych warunków z kilku prostszych wyrażeń testowych. Operator && (AND) wymaga spełnienia wszystkich warunków jednocześnie, co jest przydatne na przykład przy sprawdzaniu kilku wymagań przed wykonaniem krytycznej operacji. Operator || (OR) wystarcza, by przynajmniej jeden warunek był spełniony, co daje większą elastyczność w podejmowaniu decyzji. Negacja ! odwraca wynik warunku, co jest szczególnie przydatne przy sprawdzaniu braku czegoś, na przykład nieistnienia pliku. W konstrukcji [[ ... ]] operatory && i || działają zgodnie z zasadą leniwej oceny, co oznacza, że bash nie sprawdza kolejnych warunków, jeśli wynik jest już znany. Łączenie operatorów pozwala na tworzenie bardzo złożonych wyrażeń warunkowych.

Należy unikać starszej składni z opcjami -a i -o wewnątrz [ ... ], ponieważ jest ona mniej czytelna i może prowadzić do błędów przy złożonych warunkach. Nowoczesna składnia [[ ... ]] jest bezpieczniejsza i bardziej intuicyjna dla programistów znających inne języki.

17Operacje na plikach i katalogach

Sprawdzanie atrybutów plików

Jednym z najczęstszych zadań w skryptach jest sprawdzanie istnienia plików, ich typu lub uprawnień. bash dostarcza do tego celu zestaw operatorów (tzw. "testów"), których używamy w instrukcjach warunkowych.

Najważniejsze operatory plików:

  • -e ścieżka: prawda, jeśli ścieżka istnieje (plik lub katalog).
  • -f ścieżka: prawda, jeśli ścieżka jest zwykłym plikiem.
  • -d ścieżka: prawda, jeśli ścieżka jest katalogiem.
  • -s ścieżka: prawda, jeśli plik istnieje i ma rozmiar większy od zera.
  • -r ścieżka: prawda, jeśli plik jest odczytywalny.
  • -w ścieżka: prawda, jeśli plik jest zapisywalny.
  • -x ścieżka: prawda, jeśli plik jest wykonywalny.
#!/bin/bash

PLIK_KONFIGuracyjny="/etc/resolv.conf"
KATALOG_DOMOWY="$HOME"
PLIK_NIEISTNIEJACY="/tmp/tego_pliku_na_pewno_nie_ma"

# Sprawdzenie, czy plik istnieje i jest plikiem
if [[ -f "$PLIK_KONFIGuracyjny" ]]; then
    echo "Plik konfiguracyjny '$PLIK_KONFIGuracyjny' istnieje."

    # Sprawdzenie, czy jest odczytywalny
    if [[ -r "$PLIK_KONFIGuracyjny" ]]; then
        echo "I można go odczytać."
    fi
else
    echo "Brak pliku konfiguracyjnego."
fi

# Sprawdzenie, czy katalog istnieje
if [[ -d "$KATALOG_DOMOWY" ]]; then
    echo "Katalog domowy '$KATALOG_DOMOWY' istnieje."
fi

# Sprawdzenie nieistniejącego pliku
if [[ -e "$PLIK_NIEISTNIEJACY" ]]; then
    echo "To się nie powinno wyświetlić."
else
    echo "Plik '$PLIK_NIEISTNIEJACY' nie istnieje."
fi

Operatory testowania plików w bashu umożliwiają sprawdzenie praktycznie dowolnego atrybutu pliku lub katalogu przed wykonaniem na nim operacji. Jest to niezwykle ważne z perspektywy tworzenia niezawodnych skryptów, ponieważ zapobiega próbom odczytu nieistniejących plików czy zapisu do katalogów bez odpowiednich uprawnień. Operator -e sprawdza samo istnienie ścieżki, niezależnie od jej typu, podczas gdy -f i -d precyzują, czy chodzi o plik czy katalog. Testowanie uprawnień za pomocą -r, -w, -x pozwala na wcześniejsze wykrycie potencjalnych problemów z dostępem. Operator -s jest przydatny przy przetwarzaniu plików, ponieważ plik o zerowym rozmiarze zazwyczaj nie zawiera interesujących nas danych. Testy plików działają zarówno dla ścieżek względnych, jak i bezwzględnych.

W codziennej pracy testy plików są używane w skryptach instalacyjnych, backupowych i monitorujących. Dobrą praktyką jest sprawdzanie każdego pliku przed próbą jego odczytu lub zapisu, co znacząco podnosi niezawodność skryptu i ułatwia diagnozowanie problemów.

18Instrukcja warunkowa `if`

Podejmowanie decyzji w skrypcie

Instrukcja if pozwala na wykonanie określonego bloku kodu tylko wtedy, gdy zadany warunek jest spełniony (czyli polecenie testowe zwraca kod wyjścia 0).

Podstawowa składnia

Każda instrukcja if musi być zakończona słowem kluczowym fi (czyli `if` czytane od tyłu).

if [[ warunek ]]; then
    # Blok kodu do wykonania,
    # jeśli warunek jest prawdziwy
fi

Warunek jest zazwyczaj wyrażeniem testowym umieszczonym w [[ ... ]], ale technicznie może to być dowolne polecenie. Blok then wykona się, jeśli polecenie zakończy się sukcesem (kod wyjścia 0).

#!/bin/bash

echo "Sprawdzam połączenie z google.com..."

# Polecenie 'ping' zwraca 0, jeśli host odpowie.
# -c 1 -> wyślij 1 pakiet
# > /dev/null -> ukryj wyjście polecenia
if ping -c 1 google.com > /dev/null; then
    echo "Połączenie z internetem jest aktywne."
fi

# Użycie warunku z operatorem testowym
read -p "Podaj liczbę: " LICZBA
if [[ "$LICZBA" -gt 100 ]]; then
    echo "Podana liczba jest większa od 100."
fi

Instrukcja if jest podstawową konstrukcją sterującą w bashu, umożliwiającą wykonanie kodu tylko po spełnieniu określonego warunku. Warunkiem może być dowolne polecenie, a nie tylko wyrażenie testowe, co daje dużą elastyczność. Jeśli polecenie zwróci kod wyjścia 0 (sukces), blok then zostanie wykonany. Każda instrukcja if musi być zakończona słowem fi, co jest charakterystyczne dla składni basha. Wewnątrz bloku then można umieścić dowolną liczbę poleceń, w tym kolejne instrukcje warunkowe. Warto pamiętać o odpowiednim wcięciu kodu, aby struktura warunków była czytelna. bash nie wymaga wcięć, ale są one nieocenione przy utrzymaniu kodu.

Typowym błędem początkujących jest zapominanie o spacji po [[ i przed ]], co powoduje błąd składniowy. Również średnik przed then jest wymagany, gdy then znajduje się w tej samej linii co warunek, co jest najczęstszą praktyką w skryptach bash.

19Instrukcja warunkowa `if-else`

Obsługa alternatywnych ścieżek

Często chcemy wykonać jeden blok kodu, gdy warunek jest prawdziwy, i inny blok, gdy jest fałszywy. Do tego służy konstrukcja if-else.

Składnia

if [[ warunek ]]; then
    # Blok 'then' - wykonuje się,
    # gdy warunek jest prawdziwy
else
    # Blok 'else' - wykonuje się,
    # gdy warunek jest fałszywy
fi
#!/bin/bash

# Sprawdzenie, czy skrypt jest uruchomiony jako root (użytkownik o UID 0)
# Zmienna $EUID przechowuje "efektywny" ID użytkownika

if [[ "$EUID" -eq 0 ]]; then
    echo "Skrypt uruchomiony z uprawnieniami administratora (root)."
    # Tutaj można umieścić operacje wymagające sudo, np. instalację oprogramowania
    # apt-get update
else
    echo "Skrypt uruchomiony jako zwykły użytkownik."
    echo "Niektóre operacje mogą wymagać podwyższonych uprawnień."
fi

Konstrukcja if-else wprowadza możliwość wykonania alternatywnego bloku kodu, gdy warunek nie jest spełniony, co czyni skrypty bardziej elastycznymi. W praktyce większość decyzji w skryptach sprowadza się do wyboru między dwoma ścieżkami, właśnie tak jak w if-else. Sprawdzanie, czy użytkownik ma odpowiednie uprawnienia, to klasyczny przykład użycia if-else w skryptach administracyjnych. Zmienna $EUID przechowuje identyfikator użytkownika, przy czym wartość 0 oznacza roota. Dzięki if-else skrypt może odpowiednio zareagować zarówno w przypadku uruchomienia z uprawnieniami, jak i bez nich. Blok else jest opcjonalny, ale jego dodanie często poprawia czytelność i kompletność logiki skryptu.

W przypadku prostych warunków warto rozważyć użycie operatorów && i || jako skróconej formy if-else. Na przykład: [[ -f plik ]] && echo "istnieje" || echo "nie istnieje" działa podobnie jak if-else, ale jest zwarte i czytelne dla prostych przypadków.

20Instrukcja warunkowa `if-elif-else`

Sprawdzanie wielu warunków

Gdy musimy sprawdzić serię warunków jeden po drugim, używamy konstrukcji if-elif-else. elif to skrót od ang. else if. bash sprawdzi warunki w podanej kolejności i wykona blok kodu dla pierwszego prawdziwego warunku. Jeśli żaden warunek nie będzie prawdziwy, wykona się opcjonalny blok else.

Składnia

if [[ warunek1 ]]; then
    # Blok 1
elif [[ warunek2 ]]; then
    # Blok 2
elif [[ warunek3 ]]; then
    # Blok 3
else
    # Blok 'else'
fi
#!/bin/bash

read -p "Podaj ocenę (1-6): " OCENA

if [[ "$OCENA" -eq 6 ]]; then
    echo "Celujący!"
elif [[ "$OCENA" -eq 5 ]]; then
    echo "Bardzo dobry."
elif [[ "$OCENA" -eq 4 ]]; then
    echo "Dobry."
elif [[ "$OCENA" -eq 3 ]]; then
    echo "Dostateczny."
elif [[ "$OCENA" -eq 2 ]]; then
    echo "Dopuszczający."
elif [[ "$OCENA" -eq 1 ]]; then
    echo "Niedostateczny."
else
    echo "Nieprawidłowa wartość. Podaj liczbę od 1 do 6."
fi

Konstrukcja if-elif-else pozwala na sprawdzenie dowolnej liczby warunków w jednej strukturze, co jest bardziej czytelne niż zagnieżdżanie wielu instrukcji if. Warunki są sprawdzane w kolejności od góry do dołu, a wykonany zostaje tylko blok dla pierwszego prawdziwego warunku. Po wykonaniu jednego bloku reszta warunków jest pomijana, co zapobiega przypadkowemu wykonaniu kilku bloków naraz. Blok else na końcu jest opcjonalny i wykonuje się, gdy żaden z warunków nie był prawdziwy. Liczba bloków elif jest praktycznie nieograniczona, ale przy bardzo długich łańcuchach warto rozważyć użycie case. Przykład z ocenami szkolnymi dobrze ilustruje działanie elif, ponieważ mamy wiele możliwych wartości tej samej zmiennej.

Przy projektowaniu skryptów warto zastanowić się nad kolejnością warunków. Najczęściej występujące przypadki należy umieszczać na początku, co przyspiesza działanie skryptu. Również bardziej restrykcyjne warunki powinny być sprawdzane wcześniej, aby uniknąć błędów.

21Instrukcja `case`

Alternatywa dla `if-elif`

Gdy mamy długą serię warunków elif, które sprawdzają tę samą zmienną pod kątem różnych wartości, kod może stać się nieczytelny. W takich sytuacjach lepszym rozwiązaniem jest instrukcja case.

Porównuje ona wartość zmiennej z listą wzorców. Wykonywany jest blok kodu dla pierwszego pasującego wzorca.

Składnia

  • Każdy blok wzorca musi kończyć się podwójnym średnikiem ;;.
  • Wzorzec *) działa jak domyślny blok else - pasuje do wszystkiego.
  • Można używać symbolu | do łączenia kilku wzorców w jeden.
#!/bin/bash

read -p "Co chcesz zrobić? (start|stop|restart): " AKCJA

case "$AKCJA" in
    start)
        echo "Uruchamianie usługi..."
        # /etc/init.d/apache2 start
        ;;
    stop)
        echo "Zatrzymywanie usługi..."
        # /etc/init.d/apache2 stop
        ;;
    restart|reload) # Dwa wzorce dla tego samego bloku
        echo "Restartowanie usługi..."
        # /etc/init.d/apache2 restart
        ;;
    *) # Wzorzec domyślny
        echo "Nieznana akcja. Użyj: start, stop lub restart."
        exit 1
        ;;
esac # 'case' czytane od tyłu

Instrukcja case jest idealnym rozwiązaniem, gdy zmienna może przyjąć kilka znanych wartości i dla każdej z nich chcemy wykonać inne działania. W przeciwieństwie do łańcucha elif, case jest bardziej czytelny i łatwiejszy w utrzymaniu. Każdy wzorzec w case może zawierać symbole wieloznaczne, takie jak * czy ?, co umożliwia grupowanie podobnych wartości. Wzorzec *) pełni funkcję domyślną i powinien zawsze znajdować się na końcu listy. Łączenie wzorców za pomocą | pozwala na przypisanie tego samego bloku kodu do kilku różnych wartości. Każdy blok kodu w case musi być zakończony dwoma średnikami ;;, co jest charakterystyczne tylko dla tej konstrukcji. Instrukcja case jest często używana w skryptach startowych i menu wyboru.

Zakończenie case słowem esac (czyli case od tyłu) jest typowym dla basha zabiegiem, który spotykamy również w przypadku if/fi. Warto zwrócić uwagę na tę konsekwencję, gdyż ułatwia zapamiętanie składni.

22Pętla `for` - iteracja po liście

Wykonywanie operacji na kolekcji elementów

Pętla for to jedna z najważniejszych struktur kontrolnych. Pozwala na iterowanie po liście elementów (słów, liczb, nazw plików) i wykonywanie dla każdego z nich tego samego bloku kodu.

Składnia

for ZMIENNA in element1 element2 element3 ...; do
    # Blok kodu, który wykona się dla każdego elementu.
    # Wartość bieżącego elementu jest dostępna
    # w zmiennej $ZMIENNA.
done

Lista elementów może być zdefiniowana bezpośrednio w kodzie, pochodzić ze zmiennej lub być wynikiem polecenia.

#!/bin/bash

# Iteracja po zdefiniowanej liście serwerów
echo "Sprawdzanie statusu serwerów:"
for SERWER in web01 db01 cache01 monitoring; do
    echo " - Pinging $SERWER..."
    # ping -c 1 "$SERWER" > /dev/null
done

echo ""

# Iteracja po liście plików zwróconej przez polecenie
# Gwiazdka (*) jest rozwijana przez bash do listy wszystkich
# plików i katalogów w bieżącej lokalizacji.
echo "Pliki w bieżącym katalogu:"
for PLIK in *; do
    if [[ -f "$PLIK" ]]; then
        echo "  - Plik: $PLIK"
    elif [[ -d "$PLIK" ]]; then
        echo "  - Katalog: $PLIK"
    fi
done

Pętla for w bashu różni się od pętli for w językach takich jak C czy Java, ponieważ domyślnie iteruje po liście wartości, a nie po zakresie liczbowym. Lista wartości może być podana jawnie, pochodzić z rozwinięcia zmiennej lub być wynikiem wykonania polecenia. W każdej iteracji zmienna sterująca przyjmuje kolejną wartość z listy i jest dostępna wewnątrz pętli. Pętla for jest szczególnie przydatna przy przetwarzaniu list plików, użytkowników czy serwerów. Warto pamiętać, że jeśli lista zawiera elementy ze spacjami, należy odpowiednio cytować zmienne w pętli. Rozwinięcie globbingowe, takie jak *, jest wykonywane przez powłokę przed rozpoczęciem pętli, co daje listę pasujących plików.

Pętla for świetnie współpracuje z potokami i podstawianiem polecenia, umożliwiając przetwarzanie dynamicznie generowanych danych. Na przykład wynik polecenia find może być bezpośrednio użyty jako lista elementów dla pętli for, co daje ogromne możliwości automatyzacji.

23Pętla `for` - iteracja po zakresie liczb

Różne sposoby tworzenia sekwencji liczbowych

Często potrzebujemy wykonać pętlę określoną liczbę razy. bash oferuje kilka sposobów na generowanie sekwencji liczb do użycia w pętli for.

  • Rozwinięcie klamrowe (Brace Expansion) {START..END}:
    • Prosta i czytelna składnia do generowania sekwencji.
    • Można również zdefiniować krok: {START..END..KROK} (od bash 4+).
  • Polecenie seq:
    • Zewnętrzny program, bardziej elastyczny (np. pozwala na formatowanie).
    • Składnia: seq LAST lub seq FIRST LAST lub seq FIRST STEP LAST.
  • Składnia w stylu C:
    • Najbardziej elastyczna, znana z innych języków programowania.
    • Składnia: for (( i=START; i<=END; i++ )); do ... done.
#!/bin/bash

echo "--- Użycie rozwinięcia klamrowego ---"
for i in {1..5}; do
    echo "Liczba: $i"
done

echo ""
echo "--- Użycie polecenia 'seq' ---"
for i in $(seq 1 2 10); do # Od 1 do 10 z krokiem 2
    echo "Liczba nieparzysta: $i"
done

echo ""
echo "--- Użycie pętli w stylu C ---"
for (( j=10; j>0; j-- )); do # Odliczanie
    echo "Pozostało: $j"
done
echo "Start!"

Bash oferuje kilka sposobów na iterację po zakresie liczb, co pozwala dostosować składnię do konkretnych potrzeb. Rozwinięcie klamrowe {1..10} jest wykonywane przez powłokę przed interpretacją reszty polecenia, co czyni je bardzo szybkim. Polecenie seq jest zewnętrznym programem, który oferuje większą kontrolę nad formatowaniem, ale każde wywołanie wymaga uruchomienia nowego procesu. Składnia w stylu C for ((...)) jest najpotężniejsza, ponieważ pozwala na użycie dowolnych wyrażeń arytmetycznych w warunkach i krokach. Wybór odpowiedniej metody zależy od konkretnego zadania i wersji basha. W starszych wersjach basha rozwinięcie klamrowe nie wspiera kroku, co wymusza użycie seq lub pętli w stylu C. Pętla w stylu C jest również najszybsza przy dużej liczbie iteracji.

Niezależnie od wybranej metody, warto pamiętać o zabezpieczeniu pętli przed nieskończonym działaniem. W przypadku pętli w stylu C należy upewnić się, że warunek zakończenia zostanie kiedyś spełniony.

24Pętla `while`

Pętla sterowana warunkiem

Pętla while wykonuje blok kodu tak długo, jak długo podany warunek jest prawdziwy. Warunek jest sprawdzany przed każdą iteracją. Jeśli na samym początku warunek jest fałszywy, pętla nie wykona się ani razu.

Składnia

while [[ warunek ]]; do
    # Blok kodu do wykonania,
    # dopóki warunek jest prawdziwy.
    # Ważne: wewnątrz pętli coś musi
    # wpływać na warunek, aby uniknąć
    # pętli nieskończonej!
done

Pętle `while` są idealne w sytuacjach, gdy nie wiemy z góry, ile iteracji będzie potrzebnych (np. czekanie na dostępność usługi, przetwarzanie danych do osiągnięcia celu).

#!/bin/bash

# Prosty licznik
LICZNIK=1
while [[ "$LICZNIK" -le 5 ]]; do
    echo "Iteracja numer: $LICZNIK"
    LICZNIK=$((LICZNIK + 1)) # Kluczowe: zmiana wartości wpływającej na warunek
done

echo ""

# Przykład "czekania" na plik
echo "Czekam na pojawienie się pliku /tmp/flaga.txt..."
while ! [[ -f "/tmp/flaga.txt" ]]; do
    echo -n "." # Wyświetl kropkę bez nowej linii
    sleep 2     # Czekaj 2 sekundy
done

echo ""
echo "Plik /tmp/flaga.txt został znaleziony! Kontynuuję."
# rm /tmp/flaga.txt

Pętla while różni się od for tym, że wykonuje się tak długo, jak długo warunek jest prawdziwy, co czyni ją idealną do sytuacji, gdy nie znamy z góry liczby iteracji. Warunek w pętli while jest sprawdzany przed każdą iteracją, więc jeśli od początku jest fałszywy, pętla nie wykona się ani razu. Kluczowym elementem pętli while jest modyfikacja zmiennej warunkowej wewnątrz pętli, która zapobiega zapętleniu nieskończonemu. Pętla while jest często używana do monitorowania stanu systemu, czekania na określone zdarzenia lub przetwarzania danych do momentu osiągnięcia celu. Polecenie sleep wewnątrz pętli jest często stosowane, aby zmniejszyć obciążenie procesora przy częstym sprawdzaniu warunku. Pętla while doskonale nadaje się również do czytania plików linia po linii, co jest jej najważniejszym zastosowaniem.

Należy uważać na pętle nieskończone, które mogą zawiesić działanie skryptu. W takich sytuacjach przerwanie skryptu kombinacją Ctrl+C zazwyczaj zatrzymuje działanie pętli i przywraca kontrolę nad terminalem.

25Czytanie pliku linią po linii

Najważniejszy wzorzec pętli `while`

Jednym z najczęstszych i najważniejszych zastosowań pętli while jest przetwarzanie plików tekstowych, linia po linii. Używa się do tego kombinacji pętli `while`, polecenia `read` i przekierowania wejścia.

Składnia

while IFS= read -r LINIA; do
    # Przetwarzaj zmienną $LINIA
    echo "Odczytano: $LINIA"
done < "ścieżka/do/pliku.txt"
  • < "plik.txt": Przekierowuje zawartość pliku na standardowe wejście pętli.
  • read -r LINIA: Polecenie read odczytuje jedną linię z wejścia i zapisuje ją do zmiennej $LINIA. Opcja -r zapobiega interpretacji backslashy.
  • IFS=: (Internal Field Separator) Ustawienie tej zmiennej na pustą wartość zapobiega niechcianemu usuwaniu białych znaków z początku i końca linii.
#!/bin/bash

PLIK_WEJŚCIOWY="dane.txt"

# Utworzenie przykładowego pliku
echo "Alicja;Nowak;35" > "$PLIK_WEJŚCIOWY"
echo "  Piotr;Kowalski;28" >> "$PLIK_WEJŚCIOWY"
echo "Tomasz;Zieliński;42" >> "$PLIK_WEJŚCIOWY"

echo "Przetwarzanie pliku $PLIK_WEJŚCIOWY..."
NUMER_LINII=0
while IFS= read -r LINIA; do
    # Pomiń puste linie
    if [[ -z "$LINIA" ]]; then
        continue
    fi
    NUMER_LINII=$((NUMER_LINII + 1))
    echo "Linia $NUMER_LINII: '$LINIA'"
done < "$PLIK_WEJŚCIOWY"

# Usunięcie pliku po przetworzeniu
rm "$PLIK_WEJŚCIOWY"

Odczyt pliku linia po linii jest jednym z najczęstszych zadań w skryptach bash, używanym na przykład przy przetwarzaniu logów, plików CSV czy list konfiguracyjnych. Wzorzec while IFS= read -r linia jest standardem, który gwarantuje poprawne odczytanie każdej linii, niezależnie od jej zawartości. Ustawienie IFS na pustą wartość zapobiega usuwaniu białych znaków z początku i końca linii. Opcja -r w read zapobiega interpretacji znaków backslasha, co jest ważne przy przetwarzaniu ścieżek plików w systemie Windows. Przekierowanie pliku na wejście pętli za pomocą < pozwala na przetwarzanie pliku bez użycia zewnętrznych narzędzi. Wewnątrz pętli możemy dowolnie przetwarzać każdą linię, na przykład dzielić ją na pola, filtrować lub modyfikować. Wzorzec ten jest na tyle ważny, że warto go zapamiętać na pamięć, ponieważ pojawia się w niemal każdym skrypcie bash.

W przypadku bardzo dużych plików odczyt linia po linii jest efektywniejszy pamięciowo niż wczytanie całego pliku do zmiennej. Jest to szczególnie istotne w środowiskach z ograniczonymi zasobami, takich jak systemy wbudowane lub serwery o małej ilości pamięci RAM.

26Sterowanie pętlami: `break` i `continue`

Modyfikowanie przebiegu pętli

Czasami potrzebujemy zmienić standardowy przepływ pętli - przerwać ją przedwcześnie lub pominąć bieżącą iterację. Służą do tego dwie instrukcje:

  • break:
    • Natychmiast przerywa działanie pętli i przechodzi do pierwszej instrukcji po pętli.
    • Użyteczne, gdy cel pętli został osiągnięty (np. znaleziono szukany element).
  • continue:
    • Natychmiast przerywa bieżącą iterację i przechodzi do sprawdzenia warunku i rozpoczęcia następnej iteracji.
    • Użyteczne, gdy dla pewnych elementów nie chcemy wykonywać reszty kodu w pętli (np. pomijanie komentarzy w pliku konfiguracyjnym).
#!/bin/bash

echo "--- Przykład 'continue' ---"
# Pomiń liczby parzyste
for i in {1..10}; do
    # Sprawdź, czy reszta z dzielenia przez 2 jest 0
    if (( i % 2 == 0 )); then
        continue # Pomiń resztę pętli dla tej iteracji
    fi
    echo "Liczba nieparzysta: $i"
done

echo ""
echo "--- Przykład 'break' ---"
# Szukaj pliku, przerwij po znalezieniu
SZUKANY_PLIK="bash"
for PLIK in /bin/*; do
    # basename zwraca samą nazwę pliku bez ścieżki
    if [[ "$(basename "$PLIK")" == "$SZUKANY_PLIK" ]]; then
        echo "Znaleziono plik: $PLIK"
        break # Zakończ pętlę, nie szukaj dalej
    fi
done

Instrukcje break i continue dają programiście precyzyjną kontrolę nad przebiegiem pętli, pozwalając na elastyczne dostosowanie jej działania. Break jest szczególnie przydatny przy wyszukiwaniu elementów, ponieważ po znalezieniu szukanego obiektu nie ma sensu kontynuować przeszukiwania. Continue jest natomiast idealne do pomijania elementów, które nie spełniają określonych kryteriów. W przypadku zagnieżdżonych pętli można przekazać liczbę jako argument do break lub continue, aby określić, która pętla ma być przerwana. Instrukcje te działają zarówno w pętlach for, jak i while. Używanie break i continue w przemyślany sposób znacząco poprawia wydajność skryptów, ponieważ zapobiega niepotrzebnemu przetwarzaniu danych.

Należy jednak unikać nadmiernego stosowania tych instrukcji, ponieważ mogą one utrudnić zrozumienie przepływu sterowania w skrypcie. W wielu przypadkach lepiej przeprojektować warunek pętli, niż używać break do wychodzenia z niej w nieoczekiwanym momencie.

27Funkcje: Wprowadzenie

Grupowanie kodu do ponownego użycia

Funkcje pozwalają na wydzielenie logicznego fragmentu kodu, nadanie mu nazwy i wielokrotne wywoływanie go w różnych miejscach skryptu. Jest to fundamentem dobrej organizacji kodu, unikania powtórzeń (zasada DRY - Don't Repeat Yourself) i tworzenia modułowych, czytelnych skryptów.

Składnia definicji funkcji

Istnieją dwie równoważne składnie:

# Sposób 1 (preferowany)
nazwa_funkcji() {
    # Ciało funkcji
    # ...
}

# Sposób 2 (ze słowem kluczowym 'function')
function nazwa_innej_funkcji {
    # Ciało funkcji
    # ...
}

Funkcję wywołuje się po prostu przez podanie jej nazwy, tak jak każdego innego polecenia.

#!/bin/bash

# Definicja prostej funkcji
wyswietl_date() {
    echo "--------------------------"
    echo "Dzisiaj jest: $(date)"
    echo "--------------------------"
}

# Główna część skryptu
echo "Rozpoczynam pracę..."

# Wywołanie funkcji
wyswietl_date

echo "Wykonuję jakieś zadania..."
sleep 2

echo "Zadania zakończone. Wyświetlam datę ponownie."

# Ponowne wywołanie tej samej funkcji
wyswietl_date

echo "Skrypt zakończony."

Funkcje w bashu umożliwiają grupowanie powiązanych poleceń w logiczne bloki, które można wielokrotnie wywoływać z różnych miejsc skryptu. Jest to podstawowa technika programowania strukturalnego, która znacząco poprawia czytelność i możliwość ponownego wykorzystania kodu. Funkcje mogą być definiowane w dowolnym miejscu skryptu, ale muszą być zdefiniowane przed pierwszym wywołaniem. W bashu funkcje nie mają jawnie zadeklarowanych parametrów, a argumenty są dostępne za pomocą zmiennych pozycyjnych. Wewnątrz funkcji można deklarować zmienne lokalne za pomocą słowa local, co zapobiega konfliktom nazw ze zmiennymi globalnymi. Dobrze napisane funkcje powinny być krótkie i wykonywać jedno konkretne zadanie. Funkcje mogą być wywoływane nie tylko z głównego skryptu, ale także z innych funkcji, co pozwala na tworzenie zaawansowanych hierarchii wywołań.

Stosowanie funkcji jest szczególnie ważne w dłuższych skryptach, ponieważ ułatwia testowanie poszczególnych fragmentów kodu niezależnie. Dzięki funkcjom można również tworzyć biblioteki kodu, które są współdzielone między wieloma skryptami.

28Funkcje: Przekazywanie argumentów

Uczynienie funkcji bardziej elastycznymi

Funkcje mogą przyjmować argumenty (parametry), podobnie jak skrypty. Dzięki temu ta sama funkcja może działać na różnych danych.

Wewnątrz funkcji argumenty są dostępne za pomocą tych samych zmiennych specjalnych co argumenty skryptu:

  • $1: Pierwszy argument przekazany do funkcji.
  • $2: Drugi argument.
  • $#: Liczba argumentów przekazanych do funkcji.
  • $@: Wszystkie argumenty jako osobne słowa (zalecane).
  • $*: Wszystkie argumenty jako jedno słowo.

Ważne: Te zmienne wewnątrz funkcji odnoszą się do argumentów funkcji, a nie do argumentów całego skryptu. Mają zasięg lokalny.

#!/bin/bash

# Funkcja, która przyjmuje dwa argumenty
powitaj_uzytkownika() {
    # Sprawdzenie, czy podano odpowiednią liczbę argumentów
    if [[ $# -ne 2 ]]; then
        echo "Błąd funkcji: Oczekiwano 2 argumentów (imię, miasto)."
        return 1 # Zwróć kod błędu
    fi

    local imie=$1   # Użycie 'local' dla zmiennych wewnątrz funkcji
    local miasto=$2
    
    echo "Witaj, $imie z miasta $miasto!"
}

# Wywołania funkcji z różnymi argumentami
powitaj_uzytkownika "Anna" "Kraków"
powitaj_uzytkownika "Marek" "Warszawa"

# Wywołanie z nieprawidłową liczbą argumentów
powitaj_uzytkownika "Piotr"

Przekazywanie argumentów do funkcji czyni je uniwersalnymi i niezależnymi od konkretnych danych. Wewnątrz funkcji argumenty są dostępne za pomocą tych samych zmiennych $1, $2, $# co w przypadku głównego skryptu, ale odnoszą się wyłącznie do argumentów przekazanych funkcji. Dzięki temu funkcja może być wywołana z różnymi wartościami w różnych kontekstach. Sprawdzanie liczby argumentów za pomocą $# wewnątrz funkcji jest dobrą praktyką, która zapobiega błędom wynikającym z nieprawidłowego wywołania. Zmienna $@ wewnątrz funkcji zawiera wszystkie argumenty przekazane do funkcji, a nie do skryptu. Deklarowanie zmiennych jako local wewnątrz funkcji zapobiega przypadkowemu nadpisaniu zmiennych globalnych o tych samych nazwach. Funkcje mogą przyjmować dowolną liczbę argumentów, ale dla czytelności warto ograniczyć się do kilku najważniejszych.

W praktyce argumenty funkcji są często używane do przekazywania ścieżek plików, nazw serwerów, progów liczbowych i innych parametrów konfiguracyjnych. Dzięki argumentom funkcje stają się niezwykle elastyczne i łatwe w testowaniu.

29Funkcje: Zwracanie wartości

Dwa sposoby "zwracania" wartości z funkcji

bash nie pozwala na zwracanie dowolnych wartości z funkcji w sposób znany z innych języków (np. return "jakiś tekst"). Istnieją dwa mechanizmy, które służą do tego celu:

  1. Kod wyjścia (Exit Status):
    • Za pomocą polecenia return N, gdzie N to liczba całkowita od 0 do 255.
    • Służy do sygnalizowania, czy funkcja zakończyła się sukcesem (0) czy błędem (1-255).
    • Kod wyjścia ostatniej funkcji jest dostępny w zmiennej $?.
  2. Standardowe wyjście (stdout):
    • Aby "zwrócić" tekst lub wynik obliczeń, funkcja po prostu wyświetla go na standardowe wyjście (za pomocą echo).
    • Następnie można przechwycić to wyjście do zmiennej za pomocą podstawienia polecenia: WYNIK=$(nazwa_funkcji).
#!/bin/bash

# 1. Funkcja zwracająca kod wyjścia
sprawdz_plik() {
    if [[ -f "$1" ]]; then
        return 0 # Sukces
    else
        return 1 # Błąd
    fi
}

# 2. Funkcja "zwracająca" tekst przez stdout
dodaj_dwie_liczby() {
    local suma=$(($1 + $2))
    echo "$suma"
}

# Użycie funkcji z kodem wyjścia
if sprawdz_plik "/etc/hosts"; then
    echo "Plik /etc/hosts istnieje."
else
    echo "Plik /etc/hosts nie istnieje."
fi
echo "Kod wyjścia funkcji sprawdz_plik: $?"


# Użycie funkcji zwracającej tekst
WYNIK_DODAWANIA=$(dodaj_dwie_liczby 15 27)
echo "Wynik dodawania 15 i 27 to: $WYNIK_DODAWANIA"

Mechanizm zwracania wartości z funkcji w bashu różni się od innych języków, ale jest równie funkcjonalny, jeśli się go odpowiednio zrozumie. Polecenie return służy wyłącznie do zwracania kodu wyjścia, który może przyjąć wartość od 0 do 255. Jest to wystarczające do sygnalizowania sukcesu lub błędu, ale nie do zwracania danych. Aby zwrócić dane z funkcji, należy wykorzystać standardowe wyjście przez echo i przechwycić wynik za pomocą podstawienia polecenia. Ten wzorzec jest szeroko stosowany w skryptach bash i stanowi odpowiednik zwracania wartości w innych językach. Funkcja może zwrócić zarówno kod wyjścia, jak i dane na stdout, co daje dwa niezależne kanały komunikacji. Kod wyjścia jest najczęściej używany w instrukcjach warunkowych, a dane trafiają do zmiennych.

Należy pamiętać, że funkcja wykonuje się do napotkania return lub do końca swojego ciała. Wszystkie zmienne zadeklarowane bez słowa local są widoczne poza funkcją, co może być zarówno zaletą, jak i wadą. Dlatego zaleca się deklarowanie zmiennych pomocniczych jako local.

30Argumenty wywołania skryptu

Przekazywanie danych do skryptu z zewnątrz

Skrypty stają się znacznie bardziej użyteczne, gdy można nimi sterować z linii poleceń, przekazując im dane wejściowe w momencie uruchomienia. Służą do tego argumenty wywołania.

Wewnątrz skryptu są one dostępne za pomocą specjalnych zmiennych pozycyjnych:

  • $0: Nazwa, pod jaką skrypt został uruchomiony.
  • $1, $2, $3, ...: Kolejne argumenty (pierwszy, drugi, trzeci, itd.).
  • $#: Całkowita liczba przekazanych argumentów (nie licząc $0).
  • $@: Reprezentuje wszystkie argumenty jako listę osobnych, cytowanych słów. Używaj "$@" do bezpiecznego przekazywania argumentów.
  • $*: Reprezentuje wszystkie argumenty jako pojedynczy, cytowany ciąg znaków.
#!/bin/bash
# Uruchom ten skrypt np. tak:
# ./moj_skrypt.sh "Jan Kowalski" /tmp/logs _backup

echo "Nazwa skryptu: $0"
echo "Liczba przekazanych argumentów: $#"
echo "--- Poszczególne argumenty ---"
echo "Argument 1: $1"
echo "Argument 2: $2"
echo "Argument 3: $3"
echo "------------------------------"

# Pętla po wszystkich argumentach
echo "Iteracja po wszystkich argumentach za pomocą 'for arg in \"\$@\"':"
for arg in "$@"; do
    echo "  - Przetwarzam argument: '$arg'"
done

Argumenty wywołania skryptu pozwalają na przekazywanie danych z linii poleceń bezpośrednio do skryptu, co czyni go elastycznym narzędziem sterowanym z zewnątrz. Zmienna $0 przechowuje nazwę skryptu i jest przydatna przy wyświetlaniu komunikatów o błędach lub sposobie użycia. Zmienne pozycyjne $1, $2 itd. są przypisywane w kolejności argumentów podanych w linii poleceń. Zmienna $# pozwala na sprawdzenie, ile argumentów zostało przekazanych, co jest pierwszym krokiem walidacji wejścia. Różnica między $@ a $* polega na sposobie cytowania: "$@" zachowuje poszczególne argumenty jako osobne słowa, podczas gdy "$*" scala je w jeden ciąg. Używanie "$@" do iteracji po argumentach jest bezpieczniejszą i bardziej zalecaną praktyką. Argumenty wywołania mogą zawierać spacje, jeśli zostaną odpowiednio zacytowane w momencie wywołania skryptu.

Profesjonalne skrypty zawsze walidują liczbę i typ argumentów przed przystąpieniem do głównego zadania. Dzięki temu użytkownik od razu wie, czy skrypt został wywołany poprawnie, bez konieczności analizowania enigmatycznych komunikatów błędów.

31Argumenty: Praktyczny przykład i walidacja

Sprawdzanie poprawności argumentów

Dobrze napisany skrypt powinien zawsze na początku sprawdzać, czy otrzymał poprawną liczbę i rodzaj argumentów. Jeśli nie, powinien wyświetlić komunikat o sposobie użycia i zakończyć działanie z kodem błędu.

Najczęściej sprawdza się wartość zmiennej $#, aby upewnić się, że użytkownik podał wszystkie wymagane parametry.

Polecenie exit N natychmiast kończy działanie skryptu, zwracając kod wyjścia `N` (zazwyczaj `1` dla ogólnego błędu).

#!/bin/bash
# Skrypt do tworzenia kopii zapasowej pliku.
# Oczekuje dwóch argumentów: pliku źródłowego i katalogu docelowego.

# Walidacja liczby argumentów
if [[ $# -ne 2 ]]; then
    echo "Błąd: Nieprawidłowa liczba argumentów!"
    echo "Użycie: $0  "
    exit 1
fi

PLIK_ZRODLOWY=$1
KATALOG_DOCELOWY=$2

# Walidacja, czy źródło istnieje i jest plikiem
if ! [[ -f "$PLIK_ZRODLOWY" ]]; then
    echo "Błąd: Plik źródłowy '$PLIK_ZRODLOWY' nie istnieje."
    exit 1
fi

# Walidacja, czy cel istnieje i jest katalogiem
if ! [[ -d "$KATALOG_DOCELOWY" ]]; then
    echo "Błąd: Katalog docelowy '$KATALOG_DOCELOWY' nie istnieje."
    exit 1
fi

echo "Kopiowanie '$PLIK_ZRODLOWY' do '$KATALOG_DOCELOWY'..."
cp -v "$PLIK_ZRODLOWY" "$KATALOG_DOCELOWY"
echo "Kopia zapasowa zakończona sukcesem."

Walidacja argumentów na początku skryptu jest jedną z najważniejszych praktyk w programowaniu bash, która znacząco podnosi niezawodność. Sprawdzenie liczby argumentów za pomocą $# to najprostsza i najskuteczniejsza metoda wykrycia, czy użytkownik przekazał poprawne dane wejściowe. W przypadku błędnych argumentów skrypt powinien wyświetlić czytelny komunikat z przykładem poprawnego użycia, włączając w to nazwę skryptu ze zmiennej $0. Polecenie exit natychmiast kończy działanie skryptu z określonym kodem błędu, co pozwala innym programom na sprawdzenie, czy skrypt zakończył się pomyślnie. Oprócz liczby argumentów warto sprawdzić także ich typ, na przykład czy podana ścieżka istnieje i jest plikiem lub katalogiem. Walidacja powinna być wykonywana przed rozpoczęciem głównych operacji, aby uniknąć częściowego wykonania zadania. Komunikaty błędów powinny być jednoznaczne i wskazywać dokładnie, co poszło nie tak.

Dobrze zaprojektowana walidacja argumentów nie tylko ułatwia użytkowanie skryptu, ale także przyspiesza debugowanie, ponieważ błędy są wykrywane na najwcześniejszym możliwym etapie. Jest to szczególnie ważne w skryptach używanych przez wiele osób w środowisku produkcyjnym.

32Przekierowanie strumieni: Wprowadzenie

Standardowe strumienie I/O

W systemach uniksowych każdy proces domyślnie ma otwarte trzy standardowe strumienie komunikacji:

  • Standardowe wejście (stdin, deskryptor pliku 0): Domyślnie klawiatura. Stąd proces czyta dane (np. polecenie `read`).
  • Standardowe wyjście (stdout, deskryptor pliku 1): Domyślnie ekran terminala. Tu proces wysyła swoje normalne wyniki (np. polecenie `echo`, `ls`).
  • Standardowe wyjście błędów (stderr, deskryptor pliku 2): Domyślnie ekran terminala. Tu proces wysyła komunikaty o błędach i ostrzeżenia.

Przekierowanie to mechanizm powłoki, który pozwala na zmianę domyślnego źródła (dla stdin) lub celu (dla stdout/stderr) tych strumieni, np. na plik.

  Klawiatura           Ekran
 (stdin, 0) <--- [   PROCES   ] ---> (stdout, 1)
                  [ (program)  ] ---> (stderr, 2)

Koncepcja trzech standardowych strumieni komunikacji pochodzi z systemu Unix i jest jednym z fundamentów filozofii tego systemu. Każdy proces w systemie Linux dziedziczy te strumienie od procesu nadrzędnego, co umożliwia łączenie programów w łańcuchy. Standardowe wejście (stdin) jest domyślnie podpięte pod klawiaturę, ale może być przekierowane z pliku lub wyjścia innego programu. Standardowe wyjście (stdout) trafia domyślnie na ekran i jest przeznaczone dla normalnych wyników działania programu. Standardowe wyjście błędów (stderr) jest oddzielne, co pozwala na rozdzielenie poprawnych danych od komunikatów błędów. Deskryptory plików 0, 1 i 2 są zarezerwowane dla tych strumieni i nie należy ich używać do innych celów. Zrozumienie strumieni jest kluczowe dla efektywnego używania potoków i przekierowań. Mechanizm ten pozwala na tworzenie złożonych systemów przetwarzania danych z prostych komponentów.

W praktyce rozdzielenie stdout i stderr jest niezwykle przydatne przy logowaniu, ponieważ poprawne dane mogą trafić do pliku logu, a błędy do osobnego pliku lub na konsolę administratora. Taki podział znacznie ułatwia monitorowanie i diagnozowanie problemów systemowych.

33Przekierowanie wyjścia (stdout i stderr)

Zapisywanie wyników i błędów do plików

Najczęstszym zastosowaniem przekierowań jest zapisywanie wyników działania skryptów do plików logów.

Operatory przekierowania wyjścia:

  • polecenie > plik.txt:
    • Przekierowuje stdout (strumień 1) do pliku.
    • Nadpisuje plik, jeśli istnieje. Jeśli nie, tworzy go.
  • polecenie >> plik.txt:
    • Przekierowuje stdout do pliku.
    • Dopisuje do końca pliku, jeśli istnieje. Jeśli nie, tworzy go.
  • polecenie 2> bledy.txt:
    • Przekierowuje stderr (strumień 2) do pliku.
  • polecenie &> wszystko.txt:
    • Przekierowuje stdout ORAZ stderr do tego samego pliku (składnia dostępna od bash 2.0, wariant >> z dopisywaniem od bash 4+).
  • polecenie > plik.txt 2>&1:
    • Klasyczna, w pełni przenośna metoda na przekierowanie obu strumieni. Oznacza: "przekieruj stdout do pliku, a stderr tam, gdzie idzie stdout".
#!/bin/bash

# Przekieruj listę plików do pliku (nadpisz)
ls -l /etc > /tmp/lista_etc.txt

# Dopisz listę katalogu domowego do tego samego pliku
ls -l "$HOME" >> /tmp/lista_etc.txt

# Uruchom polecenie, które wygeneruje zarówno wynik, jak i błąd
# 'find /etc' - poprawny wynik
# 'find /root' - błąd braku uprawnień
find /etc /root -maxdepth 1 -name "hosts" > /tmp/wyniki.txt 2> /tmp/bledy.txt

# Przekieruj wszystko do jednego pliku (nowa składnia)
date &> /tmp/data_log.txt
ls /nie/istnieje &>> /tmp/data_log.txt

# Wyczyszczenie
# rm /tmp/lista_etc.txt /tmp/wyniki.txt /tmp/bledy.txt /tmp/data_log.txt

Przekierowanie strumieni to jeden z najczęściej używanych mechanizmów w wierszu poleceń Linuxa, umożliwiający kontrolę nad tym, dokąd trafiają dane i skąd pochodzą. Operator > do nadpisywania pliku jest przydatny przy zapisywaniu wyników jednorazowych operacji, natomiast >> jest lepszy przy logach, gdzie chcemy zachować historię. Przekierowanie stderr za pomocą 2> jest niezbędne przy filtrowaniu komunikatów błędów, na przykład podczas przeszukiwania systemu plików bez uprawnień. Nowoczesna składnia &> jest wygodna, ale starsza składnia 2>&1 jest bardziej przenośna i działa we wszystkich powłokach uniksowych. Kolejność przekierowań ma znaczenie: najpierw przekierowuje się stdout, a potem stderr do tego samego miejsca. W praktyce administracyjnej przekierowania są używane przy każdym uruchamianiu skryptów w cronie, aby logi trafiały do odpowiednich plików.

Warto również znać możliwość przekierowania na specjalne urządzenie /dev/null, które działa jak "kosz na dane". Jest to często używane do tłumienia niepotrzebnych komunikatów, gdy interesuje nas tylko kod wyjścia polecenia, a nie jego wynik.

34Potoki (pipelines)

Łączenie poleceń w łańcuchy

Potoki (ang. pipelines) to jedna z najpotężniejszych koncepcji systemów uniksowych. Operator potoku, symbolizowany przez pionową kreskę |, pozwala na przekazanie standardowego wyjścia (stdout) jednego polecenia bezpośrednio na standardowe wejście (stdin) kolejnego polecenia.

Dzięki temu możemy tworzyć złożone łańcuchy przetwarzania danych, łącząc proste, wyspecjalizowane narzędzia w jedno potężne polecenie, bez potrzeby tworzenia plików tymczasowych.

Przykład: polecenie1 | polecenie2 | polecenie3

  1. Wynik `polecenie1` staje się danymi wejściowymi dla `polecenie2`.
  2. Wynik `polecenie2` staje się danymi wejściowymi dla `polecenie3`.
  3. Ostateczny wynik jest wyświetlany przez `polecenie3`.

#!/bin/bash

# Przykład 1: Zliczanie plików w katalogu /bin
# ls -1 /bin -> listuje pliki, każdy w nowej linii
# wc -l      -> zlicza liczbę linii na swoim wejściu
echo "Liczba plików w /bin:"
ls -1 /bin | wc -l

echo ""

# Przykład 2: Znalezienie 5 największych plików w systemie
# du -ah / 2>/dev/null -> oblicza rozmiar wszystkich plików (błędy do kosza)
# sort -hr             -> sortuje wynik w formacie czytelnym dla człowieka (-h) w odwrotnej kolejności (-r)
# head -n 5            -> wyświetla pierwsze 5 linii
echo "Top 5 największych plików/katalogów w systemie:"
du -ah / 2>/dev/null | sort -hr | head -n 5

echo ""

# Przykład 3: Wyświetlenie zalogowanych użytkowników, bez duplikatów
# who   -> pokazuje, kto jest zalogowany
# awk '{print $1}' -> wycina pierwszą kolumnę (nazwy użytkowników)
# sort -u -> sortuje i usuwa duplikaty
echo "Aktualnie zalogowani unikalni użytkownicy:"
who | awk '{print $1}' | sort -u

Potoki są fundamentem filozofii Unix, która głosi, że każdy program powinien robić jedną rzecz i robić ją dobrze. Łączenie programów potokami pozwala na tworzenie złożonych operacji bez pisania dodatkowego kodu. Potoki działają asynchronicznie, co oznacza, że kolejne programy w łańcuchu rozpoczynają pracę, gdy tylko pojawią się pierwsze dane z poprzedniego programu. Dzięki temu potoki są bardzo wydajne i nie wymagają buforowania całości danych. W praktyce potoki są używane do filtrowania logów, transformacji danych, obliczeń statystycznych i wielu innych zadań. Łączenie potoków z przekierowaniami daje jeszcze większe możliwości, na przykład zapisanie końcowego wyniku do pliku. W systemie Linux istnieje wiele narzędzi zaprojektowanych specjalnie do pracy w potokach, takich jak grep, sort, uniq, wc, head, tail i awk.

Umiejętność tworzenia efektywnych potoków jest jedną z kluczowych kompetencji administratora systemów Linux. Doświadczony administrator potrafi zastąpić dziesiątki linii kodu w innym języku jednym zręcznym potokiem składającym się z kilku poleceń.

35Narzędzia do przetwarzania tekstu: `grep`

Filtrowanie tekstu i wyszukiwanie wzorców

grep (Global Regular Expression Print) to fundamentalne narzędzie do przeszukiwania tekstu (z plików lub ze standardowego wejścia) w poszukiwaniu linii zawierających określony wzorzec (może to być prosty tekst lub złożone wyrażenie regularne).

Najczęściej używane opcje:

  • grep "wzorzec" plik.txt: Wyszukuje wzorzec w pliku.
  • polecenie | grep "wzorzec": Filtruje wynik innego polecenia.
  • -i: Ignoruje wielkość liter.
  • -v: Odwraca dopasowanie (pokazuje linie, które nie zawierają wzorca).
  • -c: Zlicza liczbę pasujących linii.
  • -r lub -R: Przeszukuje rekurencyjnie katalogi.
  • -E: Używa rozszerzonych wyrażeń regularnych.
#!/bin/bash

# Znajdź linie zawierające 'root' w pliku /etc/passwd
echo "--- Użytkownik 'root' w /etc/passwd ---"
grep "root" /etc/passwd

echo ""

# Zlicz, ilu użytkowników używa powłoki bash
echo "--- Liczba użytkowników z powłoką bash ---"
grep -c "bash" /etc/passwd

echo ""

# Wyświetl listę procesów, ale odfiltruj proces samego grepa
echo "--- Procesy demona 'systemd' ---"
ps aux | grep "systemd" | grep -v "grep"

echo ""

# Znajdź wszystkie pliki .conf w /etc, które zawierają słowo 'network'
echo "--- Pliki konfiguracyjne ze słowem 'network' ---"
grep -r -i "network" /etc/*.conf 2>/dev/null

Grep jest jednym z najczęściej używanych poleceń w systemie Linux, służącym do wyszukiwania wzorców w tekście. Jego nazwa pochodzi od polecenia edytora ed, które wykonywało operację "g/re/p" (globalnie, wyrażenie regularne, wydrukuj). Grep może przeszukiwać zarówno pliki, jak i dane ze standardowego wejścia, co czyni go idealnym elementem potoków. Opcja -i jest przydatna przy wyszukiwaniu bez uwzględniania wielkości liter, na przykład przy szukaniu nazw użytkowników. Opcja -v pozwala na odwrócenie wyszukiwania, co jest przydatne do odfiltrowywania niechcianych linii. Rekurencyjne przeszukiwanie katalogów za pomocą -r to potężne narzędzie do analizy kodu źródłowego czy plików konfiguracyjnych. Grep wspiera zarówno podstawowe, jak i rozszerzone wyrażenia regularne, co daje ogromne możliwości dopasowywania wzorców.

W codziennej pracy grep jest używany do analizy logów, wyszukiwania błędów w kodzie, sprawdzania konfiguracji i monitorowania systemu. Połączenie grepa z innymi narzędziami, takimi jak cut, sort czy uniq, pozwala na szybkie uzyskiwanie cennych informacji z surowych danych.

36Narzędzia do przetwarzania tekstu: `sed`

Edytor strumieniowy

sed (Stream Editor) to potężne narzędzie do wykonywania transformacji na tekście. Jego najczęstszym zastosowaniem jest operacja "znajdź i zamień". `sed` przetwarza tekst linia po linii, stosując podane reguły, i domyślnie wyświetla zmodyfikowany tekst na standardowe wyjście.

Podstawowa składnia zamiany:

sed 's/wzorzec/zamiennik/flagi'

  • s: Oznacza operację "substitute" (zamień).
  • wzorzec: Tekst lub wyrażenie regularne do znalezienia.
  • zamiennik: Tekst, na który wzorzec ma być zamieniony.
  • flagi (opcjonalne):
    • g (global): Zamienia wszystkie wystąpienia wzorca w linii, a nie tylko pierwsze.
    • i: Ignoruje wielkość liter.
#!/bin/bash

# Przykład 1: Prosta zamiana
echo "Ala ma kota, a kot ma pchły." | sed 's/kot/pies/'
# Wynik: Ala ma piesa, a kot ma pchły. (tylko pierwsze wystąpienie)

# Przykład 2: Zamiana globalna (flaga 'g')
echo "Ala ma kota, a kot ma pchły." | sed 's/kot/pies/g'
# Wynik: Ala ma piesa, a pies ma pchły. (wszystkie wystąpienia)

# Przykład 3: Użycie na pliku
# Tworzymy plik tymczasowy
echo "user=admin" > /tmp/config.txt
echo "host=localhost" >> /tmp/config.txt

# Zmień 'localhost' na 'server.example.com' w pliku
# Opcja -i modyfikuje plik "w miejscu" (in-place)
# sed -i 's/localhost/server.example.com/' /tmp/config.txt
# cat /tmp/config.txt
# Wynik w pliku:
# user=admin
# host=server.example.com

# Przykład 4: Usunięcie linii (wzorzec 'd')
# Usuń wszystkie puste linie z pliku
# sed '/^$/d' plik.txt

Sed to potężny edytor strumieniowy, który wykonuje transformacje tekstu bez otwierania go w edytorze interaktywnym. Jego podstawowym zastosowaniem jest operacja zamiany tekstu, ale potrafi także usuwać, wstawiać i zmieniać kolejność linii. Składnia sed opiera się na wyrażeniach regularnych, co daje ogromną elastyczność przy definiowaniu wzorców do zamiany. Flaga g w operacji s (substitute) jest niezbędna, gdy chcemy zamienić wszystkie wystąpienia wzorca w linii, a nie tylko pierwsze. Opcja -i modyfikuje plik bezpośrednio, co jest wygodne, ale należy używać jej ostrożnie, najlepiej po uprzednim przetestowaniu polecenia bez tej opcji. Sed może być używany w potokach do transformacji danych w locie. Usuwanie pustych linii, zamiana separatorów w plikach CSV, maskowanie haseł w logach to tylko kilka przykładów zastosowań seda w codziennej pracy.

Sed dysponuje również zaawansowanymi możliwościami, takimi jak przechowywanie wzorców w buforze, etykiety i skoki warunkowe, co czyni go pełnoprawnym językiem programowania do przetwarzania tekstu. W praktyce jednak najczęściej używa się go do prostych operacji zamiany.

37Narzędzia do przetwarzania tekstu: `awk`

Zaawansowane przetwarzanie kolumnowe

awk to język programowania zaprojektowany do przetwarzania danych tekstowych, zwłaszcza zorganizowanych w kolumny. Domyślnie `awk` dzieli każdą linię wejściową na pola (kolumny) na podstawie białych znaków.

Wewnątrz skryptu `awk` mamy dostęp do tych pól za pomocą zmiennych $1, $2, $3, itd. $0 reprezentuje całą linię.

Podstawowa składnia:

awk 'program'

Program najczęściej ma postać '{ akcja }', gdzie akcja jest wykonywana dla każdej linii wejściowej. Można też dodawać warunki: 'warunek { akcja }'.

#!/bin/bash

# Przykład 1: Wyświetlenie pierwszej i trzeciej kolumny z wyniku 'ls -l'
# Wynik 'ls -l' wygląda mniej więcej tak:
# -rw-r--r-- 1 user group 1234 Oct 8 22:30 plik.txt
# Chcemy wyciąć 'user' i '1234'
# ls -l | awk '{print "Właściciel:", $3, "Rozmiar:", $5}'

# Przykład 2: Sumowanie rozmiarów plików
# Zsumuj piątą kolumnę (rozmiar) dla wszystkich plików
echo "Obliczanie całkowitego rozmiaru plików w /etc..."
# ls -l /etc | awk '/^-/ {sum += $5} END {print "Całkowity rozmiar:", sum, "bajtów"}'
# /^-/ -> warunek: wykonaj akcję tylko dla linii zaczynających się od '-' (pliki)
# {sum += $5} -> akcja: dodaj wartość 5. pola do zmiennej 'sum'
# END { ... } -> blok specjalny, wykonywany po przetworzeniu wszystkich linii

# Przykład 3: Zmiana separatora pól (opcja -F)
# Przetwarzanie pliku CSV (dane oddzielone przecinkami)
echo "user,pass,uid,gid" > /tmp/data.csv
echo "root,x,0,0" >> /tmp/data.csv
echo "daemon,x,1,1" >> /tmp/data.csv
awk -F',' '{print "Użytkownik:", $1, "ma UID:", $3}' /tmp/data.csv

Awk to język programowania zaprojektowany specjalnie do przetwarzania danych tekstowych, szczególnie tych w formacie kolumnowym. Jego składnia jest podobna do języka C, co ułatwia naukę programistom znającym ten język. Domyślnym separatorem pól w awk są białe znaki, ale można go zmienić za pomocą opcji -F, co jest przydatne przy plikach CSV. Awk automatycznie dzieli każdą linię na pola dostępne jako $1, $2, $3 itd., gdzie $0 reprezentuje całą linię. Blok BEGIN wykonuje się przed przetworzeniem pierwszej linii, a blok END po przetworzeniu ostatniej, co pozwala na inicjalizację i podsumowanie obliczeń. Awk jest często używany do obliczeń statystycznych, agregacji danych i raportowania. W przeciwieństwie do seda, awk lepiej radzi sobie z danymi liczbowymi i oferuje wbudowane funkcje matematyczne.

Awk może być używany jako samodzielny język programowania, z pełną obsługą pętli, tablic asocjacyjnych i funkcji. W praktyce jednak najczęściej spotyka się go w krótkich jednowierszowcach w potokach, służących do szybkiego wyodrębniania i przetwarzania danych.

38Zarządzanie procesami

Kontrola nad uruchomionymi programami

Skrypty często muszą zarządzać innymi programami - uruchamiać je, sprawdzać ich stan, a w razie potrzeby zatrzymywać.

  • Uruchamianie w tle (&):
    • Dopisanie znaku & na końcu polecenia uruchamia je w tle. Skrypt nie czeka na jego zakończenie i od razu przechodzi do następnej linii.
    • Powłoka wyświetla PID (Process ID) uruchomionego procesu.
  • ps: Wyświetla listę działających procesów. ps aux to popularna kombinacja opcji pokazująca wszystkie procesy w systemie.
  • kill: Wysyła sygnał do procesu o podanym PID.
    • kill PID: Wysyła domyślny sygnał (SIGTERM, 15), prosząc proces o "grzeczne" zakończenie.
    • kill -9 PID lub kill -SIGKILL PID: Wysyła sygnał "bezwarunkowego zabicia". Używane, gdy proces nie reaguje.
#!/bin/bash

# Uruchom proces w tle, który będzie działał przez 60 sekund
echo "Uruchamiam proces 'sleep 60' w tle..."
sleep 60 &
SLEEP_PID=$! # Specjalna zmienna $! przechowuje PID ostatniego procesu w tle

echo "Proces 'sleep' został uruchomiony z PID: $SLEEP_PID"
echo "Skrypt kontynuuje pracę, nie czekając na zakończenie 'sleep'."

# Sprawdź, czy proces o tym PID działa
# ps -p $SLEEP_PID zwróci 0, jeśli proces istnieje
if ps -p $SLEEP_PID > /dev/null 2>&1; then
    echo "Proces o PID $SLEEP_PID jest aktywny."
else
    echo "Proces o PID $SLEEP_PID już nie istnieje."
fi

# Po 5 sekundach zakończ proces
echo "Czekam 5 sekund, a następnie zatrzymuję proces..."
sleep 5
kill $SLEEP_PID

echo "Wysłano sygnał zakończenia do procesu $SLEEP_PID."
# Sprawdź ponownie
if ps -p $SLEEP_PID > /dev/null 2>&1; then
    echo "Proces nadal działa (być może potrzebuje 'kill -9')."
else
    echo "Proces został pomyślnie zatrzymany."
fi

Zarządzanie procesami w bashu umożliwia kontrolę nad uruchomionymi programami, co jest niezbędne przy tworzeniu skryptów automatyzujących złożone zadania. Uruchamianie procesów w tle za pomocą & pozwala skryptowi kontynuować działanie bez czekania na zakończenie uruchomionego programu. Zmienna $! przechowuje PID ostatnio uruchomionego procesu w tle, co umożliwia późniejsze odwołanie się do niego. Polecenie ps wyświetla informacje o procesach, a jego opcje pozwalają na dostosowanie wyświetlanych danych do konkretnych potrzeb. Polecenie kill wysyła sygnały do procesów, przy czym sygnał SIGTERM prosi o grzeczne zakończenie, a SIGKILL wymusza natychmiastowe zatrzymanie. W skryptach często używa się kill do zatrzymywania procesów potomnych przed zakończeniem działania głównego skryptu. Monitorowanie procesów pozwala na wykrywanie awarii i automatyczne ponowne uruchamianie usług.

W środowiskach produkcyjnych zarządzanie procesami jest często realizowane przez systemy init, takie jak systemd, ale w skryptach własnych umiejętność ręcznego zarządzania procesami jest nieoceniona. Dobry skrypt powinien zawsze sprzątać po sobie, czyli zatrzymywać procesy uruchomione w tle przed swoim zakończeniem.

39Tablice (Arrays)

Przechowywanie wielu wartości w jednej zmiennej

Tablice pozwalają na przechowywanie listy wartości pod jedną nazwą. Każdy element tablicy ma swój indeks, a numeracja zaczyna się od 0.

Składnia tablic

  • Deklaracja: NAZWA=(element1 element2 "element ze spacją")
  • Dostęp do elementu: ${NAZWA[indeks]}, np. ${MOJA_TABLICA[0]}
  • Dostęp do wszystkich elementów: ${NAZWA[@]} (zalecane) lub ${NAZWA[*]}
  • Liczba elementów: ${#NAZWA[@]}
  • Dodawanie elementu: NAZWA+=(nowy_element)

Tablice są niezwykle przydatne do gromadzenia wyników i przetwarzania ich w pętli.

#!/bin/bash

# Deklaracja tablicy z serwerami
SERWERY=("web-01" "db-01" "api-gateway" "monitoring-server")

# Wyświetlenie pierwszego elementu (indeks 0)
echo "Pierwszy serwer na liście: ${SERWERY[0]}"

# Wyświetlenie liczby elementów
echo "Liczba serwerów do sprawdzenia: ${#SERWERY[@]}"

# Dodanie nowego serwera do tablicy
SERWERY+=("backup-01")

echo "Dodano nowy serwer. Aktualna lista:"

# Iteracja po wszystkich elementach tablicy
for srv in "${SERWERY[@]}"; do
    echo "  - $srv"
done

# Możemy też tworzyć tablicę z wyniku polecenia
# Uwaga: ta składnia może źle działać przy nazwach plików ze spacjami
PLIKI_TXT=($(find . -maxdepth 1 -name "*.txt"))
echo "Znaleziono ${#PLIKI_TXT[@]} plików .txt"

Tablice w bashu umożliwiają przechowywanie wielu wartości w jednej zmiennej, co jest ogromnym udogodnieniem przy przetwarzaniu kolekcji danych. Indeksy tablic w bashu zaczynają się od 0, co jest zgodne z konwencją większości języków programowania. Dostęp do wszystkich elementów tablicy za pomocą składni ${TABLICA[@]} jest zalecany, ponieważ poprawnie obsługuje elementy zawierające spacje. Operator # przed nazwą tablicy zwraca liczbę jej elementów, co jest przydatne przy iteracji. Tablice mogą być dynamicznie rozszerzane za pomocą operatora +=, co pozwala na stopniowe budowanie kolekcji danych. Wynik polecenia może być bezpośrednio przypisany do tablicy, jeśli zostanie odpowiednio umieszczony w nawiasach okrągłych. Tablice są często używane do przechowywania list serwerów, plików, użytkowników i innych powtarzalnych danych.

W bashu dostępne są również tablice asocjacyjne (z indeksem tekstowym), które wymagają deklaracji za pomocą declare -A. Są one szczególnie przydatne przy tworzeniu słowników i mapowań, na przykład mapowania nazw użytkowników na ich katalogi domowe.

40Tryby debugowania skryptów

Jak znaleźć błędy w skrypcie?

Gdy skrypt nie działa zgodnie z oczekiwaniami, bash oferuje wbudowane mechanizmy do debugowania. Można je włączyć, modyfikując shebang lub używając polecenia set w skrypcie.

Najważniejsze opcje:

  • set -x (lub bash -x skrypt.sh):
    • Tryb "trace" lub "xtrace".
    • Wyświetla każde polecenie tuż przed jego wykonaniem, po zinterpretowaniu zmiennych. Jest to niezwykle pomocne do śledzenia przepływu sterowania i sprawdzania wartości zmiennych w danym momencie.
  • set -e:
    • Tryb "exit on error".
    • Powoduje natychmiastowe zakończenie skryptu, jeśli którekolwiek polecenie zakończy się kodem błędu (innym niż 0). Zapobiega to nieprzewidywalnemu zachowaniu skryptu po wystąpieniu błędu. Zalecane w większości skryptów produkcyjnych.
  • set -u:
    • Traktuje odwołanie do niezainicjowanej (pustej) zmiennej jako błąd i kończy skrypt. Pomaga wyłapać literówki w nazwach zmiennych.

Można łączyć opcje, np. set -eux.

#!/bin/bash

# Włącz tryb śledzenia, aby zobaczyć, co się dzieje
set -x

UŻYTKOWNIK="test"
KATALOG_DOMOWY="/home/$UŻYTKOWNI" # Celowa literówka w nazwie zmiennej!

echo "Tworzenie katalogu dla użytkownika: $UŻYTKOWNIK"

# Bez 'set -u' to polecenie stworzy katalog o nazwie '/home/'!
mkdir -p "$KATALOG_DOMOWY/public_html"

# Wyłącz tryb śledzenia
set +x

echo "Zakończono."

# Uruchomienie tego skryptu pokaże:
# + UŻYTKOWNIK=test
# + KATALOG_DOMOWY=/home/
# + echo 'Tworzenie katalogu dla użytkownika: test'
# + mkdir -p /home//public_html
# + set +x

# Gdybyśmy dodali 'set -u', skrypt zakończyłby się błędem
# w linii z deklaracją KATALOG_DOMOWY.

Debugowanie skryptów bash to umiejętność, która znacząco przyspiesza proces tworzenia i utrzymania kodu. Tryb xtrace (set -x) jest najczęściej używanym narzędziem debugowania, ponieważ pokazuje dokładnie, jakie polecenia są wykonywane i z jakimi wartościami zmiennych. Tryb exit on error (set -e) zapobiega kontynuowaniu skryptu po wystąpieniu błędu, co jest niezwykle ważne w skryptach produkcyjnych. Opcja set -u pomaga wykryć literówki w nazwach zmiennych, które w innym przypadku mogłyby pozostać niezauważone. Wszystkie te opcje można łączyć, na przykład set -eux, co daje kompleksowe informacje debugowania. Debugowanie można włączyć na stałe przez modyfikację shebangu na #!/bin/bash -x lub tymczasowo za pomocą set -x / set +x w wybranym fragmencie skryptu. W praktyce zaleca się włączanie opcji debugowania na czas testowania skryptu i wyłączanie ich w wersji produkcyjnej.

Oprócz wbudowanych opcji debugowania warto stosować własne komunikaty logowania za pomocą echo lub logger. Pozwalają one na śledzenie przepływu skryptu nawet w środowisku produkcyjnym bez włączania pełnego trybu debugowania, który generuje bardzo dużo informacji.

41Projekt końcowy: Skrypt do archiwizacji

Podsumowanie wiedzy w praktycznym skrypcie

Stwórzmy teraz kompletny skrypt, który wykorzystuje wiele z omówionych do tej pory koncepcji. Jego zadaniem będzie zarchiwizowanie podanego katalogu do pliku .tar.gz z datą w nazwie.

Skrypt będzie:

  1. Przyjmował jeden argument: ścieżkę do katalogu do archiwizacji.
  2. Sprawdzał poprawność argumentów.
  3. Definiował stałe i zmienne (katalog na archiwa, nazwa pliku).
  4. Używał funkcji do logowania komunikatów.
  5. Sprawdzał, czy docelowy katalog istnieje, i tworzył go w razie potrzeby.
  6. Wykonywał archiwizację za pomocą polecenia tar.
  7. Informował o sukcesie lub porażce.
#!/bin/bash

# --- Skrypt do automatycznej archiwizacji katalogów ---

# (set -e celowo pominięte, aby pokazać ręczne sprawdzanie kodów wyjścia)

# --- Zmienne globalne i stałe ---
readonly ARCHIVE_DIR="/mnt/backups"
readonly DATE_FORMAT=$(date "+%Y-%m-%d_%H-%M")

# --- Funkcje ---

# Funkcja do logowania komunikatów z datą
log() {
    echo "[$(date "+%Y-%m-%d %H:%M:%S")] - $1"
}

# Funkcja wyświetlająca sposób użycia skryptu
usage() {
    echo "Użycie: $0 "
    exit 1
}


# --- Główna logika skryptu ---

# 1. Walidacja argumentów
if [[ $# -ne 1 ]]; then
    log "Błąd: Nie podano katalogu do archiwizacji."
    usage
fi

SOURCE_DIR=$1

if ! [[ -d "$SOURCE_DIR" ]]; then
    log "Błąd: Podana ścieżka '$SOURCE_DIR' nie jest katalogiem."
    exit 1
fi

# 2. Przygotowanie nazwy archiwum i ścieżki
BASENAME=$(basename "$SOURCE_DIR")
ARCHIVE_FILENAME="${BASENAME}_${DATE_FORMAT}.tar.gz"
DESTINATION_PATH="${ARCHIVE_DIR}/${ARCHIVE_FILENAME}"

# 3. Utworzenie katalogu na archiwa, jeśli nie istnieje
log "Sprawdzanie katalogu na archiwa: $ARCHIVE_DIR"
mkdir -p "$ARCHIVE_DIR"

# 4. Wykonanie archiwizacji
log "Rozpoczynam archiwizację katalogu '$SOURCE_DIR'..."
log "Plik docelowy: $DESTINATION_PATH"

tar -czf "$DESTINATION_PATH" -C "$(dirname "$SOURCE_DIR")" "$BASENAME"

# 5. Weryfikacja i podsumowanie
if [[ $? -eq 0 ]]; then
    log "Archiwizacja zakończona pomyślnie!"
    log "Rozmiar archiwum: $(du -sh "$DESTINATION_PATH" | awk '{print $1}')"
else
    log "Błąd: Wystąpił problem podczas tworzenia archiwum."
    exit 1
fi

exit 0

Projekt końcowy w postaci skryptu do archiwizacji jest doskonałym podsumowaniem wiedzy zdobytej podczas kursu, ponieważ wykorzystuje większość omawianych konstrukcji. Skrypt przyjmuje argument z linii poleceń, waliduje go, używa zmiennych i stałych, definiuje funkcje, sprawdza warunki i wykonuje główne zadanie. Polecenie tar z opcjami -czf tworzy skompresowane archiwum, które jest standardem w systemach Linux. Wykorzystanie daty w nazwie archiwum zapewnia unikalność plików i ułatwia identyfikację, kiedy kopia została wykonana. Logowanie komunikatów z datą to dobra praktyka, która pozwala śledzić działanie skryptu w czasie. Sprawdzanie kodu wyjścia polecenia tar pozwala na wykrycie potencjalnych problemów z archiwizacją. W skrypcie celowo pominięto opcję set -e, aby zademonstrować ręczne sprawdzanie kodów wyjścia.

Taki skrypt może być z powodzeniem używany w środowisku produkcyjnym po dostosowaniu ścieżek i opcji do konkretnych potrzeb. Można go rozbudować o dodatkowe funkcje, takie jak wysyłanie powiadomień e-mail, usuwanie starych archiwów czy szyfrowanie kopii zapasowych. Stanowi on solidną podstawę do tworzenia bardziej zaawansowanych narzędzi automatyzacji.