miércoles, 31 de mayo de 2017

Aplicaciones simples

Incluso sin shift, es posible escribir aplicaciones útiles usando parámetros posicionales. A modo de ejemplo, aquí tenemos un programa de información de un archivo simple:

#!/bin/bash

# file_info: simple file information program

PROGNAME=$(basename $0)

if [[ -e $1 ]]; then
    echo -e "\nFile Type:"
    file $1
    echo -e "\nFile Status:"
    stat $1
else
    echo "$PROGNAME: usage: $PROGNAME file" >&2
    exit 1
fi

Este programa muestra el tipo de archivo (determinado por el comando file) y el estado del archivo (del comando stat) de un archivo especificado. Una característica interesante de este programa es la variable PROGNAME. Se le da el valor resultante del comando basename $0. El comando basename elimina la parte inicial de una ruta, dejando sólo el nombre base de un archivo. En nuestro ejemplo, basename elimina la parte inicial de la ruta contenida en el parámetro $0, la ruta completa de nuestro programa de ejemplo. Este valor es útil cuando construimos mensajes tales como el mensaje de uso al final del programa. Escribiendo el código de esta forma, el script puede  renombrarse y el mensaje se ajusta automáticamente para contener el nombre del programa.

martes, 30 de mayo de 2017

shift - Accediendo a muchos argumentos

Pero, ¿qué ocurre cuando le damos al programa un gran número de argumentos como en el ejemplo siguiente?:

[me@linuxbox ~]$ posit-param *

Number of arguments: 82
$0 = /home/me/bin/posit-param
$1 = addresses.ldif
$2 = bin
$3 = bookmarks.html
$4 = debian-500-i386-netinst.iso
$5 = debian-500-i386-netinst.jigdo
$6 = debian-500-i386-netinst.template
$7 = debian-cd_info.tar.gz
$8 = Desktop
$9 = dirlist-bin.txt

En este ejemplo, el comodín * se expande en 82 argumentos. ¿Cómo podemos procesar tal cantidad? El shell ofrece un método, aunque burdo, para hacerlo. El comando shift hace que todos los parámetros "bajen uno" cada vez que se ejecute. De hecho, usando shift, es posible hacerlo con sólo un parámetro (además de $0, que nunca cambia):

#!/bin/bash

# posit-param2: script to display all arguments

count=1

