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í1:

$ ./args.sh -h
args.sh: 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.
                                 Si no se indica, se pedirá interactivamente.
 -v, --verbose                   Ofrece información adicional

Artesanalmente, es posible programar una solución que se acerque al estilo POSIX (aunque no se podrán fusionar opciones cortas) y se deja, como curiosidad, un ejemplo de ello. Sin embargo, lo más conveniente es hacer uso del comando interno getopts, que define el estándar2 y que sí permite la fusión.

Esta orden interna recibe dos argumentos:

  • La cadena que declara cuáles son las opciones (cortas) admisibles y si admiten o no argumento.

  • Un nombre para la variable que almacenará el nombre de la opción.

En el ejemplo referido 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 argumentos posicionales devuelve 0 y solamente deja de devolverlo cuando:

  1. Se acaban los argumentos posicionales.

  2. Se encuentra con el parámetro --, que significa fin de las opciones.

  3. Se encuentra con un argumento que no está asociado a ninguna opción. Por ejemplo:

    $ args.sh -v hola -f fichero.txt -p password
    

    haría que sólo se analizara -v y que el resto quedara sin analizar, puesto que hola no está asociado a ninguna opción.

Cuando getopt encuentra algo que no debe, no devuelve error (o sea, un valor distinto de 0), sino que altera los valores de opt y OPTARG del siguiente modo:

Error

opts

OPTARG

La opción no existe

?

Valor de la opción

Falta argumento de la opción

:

Valor de la opción

Con todo este conocimiento, 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))

Como consecuencia de ello, 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)3 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. En caso de que estos no fueran válidos, podríamos haber añadido:

if [ $# -gt 0 ]; then
   echo "$1: Argumento sin sentido" >&2
   exit 2
fi

A todo esto, y las opciones largas, ¿dónde están? La respuesta es en ningún sitio, porque getopts no las soporta (en principio). Lo cierto es que este tratamiento bastante sencillo tiene algunas limitaciones:

  1. Los argumentos no asociados a opciones tienen que situarse siempre al final de la orden.

  2. No pueden definirse opciones con argumentos opcionales.

  3. La orden:

    $ ./args.sh -f -v
    

    se procesa sin errores puesto que getopts no entiende que a la opción -f le falte argumento, sino que el argumento de -f es -v; y que la opción -v no se ha indicado. De hecho, hay programas que se comportan así.

  4. No hay opciones largas.

Nota

Si estamos satisfechos con la solución, no hay más que hacer; pero es posible solventar las limitaciones 2, 3 y 4, creando tres funciones independientes que modifiquen el comportamiento de getopts.

Parcheando getopts

Basta descargar las funciones para el parcheo y tener presente que:

  • path_lo añade soporte para opciones largas.

    Nota

    La función usa la argucia4 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 --5.

  • patch_optarg añade soporte para opciones con argumentos opcionales.

  • patch_dash evita que que una opción posterior sea tomada como argumento de la precedente6.

  • No es necesario usar las tres (p.e. podemos dejar de usar patch_optarg si no queremos opciones con argumentos opcionales), pero debemos aplicarlas en el orden en que se han citado.

Veamos cómo usarlas con un ejemplo:

while getopts ":hvf:p:-:" opt; do
   patch_lo "help verbose file:password:" opt "$@"
   patch_optarg "p password" opt
   patch_dash
   case $opt in
      h|help)
         help
         exit 0
         ;;
      \?)
         echo "-$OPTARG: Opción inválida."
         exit 2
         ;;
      :)
         echo "-$OPTARG requiere un argumento"
         exit 2
         ;;
      f|file)
         FICHERO=$OPTARG
         ;;
      p|password)
         PASSWORD=$OPTARG
         ;;
      v|verbose)
         VERBOSE=1
         ;;
   esac
done
shift $((OPTIND-1))

El ejemplo completo del programa de prueba hecho con getopts, se puede descargar también.

Advertencia

Con dash sólo es posible usar la forma --opt=valor y no --opt valor

Notas al pie

1

El argumento de la opción -p/--password, se muestra como opcional. En la solución artesanal es posible implementar este tipo de opciones con argumento opcional; en la solución basada en getopts, no; por lo que para ella se considerará esta opción con argumento obligatorio. En cualquier caso, las opciones con argumento opcional no son muy comunes, por lo que no supone una gran pérdida.

2

Hay un tutorial interesante en bash-hackers.org.

3

Es obvio que si no nos importa usar extensiones de bash, lo más conveniente es usar un diccionario: ${params['entrda']}, ${params['password']}, etc.

4

Argucia tomada de esta respuesta en stackoverflow

5

Recuérdese que, como se soporta la fusión, --opción-larga equivale a -- opcion-larga, si es que -- es una opción con argumento, como se ha definido.

6

En realidad, se impide que el argumento de la opción empiece por un guión, sea cual sea, lo cual podría ser un problema en algunos casos. Téngalo presente.