DevOps-bootcamp · Les

Functies ontwerpen met lokale scope en returncodes

Schrijf functies die lokale variabelen, exitstatussen en op printf gebaseerde returnwaarden gebruiken in plaats van kwetsbare globals.

Les 1 van 413 stappen

Functies ontwerpen met lokale scope en returncodes is een gratis DevOps-bootcamp-les op CoddyKit. Dit is les 1 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject DevOps-bootcamp. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus DevOps-bootcamp bevat in totaal 4 lessen.

Waarom de reikwijdte van functies belangrijk is

In Bash zijn variabelen standaard globaal. Een variabele die je binnen een functie instelt, lekt naar de reikwijdte van de aanroeper, tenzij je deze expliciet als local declareert. Dit is een veelvoorkomende bron van subtiele fouten in shellscripts.

  • Functies zonder local kunnen variabelen van de aanroeper stilletjes overschrijven.
  • local-variabelen worden verwijderd wanneer de functie terugkeert.
  • Duidelijke grenzen van de reikwijdte maken functies herbruikbaar en testbaar in isolatie.

In deze les leer je zelfvoorzienende functies schrijven: ze gebruiken local-variabelen, geven resultaten door via afsluitcodes en printf en vertrouwen nooit op impliciete globale toestand.

Het probleem van globale lekken

Hier is een concreet voorbeeld van een lek van een globale variabele. De functie set_name stelt een variabele met de naam result in, waarmee de eigen result-variabele van de aanroeper ongemerkt wordt overschreven.

Voer dit script uit en bekijk de onverwachte uitvoer — de result van de aanroeper is verdwenen nadat de functie is aangeroepen.

#!/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'

Lokale variabelen declareren met 'local'

De ingebouwde opdracht local beperkt de scope van een variabele tot de omringende functie en alle functies die deze aanroept. Buiten de functie is de variabele niet ingesteld of behoudt deze de vorige waarde.

  • local varname — declareert zonder een waarde toe te kennen.
  • local varname="value" — declareert de variabele en kent er in één stap een waarde aan toe.
  • local -i count=0 — declareert een lokale variabele met een integer-type.
  • local -r PI=3.14159 — declareert een alleen-lezenconstante.

Best practice: declareer elke variabele binnen een functie als local, tenzij je deze bewust globaal nodig hebt.

#!/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'

Exitcodes als retourwaarden

Bash-functies kunnen geen tekenreeksen returnen — return stelt alleen een gehele exitstatus in (0–255). Volgens de conventie geldt:

  • return 0 — geslaagd
  • return 1 (of een andere waarde die niet nul is) — mislukt

De aanroeper leest de exitstatus via $? direct na de aanroep, of gebruikt de functie rechtstreeks in een if-voorwaarde. Exitcodes zijn de gebruikelijke manier om succes of mislukking van een functie door te geven.

#!/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

Tekenreeksresultaten doorgeven via printf

Wanneer een functie een tekenreeksresultaat moet retourneren, is het gebruikelijke patroon om naar stdout te schrijven en de uitvoer vast te leggen met opdrachtvervanging $(). printf heeft de voorkeur boven echo, omdat:

  • printf standaard geen afsluitende nieuwe regel toevoegt (tenzij je \n opneemt).
  • het gedrag van printf consistent is en door POSIX wordt vastgelegd; echo verschilt per shell.
  • opdrachtvervanging afsluitende nieuwe regels verwijdert, waardoor printf '%s' "$value" precies werkt.
#!/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"

Exitcodes en stdout-uitvoer combineren

Een goed ontworpen functie kan tegelijk een resultaat afdrukken (bij succes) en een mislukking signaleren (via de exitcode). De aanroeper bepaalt op basis van de exitcode wat er moet gebeuren, voordat de uitvoer wordt vertrouwd.

Het onderstaande patroon wordt veel gebruikt in echte Bash-bibliotheken:

  • Bij succes: gebruik printf voor het resultaat en return 0.
  • Bij mislukking: schrijf een diagnose naar stderr (niet naar stdout) en gebruik return 1.
  • Door fouten naar stderr te schrijven, blijft stdout schoon voor doorsturen via een pijplijn.
#!/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

'local' gebruiken om recursieve functies te beschermen

Recursie laat als een van de duidelijkste voorbeelden zien waarom local essentieel is. Elke recursieve aanroep krijgt een eigen, onafhankelijke kopie van elke local-variabele op de aanroepstack. Zonder local zou elke aanroep dezelfde globale variabele overschrijven en onjuiste resultaten opleveren.

De onderstaande faculteitsfunctie is veilig, omdat n en sub lokaal zijn voor elk stackframe.

#!/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

De subshell-valkuil vermijden met local -n (nameref)

Opdrachtvervanging $() wordt uitgevoerd in een subshell. Variabeletoewijzingen daarin zijn niet zichtbaar in de bovenliggende shell. Wanneer je wilt dat een functie naar een door de aanroeper aangeleverde variabele schrijft zonder een subshell te gebruiken, gebruik je een nameref (local -n), beschikbaar in Bash 4.3 en hoger.

  • local -n ref="$1" maakt van ref een alias voor de variabele waarvan de naam in $1 staat.
  • Een toewijzing aan ref binnen de functie wijzigt de variabele van de aanroeper rechtstreeks.
  • Zo vermijd je een subshell, terwijl de implementatiedetails lokaal blijven.
