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:

  1. Sólo permite manejar opciones cortas, no largas (por tanto, en nuestro ejemplo no podremos soportar --file ni --password).

  2. 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 de args.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:

  1. Se llega al final de $@ (o sea, se acaban los argumentos del programa).

  2. 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.

  3. 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