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
forsobre llistes literalsforsobre globs i sobre"$@"fora l'estil Cwhileiuntil- Quin triar: taula comparativa
- Llegir un fitxer línia a línia: el patró canònic
- El parany de la canonada i el subshell
- Llegir un CSV separant camps
breakicontinue, i els seus nivells- Bucles infinits controlats
- Imbricació, comptadors i cost
for sobre llistes literals
for sobre llistes literalsLa forma més simple de for recorre una llista de paraules separades per espais i va assignant cadascuna a una variable:
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}ofor f in $(date +%F)són igual de vàlids. do...donedelimiten el cos. El;abans dedonomé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).
for sobre globs i sobre "$@"
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"
doneLa 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 "$@":
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.
for a l'estil C
for a l'estil CQuan el que necessites és un comptador numèric i no una llista, Bash ofereix la sintaxi de tres expressions aritmètiques heretada de C:
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.
while i until
while i untilwhile 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:
Regla pràctica: si el teu while comença amb !, segurament volies un until.
- 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ò».
- 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:
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 queVAR=valor ordreafecta únicament aquella ordre). Sense això,readretallaria els espais i tabuladors del principi i del final de cada línia. AmbIFS=la línia arriba literal, amb la seva indentació intacta.-rdesactiva la interpretació de la barra invertida. Sense-r, un camí comC:\veloz\dadesperdria 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,readdiposita el contingut aREPLY.< fitxeral final: la redirecció s'aplica a tot el bucle, que actua com una única ordre composta.readva consumint aquella entrada línia a línia fins a exhaurir-la, moment en què retorna un codi diferent de 0 i elwhiles'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.
- 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 realL'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.logLa 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 <.
- 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.csvSortida: E1043 Sevilla mgarcia 31.20, una línia per incidència. Tres detalls importants:
- El
read -r _capcalerainicial consumeix la líniaid_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.
break i continue, i els seus nivells
break i continue, i els seus nivellsbreak 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
donebreak 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).
- 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
donetrue és una ordre que no fa res i sempre retorna 0, així que la condició no falla mai. Regles de seguretat:
- Sempre un
sleepa dins. Sense ell consumiràs una CPU al 100% sense guanyar-hi res. - Sempre una via de sortida: un
breaksota alguna condició, o com a mínim queCtrl+Cel mati (per defecte ho fa). - Si el bucle ha de viure per sempre, el correcte no és un
while truellançat a mà, sinó un temporitzador del sistema. Ho veuràs a 07-04 i 07-05.
- 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 servirfor f in *.csv.- Oblidar
IFS=o-raread. 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
< <(...), maiordre | while. - Bucle sobre un glob que no coincideix. Afegeix la guarda
[[ -e "$f" ]] || continueoshopt -s nullglob. - Modificar la llista mentre es recorre. La llista del
fores 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"
doneEl 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
- 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
