lunes, 2 de octubre de 2017

Comenzando un Proyecto

Para comenzar con este capítulo, vamos a empezar a construir un programa. El propósito de este proyecto es ver cómo varias funciones del shell se usan para crear programas y, más importante, para crear buenos programas.

El programa que escribiremos es un generador de informes. Presentará varias estadísticas de nuestro sistema y su estado, y producirá este informe en formato HTML, por lo que podemos verlo en un navegador web como Firefox y Chrome.


Los programas a menudo se construyen en una serie de etapas, donde cada etapa añade funciones y capacidades. La primera etapa de nuestro programa producirá una página HTML muy minimalista que no contiene información del sistema. Eso vendrá luego.

Primera etapa: Documento minimalista

Lo primero que necesitamos saber es el formato de un documento HTML bien construido. Tiene esta pinta:

 <HTML>
    <HEAD>
       <TITLE>Page Title</TITLE>
    </HEAD>
    <BODY>
       Page body.
    </BODY>
 </HTML>

Si introducimos esto en nuestro editor de texto y guardamos el archivo como foo.html, podemos usar la siguiente URL en Firefox para ver el archivo:

 file:///home/username/foo.html

La primera etapa de nuestro programa será capaz de enviar este archivo HTML a la salida estándar. Podemos escribir un programa que haga ésto muy fácilmente. Arranquemos nuestro editor de texto y creemos un nuevo archivo llamado ~/bin/sys_info_page:

 [me@linuxbox ~]$ vim ~/bin/sys_info_page

e introducimos el siguiente programa:

 #!/bin/bash


 # Program to output a system information page

 echo "<HTML>"
 echo " <HEAD>"
 echo " <TITLE>Page Title</TITLE>"
 echo " </HEAD>"
 echo " <BODY>"
 echo " Page body."
 echo " </BODY>"
 echo "</HTML>"


Nuestro primer acercamiento a este problema contiene un shebang, un comentario (siempre es una buena idea) y una secuencia de comandos echo, uno por cada línea de salida. Tras guardar el archivo, lo haremos ejecutable y trataremos de ejecutarlo:

 [me@linuxbox ~]$ chmod 755 ~/bin/sys_info_page
 [me@linuxbox ~]$ sys_info_page

Cuando el programa se ejecute, deberíamos ver el texto del documento HTML mostrado en la pantalla, ya que los comandos echo del script envían su salida a la salida estándar. Ejecutaremos el programa de nuevo y redirigiremos la salida del programa al archivosys_info_page.html, con lo que podemos ver el resultado con un navegador web:

 [me@linuxbox ~]$ sys_info_page > sys_info_page.html
 [me@linuxbox ~]$ firefox sys_info_page.html

Hasta aquí todo bien.

Cuando escribimos programas, siempre es una buena idea esforzarse en que sean simples y claros. El mantenimiento es más fácil cuando el programa es fácil de leer y comprender, no hace falta mencionar que podemos hacer que el programa sea más fácil de realizar reduciendo la cantidad de escritura. Nuestra versión actual del programa funciona bien, pero podría ser más simple. Podríamos en realidad combinar todos los comandos echo en uno, lo que ciertamente haría más fácil añadir más líneas a la salida del programa. Así que, hagamos ese cambio en nuestro programa:

#!/bin/bash


# Program to output a system information page


 echo "<HTML>
    <HEAD>
       <TITLE>Page Title</TITLE>
    </HEAD>
    <BODY>
       Page body.
    </BODY>
 </HTML>"


Una cadena entre comillas puede incluir nuevas líneas, y por lo tanto contener múltiples líneas de texto. El shell seguirá leyendo el texto hasta que se encuentre las comillas de cierre. Funciona de esta forma también en la línea de comandos:

 [me@linuxbox ~]$ echo "<HTML>
 >         <HEAD>
 >                <TITLE>Page Title</TITLE>
 >         </HEAD>
 >         <BODY>
 >                Page body.
 >         </BODY>
 >         </HTML>"


