← Automatización con Scripts en Bash Bloque 2 · Variables y datos · Tema 8 de 42

Tema 8

Arrays indexados y asociativos

Una variable común guarda un solo texto. Pero un script maneja listas todo el tiempo: los archivos a procesar, los servidores a revisar, las opciones que se le pasarán a un comando. Guardar una lista en una variable de texto, separada por espacios, funciona hasta que un elemento tiene un espacio. Los arrays resuelven ese problema de raíz, y los asociativos agregan una forma de relacionar nombres con valores. Este tema explica los dos.

1. El problema de las listas en texto

En el tema 3 quedó un problema sin resolver. Queríamos guardar en una variable las opciones de un comando, una de las cuales tenía un espacio, y no había forma de escribirla para que funcionara:

terminal · con la función args del tema 3
$ opciones='-l "mis documentos"'
$ args $opciones                          # sin comillas: se parte por todos los espacios
3 argumentos: <-l> <"mis> <documentos">
$ args "$opciones"                        # con comillas: un solo argumento
1 argumentos: <-l "mis documentos">

El fondo del problema es que una variable de texto no puede representar una lista: no tiene forma de marcar dónde termina un elemento y empieza el siguiente. La división en palabras corta por los espacios, y un espacio puede ser parte de un dato. Lo que hace falta es una variable que guarde varios textos por separado, cada uno con su contenido intacto.

Un array es una variable que guarda una lista de valores, cada uno identificado por un índice. En un array indexado, los índices son números enteros que empiezan en 0. En un array asociativo, son textos cualesquiera, llamados claves.

terminal
$ opciones=(-l "mis documentos")
$ args "${opciones[@]}"
2 argumentos: <-l> <mis documentos>

Dos argumentos, exactamente los que se escribieron. El resto del tema explica esta sintaxis y todo lo que se puede hacer con ella.

2. Crear un array

Un array indexado se crea asignando una lista de valores entre paréntesis, separados por espacios. Cada valor sigue las reglas de cualquier palabra de Bash: si tiene espacios, va entre comillas.

terminal
$ servidores=(web01 web02 db01)
$ archivos=("informe final.pdf" notas.txt "foto 1.jpg")
$ vacio=()                                 # un array sin elementos
$ declare -p archivos
declare -a archivos=([0]="informe final.pdf" [1]="notas.txt" [2]="foto 1.jpg")

declare -p, que conocimos en el tema 5, es la mejor forma de ver un array: muestra cada índice con su valor entre comillas. La -a indica que es un array indexado. También se puede declarar así explícitamente, con declare -a nombre, aunque no es necesario: la asignación con paréntesis ya crea el array.

Una lista larga puede escribirse en varias líneas, sin barras invertidas, porque los paréntesis mantienen abierta la asignación. Admite comentarios, lo que la vuelve ideal para listas de configuración:

una lista de configuración
DIRECTORIOS=(
  "$HOME/proyectos"
  "$HOME/documentos"
  /etc/nginx               # configuración del servidor web
)

3. Leer un elemento

Un elemento se lee con su índice entre corchetes, y las llaves son obligatorias. Los índices negativos cuentan desde el final:

terminal
$ lista=(uno dos "tres cuatro")
$ echo "${lista[0]}"
uno
$ echo "${lista[2]}"
tres cuatro
$ echo "${lista[-1]}"                      # el último
tres cuatro
$ echo "$lista[1]"                         # sin llaves: $lista, seguido del texto [1]
uno[1]
$ echo "$lista"                            # el nombre solo es el elemento 0
uno

Los dos últimos casos son errores frecuentes. Sin llaves, Bash lee la variable lista y deja [1] como texto. Y el nombre del array a secas da solo su primer elemento, sin aviso; ShellCheck lo señala con SC2128. El índice, por su parte, es una expresión aritmética: ${lista[i]}, ${lista[i+1]} y ${lista[$i]} funcionan igual.

4. Todos los elementos: @ y *

