Tancàvem el Mòdul 3 amb una promesa: informe-diari.sh deixaria de ser un guió lineal per convertir-se en un programa amb estructura, i els bucles serien la primera eina a arribar. Ja hi som. Fins ara, quan volies l'informe de València i el de Sevilla, executaves l'script dues vegades; quan volies mirar els quatre fitxers rotats d'app.log, copiaves i enganxaves la mateixa canonada quatre vegades canviant-ne el nom. Un bucle és exactament el contrari: escrius l'operació una sola vegada i li dones la llista de coses sobre les quals aplicar-la. En aquesta lliçó veuràs les quatre maneres d'iterar que ofereix Bash, quan fer servir cadascuna, i el patró canònic —amb els seus paranys— per llegir un fitxer línia a línia.

Contingut

  1. for sobre llistes literals
  2. for sobre globs i sobre "$@"
  3. for a l'estil C
  4. while i until
  5. Quin triar: taula comparativa
  6. Llegir un fitxer línia a línia: el patró canònic
  7. El parany de la canonada i el subshell
  8. Llegir un CSV separant camps
  9. break i continue, i els seus nivells
  10. Bucles infinits controlats
  11. Imbricació, comptadors i cost

  1. for sobre llistes literals

La forma més simple de for recorre una llista de paraules separades per espais i va assignant cadascuna a una variable:

for ciutat in Valencia Sevilla Bilbao Madrid; do
    echo "Generant informe de $ciutat..."
done

Imprimeix les quatre línies, una per ciutat. Desglossem-ne cada peça:

  • for ciutat in ...: ciutat és la variable de control. No es declara abans ni s'esborra després: en acabar conserva l'últim valor (Madrid).
  • La llista després d'in és una llista de paraules, no una cadena. Bash la genera aplicant les expansions de 03-06: for n in {1..5} o for f in $(date +%F) són igual de vàlids.
  • do ... done delimiten el cos. El ; abans de do només cal si estan a la mateixa línia.

Compte: posar entre cometes una cadena amb espais (for c in "$CIUTATS") produeix un únic element, no diversos. Per a llistes de debò es fan servir arrays (04-03).

  1. for sobre globs i sobre "$@"

Com que l'expansió de comodins (02-05) passa abans d'executar l'ordre, un patró es converteix en la llista de fitxers que existeixen. Aquest és l'idioma correcte per recórrer fitxers, i la raó per la qual a 03-06 insistíem que mai es fa for f in $(ls):

for fitxer in /var/log/veloz/app.log.*; do
    [[ -f "$fitxer" ]] || continue          # protegeix del cas "sense coincidències"
    echo "== $fitxer: $(wc -l < "$fitxer") línies"
done

La línia de guarda és important: si no existeix cap app.log.*, Bash deixa el patró sense expandir i la variable valdrà literalment /var/log/veloz/app.log.*. Les dues solucions són la guarda [[ -f ... ]] || continue o activar shopt -s nullglob, que fa que un patró sense coincidències produeixi una llista buida i el bucle no s'executi ni una sola vegada.

Sense llista in, for recorre implícitament els arguments de l'script, és a dir "$@":

for ciutat; do                 # equival a: for ciutat in "$@"; do
    echo "Processant $ciutat"
done

Escriure for ciutat in "$@" és més explícit i sempre correcte. Escriure for ciutat in $@ (sense cometes) es trenca amb qualsevol argument que contingui espais, pel que ja saps de 03-05.

  1. for a l'estil C

Quan el que necessites és un comptador numèric i no una llista, Bash ofereix la sintaxi de tres expressions aritmètiques heretada de C:

for (( i = 1; i <= 3; i++ )); do
    printf 'Intent %d de 3\n' "$i"
done

Dins dels dobles parèntesis ets en context aritmètic: les variables no porten $, es comparen amb < i > de manera numèrica, i i++ funciona. És el mateix context que veuràs a fons a 04-06. Les tres expressions són inicialització, condició de continuació i increment, i qualsevol es pot ometre (for (( ;; )) és un bucle infinit).

Fes-lo servir quan l'índex importi; per recórrer valors, el for ... in és més llegible.

  1. while i until

