revisio_salut.sh ja llegeix la seva configuració de l'entorn, però continua sent un botó d'una sola posició: no sap explicar-se, no admet «aquesta vegada fes servir llindar 60» sense exportar una variable, i no hi ha manera de demanar-li que calli. Un script sense interfície és un script que només pot fer servir qui el va escriure, i aquesta lliçó li dona aquesta interfície. Aprendràs per on entra la informació en un script i per on ha de sortir, i les dues preguntes tenen respostes molt concretes: arguments per al que canvia a cada execució, stdin per a les dades, stdout per al resultat i stderr per a l'humà. Aquesta disciplina és el que fa que un programa sigui componible, que encaixi en una canonada com hi encaixen grep o sort. Al final tindràs revisio_salut.sh amb opcions de debò i naixerà informe_reserves.sh.

Contingut

  1. Els quatre canals d'entrada
  2. Paràmetres posicionals
  3. $@ enfront de $*: la diferència que causa més errors
  4. Validar arguments i la funció d'ajuda
  5. Opcions: anàlisi manual i getopts
  6. Convencions de línia d'ordres
  7. read: llegir de l'usuari i d'un fitxer
  8. Sortida: stdout per al resultat, stderr per a l'humà
  9. Codis de sortida amb significat
  10. Fitxers de configuració i el mode --dry-run
  11. Aplicació: opcions a revisio_salut.sh i neix informe_reserves.sh

  1. Els quatre canals d'entrada

Un script pot rebre informació per quatre vies, i triar malament és la primera decisió de disseny que es fa malbé.

Canal Com arriba Fes-lo servir per a No el facis servir per a
Arguments / stdin script.sh --llindar 60 agost / cat dades | script.sh El que canvia a cada execució / fluxos de dades i canonades Dades grans o secrets (visibles a ps) / configuració
Entorn LLINDAR=60 script.sh Context heretat, secrets, ajustos de desplegament El que es teclegia diàriament
Fitxer de config. /etc/tramontana/app.conf Ajustos estables i compartits Valors que canvien per execució

La regla de precedència del curs, de més forta a més feble: arguments > entorn > fitxer de configuració > valors per defecte. És la que espera qualsevol administrador: permet fixar el que és estable en un fitxer i saltar-se'l puntualment des de la línia d'ordres.

  1. Paràmetres posicionals

En invocar script.sh alfa beta, Bash omple un conjunt de variables especials:

Variable Contingut
$0 / $1 … $9 El nom amb què es va invocar l'script / els arguments, en ordre
${10} i següents A partir de 10 calen les claus: $10 es llegeix com $1 seguit de 0
$# / $@ / $* Nombre d'arguments / tots ells (vegeu l'apartat següent)
operador@srv-tramontana:~$ cat /tmp/args.sh
printf 'invocat com a : %s (curt: %s)\n' "$0" "${0##*/}"
printf 'arguments     : %d -> [%s]\n' "$#" "$*"
shift 2
printf 'un cop shift 2: %d -> [%s]\n' "$#" "$*"
operador@srv-tramontana:~$ bash /tmp/args.sh --verbose 08 informe.txt
invocat com a : /tmp/args.sh (curt: args.sh)
arguments     : 3 -> [--verbose 08 informe.txt]
un cop shift 2: 1 -> [informe.txt]

shift descarta el primer argument i desplaça la resta: $2 passa a ser $1 i $# decreix; shift N en descarta N de cop. És el mecanisme bàsic de qualsevol bucle d'anàlisi. ${0##*/} —expansió de paràmetres de 04-02— dona el nom sense ruta, que és el que vols a l'ajuda. I pots reassignar els posicionals amb set -- "${@:-08}" per aplicar un valor per defecte; el -- és imprescindible, perquè sense ell set prendria per opció seva qualsevol argument que comenci per -.

  1. $@ enfront de $*: la diferència que causa més errors

Igual que amb les matrius a 04-02, la diferència només apareix dins de cometes dobles, i allà es trenquen els scripts tan bon punt algú passa un nom amb espais.

