jueves, 31 de agosto de 2017

Usando tuberías con nombre

Para demostrar cómo funciona la tubería con nombre, necesitaremos dos ventanas de terminal (o alternativamente, dos consolas virtuales). En el primer terminal, introducimos un comando simple y redirigimos su salida a la tubería con nombre:

[me@linuxbox ~]$ ls -l > pipe1

Tras pulsar la tecla Enter, el comando parecerá que se ha colgado. Esto es porque aún no está recibiendo nada desde el otro extremo de la tubería. Cuando esto ocurre, se dice que la tubería está bloqueada. Esta condición desaparecerá una vez que apliquemos un proceso al otro extremo y empiece a leer la entrada desde la tubería. Usando la segunda ventana de terminal, introducimos este comando:

[me@linuxbox ~]$ cat < pipe1

y el listado del directorio producido desde la primera ventana de terminal aparece en el segundo terminal como salida desde el comando cat. El comando ls en el primer terminal se completa con éxito una vez que ya no está bloqueado.

miércoles, 30 de agosto de 2017

Configurando una tubería con nombre

Primero, debemos crear una tubería con nombre. Esto se hace usando el comando mkfifo:

[me@linuxbox ~]$ mkfifo pipe1
[me@linuxbox ~]$ ls -l pipe1
prw-r--r-- 1 me me 0 2009-07-17 06:41 pipe1

Aquí hemos usado mkfifo para crear una tubería con nombre llamada pipe1. Usando ls, examinamos el archivo y vemos que la primera letra en el campo atributos es "p", indicando que es una tubería con nombre.

martes, 29 de agosto de 2017

Tuberías con nombre

En la mayoría de los sistemas tipo Unix, es posible crear un tipo especial de archivo llamado tubería con nombre. Las tuberías con nombre se usan para crear una conexión entre dos procesos y pueden usarse igual que otros tipos de archivos. No son muy populares, pero es bueno conocerlas.

Hay una arquitectura de programación común llamada cliente-servidor, que puede hacer uso de un método de comunicación como las tuberías con nombre, así como de otros tipos de comunicación entre procesos tales como conexiones de red.

El tipo más ampliamente usado de sistema cliente-servidor es, claramente, la comunicación entre un navegador web y un servidor web. El navegador web actúa como cliente, realizando peticiones al servidor y el servidor responde al navegador con páginas web.

Las tuberías con nombre se comportan como archivos, pero en realidad forman buffers "el primero en entrar es el primero en salir" (FIFO - first in first out). Igual que las tuberías normales (sin nombre), los datos entran por un extremo y salen por el otro. Con las tuberías con nombre, es posible configurar algo como esto:

proceso1 > tubería_con_nombre

y

proceso2 < tubería_con_nombre

y se comportará como si fuera:

proceso1 | proceso2

lunes, 28 de agosto de 2017

wait

Primero probaremos el comando wait. Para hacerlo necesitaremos dos scripts, un script padre:

#!/bin/bash

# async-parent : Asynchronous execution demo (parent)

echo "Parent: starting..."

echo "Parent: launching child script..." 
async-child & 
pid=$! 
echo "Parent: child (PID= $pid) launched."

echo "Parent: continuing..." 
sleep 2

echo "Parent: pausing to wait for child to finish..."
wait $pid

echo "Parent: child is finished. Continuing..."
echo "Parent: parent is done. Exiting."

y un script hijo:

#!/bin/bash

# async-child : Asynchronous execution demo (child)

echo "Child: child is running..."
sleep 5
echo "Child: child is done. Exiting."

En este ejemplo, vemos que el script hijo es muy simple. La acción real la realiza el padre. En el script padre, se arranca el script hijo y se envía a segundo plano. El ID de proceso del script hijo se graba asignando a la variable pid el valor del parámetro de shell $!, que siempre contendrá el ID de proceso del último trabajo puesto en segundo plano.

El script padre continúa y luego ejecuta un comando wait con el PID del proceso hijo. Esto hace que el script padre se pause hasta que el script hijo salga, punto en el cual el script padre concluye.

Cuando se ejecutan, los scripts padre e hijo producen la siguiente salida:

[me@linuxbox ~]$ async-parent
Parent: starting...
Parent: launching child script...
Parent: child (PID= 6741) launched.
Parent: continuing...
Child: child is running...
Parent: pausing to wait for child to finish...
Child: child is done. Exiting.
Parent: child is finished. Continuing...
Parent: parent is done. Exiting.

viernes, 25 de agosto de 2017

Ejecución asíncrona

A veces es preferible realizar más de una tarea al mismo tiempo. Hemos visto cómo los sistemas operativos modernos son al menos multitarea incluso multiusuario. Los scripts pueden construirse para comportarse de forma multitarea.

Normalmente, esto implica arrancar un script que, a su vez, arranca uno o más scripts hijos que realizan una tarea adicional mientras que el script padre continúa ejecutándose. Sin embargo, cuando una serie de scripts se ejecutan de esta forma, puede haber problemas en la coordinación entre el script padre y los hijos. Es decir, ¿qué pasa si el padre o el hijo son dependientes el uno del otro, y un script debe esperar a que el otro termine su tarea antes de finalizar la suya propia?

bash tiene un comando integrado para ayudarnos a manejar una ejecución asíncrona como esta. El comando wait hace que un script padre se pause hasta que un proceso especificado (es decir, el script hijo) termine.

jueves, 24 de agosto de 2017

Archivos temporales

Una razón por la que los gestores de señales se incluyen en los scripts es para eliminar los archivos temporales que el script pueda crear para manejar resultados intermedios durante su ejecución. Hay algo artístico en la denominación de los archivos temporales. Tradicionalmente, los programas en sistemas tipo Unix crean sus archivos temporales en el directorio /tmp, un directorio compartido creado para tales archivos. Sin embargo, como el directorio está compartido, esto conlleva algunos problemas de seguridad, particularmente para programas que se ejecutan con privilegios de superusuario. Mas allá del paso obvio de establecer permisos apropiados para los archivos expuestos a todos los usuarios del sistema, es importante dar a los archivos temporales nombres no predecibles. Esto evita un exploit conocido como temp race attack. Una forma de crear un nombre no predecible (pero descriptivo) es hacer algo como esto:

archivotemporal=/tmp/$(nombrebase $0).$$.$RANDOM

Esto creará un nombre de archivo consistente en el nombre del programa, seguido por su ID de proceso (PID), seguido por un entero aleatorio. Fíjate, sin embargo, que la variable de shell $RANDOM solo devuelve un valor del rango 1-32767, que no es un rango muy grande en términos informáticos, por lo que una única instancia no es suficiente para vencer a un posible atacante.

Una forma mejor es usar el programa mktemp (no confundir con la función mktemp de la biblioteca estándar) para crear y nombrar el archivo temporal. El programa mktemp acepta una plantilla como argumento que se usa para construir el nombre del archivo. La plantilla debe incluir una serie de caracteres "X", que se reemplazan con un número correspondiente de letras y números aleatorios. Cuanto más larga sea la serie de caracteres "X", más larga será la serie de caracteres aleatorios. Aquí tenemos un ejemplo:

archivotemporal=$(mktemp /tmp/foobar.$$.XXXXXXXXXX)

Esto crea un archivo temporal y asigna su nombre a la variable tempfile. Los caracteres "X" en la plantilla se reemplazan con letras y números aleatorios de forma que el nombre del archivo final (que, en este ejemplo, también incluye el valor expandido del parámetro especial $$ para obtener el PID) debería ser algo así:

/tmp/foobar.6593.UOZuvM6654

Para scripts que se ejecutan por usuarios normales, sería prudente evitar el uso del directorio /tmp y crear un directorio para archivos temporales dentro del directorio home del usuario, con una línea de código como esta:

[[ -d $HOME/tmp ]] || mkdir $HOME/tmp

miércoles, 23 de agosto de 2017

Trampas

En el Capítulo 10, vimos cómo los programas pueden responder a las señales. También podemos añadir esta capacidad a nuestros scripts. Aunque los scripts que hemos escrito hasta ahora no han necesitado esta capacidad (porque tienen tiempos de ejecución muy cortos, y no crean archivos temporales), los scripts más largos y complicados pueden beneficiarse de tener una rutina de manejo de señales.

