Tens quatre scripts a ~/scripts i log() està escrita quatre vegades. error() també. executar() va per la seva tercera còpia. El dia que decideixis que el registre ha de portar el nivell a més de la data hauràs d'editar quatre fitxers i recordar-te'n dels quatre; i el dia que te n'oblidis un, tindràs un script que registra diferent sense que ningú se n'assabenti fins que faci falta. Les funcions resolen això, però no només això: una funció ben anomenada converteix deu línies críptiques en una línia que es llegeix com una frase. Aquesta lliçó va de quan extreure-les, de les dues trampes que Bash reserva a qui ve d'altres llenguatges —l'àmbit dinàmic i el return que només retorna números— i de com muntar una biblioteca de debò, amb càrrega robusta, guardes i tests. En acabar existirà lib/comuns.sh i els scripts anteriors la faran servir.

Contingut

  1. Quan extreure una funció
  2. Sintaxi, i per què no fem servir function
  3. Els arguments d'una funció són seus
  4. Àmbit: local i l'àmbit dinàmic de Bash
  5. Retornar valors: return només retorna un número
  6. Funcions predicat
  7. Recursió, i sobreescriure ordres
  8. Biblioteques: lib/comuns.sh
  9. Què ha de fer i què no ha de fer una biblioteca
  10. Tests mínims sense frameworks
  11. Aplicació: refactoritzar els scripts

  1. Quan extreure una funció

Tres criteris, i n'hi ha prou que se'n compleixi un:

  • La regla de tres. Has escrit el mateix per tercera vegada: la primera pot ser casualitat, la segona impaciència; la tercera és una funció.
  • El bloc necessita un comentari per entendre's. Si anaves a escriure # comprova si el release existeix i no és buit, aquell comentari és en realitat el nom: existeix_release(). I si hi ha més de dos nivells d'imbricació, com avisàvem a 04-04, un tercer nivell gairebé sempre és una funció que falta.

El nom importa tant com el codi: un bon nom és un verb o un predicat, descriu què fa i no com, i fa que la crida es llegeixi sense comentaris. Compara if [[ -d /opt/tramontana/releases/$v && -f /opt/tramontana/releases/$v/app.jar ]] amb if existeix_release "$v". I un avís d'equilibri: una funció d'una línia feta servir una sola vegada sol ser soroll. La mesura no és «com més, millor», sinó «que cada nom estalviï una explicació».

  1. Sintaxi, i per què no fem servir function

