viernes, 28 de abril de 2017

Casos de prueba

Para realizar una prueba útil, es importante desarrollar y aplicar buenos casos de prueba. Esto se hace eligiendo cuidadosamente la entrada de datos o las condiciones de operación que reflejen casos límite. En nuestro fragmento de código (que es muy simple), queremos saber cómo se comporta el código bajo tres condiciones específicas:
  1. dir_name contiene el nombre de un directorio existente
  2. dir_name contiene el nombre de un directorio no existente
  3. dir_name está vacío
Realizando la prueba con cada una de estas condiciones, obtenemos una buena cobertura de prueba.

Al igual que con el diseño, las pruebas también dependen del tiempo. No todas las características del script necesitan ser ampliamente probadas. De hecho, es una forma de determinar qué es lo más importante. Como podría ser potencialmente destructivo si no funciona bien, nuestro fragmento de código se merece un análisis preciso tanto durante el diseño como las pruebas.

jueves, 27 de abril de 2017

Pruebas

Las pruebas son un paso importante en todo tipo de desarrollo de software, incluidos los scripts. Hay un dicho en el mundo del software libre, "publica pronto, publica a menudo", que refleja este hecho. Publicando pronto y a menudo, el software está más expuesto al uso y a las pruebas. La experiencia nos ha demostrado que los bugs son más fáciles de encontrar, y mucho menos caros de arreglar, si se encuentran pronto en el ciclo de desarrollo.

En un tema anterior, vimos cómo podemos usar stubs para verificar el flujo del programa. Desde las primeras fases del desarrollo del script, son una técnica valiosa para comprobar el progreso de nuestro trabajo.

Echemos un vistazo al problema de eliminación de archivos anterior y veamos cómo puede codificarse para que sea más fácil la realización de pruebas. Probar el fragmento original de código puede ser peligroso, ya que su propósito es borrar archivos, pero podríamos modificar el código para hacer que la prueba sea segura:

if [[ -d $dir_name ]]; then
    if cd $dir_name; then
        echo rm * # TESTING
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
else
    echo "no such directory: '$dir_name'" >&2
    exit 1
fi
exit # TESTING

Como las condiciones del error ya muestran mensajes útiles, no tenemos que añadir nada. El cambio más importante es colocar un comando echo justo antes del comando rm para permitir al comando y su lista de argumentos expandidos que se muestren, en lugar de que el comando realmente se ejecute. Este cambio permite la ejecución segura del código. Al final del fragmento de código, colocamos un comando exit para concluir la prueba y prevenir que cualquier otra parte del script se ejecute. La necesidad de esto variará segun el diseño del script.

También incluimos algunos comentarios que actúan como "marcadores" de nuestros cambios relacionados con la prueba. Estos pueden usarse para ayudarnos a encontrar y eliminar los cambios cuando se completa la prueba.

miércoles, 26 de abril de 2017

El diseño va en función del tiempo

Cuando estudiaba diseño industrial en la universidad, un sabio profesor expuso que el grado de diseño en un proyecto viene determinado por la cantidad de tiempo que le den al diseñador. Si te dieran cinco minutos para diseñar un dispositivo "que mate moscas," diseñarías un matamoscas. Si te dieran cinco meses, podrías diseñar un "sistema anti-moscas" guiado por láser.

El mismo principio se aplica a la programación. A veces un script "rápido y sucio" servirá si sólo va a usarse una vez y sólo va a usarlo el programador. Este tipo de script es común y debería desarrollarse rápidamente para hacer que el esfuerzo sea rentable. Tales scripts no necesitan muchos comentarios ni comprobaciones defensivas. En el otro extremo, si el script está destinado a un uso en producción, es decir, un script que se usará una y otra vez para una tarea importante o por muchos usuarios, necesita un desarrollo mucho más cuidadoso.

martes, 25 de abril de 2017

Verificando la entrada

Una regla general de buena programación es que, si un programa acepta entrada, debe ser capaz de gestionar cualquier cosa que reciba. Esto normalmente significa que la entrada debe ser cuidadosamente filtrada, para asegurar que sólo se acepte una entrada válida para su procesamiento posterior. Vimos un ejemplo de esto en el capítulo anterior cuando estudiamos el comando read. Un script conteniendo el siguiente test para verificar una selección de menú:

[[ $REPLY =~ ^[0-3]$ ]]

