jueves, 5 de octubre de 2017

Arrays

En el último capítulo, vimos como el shell puede manipular cadenas y números. El tipo de datos que hemos visto hasta ahora se conocen en los círculos informáticos como variables escalares; o sea, variables que contienen un valor individual.

En este capítulo, veremos otro tipo de estructura de datos llamados arrays (vectores), que contienen valores múltiples. Los arrays son una característica de prácticamente todos los lenguajes de programación. El shell los soporta también, sólo que de una forma algo limitada. Incluso así, pueden ser muy útiles para resolver problemas de programación.


¿Qué son los arrays?

Los arrays son variables que almacenan más de un valor a la vez. Los arrays se organizan como una tabla. Consideremos una hoja de cálculo como ejemplo. Una hoja de cálculo funciona como un array de dos dimensiones. Tiene tanto filas como columnas, y una celda individual de la hoja de cálculo puede localizarse según su dirección de fila y columna. Un array se comporta de la misma forma. Un array tiene celdas, que se llaman elementos, y cada elemento contiene datos. Un elemento individual de un array es accesible usando una dirección llamada un indice o subscript.

La mayoría de lenguajes de programación soportan arrays multidimensionales. Una hoja de cálculo es un ejemplo de un array multidimensional de dos dimensiones, anchura y altura. Muchos lenguajes de programacion soportan arrays con un número arbitrario de dimensiones, aunque los arrays de dos y tres dimensiones son probablemente los más usados.

Los arrays en bash se limitan a una única dimensión. Podemos pensar que son hojas de cálculo con una única columna. Incluso con esta limitación, hay muchas aplicaciones para ellos. El soporte para arrays apareció por primera vez en bash versión 2. El programa de shell original de unix, sh, no soporta arrays de ningún tipo.


Creando un array

Las variables array se nombran igual que otras variables de bash, y se crean automáticamente cuando se accede a ellas. Aquí tenemos un ejemplo:

 [me@linuxbox ~]$ a[1]=foo
 [me@linuxbox ~]$ echo ${a[1]}
 foo

Aquí vemos un ejemplo tanto de asignación como de acceso a un elemento de un array. Con el primer comando, al elemento 1 del array a se le asigna el valor "foo". El segundo comando muestra el valor almacenado en el elemento 1. El uso de llaves en el segundo comando se requiere para evitar que el shell intente una expansión de ruta en el nombre del elemento del array.

Un array puede también crearse con el comando declare:

 [me@linuxbox ~]$ declare -a a

Usando la opción -a, este ejemplo de declare crea el array a.


Asignando valores a un array

Los valores pueden asignarse de dos formas. Valores individuales pueden asignarse usando la siguiente sintaxis:

 nombre[índice]=valor

donde nombre es el nombre del array e índice es un entero (o una expresión aritmética) mayor o igual que cero. Fíjate que el primer elemento de un array es el índice cero, no uno. valor es una cadena o un entero asignado al elemento del array.

Valores múltiples pueden asignarse usando la siguiente sintaxis:

 nombre=(valor1 valor2 ...)

donde nombre es el nombre del array y valor... son los valores asignados secuencialmente a elementos del array, comenzando por el elemento cero. Por ejemplo, si queremos asignar los días de las semana en abreviaturas al array days, podríamos hacer esto:

 [me@linuxbox ~]$ days=(Sun Mon Tue Wed Thu Fri Sat)

También es posible asignar valores a un elemento en concreto especificando un índice para cada valor:

[me@linuxbox ~]$ days=([0]=Sun [1]=Mon [2]=Tue [3]=Wed [4]=Thu 

[5]=Fri [6]=Sat)

Accediendo a los elementos de un array

Entonces ¿para qué sirven los arrays? De la misma forma que muchas tareas de manejo de datos pueden realizarse con un programa de hojas de cálculo, muchas tareas de programación pueden realizarse con arrays.

Consideremos un ejemplo simple de recogida y presentación de datos. Construiremos un script que examine la hora de modificación de los archivos de un directorio determinado. Desde estos datos, nuestro script mostrará una tabla con la hora en que los datos fueron modificados por última vez. Dicho script podría usarse para determinar cuando está más activo un sistema. Este script, llamado hours, produce este resultado:

 [me@linuxbox ~]$ hours
 Hour  Files  Hour  Files
 ----  -----  ----  -----
 00    0      12    11
 01    1      13    7
 02    0      14    1
 03    0      15    7
 04    1      16    6
 05    1      17    5
 06    6      18    4
 07    3      19    4
 08    1      20    1
 09    14     21    0
 10    2      22    0
 11    5      23    0


 Total files = 80