Bash admet nom() { cos; } (POSIX), function nom { cos; } (extensió de Bash/ksh) i la barreja function nom() { ... }. Fem servir sempre la primera: és l'única que funciona en qualsevol shell POSIX, és la que espera shellcheck per defecte i estalviar-se els parèntesis no aporta res. Detalls de sintaxi que donen errors misteriosos:

  • El cos entre { } necessita espais després de { i abans de }, perquè { és una paraula reservada, no un símbol, i l'última instrucció abans de } necessita ; o un salt de línia.
  • Les funcions s'han de definir abans de fer-les servir: Bash llegeix el fitxer de dalt a baix. D'aquí que a 04-07 posem main "$@" com a última línia de l'script.
  • Una funció i un àlies amb el mateix nom xoquen, i guanya l'àlies: una altra raó per no abusar-ne.

declare -f nom mostra el codi d'una funció ja definida i declare -F llista els noms: són l'equivalent de declare -p per a variables, molt útils quan dubtes de quina versió s'ha carregat.

  1. Els arguments d'una funció són seus

Dins d'una funció, $1, $@ i $# no són els de l'script: són els de la crida. $0, en canvi, continua sent el nom de l'script.

operador@srv-tramontana:~$ cat /tmp/args_fn.sh
mostrar() { printf 'fn: $#=%d $1=%s $0=%s\n' "$#" "${1:-buit}" "${0##*/}"; }
printf 'script: $#=%d $1=%s\n' "$#" "${1:-buit}"
mostrar alfa beta
mostrar                      # sense arguments
operador@srv-tramontana:~$ bash /tmp/args_fn.sh 3.2.1
script: $#=1 $1=3.2.1
fn: $#=2 $1=alfa $0=args_fn.sh
fn: $#=0 $1=buit $0=args_fn.sh

És la confusió número u de qui comença: cridar la funció esperant que «vegi» els arguments de l'script. Si els hi vols passar, fes-ho explícitament amb mostrar "$@", que ja saps de 04-03 que és l'única forma que respecta els espais. I fixa't en ${1:-buit}: amb set -u actiu, llegir $1 en una funció sense arguments avorta l'script, així que un valor per defecte és obligatori si l'argument pot faltar.

  1. Àmbit: local i l'àmbit dinàmic de Bash

Tota variable creada dins d'una funció s'ha de declarar local. Sense local, la variable és global i sobreviu a la funció, amb conseqüències divertides:

operador@srv-tramontana:~$ comptar() { for i in 1 2 3; do :; done; }
operador@srv-tramontana:~$ for i in a b c; do comptar; printf '%s ' "$i"; done; echo
3 3 3
operador@srv-tramontana:~$ comptar() { local i; for i in 1 2 3; do :; done; }
operador@srv-tramontana:~$ for i in a b c; do comptar; printf '%s ' "$i"; done; echo
a b c

En el primer cas comptar va trepitjar la i del cridant i el bucle exterior va imprimir tres vegades el 3 en comptes d'a b c. Un local i ho arregla. Amb noms genèrics com i, n, tmp o linia això passa constantment.

Ara la part que sorprèn qui ve d'altres llenguatges. Bash té àmbit dinàmic, no lèxic: una variable local és visible també dins de les funcions que aquella funció cridi, encara que estiguin definides en un altre fitxer.

operador@srv-tramontana:~$ interior() { echo "interior veu: ${secret:-res}"; }
operador@srv-tramontana:~$ exterior() { local secret="visible"; interior; }
operador@srv-tramontana:~$ exterior; interior
interior veu: visible
interior veu: res

En un llenguatge d'àmbit lèxic —Python, Java, C— interior no veuria mai aquella variable, perquè només veu el que hi ha al text que l'envolta; en Bash veu el que hi hagi a la pila de crides. Té un ús legítim —passar context sense arguments— però és sobretot acoblament invisible: no t'hi recolzis, declara local a tot arreu i passa les dades per arguments.

  1. Retornar valors: return només retorna un número

operador@srv-tramontana:~$ sumar() { return $(( $1 + $2 )); }
operador@srv-tramontana:~$ sumar 200 100; echo "$?"
44

300 es va convertir en 44, perquè return accepta només un enter de 0 a 255 i el valor es trunca mòdul 256. return no és «retornar un resultat»: és «retornar un codi de sortida», exactament el mateix concepte de 04-01 i 04-04. Per retornar dades hi ha tres camins:

Forma Com Avantatge Inconvenient
stdout + $( ) printf a dins, x=$(fn) a fora Componible, sense efectes secundaris Crea un subshell: lent en bucles grans
Global convinguda La funció assigna RESULTAT=... Ràpida, sense subshell Contamina l'espai de noms
Nameref (local -n) El cridant passa el nom de la seva variable Explícita i sense subshell Bash ≥ 4.3; col·lisiona si els noms coincideixen
# stdout: la forma preferent, i l'única realment componible
ruta_release() { printf '%s\n' "/opt/tramontana/releases/$1"; }
r=$(ruta_release 3.2.1)
# nameref: quan retornes diversos valors o ets en un bucle atapeït
partir_versio() {
    local -n _major="$2" _menor="$3"   # _major passa a SER la variable del cridant
    local resta="${1#*.}"
    _major="${1%%.*}"; _menor="${resta%%.*}"
}
partir_versio "3.10.2" maj men
printf 'major=%s menor=%s\n' "$maj" "$men"     # -> major=3 menor=10

El guió baix de _major no és cap caprici: si el cridant tingués una variable anomenada exactament igual que el nameref, Bash donaria circular name reference. Prefixar els namerefs amb _ redueix aquella col·lisió a la impossibilitat pràctica.

Regla del curs: stdout per defecte, i nameref només quan retornis diversos valors o el subshell sigui un problema mesurat, no imaginat.

  1. Funcions predicat

Una funció que només respon sí o no ha de retornar 0 per a «sí» i fer-se servir directament en un if, sense == true ni variables intermèdies.

# es_numero CADENA -> 0 si és un enter no negatiu, 1 si no.
es_numero() { [[ ${1:-} =~ ^[0-9]+$ ]]; }
# existeix_release VERSIO -> 0 si el release existeix i no és buit.
existeix_release() {
    local dir="/opt/tramontana/releases/${1:-}"
    [[ -d $dir ]] && [[ -n "$(ls -A "$dir" 2>/dev/null)" ]]
}

operador@srv-tramontana:~$ es_numero 80 && echo sí; es_numero "8O" || echo no
sí
no
operador@srv-tramontana:~$ if existeix_release 3.2.1; then echo "a punt"; fi
a punt

Cap de les dues no porta return: una funció retorna el codi de la seva última instrucció, i l'última instrucció és justament el test. Escriure if ...; then return 0; else return 1; fi és quatre vegades més llarg i fa el mateix.

Un avís que enllaça amb 04-06: si l'última instrucció pot fallar legítimament —un (( comptador )) que valgui 0, un grep sense coincidències— la funció retornarà 1 i, amb set -e, tombarà l'script. Per això log() acaba amb return 0 explícit.

  1. Recursió, i sobreescriure ordres

Bash admet recursió, però poques vegades és la resposta: no hi ha optimització de crides de cua, FUNCNEST limita la profunditat, i gairebé tot el que és recursiu que fa un administrador —recórrer un arbre de directoris— ho resol find millor i més ràpid. El que sí que és útil és sobreescriure una ordre amb una funció, típicament per afegir seguretat o registre; la clau és poder cridar l'original:

rm() {
    error "fes servir 'executar rm' en comptes de rm directe, per respectar --dry-run"
    command rm "$@"      # command salta les funcions i crida el programa real
}

command ignora funcions i àlies i executa el binari o builtin; builtin força el builtin del shell (útil si embolcalles cd o echo). Sense un dels dos, la funció es cridaria a si mateixa infinitament. I una advertència: no sobreescriguis ordres en una biblioteca compartida; que rm faci coses estranyes a deu metres d'on està escrit és una manera excel·lent d'arruïnar-li la tarda a algú.

  1. Biblioteques: lib/comuns.sh

Creem ~/scripts/lib/comuns.sh amb les funcions que es repeteixen:

#!/usr/bin/env bash
#
# comuns.sh - Funcions compartides pels scripts de Tramontana.
# Autor : Operador de sistemes <operador@srv-tramontana>  Data: 2026-08-18
# NO s'executa: es carrega amb 'source'. Sense codi de nivell superior més
# enllà de definir funcions i de la guarda d'inclusió.

# Guarda d'inclusió múltiple: si dos scripts carreguen la biblioteca, 'return 0'
# avorta aquesta lectura sense avortar l'script que va fer el source.
[[ -n ${TRAMONTANA_COMUNS_CARREGADA:-} ]] && return 0
readonly TRAMONTANA_COMUNS_CARREGADA=1
TRAMONTANA_VERBOSE="${TRAMONTANA_VERBOSE:-0}"
# log MISSATGE... -> stderr amb marca de temps, només si TRAMONTANA_VERBOSE=1.
#   Retorna: sempre 0 (per no trencar 'set -e' quan VERBOSE=0).
log() {
    (( TRAMONTANA_VERBOSE )) && printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2
    return 0
}
# error MISSATGE... -> missatge d'error a stderr. Retorna 0.
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }
# morir CODI MISSATGE... -> registra l'error i acaba l'SCRIPT amb CODI.
morir() { local codi="$1"; shift; error "$@"; exit "$codi"; }

