3.6.2. Tratamiento de argumentos¶
Ya se explicó cómo acceder a los argumentos de un script. Sin embargo, si se quiere escribir uno en que estos argumentos sean variados y algunos opcionales, usar argumentos posicionales es absolutamente desaconsejable. Lo más apropiado es crear scripts que usen para sus argumentos el estilo POSIX. Por ejemplo, algo así:
$ ./args.sh -h
args.sh [opciones] arg1 [arg2 ...]
Ilustra el procesamiento de las opciones de un script.
Opciones:
-h, --help Muestra esta misma ayuda.
-f, --file <FICHERO> Fichero de entrada...
-p, --password <PASSWORD> Permite indicar la contraseña.
-v, --verbose Ofrece información adicional
donde vemos que hay algunas opciones con y sin argumento adicional y uno o varios argumentos no asociados a ninguna opción (arg1, arg2, etc.)
El estándar[1] proporciona la orden interna getopts, que tiene algunas limitaciones:
Sólo permite manejar opciones cortas, no largas (por tanto, en nuestro ejemplo no podremos soportar --file ni --password).
No es trivial soportar que los argumentos no asociados a opciones se antepongan a éstas, esto es, que permitamos escribir, por ejemplo,
args.sh arg1 -farchivo
en vez deargs.sh -farchivo arg1
.
La primera, en especial, es bastante frustante, si queremos darle cierto aire de profesionalidad a nuestro script. En cualquier caso, expongamos cómo se usa. Recibe dos argumentos:
La cadena que declara cuáles son las opciones (cortas) admisibles y si requieren argumento.
Un nombre para la variable que almacenará el nombre de la opción.
En el ejemplo ilustrativo getopts se podría usar así:
getopts ":hvf:p:" opt
Como los dos puntos (:
) indican que la opción requiere argumento, resulta
que declaramos dos opciones (-h
y -v
) que no necesitan ninguno; y otras
dos opciones (-f
y -p
) que sí lo necesitan. Los dos puntos con que se
abre la cadena, le indican a getopts que no muestre mensajes de error
cuando procesa los argumentos.
Ante este orden getopts comienza a analizar $@
y parará su
análisis cuando encuentre la primera opción. Por ejemplo, si escribimos:
$ args.sh -v -f fichero.txt -p password
hallará la opción -v
y como esta no requiere argumento parará.
getopts almacena en la variable OPTIND
cuál es el próximo
parámetro posicional que debe analizar. Antes del análisis era 1 y, después
de ejecutarse, valdrá 2. Además, almacena en la variable que le hemos pasado
(opt
) el nombre de la variable (v, en esta pasada) y en la variable
OPTARG
, el valor del argumento, que en este caso no tiene sentido porque
-v
no admite argumento. Si queremos seguir analizando, debemos volver a
ejecutar otra vez la orden:
getopts ":hvf:p:" opt
Ahora, lo que getopts encuentra es -f
, pero como tal opción
requiere un argumento toma también el siguiente parámetro posicional
(fichero.txt), con lo que opt
valdrá f, OPTARG
fichero.txt y
OPTIND
4. Mientras getopts encuentra opciones (sean o no
válidas) devuelve 0 y solamente deja de devolverlo cuando:
Se llega al final de
$@
(o sea, se acaban los argumentos del programa).Se encuentra con el argumento
--
, que significa en el estándar que ya no hay más opciones y todo lo que venga a continuación debe entenderse como argumento no asociado a opciones.Se encuentra con un argumento que no está asociado a ninguna opción, por lo que en la orden
$ args.sh -v hola -f fichero.txt -p password
no devolverá 0 al toparse con hola.
En cambio, cuando getopts encuentra algo que no se ajusta a la sintaxis que hemos definido, no genera error ni devuelve algo distinto de 0, sino que juega con los valores de opt (o como quiera que la hayamos llamado) y OPTARG para hacérnoslo notar:
Error |
opt |
OPTARG |
---|---|---|
La opción no existe |
|
Opción |
Falta argumento de la opción |
|
Opción |
Sabido esto, el análisis de los argumentos puede hacerse así:
while getopts ":hv:f:p:" opt; do
case $opt in
h)
help # Función "help" que tenemos definida antes.
exit 0 ;;
v) VERBOSE=1 ;;
f) ENTRADA=$OPTARG ;;
p) PASSWORD=$OPTARG ;;
\?)
echo "-$OPTARG: La opción no existe" >&2
exit 2 ;;
:)
echo "-$OPTARG requiere argumento" >&2
exit 2 ;;
esac
done
shift $((OPTIND-1))
# En $@ quedan los argumentos no asociados a opciones.
Así, de forma relativamente sencilla, el programa generará un error y saldrá con código
2, si no se introdujeron bien los parámetros; o, en caso contrario,
tendremos disponible en distintas variables (ENTRADA
, PASSWORD
y
VERBOSE
)[2] la información que introdujo el usuario. Además, el último
shift elimina todos los parámetros revisados por getopts con lo
que en $@
quedarán los argumentos no asociados a opciones.
Parcheando getopts
Si nos basta con esto, getopts es una buena solución. Si deseamos dar
soporte a opciones largas, podemos definir una función que usa la argucia[3]
de añadir como opción válida el guión y que este requira argumento. De este modo
todas las opciones largas se identificarán con la opción corta --
[4]:
patch_lo() {
local LO="$1" _OPT="$2"
shift 2
eval "[ \$$_OPT = '-' ] || return 0"
local o=${OPTARG%%=*}
eval $_OPT=\$o
if ! echo "$LO" | grep -qw "$o"; then # La opción no existe
eval $_OPT='\?'
OPTARG=-$o
return 1
fi
OPTARG=$(echo "$OPTARG" | cut -s -d= -f2-) # Suponiendo --opcion=valor
if echo "$LO" | grep -q "\<$o:"; then # La opción requiere argumento
if [ -z "$OPTARG" ]; then # Se debió escribir --opcion valor
eval OPTARG=\$$((OPTIND))
if [ -z "$OPTARG" ]; then # No se facilitó argumento
eval $_OPT=":"
OPTARG=-$o
return 1
fi
OPTIND=$((OPTIND+1))
fi
elif [ -n "$OPTARG" ]; then # No requiere argumento, pero se ha añadido uno.
# Se elimina el argumento
OPTARG=""
fi
}
Advertencia
Con dash sólo es posible usar la forma --opt=valor y no --opt valor
Su uso no difiere mucho del que ya hemos visto:
while getopts ":hvf:p:-:" opt; do
patch_lo "help verbose file:password:" opt "$@"
case $opt in
h|help)
help
exit 0 ;;
v|verbose) VERBOSE=1 ;;
f|file) FICHERO=$OPTARG ;;
p|password) PASSWORD=$OPTARG ;;
\?)
echo "-$OPTARG: Opción inválida."
exit 2 ;;
:)
echo "-$OPTARG requiere un argumento"
exit 2 ;;
esac
done
shift $((OPTIND-1))
Nota
Obsérvese que a la declaración en getopts de las opciones
cortas se ha añadido «-:
».
El ejemplo completo del programa de prueba hecho con getopts, se
puede descargar también
.
Solución artesanal
La limitación casi intolerable de getopts es que sólo admite opciones cortas y, si se quiere dar soporte a opciones largas, es necesario escribir una función con abundante código. Otro modo de abordar el problema es prescindir de él e implementar una solución nosotros mismos, mientras seamos capaces:
De no complicar en exceso el código.
De mantener el mayor número de características del estándar POSIX.
¿Es posible? Lo cierto es que sí. Del código completo del ejemplo
las líneas que tratan directamente los argumentos son
estas:
{ ### Análisis de las opciones.
opts="hp:f:v" # Como en getopts, ":" significa que requiere argumento propio
post=0 # Cantidad de argumentos pospuestos no asociados a ninguna opción
while [ $# -gt $post ]; do
case "$1" in
-h|--help) # Ayuda
help
exit 0 ;;
-f|--file)
[ -z "$2" ] && echo "$1 requiere argumento" >&2 && exit 2
ARCHIVO="$2"
shift 2 ;;
-p|--password)
[ -z "$2" ] && echo "$1 requiere argumento" >&2 && exit 2
PASSWORD="$2"
shift 2 ;;
-v|--verbose)
VERBOSE=1
shift ;;
--) # Final de las opciones
shift
break ;;
--??*=*) #1 --opt=value
arg=${1%%=*}
value=${1#*=}
shift
set -- "$arg" "$value" "$@" ;;
--?*|-?)
echo "$1: Opción desconocida"
exit 2 ;;
-??*) #2 Fusión de opciones cortas
rarg="${1#-?}"
arg="${1%$rarg}"
shift
# Si "arg" no necesita argumento, rarg no es su argumento, sino otra opción
expr "$opts" : ".*${arg#-}:" > /dev/null || rarg="-$rarg"
set -- "$arg" "$rarg" "$@" ;;
*) #3 Argumentos no asociados a opciones no puestos al final
arg=$1
shift
set -- "$@" "$arg"
post=$((post+1)) ;;
esac
done
# $@ contiene los argumentos no asociados a opciones.
}
Básicamente, un while que va recorriendo los argumentos posicionales e
identificándolos dentro de una sentencia case. Los primeros ítem de tal
sentencia tratan los argumentos del program particular y los últimos son los que
obran la magia de dar soporte a las funcionalidades no triviales (fusión de
opciones cortas, etc.). El código es algo más complejo que usar directamente
getopts, pero desde luego más sencillo que si se le intenta dar
soporte con la función patch_lo
que hemos propuesto. En conclusión:
- Ventajas
Soporta opciones cortas.
Soporta la fusión de opciones cortas.
Soporta opciones largas.
Soporta ambas sintaxis para opciones largas (--opcion valor y --opcion=valor).
No es obligado que los argumentos no asociados a opciones se encuentren al final de la orden.
Las líneas que añaden el soporte extra (las resaltadas) no dependen de las opciones concretas y, por tanto, basta con copiarlas en cualquier otro script.
Si alguno de los soportes no nos interesa, podemos eliminarlo, eliminando el ítem del case correspondiente (p.e. si no nos interesa dar soporte a la sintaxis --option=valor), podemos eliminar
--??*=*)
.Como es más artesanal, es una solución más versátil.
- Desventajas
Alteramos el $@ original, que pasará a contener únicamente los argumentos no asociados a opciones. Esto no suele ser un problema, porque si ya hemos analizado la línea de órdenes, ¿para qué conservar $@?
Notas al pie