martes, 3 de octubre de 2017

Diseño de Arriba a Abajo

A medida que los programas se hacen más grandes y complejos, se hacen más difíciles de diseñar, codificar y mantener. Al iguar que con cualquier gran proyecto, es a menudo una buena idea dividir las tareas grandes y complejas en una serie de tareas simples y pequeñas. Imaginemos que estamos intentando describir una tarea común de todos los días, ir al mercado a comprar comida, para una persona de Marte. Describiríamos el proceso completo como la siguiente serie de pasos:
  1. Subir al coche
  2. Conducir hasta el mercado
  3. Aparcar el coche
  4. Entrar en el mercado
  5. Comprar comida
  6. Volver al coche
  7. Conducir a casa
  8. Aparcar el coche
  9. Entrar en casa
Sin embargo una persona de Marte seguro que necesita más detalles. Podríamos dividir aún más la subtarea "Aparcar el coche" en una serie de pasos:
  1. Encontrar un sitio para aparcar
  2. Meter el coche dentro de ese sitio
  3. Apagar el motor
  4. Poner el freno de mano
  5. Salir del coche
  6. Cerrar el coche
La subtarea "Apagar el motor" podría dividirse aún más en pasos incluyendo "Apagar el contacto", "Sacar la llave", y así sucesivamente, hasta que cada paso del proceso completo de ir al mercado hubiera sido completamente definido.

El proceso de identificar los pasos de alto nivel y desarrollar incrementalmente vistas detalladas de dichos pasos se llama diseño de arriba a abajo. Esta técnica nos permite romper tareas grandes y complejas en muchas tareas pequeñas y simples. El diseño de arriba a abajo es un método común de diseñar programas y uno de los que se ajustan muy bien a la programación en shell en particular.

En este capítulo, usaremos el diseño de arriba a abajo para seguir desarrollando nuestro script generador de informes.


Funciones de shell

Nuestro script actualmente realiza los siguientes pasos para generar el documento HTML:

  1. Abre la página.
  2. Abre el encabezado de página.
  3. Establece el título de página.
  4. Cierra el encabezado de página.
  5. Abre el cuerpo de la página.
  6. Muestra el encabezado de la página.
  7. Muestra la hora.
  8. Cierra el cuerpo de la página.
  9. Cierra la página.
Para nuestra próxima fase de desarrollo, añadiremos algunas tareas entre los pasos 7 y 8. Estos incluirán:
  • Hora de encendido y carga del sistema. Es la cantidad de tiempo desde el último encendido o reinicio y el número medio de tareas corriendo actualmente en el procesador en varios intervalos de tiempo.
  • Espacio en disco. El uso total de espacio en los dispositivos del sistema de almacenamiento.
  • Espacio Home. La cantidad de espacio de almacenamiento usado por cada usuario.
Si tuviéramos un comando para cada una de dichas tareas, podríamos añadirlos a nuestro script simplemente a través de sustitución de comandos:

 #!/bin/bash

 # Program to output a system information page

 TITLE="System Information Report For $HOSTNAME"
 CURRENT_TIME=$(date +"%x %r %Z")
 TIMESTAMP="Generated $CURRENT_TIME, by $USER"

 cat << _EOF_
 <HTML>
         <HEAD>
                <TITLE>$TITLE</TITLE>
         </HEAD>
         <BODY>
                <H1>$TITLE</H1>
                <P>$TIMESTAMP</P>
                $(report_uptime)
                $(report_disk_space)
                $(report_home_space)
         </BODY>
 </HTML>
 _EOF_

Podríamos crear estos comandos adicionales de dos formas. Podríamos escribir tres scripts separados y colocarlos en un directorio listado en nuestro PATH, o podríamos incluir los scripts dentro de nuestro programa como funciones de shell. Como hemos mencionado antes, las funciones de shell son "mini-scripts" que se colocan dentro de otros scripts y pueden actuar como programas autónomos. Las funciones de shell tienen dos formas sintácticas:

 function name {
      commands
      return
 }

y

 name () {
      commands
      return
 }

donde name es el nombre de la función y commands es una serie de comandos contenidos dentro de la función. Ambas formas son equivalentes y podrían usarse indistintamente. A continuación vemos un script que demuestra el uso de una función de shell:

  1 #!/bin/bash
  2
  3 # Shell function demo
  4
  5 function funct {
  6      echo "Step 2"
  7      return
  8 }
  9
 10 # Main program starts here
 11
 12 echo "Step 1"
 13 Step 2
 14 echo "Step 3"