# requereix_comanda NOM...
#   Retorna: 0 si totes estan disponibles; 1 si en falta alguna (ho diu).
requereix_comanda() {
    local cmd falten=0
    for cmd in "$@"; do
        command -v "$cmd" >/dev/null 2>&1 || { error "falta l'ordre: $cmd"; falten=1; }
    done
    return "$falten"
}
# confirmar PREGUNTA
#   Retorna: 0 si respon s/si. Sense terminal (cron), 1 sense preguntar.
confirmar() {
    local resposta
    [[ -t 0 ]] || { error "sense terminal interactiu: no confirmo"; return 1; }
    read -r -p "${1:-Vols continuar?} [s/N] " resposta
    [[ ${resposta,,} == s || ${resposta,,} == si ]]
}
# formatar_bytes N -> imprimeix N bytes a la unitat llegible més propera.
formatar_bytes() {
    LC_ALL=C awk -v b="${1:-0}" 'BEGIN {
        split("B KiB MiB GiB TiB", u, " "); i = 1
        while (b >= 1024 && i < 5) { b /= 1024; i++ }
        printf (i == 1 ? "%d %s\n" : "%.1f %s\n"), b, u[i]
    }'
}

Carregar-la de manera robusta

source lib/comuns.sh només funciona si el directori de treball és ~/scripts, i cron executa des de $HOME. La solució canònica:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"

