A la lliçó anterior vam colar una línia sense explicar-la: readonly CIUTATS=(Valencia Sevilla Bilbao Madrid), recorreguda després amb for ciutat in "${CIUTATS[@]}". Això és un array, i és l'estructura de dades que li faltava a informe-diari.sh per deixar de llegir el CSV una vegada per ciutat. Bash en té dos tipus: els indexats, llistes ordenades amb índex numèric, i els associatius, mapes de clau arbitrària a valor, que són la resposta nativa a «quantes incidències té cada ciutat» sense sortir del shell. En acabar aquesta lliçó sabràs acumular comptadors en una sola passada i jubilar el sort | uniq -c del Mòdul 2 quan el resultat s'hagi de fer servir dins de l'script.

Contingut

  1. Crear arrays indexats
  2. Accedir als elements i a tot l'array
  3. "${arr[@]}" davant de "${arr[*]}"
  4. Longitud, índexs i recorregut
  5. Afegir, llescar i esborrar
  6. Arrays associatius
  7. El patró comptador
  8. Poblar arrays: mapfile i IFS
  9. Passar arrays a funcions
  10. Taula d'operacions
  11. Aplicació a informe-diari.sh

  1. Crear arrays indexats

Hi ha tres maneres, i totes produeixen el mateix:

ciutats=(Valencia Sevilla Bilbao Madrid)           # literal, l'habitual
declare -a repartidors                             # declaració explícita, buit
repartidors[0]="alopez"; repartidors[1]="mgarcia"
repartidors[5]="jruiz"                             # els índexs poden saltar

Punts clau: els elements van separats per espais, no per comes ((a, b) crea els elements a, i b); si un element conté espais cal posar-lo entre cometes, ciutats=("Sant Sebastià" Valencia); els índexs comencen a 0 i són dispersos, de manera que l'array anterior té 3 elements amb índexs 0, 1 i 5 i no hi ha forats buits pel mig; i declare -a no és obligatori, però documenta la intenció i cal si vols l'array buit des del principi. Una variable normal és, en realitat, l'element 0 d'un array: x=hola; echo "${x[0]}" imprimeix hola.

  1. Accedir als elements i a tot l'array

ciutats=(Valencia Sevilla Bilbao Madrid)
echo "${ciutats[0]}"         # Valencia
echo "${ciutats[-1]}"        # Madrid    (índex negatiu: des del final, Bash 4.3+)
echo "$ciutats"              # Valencia  ← parany! sense índex equival a [0]

Les claus són obligatòries. $ciutats[1] no accedeix a l'element 1: Bash expandeix $ciutats (element 0) i deixa [1] com a text literal. Sempre ${ciutats[1]}.

L'índex pot ser qualsevol expressió aritmètica: ${ciutats[i+1]} funciona, i dins dels claudàtors les variables no necessiten $.

  1. "${arr[@]}" davant de "${arr[*]}"

És exactament la mateixa distinció que "$@" davant de "$*" de 03-05, i per la mateixa raó:

Expansió Resultat
"${arr[@]}" Un argument per element, amb els espais interns intactes
"${arr[*]}" Un únic argument, elements units pel primer caràcter d'IFS
${arr[@]} sense cometes Es divideixen també els espais dins de cada element
llibres=("El Quixot" "La Regenta")
for x in "${llibres[@]}"; do echo "[$x]"; done   # [El Quixot] [La Regenta] → 2 voltes ✓
for x in "${llibres[*]}"; do echo "[$x]"; done   # [El Quixot La Regenta]   → 1 volta
for x in ${llibres[@]};   do echo "[$x]"; done   # [El][Quixot][La][Regenta] → 4, desastre

Regla sense excepcions: per recórrer o passar arrays, "${arr[@]}" amb cometes. "${arr[*]}" només serveix per a una cosa útil, unir els elements en una cadena llegible aprofitant IFS: IFS=', '; echo "Ciutats: ${ciutats[*]}"; unset IFS imprimeix Ciutats: Valencia, Sevilla, Bilbao, Madrid.

  1. Longitud, índexs i recorregut

