miércoles, 4 de octubre de 2017

Parámetros Posicionales

Una prestación que ha estado desaparecida en nuestros programas es la capacidad de aceptar y procesar opciones y argumentos de la línea de comandos. En este capítulo, examinaremos las funcionalidades del shell que permiten que nuestros programas accedan al contenido de la línea de comandos.

Accediendo a la línea de comandos


El shell ofrece una serie de variables llamadas parámetros posicionales que contienen las palabras individuales de la línea de comandos. Las variables se nombran del 0 al 9. Puede comprobarse así:

 #!/bin/bash


 # posit-param: script to view command line parameters

 echo "
 \$0 = $0
 \$1 = $1
 \$2 = $2
 \$3 = $3
 \$4 = $4
 \$5 = $5
 \$6 = $6
 \$7 = $7
 \$8 = $8
 \$9 = $9
 "

Un script muy simple que muestre el valor de las variables $0-$9. Cuando lo ejecutamos sin argumentos de la línea de comandos:

 [me@linuxbox ~]$ posit-param

 $0 = /home/me/bin/posit-param
 $1 =
 $2 =
 $3 =
 $4 =
 $5 =
 $6 =
 $7 =
 $8 =
 $9 =

Incluso cuando no se le dan argumentos, $0 siempre contendrá el primer elemento que aparezca en la línea de comandos, que es la ruta del programa que se está ejecutando. Cuando se le pasan argumentos, vemos el resultado:

 [me@linuxbox ~]$ posit-param a b c d

 $0 = /home/me/bin/posit-param
 $1 = a
 $2 = b
 $3 = c
 $4 = d
 $5 =
 $6 =
 $7 =
 $8 =
 $9 =

Nota: En realidad puedes acceder a más de nueve parámetros usando la expansión de parámetros. Para especificar un número mayor que nueve, incluimos el número entre llaves. Por ejemplo ${10}, ${55}, ${211} y así sucesivamente.


Determinando el número de argumentos

El shell también ofrece una variable, $#, que muestra el número de argumentos en la línea de comandos:

 #!/bin/bash

 # posit-param: script to view command line parameters

 echo "
 Number of arguments: $#
 \$0 = $0
 \$1 = $1
 \$2 = $2
 \$3 = $3
 \$4 = $4
 \$5 = $5
 \$6 = $6
 \$7 = $7
 \$8 = $8
 \$9 = $9
 "

El resultado:

 [me@linuxbox ~]$ posit-param a b c d
 Number of arguments: 4
 $0 = /home/me/bin/posit-param
 $1 = a
 $2 = b
 $3 = c
 $4 = d
 $5 =
 $6 =
 $7 =
 $8 =
 $9 =

shift - Accediendo a muchos argumentos

Pero, ¿qué ocurre cuando le damos al programa un gran número de argumentos como en el ejemplo siguiente?:

 [me@linuxbox ~]$ posit-param *


 Number of arguments: 82
 $0 = /home/me/bin/posit-param
 $1 = addresses.ldif
 $2 = bin
 $3 = bookmarks.html
 $4 = debian-500-i386-netinst.iso
 $5 = debian-500-i386-netinst.jigdo
 $6 = debian-500-i386-netinst.template
 $7 = debian-cd_info.tar.gz
 $8 = Desktop
 $9 = dirlist-bin.txt