Esmicolada de dins cap enfora:

  • ${BASH_SOURCE[0]} és la ruta del fitxer que s'està llegint. Es fa servir en comptes de $0 perquè si el fitxer es carrega amb source, $0 seria el de l'script pare, no el seu. dirname n'extreu el directori, que pot ser relatiu.
  • cd ... && pwd converteix aquella ruta relativa en absoluta i resol els .., tot dins de $( ), és a dir en un subshell: el teu directori de treball real no canvia. El && garanteix que si el cd falla no s'executa el pwd (SC2164 de 04-01).
  • El comentari # shellcheck source=... li diu a ShellCheck on és el fitxer perquè el pugui analitzar; sense ell avisa amb SC1091.

  1. Què ha de fer i què no ha de fer una biblioteca

Ha de No ha de
Definir funcions i, com a molt, constants Executar feina en carregar-se
Portar guarda d'inclusió múltiple Cridar exit al nivell superior
Documentar cada funció: paràmetres, sortida, retorn Escriure a stdout tret que sigui el seu resultat
Fer servir local a totes les seves variables Redefinir ordres del sistema
Prefixar el que no siguin funcions públiques Dependre del directori de treball

La prohibició d'exit mereix un matís: una biblioteca no l'ha de cridar al seu nivell superior —mataria el shell de qui la carregui amb source, inclòs el teu terminal—, però sí que ho pot fer dins d'una funció la finalitat declarada de la qual sigui acabar, com morir(). La diferència és en qui decideix: l'autor de la biblioteca no, el que la crida sí.

Sobre els prefixos: en un projecte propi, log i error són còmodes i acceptables; en una biblioteca pensada perquè altres la carreguin, prefixa-ho tot (tram_log, tram_error), perquè el dia que algú carregui la teva biblioteca i una altra que també defineix log guanyarà l'última llegida, en silenci. Nosaltres ja prefixem les variables (TRAMONTANA_*), que és on el xoc seria més difícil de diagnosticar.

  1. Tests mínims sense frameworks

Una biblioteca sense proves és una biblioteca que trencaràs sense adonar-te'n. No cal cap framework: n'hi ha prou d'executar cada funció amb entrades conegudes i comptar les fallades.

#!/usr/bin/env bash
#
# test_comuns.sh - Proves de lib/comuns.sh.
# Ús: test_comuns.sh   -> 0 si tot passa, 1 si hi ha alguna fallada.
# Sense -e a propòsit: volem executar TOTS els casos, no parar al primer.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"

fallades=0
comprovar() {   # comprovar DESCRIPCIO ESPERAT OBTINGUT
    if [[ $2 == "$3" ]]; then printf '  ok      %s\n' "$1"
    else printf '  FALLADA %s: esperava [%s], he obtingut [%s]\n' "$1" "$2" "$3" >&2
         (( ++fallades ))
    fi
}
comprovar "bytes 0"       "0 B"      "$(formatar_bytes 0)"
comprovar "bytes 2048"    "2.0 KiB"  "$(formatar_bytes 2048)"
comprovar "bytes release" "97.0 MiB" "$(formatar_bytes 101711872)"
requereix_comanda bash awk 2>/dev/null
comprovar "requereix_comanda existent" "0" "$?"
requereix_comanda ordre_inexistent_xyz 2>/dev/null
comprovar "requereix_comanda absent"   "1" "$?"
TRAMONTANA_VERBOSE=0; log "no s'ha de veure" 2>/dev/null
comprovar "log callat retorna 0" "0" "$?"

printf '\n%d fallada/es\n' "$fallades"
(( fallades == 0 ))

