0Pricing
DevOps Bootcamp · Lezione

Progettazione di funzioni con ambito locale e codici di ritorno

Scriva funzioni che usano variabili locali, stati di uscita e valori di ritorno basati su printf invece di globali fragili.

Progettazione di funzioni con ambito locale e codici di ritorno è una lezione DevOps Bootcamp gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DevOps Bootcamp, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DevOps Bootcamp include 4 lezioni in totale.

Perché l'ambito delle funzioni è importante

In Bash, le variabili sono globali per impostazione predefinita. Una variabile impostata all'interno di una funzione si propaga all'ambito del chiamante, a meno che non venga dichiarata esplicitamente local. Questa è una fonte comune di bug difficili da individuare negli script della shell.

  • Le funzioni senza local possono sovrascrivere silenziosamente le variabili del chiamante.
  • Le variabili local vengono eliminate quando la funzione restituisce il controllo.
  • Confini chiari tra gli ambiti rendono le funzioni riutilizzabili e testabili in isolamento.

Questa lezione insegna a scrivere funzioni autonome: usano variabili local, comunicano i risultati tramite codici di uscita e printf e non si affidano mai a uno stato globale implicito.

Il problema delle perdite globali

Ecco un esempio concreto di perdita di una variabile globale. La funzione set_name imposta una variabile chiamata result, sovrascrivendo silenziosamente la variabile result del chiamante.

Esegua questo script e osservi l'output imprevisto: la variabile result del chiamante scompare dopo la chiamata alla funzione.

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

Dichiarare variabili locali con «local»

Il comando incorporato local limita l'ambito di una variabile alla funzione che la contiene e a tutte le funzioni da essa chiamate. Al di fuori della funzione, la variabile non è impostata oppure conserva il valore precedente.

  • local varname — dichiara la variabile senza assegnarle un valore.
  • local varname="value" — dichiara la variabile e le assegna un valore in un unico passaggio.
  • local -i count=0 — dichiara una variabile locale di tipo intero.
  • local -r PI=3.14159 — dichiara una costante locale di sola lettura.

Buona pratica: dichiari ogni variabile all'interno di una funzione come local, a meno che non sia necessario intenzionalmente renderla globale.

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

Codici di uscita come valori restituiti

Le funzioni Bash non possono usare return per restituire stringhe: return imposta esclusivamente uno stato di uscita intero (0–255). Per convenzione:

  • return 0 — operazione riuscita
  • return 1 (o qualsiasi valore diverso da zero) — errore

Il chiamante legge lo stato di uscita tramite $? subito dopo la chiamata oppure usa direttamente la funzione nella condizione di un if. I codici di uscita sono il modo idiomatico per segnalare il successo o l'errore di una funzione.

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

Comunicare risultati stringa tramite printf

Quando una funzione deve restituire un risultato stringa, il modello standard consiste nello scrivere su stdout e acquisire il risultato con la sostituzione di comando $(). È preferibile usare printf invece di echo perché:

  • printf non aggiunge per impostazione predefinita un carattere di nuova riga finale (a meno che non si includa \n).
  • Il comportamento di printf è coerente ed è definito da POSIX; echo varia tra le shell.
  • La sostituzione di comando rimuove i caratteri di nuova riga finali, quindi printf '%s' "$value" è preciso.
#!/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"

Combinare codici di uscita e output su stdout

Una funzione ben progettata può sia stampare un risultato (in caso di successo) sia segnalare un errore (tramite il codice di uscita) contemporaneamente. Il chiamante decide come procedere in base al codice di uscita, prima di considerare attendibile l'output.

Il modello seguente è ampiamente utilizzato nelle librerie Bash reali:

  • In caso di successo: usare printf per il risultato e return 0.
  • In caso di errore: scrivere una diagnostica su stderr (non su stdout) e usare return 1.
  • Scrivere gli errori su stderr mantiene stdout pulito per il piping.
#!/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

Usare «local» per proteggere le funzioni ricorsive

La ricorsione è una delle dimostrazioni più chiare del motivo per cui local è essenziale. Ogni chiamata ricorsiva riceve una copia indipendente di ogni variabile local nello stack delle chiamate. Senza local, ogni chiamata sovrascriverebbe la stessa variabile globale, producendo risultati errati.

La funzione fattoriale seguente è sicura perché n e sub sono locali a ciascun frame dello stack.

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

Evitare la trappola della subshell con local -n (Nameref)

La sostituzione di comando $() viene eseguita in una subshell. Qualsiasi assegnazione di variabile al suo interno è invisibile alla shell padre. Quando è necessario che una funzione scriva in una variabile fornita dal chiamante senza usare una subshell, si utilizzi un nameref (local -n), disponibile in Bash 4.3 e versioni successive.

  • local -n ref="$1" rende ref un alias della variabile il cui nome è memorizzato in $1.
  • L'assegnazione a ref all'interno della funzione modifica direttamente la variabile del chiamante.
  • In questo modo si evita una subshell, mantenendo comunque locali i dettagli dell'implementazione.