while repeteix el cos mentre una ordre retorni codi de sortida 0, i until fins que el retorni. Nota clau que enllaça amb 03-04: la condició no és una expressió booleana, és una ordre de la qual s'avalua el $?.

intents=0
while ! curl -sf http://localhost:8080/salud > /dev/null; do
    intents=$(( intents + 1 ))
    (( intents >= 5 )) && { echo "veloz-api no respon" >&2; exit 4; }
    echo "Esperant la veloz-api (intent $intents)..."
    sleep 2
done
echo "veloz-api operativa"

El mateix bucle amb until elimina la negació i es llegeix millor:

until curl -sf http://localhost:8080/salud > /dev/null; do
    ...
done

Regla pràctica: si el teu while comença amb !, segurament volies un until.

  1. Quin triar: taula comparativa

Forma Es fa servir quan Exemple típic a Veloz Envíos
for x in llista Coneixes d'entrada els elements Les quatre ciutats, els repartidors
for (( i=0; i<n; i++ )) Necessites un índex numèric Recórrer un array per posició, reintents numerats
while cond Repeteixes mentre una cosa sigui certa i no saps quantes vegades Llegir línies d'un registre fins al final
until cond Igual que while, però la condició natural és la negativa Esperar que l'API arrenqui

for respon a «per a cadascun d'aquests»; while/until responen a «mentre/fins que passi això».

  1. Llegir un fitxer línia a línia: el patró canònic

Aquest és probablement el fragment de Bash que més vegades escriuràs a la vida. Memoritza'l sencer:

while IFS= read -r linia; do
    echo "[$linia]"
done < /var/log/veloz/app.log

Cada peça hi és per una raó concreta:

  • IFS= buida el separador de camps només per a aquesta ordre (recorda de 03-06 que VAR=valor ordre afecta únicament aquella ordre). Sense això, read retallaria els espais i tabuladors del principi i del final de cada línia. Amb IFS= la línia arriba literal, amb la seva indentació intacta.
  • -r desactiva la interpretació de la barra invertida. Sense -r, un camí com C:\veloz\dades perdria les barres i un \ final uniria dues línies. Llevat que estiguis implementant un intèrpret d'escapaments, sempre -r.
  • linia és la variable destinació. Si no en poses cap, read diposita el contingut a REPLY.
  • < fitxer al final: la redirecció s'aplica a tot el bucle, que actua com una única ordre composta. read va consumint aquella entrada línia a línia fins a exhaurir-la, moment en què retorna un codi diferent de 0 i el while s'acaba.

Un advertiment poc conegut: si el fitxer no acaba en salt de línia, read desa l'última línia però retorna codi d'error i el bucle la descarta. L'assegurança és while IFS= read -r linia || [[ -n "$linia" ]]; do ... done < fitxer.

  1. El parany de la canonada i el subshell

És temptador escriure el bucle anterior alimentant-lo amb una canonada. Funciona… fins que intentes fer servir una variable després:

errors=0
grep '\[ERROR\]' /var/log/veloz/app.log | while IFS= read -r linia; do
    errors=$(( errors + 1 ))
done
echo "Errors: $errors"        # imprimeix 0, no el total real

L'explicació ve directament de 01-04: cada tram d'una canonada s'executa en un subshell, un procés fill amb la seva pròpia còpia de les variables. El bucle incrementa errors de debò, però a la còpia del fill; quan el fill mor, el valor mor amb ell i el pare continua veient el seu 0 original.

flowchart LR
    A["Shell pare<br/>errors=0"] -->|"| canonada"| B["Subshell<br/>errors=0→1→2"]
    B -.->|"el fill mor<br/>i s'endú el valor"| C["Shell pare<br/>errors=0"]
    A ==>|"< redirecció"| D["Mateix shell<br/>errors=2 ✓"]

Les dues solucions correctes:

# A) Redirecció al final: el bucle corre al shell actual
errors=0
while IFS= read -r linia; do errors=$(( errors + 1 )); done \
    < <(grep '\[ERROR\]' /var/log/veloz/app.log)

# B) Substitució de processos amb un fitxer real
while IFS= read -r linia; do ...; done < /var/log/veloz/app.log