El carácter ">" que va delante es el prompt de shell contenido en la variable de shell PS2. Aparece siempre que escribamos una sentencia multilínea en el shell. Esta función es un poco oscura ahora mismo, pero más adelante, cuando veamos las sentencias de programación multilínea, se transformará en algo más útil.


Segunda etapa: Añadiendo algunos datos

Ahora que nuestro programa puede generar un documento minimalista, pongamos algunos datos en el informe. Para hacerlo, haremos los siguientes cambios:

 #!/bin/bash


 # Program to output a system information page


 echo "<HTML>
         <HEAD>
                  <TITLE>System Information Report
</TITLE>
         </HEAD>
         <BODY>
                  <H1>System Information Report</H1>
         </BODY>
         </HTML>"

Hemos añadido un título de página y un encabezado al cuerpo del informe.

Variables y constantes

Sin embargo, hay un problema con nuestro script. ¿Ves cómo la cadena "System Information Report" está repetida? Con nuestro diminuto script no es un problema, pero imaginemos que nuestro script fuera realmente largo y que tuviéramos múltiples instancias de esta cadena. Si quisiéramos cambiar el título a otra cosa, habríamos tenido que cambiarlo en múltiples sitios, lo que podría ser mucho trabajo. ¿Y si pudiéramos arreglar el script para que la cadena sólo apareciera una vez y no múltiples veces? Podría hacer que el mantenimiento en el futuro del script sea más fácil. Aquí tenemos cómo podríamos hacerlo:

 #!/bin/bash


 # Program to output a system information page

title="System Information Report"

echo "<HTML>
        <HEAD>
                <TITLE>$title</TITLE>
        </HEAD>
        <BODY>
                <H1>$title</H1>
        </BODY>
     </HTML>"

Creando una variable llamada title y asignándole el valor "System Information Report", podemos sacar ventaja de la expansión de parámetros y colocar la cadena en múltiples lugares.

Pero ¿cómo creamos una variable? Simplemente, la usamos. Cuando el shell encuentra una variable, automáticamente la crea. Esto difiere de muchos lenguajes de programación en los que las variables deben ser declaradas o definidas explícitamente antes de usarla. El shell es muy laxo en esto, lo que puede causar algunos problemas. Por ejemplo, considera este escenario ejecutado en la línea de comandos:

 [me@linuxbox ~]$ foo="yes"
 [me@linuxbox ~]$ echo $foo
 yes
 [me@linuxbox ~]$ echo $fool

 [me@linuxbox ~]$

Primero asignamos el valor "yes" a la variable foo, y luego mostramos su valor con echo. A continuación mostramos el valor de la variable mal escrita como "fool" y tenemos un resultado en blanco. Esto es porque el shell ha creado felizmente la variable fool cuando la encontró, y le ha dado el valor por defecto de nada, o vacía. Con esto, aprendemos que ¡debemos prestar mucha atención a nuestra escritura! También es importante entender que ha ocurrido realmente en este ejemplo. De nuestro vistazo previo a cómo el shell realiza las expansiones, sabemos que el comando:

 [me@linuxbox ~]$ echo $foo

se somete a expansión de parámetros resultando en:

 [me@linuxbox ~]$ echo yes

Mientras que el comando:

 [me@linuxbox ~]$ echo $fool

se expande a:

 [me@linuxbox ~]$ echo

¡La variable vacía se expande en nada! Esto puede causar estragos en comandos que requieren argumentos. Aquí tenemos un ejemplo:

 [me@linuxbox ~]$ foo=foo.txt
 [me@linuxbox ~]$ foo1=foo1.txt
 [me@linuxbox ~]$ cp $foo $fool
 cp: missing destination file operand after `foo.txt'
 Try `cp --help' for more information.

Asignamos valores a dos variables, foo y foo1. Cuando realizamos un cp, pero escribimos mal el nombre del segundo argumento. Tras la expansión, al comando cpsólo se le envía un argumento, aunque requiere dos.

Hay algunas reglas sobre los nombres de variables:

Los nombres de variables pueden consistir en caracteres alfanuméricos (letras y números) y caracteres guión bajo.

El primer carácter de un nombre de variable puede ser tanto una letra o un guión bajo.

Los espacios y símbolos de puntuación no están permitidos.
La palabra "variable" implica un valor que cambia, y en muchas aplicaciones, las variables se usan de esta forma. Sin embargo, la variable de nuestra aplicación, title, se usa como una constante. Una constante es casi como una variable ya que tiene un nombre y contiene un valor. La diferencia es que el valor de una constante no cambia. En una aplicación que realice cálculos geométricos, podríamos definir PI como una constante, y asignarle el valor de 3,1415, en lugar de usar el número literalmente a lo largo de nuestro programa. El shell no hace distinción entre constantes y variables; es más bien a conveniencia del programador. Una convención común es usar letras mayúsculas para designar contantes y minúsculas para variables. Modificaremos nuestro script para cumplir con esta convención:

 #!/bin/bash

 # Program to output a system information page
 TITLE="System Information Report For $HOSTNAME"

 echo "<HTML>
         <HEAD>
                 <TITLE>$TITLE</TITLE>
         </HEAD>
         <BODY>
                 <H1>$TITLE</H1>
         </BODY>
 </HTML>"

Tenemos la oportunidad de mejorar nuestro título añadiendo el valor de la variable de shell HOSTNAME. Es el nombre de red de la máquina.

Nota: El shell en realidad proporciona una forma de forzar la inmutabilidad de las constantes, a través del uso del comando incluido declare con la opción -r (sólo lectura). Si hemos asignado TITTLE de esta forma:

 declare -r TITLE="Page Title"

el shell prevendrá cualquier asignación posterior a TITTLE. Esta característica es poco usada, pero existe en scripts muy formales.

Asignando valores a variables y constantes

Aquí es donde nuestro conocimiento de las expansiones empieza a dar resultados. Como hemos visto, a las variables se le asignan valores así:

 variable=valor

donde variable es el nombre de la variable y valor es una cadena. Al contrario de otros lenguajes de programación, el shell no se preocupa por el tipo de datos asignados a una variable; los trata a todos como cadenas. Puedes forzar al shell a restringir la asignación a enteros usando el comando declare con la opción -i, pero como el configurar las variables como sólo lectura, no se suele hacer.

Fíjate que en una asignación, no debe haber espacios entre el nombre de la variable, el signo igual, y el valor. Entonces ¿en qué puede consistir el valor? Cualquier cosa que pueda expandirse en una cadena:

 a=z                 # Assign the string "z" to variable a.
 b="a string"        # Embedded spaces must be within quotes.
 c="a string and $b" # Other expansions such as variables can
                     # 
be expanded into the assignment.
 d=$(ls -l foo.txt)  # Results of a command.
 e=$((5 * 7))        # Arithmetic expansion.
 f="\t\ta string\n"  # Escape sequences such as tabs 

                     # and newlines.

Pueden hacerse múltiples asignaciones de variables en una única línea:

 a=5 b="a string"

Durante la expansión, los nombres de variables pueden ir rodeados de llaves opcionales "{}". Ésto es útil en casos donde una variable puede llegar a ser ambigua para su contexto próximo. Aquí, tratamos de cambiar el nombre de un archivo de myfile a myfile1, usando una variable:

 [me@linuxbox ~]$ filename="myfile"
 [me@linuxbox ~]$ touch $filename
 [me@linuxbox ~]$ mv $filename $filename1
 mv: missing destination file operand after `myfile'
 Try `mv --help' for more information.

Este intento falla porque el shell interpreta el segundo argumento del comando mv como una variable nueva (y vacía). El problema puede solucionarse de esta forma:

 [me@linuxbox ~]$ mv $filename ${filename}1

Añadiendo las llaves en los extremos, el shell ya no interpreta el 1 del final como parte del nombre de la variable.

