← Automatización con Scripts en Bash Bloque 5 · Funciones y argumentos · Tema 21 de 42

Tema 21

Opciones al estilo Unix con getopts

El analizador de opciones del tema 20 funciona, pero no admite dos formas que cualquier usuario de Unix escribe sin pensar: las opciones agrupadas, como -nv, y el valor pegado a la opción, como -d/mnt. Bash trae un comando que resuelve eso de forma estándar, getopts. Este tema explica cómo usarlo, cómo producir mensajes de error propios, y qué hacer con las opciones largas, que getopts no admite.

1. Qué hace getopts

getopts es un comando interno de Bash, definido por el estándar POSIX, que procesa las opciones cortas de un script de a una. Cada vez que se lo llama, examina el siguiente argumento, reconoce la opción, guarda su letra en una variable y, si la opción lleva un valor, lo guarda en otra. Llamado dentro de un while, recorre todas las opciones.

A cambio de una sintaxis algo particular, entiende todas las formas en que se escriben las opciones en Unix:

Escrito asígetopts entiende
-n -vLas opciones n y v.
-nvLo mismo: opciones agrupadas.
-d /mntLa opción d con el valor /mnt.
-d/mntLo mismo: el valor pegado.
-nvd /mntn, v, y d con valor /mnt.
-- -raroFin de las opciones; -raro es un argumento.

2. La cadena de opciones

Lo primero que recibe getopts es una cadena con las letras de las opciones válidas. Una letra seguida de dos puntos indica que esa opción lleva un valor:

la cadena "nvd:h"
n    # -n, sin valor
v    # -v, sin valor
d:   # -d, con valor obligatorio
h    # -h, sin valor

El orden de las letras no importa, y las opciones sin valor se suelen llamar banderas: solo indican sí o no. Si la cadena empieza con dos puntos, como ":nvd:h", getopts pasa al modo silencioso de la sección 5.

3. El bucle de getopts

La forma de uso es siempre la misma: un while que llama a getopts con la cadena y el nombre de una variable, y un case que actúa según la letra que dejó en esa variable. El valor de una opción, cuando lo tiene, queda en OPTARG.

el bucle de getopts
simular=0
detalle=0
destino=.

while getopts "nvd:h" opcion; do
  case "$opcion" in
    n)  simular=1 ;;
    v)  detalle=1 ;;
    d)  destino="$OPTARG" ;;
    h)  uso; exit 0 ;;
    \?) uso >&2; exit 2 ;;          # opción desconocida o falta un valor
  esac
done
shift $(( OPTIND - 1 ))           # quitar las opciones procesadas
  1. getopts examina el siguiente argumento. Si es una opción, guarda su letra en opcion y termina con éxito. cada vuelta
  2. Si la opción lleva valor, lo toma del resto del mismo argumento o del siguiente, y lo guarda en OPTARG. cada vuelta
  3. El case actúa según la letra. cada vuelta
  4. Cuando ya no quedan opciones, porque llegó a un argumento que no empieza con guion, a -- o al final, getopts falla y el bucle termina. salida

El \? del último caso es un signo de interrogación con una barra, para que no funcione como comodín: es la letra que getopts pone en la variable cuando encuentra algo que no reconoce.

4. OPTIND y los argumentos restantes

getopts no modifica los argumentos: los recorre llevando la cuenta en la variable OPTIND, que contiene el número del próximo argumento a examinar. Al terminar el bucle, OPTIND apunta al primer argumento que no es una opción, y shift $(( OPTIND - 1 )) descarta todos los anteriores. A partir de ahí, "$@" contiene solo los argumentos.

terminal · con el bucle de la sección 3
$ script -nv -d /tmp informe.pdf notas.txt
# getopts procesa: -n, -v, -d con OPTARG=/tmp
# al terminar, OPTIND vale 4: el cuarto argumento es «informe.pdf»
# después de shift 3, "$@" contiene: informe.pdf notas.txt

Una consecuencia de este diseño, que sigue el estándar POSIX: getopts se detiene en el primer argumento que no es una opción. En script archivo -n, el -n no se procesa como opción, porque viene después de un argumento. Es el comportamiento de muchos comandos clásicos de Unix, aunque los de GNU, como ls, aceptan las opciones en cualquier lugar.