#!/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]}"

Een kleine functiebibliotheek bouwen

Echte Bash-projecten verdelen herbruikbare functies over bibliotheekbestanden die door scripts worden ingeladen met source (of de puntoperator .). Goede regels voor bibliotheekontwerp:

  • Elke variabele binnen een bibliotheekfunctie moet local zijn.
  • Bibliotheekfuncties gebruiken nooit exit — ze gebruiken return, zodat de aanroeper actief blijft.
  • Gebruik een consistent naamruimtevoorvoegsel (bijvoorbeeld str_, log_) om naamconflicten te voorkomen.
  • Voorkom dubbel inladen met een wachtvariabele.

Hieronder staat een minimale bibliotheek voor tekenreeksfuncties die deze conventies volgt.

#!/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

Argumenten binnen functies valideren

Functies die argumenten ontvangen, moeten deze vroegtijdig valideren en bij ongeldige invoer een specifieke exitcode retourneren. Dit heet het patroon van de beveiligingsclausule — faal snel en duidelijk.

  • Controleer het aantal argumenten met $#.
  • Valideer typen of indelingen voordat je werk uitvoert.
  • Druk diagnoseberichten uitsluitend af naar stderr, nooit naar stdout.
  • Gebruik verschillende exitcodes die niet nul zijn (bijvoorbeeld 1 = onjuiste argumenten, 2 = bestand niet gevonden), zodat aanroepers verschillend kunnen reageren op verschillende manieren waarop iets kan mislukken.
#!/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

Alles samenbrengen: een voorbeeld uit de praktijk

Hier is een compleet, zelfstandig script dat alle concepten uit deze les samenwerkend demonstreert:

  • local-variabelen in elke functie.
  • Exitcodes om succes of mislukking te signaleren.
  • printf om tekenreeksresultaten door te geven.
  • Fouten naar stderr en resultaten naar stdout schrijven.
  • Beveiligingsclausules voor argumentvalidatie.

Bestudeer de uitvoeringsstroom: parse_version haalt gegevens op, version_ge vergelijkt deze en main gebruikt beide op een nette manier.

#!/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

Kennistoets: lokale variabelen en retourwaarden

Bekijk de volgende Bash-functie. Wat is de juiste manier om het tekenreeksresultaat ervan in de aanroeper vast te leggen, en welke uitspraak over de variabele tmp is waar?

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

Samenvatting: functies met lokale scope en retourcodes

In deze les heb je geleerd hoe je schone, combineerbare en veilige Bash-functies schrijft:

  • Gebruik altijd local voor variabelen binnen functies om te voorkomen dat je de scope van de aanroeper vervuilt.
  • Gebruik exitcodes (return 0/1/N) om succes of mislukking te signaleren — ze werken vanzelfsprekend samen met if, && en ||.
  • Gebruik printf naar stdout om tekenreeksresultaten door te geven; leg ze in de aanroeper vast met $().
  • Schrijf fouten naar stderr (>&2), zodat stdout schoon blijft voor gegevensstromen en doorsturen via een pijplijn.
  • Gebruik local -n (nameref) wanneer je naar een door de aanroeper aangeleverde variabele moet schrijven zonder de overhead van een subshell.
  • Beveiligingsclausules (argumenten vroeg valideren en bij ongeldige invoer meteen retourneren) maken functies robuust en verklaren hun werking duidelijk.
  • Bibliotheekbestanden moeten worden ingeladen, naamruimtevoorvoegsels gebruiken, nooit exit aanroepen en dubbel inladen voorkomen.

Het beheersen van deze patronen maakt het verschil tussen kwetsbare eenmalige scripts en professionele, onderhoudbare Bash-codebases.

Gratis beginnen

Leer DevOps-bootcamp met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
142
Lessen
568

Veelgestelde vragen

Is de les “Functies ontwerpen met lokale scope en returncodes” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad DevOps-bootcamp, waaronder “Functies ontwerpen met lokale scope en returncodes”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus DevOps-bootcamp bevat in totaal 4 lessen.

Wat leer ik in “Functies ontwerpen met lokale scope en returncodes”?

Schrijf functies die lokale variabelen, exitstatussen en op printf gebaseerde returnwaarden gebruiken in plaats van kwetsbare globals. Je oefent met DevOps-bootcamp door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met DevOps-bootcamp te beginnen?

Ervaring vooraf is niet nodig. DevOps-bootcamp op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 1 van 4.

Hoe lang duurt de les “Functies ontwerpen met lokale scope en returncodes”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over DevOps-bootcamp?

Ja. Elke les over DevOps-bootcamp bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Functies ontwerpen met lokale scope en returncodes
  2. Herbruikbare Bash-bibliotheken bouwen en sourcen
  3. Flags en argumenten parsen met getopts
  4. Arrays en associatieve maps tussen functies doorgeven
← Terug naar DevOps-bootcamp