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
- Crear arrays indexats
- Accedir als elements i a tot l'array
"${arr[@]}"davant de"${arr[*]}"- Longitud, índexs i recorregut
- Afegir, llescar i esborrar
- Arrays associatius
- El patró comptador
- Poblar arrays:
mapfileiIFS - Passar arrays a funcions
- Taula d'operacions
- Aplicació a
informe-diari.sh
- 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 saltarPunts 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.
- 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 $.
"${arr[@]}" davant de "${arr[*]}"
"${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, desastreRegla 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.
- 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 existentsCompte 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 → jruizSi 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.
- 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 sencerunset 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.
- 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 GARANTITTambé 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:
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]'.
- 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}"
doneSortida: 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 enawk(06-01).
- Poblar arrays:
mapfile i IFS
mapfile i IFSPer 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.
- 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 NOMEl 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.
- 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[@]}" |
- Aplicació a
informe-diari.sh
informe-diari.shSubstituï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 -Aen 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
$arrcreient 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 servirmapfile -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" 3 → Valencia.
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]}"
doneSortida: 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
- 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
