revisio_salut.sh ja existeix, però porta el llindar de disc escrit a mà al cos i la IP del servidor just a sobre. El dia que la Marta demani vigilar també el servidor de proves hauràs de duplicar el fitxer, i a partir d'aquí tindràs dos scripts que se separen a poc a poc fins que un dels dos menteix. La cura és treure els valors del cos i posar-los en variables ben pensades.

Aquesta lliçó va d'això, però sobretot va d'una cosa més profunda: en Bash tot és una cadena de text, i aquesta única frase explica el 90 % de les sorpreses que t'enduràs. A canvi, Bash et dona un arsenal de manipulació de cadenes —l'expansió de paràmetres— tan potent que substitueix basename, dirname, cut, tr i bona part de sed sense llançar ni un sol procés.

Contingut

  1. Bash no té tipus: tot és cadena
  2. Assignació: les tres regles que trenquen tothom
  3. Àmbit: script, subshell i entorn
  4. declare i els seus atributs
  5. Expansió de paràmetres a fons
  6. Aritmètica, i per què Bash no sap de decimals
  7. Matrius indexades
  8. Matrius associatives: reserves per casa
  9. Cadenes i printf
  10. Aplicació: revisio_salut.sh configurable

  1. Bash no té tipus: tot és cadena

En Python o Java declares el tipus i el llenguatge et protegeix. En Bash no hi ha tipus: tota variable guarda una cadena de bytes, i el «tipus» apareix només en fer-la servir, segons el context.

operador@srv-tramontana:~$ n=42; echo "${n} + 1 = $((n + 1))"
42 + 1 = 43
operador@srv-tramontana:~$ n=42abc; echo "$((n + 1))"
-bash: 42abc: value too great for base (error token is "42abc")

La mateixa variable és un número dins de $(( )) i text fora. Ningú no et va avisar en assignar 42abc; l'error va arribar en intentar sumar. Tres conseqüències que convé interioritzar avui:

  • Res no valida el que hi guardes. Validar és feina teva, i ho formalitzarem a 04-04 amb [[ $v =~ ^[0-9]+$ ]].
  • Comparar números i comparar cadenes són operacions diferents. "10" < "9" és cert com a text i fals com a número; d'aquí que existeixin -lt i < per separat.
  • No hi ha decimals. $(( 7842 / 25 )) dona 313, no 313.70 (apartat 6). Les úniques excepcions parcials a la regla són els atributs de declare i les matrius, que sí que són una estructura de dades de debò.

  1. Assignació: les tres regles que trenquen tothom

operador@srv-tramontana:~$ versio=3.2.1              # correcte
operador@srv-tramontana:~$ versio = 3.2.1
-bash: versio: command not found
operador@srv-tramontana:~$ missatge=còpia completa
-bash: completa: command not found
operador@srv-tramontana:~$ missatge="còpia completa"   # correcte
operador@srv-tramontana:~$ LC_ALL=C sort ~/dades/cases.txt | head -1
cal-ferrer
operador@srv-tramontana:~$ echo "LC_ALL continua valent: '${LC_ALL:-buit}'"
LC_ALL continua valent: 'buit'
  • Sense espais al voltant de l'=. versio = 3.2.1 no és una assignació: Bash ho llegeix com l'ordre versio amb els arguments = i 3.2.1. És la regla més trencada per qui ve d'altres llenguatges, i el missatge d'error no ajuda gens.
  • Cometes si hi ha espais. Sense elles l'assignació acaba al primer espai. Regla operativa: entrecometa sempre el valor, tingui espais o no.
  • En fer-la servir, $; en assignar-la, no. nom=valor per escriure, "$nom" per llegir.

Les dues últimes línies de l'exemple mostren una altra cosa: anteposar una variable a una ordre la fa existir només durant aquella execució, que és exactament el que fa la convenció LC_ALL=C del curs. Sobre els noms: lletres, dígits i _, sense començar per dígit; MAJÚSCULES per a constants i entorn, minúscules per a les de treball. I evita trepitjar PATH, HOME, IFS, PWD, RANDOM o SECONDS, que tenen amo.

  1. Àmbit: script, subshell i entorn

Reprenent 03-01, ara des de dins d'un script:

Àmbit Com es crea Qui la veu
De shell VAR=valor Només l'script actual, incloses les seves funcions
D'entorn export VAR=valor L'script i tots els processos fills que llanci
Local local var=valor (a funcions) Només la funció i les que ella cridi
operador@srv-tramontana:~$ cat /tmp/ambit.sh
CASA="mas-figueres"; export RELEASE="3.2.1"
bash -c 'echo "el fill veu CASA=[${CASA:-}] RELEASE=[${RELEASE:-}]"'
( CASA="can-ventos"; echo "el subshell canvia CASA a $CASA" )
echo "el pare continua amb CASA=$CASA"
operador@srv-tramontana:~$ bash /tmp/ambit.sh
el fill veu CASA=[] RELEASE=[3.2.1]
el subshell canvia CASA a can-ventos
el pare continua amb CASA=mas-figueres

Tres lliçons en sis línies. El fill no veu CASA perquè no està exportada, però sí RELEASE. El subshell —els parèntesis— sí que veu tot el del pare perquè n'és una còpia, però els seus canvis no tornen: el mateix mur que vas trobar amb cd, i el que farà que un while read dins d'una canonada perdi les seves variables (04-03). Quant a local, només es pot fer servir dins d'una funció i és un avançament de 04-05, però la regla és tan important que l'enunciem ja: tota variable creada dins d'una funció ha de ser local; sense això és global, i una funció pot trepitjar silenciosament el comptador d'una altra.

  1. declare i els seus atributs

declare (o el seu sinònim typeset) crea variables amb atributs, el més semblant a un tipus que té Bash.

Atribut Efecte
-r / -x Només lectura (= readonly) / exportada (= export)
-i Enter: les assignacions s'avaluen com a aritmètica
-a / -A Matriu indexada / associativa — aquesta última és obligatori declarar-la
-l / -u Converteix a minúscules / majúscules en assignar
-p No declara: imprimeix com està declarada una variable
operador@srv-tramontana:~$ declare -r RELEASE_ACTIU="3.2.1"; RELEASE_ACTIU="3.3.0"
-bash: RELEASE_ACTIU: readonly variable
operador@srv-tramontana:~$ declare -i errors=0; errors+=14; errors+=5; echo "$errors"
19
operador@srv-tramontana:~$ declare -u codi="ok"; echo "$codi"; declare -p errors
OK
declare -i errors="19"

Fixa't en errors+=14: amb l'atribut -i suma, mentre que sense ell concatenaria i deixaria 014. Aquell comportament diferent segons un atribut invisible és la raó per la qual molts prefereixen l'aritmètica explícita amb $(( )); farem servir -i només per a comptadors evidents. readonly sí que el farem servir molt: converteix un error lògic —reassignar una constant per accident— en un error immediat i sorollós, sempre millor que un de silenciós.

  1. Expansió de paràmetres a fons

Aquí Bash deixa de semblar un llenguatge de joguina. Tot el que segueix passa dins del shell, sense llançar processos. Partim d'un nom real de les nostres còpies:

operador@srv-tramontana:~$ f="/srv/tramontana/backups/enviaments/reserves-2026-08-18.csv.gz"
operador@srv-tramontana:~$ base="${f##*/}"; echo "$base"   # després de l'última /
reserves-2026-08-18.csv.gz
operador@srv-tramontana:~$ echo "${f%/*}"                 # abans de l'última /
/srv/tramontana/backups/enviaments
operador@srv-tramontana:~$ echo "${base%%.*}"             # fins al primer punt
reserves-2026-08-18
operador@srv-tramontana:~$ echo "${base#*-}"              # després del primer guionet
2026-08-18.csv.gz

Això és basename i dirname sense basename ni dirname. La regla mnemotècnica és visual: en un teclat, # és a l'esquerra del $ i treu per l'esquerra; % és a la dreta i treu per la dreta. Doblar el símbol fa el patró cobejós, com la cobdícia de 03-02. Compte: són patrons de globbing (*, ?, [...]), no expressions regulars.