Ejecutamos el programa hours, especificando el directorio actual como destino. Produce una tabla mostrando, para cada hora del día (0-23), cuantos archivos han sido modificados por última vez. El código para producir esto es el que sigue:

 #!/bin/bash

 # hours : script to count files by modification time

 usage () {
     echo "usage: $(basename $0) directory" >&2
 }

 # Check that argument is a directory
 if [[ ! -d $1 ]]; then
     usage
     exit 1
 fi

 # Initialize array
     for i in {0..23}; do hours[i]=0; done
 # Collect data
 for i in $(stat -c %y "$1"/* | cut -c 12-13); do
     j=${i/#0}
     ((++hours[j]))
     ((++count))
 done

 # Display data
 echo -e "Hour\tFiles\tHour\tFiles"
 echo -e "----\t-----\t----\t-----"
 for i in {0..11}; do
     j=$((i + 12))
     printf "%02d\t%d\t%02d\t%d\n" $i ${hours[i]} $j ${hours[j]}
 done
 printf "\nTotal files = %d\n" $count

El script consiste en una función (usage) y un cuerpo principal con cuatro secciones. En la primera sección, comprobamos que hay un argumento en la línea de comandos y que es un directorio. Si no, mostramos el mensaje de uso y salimos.

La segunda sección inicializa el array hours. Lo hace asignando a cada elemento un valor cero. No hay ningún requerimiento especial para preparar arrays antes de usarlos, pero nuestro script necesita asegurarse de que ningún elemento se queda vacío. Fíjate la interesante forma en que el bucle se construye. Empleado expansión con llaves ({0..23}), podemos generar fácilmente una secuencia de palabras para el comando for.

La siguiente sección recoge los datos ejecutando el programa stat en cada archivo del directorio. Usamos cut para extraer los dígitos de la hora del resultado. Dentro del bucle, necesitamos eliminar los ceros a la izquierda de nuestro campo hora, ya que el shell trata (y finalmente falla) de interpretar los valores del "00" al "99" como números octales (ver Tabla 34-1). A continuación, incrementamos el valor del elemento del array correspondiente a la hora del día. Finalmente, incrementamos un contador (count) para seguir la pista del número total de archivos en el directorio.

La última sección del script muestra el contenido del array. Primero mostramos un par de líneas de encabezado y luego entramos en un bucle que produce una salida en dos columnas. Finalmente, mostramos la lista completa de archivos.

Operaciones con arrays

Hay muchas operaciones comunes con arrays. Cosas como borrar arrays, determinar su tamaño, su orden, etc. tienen muchas aplicaciones en scripting.

Mostrando todo el contenido de un array

Los subscripts * y @ pueden usarse para acceder a todos los elementos de un array. Al igual que con los parámetros posicionales, la notación @ es la más útil de las dos. Aquí tenemos una prueba:

 [me@linuxbox ~]$ animals=("a dog" "a cat" "a fish")
 [me@linuxbox ~]$ for i in ${animals[*]}; do echo $i; done
 a
 dog
 a
 cat
 a
 fish
 [me@linuxbox ~]$ for i in ${animals[@]}; do echo $i; done
 a
 dog
 a
 cat
 a
 fish
 [me@linuxbox ~]$ for i in "${animals[*]}"; do echo $i; done
 a dog a cat a fish
 [me@linuxbox ~]$ for i in "${animals[@]}"; do echo $i; done
 a dog
 a cat
 a fish

Creamos el array animals y le asignamos tres cadenas de dos palabras. Luego ejecutamos cuatro bucles para ver el efecto de la separación de palabras en el contenido del array. El comportamiento de las notaciones ${animals[*]} y${animals[@]}es idéntico hasta que se entrecomillen. La notación * da como resultado una palabra individual con el contenido del array, mientras que la notación @ da como resultado tres palabras, lo que coincide con el contenido "real" del array.


Determinando el número de elementos de una array

Usando expansión de parámetros, podemos determinar el número de elementos en un array de forma muy parecida a determinar la longitud de una cadena. Aquí tenemos un ejemplo:

 [me@linuxbox ~]$ a[100]=foo
 [me@linuxbox ~]$ echo ${#a[@]} # number of array elements
 1
 [me@linuxbox ~]$ echo ${#a[100]} # length of element 100
 3

Creamos un array a y le asignamos la cadena "foo" al elemento 100. A continuación, usamos expansión de parámetros para examinar la longitud del array, usando la notación @. Finalmente, vemos la longitud del elemento 100 que contiene la cadena "foo". Es interesante fijarse que mientras que asignamos nuestra cadena al elemento 100, bash sólo reporta un elemento en el array. Esto difiere del comportamiento de otros lenguajes en los que los elementos sin uso del array (elementos 0-99) serían inicializados con valores vacíos y se cuentan.


Encontrando los subscripts usados por un array

Como bash permite que los arrays contengan "huecos" en la asignación de subscripts, a veces es útil para determinar qué elementos existen en realidad. Esto puede hacerse con una expansión de parámetros usando las siguientes fórmulas:

 ${!array[*]}
 ${!array[@]}

donde array es el nombre de una variable array. Como en las otras expansiones que usan * y @, la forma @ entre comillas es la más útil, ya que se expande en palabras separadas:

 [me@linuxbox ~]$ foo=([2]=a [4]=b [6]=c)
 [me@linuxbox ~]$ for i in "${foo[@]}"; do echo $i; done
 ab
 c
 [me@linuxbox ~]$ for i in "${!foo[@]}"; do echo $i; done
 2

 4
 6

Añadiendo elementos al final de un array

Saber el número de elementos de un array no ayuda si necesitamos añadir valores al final del array, ya que los valores devueltos por las notaciones * y @ no nos dicen el máximo índice del array en uso. Afortunadamente, el shell nos da una solución. Usando el operador de asignación +=, podemos añadir valores automáticamente al final de un array. Aquí, asignamos tres valores al array foo, y luego le añadimos tres más.

 [me@linuxbox ~]$ foo=(a b c)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c
 [me@linuxbox ~]$ foo+=(d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f


Ordenando un array

Como en las hojas de cálculo, a menudo es necesario ordenar los valores de una columna de datos. El shell no tiene una forma directa de harcelo, pero no es complicado hacerlo con un poco de código:

 #!/bin/bash


 # array-sort : Sort an array

 a=(f e d c b a)

 echo "Original array: ${a[@]}"
 a_sorted=($(for i in "${a[@]}"; do echo $i; done | sort))
 echo "Sorted array: ${a_sorted[@]}"

Cuando lo ejecutamos, el script produce esto:

 [me@linuxbox ~]$ array-sort
 Original array:  f e d c b a
 Sorted array:    a b c d e f

El script opera copiando el contenido del array original (a) en un segundo array (a_sorted) con un un pequeño truco con sustitución de comandos. Esta técnica básica puede usarse para realizar muchos tipos de operaciones en el array cambiando el diseño del entubado.

Borrando un array

Para borrar un array, usa el comando unset:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f
 [me@linuxbox ~]$ unset foo
 [me@linuxbox ~]$ echo ${foo[@]}

 [me@linuxbox ~]$

unset puede usarse también para borrar elementos individuales del array:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f
 [me@linuxbox ~]$ unset 'foo[2]'
 [me@linuxbox ~]$ echo ${foo[@]}
 a b d e f

En este ejemplo, borramos el tercer elemento del array, subscript 2. Recuerda, los arrays comienzan con el subscript cero, ¡no uno! Fíjate también que el elemento del array debe entrecomillarse para evitar que el shell realice expansión de rutas.

Es interesante cómo, la asignación de un valor vacío a un array no vacía su contenido:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ foo=
 [me@linuxbox ~]$ echo ${foo[@]}
 b c d e f

Cualquier referencia a una variable array sin un subscript se refiere al elemento cero del array:

 [me@linuxbox ~]$ foo=(a b c d e f)
 [me@linuxbox ~]$ echo ${foo[@]}
 a b c d e f
 [me@linuxbox ~]$ foo=A
 [me@linuxbox ~]$ echo ${foo[@]}
 A b c d e f

Arrays asociativos

Versiones recientes de bash soportan ahora arrays asociativos. Los arrays asociativos usan cadenas en lugar de enteros como índices del array. Esta capacidad permite nuevos enfoques interesantes en el manejo de datos. Por ejemplo, podemos crear un array llamado "colors" y usar nombres de colores como índices:

 declare -A colors
 colors["red"]="#ff0000"
 colors["green"]="#00ff00"
 colors["blue"]="#0000ff"

Al contrario de los arrays indexados con enteros, que se crean simplemente referenciándolos, los arrays asociativos deben crearse con el comando declareusando la nueva opción -A. Los elementos de arrays asociativos son accesibles de forma muy parecida a los arrays indexados por enteros:

 echo ${colors["blue"]}

En el próximo capítulo, veremos un script que hace un buen uso de arrays asociativos para producir un interesante informe.


Resumiendo

Si buscamos en la man page de bash la palabra "array", encontramos muchas instancias donde bash hace uso de variables array. Muchas de ellas son algo confusas, pero pueden ofrecer una utilidad ocasional en algunas circunstancias especiales. De hecho, todo el tema de los arrays está algo infrautilizado en la programación shell más allá del hecho de que los programas del shell Unix tradicional (como sh) carecen de soporte para arrays. Es una desafortunada falta de popularidad ya que los arrays se usan ampliamente en otros lenguajes de programación y proporcionan una herramienta poderosa para resolver muchos tipos de problemas de programación.

Los arrays y los bucles tiene una afinidad popular y a menudo se usan juntos. El formato de bucle

 for ((expr; expr; expr))

está particularmente bien adecuado para calcular subscripts de arrays.


Para saber más

No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.