martes, 3 de octubre de 2017

Leyendo la Entrada del Teclado

Los scripts que hemos escrito hasta ahora carecen de una característica común a la mayoría de los programas informáticos - interactividad. O sea, la capacidad de los programas para interactuar con el usuario. Aunque la mayoría de los programas no necesitan ser interactivos, algunos programas se benefician de poder aceptar entrada directamente del usuario. Tomemos, por ejemplo, el script del capítulo anterior:

 #!/bin/bash


 # test-integer2: evaluate the value of an integer.

 INT=-5

 if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
     if [ $INT -eq 0 ]; then
          echo "INT is zero."
     else
          if [ $INT -lt 0 ]; then
               echo "INT is negative."
          else
               echo "INT is positive."
          fi
          if [ $((INT % 2)) -eq 0 ]; then
               echo "INT is even."
          else
               echo "INT is odd."
          fi
     fi
 else
     echo "INT is not an integer." >&2
     exit 1
 fi

Cada vez que queremos cambiar el valor de INT, tenemos que editar el script. Sería mucho más útil si el script pudiera pedir un valor al usuario. En este capítulo, empezaremos a ver como podemos añadir interactividad a nuestros programas.


read - Lee valores de la entrada estándar

El comando incorporado read se usa para leer una única línea de la entrada estándar. Este comando puede usarse para leer entrada de teclado o, cuando se emplea redirección, una línea de datos desde un archivo. El comando tiene la siguiente sintaxis:

 read [-opciones] [variable...]

donde opciones es una o más opciones disponibles listadas a continuación y variable es el nombre de una o más variables para almacenar el valor de entrada. Si no se proporciona ningún nombre de variable, la variable de shell REPLY contiene la línea de datos.

Básicamente, read asigna campos desde la entrada estándar hacia las variables especificadas. Si modificamos nuestro script de evaluación de entero para usar read, aparecería así:

 #!/bin/bash

 # read-integer: evaluate the value of an integer.

 echo -n "Please enter an integer -> "
 read int

 if [[ "$int" =~ ^-?[0-9]+$ ]]; then
     if [ $int -eq 0 ]; then
          echo "$int is zero."
     else
          if [ $int -lt 0 ]; then
               echo "$int is negative."
          else
               echo "$int is positive."
          fi
          if [ $((int % 2)) -eq 0 ]; then
               echo "$int is even."
          else
               echo "$int is odd."
          fi
     fi
 else
     echo "Input value is not an integer." >&2
     exit 1
 fi

Usamos echo con la opción -n (que elimina la nueva línea en blanco en la salida) para mostrar un prompt, y luego usamos read para introducir un valor para la variable int. Ejecutando este script obtenemos esto:

 [me@linuxbox ~]$ read-integer
 Please enter an integer -> 5
 5 is positive.
 5 is odd.

read puede asignar entrada a múltiples variables, como se muestra en este script:

 #!/bin/bash 

 # read-multiple: read multiple values from keyboard

 echo -n "Enter one or more values > "
 read var1 var2 var3 var4 var5

 echo "var1 = '$var1'"
 echo "var2 = '$var2'"
 echo "var3 = '$var3'"
 echo "var4 = '$var4'"
 echo "var5 = '$var5'"

En este script, asignamos y mostramos los cinco valores. Fíjate como se comporta readcuando se le dan diferentes números de valores:

 [me@linuxbox ~]$ read-multiple
 Enter one or more values > a b c d e
 var1 = 'a'
 var2 = 'b'
 var3 = 'c'
 var4 = 'd'
 var5 = 'e'
 [me@linuxbox ~]$ read-multiple
 Enter one or more values > a
 var1 = 'a'
 var2 = ''
 var3 = ''
 var4 = ''
 var5 = ''
 [me@linuxbox ~]$ read-multiple
 Enter one or more values > a b c d e f g
 var1 = 'a'
 var2 = 'b'
 var3 = 'c'
 var4 = 'd'
 var5 = 'e f g'

Si read recibe menos que el número esperado, las variables extra estarán vacías, mientras que una cantidad excesiva de entrada provoca que la última variable contenga toda la entrada extra.