operador@srv-tramontana:~$ ~/scripts/test_comuns.sh; echo "codi: $?"
  ok      bytes 0
  ok      bytes 2048
  ok      bytes release
  ok      requereix_comanda existent
  ok      requereix_comanda absent
  ok      log callat retorna 0

0 fallada/es
codi: 0

Trenta línies i ja tens una xarxa de seguretat. El cas log callat retorna 0 no és decoratiu: comprova el return 0 de l'apartat 6, el que evita que set -e mati l'script quan el mode detallat està desactivat. Els tests valen sobretot per a això: fixar les decisions subtils abans que algú les «simplifiqui».

  1. Aplicació: refactoritzar els scripts

Amb la biblioteca a punt, la capçalera de revisio_salut.sh i d'informe_reserves.sh queda idèntica i les funcions duplicades desapareixen:

set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"
requereix_comanda curl df awk || morir 69 "falten dependències"

while getopts ":hvu:" opcio; do
    case "$opcio" in
        h)  us; exit 0 ;;
        v)  TRAMONTANA_VERBOSE=1 ;;      # ara la controla la biblioteca
        u)  es_numero "$OPTARG" || morir 2 "el llindar ha de ser un número: $OPTARG"
            LLINDAR_DISC="$OPTARG" ;;
        \?) morir 2 "opció desconeguda: -$OPTARG" ;;
        :)  morir 2 "l'opció -$OPTARG necessita un valor" ;;
    esac
done
shift $(( OPTIND - 1 ))

operador@srv-tramontana:~$ ~/scripts/revisio_salut.sh -u vuitanta; echo "codi: $?"
[2026-08-18 12:03:44] ERROR: el llindar ha de ser un número: vuitanta
codi: 2

Tres guanys concrets. morir 2 "..." substitueix error ...; exit 2 en cinc llocs, així que cap camí d'error no es pot oblidar de l'exit. La validació del llindar apareix per primera vegada, i apareix perquè es_numero ja existia i costava una línia fer-la servir. I TRAMONTANA_VERBOSE és una sola variable en un sol fitxer, en comptes de quatre VERBOSE que es podien desincronitzar. Afegeix-hi també purgar_releases.sh: el seu executar() es muda a la biblioteca tal qual, i la seva comprovació del release actiu es converteix en existeix_release "$actiu" || morir 66 "el release actiu no existeix".

Errors Comuns i Consells

  • Oblidar local. La variable es torna global i trepitja la del cridant; amb noms com i, tmp o linia és qüestió de temps.
  • local x=$(ordre) i creure que has comprovat l'error. El codi de sortida que es veu és el de local, no el de l'ordre: sempre 0. Declara primer (local x) i assigna després (x=$(ordre)). És SC2155, i ho veurem a fons a 04-06.
  • Esperar que return retorni una dada. Només retorna 0-255, i 300 es converteix en 44: les dades van per stdout. I compte amb fer servir els arguments de l'script dins d'una funció: a dins, $1 és el de la funció, així que passa-los amb "$@". Defineix la funció abans de fer-la servir, perquè Bash llegeix de dalt a baix i la crida fallaria amb command not found.
  • Posar codi executable al nivell superior d'una biblioteca. Carregar-la tindrà efectes que ningú no ha demanat, i un exit allà tancarà el terminal de qui la carregui a mà. I no la carreguis amb ruta relativa: funciona al teu terminal i falla a cron, que arrenca des d'un altre directori; fes servir SCRIPT_DIR.
  • Consell: documenta cada funció amb tres línies de contracte —què rep, què imprimeix, què retorna— just a sobre: és el que permet fer-la servir sense llegir-ne el cos. I executa test_comuns.sh abans de cada commit de la biblioteca: deu segons que eviten trencar quatre scripts alhora.
  • Consell: si una funció necessita més de cinc arguments o més de trenta línies, probablement són dues funcions.

Exercicis

Exercici 1. Explica què imprimeix aquest fragment i per què, i després arregla'l perquè imprimeixi el que el seu autor esperava.

processar() {
    total=0
    for n in "$@"; do total=$(( total + n )); done
    return "$total"
}
total=1000
processar 7 6 5 4 3
echo "reserves: $total"