Este test es muy específico. Sólo devolverá un estado de salida cero si la cadena devuelta por el usuario es un número en el rango entre cero y tres. Nada más será aceptado. A veces estos tipos de tests pueden ser muy complicados de escribir, pero el esfuerzo es necesario para producir un script de alta calidad.

lunes, 24 de abril de 2017

Programación defensiva

Es importante verificar las suposiciones cuando programamos. Esto significa una evaluación cuidadosa del estado de salida de los programas y comandos que se usan en un script. Aquí hay un ejemplo, basado en hechos reales. Un desafortunado administrador de sistemas escribió un script para realizar una tarea de mantenimiento en un importante servidor. El script contenía las siguientes dos líneas de código:

cd $dir_name
rm *

No hay nada intrínsecamente malo en estas dos líneas, siempre que el directorio citado en la variable, dir_name exista. Pero ¿qué ocurre si no es así? En ese caso, el comando cd falla y el script continúa con la siguiente línea y borra los archivos en el directorio de trabajo actual. ¡No es para nada el resultado deseado! Este desafortunado administrador destruyó una parte importante del servidor por esta decisión de diseño.

Veamos algunas formas en que este diseño puede mejorarse. Primero, sería prudente hacer que la ejecución de rm dependea del éxito de cd:

cd $dir_name && rm *

De esta forma, si el comando cd falla, el comando rm no se ejecuta. Esto es mejor, pero aún deja abierta la posibilidad de que la variable, dir_name, esté sin configurar o vacía, lo que daría como resultado el borrado de los archivos en el directorio home del usuario. Esto también podría evitarse comprobando si dir_name realmente contiene el nombre de un directorio existente:

[[ -d $dir_name ]] && cd $dir_name && rm *

A menudo, es mejor terminar el script con un error cuando ocurre una situación como la anterior:

# Delete files in directory $dir_name
if [[ ! -d "$dir_name" ]]; then
    echo "No such directory: '$dir_name'" >&2
    exit 1
fi
if ! cd $dir_name; then
    echo "Cannot cd to '$dir_name'" >&2
    exit 1
fi
if ! rm *; then
    echo "File deletion failed. Check results" >&2
    exit 1
fi

Aquí, comprobamos tanto el nombre, para ver si es el de un directorio existente, como el éxito del comando cd. Si cualquiera falla, se envía un error descriptivo al error estándar y el script termina con un estado de salida de uno para indicar un fallo.

viernes, 21 de abril de 2017

Errores lógicos

Al contrario de los errores sintácticos, los errores lógicos no impiden que se ejecute un script. El script se ejecutará, pero no producirá el resultado deseado, debido a un problema con su lógica. Hay un número incontable de errores lógicos posibles, pero aquí os dejo unos pocos de los tipos más comunes que se encuentran en scripts:
  1. Expresiones condicionales incorrectas. Es fácil codificar mal un if/then/else y llevar a cabo una lógica equivocada. A veces la lógica se invierte, o queda incompleta.
  2. Errores "por uno". Cuando codificamos bucles que utilizan contadores, es posible pasar por alto que el bucle requiere que el contador empiece por cero en lugar de uno, para que concluya en el punto correcto. Este tipo de errores producen un bucle que "va más allá del final" contando demasiado lejos, o que pierde la última iteración del bucle terminando una iteración antes de tiempo.
  3. Situaciones no previstas. La mayoría de los errores lógicos son producidos porque un programa se encuentra con datos o situaciones que no han sido previstas por el programador. Esto puede incluir tanto expansiones no previstas, como un nombre de archivo que contiene espacios y que se expande en múltiples argumentos de comando en lugar de un nombre de archivo único.

jueves, 20 de abril de 2017

Expansiones inesperadas

Es posible tener errores que sólo ocurren de forma intermitente en un script. A veces el script se ejecutará correctamente y otras veces fallará debido al resultado de una expansión. Si devolvemos nuestro punto y coma perdido y cambiamos el valor de number a una variable vacía, podemos comprobarlo:

#!/bin/bash

# trouble: script to demonstrate common errors

number=

if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