En este ejemplo, el comodín * se expande en 82 argumentos. ¿Como podemos procesar tal cantidad?
El shell ofrece un método, aunque de mala gana, para hacerlo. El comando shift hace que todos los parámetros "bajen uno" cada vez que se ejecute. De hecho, usando shift, es posible hacerlo con sólo un parámetro (además de $0, que nunca cambia):

 #!/bin/bash

 # posit-param2: script to display all arguments

 count=1

 while [[ $# -gt 0 ]]; do
     echo "Argument $count = $1"
     count=$((count + 1))
     shift
 done

Cada vez que se ejecuta shift, el valor de $2 se mueve a $1, el valor de $3 se mueve a $2 y así sucesivamente. El valor de $# se reduce también en uno.

En el programa posit-param2, creamos un bucle que evaluaba el número de argumentos restantes y continuaba hasta que quedara al menos uno. Mostramos el argumento actual, incrementeamos la variable count con cada iteración del bucle para conseguir un contador del número de argumentos procesados y, finalmente, ejecutamos un shift para cargar $1 con el siguiente argumento. Aquí está el programa en funcionamiento:

 [me@linuxbox ~]$ posit-param2 a b c d
 Argument 1 = a
 Argument 2 = b
 Argument 3 = c
 Argument 4 = d


Aplicaciones simples

Incluso sin shift, es posible escribir aplicaciones útiles usando parámetros posicionales. A modo de ejemplo, aquí tenemos un programa de información de archivo simple:

 #!/bin/bash

 # file_info: simple file information program

 PROGNAME=$(basename $0)

 if [[ -e $1 ]]; then
     echo -e "\nFile Type:"
     file $1
     echo -e "\nFile Status:"
     stat $1
 else
     echo "$PROGNAME: usage: $PROGNAME file" >&2
     exit 1
 fi

Este programa muestra el tipo de archivo (determinado por el comando file) y el estado del archivo (del comando stat) de un archivo especificado. Una función interesante de este programa es la variable PROGNAME. Se le da el valor resultante del comando basename $0. El comando basename elimina la parte delantera de una ruta, dejando sólo el nombre base de un archivo. En nuestro ejemplo, basenameelimina la parte de la ruta contenida en el parámetro $0, la ruta completa de nuestro programa de ejemplo. Este valor es útil cuando construimos mensajes tales como el mensaje de uso al final del programa. Escribiendo el código de esta forma, el script puede ser renombrado y el mensaje se ajusta automáticamente para contener el nombre del programa.


Usando parámetros posicionales con funciones de shell

Además de usar los parámetros posicionales para pasar argumentos a scripts de shell, pueden usarse para pasar argumentos a funciones de shell. Para comprobarlo, convertiremos el script file_info en una función de shell:

 file_info () {

    # file_info: function to display file information

    if [[ -e $1 ]]; then
        echo -e "\nFile Type:"
        file $1
        echo -e "\nFile Status:"
        stat $1
    else
        echo "$FUNCNAME: usage: $FUNCNAME file" >&2
        return 1
    fi
 }

Ahora, si un script que incorpora la función de shell file_info llama a la función con un argumento de nombre de archivo, el argumento pasará a la función.

Con esta capacidad, podemos escribir muchas funciones de shell útiles que no sólo pueden ser usadas con scripts, si no también dentro del archivo .bashrc

Fíjate que la variable PROGNAME ha cambiado a la variable de shell FUNCNAME. El shell actualiza automáticamente esta variable para mantener el control de la función de shell ejecutada actualmente. Fíjate que $0 siempre contiene la ruta completa del primer elemento de la linea de comandos (p.ej., el nombre del programa) y no contiene el nombre de la función de shell como podríamos esperar.

Manejando parámetros posicionales en masa

A veces es útil manejar todos los parámetros posicionales como un grupo. Por ejemplo, podríamos querer escribir un "envoltorio" alrededor de otro programa. Esto significa que creamos un script o función de shell que simplifique la ejecución de otro programa. El envoltorio proporciona una lista de opciones arcanas de la línea de comandos y luego pasa una lista de argumentos al programa de menor nivel.

El shell ofrece dos parámetros especiales para este propósito. Ambos se expanden en la lista completa de parámetros posicionales, pero difieren en cosas muy sutiles. Son:

Los parámetros especiales * y @

$* 
Se expande en la lista de parámetros posicionales, comenzando por 1. Cuando lo incluimos en comillas dobles, se expande en una cadena con comillas dobles conteniendo todos los parámetros posicionales, cada uno separado por el primer carácter de la variable de shell IFS (por defecto un carácter espacio)

$@ 
Se expande en la lista de parámetros posicionales, comenzando por 1. Cuando lo incluimos en comillas dobles, expande cada parámetro posicional en una palabra separada incluida entre comillas dobles.

Aquí tenemos un script que muestra estos parámetros especiales en acción:

 #!/bin/bash

 # posit-params3 : script to demonstrate $* and $@

 print_params () {
    echo "\$1 = $1"
    echo "\$2 = $2"
    echo "\$3 = $3"
    echo "\$4 = $4"
 }

 pass_params () {
    echo -e "\n" '$* :'; print_params $*
    echo -e "\n" '"$*" :'; print_params "$*"
    echo -e "\n" '$@ :'; print_params $@
    echo -e "\n" '"$@" :'; print_params "$@"
 }

 pass_params "word" "words with spaces"

En este programa tan complicado, creamos dos argumentos: "word" y "words with spaces", y los pasamos a la función pass_params. Esa función, por turnos, los pasa a la función print_params, usando cada uno de los cuatro métodos disponibles con los parámetros especiales $! y $@. Cuando lo ejecutamos, el script revela las diferencias:

 [me@linuxbox ~]$ posit-param3

  $* :
 $1 = word
 $2 = words
 $3 = with
 $4 = spaces

  "$*" :
 $1 = word words with spaces
 $2 =
 $3 =
 $4 =
 $@ :
 $1 = word
 $2 = words
 $3 = with
 $4 = spaces

  "$@" :
 $1 = word
 $2 = words with spaces
 $3 =
 $4 =

Con nuestros argumentos, tanto $! como $@ producen un resultado de cuatro palabras:

word words with spaces

"$*" produce un resultado de una palabra:
      "word words with spaces"

"$@" produce un resultado de dos palabras:
      "word" "words with spaces"

que coincide con nuestra intención real. La lección que aprendemos de esto es que aunque el shell proporciona cuatro formas diferentes de obtener la lista de parámetros posicionales, "$@" es de lejos la más útil para la mayoría de los casos, porque conserva la integridad de cada parámetro posicional.

Una aplicación más completa

Tras un largo paréntesis vamos a retomar nuestro trabajo con nuestro programa sys_info_page. Nuestra próxima mejora añadirá varias opciones de línea de comandos al programa tal como sigue:

  • Archivo de salida. Añadiremos una opción para especificar un nombre para un archivo que contenga la salida del programa. Se especificará como -f archivo o como --file archivo.
  • Modo interactivo. Esta opción preguntará al usuario un nombre para el archivo de salida y comprobará si el archivo especificado ya existe. Si es así, el usuario será preguntado antes de que el archivo existente se sobrescriba. Esta opción se especificara como -i o como --interactive.
  • Ayuda. Tanto -h como --help pueden especificarse para hacer que el programa produzca un mensaje de uso interactivo.
Aquí está el código necesario para implementar el procesado en la línea de comandos:

 usage () {
     echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
     return
 }

 # process command line options

 interactive=
 filename=

 while [[ -n $1 ]]; do
     case $1 in
          -f | --file)       shift
                             filename=$1
                             ;;
         -i | --interactive) interactive=1
                             ;;
         -h | --help) usage
                             exit
                             ;;
          *)                 usage >&2
                             exit 1
                             ;;
     esac
     shift
 done

