informe-diari.sh ja funciona, però és un script rígid: /var/log/veloz/app.log i /srv/veloz/dades/enviaments.csv apareixen escrits literalment enmig del cos, i el llindar d'errors que vols vigilar no és enlloc. El dia que operacions mogui el CSV a un altre directori hauràs de buscar i reemplaçar per tot el fitxer, i cada aparició que se t'escapi serà una fallada silenciosa. Aquesta lliçó resol aquest problema i obre la porta a tota la resta: les variables són el mecanisme amb què un script deixa de ser una llista d'ordres i comença a manipular informació.
Contingut
- Assignar variables: la regla d'or de l'
=sense espais - Noms vàlids i convencions de majúscules
- Fer servir variables:
$vardavant de${var} - Bash no té tipus: tot és una cadena
- Substitució d'ordres amb
$(...) - Constants amb
readonlyideclare -r declarei les seves opcions útils- Àmbit: variables de shell davant de variables d'entorn
- Variables especials del shell
unseti la refactorització d'informe-diari.sh
- Assignar variables: la regla d'or de l'
= sense espais
= sense espaisUna variable és un nom associat a un valor. Es crea assignant-li alguna cosa, i a Bash la sintaxi és implacable: ciutat=Valencia, repartidor=alopez, llindar=50.
No hi pot haver espais al voltant de l'=. Aquesta és la primera regla que tothom trenca, i val la pena que n'entenguis el perquè en lloc de memoritzar-la. Bash separa la línia en paraules pels espais: si escrius ciutat = Valencia, hi veu tres paraules i dedueix que vols executar l'ordre ciutat amb els arguments = i Valencia, d'aquí el missatge bash: ciutat: no s'ha trobat l'ordre. La variant ciutat =Valencia produeix el mateix error, mentre que ciutat= Valencia intenta executar Valencia amb la variable ciutat buida. Tres formes d'equivocar-se, tres símptomes diferents, una sola regla: enganxat a l'=, sense excepcions.
Si el valor conté espais, cal posar-lo entre cometes, perquè si no la segona paraula s'interpretaria com una ordre:
Les cometes dobles són l'opció per defecte i les estudiaràs a fons a 03-06; de moment queda't amb això: posar cometes no fa mal mai.
- Noms vàlids i convencions de majúscules
Un nom de variable pot contenir lletres, dígits i guions baixos, i no pot començar per un dígit. No admet guions, punts ni espais: la-meva-variable no és vàlid (Bash ho llegeix com una resta), però la_meva_variable sí.
A més de les regles del llenguatge, hi ha una convenció universal que has de seguir:
| Estil | Es fa servir per a | Exemples |
|---|---|---|
MAJÚSCULES |
Constants de l'script i variables d'entorn | CAMI_LOG, LLINDAR_ERRORS, PATH, HOME |
minúscules |
Variables internes de l'script, temporals, comptadors | data, total_errors, ciutat |
La raó és pràctica: l'entorn del sistema fa servir majúscules (PATH, HOME, USER, LANG), de manera que utilitzar-les per a variables qualssevol t'exposa a trepitjar-ne una del sistema —un PATH=/tmp descuidat deixa l'script sense trobar cap ordre—. Reserva les majúscules per al que de debò és constant o d'entorn.
- Fer servir variables:
$var davant de ${var}
$var davant de ${var}Per llegir el valor s'hi anteposa $: si ciutat=Valencia, aleshores echo "Ciutat: $ciutat" imprimeix Ciutat: Valencia. La forma amb claus ${ciutat} és equivalent... fins que deixa de ser-ho. Les claus són obligatòries quan el que segueix el nom es podria confondre amb part del nom mateix:
data=2026-08-03
echo "enviaments_$data.csv" # enviaments_2026-08-03.csv (el punt talla el nom)
echo "informe_$data_v2.txt" # informe_ ← MALAMENT!
echo "informe_${data}_v2.txt" # informe_2026-08-03_v2.txtLa segona línia surt mutilada perquè el guió baix sí que és un caràcter vàlid en un nom de variable: Bash ha buscat una variable anomenada data_v2, que no existeix, i l'ha substituïda per res. Les claus marquen on acaba el nom i eliminen l'ambigüitat.
Hi ha un tercer motiu: les claus són la sintaxi de l'expansió de paràmetres, la família que inclou valors per defecte com ${var:-valor} (03-06) i manipulació de cadenes com ${var^^} (04-04). Per això molts equips adopten la norma d'escriure sempre ${var}.
- Bash no té tipus: tot és una cadena
Aquesta és una idea que cal assimilar bé, perquè explica molts comportaments desconcertants. A Bash tots els valors són cadenes de text. No existeixen ni els enters, ni els booleans, ni els decimals.
A llindar=50, aquest 50 és el text «cinc, zero», no pas el número cinquanta. Conseqüències directes:
- Per comparar com a números cal fer servir operadors específics (
-eq,-gt, …) o(( )); comparar amb=compara text, i"10" = "9"és fals mentre que10 -gt 9és cert. Aquest contrast és la lliçó 03-03. - Per sumar no n'hi ha prou amb
a=$b+$c. Cal l'expansió aritmètica$(( )), que veuràs a 03-03 i a fons a 04-06. - No hi ha decimals.
50.5es desa sense problema com a text, però qualsevol operació aritmètica amb ell donarà error. Per als decimals es recorre abcoawk(04-06 i 06-01).
Que tot sigui text també vol dir que una variable no declarada i una variable buida es comporten gairebé igual: totes dues s'expandeixen a res. És còmode i alhora perillós, i d'aquí que existeixi ${var:?missatge} per exigir que una variable tingui valor (03-06).
- Substitució d'ordres amb
$(...)
$(...)Aquí és on les variables es tornen realment útils en operacions. La substitució d'ordres executa una ordre i substitueix l'expressió sencera per la seva sortida:
Bash executa date +%F, recull el que escriu a stdout i ho desa a data. Els salts de línia finals s'eliminen automàticament, cosa molt convenient. Això converteix qualsevol canonada del Mòdul 2 en un valor manipulable:
total_errors=$(grep -c ERROR /var/log/veloz/app.log)
total_enviaments=$(tail -n +2 /srv/veloz/dades/enviaments.csv | wc -l)
incidencies=$(grep -c ',incidencia,' /srv/veloz/dades/enviaments.csv)
echo "Errors: $total_errors | Enviaments: $total_enviaments | Incidències: $incidencies"
# → Errors: 37 | Enviaments: 1246 | Incidències: 118Hi ha una forma antiga amb cometes invertides, `ordre`, que encara veuràs en scripts heretats. Està desaconsellada:
| Aspecte | $(ordre) |
`ordre` |
|---|---|---|
| Imbricació | Directa: $(dirname $(which bash)) |
Cal escapar cada nivell |
| Llegibilitat | Els parèntesis s'aparellen visualment | Les dues cometes són idèntiques |
| Confusió visual | Cap | Es confon amb ' en moltes tipografies |
| Escapaments interns | Naturals | Regles pròpies i sorprenents |
Fes servir sempre $( ). Un avís de rendiment: cada $(...) llança un subshell, és a dir, un procés nou (lliçó 01-04). Amb tres o quatre no es nota, però dins d'un bucle de mil voltes sí; aquest és el tipus d'optimització que tracta 08-02.
- Constants amb
readonly i declare -r
readonly i declare -rAlgunes variables no han de canviar mai: els camins base de l'script, els llindars acordats amb negoci, el nom del servei. Marcar-les com a constants documenta aquesta intenció i, a més, fa que Bash la faci complir.
readonly CAMI_APP_LOG="/var/log/veloz/app.log"
readonly LLINDAR_ERRORS=50
declare -r CAMI_CSV="/srv/veloz/dades/enviaments.csv" # forma equivalentSi després alguna cosa les intenta modificar, Bash s'hi nega amb bash: LLINDAR_ERRORS: variable de només lectura. Dos matisos. Aquest error no atura l'script per si sol (llevat que hi hagi set -e, lliçó 05-03), però deixa rastre i retorna un codi diferent de zero. I readonly és irreversible: no es desfà amb unset a la sessió actual. En un script això és el que vols; al teu terminal interactiu, ves amb compte abans de declarar constants a la lleugera.
Regla d'or a partir d'ara: totes les constants van juntes al principi del fitxer, sota la capçalera. Qui obri l'script veu en deu segons quins camins fa servir i quins llindars aplica, sense llegir-ne el cos.
declare i les seves opcions útils
declare i les seves opcions útilsdeclare crea variables amb atributs. Les opcions que importen ara:
| Opció | Efecte | Exemple |
|---|---|---|
-r |
Només lectura (constant) | declare -r MAX=100 |
-i |
Tracta la variable com un enter | declare -i comptador=0 |
-x |
L'exporta a l'entorn (com export) |
declare -x VELOZ_ENV=produccio |
-a / -A |
Array indexat / associatiu | S'estudien a 04-03 |
-p |
Mostra la declaració d'una variable | declare -p LLINDAR_ERRORS |
L'atribut -i és curiós: fa que les assignacions s'avaluïn aritmèticament sense necessitat de $(( )). I declare -p és una eina de depuració excel·lent, perquè mostra el valor i els atributs:
declare -i comptador=0; comptador=comptador+5; echo "$comptador" # → 5
sense_atribut=0; sense_atribut=sense_atribut+5; echo "$sense_atribut" # → sense_atribut+5
declare -p CAMI_CSV # → declare -r CAMI_CSV="/srv/veloz/dades/enviaments.csv"
- Àmbit: variables de shell davant de variables d'entorn
Reprenem la distinció de la lliçó 01-04, ara amb conseqüències pràctiques. Una variable pot viure en dos llocs:
- Variable de shell: només existeix al shell que la va crear. És el que obtens amb una assignació normal.
- Variable d'entorn: es copia a tots els processos fills. S'aconsegueix amb
export.
ciutat=Valencia # variable de shell
export VELOZ_ENTORN=produccio # variable d'entorn
bash -c 'echo "ciutat=[$ciutat] entorn=[$VELOZ_ENTORN]"'
# → ciutat=[] entorn=[produccio]El shell fill no veu ciutat perquè no s'ha exportat. Aquesta és la causa número u de la pregunta «per què el meu script no veu la variable que vaig definir abans?»: un script és un procés fill, i només hereta el que s'ha exportat.
L'herència, a més, va en una sola direcció: si el fill modifica VELOZ_ENTORN, el pare no se n'assabenta. En rep una còpia, no pas un enllaç. Per això un script no pot canviar el directori ni les variables del teu terminal, i per això existeix source (03-01).
Consulta l'entorn amb env o printenv, i totes les variables amb set. Per fixar una variable d'entorn només durant una invocació concreta, s'anteposa a l'ordre: VELOZ_ENTORN=proves informe-diari.sh. A la pràctica s'exporta poc: només allò que altres programes necessiten llegir.
- Variables especials del shell
Bash defineix automàticament variables que aporten informació del context. Les més útils ara mateix:
| Variable | Conté |
|---|---|
$? |
Codi de sortida de l'última ordre (01-04) |
$$ |
PID del shell o script actual — ideal per a fitxers temporals únics |
$0 |
Nom amb què s'ha invocat l'script |
$HOME, $USER, $HOSTNAME |
Directori personal, usuari i nom de l'equip |
$PWD / $OLDPWD |
Directori actual / anterior |
$RANDOM |
Enter pseudoaleatori entre 0 i 32767, diferent a cada lectura |
$SECONDS |
Segons transcorreguts des que va arrencar l'script |
$LINENO |
Número de línia actual — molt útil en depurar (05-03) |
temporal="/tmp/informe-$$-$RANDOM.tmp"
echo "Executant $0 com a $USER a $HOSTNAME"
echo "Temporal: $temporal · Durada: $SECONDS s"Executant /home/joan/veloz-ops/bin/informe-diari.sh com a joan a srv-veloz-01 Temporal: /tmp/informe-48213-9174.tmp · Durada: 2 s
Combinar $$ i $RANDOM és el truc clàssic per a noms temporals que no col·lideixin si l'script s'executa dues vegades alhora; a 05-01 el substituirem per mktemp, que a més neteja darrere seu.
Falten les variables d'arguments —$1, $@, $#—, que són el tema complet de la lliçó 03-05.
unset i la refactorització d'informe-diari.sh
unset i la refactorització d'informe-diari.shunset ciutat elimina la variable del tot, i la deixa indefinida: echo "[$ciutat]" imprimirà []. Compte: unset no funciona sobre constants declarades amb readonly, i no és el mateix que assignar-hi la cadena buida. Per a gairebé tot donen el mateix resultat, però ${var:?} i ${var+x} (03-06) sí que distingeixen tots dos casos.
Amb totes les peces sobre la taula, així queda informe-diari.sh:
#!/usr/bin/env bash
#
# informe-diari.sh - Resum diari d'activitat de Veloz Envíos
#
# Propòsit : Comptar els ERROR d'app.log i agrupar els enviaments per estat.
# Autor : Joan Costa <[email protected]> · Ús: informe-diari.sh
# Codis : 0 correcte
#
# --- Constants ---------------------------------------------------------
readonly CAMI_APP_LOG="/var/log/veloz/app.log"
readonly CAMI_CSV="/srv/veloz/dades/enviaments.csv"
readonly DIR_INFORMES="$HOME/veloz-ops/logs"
readonly LLINDAR_ERRORS=50
readonly SERVIDOR="srv-veloz-01"
# --- Dades calculades --------------------------------------------------
data=$(date +%F)
hora=$(date +%T)
total_errors=$(grep -c ERROR "$CAMI_APP_LOG")
total_enviaments=$(tail -n +2 "$CAMI_CSV" | wc -l)
# --- Informe -----------------------------------------------------------
echo "==================================================="
echo " INFORME DIARI - VELOZ ENVIOS ($SERVIDOR)"
echo " Generat: ${data} ${hora}"
echo "==================================================="
echo
echo "-- Errors a app.log --"
echo "Total: ${total_errors} (llindar d'avís: ${LLINDAR_ERRORS})"
echo
echo "-- Enviaments per estat (${total_enviaments} en total) --"
tail -n +2 "$CAMI_CSV" | cut -d, -f5 | sort | uniq -c | sort -rn
exit 0Compara'l amb la versió de 03-01 i mesura la millora. Els camins apareixen una sola vegada, agrupats i visibles a les primeres línies: moure enviaments.csv és canviar un caràcter. El llindar acordat amb negoci existeix ara com a dada del programa, amb nom propi, a punt perquè la lliçó 03-04 el converteixi en un avís automàtic. Els recomptes es calculen un cop i es reutilitzen. I l'informe mostra data, hora i servidor, informació imprescindible quan algú el llegeixi tres setmanes després.
Fixa't en dos detalls que anticipen 03-06: les variables es fan servir sempre entre cometes dobles ("$CAMI_CSV") i s'hi posen claus quan el valor va enganxat a un altre text. Adopta l'hàbit des d'ara.
Errors Habituals i Consells
- Espais al voltant de l'
=.ciutat = Valenciaintenta executar l'ordreciutat. És l'error número u del principiant. - Oblidar el
$en llegir, o posar-lo en assignar.echo ciutatimprimeix la paraula;$ciutat=Valenciaés un error de sintaxi. El$només es fa servir per llegir. - Noms enganxats a text sense claus.
$data_v2busca la variabledata_v2. Fes servir${data}_v2. - Valors amb espais sense cometes.
msg=Informe diariintenta executardiariambmsg=Informea l'entorn. - Fer servir MAJÚSCULES per a tot. Tard o d'hora trepitjaràs
PATH,HOMEoIFS, amb conseqüències difícils de diagnosticar. - Esperar que un script vegi les variables del pare. Només hereta el que s'ha exportat; per a això hi ha
exportosource. - Confiar en l'aritmètica amb text.
total=$a+$bprodueix la cadena5+3. Necessites$(( )).
Exercicis
Exercici 1 — Capçalera de constants. Escriu el bloc de constants d'un script resum-ciutat.sh que hagi de treballar amb enviaments.csv, escrigui a ~/veloz-ops/logs, consideri «moltes incidències» a partir de 30 i analitzi València per defecte. Fes servir la convenció correcta de majúscules i readonly, i afegeix-hi una variable no constant amb la data del dia en format AAAA-MM-DD.
Exercici 2 — Depurar un script trencat. Un company ha escrit això i no funciona. Identifica'n els cinc errors i reescriu-lo.
#!/usr/bin/env bash
CAMI = "/srv/veloz/dades/enviaments.csv"
total_enviaments = `wc -l < $CAMI`
fitxer_sortida="$HOME/veloz-ops/logs/resum_$data_final.txt"
echo "Enviaments: total_enviaments" > $fitxer_sortidaExercici 3 — Mètriques de l'informe. Amplia el bloc de dades calculades d'informe-diari.sh perquè desi en variables: el nombre de lliuraments completats, el nombre d'incidències, el repartidor amb més enviaments del dia i el nom del fitxer de sortida (que ha d'incloure la data). Imprimeix les quatre amb etiquetes.
Solucions
Solució a l'Exercici 1
readonly CAMI_CSV="/srv/veloz/dades/enviaments.csv" # --- Constants ---
readonly DIR_INFORMES="$HOME/veloz-ops/logs"
readonly LLINDAR_INCIDENCIES=30
readonly CIUTAT_DEFECTE="Valencia"
data=$(date +%F) # --- Dades calculades ---Les quatre primeres són constants, en majúscules i amb readonly; data va en minúscules i sense readonly perquè és un valor calculat a cada execució, no una decisió de disseny. Fixa't que $HOME dins de cometes dobles s'expandeix correctament, cosa que fa l'script vàlid per a qualsevol usuari: escriure /home/joan/... a mà el trencaria per a tots els altres.
Solució a l'Exercici 2
Els cinc errors: (1) CAMI = "..." té espais al voltant de l'=; (2) total_enviaments = ... repeteix la mateixa errada; (3) fa servir cometes invertides en lloc de $( ); (4) $data_final busca una variable inexistent —hi falta ${data}_final— i, a més, data no es defineix mai; (5) echo "Enviaments: total_enviaments" imprimeix el text literal perquè hi falta el $, i > $fitxer_sortida va sense cometes.
#!/usr/bin/env bash
readonly CAMI="/srv/veloz/dades/enviaments.csv"
data=$(date +%F)
total_enviaments=$(tail -n +2 "$CAMI" | wc -l)
fitxer_sortida="$HOME/veloz-ops/logs/resum_${data}_final.txt"
echo "Enviaments: $total_enviaments" > "$fitxer_sortida"S'hi ha afegit a més tail -n +2 per no comptar la capçalera del CSV, una errada de contingut que la versió original arrossegava sense que ningú se n'adonés.
Solució a l'Exercici 3
lliurats=$(grep -c ',lliurat,' "$CAMI_CSV")
incidencies=$(grep -c ',incidencia,' "$CAMI_CSV")
top_repartidor=$(tail -n +2 "$CAMI_CSV" | cut -d, -f4 | sort | uniq -c \
| sort -rn | head -1 | tr -s ' ' | cut -d' ' -f3)
fitxer_sortida="${DIR_INFORMES}/informe-$(date +%F).txt"
echo "Lliurats : $lliurats" # → 981
echo "Incidències : $incidencies" # → 118
echo "Top repartidor : $top_repartidor" # → mgarcia
echo "Fitxer destí : $fitxer_sortida" # → ~/veloz-ops/logs/informe-2026-08-03.txtLa línia de top_repartidor mereix explicació. uniq -c genera línies amb espais d'emplenament al principi ( 412 mgarcia), així que tr -s ' ' els comprimeix en un de sol i cut -d' ' -f3 pren el tercer camp —el primer queda buit per culpa de l'espai inicial—. És funcional, però també un avís: quan una canonada necessita tants ajustos, l'eina adequada és awk, i a 06-01 aquesta mateixa línia es reduirà a una expressió molt més neta.
Conclusió
informe-diari.sh ja no és una llista d'ordres: és un programa amb dades. Saps assignar sense espais al voltant de l'= i per què existeix aquesta regla; anomenes seguint la convenció de majúscules per a les constants i minúscules per a la resta; fas servir ${var} quan les claus són necessàries; assumeixes que a Bash tot és text; captures la sortida de qualsevol canonada amb $( ); blindes camins i llindars amb readonly; coneixes declare i els seus atributs; distingeixes les variables de shell de les d'entorn; i manejes les especials com $?, $$ o $SECONDS.
L'script té ara un LLINDAR_ERRORS=50 a la capçalera que no serveix de res: s'imprimeix, però ningú no el compara amb res. I continua donant per fet que app.log i enviaments.csv existeixen i són llegibles; si operacions reanomena el CSV, l'script no avisarà: escopirà un informe amb zeros.
A la lliçó 03-03 aprendràs a preguntar. Veuràs els operadors del shell: com encadenar ordres amb && i ||, com comprovar si un fitxer existeix amb [[ -f ... ]], i com comparar números —el clàssic -gt davant de >— per poder respondre la pregunta que aquest llindar porta esperant des del principi: hi ha avui més errors dels tolerables?
Curs de Programació en Bash
Mòdul 1: Introducció a Bash
- Què és Bash?
- Configurar el teu Entorn
- Navegació Bàsica per la Línia d'Ordres
- Entendre el Shell
- Trobar Ajuda: man, help i --help
Mòdul 2: Ordres Bàsiques de Bash
- Operacions amb Fitxers i Directoris
- Ordres de Processament de Text
- Permisos i Propietat dels Fitxers
- Redirecció i Canonades
- Comodins i Expansió de Rutes
- Historial i Dreceres de Teclat
Mòdul 3: Fonaments de Scripting
- Crear i Executar un Script
- Variables i Constants
- Operadors Bàsics
- Sentències Condicionals
- Arguments i Entrada de l'Usuari
- Cometes, Expansió i Substitució
Mòdul 4: Scripting Intermedi
- Bucles en Bash
- Funcions en Bash
- Arrays i Arrays Associatius
- Manipulació de Cadenes
- La Sentència case i els Menús Interactius
- Aritmètica i Càlculs Numèrics
Mòdul 5: Tècniques Avançades de Scripting
- Operacions Avançades amb Fitxers
- Gestió de Processos
- Gestió d'Errors i Depuració
- Expressions Regulars
- Entrada/Sortida Avançada: Descriptors i Here-Documents
- Scripts Modulars i Llibreries Reutilitzables
Mòdul 6: Treballar amb Eines Externes
Mòdul 7: Automatització i Programació
- Tasques Cron
- Automatitzar Tasques
- Scripts de Còpia i Restauració
- Monitoratge i Registre
- Serveis i Temporitzadors amb systemd
- Automatització Remota amb SSH
Mòdul 8: Bones Pràctiques i Optimització
- Escriure Codi Llegible
- Optimitzar Scripts en Bash
- Consideracions de Seguretat
- Control de Versions amb Git
- Anàlisi Estàtica amb ShellCheck i shfmt
- Proves Automatitzades amb Bats
- Portabilitat: POSIX sh enfront de Bashismes
