martes, 3 de octubre de 2017

Control de Flujo: Ramificando con if

En el último capítulo, nos hemos encontrado con un problema. ¿Cómo podemos hacer que nuestro script generador de informes se adapte a los privilegios del usuario que está ejecutando el script? La solución a este problema requerirá que encontremos una forma de "cambiar direcciones" dentro de nuestro script, basándose en el resultado de un test. En términos de programación, necesitamos un programa para ramificar.

Consideremos un ejemplo simple de lógica expresada en pseudocódigo, un lenguaje de ordenador simulado pensado para el consumo humano:

 X = 5
 Si X = 5, entonces:
          Dí “X es igual a 5.”
 Si nó::
          Dí “X no es igual a 5.”

Este es un ejemplo de ramificación. Basada en la condición "¿Es X = 5?" haz una cosa "Dí X es igual a 5", si no haz otra cosa, "Dí X no es igual a 5."


if

Usando el shell, podemos codificar la lógica anterior tal como sigue:

 x=5
 if [ $x -eq 5 ]; then
    echo "x equals 5."
 else
    echo "x does not equal 5."
 fi

o podemos introducirlo directamente en la línea de comandos (lígeramente simplificada):

[me@linuxbox ~]$ x=5
[me@linuxbox ~]$ if [ $x -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
equals 5
[me@linuxbox ~]$ x=0
[me@linuxbox ~]$ if [ $x -eq 5 ]; then echo "equals 5"; else echo "does not equal 5"; fi
does not equal 5

En este ejemplo, ejecutamos el comando dos veces. La primera, con el valor de xestablecido en 5, que da como resultado la cadena "equals 5" como salida, y la segunda vez con el valor de x establecido en 0, que da como resultado la cadena "does not equal 5" como salida.

La sentencia if tiene la siguiente sintaxis:

 if comandos; then
     comandos
 [elif comandos; then
     comandos...]
 [else
     comandos]
 fi

donde comandos es una lista de comandos. Ésto es un poco confuso a primera vista. Pero antes de que podamos aclararlo, tenemos que ver cómo evalúa el shell el éxito o error de un comando.


Estado de salida

Los comandos (incluyendo los scripts y las funciones de shell que escribamos) envían un valor al sistema cuando terminan, llamado estado de salida. Este valor, que es un entero dentro del rango de 0 a 255, indica el éxito o fracaso de la ejecución del comando. Por consenso, un valor de cero indica éxito y cualquier otro valor indica fracaso. El shell proporciona un parámetro que puede usarse para examinar el estado de salida. Aquí lo vemos en acción:

 [me@linuxbox ~]$ ls -d /usr/bin
 /usr/bin
 [me@linuxbox ~]$ echo $?
 0 

 [me@linuxbox ~]$ ls -d /bin/usr
 ls: cannot access /bin/usr: No such file or directory
 [me@linuxbox ~]$ echo $?
 2

En este ejemplo, ejecutamos el comando ls dos veces. La primera vez, el comando se ejecuta con éxito. Si mostramos el valor del parámetro $?, vemos que es un cero. Ejecutamos el comando ls una segunda vez, produciendo un error, y examinamos el parámetro $? otra vez. Esta vez contiene un 2, indicando que el comando ha encontrado un error. Algunos comandos usan estados de salida diferentes para proporcionar diagnósticos de error, mientras que muchos comandos simplemente salen con el valor uno cuando fallan. Las man pages incluyen a menudo una sección llamada "Estado de salida" describiendo que códigos se usan. Sin embargo, un cero siempre indica éxito.

El shell proporciona dos comandos extremadamente simples que no hacen nada excepto terminar con un cero o un uno en el estado de salida. El comando true siempre se ejecuta con éxito y el comando false siempre se ejecuta sin éxito:

 [me@linuxbox ~]$ true
 [me@linuxbox ~]$ echo $?
 0
 [me@linuxbox ~]$ false
 [me@linuxbox ~]$ echo $?
 1

Podemos usar estos comandos para ver como funcionan las sentencias if. Lo que la sentencia if hace en realidad es evaluar el éxito o el fracaso de los comandos:

 [me@linuxbox ~]$ if true; then echo "It's true."; fi
 It's true.
 [me@linuxbox ~]$ if false; then echo "It's true."; fi
 [me@linuxbox ~]$

El comando echo "It's true." se ejecuta cuando el comando que sigue a if se ejecuta con éxito, y no se ejecuta cuando el comando que sigue a if no se ejecuta con éxito. Si es una lista de comandos lo que sigue a if, el que se evalua es el último comando de la lista:

 [me@linuxbox ~]$ if false; true; then echo "It's true."; fi
 It's true.
 [me@linuxbox ~]$ if true; false; then echo "It's true."; fi
 [me@linuxbox ~]$


test

De lejos, el comando usado más frecuentemente con if es test. El comando testrealiza una variedad de comprobaciones y comparaciones. Tiene dos formas equivalentes:

 test expresión

y la más popular:

 [ expresión ]

donde expresión es una expresión que se evalua tanto si es falsa como si es verdadera. El comando test devuelve un estado de salida de cero cuando la expresión es verdadera y de uno cuando la expresión es falsa.

Expresiones para archivo

Las siguientes expresiones se usan para evaluar el estado de los archivos.

Expresiones test para archivos

Es verdadero si:


archivo1 -ef archivo2 
archivo1 y archivo2 tienen los mismos números de inodo (los dos nombres de archivo se refieren al mismo archivo por enlace duro).

archivo1 -nt archivo2 
archivo1 es más nuevo que archivo2.archivo1 -otarchivo2 archivo1 es más antiguo que archivo2.

-b archivo 
archivo existe y es un archivo con bloqueo especial (dispositivo).

-c archivo 
archivo existe y es un archivo de caracter especial (dispositivo).

-d archivo 
archivo existe y es un directorio.

-e archivo 
archivo existe.

-f archivo 
archivo existe y es un archivo normal.

-g archivo 
archivo existe y tiene establecida una ID de grupo.

-G archivo 
archivo existe y su propietario es el ID de grupo efectivo.

-k archivo 
archivo existe y tiene establecido su "sticky bit"

-L archivo 
archivo existe y es un enlace simbólico.

-O archivo 
archivo existe y su propietario es el ID de usuario efectivo.

-p archivo 
archivo existe y es un entubado (pipe) con nombre.

-r archivo 
archivo existe y es legible (tiene permisos de lectura para el usuario efectivo).

-s archivo 
archivo existe y tiene una longitud mayor que cero.

-S archivo 
archivo existe y es una conexión de red

-t fd 
fd es un descritor de archivo dirigido de/hacia el terminal. Puede usarse para determinar que entrada/salida/error estándar está siendo redirigido.

-u archivo 
archivo existe y es setuid

-w archivo 
archivo existe y es editable (tiene permisos de escritura para el usuario efectivo).

-x archivo 
archivo existe y es ejecutable (tiene permisos de ejecución/búsqueda para el usuario efectivo).

Aquí tenemos un script que demuestra algunas de las expresiones para archivo:

 #!/bin/bash


 # test-file: Evaluate the status of a file

 FILE=~/.bashrc
 if [ -e "$FILE" ]; then
    if [ -f "$FILE" ]; then
         echo "$FILE is a regular file."
    fi
    if [ -d "$FILE" ]; then
         echo "$FILE is a directory."
    fi
    if [ -r "$FILE" ]; then
         echo "$FILE is readable."
    fi
    if [ -w "$FILE" ]; then
         echo "$FILE is writable."
    fi
    if [ -x "$FILE" ]; then
         echo "$FILE is executable/searchable."
    fi
 else
    echo "$FILE does not exist"
 exit 1
 fi

 exit

El script evalua el archivo asignado a la constante FILE y muestra su resultado una vez que se realiza la evaluación. Hay dos cosas interesantes a tener en cuenta sobre este script. Primero, fíjate como el parámetro $FILE está entrecomillado junto con las expresiones. Esto no se requiere, pero es una defensa contra un parámetro vacío. Si la expansión de parámetros de $FILE fuera a dar como resultado un valor vacío, podría causar un error (los operadores serían interpretados como cadenas no nulas en lugar de como operadores). Usando las comillas alrededor de los parámetros nos aseguramos que el operador siempre está seguido por una cadena, incluso si la cadena está vacía. Segundo, fíjate en la presencia del comando exit cerca del final del script. El comando exit acepta un único, y opcional, argumento que se convierte en el estado de salida del script. Cuando no se le pasa ningún argumento, el estado de salida por defecto es la salida el último comando ejecutado. Usar exit de esta forma permite que el script indique fallo si $FILE se expande en el nombre de un archivo inexistente. El comando exit que aparece en la última línea del script es una formalidad aquí. Cuando un script "alcanza el final" (llega al final del archivo), termina con un estado de salida del último comando ejecutado por defecto, de todas formas.

Similarmente, las funciones de shell pueden devolver un estado de salida incluyendo un argumento entero al comando return. Si fueramos a convertir el script siguiente en una función para incluirlo en un programa mayor, podríamos reemplazar los comandosexit con sentencias return y obtener el comportamiento deseado:

 test_file () {
   
     # test-file: Evaluate the status of a file

     FILE=~/.bashrc
     

     if [ -e "$FILE" ]; then
          if [ -f "$FILE" ]; then
               echo "$FILE is a regular file."
          fi
          if [ -d "$FILE" ]; then
              

               echo "$FILE is a directory."
          fi
          if [ -r "$FILE" ]; then
               echo "$FILE is readable."
          fi
          if [ -w "$FILE" ]; then
               echo "$FILE is writable."
          fi
          if [ -x "$FILE" ]; then
              echo "$FILE is executable/searchable."
         fi
     else
         echo "$FILE does not exist"
         return 1
     fi

}

Expresiones para cadenas

Las siguientes expresiones se usan para evaluar cadenas:

Expresiones para test de cadenas

Es verdadero si:


cadena 

cadena no es nula.

-n cadena 
La longitud de cadena es mayor que cero.-z cadena La longitud de cadena es cero.

cadena1 = cadena2
cadena1 == cadena2 
cadena1 y cadena2 son iguales. Pueden usarse signos igual simples o dobles, pero es preferible usar dobles signos igual.

cadena1 != cadena2 
cadena1 y cadena2 no son iguales.

cadena1 > cadena2 
cadena1 se ordena detrás de cadena2.

cadena1 < cadena2 
cadena1 se ordena antes de cadena2.


Advertencia: Los operadores de expresión > y < deben ser entrecomillados (o escapados con una barra invertida) cuando se usan con test. Si no se hace esto, serán interpretados por el shell como operadores de redirección, con resultados potencialmente destructivos. Fíjate también que mientras que la documentación bash establece que el ordenado se hace según el locale actual, no lo hace así. Se usa el orden ASCII (POSIX) en versiones de bash hasta la 4.0 incluida.

Aquí tenemos un script que incorpora expresiones para cadenas:

 #!/bin/bash

 # test-string: evaluate the value of a string

 ANSWER=maybe

 if [ -z "$ANSWER" ]; then
     echo "There is no answer." >&2
     exit 1
 fi
 if [ "$ANSWER" = "yes" ]; then
     echo "The answer is YES."
 elif [ "$ANSWER" = "no" ]; then
     echo "The answer is NO."
 elif [ "$ANSWER" = "maybe" ]; then
     echo "The answer is MAYBE."
 else
     echo "The answer is UNKNOWN."
 fi

En este script, evaluamos la constante ANSWER. Primero determinamos si la cadena está vacía. Si es así, terminamos el script y establecemos el estado de salida a uno. Fíjate la redirección que se aplica al comando echo. Redirige el mensaje de error "No hay respuesta." al error estándar, que es lo "apropiado" que hay que hacer con los mensajes de error. Si la cadena no está vacía, evaluamos el valor de la cadena para ver si es igual a "sí", "no" o "quizás". Hacemos esto usando elif, que es la abreviatura para "else if" (y si no). Usando elif, podemos construir un test lógico más complejo.

Expresiones con enteros

Las siguientes expresiones se usan con enteros:

Expresiones de test con enteros

Expresión            Es verdadero si
entero1 -eq entero2  entero1 es igual a entero2.
entero1 -ne entero2  entero1 no es igual a entero2.
entero1 -le entero2  entero1 es menor o igual a entero2.
entero1 -lt entero2  entero1 es menor que entero2.
entero1 -ge entero2  entero1 es mayor o igual a entero2.
entero1 -gt entero2  entero1 es mayor que entero2.

Aquí hay un script que lo demuestra:

 #!/bin/bash

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

 INT=-5

 if [ -z "$INT" ]; then
     echo "INT is empty." >&2
     exit 1
 fi
 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

La parte interesante del script es como determina si es un entero es par o impar. Realizando una operación de módulo 2 con el número, que divide el número en dos y devuelve el resto, puede decirnos si es par o impar.

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 evalúa si un resultado es 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 está marcada 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 excepto un entero. El script necesita una forma de verificar si la constante contiene un entero. Usando [[ ]] con la cadena de operador de expresión =~, 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. 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.

(( )) - 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 veracidad aritmética. Una prueba de veracidad 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 ligéramente 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.

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:

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 simple uso de [[ ]], que incluya 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 la salida 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 agruparlo. 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 (( )) ), que son 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, ¿cual 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.

La portabilidad es el duende 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 como-Unix.

La gente de Unix tiene una buena razón para creer esto. Han visto lo que las extensiones propietarias de los comandos y del shell hicieron al mundo Unix antes de POSIX, están naturalmente escarmentados 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 están capando dispositivos 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.

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 este 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, comando2 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 mkdirtiene éxito. Igualmente, un comando como éste:

 [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 detectar 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.


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 con las ventajas de los privilegios del superusuario, y el otro, restringido a 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.


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 INCLUIDOS EN EL SHELL (incluye test)
  • Además, la Wikipedia tiene un buen artículo sobre el concepto de pseudocódigo:
https://es.wikipedia.org/wiki/Pseudoc%C3%B3digo

No hay comentarios:

Publicar un comentario

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