Przejdź do treści
Lekcja 40 z 117

Kurs › Moduł 3 › Lekcja 40

Funkcje

⏱ ok. 10 min czytania· moduł 3: Pisanie skryptów

Po tej lekcji: kursant zamyka powtarzalne kawałki skryptu w funkcjach, przekazuje im argumenty, chroni zmienne przez local i wie, kiedy funkcja ma „oddać” wynik przez echo, a kiedy przez return.

Wstęp

Skrypt, który odziedziczyłeś po poprzednim adminie. W czterdziestu miejscach jest linijka wypisująca komunikat z datą. Szef mówi: „Dopisz do każdego komunikatu nazwę serwera”. Czterdzieści poprawek. W trzydziestu dziewięciu zrobisz dobrze, a w jednej się pomylisz i nikt tego nie zauważy przez pół roku.

A gdyby ta linijka była w jednym miejscu, a w czterdziestu pozostałych tylko się do niej odwoływać? Jedna poprawka, czterdzieści efektów. To są funkcje. Ostatnia lekcja tego modułu i ostatni klocek, którego brakowało, żeby pisać prawdziwe narzędzia.

Funkcja, czyli przycisk w windzie

Wyobraź sobie windę. Wciskasz „4” i winda sama robi całą procedurę: zamyka drzwi, rusza, hamuje na czwartym, otwiera. Nie musisz wiedzieć, jak to działa w środku. Ważne, że przycisk jest jeden, a procedurę ktoś opisał raz, porządnie.

Funkcja to właśnie taki przycisk. Nazwa plus kawałek kodu, który się wykona, kiedy użyjesz tej nazwy jak komendy.

#!/bin/bash
# f1.sh — pierwsze funkcje

powitanie() {
    echo "Cześć, $1! Dostałem argumentów: $#"
}

log() {
    local poziom=$1
    shift
    echo "$(date '+%F %T') [$poziom] $*"
}

powitanie Eryk
powitanie "Anna Nowak" drugi trzeci
log INFO "Zaczynam kopię"
log BLAD "Brak miejsca na dysku"
echo "A w skrypcie \$1 to nadal: '$1'"
./f1.sh skryptowy
Cześć, Eryk! Dostałem argumentów: 1
Cześć, Anna Nowak! Dostałem argumentów: 3
2026-11-03 16:40:18 [INFO] Zaczynam kopię
2026-11-03 16:40:18 [BLAD] Brak miejsca na dysku
A w skrypcie $1 to nadal: 'skryptowy'

Rozbieramy.

powitanie() { ... } to definicja. Nazwa, pusta para nawiasów i w klamrach to, co ma się dziać. Nawiasy są zawsze puste. Nie wpisujesz tam argumentów, jak w innych językach. To tylko znak dla Basha: „to jest funkcja”.

Wywołanie wygląda dokładnie jak zwykła komenda: powitanie Eryk. Bez nawiasów, bez przecinków. Dla Basha Twoja funkcja to po prostu kolejna komenda w okienku.

Argumenty? Tak samo jak w skrypcie. W środku funkcji $1 to pierwszy argument funkcji, $# to ich liczba, "$@" to wszystkie. Wszystko, co poznałeś przy argumentach skryptu, działa tu jeden do jednego. Nawet shift, które w funkcji log zdejmuje pierwszy argument, żeby resztę wypisać jako treść komunikatu.

I ważne: funkcja ma swoje własne $1. Skrypt dostał skryptowy, funkcja dostała Eryk, i nic się nie pomieszało. Ostatnia linijka to pokazuje. Każdy przycisk w windzie ma własne kabelki.

Teraz hak z czterdziestoma linijkami załatwiasz tak: dopisujesz $(hostname) w jednym miejscu, w funkcji log. Gotowe. Możesz iść na kawę, którą właśnie zaoszczędziłeś.

Funkcja w ~/.bashrc, czyli alias na sterydach

