viernes, 31 de marzo de 2017

Control de flujo: Bucles con while / until

En el capítulo anterior, hemos desarrollado un programa basado en menús para producir varios tipos de información del sistema. El programa funciona, pero todavía tiene un problema de usabilidad significativo. Sólo ejecuta una única opción y luego termina. Peor aún, si se hace una selección no válida, el programa termina con un error, sin dar al usuario una oportunidad de probar de nuevo. Sería mejor si pudiéramos, de alguna manera, construir el programa de forma que pueda repetir la visualización y selección del menú una y otra vez, hasta que el usuario elija salir del programa.

En este capítulo, veremos un concepto de programación llamado bucle, que puede usarse para hacer que partes del programa se repitan. El shell proporciona tres comandos compuestos para bucles. Veremos dos de ellos en este capítulo, y el tercero en un capítulo posterior.

miércoles, 29 de marzo de 2017

Para saber más

martes, 28 de marzo de 2017

Punto extra

Es importante estudiar los programas de este capítulo cuidadosamente y comprender perfectamente 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 analiza el estado de salida. Será una buena costumbre.

lunes, 27 de marzo de 2017

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 especializados de cálculo 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.

domingo, 26 de marzo de 2017

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 una 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 al 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 generalmente es una mala idea (hace que la lógica del programa sea más difícil de comprender), pero funciona en este script.

viernes, 24 de marzo de 2017

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 forma de 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 va a ser utilizado una sola vez y sólo por 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, [[ ]], (( )), el operador de control &&, e if, así como una buena dosis de expresiones regulares.

miércoles, 22 de marzo de 2017

No puedes canalizar a read

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

echo "foo" | read

Esperaríamos que esto funcionase, pero no lo hace. El comando parecerá funcionar pero la variable REPLY siempre estará 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 tipo Unix crean copias del entorno para que los procesos lo usen mientras se ejecuten. Cuando los procesos terminan, 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 de evitar este comportamiento. Veremos otro método en el Capítulo 36.

lunes, 20 de marzo de 2017

IFS

Normalmente, el shell realiza separació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 contiene un espacio, un tabulador y 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 ingresados en 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 para 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 conicidirá con una sola 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 el tiempo que dura el 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í (here string). 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 por qué elegimos este modo tan indirecto en lugar de:

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

Bueno, hay una razón...

viernes, 17 de marzo de 2017

Opciones

read soporta las siguientes opciones:

Tabla 28-1: Opciones de read
Opción Descripción
-a array Asigna la entrada a array, comenzando con el índice 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 de espera. Finaliza la entrada tras segundos. read devuelve un estado de salida distinto de cero si una entrada excede el tiempo.
-u fd Usa como entrada el 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 entrada 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 y 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'

jueves, 16 de marzo de 2017

read - Lee valores de la entrada estándar

El comando integrado read se usa para leer una única línea de la entrada estándar. Este comando puede usarse para leer la entrada del 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 un 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 final 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 la 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 hasta cinco valores. Fíjate cómo se comporta read cuando 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'

miércoles, 15 de marzo 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. Es decir, la capacidad de los programas para interactuar con el usuario. Aunque muchos programas no necesitan ser interactivos, algunos programas se benefician de poder aceptar entradas 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 cómo podemos añadir interactividad a nuestros programas.

martes, 14 de marzo de 2017

Para saber más

Hay varias secciones de la man page de bash que ofrecen más detalles de los temas que hemos visto en este capítulo:
  • Listas (trata los operadores de control || y &&)
  • Comandos compuestos (trata [[ ]], (( )) e if)
  • EXPRESIONES CONDICIONALES
  • COMANDOS SHELL BUILTIN (incluye test)
Además, la Wikipedia tiene un buen artículo sobre el concepto de pseudocódigo:
http://en.wikipedia.org/wiki/Pseudocode

lunes, 13 de marzo de 2017

Resumiendo

Hemos comenzado este capítulo con una pregunta. ¿Cómo podríamos hacer que nuestro script sys_info_page detecte si el usuario tiene permisos para leer todos los directorios home? Con nuestro conocimiento de if, podemos resolver el problema añadiendo este código a la función report_home_space:

report_home_space () {
     if [[ $(id -u) -eq 0 ]]; then
          cat <<- _EOF_
               <H2>Home Space Utilization (All Users)</H2>
               <PRE>$(du -sh /home/*)</PRE>
               _EOF_
     else
          cat <<- _EOF_
               <H2>Home Space Utilization ($USER)</H2>
               <PRE>$(du -sh $HOME)</PRE>
          _EOF_
     fi
     return
}

Evaluamos la salida del comando id. Con la opción -u, id muestra el ID numérico del usuario efectivo. El superusuario siempre es cero y cualquier otro usuario es un número mayor de cero. Sabiendo esto, podemos construir dos documentos-aquí diferentes, uno aprovechando los privilegios del superusuario, y el otro, restringido al directorio home del usuario.

Vamos a tomarnos un descanso del programa sys_info_page, pero no te preocupes. Volverá. Mientras, veremos algunos temas que necesitaremos cuando retomemos nuestro trabajo.

viernes, 10 de marzo de 2017

Operadores de control: otra forma de ramificar

bash ofrece dos operadores de control que pueden realizar ramificaciones. Los operadores && (AND) y || (OR) funcionan como los operadores lógicos del comando compuesto [[ ]]. Esta es la sintaxis:

comando1 && comando2

y

comando1 || comando2

Es importante entender su comportamiento. Con el operador &&, se ejecuta comando1, y comando2 se ejecuta si, y sólo si, comando1 es exitoso. Con el operador ||, se ejecuta comando1, y comando2 se ejecuta si, y sólo si, comando1 no es exitoso.

En términos prácticos, esto significa que podemos hacer algo así:

[me@linuxbox ~]$ mkdir temp && cd temp

Esto creará un directorio llamado temp, y si tiene éxito, el directorio de trabajo actual se cambiará a temp. El segundo comando sólo se intenta ejecutar si el comando mkdir tiene éxito. Igualmente, un comando como este:

[me@linuxbox ~]$ [ -d temp ] || mkdir temp

comprobaremos si existe el directorio temp, y sólo si falla el test, se creará el directorio. Este tipo de construcción es muy útil para tratar errores en scripts, un asunto que se tratará más tarde en capítulos posteriores. Por ejemplo, podríamos hacer esto en un script:

[ -d temp ] || exit 1

Si el script requiere el directorio temp, y no existe, entonces el script terminará con un estado de salida de uno.

miércoles, 8 de marzo de 2017

La portabilidad es la pesadilla de las mentes pequeñas

Si hablas con gente del Unix "real", descubrirás rápidamente que a muchos de ellos no les gusta mucho Linux. Lo consideran impuro y sucio. Un principio de los seguidores de Unix es que todo debe ser "portable". Esto significa que cualquier script que escribas debería poder funcionar, sin cambios, en cualquier sistema tipo Unix.

La gente de Unix tiene una buena razón para creer esto. Después de haber visto lo que las extensiones propietarias de los comandos y shells hicieron al mundo Unix antes de POSIX, lógicamente desconfían del efecto de Linux en su amado SO.

Pero la portabilidad tiene un serio inconveniente. Impide el progreso. Requiere que las cosas se hagan siempre usando técnicas de "mínimo común denominador". En el caso de la programación en shell, significa hacerlo todo compatible con sh, el Bourne shell original.

Este inconveniente es la excusa que usan los distribuidores propietarios para justificar sus extensiones propietarias, sólo que las llaman "innovaciones". Pero realmente sólo son dispositivos de bloqueo para sus clientes.

Las herramientas GNU, como bash, no tienen estas restricciones. Consiguen la portabilidad soportando estándares y estando disponibles universalmente. Puedes instalar bash y las otras herramientas GNU en casi todo tipo de sistema, incluso en Windows, sin coste. Así que siéntete libre para usar todas las características de bash. Es realmente portable.

lunes, 6 de marzo de 2017

Combinando expresiones

También es posible combinar expresiones para crear evaluaciones más complejas. Las expresiones se combinan usando operadores lógicos. Los vimos en el Capítulo 17, cuando aprendimos el comando find. Hay tres operaciones lógicas para test y [[ ]]. Son AND, OR y NOT. test y [[ ]] usan operadores diferentes para representar estas operaciones:

Tabla 27-4: Operadores lógicos
Operación test [[ ]] y (( ))
AND -a &&
OR - o | |
NOT ! !

Aquí tenemos un ejemplo de una operación AND. El siguiente script determina si un entero está dentro de un rango de valores:

#!/bin/bash

# test-integer3: determine if an integer is within a
# specified range of values.

MIN_VAL=1
MAX_VAL=100

INT=50

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
     if [[ INT -ge MIN_VAL && INT -le MAX_VAL ]]; then
          echo "$INT is within $MIN_VAL to $MAX_VAL."
     else
          echo "$INT is out of range."
     fi
else
        echo "INT is not an integer." >&2
        exit 1
fi

En este script, determinamos si el valor del entero INT se encuentra entre los valores MIN_VAL y MAX_VAL. Esto se realiza con un sólo uso de [[ ]], que incluye dos expresiones separadas por el operador &&. Podríamos haber realizado este código usando test:

     if [ $INT -ge $MIN_VAL -a $INT -le $MAX_VAL ]; then
          echo "$INT is within $MIN_VAL to $MAX_VAL."
     else
          echo "$INT is out of range."
     fi

El operador de negación ! invierte el resultado de una expresión. Devuelve verdadero si una expresión es falsa, y devuelve falso si una expresión es verdadera. En el siguiente script, modificamos la lógica de nuestra evaluación para encontrar valores de INT que están fuera del rango especificado:

#!/bin/bash

# test-integer4: determine if an integer is outside a
# specified range of values.

MIN_VAL=1
MAX_VAL=100

INT=50

if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
     if [[ ! (INT -ge MIN_VAL && INT -le MAX_VAL) ]]; then
          echo "$INT is outside $MIN_VAL to $MAX_VAL."
     else
          echo "$INT is in range."
     fi
else
        echo "INT is not an integer." >&2
        exit 1
fi

También incluimos paréntesis alrededor de la expresión, para agruparla. Si no se incluyeran, la negación sólo se aplicaría a la primera expresión y no a la combinación de las dos. Para escribir el código con test lo haríamos de esta forma:

if [ ! \( $INT -ge $MIN_VAL -a $INT -le $MAX_VAL \) ]; then
    echo "$INT is outside $MIN_VAL to $MAX_VAL."
else
    echo "$INT is in range."
fi

Como todas las expresiones y operaciones usadas con test se tratan como argumentos de comandos por el shell (al contrario de [[ ]] y (( )) ), los caracteres que tienen un significado especial para bash, como <, >, (, y ), deben ser entrecomillados o escapados.

Viendo que test y [[ ]] hacen más o menos lo mismo, ¿cuál es mejor? test es tradicional (y parte de POSIX), mientras que [[ ]] es especifico para bash. Es importante saber como usar test, ya que su uso está muy expandido, pero [[ ]] es claramente más útil y fácil para escribir código.

viernes, 3 de marzo de 2017

(( )) - Diseñado para enteros

Además del comando compuesto [[ ]], bash también proporciona el comando compuesto (( )), que es útil para operar con enteros. Soporta una serie completa de evaluaciones aritméticas, un asunto que veremos a fondo en el Capítulo 34.

(( )) se usa para realizar pruebas de verdad aritmética. Una prueba de verdad aritmética es verdadera si el resultado de la evaluación aritmética no es cero.

[me@linuxbox ~]$ if ((1)); then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ if ((0)); then echo "It is true."; fi
[me@linuxbox ~]$

Usando (( )), podemos simplificar ligeramente el script test-integer2 así:

#!/bin/bash

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

INT=-5

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

Fíjate que usamos los signos menor-que y mayor-que y que == se usa para comprobar equivalencias. Esta es una sintaxis con un aspecto más natural para trabajar con enteros. Fíjate también, que ya que el comando compuesto (( )) es parte de la sintaxis del shell en lugar de un comando ordinario, y que sólo trabaja con enteros, puede reconocer variables por el nombre y no requiere expansión para hacerlo. Veremos (( )) y la expansión aritmética relacionada más adelante en el Capítulo 34.

miércoles, 1 de marzo de 2017

Una versión más moderna de test

Versiones recientes de bash incluyen un comando compuesto que actúa como reemplazo mejorado de test. Usa la siguiente sintaxis:

[[ expresión ]]

donde, como en test, expresión es una expresión que se evalúa como un resultado verdadero o falso. El comando [[ ]] es muy parecido a test (soporta todas sus expresiones), pero añade una nueva expresión de cadena importante:

cadena1 =~ regex

que devuelve verdadero si cadena1 coincide con la expresión regular extendida regex. Esto abre un montón de posibilidades para realizar tareas tales como validación de datos. En nuestro ejemplo anterior de las expresiones con enteros, el script fallaría si la constante INT contiene cualquier cosa que no sea un entero. El script necesita una forma de verificar si la constante contiene un entero. Usando [[ ]] con el operador de expresión para cadenas  =~, se podría mejorar el script de esta forma:

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

Aplicando la expresión regular, podemos limitar el valor de INT a sólo cadenas que comiencen con un signo menos opcional, seguido de uno o más números. Esta expresión también elimina la posibilidad de valores vacíos.

Otra característica añadida de [[ ]] es que el operador == soporta coincidencias de patrones de la misma forma que lo hace la expansión de nombre de archivo. Por ejemplo:

[me@linuxbox ~]$ FILE=foo.bar
[me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
> echo "$FILE matches pattern 'foo.*'"
> fi
foo.bar matches pattern 'foo.*'

Esto hace a [[ ]] útil para evaluar archivos y rutas.