Cuando diseñamos un script largo y complicado, es importante considerar qué ocurre si el usuario cierra la sesión o apaga el ordenador mientras el script se está ejecutando. Cuando ocurre un evento como este, debe enviarse una señal a todos los procesos afectados. En respuesta, los programas que representan estos procesos pueden realizar acciones para asegurar una terminación apropiada y ordenada del programa. Digamos, por ejemplo, que hemos escrito un script que crea un archivo temporal durante su ejecución. En nombre del buen diseño, habríamos hecho que el script borre el archivo cuando el script termine su trabajo. También sería inteligente hacer que el script borre el archivo si recibe una señal indicando que el programa va a terminar prematuramente.

bash ofrece un mecanismo para este propósito conocido como trampa. Las trampas se implementan con el comando integrado, apropiadamente denominado, trap. trap usa la siguiente sintaxis:

trap argumento señal [señal...]

donde argumento es una cadena que se leerá y tratará como un comando y señal es la especificación de una señal que pone en funcionamiento la ejecución del comando interpretado.

Aquí tenemos un ejemplo simple:

#!/bin/bash

# trap-demo : simple signal handling demo

trap "echo 'I am ignoring you.'" SIGINT SIGTERM

for i in {1..5}; do
    echo "Iteration $i of 5"
    sleep 5
done

Este script define una trampa que ejecutará un comando echo cada vez que recibe la señal SIGINT o SIGTERM mientras el script se está ejecutando. La ejecución del programa aparece así cuando el usuario intenta detener el script presionando Ctrl-c:

[me@linuxbox ~]$ trap-demo
Iteration 1 of 5
Iteration 2 of 5
I am ignoring you.
Iteration 3 of 5
I am ignoring you.
Iteration 4 of 5
Iteration 5 of 5

Como podemos ver, cada vez que el usuario intenta interrumpir el programa, se muestra el mensaje en su lugar.

Construir una cadena para formar una secuencia útil de comandos puede ser complicado, por lo que es una práctica habitual especificar una función de shell como comando. En este ejemplo, se especifica una función de shell separada para manejar cada señal:

#!/bin/bash

# trap-demo2 : simple signal handling demo

exit_on_signal_SIGINT () {
    echo "Script interrupted." 2>&1
    exit 0
}

exit_on_signal_SIGTERM () {
    echo "Script terminated." 2>&1
    exit 0
}

trap exit_on_signal_SIGINT SIGINT
trap exit_on_signal_SIGTERM SIGTERM

for i in {1..5}; do
    echo "Iteration $i of 5"
    sleep 5
done

Este script presenta dos comandos trap, uno para cada señal. Cada trampa, a su vez, especifica una función de shell a ejecutar cuando se recibe una señal en particular. Fíjate en la inclusión de un comando exit en cada una de las funciones de manejo de señales. Sin un exit, el script continuaría tras completar la función.

Cuando el usuario presione Ctrl-c durante la ejecución de este script, el resultado aparece así:

[me@linuxbox ~]$ trap-demo2
Iteration 1 of 5
Iteration 2 of 5
Script interrupted.

martes, 22 de agosto de 2017

Sustitución de procesos

Aunque parecen iguales y ambos pueden usarse para combinar secuencias para redireccionarlas, hay una diferencia importante entre los comandos agrupados y los subshells. Mientras que un comando agrupado ejecuta todos sus comandos en el shell actual, un subshell (como su nombre indica) ejecuta sus comandos en una copia hijo del shell actual. Esto significa que el entorno se copia y se pasa a una instancia del shell. Cuando el subshell termina, la copia del entorno se pierde, por lo que cualquier cambio hecho al entorno del subshell (incluyendo la asignación de variables) se pierde también. Por lo tanto, en la mayoría de los casos, a menos que un script requiera un subshell, los comandos agrupados son preferibles a los subshells. Los comandos agrupados son también más rápidos y requieren menos memoria.

Vimos un ejemplo del problema del entorno del subshell en el capítulo 28, cuando descubrimos que el comando read en una tubería no funciona como esperaríamos intuitivamente. Para resumir, si construimos una tubería como esta:

echo "foo" | read
echo $REPLY

El contenido de la variable REPLY siempre está vacío porque el comando read se ejecuta en un subshell, y su copia de REPLY se destruye cuando el subshell termina.

Como los comandos en tuberías siempre se ejecutan en subshells, cualquier comando que asigne variables se encontrará con este problema. Afortunadamente, el shell ofrece una forma exótica de expansión llamada sustitución de procesos que puede usarse para solucionar este problema.

La sustitución de procesos se expresa de dos formas:

Para procesos que producen salida estándar:

<(lista)

o, para procesos que toman entrada estándar:

>(lista)

donde lista es una lista de comandos.

Para resolver nuestro problema con read, podemos emplear sustitución de procesos así:

read < <(echo "foo")
echo $REPLY

La sustitución de procesos nos permite tratar la salida de un subshell como un archivo ordinario para propósitos de redirección. De hecho, como es una forma de expansión, podemos examinar su valor real:

[me@linuxbox ~]$ echo <(echo "foo")
/dev/fd/63

Usando echo para ver el resultado de la expansión, vemos que la salida del subshell está proporcionada por el archivo llamado /dev/fd/63.

La sustitución de procesos se una a menudo con bucles que contienen read. Aquí tenemos un ejemplo de un bucle read que procesa el contenido de un listado de directorios creado por un subshell:

#!/bin/bash

# pro-sub : demo of process substitution

while read attr links owner group size date time filename; do
    cat <<- EOF
        Filename:   $filename
        Size:       $size
        Owner:      $owner
        Group:      $group
        Modified:   $date $time
        Links:      $links
        Attributes: $attr

    EOF
done < <(ls -l | tail -n +2)

El bucle ejecuta read para cada línea de un listado de directorios. El propio listado se produce en la línea final del script. Esta línea redirige la salida de la sustitución de procesos a la entrada estándar del bucle. El comando tail está incluido en la tubería de la sustitución de procesos para eliminar la primera línea del listado, que no se necesita.

Cuando se ejecuta, el script produce una salida como esta:

[me@linuxbox ~]$ pro_sub | head -n 20
Filename:   addresses.ldif
Size:       14540
Owner:      me
Group:      me
Modified:   2009-04-02 11:12
Links:      1
Attributes: -rw-r--r--

Filename:   bin
Size:       4096
Owner:      me
Group:      me
Modified:   2009-07-10 07:31
Links:      2
Attributes: drwxr-xr-x

Filename:   bookmarks.html
Size:       394213
Owner:      me
Group:      me

lunes, 21 de agosto de 2017

Comandos agrupados y subshells

bash permite agrupar comandos. Esto puede hacerse de dos formas; con un comando agrupado o con un subshell. Aquí tenemos ejemplos de la sintáxis de cada uno de ellos:

Comando agrupado:

{ comando1; comando2; [comando3; ...] }

Subshell:

(comando1; comando2; [comando3;...])

Las dos formas difieren en que el comando agrupado rodea sus comandos con llaves y el subshell usa paréntesis. Es importante fijarse en que, debido a la forma en que bash implementa los comandos agrupados, las llaves deben separarse de los comandos por un espacio y el último comando debe terminar con un punto y coma o con una nueva línea antes de la llave de cierre.

Entonces ¿para qué sirven los comandos agrupados y los subshells? Aunque tienen una diferencia importante (que veremos en un momento), ambos se usan para gestionar redirecciones. Consideremos un segmento de script que realiza redirecciones en múltiples comandos:

ls -l > output.txt
echo "Listing of foo.txt" >> output.txt
cat foo.txt >> output.txt

Esto es bastante sencillo. Tres comandos con su salida redireccionada a un archivo llamado output.txt. Usando un comando agrupado, podríamos codificarlo de la siguiente forma:

{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt

Usando un subshell es similar:

(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt

Usando esta técnica nos hemos ahorrado algo de escritura, pero donde brilla un comando agrupado o un subshell realmente es en las tuberías. Cuando construimos una tubería de comandos, a menudo es útil combinar el resultado de varios comandos en una única secuencia. Los comandos agrupados y los subshell hacen esto de forma fácil:

{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } | lpr

Aquí hemos combinado la salida de nuestros tres comandos y la hemos canalizado a la entrada de lpr para producir un informe impreso.

En el script que sigue, usaremos comandos agrupados y veremos varias técnicas de programación que pueden emplearse junto con arrays asociativos. Este script, llamado array-2, cuando se el da el nombre de un directorio, imprime una lista de los archivos en el directorio junto a los nombres de los propietarios de los archivos y de los grupos propietarios. Al final del listado, el script imprime un recuento del número de archivos que pertenecen a cada propietario y grupo. Aquí vemos el resultado (resumido para abreviar) cuando se le da al script el directorio /usr/bin:

[me@linuxbox ~]$ array-2 /usr/bin
/usr/bin/2to3-2.6           root  root
/usr/bin/2to3               root  root
/usr/bin/a2p                root  root
/usr/bin/abrowser           root  root
/usr/bin/aconnect           root  root
/usr/bin/acpi_fakekey       root  root
/usr/bin/acpi_listen        root  root
/usr/bin/add-apt-repository root  root
.
.
.
/usr/bin/zipgrep            root  root
/usr/bin/zipinfo            root  root
/usr/bin/zipnote            root  root
/usr/bin/zip                root  root
/usr/bin/zipsplit           root  root
/usr/bin/zjsdecode          root  root
/usr/bin/zsoelim            root  root

File owners:
daemon  :    1 file(s)
root    : 1394 file(s)

File group owners:
crontab :    1 file(s)
daemon  :    1 file(s)
lpadmin :    1 file(s)
mail    :    4 file(s)
mlocate :    1 file(s)
root    : 1380 file(s)
shadow  :    2 file(s)
ssh     :    1 file(s)
tty     :    2 file(s)
utmp    :    2 file(s)

Aquí tenemos un listado (con los números de línea) del script:

 1 #!/bin/bash
 2
 3 # array-2: Use arrays to tally file owners
 4
 5 declare -A files file_group file_owner groups owners
 6
 7 if [[ ! -d "$1" ]]; then
 8    echo "Usage: array-2 dir" >&2
 9    exit 1
10 fi
11
12 for i in "$1"/*; do
13    owner=$(stat -c %U "$i")
14    group=$(stat -c %G "$i")
15    files["$i"]="$i"
16    file_owner["$i"]=$owner
17    file_group["$i"]=$group
18    ((++owners[$owner]))
19    ((++groups[$group]))
20 done
21
22 # List the collected files
23 { for i in "${files[@]}"; do
24    printf "%-40s %-10s %-10s\n" \
25       "$i" ${file_owner["$i"]} ${file_group["$i"]}
26 done } | sort
27 echo
28
29 # List owners
30 echo "File owners:"
31 { for i in "${!owners[@]}"; do
32     printf "%-10s: %5d file(s)\n" "$i" ${owners["$i"]}
33 done } | sort
34 echo
35
36 # List groups
37 echo "File group owners:"
38 { for i in "${!groups[@]}"; do
39     printf "%-10s: %5d file(s)\n" "$i" ${groups["$i"]}
40 done } | sort

Echemos un vistazo a la mecánica de este script:

Línea 5: Los arrays asociativos deben crearse con el comando declare usando la opción -A. En este script creamos los cinco arrays siguientes:

files contiene los nombres de los archivos en el directorio, indexados por nombre de archivo
file_group contiene el grupo propietario de cada archivo, indexado por nombre de archivo
file_owner contiene el propietario de cada archivo, indexado por nombre de archivo
groups contiene el número de archivos pertenecientes al grupo indexado
owners contiene el número de archivos pertenecientes al propietario indexado

Líneas 7-10: Comprueba que se ha pasado un nombre de directorio válido como parámetro posicional. Si no, se muestra un mensaje de uso y el script sale con un estado de salida de 1.

Líneas 12-20: Hace un bucle a través de los archivos del directorio. Usando el comando stat, las líneas 13 y 14 extraen los nombres del propietario del archivo y del grupo propietario y asigna valores a sus arrays respectivos (líneas 16, 17) usando el nombre del archivo como índice del array. Del mismo modo, el nombre del archivo se asigna al array files (línea 15).

Líneas 18-19: El número total de archivos pertenecientes al propietario del archivo y al grupo propietario se incrementan en uno.

Líneas 22-27: Se muestra la lista de archivos. Esto se hace usando la expansión de parámetros "${array[@]}" que se expande en la lista completa de elementos del array cada uno tratado como una palabra separada. Esto permite la posibilidad de que un nombre de archivo contenga espacios en blanco. Fíjate también que el bucle completo está incluido entre llaves para que forme un comando agrupado. Esto permite que la salida completa del bucle se canalice al comando sort. Esto es necesario porque la expansión de los elementos del array no está ordenada.

Líneas 29-40: Estos dos bucles son similares al bucle de la lista de archivos excepto que usan la expansión "${!array[@]}" que se expande en la lista de índices del array en lugar de en la lista de elementos del array.

viernes, 18 de agosto de 2017

Cosas exóticas

En este, el último capítulo de nuestro viaje, veremos algunos flecos. Aunque hemos cubierto mucho terreno en los capítulos anteriores, hay muchas características de bash que no hemos cubierto. Muchas son algo confusas, y útiles principalmente para los que integran bash en una distribución Linux. Sin embargo, hay unas pocas que, aunque no son de uso común, son de ayuda para algunos problemas de programación. Las veremos aquí.

miércoles, 16 de agosto de 2017

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 debido principalmente al hecho de que los programas tradicionales del shell Unix (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 tienen una afinidad natural y a menudo se usan juntos. El formato de bucle

for ((expr; expr; expr))

está particularmente adecuado para calcular índices de arrays.

martes, 15 de agosto de 2017

Arrays asociativos

Las versiones recientes de bash ahora soportan 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 declare usando 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.

lunes, 14 de agosto de 2017

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 también puede usarse 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, el de índice 2. Recuerda, los arrays comienzan con el índice cero, ¡no uno! Fíjate también que el elemento del array debe entrecomillarse para evitar que el shell realice expansión de rutas.

Curiosamente, 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 índice 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

viernes, 11 de agosto de 2017

Ordenando un array

Al igual que 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 hacerlo, pero no es complicado de realizar 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 funciona 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 de la tubería.

jueves, 10 de agosto de 2017

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

miércoles, 9 de agosto de 2017

Encontrando los índices usados por un array

Como bash permite que los arrays contengan "huecos" en la asignación de índices, a veces es útil 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
a
b
c
[me@linuxbox ~]$ for i in "${!foo[@]}"; do echo $i; done
2
4
6

martes, 8 de agosto de 2017

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 la expansión de parámetros para examinar la longitud del array, utilizando la notación @. Finalmente, vemos la longitud del elemento 100 que contiene la cadena "foo". Es interesante fijarse que al haber asignado 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 contarían.

lunes, 7 de agosto de 2017

Mostrando todo el contenido de un array

Los índices * 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 entrecomillan. La notación * da como resultado una sola palabra con el contenido del array, mientras que la notación @ da como resultado tres palabras, lo que coincide con el contenido "real" del array.

viernes, 4 de agosto de 2017

Operaciones con arrays

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

jueves, 3 de agosto de 2017

Accediendo a los elementos de un array

Entonces ¿para qué sirven los arrays? De la misma forma que muchas tareas de gestión 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. A partir de 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 cuándo 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 objetivo. Produce una tabla mostrando, para cada hora del día (0-23), cuántos 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 tratará (y finalmente fallará) de interpretar los valores del "00" al "09" 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 el recuento final de los archivos.

miércoles, 2 de agosto de 2017

Asignando valores a un array

Los valores pueden asignarse de dos formas. Los 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.

Se pueden asignar múltiples valores usando la siguiente sintaxis:

nombre=(valor1 valor2 ...)

donde nombre es el nombre del array y valor... son los valores asignados secuencialmente a los 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)

martes, 1 de agosto de 2017

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. Se requiere el uso de llaves en el segundo comando para evitar que el shell intente una expansión de ruta en el nombre del elemento del array.

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

[me@linuxbox ~]$ declare -a a

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