Pamiętasz aliasy z lekcji o ustawianiu Basha pod siebie? Spróbujmy aliasem zrobić komendę „załóż folder i od razu do niego wejdź”:

alias mkcd='mkdir -p $1 && cd $1'
mkcd projekt
mkdir: missing operand
Try 'mkdir --help' for more information.

Alias nie umie przyjmować argumentów. To tylko podmiana tekstu: Bash wkleja treść aliasu, a projekt dokleja na samym końcu. $1 w aliasie nic nie znaczy. Funkcja umie:

mkcd() {
    mkdir -p "$1" && cd "$1" || return
}

Wrzuć to do ~/.bashrc, przeczytaj plik ponownie przez source ~/.bashrc i od teraz mkcd /tmp/nowy/projekt zakłada całe drzewo folderów i od razu Cię tam przenosi. Tu widać jeszcze jedną przewagę: funkcja działa w Twojej powłoce, więc jej cd zmienia Twój folder. Skrypt uruchomiony przez ./skrypt.sh tego nie potrafi, bo działa w osobnym procesie.

Sprawdź, co Bash wie o funkcji:

type mkcd
mkcd is a function
mkcd ()
{
    mkdir -p "$1" && cd "$1" || return
}

A unset -f mkcd usuwa funkcję z bieżącej sesji.

return kontra echo, czyli kciuk i kartka

To jest miejsce, w którym mylą się wszyscy, którzy przyszli z innych języków programowania. W Pythonie czy JavaScripcie return oddaje wynik. W Bashu nie.

Funkcja w Bashu może oddać dwie rzeczy, tak jak każda komenda. Kciuk, czyli kod wyjścia od 0 do 255. I kartkę z tekstem, czyli to, co wypisze na pierwszy głośnik. return ustawia kciuk. echo pisze na kartce.

#!/bin/bash
# f2.sh — return kontra echo

czy_root() {
    [ "$(id -u)" -eq 0 ]
}

rozmiar() {
    du -sh "$1" | cut -f1
}

if czy_root; then
    echo "Jestem rootem"
else
    echo "Nie jestem rootem"
fi

r=$(rozmiar /etc)
echo "Folder /etc zajmuje: $r"
Jestem rootem
Folder /etc zajmuje: 18M

czy_root to pytanie typu tak albo nie. Nie ma w niej nawet słowa return. Jeśli go nie napiszesz, funkcja oddaje kciuk ostatniej komendy, którą wykonała. Ostatnią komendą jest bramkarz [ ], więc jego kciuk staje się kciukiem funkcji. Dzięki temu funkcję wstawiasz prosto w if, jak grep -q czy test.

rozmiar to pytanie o wartość. Wynik wypisuje na ekran, a ten, kto pyta, łapie go gońcem $( ) z poprzedniej lekcji.

Zasada na kartkę przy monitorze: tak albo nie, czyli sukces albo porażka, idzie przez return albo kciuk ostatniej komendy. Wszystko inne, czyli liczby, nazwy, ścieżki, teksty, idzie przez echo i $( ).

A co się stanie, jeśli spróbujesz zwrócić przez return prawdziwą liczbę?

za_duzo() {
    return 300
}
za_duzo
echo "kod: $?"
kod: 44

Trzysta zamieniło się w czterdzieści cztery, bo kod wyjścia mieści tylko liczby od 0 do 255 i reszta się „przekręca”, jak licznik kilometrów w starym maluchu. ShellCheck od razu krzyczy: Can only return 0-255. Other data should be written to stdout. A return abc kończy się komunikatem numeric argument required. Kciuk nie umie pokazać słowa.

local, czyli własne pudełka funkcji

Zmienne w Bashu są domyślnie wspólne dla całego skryptu. Funkcja, która użyje zmiennej o tej samej nazwie co skrypt, po cichu ją nadpisze. Jak wspólna lodówka w biurze: ktoś włożył jogurt podpisany „plik”, a ktoś inny, nie patrząc, włożył tam swój i zabrał tamten.