A medida que el shell lee el script, va pasando por alto de la línea 1 a la 11, ya que estas líneas son comentarios y la definición de la función. La ejecución comienza en la línea 12 con un comando echo. La línea 13 llama a la función de shell funct y el shell ejecuta la función tal como haría con cualquier otro comando. El control del programa se mueve entonces a la línea 6, y se ejecuta el segundo comando echo. La línea 7 se ejecuta a continuación. Su comando return finaliza la función y devuelve el control al programa en la línea que sigue a la función call (línea 14), y el comando final echo se ejecuta. Fíjate que, para que las llamadas a funciones sean reconocidas como funciones de shell y no interpretadas como nombres de programas externos, las definiciones de funciones de shell deben aparecer antes de que sean llamadas.

Añadiremos unas definiciones de funciones mínimas a nuestro script:

 #!/bin/bash

 # Program to output a system information page

 TITLE="System Information Report For $HOSTNAME"
 CURRENT_TIME=$(date +"%x %r %Z")
 TIMESTAMP="Generated $CURRENT_TIME, by $USER"

 report_uptime () {
    return
 }

 report_disk_space () {
    return
 }

 report_home_space () {
    return
 }

 cat << _EOF_
 <HTML>
     <HEAD>
          <TITLE>$TITLE</TITLE>
     </HEAD>
     <BODY>
          <H1>$TITLE</H1>
          <P>$TIMESTAMP</P>
          $(report_uptime)
          $(report_disk_space)
          $(report_home_space)
    </BODY>
 </HTML>
 _EOF_

Los nombres de funciones de shell siguen las mismas reglas que las variables. Una función debe contener al menos un comando. El comando return (que es opcional) satisface este requerimiento.

Variables locales

En los scripts que hemos escrito hasta ahora, todas las variables (incluyendo las constantes) han sido variables globales. Las variables globales siguen existiendo a lo largo del programa. Esto es bueno para muchas cosas, pero, a veces, puede complicar el uso de las funciones de shell. Dentro de las funciones de shell, a menudo es preferible tener variables locales. Las variables locales solo son accesibles dentro de la función de shell en la que han sido definidas y dejan de existir una vez que la función de shell termina.

Tener variables locales permite al programador usar variables con nombres que pueden existir anteriormente, tanto en el script globalmente o en otras funciones de shell, si tener que preocuparnos por potenciales confilctos de nombres.

Aquí hay un ejemplo de script que demuestra como se definen y usan las variables locales:

 #!/bin/bash

 # local-vars: script to demonstrate local variables


 foo=0   # global variable foo

 funct_1 () {

    local foo # variable foo local to funct_1
    foo=1
    echo "funct_1: foo = $foo"
 }

 funct_2 () {
    local foo # variable foo local to funct_2
    foo=2
    echo "funct_2: foo = $foo"
 }

 echo "global: foo = $foo"
 funct_1
 echo "global: foo = $foo"
 funct_2
 echo "global: foo = $foo"

Como podemos ver, las variables locales se definen precediendo al nombre de la variable con la palabra local. Ésto crea una variable que es local para la función de shell en la que se define. Una vez fuera de la función de shell, la variable deja de existir. Cuando ejecutamos este script, vemos los resultados:

 [me@linuxbox ~]$ local-vars
 global:  foo = 0
 funct_1: foo = 1
 global:  foo = 0
 funct_2: foo = 2
 global:  foo = 0

Vemos que la asignación de valores a la variable local foo dentro de ambas funciones de shell no tiene efecto en el valor de foo definido fuera de las funciones.

Esta característica permite que las funciones de shell sean escritas de forma que se mantengan independientes una de otra y del script en el que aparecen. Esto es muy valioso, ya que ayuda a prevenir que una parte del programa interfiera en otra. También permite que las funciones de shell sean escritas de forma que sean portables. O sea, pueden cortarse y pegarse de un script a otro, como sea necesario.

Mantener los scripts ejecutándose