operador@srv-tramontana:~$ cat /tmp/cometes.sh          # tres bucles idèntics
for a in "$@"; do echo -n "[$a] "; done; echo '  <- "$@"'
for a in "$*"; do echo -n "[$a] "; done; echo '  <- "$*"'
for a in $@;   do echo -n "[$a] "; done; echo '  <- $@ sense cometes'
operador@srv-tramontana:~$ bash /tmp/cometes.sh "informe agost.txt" reserves.csv
[informe agost.txt] [reserves.csv]   <- "$@"
[informe agost.txt reserves.csv]   <- "$*"
[informe] [agost.txt] [reserves.csv]   <- $@ sense cometes

"$@" és l'única forma correcta de reenviar els arguments a una altra ordre: produeix exactament els mateixos que vas rebre, amb els seus espais intactes. "$*" els uneix en una cadena separada pel primer caràcter d'IFS i només serveix per imprimir. Sense cometes, tots dos es parteixen i «informe agost.txt» es converteix en dos fitxers inexistents. La regla, sense excepcions: "$@" per passar, "$*" per mostrar.

  1. Validar arguments i la funció d'ajuda

Un script seriós comprova el que rep abans de fer res, i sap explicar-se:

us() {
    cat <<EOF
Ús: ${0##*/} [opcions] <mes>
Genera l'informe de reserves del mes indicat (07, 08 o 09).

Opcions:
  -o FITXER    Escriu l'informe a FITXER en comptes de a la ruta estàndard
  -v           Mode detallat: explica cada pas per stderr
  -h           Mostra aquesta ajuda i acaba
EOF
}

[[ $# -ge 1 ]] || { us >&2; exit 2; }    # error: l'ajuda va a stderr

Fixa't en un detall que gairebé ningú no fa bé i que separa un script educat d'un de maleducat: si l'usuari demana ajuda amb -h, l'ajuda és el resultat i va a stdout amb codi 0; si es mostra perquè s'ha equivocat, és un missatge d'error i va a stderr amb codi diferent de 0. La diferència importa: script.sh -h | less funciona en el primer cas, i script.sh 2>/dev/null no silencia el resultat legítim en el segon. El cat <<EOF és el here-document de 03-04: sense cometes a EOF, ${0##*/} s'expandeix.

  1. Opcions: anàlisi manual i getopts

Anàlisi manual amb while i case

Única forma d'admetre opcions llargues (--llindar) en Bash pur, i el patró és sempre el mateix:

DRY_RUN=0; LLINDAR=80       # valors per defecte, més febles que tot
while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)     us; exit 0 ;;
        -n|--dry-run)  DRY_RUN=1; shift ;;
        -u|--llindar)  LLINDAR="$2"; shift 2 ;;
        --llindar=*)   LLINDAR="${1#*=}"; shift ;;
        --)            shift; break ;;
        -*)            printf 'Opció desconeguda: %s\n' "$1" >&2; exit 2 ;;
        *)             break ;;
    esac
done

Cada branca consumeix el que li pertoca amb shift (un si és un interruptor, dos si porta valor) i el bucle torna a començar. --llindar=60 es resol amb ${1#*=}, que retalla fins al primer =; -- marca el final d'opcions; el que comenci per - i no reconeguis és un error d'ús, i qualsevol altra cosa és el primer posicional i acaba l'anàlisi. (case es formalitza a 04-04.)

getopts per a opcions curtes

Quan et basten les curtes, getopts és un builtin que fa tota la feina, inclòs agrupar -vn com a -v -n:

VERBOSE=0; LLINDAR=80
while getopts ":hvu:" opcio; do
    case "$opcio" in
        h)  us; exit 0 ;;
        v)  VERBOSE=1 ;;
        u)  LLINDAR="$OPTARG" ;;
        \?) printf 'Opció desconeguda: -%s\n' "$OPTARG" >&2; us >&2; exit 2 ;;
        :)  printf "L'opció -%s necessita un valor\n" "$OPTARG" >&2; exit 2 ;;
    esac
done
shift $(( OPTIND - 1 ))

Quatre peces cal entendre:

  • La cadena ":hvu:". Cada lletra és una opció; els dos punts al darrere d'una lletra (u:) signifiquen «aquesta porta valor».
  • Els dos punts inicials. Activen el mode silenciós: getopts deixa d'imprimir els seus missatges en anglès i t'avisa amb \? (opció desconeguda) i : (falta el valor), posant la lletra culpable a OPTARG. Posa'ls sempre, o el teu script barrejarà missatges seus amb missatges de Bash.
  • OPTARG conté el valor de l'opció actual, i OPTIND l'índex del següent argument per processar. El shift $(( OPTIND - 1 )) final descarta les opcions consumides i deixa a $1 el primer posicional real. Si oblides aquella línia, $1 continuarà sent -v.