Exercici 2. Afegeix a lib/comuns.sh una funció resum_casa CSV CASA que imprimeixi casa;reserves;nits;import per a una casa de reserves.csv, amb codi 0 si existeix i 1 si no. Afegeix dos casos a test_comuns.sh: mas-figueres (ha de donar mas-figueres;7;21;2450.00) i una casa inexistent.

Exercici 3. En Luis ha escrit aquesta biblioteca. Troba-hi els cinc problemes i reescriu-la.

#!/bin/sh
echo "carregant biblioteca..."
LOGFILE=/tmp/tramontana.log
function log { echo "$1" >> $LOGFILE; }
function comprovar_disc {
  us=`df / | tail -1 | awk '{print $5}' | tr -d %`
  if [ $us -gt 80 ]; then log "disc ple"; exit 1; fi
}

Solucions

Solució 1. Imprimeix reserves: 25, i és una casualitat afortunada que amaga dos errors.

El que passa: total dins de processar no és local, així que la funció trepitja la variable global del cridant; la suma 7+6+5+4+3 dona 25 i aquella és la que s'imprimeix, no el 1000 assignat abans. El return "$total" és a més inútil aquí i perillós en general: si les reserves sumessin 300, return retornaria 44 (300 mòdul 256) i qui el fes servir obtindria una dada falsa sense cap avís.

# Retorna la suma per stdout, el canal correcte per a una dada. 'local'
# impedeix trepitjar variables del cridant (també la 'n' del bucle, que és
# l'oblit més freqüent).
processar() {
    local suma=0 n
    for n in "$@"; do suma=$(( suma + n )); done
    printf '%d\n' "$suma"
}
total=1000
reserves=$(processar 7 6 5 4 3)
printf 'previ: %d  reserves: %d\n' "$total" "$reserves"
# -> previ: 1000  reserves: 25

Ara total conserva el seu valor i el resultat arriba pel seu canal.

Solució 2.