#!/bin/bash
# zasieg.sh — funkcja bez local psuje zmienną skryptu

plik="raport.txt"

policz_logi() {
    for plik in logi/*.log; do
        :
    done
    echo "Ostatni log: $plik"
}

policz_logi
echo "Zapiszę raport do: $plik"
Ostatni log: logi/web.log
Zapiszę raport do: logi/web.log

Skrypt chciał zapisać raport do raport.txt, a teraz zapisze go do logi/web.log. Czyli nadpisze log aplikacji raportem. Taki błąd wychodzi dopiero na produkcji, oczywiście w piątek.

(Dwukropek w pętli to komenda „nic nie rób”. Tu tylko udaje prawdziwą robotę.)

Naprawa to jedna linijka na początku funkcji:

policz_logi() {
    local plik
    for plik in logi/*.log; do
        :
    done
    echo "Ostatni log: $plik"
}
Ostatni log: logi/web.log
Zapiszę raport do: raport.txt

local plik mówi: „na czas działania tej funkcji mam swoje własne pudełko o tej nazwie”. Funkcja bawi się swoim, a pudełko skryptu leży nietknięte. Kiedy funkcja się kończy, jej pudełko znika, a nazwa wraca do poprzedniej wartości.

Zasada: każda zmienna, której używasz tylko w funkcji, dostaje local. Bez wyjątków. Tak jak w biurowej lodówce: podpisuj jogurty.

Jak to działa pod spodem

Kiedy Bash czyta definicję funkcji, nie wykonuje jej. Tylko zapamiętuje: „pod nazwą log jest taki kawałek kodu”. Dopiero kiedy ktoś wpisze log, urzędnik sprawdza w kolejności: czy to alias, czy to słowo kluczowe, czy to funkcja, czy to wbudowana komenda, a na samym końcu szuka programu w folderach z PATH. Funkcje są przed programami. Dlatego funkcja o nazwie ls przykryje prawdziwe ls. Możesz się tak zabawić z kolegą, ale nie zdziw się, jak kolegą przestanie być.

Funkcja nie uruchamia nowego procesu. Wykonuje się w tej samej powłoce co skrypt, przez tego samego kucharza. Dlatego jest szybka, dlatego może zmienić folder przez cd i dlatego bez local może popsuć zmienne skryptu. To wszystko jedna i ta sama cecha.

Co robi Bash przy wywołaniu? Odkłada na bok argumenty skryptu, podstawia na ich miejsce argumenty funkcji, wykonuje jej ciało, a na końcu przywraca argumenty skryptu. Przy local robi to samo ze zmiennymi: zapamiętuje starą wartość i przywraca ją przy wyjściu z funkcji.

Ściąga flag

KomendaFlaga / elementCo robi
definicjanazwa() { ...; }tworzy funkcję
definicjafunction nazwa { ...; }to samo, składnia tylko Basha
argumenty$1, $#, "$@"argumenty funkcji, nie skryptu
locallocal x=5zmienna widoczna tylko w funkcji
returnreturn 0 / return 1kończy funkcję z kodem 0–255
returnreturn bez liczbyoddaje kod ostatniej komendy
echoecho "$wynik"oddaje wynik do złapania przez $( )
echoecho "..." >&2komunikat na drugi głośnik, nie do wyniku
typetype nazwaczy to funkcja, alias, czy program, i jaka treść
declare-F / -flista funkcji / ich treść
unset-f nazwausuwa funkcję

⚠ Typowy błąd

Funkcja ma zwrócić wolne miejsce na dysku. Dla wygody dopisujesz komunikat, co robi:

#!/bin/bash
# debug.sh — komunikat diagnostyczny wpada do wyniku

wolne_miejsce() {
    echo "Sprawdzam dysk $1..."
    df --output=avail -h "$1" | tail -n 1
}

wolne=$(wolne_miejsce /)
echo "Wolne: [$wolne]"
Wolne: [Sprawdzam dysk /...
  26G]

W zmiennej wylądowało wszystko, co funkcja wypisała. Także Twoja uprzejma informacja. Goniec $( ) nie odróżnia wyniku od gadania, zabiera całą kartkę. Jeśli potem porównasz to z liczbą albo użyjesz jako nazwy pliku, skrypt się wyłoży w miejscu, które z funkcją nie ma nic wspólnego.

Naprawa: komunikaty dla człowieka na drugi głośnik.

wolne_miejsce() {
    echo "Sprawdzam dysk $1..." >&2
    df --output=avail -h "$1" | tail -n 1
}
Sprawdzam dysk /...
Wolne: [  26G]

Komunikat dalej widać na ekranie, ale do zmiennej trafił tylko wynik. >&2 to dokładnie ten sam drugi głośnik, który poznaliśmy przy przekierowaniach: wyniki pierwszym, gadanie i błędy drugim.

I drugi, krótki: funkcja musi być zdefiniowana, zanim jej użyjesz. Bash czyta skrypt od góry i wywołanie przed definicją kończy się tak:

kolej.sh: line 2: przywitaj: command not found

Dlatego w porządnym skrypcie wszystkie funkcje stoją na górze, a właściwa robota na dole. ShellCheck to wyłapie: This function is only defined later. Move the definition up.

✎ Zadanie

Napisz funkcję kopia, która dostaje nazwę pliku i robi jego kopię z dzisiejszą datą w nazwie (np. notatki.txt.2026-11-03). Jeśli plik nie istnieje, wypisuje błąd i zwraca 1.

Pokaż rozwiązanie

Zadanie było takie: funkcja kopia, która dostaje nazwę pliku i robi jego kopię z dzisiejszą datą w nazwie. A jak pliku nie ma, krzyczy i zwraca 1.

#!/bin/bash
# kopia PLIK – robi kopię pliku z dzisiejszą datą w nazwie
kopia() {
    local plik=$1
    local cel
    cel="$plik.$(date +%F)"
    if [[ ! -f $plik ]]; then
        echo "kopia: nie ma takiego pliku: '$plik'" >&2
        return 1
    fi
    cp -p -- "$plik" "$cel"
    echo "Zrobione: $cel"
}

Sprawdzamy. Wczytuję plik z funkcją przez source i naciskam przycisk w windzie:

source ./kopia.sh
kopia notatki.txt; echo "kod: $?"
kopia nie_ma.txt; echo "kod: $?"
kopia; echo "kod: $?"
Zrobione: notatki.txt.2026-11-03
kod: 0
kopia: nie ma takiego pliku: 'nie_ma.txt'
kod: 1
kopia: nie ma takiego pliku: ''
kod: 1

Kilka rzeczy wartych uwagi. date +%F daje datę w formacie rok-miesiąc-dzień, czyli taką, która sortuje się sama. local, żeby funkcja nie nabałaganiła w zmiennych reszty skryptu. Komunikat błędu idzie przez >&2, czyli drugim głośnikiem, tym od błędów. cp -p zachowuje datę modyfikacji i uprawnienia oryginału, a -- mówi „koniec opcji”, na wypadek gdyby ktoś nazwał plik -rf. Tak, ludzie tak robią. Zwykle raz.

I zauważ ostatni przypadek: wywołanie bez argumentu. Funkcja mówi „nie ma pliku ''”. Działa, ale wygląda jak urzędnik, który na pusty formularz odpowiada „Pan Nikt nie figuruje w systemie”. Dziś nauczysz się to zrobić ładniej.

Co dalej

W zadaniu doklejasz datę do nazwy pliku. A co, jeśli chciałbyś z nazwy coś wyciąć, na przykład końcówkę .txt, albo zamienić spacje na podkreślniki, bez wołania żadnych zewnętrznych programów? Następna lekcja to nożyczki do tekstu wbudowane w samego Basha.