Existeix a més getopt (sense s, el programa extern de GNU), que sí que admet opcions llargues reordenant els arguments, però la seva sintaxi amb eval set -- "$OPCIONS" és fràgil i la versió de macOS no és compatible. Resumint: getopts si et basten les curtes, anàlisi manual si necessites les llargues, i getopt només si saps exactament què fas.

  1. Convencions de línia d'ordres

Respectar-les és el que fa que una altra persona pugui fer servir el teu script sense llegir-lo.

Opció Significat esperat
-h, --help / --version Ajuda o versió a stdout i exit 0
-v, --verbose / -q, --quiet Més detall per stderr / només errors
-n, --dry-run / -f, --force Ensenya què faria / salta les confirmacions
-- Final d'opcions: el que segueix són dades encara que comenci per -. Resol un problema real: rm -- "-informe.txt" és l'única forma d'esborrar aquell fitxer

  1. read: llegir de l'usuari i d'un fitxer

read pren una línia de stdin i la reparteix en variables. Les seves opcions útils:

Opció Efecte
-r No interpreta \ com a escapament. Posa-la sempre; sense ella una ruta de Windows es destrossa
-p TEXT / -s Mostra un avís abans de llegir / no fa eco, per a contrasenyes
-t N / -n N / -a A Espera N segons / llegeix N caràcters sense Intro / reparteix a la matriu A
operador@srv-tramontana:~$ read -r -s -p "Contrasenya de la BD: " clau; echo
Contrasenya de la BD:
operador@srv-tramontana:~$ echo "${#clau} caràcters llegits, no mostrats"
14 caràcters llegits, no mostrats

Aquell -s és la forma correcta de demanar un secret de manera interactiva: no queda ni a la pantalla ni a l'historial. A 04-07 veurem l'alternativa per a tasques desateses.

El patró canònic per llegir un fitxer

while IFS= read -r linia; do ... done < fitxer és de les línies que convé memoritzar tal qual, perquè cada peça hi és per un motiu concret:

  • IFS= (buit, i només per a aquest read) impedeix que Bash retalli els espais del principi i del final; sense ell, la indentació d'un fitxer de configuració desapareix. -r impedeix que les barres invertides s'interpretin com a escapaments, i < fitxer al final del done —no una canonada— és el que importa, com veiem ara.

El bucle que perd les variables

operador@srv-tramontana:~$ comptador=0; grep -c '' /var/log/tramontana/errors.log
87
operador@srv-tramontana:~$ cat /var/log/tramontana/errors.log |
>     while read -r l; do comptador=$(( comptador + 1 )); done
operador@srv-tramontana:~$ echo "$comptador"
0

Ha recorregut 87 línies i el comptador val 0. La raó la coneixes des de 04-02: cada tram d'una canonada s'executa en un subshell, així que el while va incrementar la seva còpia i aquella còpia va morir en tancar-se la canonada. El mateix mur del cd, disfressat. Tres solucions, en ordre de preferència:

# 1. Redirecció directa: el while corre al shell principal
while read -r l; do (( ++comptador )); done < /var/log/tramontana/errors.log
# 2. Substitució de procés: quan l'entrada ve d'una ordre
while read -r l; do (( ++comptador )); done < <(grep 500 /var/log/tramontana/errors.log)
# 3. lastpipe: fa que l'ÚLTIM tram corri al shell principal
shopt -s lastpipe
grep 500 /var/log/tramontana/errors.log | while read -r l; do (( ++comptador )); done

La 2 és la que més faràs servir. La 3 té lletra petita: lastpipe només funciona amb el control de treballs desactivat, cosa que passa en un script no interactiu però no al teu terminal, així que provar-ho a mà et pot enganyar.

  1. Sortida: stdout per al resultat, stderr per a l'humà

La regla que converteix un script en una peça d'Unix: stdout és el resultat —el que un altre programa podria voler processar, dades i només dades— i stderr és tota la resta: avisos, progrés, errors, missatges de -v. Si els barreges, el_meu_script.sh | awk '{...}' s'ennuega amb el teu «Processant...» i el_meu_script.sh > informe.txt deixa els errors dins de l'informe. Les dues funcions que garanteixen la disciplina:

# Registre informatiu i error, tots dos amb marca de temps ISO i a stderr.
log()   { printf '[%s] %s\n'        "$(date '+%F %T')" "$*" >&2; }
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }

operador@srv-tramontana:~$ bash /tmp/demo_log.sh > /tmp/sortida.txt
[2026-08-18 10:02:11] Comprovant el release actiu
operador@srv-tramontana:~$ cat /tmp/sortida.txt
3.2.1

Els missatges han aparegut al terminal i el fitxer només conté la dada: això és un script componible. Fem servir printf en lloc d'echo per la raó de 04-02: comportament definit i format controlat.

  1. Codis de sortida amb significat

Amb la interfície muntada, el codi de sortida deixa de ser «0 o 1» i passa a ser documentació executable. La convenció de Tramontana pren prestats els valors de sysexits.h:

Codi Nom Quan
0 / 1 OK / error genèric Tot correcte / fallada no classificada
2 Ús incorrecte Falten arguments, opció desconeguda
64 / 65 EX_USAGE / EX_DATAERR Ús incorrecte, en scripts sysexits / dades mal formades
66 / 69 EX_NOINPUT / EX_UNAVAILABLE No es pot llegir l'entrada / un servei no respon
73 / 78 EX_CANTCREAT / EX_CONFIG No s'ha pogut crear la sortida / configuració incorrecta

L'essencial no és adoptar exactament aquests números, sinó triar-ne uns, documentar-los a la capçalera i ser coherent: un script el 69 del qual significa sempre «l'aplicació no respon» permet que qui el crida actuï sense llegir-ne la sortida.

  1. Fitxers de configuració i el mode --dry-run

Hi ha dues maneres de llegir un fitxer de configuració, i una és perillosa. source /etc/tramontana/app.conf és còmoda, però el fitxer és codi que s'executa amb els teus permisos: si algú amb escriptura sobre ell hi afegeix rm -rf /, el teu script ho executa. És el mateix risc que va deixar una credencial exposada en una còpia amb permisos 644. Amb source només s'hi pot viure si el fitxer és teu, amb permisos estrictes i verificats. L'alternativa segura és analitzar-lo:

llegir_config() {
    local fitxer="$1" clau valor
    [[ -r $fitxer ]] || return 0           # sense fitxer, valors per defecte
    while IFS='=' read -r clau valor; do
        clau="${clau// /}"                            # treu espais de la clau
        [[ -z $clau || $clau == \#* ]] && continue    # salta blancs i comentaris
        valor="${valor#"${valor%%[![:space:]]*}"}"    # i els de l'inici del valor
        case "$clau" in
            max_connexions)   MAX_CONNEXIONS="$valor" ;;
            timeout_consulta) TIMEOUT="$valor" ;;
            *) : ;;                                   # desconeguda: s'ignora
        esac
    done < "$fitxer"
}

Només s'accepten les claus que l'script coneix. És més codi, però és la diferència entre llegir dades i executar codi aliè.

El mode --dry-run és l'altre patró imprescindible, i s'implementa amb un embolcall:

DRY_RUN=0
executar() { if (( DRY_RUN )); then printf '[simulat] %s\n' "$*" >&2; else "$@"; fi; }
executar rm -f /srv/tramontana/backups/temporals/parcial.tar

"$@" dins d'executar reconstrueix l'ordre amb els seus arguments exactes —d'aquí que l'apartat 3 importés tant— i amb DRY_RUN=1 només la imprimeix. La regla de 03-03 es torna així automàtica: tot script que esborri, mogui o sobreescrigui alguna cosa té --dry-run.

  1. Aplicació: opcions a revisio_salut.sh i neix informe_reserves.sh