5. Errores: el modo normal y el silencioso

Ante una opción desconocida, o una opción que necesita un valor y no lo tiene, getopts puede comportarse de dos maneras, según el primer carácter de la cadena:

SituaciónModo normal: "nd:"Modo silencioso: ":nd:"
Opción desconocida, -xMuestra illegal option -- x; la variable vale ?.No muestra nada; la variable vale ? y OPTARG es x.
Falta el valor, -d al finalMuestra option requires an argument -- d; la variable vale ?.No muestra nada; la variable vale : y OPTARG es d.
terminal · modo normal
$ script -x
script: illegal option -- x

El modo normal es cómodo, pero sus mensajes están en inglés y no se pueden cambiar. El silencioso permite escribir mensajes propios, distinguiendo los dos errores, y es el que usará el curso:

el modo silencioso, con mensajes propios
while getopts ":nvd:h" opcion; do
  case "$opcion" in
    n)  simular=1 ;;
    v)  detalle=1 ;;
    d)  destino="$OPTARG" ;;
    h)  uso; exit 0 ;;
    :)  error_uso "la opción -$OPTARG necesita un valor" ;;
    \?) error_uso "opción desconocida: -$OPTARG" ;;
  esac
done

error_uso es la función del tema 20, que muestra el mensaje y una sugerencia en la salida de errores y termina con código 2.

6. Validar los valores

getopts garantiza que una opción con valor tenga uno, pero no que ese valor tenga sentido. La validación es la misma del tema 20, y conviene hacerla después del bucle, cuando ya se procesaron todas las opciones:

validar después del bucle
shift $(( OPTIND - 1 ))

[[ "$cantidad" =~ ^[1-9][0-9]*$ ]] || error_uso "-n debe ser un número positivo: $cantidad"
[[ -d "$destino" ]]                 || error_uso "-d no es un directorio: $destino"
(( $# > 0 ))                        || error_uso "falta al menos un archivo"

Validar después tiene una ventaja: si la misma opción aparece dos veces, como -n 5 -n 10, se valida solo el valor que finalmente cuenta, el último. Y las comprobaciones que relacionan varias opciones, como «-a y -b no pueden usarse juntas», solo pueden hacerse cuando se conocen todas.

7. Opciones repetidas y acumuladas

Algunas opciones tienen sentido repetidas, y getopts las entrega una vez por cada aparición. Dos patrones habituales:

contar repeticiones y acumular valores
detalle=0
excluir=()

while getopts ":ve:" opcion; do
  case "$opcion" in
    v) detalle=$(( detalle + 1 )) ;;      # -v, -vv, -vvv: más detalle cada vez
    e) excluir+=("$OPTARG") ;;            # -e a -e b: una lista
    …
  esac
done
terminal
$ script -vvv -e '*.tmp' -e 'cache/'
# detalle=3, excluir=('*.tmp' 'cache/')

El array del tema 8 es la forma natural de acumular valores, y luego se usan para construir los argumentos de otro comando, como en la sección 10 de ese tema.

8. getopts dentro de una función

Una función también puede usar getopts para sus propias opciones. Hay que tener en cuenta que OPTIND es una variable global: después de la primera llamada queda apuntando al final, y la segunda llamada a la función no encuentra ninguna opción. La solución es declararla local, junto con la variable de la opción:

getopts en una función
log() {
  local OPTIND opcion nivel="INFO"
  while getopts "n:" opcion; do
    case "$opcion" in
      n) nivel="$OPTARG" ;;
    esac
  done
  shift $(( OPTIND - 1 ))
  printf '%s [%s] %s\n' "$(date +%T)" "$nivel" "$*" >&2
}

log "Comenzando"
log -n AVISO "Queda poco espacio"

Con local OPTIND, cada llamada empieza a contar desde 1. Es un detalle fácil de olvidar, porque la primera llamada funciona y el error solo aparece en la segunda.

9. Las opciones largas

getopts solo maneja opciones cortas de una letra. Para las largas, como --destino, hay tres caminos:

CaminoVentajasInconvenientes
Solo opciones cortas, con getoptsEstándar, portable, admite agrupación.Sin nombres descriptivos.
El analizador manual del tema 20Admite opciones largas y cortas, sin dependencias.No admite -nv ni -d/mnt sin trabajo extra.
El programa getopt (sin «s»), de util-linuxAdmite todo: largas, cortas, agrupadas, --destino=valor, opciones en cualquier lugar.Es un programa externo, que existe en Linux pero no siempre en otros sistemas, como macOS, y su uso es más complejo.

Para scripts personales o de administración, las opciones cortas con getopts suelen bastar. Si las opciones largas importan, por ejemplo porque el script lo usarán muchas personas, el analizador manual es más simple que getopt y no depende de nada. Lo que conviene evitar es mezclar getopts con trucos para simular opciones largas, que se vuelven difíciles de mantener.

10. Un buen mensaje de uso

La ayuda de un script es lo primero que lee quien lo usa por primera vez. Una buena ayuda tiene siempre las mismas partes, en este orden:

la estructura de una ayuda
Uso: buscar-grandes [-n NÚMERO] [-m TAMAÑO] [-e EXT]... [-h] [DIRECTORIO...]   # sinopsis

Lista los archivos más grandes de los directorios indicados (o del actual).    # qué hace

Opciones:                                                                       # cada opción,
  -n NÚMERO   cuántos archivos mostrar (10)                                     # con su valor
  -m TAMAÑO   tamaño mínimo, con sufijo k, M o G (1M)                          # por defecto
  -e EXT      solo esa extensión; se puede repetir: -e jpg -e png
  -h          muestra esta ayuda

Ejemplo:                                                                        # un ejemplo real
  buscar-grandes -n 5 -e mp4 ~/videos

11. Un script completo

Este script lista los archivos más grandes de uno o varios directorios, con opciones para la cantidad, el tamaño mínimo y las extensiones. Muestra getopts en modo silencioso, una opción repetible que se acumula en un array, la validación posterior y la construcción de los argumentos de find según las opciones.

buscar-grandes
#!/usr/bin/env bash
#
# buscar-grandes — lista los archivos más grandes de uno o varios directorios.
#
set -euo pipefail

uso() {
  cat <<FIN
Uso: ${0##*/} [-n NÚMERO] [-m TAMAÑO] [-e EXT]... [-h] [DIRECTORIO...]

Lista los archivos más grandes de los directorios indicados (o del actual).

Opciones:
  -n NÚMERO   cuántos archivos mostrar (10)
  -m TAMAÑO   tamaño mínimo, con sufijo k, M o G (1M)
  -e EXT      solo esa extensión; se puede repetir: -e jpg -e png
  -h          muestra esta ayuda
FIN
}

error_uso() {
  echo "${0##*/}: $*" >&2
  echo "Prueba «${0##*/} -h» para más información." >&2
  exit 2
}

cantidad=10
minimo=1M
extensiones=()

while getopts ":n:m:e:h" opcion; do
  case "$opcion" in
    n)  cantidad="$OPTARG" ;;
    m)  minimo="$OPTARG" ;;
    e)  extensiones+=("${OPTARG#.}") ;;       # acepta «jpg» y «.jpg»
    h)  uso; exit 0 ;;
    :)  error_uso "la opción -$OPTARG necesita un valor" ;;
    \?) error_uso "opción desconocida: -$OPTARG" ;;
  esac
done
shift $(( OPTIND - 1 ))

