0Pricing
Linux Command Line & Bash Scripting Mastery · Lekcja

Projektowanie funkcji z zakresem lokalnym i kodami zwrotnymi

Pisz funkcje korzystające ze zmiennych lokalnych, statusów zakończenia i wartości zwracanych przez printf zamiast zawodnych zmiennych globalnych.

Projektowanie funkcji z zakresem lokalnym i kodami zwrotnymi to bezpłatna lekcja Linux Command Line & Bash Scripting Mastery na CoddyKit. To lekcja 1 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Linux Command Line & Bash Scripting Mastery, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Linux Command Line & Bash Scripting Mastery zawiera 4 lekcji w sumie.

Dlaczego zasięg funkcji ma znaczenie

W Bash zmienne są domyślnie globalne. Zmienna ustawiona wewnątrz funkcji przenika do zasięgu wywołującego, chyba że zostanie jawnie zadeklarowana jako local. Jest to częste źródło trudnych do wykrycia błędów w skryptach powłoki.

  • Funkcje bez local mogą po cichu nadpisywać zmienne wywołującego.
  • Zmienne local są usuwane po zakończeniu działania funkcji.
  • Wyraźne granice zasięgu sprawiają, że funkcje są wielokrotnego użytku i łatwe do testowania w izolacji.

W tej lekcji nauczy się Pan/Pani pisać samodzielne funkcje: używać zmiennych local, przekazywać wyniki za pomocą kodów wyjścia i printf oraz nigdy nie polegać na niejawnym stanie globalnym.

Problem wycieku do zakresu globalnego

Oto konkretny przykład wycieku zmiennej globalnej. Funkcja set_name ustawia zmienną o nazwie result, przez co po cichu nadpisuje własną zmienną result wywołującego.

Uruchom ten skrypt i obserwuj nieoczekiwany wynik — zmienna result wywołującego znika po wywołaniu funkcji.

#!/usr/bin/env bash

set_name() {
    result="Alice"   # No 'local' — this is GLOBAL
}

result="important data"
echo "Before: $result"

set_name

echo "After:  $result"   # Prints 'Alice', not 'important data'

Deklarowanie zmiennych lokalnych za pomocą „local”

Wbudowane polecenie local ogranicza zakres zmiennej do otaczającej funkcji oraz wszystkich funkcji przez nią wywoływanych. Poza funkcją zmienna jest albo nieustawiona, albo zachowuje swoją wcześniejszą wartość.

  • local varname — deklaruje zmienną bez przypisywania wartości.
  • local varname="value" — deklaruje zmienną i przypisuje jej wartość w jednym kroku.
  • local -i count=0 — deklaruje lokalną zmienną typu całkowitego.
  • local -r PI=3.14159 — deklaruje lokalną stałą tylko do odczytu.

Dobra praktyka: każdą zmienną wewnątrz funkcji należy deklarować jako local, chyba że celowo ma być globalna.

#!/usr/bin/env bash

greet() {
    local name="$1"          # local — safe
    local greeting="Hello, ${name}!"
    echo "$greeting"
}  # 'name' and 'greeting' vanish here

name="global value"
greet "Bob"
echo "name is still: $name"   # Prints 'global value'

Kody zakończenia jako wartości zwracane

Funkcje Bash nie mogą zwracać ciągów znaków za pomocą return — return ustawia wyłącznie całkowity status zakończenia (0–255). Przyjęta konwencja jest następująca:

  • return 0 — powodzenie
  • return 1 (lub dowolna wartość różna od zera) — niepowodzenie

Wywołujący odczytuje status zakończenia za pomocą $? bezpośrednio po wywołaniu albo używa funkcji bezpośrednio jako warunku if. Kody zakończenia to idiomatyczny sposób sygnalizowania powodzenia lub niepowodzenia funkcji.

#!/usr/bin/env bash

is_even() {
    local -i n="$1"
    (( n % 2 == 0 ))   # arithmetic command: exits 0 if true, 1 if false
}

for num in 2 3 7 10; do
    if is_even "$num"; then
        echo "$num is even"
    else
        echo "$num is odd"
    fi
done

Przekazywanie wyników tekstowych za pomocą printf