Para obtener todos los elementos se usa @ o * como índice. Las dos formas dan lo mismo sin comillas, pero entre comillas se comportan de manera muy distinta, y esa diferencia es la clave de los arrays:

terminal
$ lista=(uno dos "tres cuatro")
$ args "${lista[@]}"                       # cada elemento, un argumento
3 argumentos: <uno> <dos> <tres cuatro>
$ args "${lista[*]}"                       # todos unidos en un solo argumento
1 argumentos: <uno dos tres cuatro>
$ args ${lista[@]}                         # sin comillas: se dividen por los espacios
4 argumentos: <uno> <dos> <tres> <cuatro>
FormaProduceCuándo usarla
"${lista[@]}"Un argumento por elemento, cada uno intacto.Casi siempre. Para recorrer, pasar a un comando o copiar.
"${lista[*]}"Un solo texto, con los elementos unidos por el primer carácter de IFS (un espacio, por defecto).Para mostrar la lista en un mensaje, o unirla con un separador (sección 13).
${lista[@]} o ${lista[*]}Los elementos, divididos otra vez por los espacios y expandidos como comodines.Nunca: pierde justamente lo que el array protege.

La forma "${lista[@]}" es la única excepción a una regla que vimos en el tema 3: una expansión entre comillas produce siempre una sola palabra. Aquí produce tantas como elementos tenga el array, y cada una conserva sus espacios. Es el mismo comportamiento que tiene "$@" con los argumentos de un script, como veremos en el tema 20.

5. Cantidad e índices

Con # delante se obtiene la cantidad de elementos, y con ! la lista de índices:

terminal
$ lista=(uno dos "tres cuatro")
$ echo "${#lista[@]}"                      # cantidad de elementos
3
$ echo "${!lista[@]}"                      # índices
0 1 2
$ echo "${#lista[2]}"                      # longitud de un elemento
11

