miércoles, 4 de octubre 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 cual 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.

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 miramos 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.

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:

Ejemplos de patrones de case

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; o sea, para tomar todos los valores no válidos posibles.

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 de "o" condicional. Esto es útil para cosas como manejar caracteres tanto mayúsculas como 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.

Realizando múltiples acciones

En versiones de bash anteriores a la 4.0, case permite que se realice 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.

El script funciona en su mayor parte, pero falla si un carácter coincide en más de una clase de caracter POSIX. Por ejemplo, el carácter "a" es tanto mayúscula como minúscula, así como un dígito hexadecimal. En bash anterior a la versión 4.0 no hay forma de que case coincida 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.

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.

Para saber más


No hay comentarios:

Publicar un comentario

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