Gdy funkcja musi zwrócić wynik tekstowy, standardowy wzorzec polega na wypisaniu go na stdout i przechwyceniu za pomocą substytucji poleceń $(). Użycie printf zamiast echo jest preferowane, ponieważ:

  • printf domyślnie nie dodaje końcowego znaku nowej linii (chyba że dołączą Państwo \n).
  • Zachowanie printf jest spójne i zdefiniowane przez POSIX, natomiast echo różni się w zależności od powłoki.
  • Substytucja poleceń usuwa końcowe znaki nowej linii, dlatego printf '%s' "$value" zapewnia precyzyjny wynik.
#!/usr/bin/env bash

to_uppercase() {
    local input="$1"
    printf '%s' "${input^^}"   # Bash 4+ parameter expansion
}

word="hello"
upper=$(to_uppercase "$word")
echo "Original: $word"
echo "Upper:    $upper"

Łączenie kodów zakończenia z wyjściem stdout

Dobrze zaprojektowana funkcja może jednocześnie wypisać wynik (w przypadku powodzenia) i zasygnalizować niepowodzenie (za pomocą kodu zakończenia). Wywołujący na podstawie kodu zakończenia decyduje, co zrobić, zanim zaufa danym wyjściowym.

Poniższy wzorzec jest szeroko stosowany w rzeczywistych bibliotekach Bash:

  • W przypadku powodzenia: printf wypisuje wynik, a funkcja wykonuje return 0.
  • W przypadku niepowodzenia: komunikat diagnostyczny jest zapisywany na stderr (nie na stdout), a funkcja wykonuje return 1.
  • Zapisywanie błędów na stderr pozostawia stdout wolne do przekierowywania potokami.
#!/usr/bin/env bash

divide() {
    local -i numerator="$1"
    local -i denominator="$2"

    if (( denominator == 0 )); then
        printf 'Error: division by zero\n' >&2
        return 1
    fi

    printf '%d' $(( numerator / denominator ))
    return 0
}

if result=$(divide 20 4); then
    echo "20 / 4 = $result"
else
    echo "Division failed."
fi

if result=$(divide 10 0); then
    echo "10 / 0 = $result"
else
    echo "Division failed (caught the error)."
fi

Ochrona funkcji rekurencyjnych za pomocą „local”

Rekurencja jest jednym z najwyraźniejszych przykładów pokazujących, dlaczego local jest niezbędne. Każde rekurencyjne wywołanie otrzymuje własną, niezależną kopię każdej zmiennej local na stosie wywołań. Bez local każde wywołanie nadpisywałoby tę samą zmienną globalną i zwracało nieprawidłowe wyniki.

Poniższa funkcja silni jest bezpieczna, ponieważ zmienne n i sub są lokalne dla każdej ramki stosu.

#!/usr/bin/env bash

factorial() {
    local -i n="$1"
    local -i sub

    if (( n <= 1 )); then
        printf '1'
        return 0
    fi

    sub=$(factorial $(( n - 1 )))
    printf '%d' $(( n * sub ))
}

for i in 1 2 3 4 5 6; do
    echo "${i}! = $(factorial $i)"
done

Unikanie pułapki podpowłoki za pomocą local -n (nameref)

Substytucja poleceń $() jest wykonywana w podpowłoce. Przypisania zmiennych wykonane wewnątrz niej są niewidoczne dla powłoki nadrzędnej. Gdy funkcja musi zapisać wartość do zmiennej przekazanej przez wywołującego, bez używania podpowłoki, należy użyć odwołania po nazwie (local -n), dostępnego w Bash 4.3 i nowszych.

  • local -n ref="$1" tworzy z ref alias zmiennej, której nazwa jest przechowywana w $1.
  • Przypisanie wartości do ref wewnątrz funkcji bezpośrednio zmienia zmienną wywołującego.
  • Pozwala to uniknąć podpowłoki, a jednocześnie zachować szczegóły implementacji w zakresie lokalnym.
#!/usr/bin/env bash

# Fills caller's array by reference — no subshell needed
read_csv_line() {
    local -n _out="$1"    # nameref to caller's variable
    local line="$2"
    local IFS=','
    read -ra _out <<< "$line"
}

declare -a fields
read_csv_line fields "alice,30,engineer"

echo "Name:  ${fields[0]}"
echo "Age:   ${fields[1]}"
echo "Role:  ${fields[2]}"

Tworzenie małej biblioteki funkcji

