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
- Els quatre canals d'entrada
- Paràmetres posicionals
$@enfront de$*: la diferència que causa més errors- Validar arguments i la funció d'ajuda
- Opcions: anàlisi manual i
getopts - Convencions de línia d'ordres
read: llegir de l'usuari i d'un fitxer- Sortida: stdout per al resultat, stderr per a l'humà
- Codis de sortida amb significat
- Fitxers de configuració i el mode
--dry-run - Aplicació: opcions a
revisio_salut.shi neixinforme_reserves.sh
- 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.
- 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 -.
$@ enfront de $*: la diferència que causa més errors
$@ enfront de $*: la diferència que causa més errorsIgual 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.
- 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 stderrFixa'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.
- Opcions: anàlisi manual i
getopts
getoptsAnà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
doneCada 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:
getoptsdeixa d'imprimir els seus missatges en anglès i t'avisa amb\?(opció desconeguda) i:(falta el valor), posant la lletra culpable aOPTARG. Posa'ls sempre, o el teu script barrejarà missatges seus amb missatges de Bash. OPTARGconté el valor de l'opció actual, iOPTINDl'índex del següent argument per processar. Elshift $(( OPTIND - 1 ))final descarta les opcions consumides i deixa a$1el primer posicional real. Si oblides aquella línia,$1continuarà 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.
- 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 |
read: llegir de l'usuari i d'un fitxer
read: llegir de l'usuari i d'un fitxerread 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 mostratsAquell -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 aquestread) impedeix que Bash retalli els espais del principi i del final; sense ell, la indentació d'un fitxer de configuració desapareix.-rimpedeix que les barres invertides s'interpretin com a escapaments, i< fitxeral final deldone—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"
0Ha 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 )); doneLa 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.
- 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.1Els 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.
- 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.
- Fitxers de configuració i el mode
--dry-run
--dry-runHi 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.
- Aplicació: opcions a
revisio_salut.sh i neix informe_reserves.sh
revisio_salut.sh i neix informe_reserves.shLa 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: 2El 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 0operador@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.50Dos 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 ambgetopts: les opcions continuen ocupant$1i 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. readsense-r. Una ruta amb\es corromp silenciosament; no hi ha cap cas en què vulguisreadsense-r. I evita els bucleswhile readal final d'una canonada: les variables moren amb el subshell, així que fes servir< fitxero< <(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
getoptssense 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
psel 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-hfuncioni 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 0operador@srv-tramontana:~$ ~/scripts/comptar_errors.sh -v < /var/log/tramontana/acces.log
Llegides 412 línies buscant el codi 500
14read 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: 5Les 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}iset --, 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 ambgetopts—cadena d'opcions, dos punts inicials,OPTARGi l'imprescindibleshift $(( OPTIND - 1 ))— o a mà ambwhileicaseper a les llargues, respectant les convencions-h,-v,-q,-ni--. - Fas servir
readamb-rsempre, i coneixes el patró canònicwhile 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()ierror(), retornes codis de sortida documentats, llegeixes configuració analitzant-la en comptes d'ambsource, i portes--dry-runincorporat amb l'embolcallexecutar().
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
- Què és Linux?
- Història de Linux
- Distribucions de Linux
- Instal·lant Linux
- Primer Contacte amb el Sistema
- Estructura del Sistema de Fitxers de Linux
Mòdul 2: Comandes Bàsiques de Linux
- Introducció a la Línia de Comandes
- Obtenir Ajuda i Documentació del Sistema
- Navegant pel Sistema de Fitxers
- Operacions amb Fitxers i Directoris
- Visualització i Edició de Fitxers
- Enllaços Durs i Simbòlics
- Permisos i Propietat dels Fitxers
Mòdul 3: Habilitats Avançades en la Línia de Comandes
- L'Entorn del Shell: Variables, Àlies i Historial
- Ús de Comodins i Expressions Regulars
- Cerca de Fitxers i Contingut: find, locate i grep
- Canonades i Redirecció
- Processament de Text: cut, sort, uniq, sed i awk
- Gestió de Processos
- Programació de Tasques amb Cron
- Comandes de Xarxa
Mòdul 4: Scripting en Shell
- Introducció al Scripting en Shell
- Variables i Tipus de Dades
- Entrada, Sortida i Arguments d'un Script
- Estructures de Control
- Funcions i Biblioteques
- Depuració i Gestió d'Errors
- Scripts de Producció: Bones Pràctiques
Mòdul 5: Administració del Sistema
- Gestió d'Usuaris i Grups
- sudo i Permisos Especials
- Gestió de Paquets
- Gestió de Discs
- systemd i la Gestió de Serveis
- Registres del Sistema: journald i syslog
- Monitoratge del Sistema i Optimització del Rendiment
- Còpies de Seguretat i Restauració
Mòdul 6: Xarxes i Seguretat
- Configuració de Xarxes
- SSH i Accés Remot
- Tallafocs i Seguretat Perimetral
- Sistemes de Detecció d'Intrusions
- Gestió de Secrets i Certificats TLS
- Assegurant Sistemes Linux
Mòdul 7: Temes Avançats
- El Procés d'Arrencada i la Recuperació del Sistema
- Diagnòstic Avançat: strace, perf i eBPF
- Optimització del Nucli de Linux
- Virtualització amb Linux
- Contenidors de Linux i Docker
- Automatització amb Ansible
- Alta Disponibilitat i Balanceig de Càrrega