Primero, añadimos una función de shell llamada usage para mostrar un mensaje cuando se invoca la opción de ayuda o se intenta una opción desconocida.

A continuación, comenzamos el bucle de procesamiento. Este bucle continua mientras que el parámetro posicional $1 no esté vacío. Al final del bucle, tenemos un comando shift para avanzar el parámetro posicional y asegurarnos de que el bucle finalmente terminará.

Dentro del bucle, tenemos una sentencia case que examina el parámetro posicional actual para ver si coincide con alguna de las opciones soportadas. Si encuentra un parámetro soportado, se actúa sobre él. Si no, se muestra el mensaje de uso y el script termina con un error.

El parámetro -f se maneja de una forma interesante. Cuando se detecta, hace que ocurra un shift adicional, que avanza el parámetro posicional $1 al argumento de nombre de archivo proporcionado por la opción -f.

A continuación añadimos el código para implementar el modo interactivo:

 # interactive mode

 if [[ -n $interactive ]]; then
    while true; do
         read -p "Enter name of output file: " filename
         if [[ -e $filename ]]; then
             read -p "'$filename' exists. Overwrite? [y/n/q] > "
             case $REPLY in
                 Y|y) break
                      ;;
                 Q|q) echo "Program terminated."
                      exit
                      ;;
                 *) continue
                      ;;
             esac
         elif [[ -z $filename ]]; then
             continue
         else
             break
         fi
     done
 fi