La capçalera de revisio_salut.sh passa a documentar la sintaxi i els codis (# Ús: revisio_salut.sh [-h] [-v] [-u LLINDAR], # Sortida: 0 correcte | 2 ús incorrecte), i entre la configuració de 04-02 i el cos s'intercala això:

VERBOSE=0
log()   { (( VERBOSE )) && printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; return 0; }
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }
us() {
    cat <<EOF
Ús: ${0##*/} [-h] [-v] [-u LLINDAR]
Comprova l'aplicació (port ${PORT}), el disc i els errors d'avui.
  -u LLINDAR  Percentatge de disc a partir del qual avisar (defecte ${LLINDAR_DISC})
  -v          Mode detallat per stderr
  -h          Aquesta ajuda
EOF
}

while getopts ":hvu:" opcio; do
    case "$opcio" in
        h)  us; exit 0 ;;   v) VERBOSE=1 ;;   u) LLINDAR_DISC="$OPTARG" ;;
        \?) error "opció desconeguda: -$OPTARG"; us >&2; exit 2 ;;
        :)  error "l'opció -$OPTARG necessita un valor"; exit 2 ;;
    esac
done
shift $(( OPTIND - 1 ))

log "iniciant la revisió (llindar de disc: ${LLINDAR_DISC}%)"
# ... el cos de 04-02, sense canvis ...  ->  log "revisió acabada"

LLINDAR_DISC ja no pot ser readonly: l'opció -u ha de sobreescriure el valor de l'entorn, que al seu torn sobreescriu el defecte. És la precedència de l'apartat 1, implementada.

operador@srv-tramontana:~$ ~/scripts/revisio_salut.sh -v -u 60 > /tmp/salut.txt
[2026-08-18 10:14:03] iniciant la revisió (llindar de disc: 60%)
[2026-08-18 10:14:04] revisió acabada
operador@srv-tramontana:~$ ~/scripts/revisio_salut.sh -x; echo "codi: $?"
[2026-08-18 10:14:20] ERROR: opció desconeguda: -x
codi: 2

El resultat va anar al fitxer i la traça al terminal, tal com promet l'apartat 8. I fixa't en el return 0 de log(): sense ell, quan VERBOSE val 0 la funció retorna 1 i amb set -e l'script moriria a la primera crida; és un dels paranys que 04-06 esmicola. Anem amb l'script nou que la Marta va demanar per al seu informe mensual:

#!/usr/bin/env bash
#
# informe_reserves.sh - Informe de reserves d'un mes de 2026.
# Autor : Operador de sistemes <operador@srv-tramontana>  Data: 2026-08-18
# Ús    : informe_reserves.sh [-o FITXER] [-v] <mes: 07|08|09>
# Sortida: 0 correcte | 2 ús incorrecte | 66 no es troba el CSV

set -euo pipefail
readonly CSV="${TRAMONTANA_CSV:-/home/operador/dades/reserves.csv}"
readonly BASE_TREBALL="/home/operador/treball/2026"
SORTIDA=""; VERBOSE=0
log()   { (( VERBOSE )) && printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; return 0; }
error() { printf '[%s] ERROR: %s\n' "$(date '+%F %T')" "$*" >&2; }
us()    { printf 'Ús: %s [-o FITXER] [-v] <mes: 07|08|09>\n' "${0##*/}"; }