# resum_casa FITXER_CSV CASA -> "casa;reserves;nits;import".
#   Retorna: 0 si la casa apareix al fitxer; 1 si no o si no es pot llegir.
resum_casa() {
    local csv="${1:-}" casa="${2:-}" sortida
    [[ -r $csv ]] || { error "no puc llegir $csv"; return 1; }
    sortida=$(LC_ALL=C awk -F';' -v c="$casa" '
        NR > 1 && $3 == c { r++; n += $5; i += $6 }
        END { if (r) printf "%s;%d;%d;%.2f\n", c, r, n, i }' "$csv")
    [[ -n $sortida ]] || return 1
    printf '%s\n' "$sortida"
}

# Casos afegits a test_comuns.sh
CSV="/home/operador/dades/reserves.csv"
comprovar "resum mas-figueres" "mas-figueres;7;21;2450.00" \
    "$(resum_casa "$CSV" mas-figueres)"
resum_casa "$CSV" casa-inexistent >/dev/null 2>&1
comprovar "resum casa inexistent retorna 1" "1" "$?"
operador@srv-tramontana:~$ ~/scripts/test_comuns.sh | tail -2
  ok      resum mas-figueres
  ok      resum casa inexistent retorna 1

El disseny respecta les regles de l'apartat 9: la dada surt per stdout, l'error per stderr, el codi de retorn distingeix els casos, i la funció no crida exit ni assumeix directori de treball. Que awk no imprimeixi res sense coincidències és el que permet distingir «no existeix» d'«existeix amb zeros».

Solució 3. Els cinc problemes:

  1. #!/bin/sh amb function. Contradictori: dash no coneix function. Una biblioteca que es carrega amb source porta shebang per convenció i per als editors, però no ha de ser executable.
  2. echo "carregant biblioteca..." al nivell superior. Executa feina en carregar-se i escriu a stdout, contaminant la sortida de qualsevol script que la faci servir. Una biblioteca carrega en silenci.
  3. exit 1 dins de la biblioteca. Mata l'script del cridant sense donar-li opció, i si algú la carrega al seu terminal, l'hi tanca. La funció ha d'informar i retornar un codi.
  4. Sense local, sense guarda d'inclusió i sense documentació. us és global i trepitjarà qualsevol variable homònima, carregar el fitxer dues vegades ho redefineix tot, i cap funció no diu què retorna.
  5. ` `, $us sense cometes i LOGFILE fix a /tmp. Un fitxer de registre predictible a /tmp és un risc de seguretat clàssic (04-06), i [ $us -gt 80 ] falla si us queda buida.
#!/usr/bin/env bash
# comuns_disc.sh - Utilitats de disc. Es carrega amb 'source'.
[[ -n ${TRAMONTANA_DISC_CARREGADA:-} ]] && return 0
readonly TRAMONTANA_DISC_CARREGADA=1
# us_disc [PUNT_MUNTATGE] -> imprimeix el percentatge d'ús (només el número).
#   Retorna: 1 si no s'ha pogut mesurar.
us_disc() {
    local punt="${1:-/}" pct
    pct=$(df --output=pcent "$punt" 2>/dev/null | tail -1 | tr -dc '0-9') || return 1
    [[ -n $pct ]] || return 1
    printf '%s\n' "$pct"
}
# disc_per_sobre LLINDAR [PUNT_MUNTATGE]
#   Retorna: 0 si l'ús supera LLINDAR, 1 si no, 2 si no s'ha pogut mesurar.
disc_per_sobre() {
    local llindar="${1:?falta el llindar}" punt="${2:-/}" pct
    pct=$(us_disc "$punt") || { error "no puc mesurar $punt"; return 2; }
    (( pct > llindar ))
}

Ara qui la fa servir decideix: disc_per_sobre 80 || exit 0, o disc_per_sobre 80 && purgar_releases.sh. La biblioteca informa, l'script actua.

Conclusió

Els teus scripts han deixat de repetir-se, i de passada has entès les dues raresses de Bash que més confonen qui ve d'altres llenguatges.

  • Extreus funcions per la regla de tres, perquè el bloc demanava un comentari o perquè hi havia un tercer nivell d'imbricació, i les anomenes perquè la crida es llegeixi com una frase. Fas servir la sintaxi nom() { ... }, defineixes abans de fer servir, i saps que dins d'una funció $1 i $@ són els de la funció, no els de l'script.
  • Declares local a totes les variables de funció, i entens que Bash té àmbit dinàmic: el que declaris és visible a les funcions que cridis, amb l'acoblament invisible que això implica.
  • Saps que return només retorna un codi de 0 a 255 i coneixes les tres formes reals de retornar dades, amb stdout per defecte i local -n prefixat amb _ quan cal.
  • Escrius funcions predicat sense return explícit, aprofitant que la funció retorna el codi de la seva última instrucció, i afegeixes return 0 quan aquella última instrucció podria fallar legítimament. Coneixes command i builtin per cridar l'original quan sobreescrius un nom, i la raó per no fer-ho en una biblioteca compartida.
  • Tens lib/comuns.sh amb log(), error(), morir(), requereix_comanda(), confirmar() i formatar_bytes(), carregada amb SCRIPT_DIR, protegida amb guarda d'inclusió, documentada funció a funció i provada per test_comuns.sh.

I un exercici d'honestedat. revisio_salut.sh porta set -euo pipefail des de la primera lliçó i mai no has vist què fa exactament. log() acaba amb un return 0 que ha aparegut dues vegades «perquè si no, l'script mor», sense explicació real. informe_reserves.sh crea el directori de l'informe i, si alguna cosa falla a mitges, el deixa a mig escriure. I copia_tramontana.sh, promès des del Mòdul 2, continua sense existir. La lliçó següent, Depuració i Gestió d'Errors, tanca tot això: bash -x amb un PS4 llegible, els codis de shellcheck que més importen, el mode estricte esmicolat —inclosos els cinc llocs on set -e no funciona—, trap amb EXIT, ERR i INT perquè cap temporal ni cap bloqueig sobrevisqui a una interrupció, la idempotència com a objectiu de disseny, i per fi l'script de còpia complet i blindat.

Curs de Linux: De Principiant a Administrador de Sistemes

Mòdul 1: Introducció a Linux

Mòdul 2: Comandes Bàsiques de Linux

Mòdul 3: Habilitats Avançades en la Línia de Comandes

Mòdul 4: Scripting en Shell

Mòdul 5: Administració del Sistema

Mòdul 6: Xarxes i Seguretat

Mòdul 7: Temes Avançats

Mòdul 8: Projectes Pràctics

© Copyright 2026. Tots els drets reservats