Aprovecharemos esta oportunidad para añadir algunos datos a nuestro informe, digamos la fecha y hora en que fue creado el informe y el nombre de usuario del creador:

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

 echo "<HTML>
         <HEAD>
                 <TITLE>$TITLE</TITLE>
       </HEAD>
       <BODY>
                 <H1>$TITLE</H1>
                 <P>$TIMESTAMP</P>
       </BODY>
 </HTML>"


Documentos-aquí

Hemos visto dos métodos distintos de mostrar nuestro texto, ambos usando el comando echo. Hay una tercera forma llamada documento-aquí o script-aquí. Un documento-aquí es una forma adicional de redirección I/O en la que incluimos un cuerpo de texto en nuestro script y lo introducimos en la entrada estándar de un comando. Funciona así:

comando << token
texto
token

donde comando es el nombre de un comando que acepta la entrada estándar y token es una cadena usada para indicar el final del texto incluido. Modificaremos nuestro script para usar un doucmento-aquí:

 #!/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>
         </BODY>
 </HTML>
 _EOF_

En lugar de usar echo, nuestro script ahora usa cat y un documento-aquí. La cadena _EOF_ (que significa "End Of File" o "Fin del archivo", una convención común) ha sido seleccionada como token, y marca el final del texto incluido. Fíjate que el token debe aparecer sólo y no debe haber espacios delante en la línea.

Entonces ¿cual es la ventaja de usar un documento-aquí? Es principalmente lo mismo que echo, excepto que, por defecto, las comillas sencillas y dobles dentro de los documentos-aquí pierden su significado especial para el shell. Aquí hay un ejemplo en la línea de comandos:

 [me@linuxbox ~]$ foo="some text"
 [me@linuxbox ~]$ cat << _EOF_
 > $foo
 > "$foo"
 > '$foo'
 > \$foo
 > _EOF_

 some text
 "some text"
 'some text'
 $foo

Como podemos ver, el shell no presta atención a las comillas. Las trata como caracteres ordinarios. Esto nos permite incluir comillas libremente dentro de un documento-aquí. Podría ser útil para nuestro programa de informes.

Los documentos-aquí pueden usarse con cualquier comando que acepte entrada estándar. En este ejemplo, usamos un documento-aquí para pasar una serie de comandos al programa ftp para descargar un archivo de un servidor FTP remoto:

#!/bin/bash

# Script to retrieve a file via FTP

FTP_SERVER=ftp.nl.debian.org
FTP_PATH=/debian/dists/lenny/main/installer-i386/current/images/cdrom
REMOTE_FILE=debian-cd_info.tar.gz

ftp -n << _EOF_
open $FTP_SERVER
user anonymous me@linuxbox
cd $FTP_PATH
hash
get $REMOTE_FILE
bye
_EOF_
ls -l $REMOTE_FILE

Si cambiamos el operador de redirección de "<<" a "<<-" el shell ignorará las caracteres tabulador del principio del documento-aquí. Esto permite que un documento-aquí sea indentado, lo que puede mejorar su legibilidad:

#!/bin/bash

# Script to retrieve a file via FTP

FTP_SERVER=ftp.nl.debian.org
FTP_PATH=/debian/dists/lenny/main/installer-i386/current/images/cdrom
REMOTE_FILE=debian-cd_info.tar.gz

ftp -n <<- _EOF_
   open $FTP_SERVER
   user anonymous me@linuxbox
   cd $FTP_PATH
   hash
   get $REMOTE_FILE
   bye
   _EOF_


ls -l $REMOTE_FILE

Resumiendo

En este capítulo, hemos comenzado un proyecto que nos llevará a través del proceso de construcción de un script exitoso. Hemos presentado el concepto de variables y constantes y como pueden utilizarse. Son la primera de las muchas aplicaciones que encontraremos para expansión de parámetros. También hemos visto como producir salida de nuestro script, y varios métodos para incluir bloques de texto.

Para saber más

  • La man page de bash incluye una sección titulada "Documentos-aquí", que tiene una descripción completa de esta característica.

No hay comentarios:

Publicar un comentario

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