3.4. Datos complejos

El estándar POSIX no define más tipos de datos que los ya vistos hasta ahora: las variables simples, y el array que define los argumentos del script (o de una función como se verá más adelante)[1]. bash sin embargo, extiende el estándar y permite definir vectores (arrays unidimensionales); y diccionarios (si usamos la terminología de python), esto es, arrays en los que el índice en vez de ser la posición es una clave.

Nota

Si su intención es programar scripts compatibles, salte directamente al tercer apartado.

3.4.1. Arrays

Sólo son admisibles los vectores unidimensionales: las claves son un entero que indica la posición dentro del vector y los valores son datos simples.

Definición

Si se pretende definir un array vacío:

declare -a vector;

y si se pretende definir con ya elementos:

vector=(a 1 2 b)

Nota

A diferencia de los arrays en algunos lenguajes de programación como C, son de tamaño variables, por lo que podemos añadir elementos a su definición inicial.

3.4.1.1. Consulta

Para la consulta de un valor individual del array debe usar la sintaxis:

$ echo ${vector[2]}
2

Nota

Observe que, como es habitual, el primer elemento está asociado a la posición 0.

Nota

Las llaves son necesarias, puesto que de lo contrario se interpreta ${vector}[2], que devuelve:

$ echo $vector[2]
a[2]

ya que $vector devuelve el primer elemento.

El número total de elementos del array puede obtenerse así:

$ echo ${#vector[@]}
4

o bien usando un asterisco en vez de un arroba:

$ echo ${#vector[*]}
4

Para la obtención de índices:

$ echo ${!vector[@]}  # También puede usarse el asterisco.
0 1 2 3

lo cual podría usarse con un for, por ejemplo:

for i in ${!vector[@]}; do
   echo ${vector[$i]}
done

Por último, los valores pueden obtenerse:

for v in ${vector[@]}; do  # También puede usarse el asterisco.
   echo $v
done

Como en el caso del array para los argumentos del script ${vector[@]} puede encerrarse entre comillas dobles cuando los valores contienen caracteres de espaciado y hacen que el código anterior falle. Compare la salida de esto:

a=(1 "2 3" 4)
for v in ${a[@]}; do
   echo $v
done

con la salida de esto otro:

a=(1 "2 3" 4)
for v in "${a[@]}"; do
   echo $v
done

Nota

Por lo general, para la obtención de valores es mejor usar la expresión entre comillas dobles, ya que habitualmente queremos que el comportamiento sea el segundo y no el primero.

También puede obtenerse una porción de los valores:

$ vector=(a "b" "c" d)
$ echo "${vector[@]:1:3}"  # Elementos segundo y tercero
b c
$ echo "${vector[@]::3}"   # Hasta el tercer elemento
a b c
$ echo "${vector[@]:2}"    # A partir del tercer elemento
c d

Nota

La consulta de arrays guarda muchas similitudes con la que se puede hacer a los argumentos de un script, ya que en el fondo estos se comportan como un array.

3.4.1.2. Manipulación

Para manipular un elemento ya existente, basta asignarle un nuevo valor:

$ vector=(a b c d)
$ vector[1]="B"
$ echo "${vector[@]}"
a B c d

También es sencillo ampliar el array:

$ vector+=(e f)
$ echo "${vector[@]}"
a B c d e f

La eliminación de elementos es algo particular, ya que unset los elimima, pero no cambia los índices:

$ vector=("a" "b" "c" "d")
$ unset vector[1]
$ echo ${!vector[@]}
0 2 3

de manera que los índices, simplemente, dejan de ser correlativos. Sin embargo, eliminar elementos iniciales también es relativamente sencillo[2]:

$ vector=(a B c d e f)
$ vector=("${vector[@]:1}")
$ echo "${vector[@]}"
B c d e f
$ echo ${!vector[@]}
0 1 2 3 4

Para eliminar elementos intermedios, actualizando los índices puede seguirse una estrategia semejante. Por ejemplo, para eliminar el segundo elemento:

$ vector=("${vector[@]::2}" "${vector[@]:3}")

Para eliminar el último elemento del array, sí podríamos usar unset, pero es algo engorroso:

$ unset vector[$((${#vector[@]}-1))]

Nota

Como puede verse, cuando se empieza a necesitar manipular los arrays, bash comienza a ser bastante engorroso y poco legible.

3.4.2. Diccionarios

O arrays asociativos, si se prefiere el nombre. Se diferencian de los anteriores en que los índices son cadenas y no enteros que representan la posición del elemento.

Se tratan exactamente del mismo modo que los arrays indexados, excepto la definición que es distinta:

$ declare -A dict
$ dict=([uno]=a [dos]=b)
$ dict[tres]=c

Advertencia

Para definirlos es obligatorio declararlos previamente con declare.

Una vez definido, podemos aplicar lo ya visto para los arrays (siempre que tenga sentido). Por ejemplo:

$ echo ${dict[tres]}
c
$ echo ${#dict[@]}
3
$ echo "${!dict[@]}"
uno dos tres
$ echo ${dict[@]}
a b c

Advertencia

Las claves pueden contener espacios. En ese caso, conviene encerrar entre comillas dobles ${!dict[@]} para obtener las claves como se hace con ${dict[@]} para obtener los valores.

3.4.3. Series de datos en POSIX

Los arrays nos permiten agrupar series de datos bajo un mismo nombre para tratarlos luego con más facilidad; pero, como en el estándar no existen, ¿qué estrategias sigo cuando programo en él?

La más simple, cuando estos datos no contienen caracteres de espacios es agruparlos en una cadena en que los datos estén separados por espacios:

$ vector="a b c d"
$ for v in $vector; do echo "$v"; done
a
b
c
d

El problema es que, muy comúnmente, no podemos descartar que estos datos no puedan contener espaciados.

Nota

Una de los mayores engorros de programar en la shell y una de las más importantes fuentes de errores es crear scripts que traten adecuadamente los espacios y no presupongan que estos no existen (p.e. en el nombre de los ficheros)

Cuando hay espacios, entonces la cosa se vuelve más complicada, pero se puede manipular la variable IFS[3], para que, por ejemplo, sólo contenga el cambio de línea:

$ vector="a b
> c
> d"
$ IFS='
> '
$ for a in $vector; echo $a; done
a b
c
d
$ unset IFS  # Restablecemos su valor predeterminado

Sin llegar a modificar IFS también se pueden conseguir efectos semejantes usando eval, pero suele ser engorroso:

$ vector="'a b' c d"
$ eval for a in $vector\; do echo '$a'\; done
a b
c
d

Otra alternativa que evita modificar el valor de IFS es usar un bucle while junto a la orden interna read:

$ vector="a b
> c
> d"
$ echo "$vector" | while read linea; do echo $linea done
a b
c
d

aunque esta alternativa, tiene el defecto de ejecutar el bucle en una subshell.

Existe aún otra alternativa para las series de datos que contienen caracteres de espaciados y consisten en aprovechar el único array que existe en el estándar: $@. En principio, este array almacena en el código principal los argumentos pasados al script (y en el código de una función, los argumentos pasados a dicha función). Pero resulta que set permite redefinir estos argumentos, ya que los argumentos que pasemos a set pasarán a ser los argumentos posicionales del script[4]:

set -- "a b" "c" "d"
for a in "$@"; do
   echo $a
done

Nota

Obviamente, perderemos los antiguos argumentos, por lo que si nuestro script los usa, antes de hacer esto hay que analizarlos.

Notas al pie