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)\ [#]_. :command:`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. .. note:: Si su intención es programar *scripts* compatibles, salte directamente al :ref:`tercer apartado `. 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. .. rubric:: 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) .. note:: 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. Consulta -------- Para la consulta de un **valor individual** del array debe usar la sintaxis:: $ echo ${vector[2]} 2 .. note:: Observe que, como es habitual, el primer elemento está asociado a la posición **0**. .. note:: Las llaves son necesarias, puesto que de lo contrario se interpreta :code:`${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* :code:`${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 .. note:: 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 .. note:: La consulta de *arrays* guarda muchas similitudes con la que se puede hacer a los :ref:`argumentos de un script `, ya que en el fondo estos se comportan como un *array*. 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 :command:`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\ [#]_:: $ 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 :command:`unset`, pero es algo engorroso:: $ unset vector[$((${#vector[@]}-1))] .. note:: Como puede verse, cuando se empieza a necesitar manipular los *arrays*, :command:`bash` comienza a ser bastante engorroso y poco legible. 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 .. warning:: Para definirlos es obligatorio declararlos previamente con :command:`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 .. warning:: Las claves pueden contener espacios. En ese caso, conviene encerrar entre comillas dobles :code:`${!dict[@]}` para obtener las claves como se hace con :code:`${dict[@]}` para obtener los valores. .. _sh-series: 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. .. note:: 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``\ [#]_, 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 :ref:`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 :ref:`while ` junto a la orden interna :ref:`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*. .. seealso:: Revise los :ref:`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 :ref:`set ` permite redefinir estos argumentos, ya que los argumentos que pasemos a :command:`set` pasarán a ser los argumentos posicionales del *script*\ [#]_: .. code-block:: bash set -- "a b" "c" "d" for a in "$@"; do echo $a done .. note:: Obviamente, perderemos los antiguos argumentos, por lo que si nuestro *script* los usa, antes de hacer esto hay que analizarlos. .. rubric:: Notas al pie .. [#] Ya se verá que el conjunto de argumentos se comporta como un array de :command:`bash`. .. [#] Incluso podríamos crear una función para ello:: ashift() { local N=${2:-1} eval $1='("${'$1'[@]:'$N'}")' } que admite como primer argumento el nombre del array y como segundo el número de elementos que desean eliminarse (si no se indica ninguno es 1):: $ v=(0 1 2 3 4) $ ashift v 2 $ echo "$v[@]" 2 3 4 .. [#] :abbr:`IFS (Input File Separators)` es el acrónimo de *Separadores de campos de entrada*, por lo que es la variable que contiene todos aquellos caracteres que la *shell* considera que sirven para separar unos campos de otros. De forma predeterminada, es una cadena que contiene el espacio, la tabulación y el cambio de línea. .. [#] El argumento `--` es opcional y, simplemente, informa a :command:`set` de que todo lo que sigue son argumentos posicionales y no opciones. En el ejemplo, no hay dudas, pero habría llegado a haberlas si alguno de los argumentos hubiera empezado por un guión.