La variable interactive no está vacía, y comienza un bucle infinito, que contiene el prompt del nombre del archivo y el consiguiente código existente de manejo de archivos. Si el archivo de salida ya existe, se pregunta al usuario si quiere sobrescribir, elegir otro nombre de archivo o salir del programa. Si el usuario elige sobrescribir un archivo existente, se ejecuta un break para terminar el bucle. Fíjate cómo la sentencia case sólo detecta si el usuario sobrescribe o sale. Cualquier otra opción hace que el bucle continué y pregunte al usuario de nuevo.

Para implementar la función de salida de nombre de archivo, deberíamos convertir primero el código existente de escritura de página en un función de shell, por las razones que veremos claras en un momento:

 write_html_page () {
     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_
     return
 }

 # output html page

 if [[ -n $filename ]]; then
     if touch $filename && [[ -f $filename ]]; then
         write_html_page > $filename
     else
         echo "$PROGNAME: Cannot write file '$filename'" >&2
         exit 1
     fi
 else
     write_html_page
 fi

El código que maneja la lógica de la opción -f aparece al final de la lista mostrada anteriormente. En él, comprobamos la existencia de un nombre de archivo, y si encuentra uno, se realiza un test para ver si el archivo es modificable. Para hacer esto, se realiza un touch, seguido de un test para determinar si el archivo resultante es un archivo normal. Estos dos tests se encargan de situaciones donde se le indica una ruta no válida (touch fallará), y si el archivo ya existe, que sea un archivo normal.

Como podemos ver, se llama a la función write_html_page para realizar la generación de la página. Su salida es dirigida a la salida estándar (si la variable file-name está vacía) o redirigida a un archivo especificado.


Resumiendo

Con la inclusión de los parámetros posicionales, ahora podemos escribir scripts bastante funcionales. Para tareas simples y repetitivas, los parámetros posicionales hacen posible escribir funciones de shell muy útiles que pueden guardarse en el archivo .bashrc del usuario.

Nuestro programa sys_info_page ha crecido en complejidad y sofisticación. Aquí tenemos un listado completo , con los cambios más recientes resaltados:

 #!/bin/bash

 # sys_info_page: program to output a system information page

 PROGNAME=$(basename $0)
 TITLE="System Information Report For $HOSTNAME"
 CURRENT_TIME=$(date +"%x %r %Z")
 TIMESTAMP="Generated $CURRENT_TIME, by $USER"
 report_uptime () {
    cat <<- _EOF_
         <H2>System Uptime</H2>
         <PRE>$(uptime)</PRE>
         _EOF_
    return
 }

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

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
 }

 usage () {
     echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
     return
 }

 write_html_page () {
     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_
     return
 }

 # process command line options

 interactive=
 filename=

  while [[ -n $1 ]]; do
     case $1 in
         -f | --file)        shift
                             filename=$1
                             ;;
         -i | --interactive) interactive=1
                             ;;
         -h | --help)        usage
                             exit
                             ;;
          *)                 usage >&2
                             exit 1
                             ;;
     esac
     shift
 done

 # interactive mode

 if [[ -n $interactive ]]; then
     while true; do
         read -p "Enter name of output file: " filename
         if [[ -e $filename ]]; then
             read -p "'$filename' exists. Overwrite? [y/n/q] > "
             case $REPLY in
                 Y|y) break
                      ;;
                 Q|q) echo "Program terminated."
                      exit
                      ;;
                 *)   continue
                      ;;
             esac
        
         elif [[ -z $filename ]]; then
             continue
         else
             break
         fi
     done
 fi

# output html page

 if [[ -n $filename ]]; then
     if touch $filename && [[ -f $filename ]]; then
         write_html_page > $filename
     else
         echo "$PROGNAME: Cannot write file '$filename'" >&2
         exit 1
     fi
 else
     write_html_page
 fi

Aún no hemos terminado. Todavía hay más cosas que podemos hacer y mejoras que podemos realizar.

Para saber más

No hay comentarios:

Publicar un comentario

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