Si no se listan variables tras el comando read, se le asignará a una variable de shell, REPLY, toda la entrada:

 #!/bin/bash

 # read-single: read multiple values into default variable

 echo -n "Enter one or more values > "
 read

 echo "REPLY = '$REPLY'"

Ejecutar este script produce esto:

 [me@linuxbox ~]$ read-single
 Enter one or more values > a b c d

 REPLY = 'a b c d'


Opciones

read soporta las siguientes opciones:

-a array 
Asigna la entrada a array, comenzando con el orden cero. Veremos los arrays en el Capítulo 35.

-d delimitador
El primer carácter de la cadena delimitador se usa para indicar el final de la entrada, en lugar de un carácter de nueva línea.

-e
Usa Readline para manejar la entrada. Esto permite editar la entrada de la misma forma que la línea de comandos.

-i cadena 
Usa cadena como una respuesta por defecto si el usuario simplemente presiona Enter. Requiere la opción -e.

-n num 
Lee num caracteres de entrada en lugar de la línea completa.

-p prompt 
Muestra un prompt para la entrada usando la cadena prompt.

-r 
Modo raw. No interpreta las barras invertidas como escapados.

-s 
Modo silencioso. No envía caracteres a la pantalla cuando son escritos. Es útil cuando introducimos contraseñas u otra información confidencial.

-t segundos 
Tiempo muerto. Finaliza la entrada tras segundos. readdevuelve un estado de salida no-cero y la entrada caduca.

-u fd 
Usa entrada del descriptor de archivo fd, en lugar de la entrada estándar.


Usando varias opciones, podemos hacer cosas interesantes con read. Por ejemplo, con la opción -p, podemos proporcionar una cadena de prompt:

 #!/bin/bash

 # read-single: read multiple values into default variable

 read -p "Enter one or more values > "

 echo "REPLY = '$REPLY'"

Con las opciones -t y -s podemos escribir un script que lee entrada "secreta" y caduca si la salida no se completa en un tiempo especificado:

 #!/bin/bash

 # read-secret: input a secret passphrase

 if read -t 10 -sp "Enter secret passphrase > " secret_pass; then
     echo -e "\nSecret passphrase = '$secret_pass'"
 else
     echo -e "\nInput timed out" >&2
     exit 1
 fi

El script pregunta al usuario por una frase secreta y espera entrada durante 10 segundos. Si la entrada no se completa en el tiempo especificado, el script sale con un error. Como la opción -s se incluye, los caracteres de la contraseña no se muestran en la pantalla al escribirla.

Es posible proporcionar al usuario una respuesta por defecto usando las opciones -e y -i juntas:

 #!/bin/bash 

 # read-default: supply a default value if user presses Enter key

 read -e -p "What is your user name? " -i $USER
 echo "You answered: '$REPLY'"

En este script, pedimos al usuario que introduzca su nombre de usuario y la variable de entorno USER proporciona un valor por defecto. Cuando se ejecuta el script muestra la cadena por defecto si el usuario pulsa simplemente la tecla Intro, read asignará la cadena por defecto a la variable REPLY.

 [me@linuxbox ~]$ read-default
 What is your user name? me
 You answered: 'me'

IFS

Normalmente, el shell realiza división de palabras en la entrada proporcionada por read. Como hemos visto, esto significa que varias palabras separadas por uno o más espacios se convierten en elementos separados en la línea de entrada, y read las asigna a variables separadas. Este comportamiento lo configura una variable de shell llamada IFS (Internal Field Separator - Separador Interno de Campos). El valor por defecto de IFS es un espacio, un tabulador o un carácter de nueva línea, cada uno de los cuales separará unos elementos de otros.

Podemos ajustar el valor de IFS para controlar la separación de campos de entrada a read. Por ejemplo, el archivo /etc/passwd contiene líneas de datos que usan los dos puntos como separador de campos. Cambiando el valor de IFS a los dos puntos, podemos usar read par introducir contenido de /etc/passwd y separar campos correctamente en diferentes variables. Aquí tenemos un script que lo hace:

#!/bin/bash


# read-ifs: read fields from a file

FILE=/etc/passwd

read -p "Enter a username > " user_name

file_info=$(grep "^$user_name:" $FILE)

if [ -n "$file_info" ]; then
    IFS=":" read user pw uid gid name home shell <<< "$file_info"
    echo "User = '$user'"
    echo "UID = '$uid'"
    echo "GID = '$gid'"
    echo "Full Name = '$name'"
    echo "Home Dir. = '$home'"
    echo "Shell = '$shell'"
else
    echo "No such user '$user_name'" >&2
    exit 1
fi

Este script pide al usuario que introduzca el nombre de usuario de una cuenta del sistema, luego muestra los diferentes campos encontrados en el registro del usuario del archivo /etc/passwd. El script contiene dos líneas interesantes. La primera es:

 file_info=$(grep "^$user_name:" $FILE)

Esta línea asigna el resultado del comando grep a la variable file_info. La expresión regular usada por grep garantiza que el nombre de usuario sólo aparece en una línea en el archivo /etc/passwd.

La segunda línea interesante es esta:

 IFS=":" read user pw uid gid name home shell <<< "$file_info"

La línea consta de tres partes: una asignación de variable, un comando read con una lista de nombres de variables como argumentos, y un nuevo y extraño operador de redirección. Veremos la asignación de variables primero.

El shell permite que una o más asignaciones de variables se produzcan inmediatamente antes de un comando. Estas asignaciones alteran el entorno para el comando que les sigue. El efecto de la asignación es temporal; sólo cambian el entorno por la duración del comando. En nuestro caso, el valor de IFS se cambia al carácter dos puntos. Alternativamente, podríamos haber escrito el código así:

 OLD_IFS="$IFS"
 IFS=":"
 read user pw uid gid name home shell <<< "$file_info"
 IFS="$OLD_IFS"

donde almacenamos el valor de IFS, asignamos un nuevo valor, realizamos el comando read y luego restauramos IFS a su valor original. Claramente, colocar la asignación de variable delante del comando es una forma más concisa de hacer lo mismo.

El operador <<< indica una cadena-aquí. Una cadena-aquí es como un documento-aquí, sólo que más corto, consistente en una única cadena. En nuestro ejemplo, la línea de datos del archivo /etc/passwd se pasa a la entrada estándar del comando read. Podríamos preguntarnos porque eleginmos este modo tan indirecto en lugar de:

echo "$file_info" | IFS=":" read user pw uid gid name home shell

Bueno, hay una razón...


No puedes entubar a read

Aunque el comando read toma normalmente entrada de la entrada estándar, no puedes hacer ésto:

 echo "foo" | read

Esperaríamos que esto funcione, pero no lo hace. El comando parecerá funcionar pero la variable REPLY estará siempre vacía ¿Y esto por qué?

La explicación tiene que ver con la forma en que el shell maneja las tuberías o pipes. En bash (y otros shells como sh), las tuberías crean subshells. Estos son copias del shell y su entorno que se usan para ejecutar el comando en la tubería. En nuestro ejemplo anterior, read se ejecuta en un subshell.

Los subshells en sistemas como-Unix crean copias del entorno para que los procesos lo usen mientras se ejecuten. Cuando el proceso termina, la copia del entorno es destruida. Esto significa que un subshell nunca puede alterar el entorno de su proceso padre. read asigna variables, que luego pasan a ser parte del entorno. En el ejemplo anterior, read asigna el valor "foo" a la variable REPLY en el entorno de su subshell, pero cuando el comando finaliza, el subshell y su entorno son destruidos, y el efecto de la asignación se pierde.

Usar cadenas-aquí es una forma alternativa a este comportamiento. Veremos otro método en el Capítulo 36.

Validando la entrada