echo "${#ciutats[@]}"       # 4        → nombre d'elements
echo "${#ciutats[2]}"       # 6        → longitud de la CADENA de l'element 2 ("Bilbao")
echo "${!ciutats[@]}"       # 0 1 2 3  → llista d'índexs existents

Compte amb l'asimetria: ${#arr[@]} compta elements, però ${#arr[i]} compta caràcters. I ${!arr[@]} amb ! al davant retorna les claus, no els valors; és l'única manera segura de recórrer un array dispers:

repartidors[0]="alopez"; repartidors[5]="jruiz"
for i in "${!repartidors[@]}"; do printf 'índex %d → %s\n' "$i" "${repartidors[$i]}"; done
# índex 0 → alopez
# índex 5 → jruiz

Si haguessis fet for (( i=0; i<${#repartidors[@]}; i++ )) recorreries els índexs 0 i 1, i l'element 5 no apareixeria mai. Aquest for a l'estil C només és fiable en arrays sense forats.

  1. Afegir, llescar i esborrar

ciutats+=(Zaragoza)                   # afegeix al final; NO facis servir ciutats=(... Zaragoza)
ciutats+=(Malaga Vigo)                # se'n poden afegir uns quants de cop
echo "${ciutats[@]:1:2}"              # Sevilla Bilbao → llesca: des d'1, dos elements
echo "${ciutats[@]: -2}"              # els dos últims (ull amb l'espai abans del -2)

L'esborrat té una particularitat que sorprèn tothom:

ciutats=(Valencia Sevilla Bilbao Madrid)
unset 'ciutats[1]'
echo "${#ciutats[@]}"      # 3
echo "${!ciutats[@]}"      # 0 2 3   ← l'índex 1 ha desaparegut, NO s'ha reindexat
ciutats=("${ciutats[@]}")     # així es reindexa a 0,1,2
unset 'ciutats'               # així s'esborra l'array sencer

unset deixa un forat: l'array passa a ser dispers, i només reconstruint-lo torna a ser compacte. Les cometes a unset 'ciutats[1]' eviten que l'expansió de comodins interpreti els claudàtors com una classe de caràcters si existeix un fitxer amb aquell nom.

  1. Arrays associatius

Disponibles des de Bash 4 (2009). Recorda de 01-02 com comprovar la teva versió, perquè a macOS el /bin/bash del sistema continua sent el 3.2: (( BASH_VERSINFO[0] >= 4 )) || { echo "Cal Bash 4+" >&2; exit 5; }.

A diferència dels indexats, declare -A és obligatori: sense ell, Bash tracta les claus com a expressions aritmètiques i tot acaba a l'índex 0.

declare -A zona                      # OBLIGATORI
zona["Valencia"]="Llevant"; zona["Sevilla"]="Sud"
zona[Bilbao]="Nord"                  # cometes només si la clau porta espais
echo "${zona[Valencia]}"             # Llevant
echo "${#zona[@]}"                   # 3
echo "${!zona[@]}"                   # Sevilla Valencia Bilbao ← ORDRE NO GARANTIT

També es poden crear d'un cop: declare -A zona=([Valencia]=Llevant [Sevilla]=Sud).

L'ordre de les claus és el de la taula hash interna, no el d'inserció ni l'alfabètic. Si necessites sortida ordenada, ordena-la tu:

for c in $(printf '%s\n' "${!zona[@]}" | sort); do printf '%-12s %s\n' "$c" "${zona[$c]}"; done

Per comprovar si una clau existeix, sense confondre-la amb una que existeix però val cadena buida, es fa servir if [[ -v zona[Madrid] ]]; then .... [[ -v ]] (Bash 4.2+) pregunta per l'existència de la variable o element. L'alternativa antiga, [[ -n "${zona[Madrid]}" ]], només mira si el valor no és buit, que no és el mateix. Esborrar una clau: unset 'zona[Madrid]'.

  1. El patró comptador

Aquest és el motiu principal pel qual existeixen els arrays associatius en un script d'operacions. La forma del Mòdul 2 —grep ',incidencia,' "$CAMI_CSV" | cut -d, -f3 | sort | uniq -c— és bonica al terminal però inútil dins de l'script: el resultat és text, i per fer servir el número de Sevilla caldria tornar a analitzar-lo. Amb un mapa, les dades queden en variables:

declare -A incidencies_per_ciutat enviaments_per_ciutat

while IFS=, read -r _ _ ciutat _ estat _; do
    (( enviaments_per_ciutat["$ciutat"]++ ))
    [[ "$estat" == "incidencia" ]] && (( incidencies_per_ciutat["$ciutat"]++ ))
done < <(tail -n +2 "$CAMI_CSV")

for ciutat in "${!enviaments_per_ciutat[@]}"; do
    printf '%-10s %4d enviaments, %3d incidències\n' \
        "$ciutat" "${enviaments_per_ciutat[$ciutat]}" "${incidencies_per_ciutat[$ciutat]:-0}"
done

Sortida: Valencia 128 enviaments, 11 incidències i una línia igual per cada ciutat. Tres detalls que fan que això funcioni:

  • (( mapa["$clau"]++ )) funciona encara que la clau no existeixi: en context aritmètic una variable sense valor compta com a 0, així que el primer increment la crea amb valor 1. No cal inicialitzar res.
  • ${incidencies_per_ciutat[$ciutat]:-0} fa servir el valor per defecte de 03-06 per a les ciutats sense incidències, que no tenen entrada en aquell mapa.
  • Tot el recompte es fa en una sola passada i sense processos externs: substitueix vuit invocacions de grep. Per a fitxers de milions de línies, en canvi, el bucle de Bash és lent i val més delegar en awk (06-01).

  1. Poblar arrays: mapfile i IFS

Per omplir un array amb la sortida d'una ordre, una línia per element, l'eina correcta és mapfile (també anomenada readarray, són sinònims):

mapfile -t logs < <(find /var/log/veloz -name 'app.log.*' -type f)
echo "${#logs[@]} fitxers rotats"; printf '  %s\n' "${logs[@]}"

L'opció -t elimina el salt de línia final de cada element; sense ella cada entrada arrossegaria un \n i les comparacions fallarien. Altres opcions útils: -n N (llegir com a màxim N línies) i -s N (saltar-se les N primeres, pràctic per a la capçalera d'un CSV). No facis servir mai arr=( $(ordre) ) sense cometes per a això: divideix per espais i aplica expansió de comodins, així que un nom amb espais o un * a la sortida trenquen l'array.

Per trossejar una cadena en array, es canvia IFS temporalment i es fa servir read -a:

linia="E1001,2026-08-03,Valencia,alopez,lliurat,24.50"
IFS=',' read -r -a camps <<< "$linia"
echo "${camps[2]}"       # Valencia
echo "${#camps[@]}"      # 6

<<< és un here-string: alimenta l'entrada estàndard de l'ordre amb aquella cadena (ho veuràs a fons a 05-05). -a camps diu a read que reparteixi els camps a l'array en comptes de a variables soltes. Com que l'IFS= va davant de l'ordre, només afecta aquella línia.

  1. Passar arrays a funcions

Aquí hi ha una limitació real de Bash: no es pot passar un array com un sol argument. Tot el que una funció rep són cadenes. Hi ha dues solucions:

# a) Expandir l'array en arguments i recollir-los amb "$@"
mostrar() { local -a items=( "$@" ); printf ' - %s\n' "${items[@]}"; }
mostrar "${ciutats[@]}"

Simple i suficient quan l'array és l'única cosa que passes. Si a més necessites passar altres arguments, posa'ls primer i fes servir shift.

# b) Nameref: passar el NOM de l'array (única opció per als associatius)
resumir_mapa() {
    local -n _mapa="$1"          # _mapa passa a ser un àlies de l'array real
    local clau
    for clau in "${!_mapa[@]}"; do printf '%-10s %s\n' "$clau" "${_mapa[$clau]}"; done
}
resumir_mapa incidencies_per_ciutat     # sense $, es passa el NOM

El nameref (local -n, introduït a 04-02) és imprescindible amb arrays associatius, perquè "${mapa[@]}" perdria les claus pel camí. També és la manera de retornar un array: la funció escriu a local -n sortida="$2" i qui la crida rep l'array complet.

  1. Taula d'operacions

Operació Array indexat Array associatiu
Declarar declare -a a (opcional) declare -A m (obligatori)
Crear a=(x y z) m=([k1]=v1 [k2]=v2)
Assignar a[3]="x" m[clau]="v"
Llegir ${a[3]} ${m[clau]}
Tots els valors "${a[@]}" "${m[@]}"
Totes les claus "${!a[@]}" (números) "${!m[@]}" (text, sense ordre)
Nombre d'elements ${#a[@]} ${#m[@]}
Afegir a+=(x) m[nova]="v"
Existeix la clau? [[ -v a[3] ]] [[ -v m[clau] ]]
Esborrar un element unset 'a[3]' (deixa forat) unset 'm[clau]'
Llesca / ordre "${a[@]:1:2}", ja ordenat no aplica; sort sobre "${!m[@]}"

  1. Aplicació a informe-diari.sh

Substituïm el bucle que rellegia el CSV per ciutat per una sola passada que acumula tres mapes:

readonly CIUTATS=(Valencia Sevilla Bilbao Madrid)
readonly REPARTIDORS=(alopez mgarcia jruiz)
# acumular_dia — Omple els mapes globals a partir del CSV d'una data.
# Ús: acumular_dia <data>   Retorna: 0 | 6 si no hi ha cap línia
acumular_dia() {
    local data="${1:?}" f ciutat estat import llegides=0
    declare -gA ENVIAMENTS INCIDENCIES IMPORT   # -g: globals tot i ser en una funció
    while IFS=, read -r _ f ciutat _ estat import; do
        [[ "$f" == "$data" ]] || continue
        (( llegides++ ))
        (( ENVIAMENTS["$ciutat"]++ ))
        (( IMPORT["$ciutat"] += ${import%.*} ))         # part entera, de moment
        [[ "$estat" == "incidencia" ]] && (( INCIDENCIES["$ciutat"]++ ))
    done < <(tail -n +2 "$CAMI_CSV")
    (( llegides > 0 )) || return 6
}

taula_ciutats() {
    local c
    printf '%-10s %8s %13s %10s\n' CIUTAT ENVIAMENTS INCIDENCIES IMPORT
    for c in "${CIUTATS[@]}"; do
        [[ -v ENVIAMENTS[$c] ]] || continue
        printf '%-10s %8d %13d %9d€\n' "$c" "${ENVIAMENTS[$c]}" "${INCIDENCIES[$c]:-0}" "${IMPORT[$c]}"
    done
}

Novetats: declare -gA, perquè dins d'una funció declare crea variables locals per defecte i -g les fa globals perquè taula_ciutats les vegi (és l'alternativa al nameref quan els mapes són compartits per tot l'script); el bucle recorre "${CIUTATS[@]}" i no les claus del mapa, així la taula surt sempre en el mateix ordre i no en l'arbitrari de la taula hash; i una sola passada sense cap grep, amb la qual cosa l'script passa de llançar 8 processos a llançar-ne 1.

Queda pendent el que és evident: ${import%.*} llença els cèntims perquè Bash no suma decimals. Aquesta expansió l'explicarem formalment a la lliçó següent, i els decimals arribaran a 04-06.

Errors Habituals i Consells

  • Descuidar el declare -A en un associatiu. Bash no avisa: converteix les claus en aritmètica i tot acaba masegant l'índex 0. És l'error número u.
  • Escriure ${arr[@]} sense cometes. Es trenca amb qualsevol element que tingui espais. Sempre "${arr[@]}".
  • Fer servir $arr creient que és tot l'array. És només l'element 0.
  • Esperar ordre a les claus d'un associatiu. No n'hi ha. Ordena-les explícitament o recorre una llista fixa.
  • arr=( $(ordre) ). Fes servir mapfile -t arr < <(ordre).
  • Comptar amb for (( i=0; i<${#a[@]}; i++ )) sobre un array amb forats. Recorre "${!a[@]}".
  • Consell: fes servir MAJÚSCULES per als mapes globals compartits de l'script i minúscules per als arrays locals; distingir-los d'un cop d'ull evita col·lisions.

Exercicis

Exercici 1. Crea un array amb els tres repartidors, afegeix-hi pcastro al final, mostra quants n'hi ha, recorre'l numerat (1) alopez) i mostra els dos últims.

Exercici 2. Amb un array associatiu, compta quants enviaments té cada repartidor a enviaments.csv en una sola passada, i imprimeix el resultat ordenat alfabèticament per repartidor.

Exercici 3. Escriu camp_n() que rebi una línia CSV i un número de camp, i escrigui aquell camp a stdout fent servir un array. Ha de funcionar així: camp_n "E1001,2026-08-03,Valencia,alopez,lliurat,24.50" 3Valencia.

Solucions

Solució 1.

repartidors=(alopez mgarcia jruiz)
repartidors+=(pcastro)
echo "Total: ${#repartidors[@]}"                  # Total: 4
for i in "${!repartidors[@]}"; do printf '%d) %s\n' "$(( i+1 ))" "${repartidors[$i]}"; done
echo "Últims dos: ${repartidors[@]: -2}"          # jruiz pcastro

${!repartidors[@]} dona els índexs; hi sumem 1 només per presentar-los començant per 1.

Solució 2.

declare -A per_repartidor
while IFS=, read -r _ _ _ rep _ _; do (( per_repartidor["$rep"]++ )); done \
    < <(tail -n +2 /srv/veloz/dades/enviaments.csv)

for r in $(printf '%s\n' "${!per_repartidor[@]}" | sort); do
    printf '%-10s %4d\n' "$r" "${per_repartidor[$r]}"
done

Sortida: alopez 137, jruiz 98, mgarcia 121. El printf | sort és necessari perquè l'ordre de les claus és impredictible.

Solució 3.

# camp_n — Extreu un camp d'una línia CSV (indexat des d'1).
# Ús: camp_n <linia> <n>   Sortida: el camp   Retorna: 0 | 2 si n és fora de rang
camp_n() {
    local linia="${1:?}" n="${2:?}"; local -a camps
    IFS=',' read -r -a camps <<< "$linia"
    (( n >= 1 && n <= ${#camps[@]} )) || return 2
    printf '%s\n' "${camps[n-1]}"
}

Fixa't en ${camps[n-1]}: dins dels claudàtors hi ha context aritmètic, així que no calen ni $ ni $(( )). El -1 tradueix de l'índex humà (1) al de Bash (0).

Conclusió

Ja tens memòria estructurada. Els arrays indexats guarden llistes ordenades —ciutats, repartidors, fitxers— i es recorren amb for x in "${arr[@]}" o per índex amb "${!arr[@]}"; els associatius, declarats obligatòriament amb declare -A, guarden mapes de clau a valor i fan possible el patró comptador que resumeix un CSV sencer en una sola passada, amb els resultats en variables en comptes de en text. mapfile -t els omple des d'una ordre, IFS=',' read -r -a els omple des d'una cadena, i els namerefs els porten i els duen de les funcions.

Pel camí han aparegut dues expansions que hem fet servir sense justificar: ${import%.*} per quedar-nos amb la part entera i ${var:-0} per al valor per defecte. Pertanyen a la mateixa família de manipulació de cadenes que vas començar a 03-06 i que la lliçó següent completa: tallar subcadenes, treure prefixos i sufixos, substituir text, canviar majúscules i formatar taules amb printf. Tot això sense llançar ni un sol procés extern, que és la diferència entre un script que triga dos segons i un que triga dos minuts.

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