Ejecutar el script con este cambio da como resultado esta salida:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 7: [: =: unary operator expected
Number is not equal to 1.

Obtenemos un mensaje de error bastante críptico, seguido de la salida del segundo comando echo. El problema es la expansión de la variable number dentro del comando test. Cuando el comando:

[ $number = 1 ]

se somete a la expansión con number estando vacío, el resultado es este:

[ = 1 ]

que no es válido y se genera el error. El operador = es un operador binario (requiere un valor a cada lado), pero el primer valor no está, así que el comando test espera un operador unario (como -Z) en su lugar. Además, como el test ha fallado (debido al error), el comando if recibe un código de salida distinto de cero y actúa de acuerdo con esto, y se ejecuta el segundo comando echo.

Este problema puede corregirse añadiendo comillas alrededor del primer argumento en el comando test:

[ "$number" = 1 ]

De esta forma cuando se produce la expansión, el resultado será este:

[ "" = 1 ]

que proporciona el número correcto de argumentos. Además de para las cadenas vacías, las comillas deben usarse en casos donde un valor puede expandirse en cadenas multipalabra, como nombres de archivo que contengan espacios.

miércoles, 19 de abril de 2017

Símbolos perdidos o inesperados

Otro error común es olvidarse de completar un comando compuesto, como if o while. Veamos qué ocurre si eliminamos el punto y coma tras el test en el comando if:

#!/bin/bash

# trouble: script to demonstrate common errors

number=1

if [ $number = 1 ] then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

El resultado es:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 9: syntax error near unexpected token `else'
/home/me/bin/trouble: line 9: `else'

De nuevo, el mensaje de error apunta a un error que ocurre más tarde que el problema real. Lo que ocurre es realmente interesante. Como dijimos, if acepta una lista de comandos y evalúa el código de salida del último comando de la lista. En nuestro programa, nuestra intención es que esta lista sea un único comando, [, un sinónimo de test. El comando [ toma lo que le sigue como una lista de argumentos; en nuestro caso, cuatro argumentos: $number, 1, =, y ]. Con el punto y coma eliminado, se añade la palabra then a la lista de argumentos, lo que es sintácticamente legal. El siguiente comando echo también es legal. Se interpreta como otro comando en la lista de comandos que if evaluará como código de salida. A continuación encontramos else, pero está fuera de sitio, ya que el shell lo reconoce como una palabra reservada (una palabra que tiene un significado especial para el shell) y no como el nombre de un comando, de ahí el mensaje de error.

martes, 18 de abril de 2017

Comillas perdidas

Si editamos nuestro script y eliminamos las comillas finales del argumento que sigue al primer comando echo:

#!/bin/bash

# trouble: script to demonstrate common errors

number=1

if [ $number = 1 ]; then
    echo "Number is equal to 1.
else
    echo "Number is not equal to 1."
fi

vemos lo que ocurre:

[me@linuxbox ~]$ trouble
/home/me/bin/trouble: line 10: unexpected EOF while looking for matching `"'
/home/me/bin/trouble: line 13: syntax error: unexpected end of file

Genera dos errores. Curiosamente, los números de línea reportados no son de donde se han eliminado las comillas, si no que están mucho más adelante en el programa. Podemos ver por qué, si seguimos el programa tras las comillas perdidas. bash continuará buscando las comillas de cierre hasta que encuentra unas, lo que ocurre inmediatamente tras el segundo comando echo. bash se queda muy confundido tras esto, y la sintaxis del comando if se rompe porque la sentencia fi está ahora dentro de una cadena entrecomillada (pero abierta).

En scripts largos, este tipo de errores pueden ser algo difíciles de encontrar. Ayudará usar un editor con resaltado sintáctico. Si está instalada la versión completa de vim, puede activarse el resaltado sintáctico introduciendo el comando:

:syntax on

lunes, 17 de abril de 2017

Errores sintácticos

Un tipo de error común es el sintáctico. Los errores sintácticos implican errores de escritura en algunos elementos de la sintaxis del shell. En la mayoría de los casos, estos tipos de errores provocarán que el shell se niegue a ejecutar el script.

En las siguientes exposiciones, usaremos este script para comprobar los tipos comunes de errores:

#!/bin/bash

# trouble: script to demonstrate common errors

number=1

if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi

Tal como está escrito, este script se ejecuta con éxito:

[me@linuxbox ~]$ trouble
Number is equal to 1.

viernes, 14 de abril de 2017

Solución de Problemas

A medida que nuestros scripts se hacen más complejos, es hora de echar un vistazo a lo que ocurre cuando las cosas no funcionan y no hacen lo que queremos. En este capítulo, veremos algunos tipos comunes de errores que ocurren en nuestros scripts, y describiremos algunas técnicas útiles que pueden usarse para localizar y erradicar los problemas.

jueves, 13 de abril de 2017

Para saber más

miércoles, 12 de abril de 2017

Resumiendo

Con la introducción de los bucles, y nuestros anteriores encuentros con las ramificaciones, las subrutinas y secuencias, hemos visto la mayoría de los tipos de control de flujo usados en programas. bash se guarda algunos trucos en la manga, pero son refinamientos de estos conceptos básicos.

martes, 11 de abril de 2017

Leyendo archivos con bucles

while y until pueden procesar entrada estándar. Esto permite que los archivos sean procesados con bucles while y until. En el siguiente ejemplo, mostraremos el contenido del archivo distros.txt usado en los capítulos anteriores:

#!/bin/bash

# while-read: read lines from a file

while read distro version release; do
    printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
        $distro \
        $version \
        $release
done < distros.txt

Para redireccionar un archivo al bucle, colocamos el operador de redirección tras la sentencia done. El bucle usará read para introducir los campos desde el archivo redirigido. El comando read saldrá cada vez que lea una línea, con un estado de salida cero hasta que se alcance el final del archivo. En este punto, saldrá con un estado de salida distinto de cero, terminando de esta forma el bucle. También es posible canalizar la entrada estándar dentro de un bucle:

#!/bin/bash

# while-read2: read lines from a file

sort -k 1,1 -k 2n distros.txt | while read distro version release; do

    printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
        $distro \
        $version \
        $release
done

Aquí hemos tomado la salida del comando sort y hemos mostrado la secuencia de texto. Sin embargo es importante recordar que como una tubería ejecutará el bucle en un subshell, cualquier variable creada o asignada dentro del bucle se perderá cuando termine el bucle.

lunes, 10 de abril de 2017

until

El comando until es casi como while, excepto que en lugar de salir de un bucle cuando se encuentra un estado de salida distinto de cero, hace lo contrario. Un bucle until continua hasta que reciba un estado de salida cero. En nuestro script while-count, continuamos el bucle mientras el valor de la variable count sea menor o igual a 5. Podríamos tener el mismo resultado codificando el script con until:

#!/bin/bash

# until-count: display a series of numbers

count=1

until [[ $count -gt 5 ]]; do
    echo $count
    count=$((count + 1))
done
echo "Finished."

Cambiando la expresión test a $count -gt 5, until terminará el bucle en el momento correcto. La decisión de usar el bucle while o until consiste normalmente en elegir el que permita escribir el test más claro.

viernes, 7 de abril de 2017

Salir de un bucle

bash proporciona dos comandos integrados que pueden usarse para controlar el flujo del programa dentro de los bucles. El comando break termina inmediatamente un bucle, y el control del programa continúa con la siguiente sentencia que siga al bucle. El comando continue hace que se salte el resto del bucle, y el control del programa continúa con la siguiente iteración del bucle. Aquí vemos una versión del programa while-menu incorporando tanto break como continue:

#!/bin/bash

# while-menu2: a menu driven system information program

DELAY=3 # Number of seconds to display results

while true; do
    clear
    cat <<- _EOF_
        Please Select:

        1. Display System Information
        2. Display Disk Space
        3. Display Home Space Utilization
        0. Quit

    _EOF_
    read -p "Enter selection [0-3] > "

    if [[ $REPLY =~ ^[0-3]$ ]]; then
        if [[ $REPLY == 1 ]]; then
            echo "Hostname: $HOSTNAME"
            uptime
            sleep $DELAY
            continue
        fi
        if [[ $REPLY == 2 ]]; then
            df -h
            sleep $DELAY
            continue
        fi
        if [[ $REPLY == 3 ]]; then
            if [[ $(id -u) -eq 0 ]]; then
                echo "Home Space Utilization (All Users)"
                du -sh /home/*
            else
                echo "Home Space Utilization ($USER)"
                du -sh $HOME
            fi
            sleep $DELAY
            continue
        fi
        if [[ $REPLY == 0 ]]; then
            break
        fi
    else
        echo "Invalid entry."
        sleep $DELAY
    fi
done
echo "Program terminated."

En esta versión del script, configuramos un bucle sin fin (uno que nunca termina por sí sólo) usando el comando true para proporcionar un estado de salida a while. Como true siempre sale con un estado de salida de cero, el bucle nunca terminará. Esta es sorprendentemente una práctica de programación común. Como el bucle no termina por sí sólo, corresponde al programador proporcionar algún tipo de interrupción del bucle cuando haga falta. En este script, el comando break se usa para salir del bucle cuando se elige la selección "0". El comando continue se ha incluido al final de las otras opciones del script para ofrecer una ejecución más eficiente. Usando continue, el script se saltará código que no se necesita cuando se identifica una selección. Por ejemplo, si se elige la selección "1" y esta se identifica, no hay razón para probar el resto de selecciones.

miércoles, 5 de abril de 2017

while

bash puede expresar una idea similar. Digamos que queremos mostrar cinco números en orden secuencial desde el uno al cinco. Un script de bash podría construirse de la siguiente forma:

#!/bin/bash

# while-count: display a series of numbers

count=1

while [[ $count -le 5 ]]; do
     echo $count
     count=$((count + 1))
done
echo "Finished."

Cuando se ejecuta, este script muestra lo siguiente:

[me@linuxbox ~]$ while-count
1
2
3
4
5
Finished.

La sintaxis del comando while es:

while comandos; do comandos; done

Al igual que if, while evalúa el estado de salida de una lista de comandos. Mientras que el estado de salida sea cero, ejecuta los comandos dentro del bucle. En el script anterior, se crea la variable count y se le asigna un valor inicial de 1. El comando while evalúa el estado de salida del comando test. Mientras el comando test devuelva un estado de salida cero, los comandos dentro del bucle se ejecutan. Al final de cada ciclo, se repite el comando test. Tras seis iteraciones del bucle, el valor de count se ha incrementado hasta 6, el comando test ya no devuelve un estado de salida de cero y el bucle termina. El programa continúa con la siguiente línea a continuación del bucle.

Podemos usar un bucle while para mejorar el programa read-menu del capítulo anterior:

#!/bin/bash

# while-menu: a menu driven system information program

DELAY=3 # Number of seconds to display results

while [[ $REPLY != 0 ]]; do
    clear
    cat <<- _EOF_
        Please Select:

        1. Display System Information
        2. Display Disk Space
        3. Display Home Space Utilization
        0. Quit

    _EOF_
    read -p "Enter selection [0-3] > "

    if [[ $REPLY =~ ^[0-3]$ ]]; then
        if [[ $REPLY == 1 ]]; then
            echo "Hostname: $HOSTNAME"
             uptime
             sleep $DELAY
        fi
        if [[ $REPLY == 2 ]]; then
            df -h
            sleep $DELAY
        fi
        if [[ $REPLY == 3 ]]; then
            if [[ $(id -u) -eq 0 ]]; then
                echo "Home Space Utilization (All Users)"
                du -sh /home/*
            else
                echo "Home Space Utilization ($USER)"
                du -sh $HOME
            fi
            sleep $DELAY
        fi
    else
        echo "Invalid entry."
        sleep $DELAY
    fi
done
echo "Program terminated."

Englobando el menú en un bucle while, podemos hacer que el programa repita la pantalla de menú tras cada selección. El bucle continua mientras que REPLY no sea igual a "0" y el menú se muestra de nuevo, dando al usuario la oportunidad de realizar otra selección. Al final de cada acción, se ejecuta un comando sleep de forma que el programa espera unos segundos para permitir que el resultado de la selección se vea antes de borrar la pantalla y volver a mostrar el menú. Una vez que REPLY es igual a "0", que indica la selección de "quit", termina el bucle y continúa la ejecución con la línea siguiente a done.

lunes, 3 de abril de 2017

Bucles

La vida diaria está llena de actividades repetitivas. Ir al trabajo cada día, pasear al perro, cortar zanahorias y todas las tareas que implican repetir una serie de pasos. Consideremos cortar una zanahoria en rodajas. Si expresamos esta actividad en pseudocódigo, sería algo parecido a esto:
  1. coger una tabla de cortar
  2. coger un cuchillo
  3. poner la zanahoria en la tabla de cortar
  4. levantar el cuchillo
  5. mover la zanahoria
  6. cortar la zanahoria
  7. si toda la zanahoria está cortada, parar, si no, volver al paso 4
Los pasos del 4 al 7 forman un bucle. Las acciones dentro del bucle se repiten hasta que se alcanza la condición, "toda la zanahoria está cortada".