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
- Bash no té tipus: tot és cadena
- Assignació: les tres regles que trenquen tothom
- Àmbit: script, subshell i entorn
declarei els seus atributs- Expansió de paràmetres a fons
- Aritmètica, i per què Bash no sap de decimals
- Matrius indexades
- Matrius associatives: reserves per casa
- Cadenes i
printf - Aplicació:
revisio_salut.shconfigurable
- 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-lti<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 dedeclarei les matrius, que sí que són una estructura de dades de debò.
- 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.1no és una assignació: Bash ho llegeix com l'ordreversioamb els arguments=i3.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=valorper 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.
- À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-figueresTres 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.
declare i els seus atributs
declare i els seus atributsdeclare (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.
- 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.gzAixò é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 comprovarEl ${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.
- 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.70L'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.
- 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 3Es 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[@]}".
- 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]}"
doneoperador@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 nitsLes 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.
- Cadenes i
printf
printfConcatenar é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/salutLes 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.50Una 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.
- Aplicació:
revisio_salut.sh configurable
revisio_salut.sh configurableEl 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 0operador@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/salutSense 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 = valorno assigna res: és l'error número u. El segon és fer servir$VARsense 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]=1avalua 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 escriu10#$mes.- Esperar decimals de
$(( )).$(( 7842 / 25 ))dona 313 i es perd la resta sense avisar. Per a diners,awkobcambLC_ALL=C. - Consell:
declare -p VARet diu què és realment una variable —matriu, enter, exportada— quan alguna cosa no quadra. És l'statde les variables. - Consell: prefereix l'expansió de paràmetres a
basename,dirname,cutotrper al que és simple: en un bucle de mil iteracions són mil processos que t'estalvies. I declarareadonlytota 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.
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/: ésdirname.${f##*/}treu el prefix més llarg acabat en/: ésbasename.${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 de3.2.1, deixantapp-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 0operador@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.50Coincideix 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:
- Espais al voltant de l'
=.posts = 114intenta executar l'ordrepostsi l'script mor a la primera línia. - L'ordre de les operacions trunca el resultat.
$posts/$total*100calcula primer114/412, que en aritmètica entera és 0, i després0*100= 0. Sempre multiplica abans de dividir. - 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 servirdeclareamb els seus atributs-r,-i,-a,-A,-xi-p. - Domines l'expansió de paràmetres completa:
:-,:=,:?i:+, longituds, subcadenes, retallades amb#,##,%,%%, substitucions amb/i//, i canvi de cas. Ja no necessitesbasenamenicutper al que és simple. - Fas aritmètica entera amb
$(( ))i(( )), coneixes el parany de l'octal i el de(( i++ )), i surts abcoawkambLC_ALL=Ctan 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'imprescindibledeclare -A, formates ambprintf(que ja produeix la taula de l'informe de la Marta), irevisio_salut.shllegeix 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
- 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
