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.
Ver también
Revise los comentarios que ya se hicieron para esta construcción.
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