W rzeczywistych projektach Bash funkcje wielokrotnego użytku są umieszczane w plikach bibliotecznych, wczytywanych przez skrypty za pomocą source (lub operatora kropki .). Zasady dobrego projektowania bibliotek:

  • Każda zmienna wewnątrz funkcji bibliotecznej musi być zadeklarowana jako local.
  • Funkcje biblioteczne nigdy nie wykonują exit — używają return, dzięki czemu wywołujący może działać dalej.
  • Należy stosować spójny prefiks przestrzeni nazw (np. str_, log_), aby uniknąć kolizji nazw.
  • Należy zabezpieczyć się przed wielokrotnym wczytaniem za pomocą zmiennej sygnalizacyjnej.

Poniżej znajduje się minimalna biblioteka narzędzi do obsługi ciągów znaków, zgodna z tymi konwencjami.

#!/usr/bin/env bash
# lib/str.sh  — string utility library

[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1

str_trim() {
    local str="$1"
    str="${str#"${str%%[![:space:]]*}"}"
    str="${str%"${str##*[![:space:]]}"}" 
    printf '%s' "$str"
}

str_repeat() {
    local -i times="$2"
    local char="$1"
    local -i i
    for (( i = 0; i < times; i++ )); do
        printf '%s' "$char"
    done
}

str_contains() {
    local haystack="$1"
    local needle="$2"
    [[ "$haystack" == *"$needle"* ]]
}

# --- self-test when executed directly ---
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    trimmed=$(str_trim "   hello world   ")
    echo "Trimmed: '${trimmed}'"
    str_repeat '-' 20; echo
    if str_contains "bash scripting" "script"; then
        echo "Contains: yes"
    fi
fi

Sprawdzanie argumentów wewnątrz funkcji

Funkcje otrzymujące argumenty powinny sprawdzać je na początku i zwracać określony kod zakończenia w przypadku nieprawidłowych danych wejściowych. Nazywa się to wzorcem klauzuli ochronnej — szybkie i jednoznaczne kończenie działania w razie błędu.

  • Liczbę argumentów należy sprawdzać za pomocą $#.
  • Typy lub formaty należy zweryfikować przed wykonaniem jakichkolwiek operacji.
  • Komunikaty diagnostyczne należy wypisywać wyłącznie na stderr, nigdy na stdout.
  • Należy używać różnych niezerowych kodów zwrotu (np. 1 = nieprawidłowe argumenty, 2 = nie znaleziono pliku), aby wywołujący mógł odmiennie reagować na różne przyczyny niepowodzenia.
#!/usr/bin/env bash

file_line_count() {
    if (( $# != 1 )); then
        printf 'Usage: file_line_count <file>\n' >&2
        return 1
    fi

    local file="$1"

    if [[ ! -f "$file" ]]; then
        printf 'Error: not a file: %s\n' "$file" >&2
        return 2
    fi

    if [[ ! -r "$file" ]]; then
        printf 'Error: cannot read: %s\n' "$file" >&2
        return 3
    fi

    local -i count
    count=$(wc -l < "$file")
    printf '%d' "$count"
    return 0
}

# Test with /etc/hosts (exists on every Linux/macOS system)
if lines=$(file_line_count /etc/hosts); then
    echo "/etc/hosts has $lines lines"
else
    echo "Failed with exit code: $?"
fi

Połączenie wszystkiego: przykład z praktyki

Oto kompletny, samodzielny skrypt pokazujący współdziałanie wszystkich pojęć z tej lekcji:

  • Zmienne local w każdej funkcji.
  • Kody zakończenia do sygnalizowania powodzenia lub niepowodzenia.
  • printf do przekazywania wyników tekstowych.
  • Błędy zapisywane na stderr, a wyniki na stdout.
  • Klauzule ochronne do sprawdzania argumentów.

Prześledź przepływ działania: parse_version wyodrębnia dane, version_ge je porównuje, a main korzysta z obu funkcji w uporządkowany sposób.

#!/usr/bin/env bash

# Parse a semver string into components via nameref
parse_version() {
    local -n _major="$2" _minor="$3" _patch="$4"
    local version="$1"
    local IFS='.'
    local -a parts
    read -ra parts <<< "$version"
    _major="${parts[0]:-0}"
    _minor="${parts[1]:-0}"
    _patch="${parts[2]:-0}"
}

# Return 0 if version $1 >= version $2
version_ge() {
    local -i maj_a min_a pat_a
    local -i maj_b min_b pat_b
    parse_version "$1" maj_a min_a pat_a
    parse_version "$2" maj_b min_b pat_b

    if   (( maj_a != maj_b )); then (( maj_a > maj_b ))
    elif (( min_a != min_b )); then (( min_a > min_b ))
    else                             (( pat_a >= pat_b ))
    fi
}

require_bash_version() {
    local required="$1"
    local actual="${BASH_VERSION%%(*}"
    if version_ge "$actual" "$required"; then
        printf 'Bash %s satisfies >= %s\n' "$actual" "$required"
        return 0
    else
        printf 'Error: need Bash >= %s, got %s\n' "$required" "$actual" >&2
        return 1
    fi
}

main() {
    require_bash_version "4.3" || return 1
    require_bash_version "99.0" || true   # demonstrates failure path
}

main

Sprawdzenie wiedzy: zmienne lokalne i wartości zwracane

Rozważ następującą funkcję Bash. Jaki jest prawidłowy sposób przechwycenia jej wyniku tekstowego przez wywołującego i które stwierdzenie dotyczące zmiennej tmp jest prawdziwe?

transform() {
    local tmp="${1,,}"   # lowercase
    printf '%s' "$tmp"
    return 0
}

Podsumowanie: funkcje z lokalnym zakresem i kodami zwrotu

W tej lekcji nauczyli się Państwo pisać funkcje Bash, które są przejrzyste, łatwe do łączenia i bezpieczne:

  • Zawsze używaj local dla zmiennych wewnątrz funkcji, aby nie zanieczyszczać zakresu wywołującego.
  • Używaj kodów zakończenia (return 0/1/N) do sygnalizowania powodzenia lub niepowodzenia — naturalnie współpracują z if, && i ||.
  • Używaj printf do stdout, aby przekazywać wyniki tekstowe; przechwytuj je przez $() u wywołującego.
  • Zapisuj błędy na stderr (>&2), aby stdout pozostał wolny na potrzeby przepływu danych i potoków.
  • Używaj local -n (nameref), gdy trzeba zapisać wartość do zmiennej przekazanej przez wywołującego bez narzutu podpowłoki.
  • Klauzule ochronne (wczesne sprawdzanie argumentów i natychmiastowy zwrot w przypadku nieprawidłowych danych) zwiększają niezawodność funkcji i jasno dokumentują ich działanie.
  • Pliki biblioteczne powinny być wczytywane za pomocą source, używać prefiksów przestrzeni nazw, nigdy nie wywoływać exit i chronić się przed wielokrotnym wczytaniem.

Opanowanie tych wzorców odróżnia kruche, jednorazowe skrypty od profesjonalnych i łatwych w utrzymaniu baz kodu Bash.

Często zadawane pytania

Czy lekcja „Projektowanie funkcji z zakresem lokalnym i kodami zwrotnymi” jest bezpłatna?

Tak — pełny tekst „Projektowanie funkcji z zakresem lokalnym i kodami zwrotnymi” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Linux Command Line & Bash Scripting Mastery, przejdź na CoddyKit PRO. Kurs Linux Command Line & Bash Scripting Mastery zawiera 4 lekcji w sumie.

Co nauczysz się w „Projektowanie funkcji z zakresem lokalnym i kodami zwrotnymi”?

Pisz funkcje korzystające ze zmiennych lokalnych, statusów zakończenia i wartości zwracanych przez printf zamiast zawodnych zmiennych globalnych. Ćwiczysz Linux Command Line & Bash Scripting Mastery z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Linux Command Line & Bash Scripting Mastery?

Nie wymagamy żadnego doświadczenia. Linux Command Line & Bash Scripting Mastery w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 1 z 4.

Ile czasu zajmuje lekcja „Projektowanie funkcji z zakresem lokalnym i kodami zwrotnymi”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Linux Command Line & Bash Scripting Mastery?

Tak. Każda lekcja Linux Command Line & Bash Scripting Mastery zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Projektowanie funkcji z zakresem lokalnym i kodami zwrotnymi
  2. Budowanie i dołączanie wielokrotnego użytku bibliotek Bash
  3. Analizowanie flag i argumentów za pomocą getopts
  4. Przekazywanie tablic i map asocjacyjnych między funkcjami
← Powrót do Linux Command Line & Bash Scripting Mastery