La construcció < <(ordre) s'anomena substitució de processos: converteix la sortida d'una ordre en una cosa que es pot redirigir com si fos un fitxer, sense crear cap subshell per al bucle. Fixa't en l'espai obligatori entre els dos <.

  1. Llegir un CSV separant camps

read accepta diverses variables i reparteix els camps segons IFS. Fixant IFS=, obtens un lector de CSV en una línia:

{
    read -r _capcalera                                  # descarta la primera línia
    while IFS=, read -r id data ciutat repartidor estat import; do
        [[ "$estat" == "incidencia" ]] || continue
        printf '%-8s %-10s %-10s %s\n' "$id" "$ciutat" "$repartidor" "$import"
    done
} < /srv/veloz/dades/enviaments.csv

Sortida: E1043 Sevilla mgarcia 31.20, una línia per incidència. Tres detalls importants:

  • El read -r _capcalera inicial consumeix la línia id_enviament,data,.... Com que és dins del mateix bloc { ... } < fitxer, comparteix l'entrada amb el bucle.
  • Si hi ha més camps que variables, l'última variable rep tota la resta. Per això import, que és l'última, es queda amb el que sobri. I si n'hi ha menys, les variables finals queden buides.
  • Aquest lector no entén cometes ni comes dins dels camps. Per a CSV complexos es fa servir awk (06-01); per al nostre, és perfecte.

  1. break i continue, i els seus nivells

break surt del bucle; continue salta a la iteració següent. Tots dos accepten un número que indica quants nivells d'imbricació afecten:

for ciutat in Valencia Sevilla Bilbao Madrid; do
    for repartidor in alopez mgarcia jruiz; do
        if [[ ! -r "$CAMI_CSV" ]]; then
            echo "CSV il·legible, avortant-ho tot" >&2
            break 2                    # surt DELS DOS bucles
        fi
        [[ "$repartidor" == "jruiz" && "$ciutat" == "Madrid" ]] && continue
        echo "$ciutat / $repartidor"
    done
done

break sense número equival a break 1 i només sortiria del bucle intern. Els nivells es compten de dins cap enfora. Fes-ne un ús moderat: un break 3 és senyal que aquell bloc demana ser una funció amb un return (04-02).

  1. Bucles infinits controlats

Un bucle sense condició de sortida és una eina legítima quan l'script és un vigilant:

while true; do
    ara=$(date '+%F %T')
    errors=$(grep -c '\[ERROR\]' /var/log/veloz/app.log)
    printf '%s  errors acumulats: %s\n' "$ara" "$errors"
    sleep 60
done

true és una ordre que no fa res i sempre retorna 0, així que la condició no falla mai. Regles de seguretat:

  • Sempre un sleep a dins. Sense ell consumiràs una CPU al 100% sense guanyar-hi res.
  • Sempre una via de sortida: un break sota alguna condició, o com a mínim que Ctrl+C el mati (per defecte ho fa).
  • Si el bucle ha de viure per sempre, el correcte no és un while true llançat a mà, sinó un temporitzador del sistema. Ho veuràs a 07-04 i 07-05.

  1. Imbricació, comptadors i cost

Imbricar bucles multiplica la feina: 4 ciutats × 3 repartidors són 12 iteracions, i si a dins hi fas grep sobre un CSV de 50.000 línies, són 12 lectures completes. El cost real en Bash gairebé mai no és al bucle, sinó als processos externs que es llancen a dins: un for de 4 ciutats amb dos grep cadascuna són 8 processos i 8 recorreguts del fitxer.

Una sola passada acumulant comptadors és molt més ràpida, però per a això et cal un comptador per ciutat: això són arrays (04-03), i el percentatge amb decimals és aritmètica (04-06). De moment, el comptador simple, que sí que ja pots escriure:

total=0; incidencies=0
while IFS=, read -r _ _ ciutat _ estat _; do
    (( total++ ))
    [[ "$estat" == "incidencia" ]] && (( incidencies++ ))
done < <(tail -n +2 "$CAMI_CSV")
echo "$incidencies incidències de $total enviaments"