Con la cantidad se comprueba si un array está vacío, algo muy común antes de procesar una lista de archivos: (( ${#lista[@]} == 0 )). Y conviene no confundir ${#lista[@]}, la cantidad de elementos, con ${#lista}, que es la longitud del primer elemento.

6. Agregar, cambiar y borrar

El operador += con paréntesis agrega elementos al final, sin necesidad de conocer el último índice. Un elemento concreto se cambia asignándole por índice:

terminal
$ lista=(uno dos "tres cuatro")
$ lista+=(cinco)                           # agregar al final
$ lista+=("seis" "siete")                  # varios a la vez
$ lista[0]=UNO                             # reemplazar el elemento 0
$ declare -p lista
declare -a lista=([0]="UNO" [1]="dos" [2]="tres cuatro" [3]="cinco" [4]="seis" [5]="siete")
lista+=x no es lista+=(x)

Sin paréntesis, += agrega texto al primer elemento, como con una variable común: lista+=cinco convierte UNO en UNOcinco. Para agregar un elemento, los paréntesis son imprescindibles.

Los arrays de Bash pueden tener huecos: los índices no tienen por qué ser consecutivos. Se crean al asignar un índice lejano o al borrar un elemento con unset:

terminal
$ lista=(uno dos tres)
$ lista[7]=siete                           # los índices 3 a 6 no existen
$ unset 'lista[1]'                         # borrar el elemento 1
$ declare -p lista
declare -a lista=([0]="uno" [2]="tres" [7]="siete")
$ echo "${#lista[@]} elementos, índices ${!lista[@]}"
3 elementos, índices 0 2 7
$ lista=("${lista[@]}")                    # reconstruir sin huecos
$ declare -p lista
declare -a lista=([0]="uno" [1]="tres" [2]="siete")

Con huecos, la cantidad de elementos ya no coincide con el último índice más uno, y un bucle que recorra los índices de 0 a ${#lista[@]} - 1 se saltará elementos y leerá otros vacíos. Por eso conviene recorrer siempre con "${lista[@]}" o con "${!lista[@]}", como en la sección siguiente, y no con índices calculados.

En unset 'lista[1]', las comillas no son un capricho: sin ellas, [1] es un patrón de comodines, y si en el directorio existe un archivo llamado lista1, la palabra se reemplaza por ese nombre. Para borrar el array entero, unset lista; para vaciarlo y seguir usándolo, lista=().

7. Recorrer un array

La forma habitual de procesar cada elemento es un bucle for sobre "${lista[@]}". El bucle se estudia en el tema 14, pero su forma básica se entiende sola:

recorrer los valores
servidores=(web01 web02 db01)
for servidor in "${servidores[@]}"; do
  ping -c1 -W2 "$servidor" > /dev/null && echo "$servidor responde"
done

Cuando además del valor hace falta su posición, por ejemplo para numerar una lista, se recorren los índices:

terminal
$ archivos=("informe final.pdf" notas.txt "foto 1.jpg")
$ for i in "${!archivos[@]}"; do printf '%d) %s\n' $(( i + 1 )) "${archivos[i]}"; done
1) informe final.pdf
2) notas.txt
3) foto 1.jpg

8. Rebanadas y transformaciones

Las expansiones de parámetros del tema 6 funcionan también sobre arrays. Aplicadas a [@], transforman cada elemento por separado, sin necesidad de un bucle:

terminal
$ rutas=(/home/ana/a.txt /var/log/b.txt /tmp/c.txt)
$ echo "${rutas[@]##*/}"                   # el nombre de cada archivo
a.txt b.txt c.txt
$ echo "${rutas[@]/%.txt/.bak}"            # cambiar la extensión de todos
/home/ana/a.bak /var/log/b.bak /tmp/c.bak
$ nombres=(ana luis eva)
$ echo "${nombres[@]^}"                    # mayúscula inicial en cada uno
Ana Luis Eva

Y la forma de subcadena se convierte en una rebanada: ${lista[@]:inicio:cantidad} da los elementos desde una posición, en lugar de los caracteres:

terminal
$ semana=(lun mar mié jue vie sáb dom)
$ echo "${semana[@]:0:5}"                  # los cinco primeros
lun mar mié jue vie
$ echo "${semana[@]:5}"                    # desde el sexto hasta el final
sáb dom

El resultado de una transformación puede guardarse como un nuevo array, con paréntesis y comillas: nombres=("${rutas[@]##*/}").

9. Llenar un array desde afuera

Los arrays rara vez se escriben a mano: casi siempre se llenan con datos que el script obtiene. Hay una forma correcta para cada fuente, y una forma tentadora que conviene evitar.

Desde un patrón de archivos

Un comodín dentro de los paréntesis llena el array con los nombres de archivo que coinciden, cada uno como un elemento, con sus espacios intactos:

terminal
$ registros=(/var/log/app/*.log)
$ echo "Hay ${#registros[@]} registros"
Hay 4 registros

Recuerda la regla del tema 3: si ningún archivo coincide, el patrón queda tal cual, y el array tendrá un elemento con el texto /var/log/app/*.log. La opción shopt -s nullglob, que veremos en el tema 14, hace que en ese caso el array quede vacío.

Desde las líneas de un archivo o de un comando

El comando mapfile, también llamado readarray, lee líneas y guarda cada una como un elemento. La opción -t quita el salto de línea del final de cada una, y casi siempre se usa:

terminal
$ mapfile -t servidores < servidores.txt   # una línea del archivo, un elemento
$ mapfile -t grandes < <(find ~ -size +100M)
$ printf '%s\n' "${grandes[@]}"
/home/ana/videos/viaje 2025.mp4
/home/ana/descargas/ubuntu.iso

La forma < <(comando) es una sustitución de procesos: hace que la salida de un comando se lea como si fuera un archivo. Hace falta porque comando | mapfile -t lista no funciona como se espera: en una tubería, mapfile se ejecuta en un proceso aparte, y el array se llena allí y se pierde al terminar. Volveremos sobre este problema en el tema 16.

Desde los campos de una línea

Para separar un texto en campos por un carácter, se usa read -a con IFS indicando el separador. Es el tema del tema 9, pero la receta es breve:

terminal
$ linea="ana:x:1000:1000:Ana Pérez:/home/ana:/bin/bash"
$ IFS=: read -ra campos <<< "$linea"
$ echo "Usuario ${campos[0]}, nombre ${campos[4]}, shell ${campos[6]}"
Usuario ana, nombre Ana Pérez, shell /bin/bash

La forma que conviene evitar

Es tentador llenar un array con lista=($(comando)). Funciona mientras ningún dato tenga espacios ni comodines, porque la salida del comando pasa por la división en palabras y la expansión de nombres de archivo:

terminal · en un directorio con mi informe.txt y notas.txt
$ mal=($(ls))
$ declare -p mal
declare -a mal=([0]="mi" [1]="informe.txt" [2]="notas.txt")
$ bien=(*)
$ declare -p bien
declare -a bien=([0]="mi informe.txt" [1]="notas.txt")

ShellCheck avisa de este patrón con SC2207 y sugiere mapfile. La regla: comodines para archivos, mapfile -t para líneas, read -a para campos.

10. Construir comandos con arrays

Llegamos al uso que justifica los arrays en la mayoría de los scripts: armar los argumentos de un comando de a poco, según las condiciones, y ejecutarlo al final con todos ellos intactos. Es la solución al problema de la sección 1.

sincronizar · opciones que dependen de la situación
opciones=(--archive --human-readable)

[[ "${SIMULAR:-0}" == 1 ]] && opciones+=(--dry-run)
[[ "${DETALLE:-0}" == 1 ]] && opciones+=(--verbose)

for patron in '*.tmp' '.cache/' 'node_modules/'; do
  opciones+=(--exclude "$patron")
done

rsync "${opciones[@]}" "$origen/" "$destino/"

Cada opción y cada valor es un elemento: --exclude y *.tmp van por separado, y el patrón conserva su asterisco sin expandirse, porque está entre comillas al agregarlo y "${opciones[@]}" no vuelve a expandir comodines. Con SIMULAR=1 DETALLE=1, rsync recibe exactamente:

los argumentos de rsync
<--archive> <--human-readable> <--dry-run> <--verbose> <--exclude> <*.tmp> <--exclude> <.cache/> <--exclude> <node_modules/> </home/ana/proyecto/> </mnt/respaldos/>

Un beneficio adicional: el array puede mostrarse antes de ejecutarlo, para dejar constancia en un registro o para revisarlo. printf '%q ' "${opciones[@]}" muestra cada elemento entrecomillado de forma que podría copiarse y pegarse en la terminal.

11. Arrays asociativos

En un array asociativo, los índices son textos, llamados claves. Sirven para relacionar nombres con valores: una configuración, la dirección de cada servidor, el total de cada categoría. A diferencia de los indexados, hay que declararlos con declare -A antes de usarlos:

terminal
$ declare -A conf=([destino]=/respaldos [dias]=30)
$ conf[servidor]=web01                     # agregar o cambiar una clave
$ echo "Copiar a ${conf[servidor]}:${conf[destino]}, ${conf[dias]} días"
Copiar a web01:/respaldos, 30 días
$ declare -p conf
declare -A conf=([destino]="/respaldos" [dias]="30" [servidor]="web01" )
Sin declare -A, no es asociativo

Si se omite la declaración, conf[servidor]=web01 no da error: crea un array indexado, y como el índice se evalúa aritméticamente, servidor se toma como una variable inexistente que vale 0. Todas las claves terminan en el índice 0, pisándose entre sí. Si un array asociativo se comporta de forma extraña, lo primero es mirarlo con declare -p: debe decir -A.

Las operaciones son las mismas que con los indexados, cambiando números por claves:

OperaciónForma
Leer un valor"${conf[clave]}"
Todos los valores"${conf[@]}"
Todas las claves"${!conf[@]}"
Cantidad de pares"${#conf[@]}"
¿Existe la clave?[[ -v conf[clave] ]]
Valor, o uno por defecto"${conf[clave]:-valor}"
Borrar una claveunset 'conf[clave]'

Las claves pueden contener espacios y cualquier carácter, y pueden venir de una variable: conf[$nombre]=valor. Lo que no tienen es un orden: "${!conf[@]}" las devuelve en un orden interno, que no es el de inserción ni el alfabético. Si el orden importa, hay que ordenarlas, como en la sección 13.

Un uso clásico es reemplazar una cadena de condiciones por una tabla. En lugar de preguntar por cada caso, se busca la respuesta:

una tabla en lugar de varias condiciones
declare -A puerto=([web]=443 [db]=5432 [cache]=6379)

servicio="${1:?Uso: verificar SERVICIO}"
if [[ ! -v puerto[$servicio] ]]; then
  echo "Servicio desconocido: $servicio (opciones: ${!puerto[*]})" >&2
  exit 2
fi
nc -z localhost "${puerto[$servicio]}" && echo "$servicio responde"

12. Contar con un array asociativo

Otra aplicación muy frecuente es contar o acumular por categoría: cuántos errores de cada tipo, cuántos accesos por usuario, cuántos bytes por extensión. La clave es la categoría, y el valor, el acumulado. El valor por defecto :-0 resuelve el caso de la primera aparición, cuando la clave todavía no existe:

contar las palabras de una lista
declare -A veces=()
for palabra in rojo verde rojo azul rojo verde; do
  veces[$palabra]=$(( ${veces[$palabra]:-0} + 1 ))
done

for color in "${!veces[@]}"; do
  echo "$color: ${veces[$color]}"
done
salida (el orden puede variar)
azul: 1
verde: 2
rojo: 3

Para contar líneas de un archivo, sort | uniq -c o awk siguen siendo más rápidos. El array asociativo conviene cuando los datos ya están en el script, o cuando cada elemento requiere un tratamiento que no se expresa con una tubería, como en el script de la sección 15.

13. Unir, ordenar y mostrar

Bash no tiene funciones para unir u ordenar arrays, pero las tres operaciones se resuelven en una línea.

Mostrar un elemento por línea es trabajo de printf, que repite su formato para cada argumento:

terminal
$ printf '%s\n' "${servidores[@]}"
web01
web02
db01

Unir con un separador aprovecha que "${lista[*]}" usa el primer carácter de IFS. Para no alterar IFS en el resto del script, se cambia dentro de una subshell, entre paréntesis, o dentro de una función con local:

terminal
$ (IFS=,; echo "${servidores[*]}")
web01,web02,db01

Ordenar se delega en sort: se muestran los elementos uno por línea, se ordenan, y el resultado se vuelve a leer con mapfile:

ordenar un array, y las claves de uno asociativo
mapfile -t ordenados < <(printf '%s\n' "${servidores[@]}" | sort)

for clave in $(printf '%s\n' "${!conf[@]}" | sort); do   # claves sin espacios
  echo "$clave=${conf[$clave]}"
done

El segundo bucle usa una sustitución sin comillas, que se divide por los espacios: es aceptable solo si las claves no los tienen. Si pueden tenerlos, se ordena primero con mapfile en otro array y se recorre ese.

14. Lo que los arrays no hacen

Los arrays de Bash son útiles, pero limitados. Conocer sus límites ayuda a reconocer cuándo una tarea ha crecido más allá de lo que Bash maneja bien, como discutimos en el tema 1:

LímiteConsecuenciaQué hacer
Una sola dimensiónNo hay arrays de arrays ni tablas con filas y columnas.Claves compuestas como m[fila,col], o pasar a otro lenguaje.
No se exportanUn programa hijo no recibe los arrays del script, aunque se use export.Pasar los elementos como argumentos: otro-script "${lista[@]}".
No se devuelven fácilmenteUna función no puede «devolver» un array.Variables globales o referencias con declare -n (tema 19).
Asociativos desde Bash 4macOS trae Bash 3.2, sin declare -A ni mapfile.Instalar un Bash actual, o evitarlos si el script debe correr allí.
Sin orden en los asociativosLas claves salen en un orden interno impredecible.Ordenar al recorrer.

15. Un script completo

Este script responde una pregunta habitual cuando se acaba el espacio: ¿qué tipo de archivo ocupa más en este directorio? Recorre todos los archivos, incluidos los de subdirectorios, y acumula en dos arrays asociativos la cantidad y el tamaño de cada extensión. Al final muestra una tabla ordenada por tamaño.

por-extension
#!/usr/bin/env bash
#
# por-extension — cuánto ocupan los archivos de un directorio, por extensión.
#
# Uso: por-extension [DIRECTORIO]
#
set -euo pipefail

dir="${1:-.}"
declare -A cantidad=() bytes=()

# find escribe «tamaño nombre», terminado en un carácter nulo, para cada archivo.
while IFS= read -r -d '' linea; do
  tam="${linea%% *}"                        # hasta el primer espacio
  nombre="${linea#* }"                      # el resto

  if [[ "$nombre" == ?*.* ]]; then        # tiene un punto que no es el inicial
    ext="${nombre##*.}"
    ext="${ext,,}"                          # JPG y jpg cuentan juntos
  else
    ext="(sin extensión)"
  fi

  cantidad[$ext]=$(( ${cantidad[$ext]:-0} + 1 ))
  bytes[$ext]=$(( ${bytes[$ext]:-0} + tam ))
done < <(find "$dir" -type f -printf '%s %f\0')

printf '%10s  %8s  %s\n' KiB Archivos Extensión
for ext in "${!cantidad[@]}"; do
  printf '%10d  %8d  %s\n' $(( (${bytes[$ext]} + 512) / 1024 )) "${cantidad[$ext]}" "$ext"
done | sort -rn
terminal
$ por-extension ~/documentos
       KiB  Archivos  Extensión
    845210        37  mp4
    127448       412  jpg
     30122        58  pdf
      2215       146  txt
       318         9  (sin extensión)
        41         3  sh

16. Errores frecuentes

EscribisteQué pasaCorrección
echo "$lista"Muestra solo el elemento 0."${lista[@]}"
echo "$lista[2]"Muestra el elemento 0 seguido de [2]."${lista[2]}"
cmd ${lista[@]}Los elementos con espacios se parten.cmd "${lista[@]}"
cmd "${lista[*]}"El comando recibe un solo argumento.cmd "${lista[@]}"
lista+=xAgrega texto al elemento 0.lista+=(x)
lista=($(comando))Divide por espacios y expande comodines.mapfile -t lista < <(comando)
comando | mapfile -t listaEl array queda vacío: se llenó en otro proceso.mapfile -t lista < <(comando)
m[clave]=v sin declare -ACrea un array indexado; todas las claves van al índice 0.declare -A m antes.
${#lista} para contarDa la longitud del primer elemento.${#lista[@]}
unset lista[1]Puede expandirse como comodín.unset 'lista[1]'

17. Resumen

18. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué no se puede guardar de forma fiable una lista de nombres de archivo en una variable de texto?

Porque una variable de texto no tiene forma de marcar dónde termina cada elemento. Al usarla, la división en palabras corta por los espacios, que pueden ser parte de los nombres.

2. Con a=(x "y z" w), ¿qué dan ${a[1]}, ${a[-1]} y $a?

y z, w y x. El nombre solo, sin índice, da el elemento 0.

3. ¿Cuántos argumentos recibe un comando con "${a[@]}", con "${a[*]}" y con ${a[@]}, si a=(x "y z" w)?

Tres, uno y cuatro. "${a[@]}" da un argumento por elemento; "${a[*]}" los une en uno solo; sin comillas, y z se divide en dos.

4. ¿Qué diferencia hay entre ${#a[@]} y ${#a}?

${#a[@]} es la cantidad de elementos del array. ${#a} es la longitud del elemento 0.

5. ¿Qué hace a+=nuevo, y qué hace a+=(nuevo)?

a+=nuevo agrega el texto al final del elemento 0. a+=(nuevo) agrega un elemento nuevo al final del array.

6. Después de a=(p q r s) y unset 'a[1]', ¿qué dan ${#a[@]} y ${!a[@]}?

3 y 0 2 3. El elemento 1 desapareció y dejó un hueco: los demás conservan sus índices.

7. ¿Por qué conviene recorrer un array con "${a[@]}" o "${!a[@]}" en lugar de contar de 0 a ${#a[@]} - 1?

Porque un array puede tener huecos. Si los tiene, los índices no son consecutivos, y contar de 0 en adelante se salta elementos y lee posiciones vacías.

8. ¿Qué da "${rutas[@]##*/}" si rutas contiene rutas de archivo?

El nombre de cada archivo, sin el directorio. La expansión se aplica a cada elemento por separado.

9. ¿Por qué lista=($(ls)) es una mala forma de obtener los archivos de un directorio?

Porque la salida de ls pasa por la división en palabras y por la expansión de comodines: un nombre con espacios se convierte en varios elementos. La forma correcta es lista=(*).

10. ¿Por qué find . -name '*.log' | mapfile -t lista deja el array vacío?

Porque cada parte de una tubería se ejecuta en un proceso aparte. mapfile llena el array en ese proceso, que termina al acabar la tubería, y el script nunca lo ve. Se usa mapfile -t lista < <(find …).

11. ¿Cómo se agrega la opción --dry-run a un comando solo cuando SIMULAR vale 1?

Guardando las opciones en un array y agregándola de forma condicional: [[ $SIMULAR == 1 ]] && opciones+=(--dry-run). Luego se ejecuta el comando con "${opciones[@]}".

12. ¿Qué ocurre si se usa m[web]=443 sin haber declarado m con declare -A?

Se crea un array indexado. El índice web se evalúa como expresión aritmética, como una variable inexistente que vale 0, y el valor queda en el índice 0. Otras claves también irían al 0, pisándose.

13. ¿Cómo se comprueba si un array asociativo tiene una clave?

Con [[ -v m[clave] ]], que es verdadero si la clave existe, aunque su valor esté vacío.

14. ¿Por qué se escribe ${veces[$p]:-0} al contar con un array asociativo?

Porque la primera vez que aparece una categoría su clave no existe. El valor por defecto 0 permite sumar desde ahí, y además evita el error de set -u.

15. ¿Cómo se unen los elementos de un array con comas?

Con (IFS=,; echo "${lista[*]}"). La forma [*] une los elementos con el primer carácter de IFS, y los paréntesis limitan el cambio de IFS a esa subshell.

Para practicar

  1. Crea un array con cinco nombres de ciudades, alguna con espacios. Muéstralo con declare -p, con printf '%s\n' y con args, usando [@] y [*], con y sin comillas.
  2. Agrega dos ciudades, borra la segunda y muestra la cantidad y los índices. Reconstruye el array sin huecos.
  3. Llena un array con los archivos .txt de un directorio y muestra cuántos hay. Pruébalo en un directorio sin archivos .txt: ¿qué cantidad informa?
  4. Usa mapfile para leer /etc/shells en un array y muestra solo las líneas que no empiezan con #.
  5. Separa en campos una línea de /etc/passwd con read -ra y muestra el usuario, su directorio personal y su shell.
  6. Escribe un script que construya un comando ls en un array, agregando -l si la variable LARGO vale 1 y -a si OCULTOS vale 1, y lo ejecute sobre un directorio con espacios en el nombre.
  7. Cuenta con un array asociativo cuántas veces aparece cada shell en /etc/passwd (el séptimo campo de cada línea).
  8. Ejecuta por-extension en tu directorio personal. Modifícalo para que muestre también el porcentaje del total que representa cada extensión, con lo aprendido en el tema 7.