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
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
}
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"
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
}
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
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
Para saber más
- La Bash Hackers Wiki tiene un buen artículo sobre parámetros posicionales:
http://wiki.bash-hackers.org/scripting/posparams - El Manual de Referencia de Bash tiene un artículo sobre parámetros especiales, incluyendo $* y $@:
http://www.gnu.org/software/bash/manual/bashref.html#Special-Parameters - Además de las técnicas vistas en este capítulo, bash incluye un comando interno llamado getopts, que también puede usarse para procesar argumentos de línea de comandos. Está descrito en la sección SHELL BUILTIN COMMAND de la man page de bash y en la Bash Hackers Wiki:
http://wiki.bash-hackers.org/howto/getopts_tutorial
No hay comentarios:
Publicar un comentario
Nota: solo los miembros de este blog pueden publicar comentarios.