Expansió Què retorna
${VAR:-valor} / ${VAR:=valor} valor si VAR és buida o no definida; la segona forma a més assigna
${VAR:?missatge} El valor, o avorta l'script amb aquest missatge si és buida
${VAR:+valor} valor només si VAR té contingut; si no, cadena buida
${#VAR} / ${VAR:despl:llarg} Longitud en caràcters / subcadena des de despl (base 0)
${VAR#patró} / ${VAR##patró} Treu el prefix més curt / més llarg que hi encaixi
${VAR%patró} / ${VAR%%patró} Treu el sufix més curt / més llarg que hi encaixi
${VAR/pat/nou} / ${VAR//pat/nou} Substitueix la primera aparició / totes
${VAR^^} / ${VAR,,} Tot a majúscules / a minúscules

Els quatre primers són la base de la configuració per defecte, i :+ és el menys conegut: afegeix una cosa només si hi ha alguna cosa a afegir.

operador@srv-tramontana:~$ data="2026-08-18"; nivell="avis"; ruta="/cases/el-molí"; verbose=""
operador@srv-tramontana:~$ echo "Any ${data:0:4}, mes ${data:5:2}, longitud ${#data}"
Any 2026, mes 08, longitud 10
operador@srv-tramontana:~$ echo "${ruta//í/i} → [${nivell^^}]"
/cases/el-moli → [AVIS]
operador@srv-tramontana:~$ echo "curl -s ${verbose:+-v} ${URL:?falta la URL a comprovar}"
-bash: URL: falta la URL a comprovar

El ${ruta//í/i} resol, sense cridar sed, l'accent que l'aplicació posa als enllaços a /cases/el-molí i que provoca els quatre 404 d'acces.log. El ${verbose:+-v} afegeix l'opció només si la variable té contingut, que és com es construeixen línies d'ordres condicionals. I ${VAR:?missatge}, ja vist a 03-01, és la manera més barata d'exigir un valor obligatori.

  1. Aritmètica, i per què Bash no sap de decimals

Bash té dues formes de fer comptes i totes dues treballen només amb enters amb signe de 64 bits.

operador@srv-tramontana:~$ echo "$(( (14 + 5 + 4) * 100 / 412 ))"     # % d'errors
5
operador@srv-tramontana:~$ i=0; (( i++ )); (( i += 10 )); echo "$i"
11

$(( )) substitueix pel resultat, així que es fa servir on vulguis el valor. (( )) avalua i no imprimeix res: serveix per modificar variables o com a condició, retornant 0 (èxit) si el resultat és diferent de zero. Dins de totes dues les variables van sense $ i disposes de + - * / % **, comparadors, && || !, ++ --, += i el ternari a ? b : c.

Dos avisos: un número amb zero a l'esquerra s'interpreta en octal: $(( 08 )) és un error de sintaxi, cosa que arruïna qualsevol compte amb mesos (08, 09); força'l amb $(( 10#08 )). I (( i++ )) amb i a 0 retorna 1, perquè el valor de l'expressió és el previ a l'increment, i amb set -e això avorta l'script; és un parany de 04-06, i de moment fes servir (( ++i )) o i=$(( i + 1 )).

Per als decimals cal sortir de Bash:

operador@srv-tramontana:~$ echo "scale=2; 7842.50 / 25" | bc
313.70
operador@srv-tramontana:~$ LC_ALL=C awk -F';' 'NR>1 { s += $6; n++ } END { printf "%.2f\n", s/n }' \
    /home/operador/dades/reserves.csv
313.70

L'import mitjà per reserva de Tramontana és de 313,70 €. A bc cal dir-li l'escala (scale=2) perquè per defecte trunca a zero decimals en dividir; bc -l carrega la biblioteca matemàtica i la puja a 20. Però fixa't en el segon exemple: si les dades ja són en un fitxer, awk fa lectura i compte alhora. La regla del curs: bc per a un compte solt, awk tan bon punt hi hagi un fitxer. I el LC_ALL=C no és decoratiu: amb locale català printf '%.2f' imprimiria 313,70 amb coma, i aquella coma trenca qualsevol comparació posterior.

  1. Matrius indexades

Una matriu és una llista ordenada de cadenes amb índex numèric des de 0. Resol «guardar diverses coses en una variable» sense recórrer a cadenes separades per espais, que es trenquen tan bon punt un element en conté un.

operador@srv-tramontana:~$ releases=(3.1.0 3.2.0 3.2.1); releases+=(3.3.0)
operador@srv-tramontana:~$ echo "${releases[0]} ... ${releases[-1]}  (total ${#releases[@]})"
3.1.0 ... 3.3.0  (total 4)
operador@srv-tramontana:~$ echo "${releases[@]:1:2}"
3.2.0 3.2.1
operador@srv-tramontana:~$ unset 'releases[0]'; echo "${!releases[@]}"
1 2 3

Es crea amb parèntesis, s'hi afegeix amb +=(...), [-1] és l'últim, ${#matriu[@]} compta elements i ${matriu[@]:1:2} és una llesca. L'última ordre revela una cosa important: unset deixa un forat, els índexs no es recol·loquen; per això existeix ${!matriu[@]}, que dona els índexs i no els valors.

${matriu[@]} enfront de ${matriu[*]}

La diferència que més errors causa, i només es manifesta dins de cometes dobles:

operador@srv-tramontana:~$ fitxers=("informe agost.pdf" "reserves.csv")
operador@srv-tramontana:~$ for f in "${fitxers[@]}"; do echo -n "[$f] "; done; echo
[informe agost.pdf] [reserves.csv]
operador@srv-tramontana:~$ for f in "${fitxers[*]}"; do echo -n "[$f] "; done; echo
[informe agost.pdf reserves.csv]
operador@srv-tramontana:~$ for f in ${fitxers[@]}; do echo -n "[$f] "; done; echo
[informe] [agost.pdf] [reserves.csv]

"${matriu[@]}" produeix un argument per element, respectant els espais: és el que vols el 99 % de les vegades. "${matriu[*]}" produeix un únic argument amb els elements units pel primer caràcter d'IFS, útil només per imprimir. I sense cometes Bash divideix per espais, i «informe agost.pdf» es converteix en dos fitxers que no existeixen. Grava-t'ho: matrius sempre amb "${matriu[@]}".

  1. Matrius associatives: reserves per casa

Una matriu associativa indexa per cadena en comptes de per número: és el que a 03-05 et donava awk, ara dins de Bash. Cal declarar-la amb declare -A abans de fer-la servir; si no, Bash la tracta com a indexada i totes les claus col·lapsen a l'índex 0.

#!/usr/bin/env bash
set -euo pipefail
CSV="/home/operador/dades/reserves.csv"
declare -A nits_casa

# tail -n +2 salta la capçalera. IFS=';' parteix cada línia pel separador
# del CSV; read -r assigna cada camp a una variable (04-03 ho detalla).
# ${var:-0} dona 0 la primera vegada que apareix una casa, amb set -u actiu.
while IFS=';' read -r _id _data casa _hoste nits _import; do
    nits_casa["$casa"]=$(( ${nits_casa["$casa"]:-0} + nits ))
done < <(tail -n +2 "$CSV")

# ${!matriu[@]} són les CLAUS; sort les deixa en ordre reproduïble.
for casa in $(printf '%s\n' "${!nits_casa[@]}" | LC_ALL=C sort); do
    printf '%-14s %3d nits\n' "$casa" "${nits_casa[$casa]}"
done
operador@srv-tramontana:~$ bash /tmp/per_casa.sh
cal-ferrer      11 nits      can-ventos      17 nits
el-moli          8 nits      la-solana       13 nits
mas-figueres    21 nits

Les 70 nits conegudes, repartides (aquí en dues columnes per estalviar espai). Dues peces noves: < <(tail -n +2 "$CSV") és una substitució de procés que evita que el while corri en un subshell i perdi les matrius en acabar —ho detalla 04-03—, i els _id, _data, _hoste són la convenció per a «llegeixo aquest camp però no el faig servir», que a més calla shellcheck. Nota honesta: aquest informe cap en una línia d'awk. Les matrius associatives brillen quan el que acumules no és text tabular —codis de sortida per servei, rutes ja processades, una memòria cau—; amb un CSV, awk continua guanyant. La regla es formalitza a 04-04.

  1. Cadenes i printf

Concatenar és escriure seguit, i += afegeix al final:

operador@srv-tramontana:~$ host="10.0.2.15"; port=8080
operador@srv-tramontana:~$ url="http://${host}:${port}"; url+="/salut"; echo "$url"
http://10.0.2.15:8080/salut

Les claus de ${host} no són opcionals: sense elles $host: funcionaria per casualitat, però $portX buscaria una variable anomenada portX. Fes servir claus sempre que la variable toqui text enganxat. Per a la sortida, printf substitueix echo en tot el que és seriós: echo varia entre shells i fa coses estranyes amb -n i les barres invertides, mentre que printf està definit amb precisió i reutilitza el format mentre quedin arguments, cosa que el converteix en un generador de taules.

Directiva Significat
%s / %d Cadena / enter decimal
%.2f Coma flotant amb dos decimals
%-20s Cadena alineada a l'esquerra en 20 columnes
%10.2f Flotant en 10 columnes, alineat a la dreta
%% / \n / \t Un % literal / salt de línia / tabulador
operador@srv-tramontana:~$ LC_ALL=C printf '%-14s %6d %10.2f\n' \
    mas-figueres 21 2450 can-ventos 17 1980 la-solana 13 1425.50
mas-figueres       21    2450.00
can-ventos         17    1980.00
la-solana          13    1425.50

Una sola crida, tres files: el format s'ha reutilitzat tres vegades. Aquella és la taula de l'informe de la Marta, i la raó per la qual echo queda ja només per a missatges d'una línia sense format.

  1. Aplicació: revisio_salut.sh configurable

El canvi de fons: cap valor no està ja clavat al cos. Tots són constants al principi, i les que té sentit ajustar des de fora es llegeixen de l'entorn amb ${VAR:-defecte}.

#!/usr/bin/env bash
#
# revisio_salut.sh - Revisió ràpida de salut de Tramontana Reserves.
#
# Autor : Operador de sistemes <operador@srv-tramontana>
# Data  : 2026-08-18
# Ús    : revisio_salut.sh
# Entorn: TRAMONTANA_HOST, TRAMONTANA_PORT, TRAMONTANA_LLINDAR_DISC,
#         TRAMONTANA_LOG_ERRORS

set -euo pipefail

# --- Configuració (entorn > valor per defecte) ----------------------------
readonly HOST="${TRAMONTANA_HOST:-10.0.2.15}"
readonly PORT="${TRAMONTANA_PORT:-8080}"
readonly LLINDAR_DISC="${TRAMONTANA_LLINDAR_DISC:-80}"
readonly LOG_ERRORS="${TRAMONTANA_LOG_ERRORS:-/var/log/tramontana/errors.log}"
readonly ESPERA_MAX=5
readonly URL_SALUT="http://${HOST}:${PORT}/salut"

# --- Cos -----------------------------------------------------------------
LC_ALL=C printf '=== Salut de Tramontana Reserves (%s) ===\n' "$(date '+%F %T')"
printf 'Servidor : %s   Objectiu: %s\n\n' "$(hostname)" "$URL_SALUT"

codi_http=$(curl -s -o /dev/null -w '%{http_code}' \
    --max-time "$ESPERA_MAX" "$URL_SALUT" || true)
us_disc=$(df --output=pcent / | tail -1 | tr -dc '0-9')
lliure_kb=$(df --output=avail / | tail -1 | tr -dc '0-9')
# Un enter de KiB no diu res a un humà: el passem a GiB amb un decimal.
lliure_gb=$(LC_ALL=C awk -v k="$lliure_kb" 'BEGIN { printf "%.1f", k/1048576 }')
avui=$(date '+%Y-%m-%d')
errors_avui=$(grep -c "^$avui" "$LOG_ERRORS" || true)

LC_ALL=C printf '%-14s %s\n' \
    "HTTP:"    "$codi_http" \
    "Disc:"    "${us_disc}% usat, ${lliure_gb} GiB lliures (llindar ${LLINDAR_DISC}%)" \
    "Errors:"  "$errors_avui avui"
exit 0
operador@srv-tramontana:~$ ~/scripts/revisio_salut.sh
=== Salut de Tramontana Reserves (2026-08-18 09:41:02) ===
Servidor : srv-tramontana   Objectiu: http://10.0.2.15:8080/salut

HTTP:          200
Disc:          30% usat, 15.4 GiB lliures (llindar 80%)
Errors:        6 avui
operador@srv-tramontana:~$ TRAMONTANA_PORT=9090 ~/scripts/revisio_salut.sh | sed -n 2p
Servidor : srv-tramontana   Objectiu: http://10.0.2.15:9090/salut

Sense tocar el fitxer, l'script apunta a un altre port. Aquest és el primer pas de la cadena de precedència arguments > entorn > fitxer de configuració > defectes que completarem a 04-03 i tancarem a 04-07. Continua sense comparar res amb LLINDAR_DISC: això arriba a 04-04.

Errors Comuns i Consells

  • Espais al voltant de l'=. VAR = valor no assigna res: és l'error número u. El segon és fer servir $VAR sense cometes: si el valor conté espais o un *, Bash el parteix o l'expandeix contra el disc. SC2086 és això, i no és un avís pedant: és una fallada esperant el seu fitxer amb espai.
  • Oblidar declare -A. Sense ell, dades[mas-figueres]=1 avalua la clau com a aritmètica, la converteix en 0 i totes les cases es trepitgen entre si, sense cap error. I no confonguis ${VAR:-x} amb ${VAR:=x}: el primer no modifica la variable, el segon sí.
  • ${matriu[*]} en un bucle. Recorre un únic element gegant; fes servir sempre "${matriu[@]}". I compte amb els zeros a l'esquerra: $(( 08 + 1 )) falla per octal, així que escriu 10#$mes.
  • Esperar decimals de $(( )). $(( 7842 / 25 )) dona 313 i es perd la resta sense avisar. Per a diners, awk o bc amb LC_ALL=C.
  • Consell: declare -p VAR et diu què és realment una variable —matriu, enter, exportada— quan alguna cosa no quadra. És l'stat de les variables.
  • Consell: prefereix l'expansió de paràmetres a basename, dirname, cut o tr per al que és simple: en un bucle de mil iteracions són mil processos que t'estalvies. I declara readonly tota constant.

Exercicis

Exercici 1. Donat f="/srv/tramontana/backups/pre-desplegament/app-3.2.1-2026-08-11.tar.gz", obtén només amb expansió de paràmetres (ni basename, ni cut, ni sed): el directori, el nom del fitxer, el nom sense extensió, la versió 3.2.1 i la data 2026-08-11. Explica cada expansió.

Exercici 2. Escriu ~/scripts/resum_reserves.sh que, llegint reserves.csv, imprimeixi una taula alineada amb casa, reserves, nits i import, més una línia de totals. Fes servir una matriu associativa i printf. Ha de coincidir amb les dades conegudes: 25 reserves, 70 nits, 7.842,50 €.

Exercici 3. En Luis proposa això per calcular el percentatge de peticions POST sobre les 412 d'acces.log. Explica els tres defectes i reescriu-lo.

posts = 114
total = 412
pct=$(($posts/$total*100))
echo "POST: $pct %"

Solucions

Solució 1.

operador@srv-tramontana:~$ f="/srv/tramontana/backups/pre-desplegament/app-3.2.1-2026-08-11.tar.gz"
operador@srv-tramontana:~$ base="${f##*/}"; nom="${base%%.tar.gz}"; sense_app="${nom#app-}"
operador@srv-tramontana:~$ printf '%s\n%s\n%s\n%s\n%s\n' "${f%/*}" "$base" "$nom" \
    "${sense_app%%-*}" "${nom##*-[0-9].[0-9].[0-9]-}"
/srv/tramontana/backups/pre-desplegament
app-3.2.1-2026-08-11.tar.gz
app-3.2.1-2026-08-11
3.2.1
2026-08-11
  • ${f%/*} treu el sufix més curt que comenci per /: és dirname. ${f##*/} treu el prefix més llarg acabat en /: és basename.
  • ${base%%.tar.gz} retalla l'extensió composta. Aquí %% i % donen el mateix perquè el patró és literal, però %%.* hauria estat perillós: tallaria també al punt de 3.2.1, deixant app-3.
  • La versió surt en dos passos: #app- treu el prefix i %%-* retalla des del primer guionet restant, el que precedeix la data. Per a la data, ##*-[0-9].[0-9].[0-9]- fa servir un patró de globbing —no una regex— que consumeix tot fins passat el número de versió.

Solució 2.

#!/usr/bin/env bash
#
# resum_reserves.sh - Taula de reserves, nits i import per casa.
# Autor : Operador de sistemes <operador@srv-tramontana>  Data: 2026-08-18

set -euo pipefail
readonly CSV="${TRAMONTANA_CSV:-/home/operador/dades/reserves.csv}"
declare -A n_reserves n_nits imports
declare -i total_reserves=0 total_nits=0
tots=""
# Els imports s'acumulen com a text separat per espais, perquè Bash no
# suma decimals; la suma la fa awk amb LC_ALL=C per al punt.
suma() { LC_ALL=C awk '{ s=0; for (i=1;i<=NF;i++) s+=$i; printf "%.2f", s }' <<<"$1"; }

while IFS=';' read -r _id _data casa _hoste nits import; do
    n_reserves["$casa"]=$(( ${n_reserves["$casa"]:-0} + 1 ))
    n_nits["$casa"]=$((    ${n_nits["$casa"]:-0} + nits ))
    imports["$casa"]="${imports["$casa"]:-}${import} "
    tots+="${import} "
    total_reserves+=1; total_nits+=nits
done < <(tail -n +2 "$CSV")

LC_ALL=C printf '%-14s %9s %8s %12s\n' CASA RESERVES NITS IMPORT
for casa in $(printf '%s\n' "${!n_reserves[@]}" | LC_ALL=C sort); do
    LC_ALL=C printf '%-14s %9d %8d %12s\n' \
        "$casa" "${n_reserves[$casa]}" "${n_nits[$casa]}" "$(suma "${imports[$casa]}")"
done
LC_ALL=C printf '%-14s %9d %8d %12s\n' TOTAL "$total_reserves" "$total_nits" "$(suma "$tots")"

exit 0
operador@srv-tramontana:~$ ~/scripts/resum_reserves.sh
CASA            RESERVES     NITS       IMPORT
cal-ferrer             4       11      1120.00
can-ventos             6       17      1980.00
el-moli                3        8       867.00
la-solana              5       13      1425.50
mas-figueres           7       21      2450.00
TOTAL                 25       70      7842.50

Coincideix amb les dades conegudes. La funció suma és un avançament de 04-05, però il·lustra per què les funcions són inevitables: la mateixa línia d'awk es fa servir en dos llocs.

Solució 3. Els tres defectes:

  1. Espais al voltant de l'=. posts = 114 intenta executar l'ordre posts i l'script mor a la primera línia.
  2. L'ordre de les operacions trunca el resultat. $posts/$total*100 calcula primer 114/412, que en aritmètica entera és 0, i després 0*100 = 0. Sempre multiplica abans de dividir.
  3. Els $ sobren dins de $(( )) —soroll, no error— i sobretot un percentatge demana un decimal que Bash no pot donar.
posts=114
total=412
# Multiplicar abans de dividir dona l'enter correcte: 27
pct_enter=$(( posts * 100 / total ))
# I amb un decimal, sortint a awk amb locale fix:
pct=$(LC_ALL=C awk -v p="$posts" -v t="$total" 'BEGIN { printf "%.1f", p*100/t }')
LC_ALL=C printf 'POST: %s de %s (%s %% enter, %s %% exacte)\n' \
    "$posts" "$total" "$pct_enter" "$pct"
# -> POST: 114 de 412 (27 % enter, 27.7 % exacte)

Fixa't en el %% del format: dins de printf, un % literal s'escriu doblat.

Conclusió

Ja no depens de valors clavats al codi, i de passada t'has endut la part de Bash que més productivitat dona per línia escrita.

  • Saps que en Bash tot és una cadena i que el «tipus» el posa el context: res no es valida, comparar números no és comparar text, i no hi ha decimals. Assignes sense espais al voltant de l'=, entrecometes sempre el valor, distingeixes els àmbits de shell, entorn i subshell sabent que els canvis del fill no tornen mai al pare, i fas servir declare amb els seus atributs -r, -i, -a, -A, -x i -p.
  • Domines l'expansió de paràmetres completa: :-, :=, :? i :+, longituds, subcadenes, retallades amb #, ##, %, %%, substitucions amb / i //, i canvi de cas. Ja no necessites basename ni cut per al que és simple.
  • Fas aritmètica entera amb $(( )) i (( )), coneixes el parany de l'octal i el de (( i++ )), i surts a bc o awk amb LC_ALL=C tan bon punt apareix un decimal: l'import mitjà de Tramontana és 313,70 €.
  • Manegues matrius indexades —sempre "${matriu[@]}", mai ${matriu[*]}—, comptes per clau amb matrius associatives després de l'imprescindible declare -A, formates amb printf (que ja produeix la taula de l'informe de la Marta), i revisio_salut.sh llegeix la seva configuració de l'entorn amb valors per defecte.

Però l'script continua sent mut: no saps demanar-li ajuda, no li pots dir «fes servir llindar 60» sense exportar una variable, i no hi ha manera que llegeixi el mes d'un informe. A la lliçó següent, Entrada, Sortida i Arguments d'un Script, li donem una interfície de debò: paràmetres posicionals i la diferència crítica entre $@ i $*, una funció d'ajuda, getopts per a opcions curtes, les convencions -h, -v, --dry-run i -- que fan que un script sembli professional, read en totes les seves variants i el patró canònic per llegir un fitxer línia a línia, la regla d'or de stdout per a resultats i stderr per a humans, i els codis de sortida amb significat. En acabar-la, revisio_salut.sh acceptarà opcions i naixerà informe_reserves.sh.

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