# Mateix bloc getopts de l'script anterior, amb ":hvo:" i o) SORTIDA="$OPTARG".
shift $(( OPTIND - 1 ))     # després d'això, $1 és el primer posicional real
[[ $# -eq 1 ]] || { error "cal un argument: el mes"; us >&2; exit 2; }
mes="$1"
[[ -r $CSV ]] || { error "no puc llegir $CSV"; exit 66; }

# Si no s'ha indicat -o, la ruta estàndard del mes a l'arbre de treball.
: "${SORTIDA:=${BASE_TREBALL}/${mes}/informes/reserves-${mes}.txt}"
mkdir -p "${SORTIDA%/*}" && log "escrivint l'informe a $SORTIDA"

{
    printf 'INFORME DE RESERVES - MES %s\n\n' "$mes"
    LC_ALL=C awk -F';' -v mes="2026-$mes" '
      NR>1 && $2 ~ "^" mes { r[$3]++; n[$3]+=$5; i[$3]+=$6; tr++; tn+=$5; ti+=$6 }
      END { printf "%-14s %9s %8s %12s\n", "CASA", "RESERVES", "NITS", "IMPORT"
            for (c in r) printf "%-14s %9d %8d %12.2f\n", c, r[c], n[c], i[c]
            printf "%-14s %9d %8d %12.2f\n", "TOTAL", tr, tn, ti }' "$CSV"
} > "$SORTIDA"
log "informe acabat"
printf '%s\n' "$SORTIDA"     # stdout: la ruta, per encadenar
exit 0
operador@srv-tramontana:~$ ruta=$(~/scripts/informe_reserves.sh -v 08)
[2026-08-18 10:21:40] escrivint l'informe a /home/operador/treball/2026/08/informes/reserves-08.txt
[2026-08-18 10:21:40] informe acabat
operador@srv-tramontana:~$ head -3 "$ruta"; tail -1 "$ruta"
INFORME DE RESERVES - MES 08

CASA            RESERVES     NITS       IMPORT
TOTAL                 25       70      7842.50

Dos detalls de disseny: : "${SORTIDA:=...}" fa servir el builtin : (que no fa res) només per forçar l'assignació per defecte ${VAR:=valor} de 04-02, i l'script imprimeix la ruta per stdout, de manera que ruta=$(...) la captura i l'informe queda a punt per encadenar amb mail o scp.

Errors Comuns i Consells

  • Oblidar shift $(( OPTIND - 1 )). La fallada número u amb getopts: les opcions continuen ocupant $1 i l'script creu que li falten arguments. La segona és fer servir $@ o $* sense cometes, amb la qual cosa qualsevol argument amb espais es parteix.
  • read sense -r. Una ruta amb \ es corromp silenciosament; no hi ha cap cas en què vulguis read sense -r. I evita els bucles while read al final d'una canonada: les variables moren amb el subshell, així que fes servir < fitxer o < <(ordre).
  • Enviar els missatges de progrés a stdout. Contamines el resultat i trenques qualsevol canonada: tot el que no sigui la dada va a stderr. I getopts sense els dos punts inicials fa que el teu script imprimeixi missatges en anglès que no controles.
  • Posar un secret a la línia d'ordres. Qualsevol amb accés a ps el veu, i queda a ~/.bash_history. Fitxer amb permisos 600 o variable d'entorn; ho tanquem a 04-07 i a 06-05.
  • Consell: escriu la funció us() abans que el cos; si et costa descriure la interfície, és que encara no l'has decidida. I fes que -h funcioni sempre, fins i tot sense arguments i sense configuració vàlida: és el primer que prova qui troba el teu script.
  • Consell: valida els arguments tan bon punt els tinguis i surt immediatament si alguna cosa no quadra. Com abans falli, menys haurà destrossat.

Exercicis

Exercici 1. Escriu ~/scripts/comptar_errors.sh, que llegeixi per stdin un registre amb el format d'acces.log i accepti -c CODI (per defecte 500), -v i -h. Ha d'imprimir per stdout només el nombre de línies amb aquell codi i, amb -v, quantes línies va llegir en total per stderr. acces.log té 412 línies i 14 respostes 500.

Exercici 2. Amplia informe_reserves.sh amb -n/--dry-run, que mostri quin fitxer s'escriuria sense crear-lo, fent servir l'embolcall executar(). Explica per què l'anàlisi ha de passar de getopts a manual.

Exercici 3. En Luis ha escrit això per comptar quantes IP diferents hi ha a acces.log i diu que «retorna 0 sempre, deu ser cosa del registre»: n=0; cut -d' ' -f6 acces.log | sort -u | while read ip; do n=$((n+1)); done; echo "$n". Diagnostica-ho i dona dues solucions.

Solucions

Solució 1. Capçalera i getopts com als apartats 4 i 5, amb ":hvc:" i CODI="$OPTARG". L'interessant és el cos:

# El bucle llegeix de stdin, heretat de qui invoca l'script; en no haver-hi
# cap canonada a dins, les variables sobreviuen. read reparteix la línia en
# camps per IFS: el codi és el cinquè del format d'acces.log.
total=0; coincidencies=0
while read -r _f _h _m _r codi _resta; do
    (( ++total ))
    [[ $codi == "$CODI" ]] && (( ++coincidencies ))
done
(( VERBOSE )) && printf 'Llegides %d línies buscant el codi %s\n' "$total" "$CODI" >&2
printf '%d\n' "$coincidencies"      # stdout: només la dada
exit 0
operador@srv-tramontana:~$ ~/scripts/comptar_errors.sh -v < /var/log/tramontana/acces.log
Llegides 412 línies buscant el codi 500
14

read amb diversos noms trosseja la línia per IFS sense cridar cut ni awk: 412 processos estalviats. Aquí sí que ometem IFS=, perquè volem precisament la divisió en camps.

Solució 2. Cal passar a anàlisi manual perquè getopts no admet opcions llargues: --dry-run arribaria sencera i getopts la llegiria com a -, -d, -r… Se substitueix el while getopts pel bucle while [[ $# -gt 0 ]] de l'apartat 5, afegint-hi la branca -n|--dry-run) DRY_RUN=1; shift ;;, i l'embolcall fa la resta:

executar() { if (( DRY_RUN )); then printf '[simulat] %s\n' "$*" >&2; else "$@"; fi; }

executar mkdir -p "${SORTIDA%/*}"
if (( DRY_RUN )); then log "s'escriuria l'informe a $SORTIDA"
else genera_informe > "$SORTIDA"    # el bloc { ... } extret a una funció
fi
printf '%s\n' "$SORTIDA"
operador@srv-tramontana:~$ ~/scripts/informe_reserves.sh --dry-run -v 09
[2026-08-18 10:33:02] s'escriuria l'informe a /home/operador/treball/2026/09/informes/reserves-09.txt
/home/operador/treball/2026/09/informes/reserves-09.txt
operador@srv-tramontana:~$ ls /home/operador/treball/2026/09/informes/

El directori continua buit: no s'ha creat res. I nota que la redirecció > "$SORTIDA" no pot passar per executar, perquè el shell la resol abans d'invocar la funció; d'aquí que aquell cas porti el seu propi if. És la limitació clàssica del patró.

Solució 3. El diagnòstic: while read és l'últim tram d'una canonada, així que corre en un subshell; n s'incrementa a la còpia i l'echo del shell principal veu el 0 original. No té res a veure amb el registre.

# A: substitució de procés, el while corre al shell principal
n=0; while IFS= read -r ip; do (( ++n )); done < <(cut -d' ' -f6 acces.log | sort -u)
# B: no fer servir cap bucle, que és el correcte aquí
n=$(cut -d' ' -f6 acces.log | sort -u | wc -l)
printf 'IP diferents: %d\n' "$n"           # -> IP diferents: 5

Les cinc IP conegudes (10.0.2.77, .31, .44, .52 i .18). Prefereix la B: si el bucle només compta, no necessites el bucle. Un tercer camí seria shopt -s lastpipe, però al teu terminal interactiu no funciona i podries creure que el teu arranjament no serveix.

Conclusió

Els teus scripts ja tenen interfície, i amb ella deixen de ser teus per ser utilitzables.

  • Distingeixes els quatre canals d'entrada i apliques la precedència arguments > entorn > configuració > defectes.
  • Maneges els paràmetres posicionals amb $#, shift, ${10} i set --, i saps que "$@" és l'única forma correcta de reenviar arguments mentre que "$*" només serveix per mostrar.
  • Escrius una funció us() que va a stdout si la demanen i a stderr si és un error, i analitzes opcions amb getopts —cadena d'opcions, dos punts inicials, OPTARG i l'imprescindible shift $(( OPTIND - 1 ))— o a mà amb while i case per a les llargues, respectant les convencions -h, -v, -q, -n i --.
  • Fas servir read amb -r sempre, i coneixes el patró canònic while IFS= read -r linia; do ... done < fitxer, amb les tres solucions al bucle que perd les variables pel subshell.
  • Separes stdout (el resultat) de stderr (l'humà) amb log() i error(), retornes codis de sortida documentats, llegeixes configuració analitzant-la en comptes d'amb source, i portes --dry-run incorporat amb l'embolcall executar().

Però mira el codi que has escrit avui: està ple d'if, de case i de [[ ]] fets servir per intuïció, sense haver-los estudiat. És el moment d'arreglar-ho. A Estructures de Control veuràs la idea que sosté tot Bash —que es ramifica segons el codi de sortida d'una ordre, no segons un booleà—, els tres tipus de test i quan fer servir cadascun, la taula completa d'operadors de fitxer, cadena i número, =~ amb BASH_REMATCH per validar dates, case amb els seus patrons i els seus tres terminadors, tots els bucles amb l'advertiment de per què mai for f in $(ls), i un mesurament real sobre acces.log que et dirà quan un bucle de Bash és mil vegades més lent que un awk. En acabar-la, revisio_salut.sh sabrà dir OK, AVÍS o CRÍTIC, i purgar_releases.sh recorrerà /opt/tramontana/releases/ sense tocar la versió activa.

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