#!/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]}"

Creare una piccola libreria di funzioni

Nei progetti Bash reali, le funzioni riutilizzabili vengono suddivise in file di libreria caricati dagli script con source (o con l'operatore punto .). Regole per una buona progettazione delle librerie:

  • Ogni variabile all'interno di una funzione di libreria deve essere local.
  • Le funzioni di libreria non usano mai exit: usano return, così il chiamante rimane in esecuzione.
  • Utilizzi un prefisso di namespace coerente (ad esempio str_, log_) per evitare conflitti tra nomi.
  • Prevenga il caricamento multiplo usando una variabile sentinella.

Di seguito è riportata una libreria minima di utilità per le stringhe che segue queste convenzioni.

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

Convalidare gli argomenti all'interno delle funzioni

Le funzioni che ricevono argomenti dovrebbero convalidarli subito e restituire un codice di uscita specifico in caso di input non valido. Questo è il cosiddetto modello della clausola di guardia: interrompere rapidamente l'esecuzione e segnalare chiaramente il problema.

  • Controlli il numero di argomenti con $#.
  • Convalidi i tipi o i formati prima di eseguire qualsiasi operazione.
  • Stampi i messaggi diagnostici esclusivamente su stderr, mai su stdout.
  • Utilizzi codici di ritorno distinti e diversi da zero (ad esempio 1 = argomenti errati, 2 = file non trovato), così i chiamanti possono reagire diversamente alle varie cause di errore.
#!/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

Unire tutti gli elementi: un esempio reale

Ecco uno script completo e autonomo che dimostra il funzionamento congiunto di tutti i concetti di questa lezione:

  • Variabili local in ogni funzione.
  • Codici di uscita per segnalare il successo o l'errore.
  • printf per comunicare risultati stringa.
  • Errori scritti su stderr e risultati su stdout.
  • Clausole di guardia per la convalida degli argomenti.

Analizzi il flusso: parse_version estrae i dati, version_ge li confronta e main utilizza entrambe le funzioni in modo ordinato.

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

Verifica delle conoscenze: variabili locali e valori restituiti

Consideri la seguente funzione Bash. Qual è il modo corretto di acquisire il suo risultato stringa nel chiamante e quale affermazione sulla variabile tmp è vera?

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

Riepilogo: funzioni con ambito locale e codici di ritorno

In questa lezione ha imparato a scrivere funzioni Bash ordinate, componibili e sicure:

  • Utilizzi sempre local per le variabili all'interno delle funzioni, così da evitare di inquinare l'ambito del chiamante.
  • Utilizzi i codici di uscita (return 0/1/N) per segnalare il successo o l'errore: si integrano naturalmente con if, && e ||.
  • Utilizzi printf su stdout per comunicare risultati stringa; li acquisisca nel chiamante con $().
  • Scriva gli errori su stderr (>&2), così stdout rimane pulito per il flusso dei dati e il piping.
  • Utilizzi local -n (nameref) quando deve scrivere in una variabile fornita dal chiamante senza il sovraccarico di una subshell.
  • Le clausole di guardia (convalidare subito gli argomenti e restituire immediatamente un valore in caso di input non valido) rendono le funzioni robuste e autoesplicative.
  • I file di libreria devono essere caricati con source, utilizzare prefissi di namespace, non chiamare mai exit e prevenire il caricamento multiplo.

Padroneggiare questi modelli fa la differenza tra script fragili e realizzati una tantum e codebase Bash professionali e manutenibili.

Domande Frequenti

La lezione «Progettazione di funzioni con ambito locale e codici di ritorno» è gratuita?

Sì — il testo completo di «Progettazione di funzioni con ambito locale e codici di ritorno» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DevOps Bootcamp, passa a CoddyKit PRO. Il corso DevOps Bootcamp include 4 lezioni in totale.

Cosa imparerò in «Progettazione di funzioni con ambito locale e codici di ritorno»?

Scriva funzioni che usano variabili locali, stati di uscita e valori di ritorno basati su printf invece di globali fragili. Eserciti DevOps Bootcamp con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DevOps Bootcamp?

Non è richiesta alcuna esperienza precedente. DevOps Bootcamp su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.

Quanto tempo richiede la lezione «Progettazione di funzioni con ambito locale e codici di ritorno»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DevOps Bootcamp?

Sì. Ogni lezione DevOps Bootcamp include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Progettazione di funzioni con ambito locale e codici di ritorno
  2. Creazione e sourcing di librerie Bash riutilizzabili
  3. Analisi di flag e argomenti con getopts
  4. Passaggio di array e mappe associative tra funzioni
← Torna a DevOps Bootcamp