[[ "$cantidad" =~ ^[1-9][0-9]*$ ]] || error_uso "-n debe ser un número positivo: $cantidad"
[[ "$minimo" =~ ^[0-9]+[kMG]?$ ]]  || error_uso "-m debe ser como 500k, 10M o 2G: $minimo"
(( $# > 0 )) || set -- .                     # sin directorios: el actual

# Construir \( -iname '*.jpg' -o -iname '*.png' \) a partir de las extensiones.
filtro=()
for ext in "${extensiones[@]}"; do
  (( ${#filtro[@]} == 0 )) || filtro+=(-o)
  filtro+=(-iname "*.$ext")
done
(( ${#filtro[@]} == 0 )) || filtro=( \( "${filtro[@]}" \) )

find "$@" -type f -size +"$minimo" "${filtro[@]}" -printf '%s\t%p\0' \
  | sort -z -rn \
  | head -z -n "$cantidad" \
  | tr '\0' '\n' \
  | numfmt --to=iec-i --suffix=B --field=1 --padding=8
terminal
$ buscar-grandes -n 3 ~
  1.4GiB /home/ana/descargas/ubuntu-24.04.iso
  845MiB /home/ana/videos/viaje 2025.mp4
  312MiB /home/ana/videos/cumpleaños.mp4
$ buscar-grandes -e jpg -e .PNG -m 5M ~/fotos
   12MiB /home/ana/fotos/panorama.jpg
  7.3MiB /home/ana/fotos/plano.png
$ buscar-grandes -n0
buscar-grandes: -n debe ser un número positivo: 0
Prueba «buscar-grandes -h» para más información.

12. Errores frecuentes

EscribisteQué pasaCorrección
getopts "nd" opcion para una -d con valorEl valor se toma como un argumento suelto."nd:"
Olvidar shift $(( OPTIND - 1 ))"$@" sigue conteniendo las opciones.Hacer el shift después del bucle.
shift $OPTINDDescarta también el primer argumento.shift $(( OPTIND - 1 ))
?) en el caseEs un comodín: coincide con cualquier letra.\?)
Modo silencioso sin el caso :)Una opción sin valor no da ningún error.Agregar :) con su mensaje.
Usar getopts en una función sin local OPTINDLa segunda llamada no procesa ninguna opción.local OPTIND
script archivo -n esperando que se procese -ngetopts se detiene en el primer argumento.Opciones primero, o el analizador manual.
Esperar que getopts acepte --destinoLo toma como la opción - o lo rechaza.Analizador manual, o getopt.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué significan los dos puntos en la cadena "vo:f"?

Que la opción -o lleva un valor. -v y -f son banderas, sin valor.

2. ¿Cómo entiende getopts "vo:" el argumento -vo/tmp?

Como la bandera -v y la opción -o con el valor /tmp, que queda en OPTARG.

3. ¿Qué contiene OPTIND al terminar el bucle, y para qué se usa?

El número del primer argumento que no es una opción. shift $(( OPTIND - 1 )) descarta las opciones, y deja en "$@" solo los argumentos.

4. ¿Qué pasa con script informe.pdf -v?

getopts se detiene en informe.pdf, el primer argumento que no es una opción, y -v queda como un argumento más.

5. ¿Qué diferencia hay entre "nd:" y ":nd:"?

La primera usa el modo normal, en el que getopts muestra sus propios mensajes de error, en inglés. La segunda, el modo silencioso: no muestra nada, y permite que el script distinga una opción desconocida (?) de un valor faltante (:) y escriba sus mensajes.

6. En modo silencioso, ¿qué valen la variable y OPTARG si se escribe -z, una opción desconocida?

La variable vale ? y OPTARG contiene z, la letra de la opción desconocida.

7. ¿Por qué el caso de opción desconocida se escribe \?) y no ?)?

Porque en un patrón de case, ? es un comodín que coincide con cualquier carácter. Con la barra se toma literalmente.

8. ¿Cómo se permite que -e se repita varias veces y se guarden todos sus valores?

Acumulando cada OPTARG en un array: e) excluir+=("$OPTARG") ;;.

9. ¿Por qué una función que usa getopts debe declarar local OPTIND?

Porque OPTIND es global y queda apuntando al final después de la primera llamada. Sin reiniciarla, las llamadas siguientes no encuentran opciones.

10. ¿Qué alternativas hay para admitir opciones largas como --destino?

El analizador manual con while, case y shift, o el programa externo getopt de util-linux, que admite todas las formas pero no está en todos los sistemas.

Para practicar

  1. Escribe un script con las opciones -a, -b y -c VALOR, que muestre qué opciones recibió y los argumentos restantes. Pruébalo con -ab -c x uno dos, -c sin valor, -cx, -z y -- -a.
  2. Compara los mensajes de error del modo normal y del silencioso.
  3. Agrega una opción -v repetible que aumente un nivel de detalle, y muestra el nivel final.
  4. Escribe la función log de la sección 8 y llámala tres veces con y sin -n. Quita local OPTIND y observa qué cambia.
  5. Convierte el analizador manual de copiar-con-fecha del tema 20 a getopts, solo con opciones cortas.
  6. Agrega a buscar-grandes una opción -x DIR, repetible, para excluir directorios, usando -path DIR -prune -o en find.