Mientras que desarrollamos nuestro programa, es útil mantener el programa en estado ejecutable. Haciendo esto, y probándolo frecuentemente, podemos detectar errores pronto en el proceso de desarrollo. Esto hará los problemas de depurado más fáciles. Por ejemplo, si ejecutamos el programa, hacemos un pequeño cambio, y ejecutamos el programa de nuevo, es muy probable que el cambio más reciente sea la fuente del problema. Añadiendo las funciones vacías, llamadas stubs en el habla de los programadores, podemos verificar el flujo lógico de nuestro programa en una fase temprana. Cuando construimos un stub, es una buena idea incluir algo que proporcione retroalimentación al programador, que muestre que el flujo lógico se está realizando. Si miramos la salida de nuestro script ahora:

 [me@linuxbox ~]$ sys_info_page
 <HTML>
     <HEAD>
          <TITLE>System Information Report For twin2</TITLE>
    </HEAD>
    <BODY>
          <H1>System Information Report For linuxbox</H1>
          <P>Generated 03/19/2009 04:02:10 PM EDT, by me</P>



    </BODY>
 </HTML>

vemos que hay algunas líneas en blanco en nuestra salida tras la marca de tiempo, pero no podemos estár seguros de cual es la causa. Si cambiamos las funciones para incluir algo de retroalimentación:

 report_uptime () {
         echo "Function report_uptime executed."
         return
 }


 report_disk_space () {
         echo "Function report_disk_space executed."
         return
 }

 report_home_space () {
         echo "Function report_home_space executed."
         return
 }

y ejecutamos el script de nuevo:

 [me@linuxbox ~]$ sys_info_page
 <HTML>
     <HEAD>
          <TITLE>System Information Report For linuxbox</TITLE>
     </HEAD>
     <BODY>
          <H1>System Information Report For linuxbox</H1>
          <P>Generated 03/20/2009 05:17:26 AM EDT, by me</P>
          Function report_uptime executed.
          Function report_disk_space executed.
          Function report_home_space executed.
     </BODY>
 </HTML>

ahora vemos, de hecho, que nuestras tres funciones se están ejecutando.

Con nuestra estrucutra de funciones en su sitio y funcionando, es hora desarrollar el código de nuestras funciónes. Primero, la función report_uptime:

 report_uptime () {
     cat <<- _EOF_
          <H2>System Uptime</H2>
          <PRE>$(uptime)</PRE>
          _EOF_

     return
 }

Es bastante sencilla. Usamos un documento-aquí para mostrar un encabezado de sección y la salida del comando uptime, rodeándolos de etiquetas <PRE> para evitar el formateo del comando. La función report_disk_space es similar:

 report_disk_space () {
     cat <<- _EOF_
          <H2>Disk Space Utilization</H2>
          <PRE>$(df -h)</PRE>
          _EOF_

     return
 }

Esta función usa el comando df -h para determinar la cantidad de espacio en disco. Finalmente, construiremos la función report_home_space:

 report_home_space () {
     cat <<- _EOF_
          <H2>Home Space Utilization</H2>
          <PRE>$(du -sh /home/*)</PRE>
          _EOF_
   
  return
 }

Usamos el comando du con las opciones -sh para realizar esta tarea. Esto, sin embargo, no es una solución completa al problema. Aunque funcionará en algunos sitemas (Ubuntu, por ejemplo), no funcionará en otros. La razón es que muchos sitemas establecen los permisos de los directorios home para prevenir que sean legibles por todo el mundo, lo que es una medida de seguridad razonable. En estos sistemas, la función report_home_space, tal como está escrita, sólo funcionará si nuestro script se ejecuta con privilegios de superusuario. Un solución mejor sería hacer que el script ajuste su comportamiento según los privilegios del usuario. Lo veremos en el próximo capítulo.

Funciones shell en su archivo .bashrc

Las funciones de shell hacen excelentes reemplazos para los alias, y son en realidad el método preferido de crear pequeños comandos para uso personal. Los alias están muy limitados por el tipo de comandos y características de shell que soportan, mientras que las funciones de shell permiten cualquier cosa que pueda ser incluida en un script. Por ejemplo, si nos gusta la función shell report_disk_space que hemos desarrollado en nuestro script, podríamos crear una función similar llamada ds para nuestro archivo .bashrc:

 ds () {
    echo “Disk Space Utilization For $HOSTNAME”
    df -h
 }


Resumiendo

En este capítulo, hemos presentado un método común de diseño de programas llamado diseño de arriba a abajo, y hemos visto cómo las funciones de shell se usan para construir el refinamiento paso a paso que requiere. También hemos visto cómo las variables locales pueden usarse para hacer a las funciones de shell independientes unas de otras y del programa en las que se encuentran. Esto hace posible que las funciones de shell sean escritas de una forma portable y que sean reutilizables permitiendo que se coloquen en múltiples programas; un gran ahorro de tiempo.

Para saber más

No hay comentarios:

Publicar un comentario

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