Con nuestra nueva habilidad para tener entrada de teclado aparece un nuevo desafío de programación, validar la entrada. Muy a menudo la diferencia entre un programa bien escrito y uno mal escrito reside en la capacidad del programa de tratar lo inesperado. Frecuentemente, lo inesperado aparece en una mala entrada. Hemos hecho algo de esto con nuestros programas de evaluación en el capítulo anterior, donde comprobamos los valores de enteros y descartamos valores vacíos y caracteres no numéricos. Es importante realizar este tipo de comprobaciones de programación cada vez que un programa recibe entrada, para protegernos de datos no válidos. Esto es especialmente importante para programas que son compartidos por múltiples usuarios. Omitir estos salvavidas en interés de la economía podría excusarse si un programa es para que lo use sólo el autor para realizar tareas especiales. Incluso así, si el programa realiza tareas peligrosas como borrar archivos, sería prudente incluir validación de datos, por si acaso.

Aquí tenemos un programa de ejemplo que valida varios tipos de entrada:

#!/bin/bash


# read-validate: validate input

invalid_input () {
    echo "Invalid input '$REPLY'" >&2
    exit 1
}

read -p "Enter a single item > "
# input is empty (invalid)
[[ -z $REPLY ]] && invalid_input

# input is multiple items (invalid)
(( $(echo $REPLY | wc -w) > 1 )) && invalid_input

# is input a valid filename?
if [[ $REPLY =~ ^[-[:alnum:]\._]+$ ]]; then
echo "'$REPLY' is a valid filename."
    if [[ -e $REPLY ]]; then
        echo "And file '$REPLY' exists."
    else
        echo "However, file '$REPLY' does not exist."
    fi

    # is input a floating point number?
    if [[ $REPLY =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
        echo "'$REPLY' is a floating point number."
    else
        echo "'$REPLY' is not a floating point number."
    fi

    # is input an integer?
    if [[ $REPLY =~ ^-?[[:digit:]]+$ ]]; then
        echo "'$REPLY' is an integer."
    else
        echo "'$REPLY' is not an integer."
    fi
else
    echo "The string '$REPLY' is not a valid filename."
fi

Este script pide al usuario que introduzca un elemento. El elemento es analizado posteriormente para determinar su contenido. Como podemos ver, el script hace uso de muchos de los conceptos que hemos visto hasta ahora, incluidas las funciones de shell, [[ ]], (( )), los operadores de control &&, e if, así como una buena dosis de expresiones regulares.

Menús

Un tipo común de interactividad se llama basada en menús. En los programas basados en menús, se le presenta al usuario un lista de opciones y se le pide que elija una. Por ejemplo, podríamos imaginar un programa que presente lo siguiente:

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


 Enter selection [0-3] >

Usando lo que hemos aprendido hasta ahora de escribir nuestro programa sys_info_page, podemos construir un programa basado en menús para realizar las tareas del menú anterior:

 #!/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

El script está dividido lógicamente en dos partes. La primera parte muestra el menú y acepta la respuesta del usuario. La segunda parte identifica la respuesta y lleva a cabo la acción seleccionada. Fíjate en el uso del comando exit en este script. Se usa aquí para prevenir que el script ejecute código innecesario después de que se haya realizado una acción. La presencia de múltiples puntos de salida en el programa es una mala idea en general (hace que la lógica del programa sea más difícil de comprender), pero funciona en este script.

Resumiendo

En este capítulo, hemos dado nuestros primeros pasos hacia la interactividad; permitiendo a los usuarios introducir datos en los programas a través del teclado. Usando las técnicas presentadas hasta ahora, es posible escribir muchos programas útiles, como programas de cálculo especializados e interfaces fáciles de usar para comandos arcaicos de la línea de comandos. En el siguiente capítulo, trabajaremos sobre el concepto de programa basado en menús para hacerlo aún mejor.

Crédito extra

Es importante estudiar los programas de este capítulo cuidadosamente y tener un entendimiento completo de la forma en que están estructurados lógicamente, ya que los programas que vendrán incrementarán la complejidad. Como ejercicio, reescribe los programas de este capítulo usando el comando test en lugar del comando compuesto [[ ]]. Pista: Usa grep para evaluar las expresiones regulares y el estado de salida. Será una buena costumbre.

Para saber más

No hay comentarios:

Publicar un comentario

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