while [[ $# -gt 0 ]]; do
    echo "Argument $count = $1"
    count=$((count + 1))
    shift
done

Cada vez que se ejecuta shift, el valor de $2 se mueve a $1, el valor de $3 se mueve a $2 y así sucesivamente. El valor de $# también se reduce en uno.

En el programa posit-param2, creamos un bucle que evalúa el número de argumentos restantes y continúa siempre que haya al menos uno. Mostramos el argumento actual, incrementamos la variable count con cada iteración del bucle para conseguir un contador del número de argumentos procesados y, finalmente, ejecutamos un shift para cargar $1 con el siguiente argumento. Aquí está el programa en funcionamiento:

[me@linuxbox ~]$ posit-param2 a b c d
Argument 1 = a
Argument 2 = b
Argument 3 = c
Argument 4 = d

lunes, 29 de mayo de 2017

Determinando el número de argumentos

El shell también ofrece una variable, $#, que muestra el número de argumentos en la línea de comandos:

#!/bin/bash

# posit-param: script to view command line parameters

echo "
Number of arguments: $#
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
"

El resultado:

[me@linuxbox ~]$ posit-param a b c d

Number of arguments: 4
$0 = /home/me/bin/posit-param
$1 = a
$2 = b
$3 = c
$4 = d
$5 =
$6 =
$7 =
$8 =
$9 =

viernes, 26 de mayo de 2017

Accediendo a la línea de comandos

El shell ofrece una serie de variables llamadas parámetros posicionales que contienen las palabras individuales de la línea de comandos. Las variables se nombran del 0 al 9. Puede comprobarse así:

#!/bin/bash

# posit-param: script to view command line parameters

echo "
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
"

Un script muy simple que muestra el valor de las variables $0-$9. Cuando lo ejecutamos sin argumentos de la línea de comandos:

[me@linuxbox ~]$ posit-param

$0 = /home/me/bin/posit-param
$1 =
$2 =
$3 =
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =

Incluso cuando no se le dan argumentos, $0 siempre contendrá el primer elemento que aparezca en la línea de comandos, que es la ruta del programa que se está ejecutando. Cuando se le pasan argumentos, vemos el resultado:

[me@linuxbox ~]$ posit-param a b c d

$0 = /home/me/bin/posit-param
$1 = a
$2 = b
$3 = c
$4 = d
$5 =
$6 =
$7 =
$8 =
$9 =

Nota: En realidad puedes acceder a más de nueve parámetros usando la expansión de parámetros. Para especificar un número mayor que nueve, incluimos el número entre llaves. Por ejemplo ${10}, ${55}, ${211} y así sucesivamente.

miércoles, 24 de mayo de 2017

Parámetros Posicionales

Una prestación que ha faltado en nuestros programas es la capacidad de aceptar y procesar opciones y argumentos de la línea de comandos. En este capítulo, examinaremos las funcionalidades del shell que permiten que nuestros programas accedan al contenido de la línea de comandos.

lunes, 22 de mayo de 2017

Para saber más

viernes, 19 de mayo de 2017

Resumiendo

El comando case es un añadido útil en nuestra maleta llena de trucos de programación. Como veremos en el próximo capítulo, es la herramienta perfecta para manejar ciertos tipos de problemas.

miércoles, 17 de mayo de 2017

Realizando múltiples acciones

En versiones de bash anteriores a la 4.0, case sólo permitía realizar una única acción en una coincidencia exitosa. Tras una coincidencia exitosa, el comando terminaría. Aquí vemos un script que prueba un carácter:

#!/bin/bash

# case4-1: test a character

read -n 1 -p "Type a character > "
echo
case $REPLY in
   [[:upper:]])  echo "'$REPLY' is upper case." ;;
   [[:lower:]])  echo "'$REPLY' is lower case." ;;
   [[:alpha:]])  echo "'$REPLY' is alphabetic." ;;
   [[:digit:]])  echo "'$REPLY' is a digit." ;;
   [[:graph:]])  echo "'$REPLY' is a visible character." ;;
   [[:punct:]])  echo "'$REPLY' is a punctuation symbol." ;;
   [[:space:]])  echo "'$REPLY' is a whitespace character." ;;
   [[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;
esac

Ejecutar este script produce esto:

[me@linuxbox ~]$ case4-1
Type a character > a
'a' is lower case.

Gran parte del script funciona, pero falla si un carácter coincide con más de una de las clases de caracteres POSIX. Por ejemplo, el carácter "a" es tanto una letra minúscula como una letra del alfabeto, así como un dígito hexadecimal. En bash anterior a la versión 4.0 no había forma de que case coincidiera en más de una prueba. Las versiones modernas de bash, añaden la notación ";;&" para terminar cada acción, así que podemos hacer esto:

#!/bin/bash

# case4-2: test a character


read -n 1 -p "Type a character > "

echo
case $REPLY in
   [[:upper:]])  echo "'$REPLY' is upper case." ;;&
   [[:lower:]])  echo "'$REPLY' is lower case." ;;&
   [[:alpha:]])  echo "'$REPLY' is alphabetic." ;;&
   [[:digit:]])  echo "'$REPLY' is a digit." ;;&
   [[:graph:]])  echo "'$REPLY' is a visible character." ;;&
   [[:punct:]])  echo "'$REPLY' is a punctuation symbol." ;;&
   [[:space:]])  echo "'$REPLY' is a whitespace character." ;;&
   [[:xdigit:]]) echo "'$REPLY' is a hexadecimal digit." ;;&
esac

Cuando ejecutamos este script, obtenemos esto:

[me@linuxbox ~]$ case4-2
Type a character > a
'a' is lower case.
'a' is alphabetic.
'a' is a visible character.
'a' is a hexadecimal digit.

Añadir la sintaxis ";;&" permite que case continúe con el siguiente test en lugar de simplemente terminar.

lunes, 15 de mayo de 2017

Patrones

Los patrones usados por case son los mismos que aquellos usados por la expansión de rutas. Los patrones terminan con un carácter ")". Aquí tenemos algunos patrones válidos:

Tabla 32-1: Ejemplos de patrones de case
Patrón Descripción
a) Coincide si palabra es igual a "a".
[[:alpha:]]) Coincide si palabra es un caracter alfabético único.
???) Coincide si palabra tiene exactamente tres caracteres de longitud.
*.txt) Coincide si palabra finaliza con los caracteres ".txt".
*) Coincide con cualquier valor de palabra. Es una buena práctica incluirlo como el último patrón en un comando case, para tomar cualquier valor de palabra que no coincida con un patrón previo; es decir, para detectar cualquier posible valor no válido.

Aquí tenemos un ejemplo de cómo funcionan los patrones:

#!/bin/bash

read -p "enter word > "

case $REPLY in
 [[:alpha:]]) echo "is a single alphabetic character." ;;
 [ABC][0-9])  echo "is A, B, or C followed by a digit." ;;
 ???)         echo "is three characters long." ;;
 *.txt)       echo "is a word ending in '.txt'" ;;
 *)           echo "is something else." ;;
esac

También es posible combinar múltiples patrones usando el carácter de la barra vertical como separador. Esto crea un patrón condicional "o". Esto es útil para cosas como manejar caracteres tanto en mayúsculas como en minúsculas. Por ejemplo:

#!/bin/bash

# case-menu: a menu driven system information program

clear
echo "
Please Select:

A. Display System Information
B. Display Disk Space
C. Display Home Space Utilization
Q. Quit
"
read -p "Enter selection [A, B, C or Q] > "

case $REPLY in
    q|Q) echo "Program terminated."
         exit
         ;;
    a|A) echo "Hostname: $HOSTNAME"
         uptime
         ;;
    b|B) df -h
         ;;
    c|C) 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
         ;;
    *)   echo "Invalid entry" >&2
         exit 1
         ;;
esac

Aquí, modificamos el programa case-menu para usar letras en lugar de dígitos para las selecciones del menú. Fíjate cómo los nuevos patrones permiten introducir tanto letras mayúsculas como minúsculas.

viernes, 12 de mayo de 2017

case

El comando compuesto de bash para múltiples opciones se llama case. Tiene la siguiente sintaxis:

case palabra in
     [patrón [| patrón]...) comandos ;;]...
esac

Si examinamos el programa read-menu del Capítulo 28, vemos la lógica usada para actuar en una selección del usuario:

#!/bin/bash

# read-menu: a menu driven system information program

clear
echo "
Please Select:

1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
"
read -p "Enter selection [0-3] > "

if [[ $REPLY =~ ^[0-3]$ ]]; then
    if [[ $REPLY == 0 ]]; then
        echo "Program terminated."
        exit
    fi
    if [[ $REPLY == 1 ]]; then
        echo "Hostname: $HOSTNAME"
        uptime
        exit
    fi
    if [[ $REPLY == 2 ]]; then
        df -h
        exit
    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
        exit
    fi
else
    echo "Invalid entry." >&2
    exit 1
fi

Usando case podemos reemplazar esta lógica con algo más simple:

#!/bin/bash

# case-menu: a menu driven system information program

clear
echo "
Please Select:

1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
"
read -p "Enter selection [0-3] > "

case $REPLY in
    0)  echo "Program terminated."
        exit
        ;;
    1)  echo "Hostname: $HOSTNAME"
        uptime
        ;;
    2)  df -h
        ;;
    3)  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
        ;;
    *)  echo "Invalid entry" >&2
        exit 1
        ;;
esac

El comando case mira el valor de palabra, en nuestro ejemplo, el valor de la variable REPLY, y luego trata de compararlo con uno de los patrones especificados. Cuando encuentra una coincidencia, los comandos asociados con el patrón especificado se ejecutan. Una vez que se encuentra una coincidencia, no se intentan más coincidencias.

miércoles, 10 de mayo de 2017

Control de Flujo: Ramificando con case

En este capítulo, continuaremos viendo el control de flujo. En el Capítulo 28, hemos construido algunos menús simples y la lógica usada para actuar ante una elección del usuario. Para hacerlo, usamos una serie de comandos if para identificar cuál de las posibles elecciones ha sido seleccionada. Este tipo de construcciones aparecen frecuentemente en programas, tanto es así que muchos lenguajes de programación (incluyendo el shell) proporcionan un mecanismo de control de flujo para decisiones con múltiples opciones.

lunes, 8 de mayo de 2017

Para saber más

viernes, 5 de mayo de 2017

Resumiendo

En este capítulo, hemos visto sólo algunos de los problemas que pueden surgir durante el desarrollo de un script. Por supuesto, hay muchos más. Las técnicas descritas aquí permitirán encontrar los errores más comunes. El depurado es un arte fino que puede desarrollarse a través de la experiencia, tanto en conocimiento de cómo evitar errores (probando constantemente durante el desarrollo) como encontrando errores (uso efectivo del trazado).

jueves, 4 de mayo de 2017

Examinando valores durante la ejecución

A menudo es útil, junto con el trazado, mostrar el contenido de variables para ver el funcionamiento interno de un script mientras se está ejecutando. Normalmente el truco es aplicar instancias adicionales de echo:

#!/bin/bash

# trouble: script to demonstrate common errors

number=1

echo "number=$number" # DEBUG
set -x # Turn on tracing
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi
set +x # Turn off tracing

En este ejemplo trivial, simplemente mostramos el valor de la variable number y marcamos la línea añadida con un comentario para facilitar su posterior identificación y eliminación. Esta técnica es particularmente útil cuando se supervisa el comportamiento de bucles y la aritmética dentro de los scripts.

miércoles, 3 de mayo de 2017

Trazado

Los errores a menudo son casos de un flujo lógico inesperado dentro de un script. Es decir, partes del script que nunca se ejecutan, o se ejecutan en el orden incorrecto o en el momento equivocado. Para ver el flujo real del programa, usamos una técnica llamada tracing o trazado.

Un método de trazado implica colocar mensajes informativos en un script que muestren la ubicación de la ejecución. Podemos añadir mensajes a nuestro fragmento de código:

echo "preparing to delete files" >&2
if [[ -d $dir_name ]]; then
    if cd $dir_name; then
echo "deleting files" >&2
        rm *
    else
        echo "cannot cd to '$dir_name'" >&2
        exit 1
    fi
else
    echo "no such directory: '$dir_name'" >&2
    exit 1
fi
echo "file deletion complete" >&2

Enviamos mensajes al error estándar para separalo de la salida normal. No sangramos las líneas que contienen los mensajes, así es más fácil encontrarlas cuando sea el momento de borrarlas.

Ahora, cuando se ejecuta el script, es posible ver que el borrado de archivos se ha realizado:

[me@linuxbox ~]$ deletion-script
preparing to delete files
deleting files
file deletion complete
[me@linuxbox ~]$

bash también proporciona un método de trazado, implementado por la opción -x y el comando set con la opción -x. Usando nuestro anterior script trouble, podemos activar el trazado para todo el script añadiendo la opción -x en la primera línea:

#!/bin/bash -x

# 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

Cuando se ejecuta, el resultado es el siguiente:

[me@linuxbox ~]$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.

Cuando el trazado está activado, vemos que los comandos se ejecutan con expansiones aplicadas. El signo más al principio de la línea indica el resultado del trazado para distinguirlo de las líneas de salida normal. El signo más es el carácter por defecto de la salida del trazado. Está contenido en la variable de shell PS4 (prompt string 4 - cadena de prompt 4). El contenido de esta variable puede ajustarse para hacer el prompt más útil. Aquí, modificamos el contenido de la variable para incluir el número de línea actual en el script donde se realiza el trazado. Fíjate que se requieren comillas simples para impedir la expansión hasta que el prompt se haya usado realmente:

[me@linuxbox ~]$ export PS4='$LINENO + '
[me@linuxbox ~]$ trouble
5 + number=1
7 + '[' 1 = 1 ']'
8 + echo 'Number is equal to 1.'
Number is equal to 1.

Para realizar un trazado en una porción concreta de un script, en lugar del script completo, podemos usar el comando set con la opción -x:

#!/bin/bash

# trouble: script to demonstrate common errors

number=1

set -x # Turn on tracing
if [ $number = 1 ]; then
    echo "Number is equal to 1."
else
    echo "Number is not equal to 1."
fi
set +x # Turn off tracing

Usamos el comando set con la opción -x para activar el trazado y la opción +x para desactivar el trazado. Esta técnica puede usarse para examinar múltiples porciones de un script problemático.

martes, 2 de mayo de 2017

Encontrando el área del problema

En algunos scripts, particularmente los largos, a veces es útil aislar el área del script relacionada con el problema. Esto no siempre será el error real, pero aislarlo a menudo proporciona un mejor enfoque de la causa real. Una técnica que puede usarse para aislar código es "comentar" secciones de un script. Por ejemplo, nuestro fragmento de borrado de archivos podría modificarse para determinar si la sección eliminada está relacionada con un error:

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

Colocar símbolos de comentarios al principio de cada línea en una sección lógica del script, impide que la sección se ejecute. Las pruebas se pueden volver a realizar para ver si la eliminación del código tiene algún impacto en el comportamiento del error.

lunes, 1 de mayo de 2017

Depuración

Si las pruebas revelan un problema con un script, el siguiente paso es la depuración (debugging). "Un problema" normalmente significa que el script, de alguna forma, no está realizando lo que espera el programador. Si este es el caso, necesitamos determinar cuidadosamente qué es lo que el script está haciendo en realidad y por qué. Encontrar errores puede implicar, a veces, mucho trabajo detectivesco.

Un script bien diseñado debería ayudar. Debería estar programado defensivamente, para detectar condiciones anormales y proporcionar retroalimentación útil al usuario. A veces, sin embargo, los problemas son bastante extraños e inesperados, y se requieren técnicas más complicadas.