El _ és un nom de variable convencional per a «aquest camp no m'interessa»; no té cap significat especial en Bash, és només un costum llegible.

Errors Habituals i Consells

  • for f in $(ls *.csv). Ja t'ho vam advertir a 03-06, però val la pena repetir-ho: es trenca amb espais als noms. Fes servir for f in *.csv.
  • Oblidar IFS= o -r a read. Funcionarà durant mesos i fallarà el dia que aparegui un camí amb \ o una línia indentada.
  • Comptar dins d'una canonada. El comptador sempre acabarà a 0. Redirecció o < <(...), mai ordre | while.
  • Bucle sobre un glob que no coincideix. Afegeix la guarda [[ -e "$f" ]] || continue o shopt -s nullglob.
  • Modificar la llista mentre es recorre. La llista del for es calcula una sola vegada, al principi; esborrar fitxers dins del bucle no canvia el que queda per recórrer.
  • Consell: quan un bucle passi de les 15 línies, extreu-ne el cos a una funció. És exactament el que faràs a la lliçó següent.

Exercicis

Exercici 1. Escriu un bucle que recorri les quatre ciutats i, per a cadascuna, imprimeixi quantes línies del CSV li corresponen, amb el format Valencia: 128 enviaments.

Exercici 2. Recorre els fitxers rotats /var/log/veloz/app.log.* i mostra de cadascun el seu nom i el nombre de línies [ERROR] que conté, saltant-te els que no existeixin o siguin buits.

Exercici 3. Llegeix enviaments.csv línia a línia (sense capçalera) i compta quants enviaments ha fet alopez i quants d'ells van acabar en incidència. Imprimeix els dos números després del bucle: han de sortir correctes.

Solucions

Solució 1.

CAMI_CSV="/srv/veloz/dades/enviaments.csv"
for ciutat in Valencia Sevilla Bilbao Madrid; do
    n=$(grep -c ",${ciutat}," "$CAMI_CSV")
    echo "${ciutat}: ${n} enviaments"
done

El patró ,${ciutat}, amb comes a banda i banda evita falsos positius si una ciutat fos subcadena d'una altra.

Solució 2.

for f in /var/log/veloz/app.log.*; do
    [[ -f "$f" && -s "$f" ]] || continue      # -s: existeix i no és buit
    printf '%-32s %4d errors\n' "$f" "$(grep -c '\[ERROR\]' "$f")"
done

-s (vist a 03-03) descarta d'un sol cop els fitxers buits i el patró sense expandir.

Solució 3.

enviaments=0; incidencies=0
while IFS=, read -r _ _ _ repartidor estat _; do
    [[ "$repartidor" == "alopez" ]] || continue
    (( enviaments++ ))
    [[ "$estat" == "incidencia" ]] && (( incidencies++ ))
done < <(tail -n +2 /srv/veloz/dades/enviaments.csv)
echo "alopez: ${enviaments} enviaments, ${incidencies} incidències"

La clau és a < <(tail ...): amb tail ... | while els dos comptadors sortirien a 0. Fixa't també en el || continue en comptes d'un if que ho envolti tot: redueix un nivell d'indentació i es llegeix igual de bé.

Conclusió

Ja saps repetir. for recorre llistes conegudes —literals, globs o "$@"—, la seva variant a l'estil C recorre índexs, i while/until repeteixen mentre una ordre continuï retornant el codi que esperes. El patró while IFS= read -r linia; do ... done < fitxer és el lector de fitxers oficial de Bash, i la regla que l'acompanya —redirecció al final, mai una canonada— t'estalviarà el bug més frustrant del llenguatge, aquell en què un comptador sempre val 0 perquè vivia en un subshell.

Però fixa't en el que ha passat als últims exemples: dins de cada bucle repetíem les mateixes cinc línies de validació i el mateix format de sortida, només que amb una altra ciutat. Hem eliminat la repetició entre invocacions de l'script, però no dins d'ell. L'eina que ho resol són les funcions (04-02): blocs de codi amb nom, amb els seus propis arguments i les seves pròpies variables, que convertiran validar_entorn, comptar_errors i resum_ciutat en peces que s'escriuen una vegada i es criden moltes.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats