Cómo usar getopts para parsear opciones cortas y largas en scripts de Bash

DUGLAS MORENODUGLAS MORENO 👁 22

Introducción

Cuando escribís scripts de Bash, es común que necesitéis recibir opciones desde la línea de comandos: por ejemplo, especificar un archivo de entrada con -f o definir un archivo de salida con -o. El built‑in getopts de Bash es la forma más portátil y segura de parsear esas opciones cortas (de un solo guión). Aunque getopts no soporta directamente opciones largas (--file, --output), podés combinarlo con una lógica adicional para soportarlas sin perder sus ventajas.

Este tutorial está dirigido a desarrolladores, administradores de sistemas y cualquiera que escriba scripts de shell y quiera mejorar la experiencia de usuario de sus herramientas. Al finalizar, vas a saber:

  • La sintaxis básica de getopts.
  • Cómo definir opciones que requieren argumento obligatorio o que son opcionales.
  • Cómo manejar tanto opciones cortas como largas en el mismo script.
  • Qué buenas prácticas seguir y qué errores evitar.

¿Qué es getopts?

getopts es un comando interno de Bash que procesa de forma secuencial las opciones que aparecen en $@ (los parámetros pasados al script). Su sintaxis es:

getopts optstring varname [args]
  • optstring: una cadena que lista las letras de las opciones esperadas. Si una opción necesita un argumento, se le agrega dos puntos (:). Si el argumento es opcional, se le agregan dos puntos dobles (::).
  • varname: la variable donde getopts guarda la letra de la opción encontrada en cada iteración.
  • [args]: opcionalmente, podés pasar un arreglo distinto de $@ para que getopts lo procese (útil para testing).

El bucle típico es:

while getopts "optstring" opt; do
    case $opt in
        # manejo de cada opción
    esac
done

Después del bucle, shift $((OPTIND-1)) elimina las opciones ya procesadas, dejando solo los argumentos restantes (operands).

Sintaxis y manejo de argumentos

Opciones sin argumento

Si una opción no necesita argumento, simplemente la listás en optstring sin puntos:

getopts "hf" opt

Aquí -h y -f son flags booleanos.

Opciones con argumento obligatorio

Agregá un dos puntos después de la letra:

getopts "f:o:" opt

Ahora -f y -o deben ir seguidos de un valor (p.ej. -f archivo.txt o -o salida.txt). Si el usuario omite el argumento, getopts genera un error y coloca ? en opt.

Opciones con argumento opcional

Usá dos puntos dobles:

getopts "f::o::" opt

En este caso, si la opción está presente pero sin valor inmediato, la variable OPTARG queda vacía. Tené en cuenta que, para que Bash reconozca el argumento opcional, tiene que estar pegado a la opción (p.ej. -fvalor o `-f valor separado por espacio no funciona para opcional; Bash trata el espacio como separador y considera que falta argumento).

Ejemplo 1: Script que acepta -f y -o (ambos obligatorios)

Guarda este código como procesar.sh:

#!/usr/bin/env bash

# Mensaje de uso
usage() {
    cat <<EOF
Uso: $0 -f <archivo_entrada> -o <archivo_salida>
Opciones:
  -f  Archivo de entrada (obligatorio)
  -o  Archivo de salida (obligatorio)
  -h  Mostrar esta ayuda
EOF
}

# Variables por defecto
INPUT_FILE=""
OUTPUT_FILE=""

# Parseo de opciones cortas
while getopts "f:o:h" opt; do
    case $opt in
        f) INPUT_FILE="$OPTARG" ;;
        o) OUTPUT_FILE="$OPTARG" ;;
        h) usage; exit 0 ;;
        \?) echo "Opción inválida: -$OPTARG" >&2; usage; exit 1 ;;
        :)  echo "La opción -$OPTARG requiere un argumento" >&2; usage; exit 1 ;;
    esac
done

# Eliminar opciones ya procesadas
shift $((OPTIND-1))

# Validación
if [[ -z "$INPUT_FILE" || -z "$OUTPUT_FILE" ]]; then
    echo "Error: faltan argumentos obligatorios" >&2
    usage
    exit 1
fi

# Acción principal (ejemplo simple)
if [[ ! -f "$INPUT_FILE" ]]; then
    echo "Error: el archivo de entrada '$INPUT_FILE' no existe" >&2
    exit 1
fi

cp "$INPUT_FILE" "$OUTPUT_FILE"
 echo "Archivo copiado de '$INPUT_FILE' a '$OUTPUT_FILE'"

Salida esperada

$ ./procesar.sh -f datos.txt -o copia.txt
Archivo copiado de 'datos.txt' a 'copia.txt'

Si olvidás un argumento:

$ ./procesar.sh -f datos.txt
La opción -o requiere un argumento
Uso: ./procesar.sh -f <archivo_entrada> -o <archivo_salida>
Opciones:
  -f  Archivo de entrada (obligatorio)
  -o  Archivo de salida (obligatorio)
  -h  Mostrar esta ayuda

Si pasás una opción desconocida:

$ ./procesar.sh -x
Opción inválida: -x
Uso: ./procesar.sh -f <archivo_entrada> -o <archivo_salida>
...

Manejo de opciones largas (--file, --output)

getopts solo reconoce opciones de un solo guión. Para soportar opciones largas, hay dos enfoques comunes:

  1. Pre‑procesar $@ convirtiendo --opción=valor en -o valor (o -ovalor) antes de llamar a getopts.
  2. Usar un case manual después del bucle de getopts para atender las opciones largas que quedaron sin procesar.

A continuación se muestra una combinación que permite tanto formas cortas como largas, con argumentos obligatorios y opcionales.

Estrategia de pre‑procesado

#!/usr/bin/env bash

# Función que transforma largas en cortas
normalize_args() {
    local normalized=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            --file)
                # necesita argumento obligatorio
                if [[ -z "$2" ]]; then
                    echo "Error: --file requiere un argumento" >&2
                    exit 2
                fi
                normalized+=(-f "$2" )
                shift 2
                ;;
            --file=*)
                normalized+=(-f "${1#*=}" )
                shift
                ;;
            --output)
                if [[ -z "$2" ]]; then
                    echo "Error: --output requiere un argumento" >&2
                    exit 2
                fi
                normalized+=(-o "$2" )
                shift 2
                ;;
            --output=*)
                normalized+=(-o "${1#*=}" )
                shift
                ;;
            --help)
                normalized+=(-h)
                shift
                ;;
            *)  # cualquier otro argumento (incluyendo opciones cortas ya en formato -x o -xvalor)
                normalized+=("$_1
                shift
                ;;
        esac
    done
    # devolver el arreglo normalizado
    printf '%s
' "${normalized[@]}"
}

# Reemplazar los parámetros originales con los normalizados
eval set -- "$(normalize_args "$@
)""  # Nota: el eval es necesario para preservar espacios y comillas

# Ahora usamos getopts solo con opciones cortas
INPUT_FILE=""
OUTPUT_FILE=""

while getopts "f:o:h" opt; do
    case $opt in
        f) INPUT_FILE="$OPTARG" ;;
        o) OUTPUT_FILE="$OPTARG" ;;
        h) usage; exit 0 ;;
        \?) echo "Opción inválida: -$OPTARG" >&2; usage; exit 1 ;;
        :)  echo "La opción -$OPTARG requiere un argumento" >&2; usage; exit 1 ;;
    esac
done

shift $((OPTIND-1))

# resto del script (igual que antes)

Nota: El eval set -- ... es una técnica común para volver a cargar los parámetros posicionales después de la normalización. Si preferís evitar eval, podés usar un arreglo y luego set -- "${array[@]}". En Bash 4.4+ podés hacer:

normalized_args=($(normalize_args "$@
))"  # no funciona bien con espacios
set -- "${normalized_args[@]}"  # forma segura

Alternativa: case manual después de getopts

Si no querés tocar los parámetros, podés dejar que getopts procese solo las opciones cortas y luego iterar sobre lo que queda ($@ después del shift) para detectar --file o --output.

while getopts "f:o:h" opt; do
    case $opt in
        f) INPUT_FILE="$OPTARG" ;;
        o) OUTPUT_FILE="$OPTARG" ;;
        h) usage; exit 0 ;;
        \?) echo "Opción inválida: -$OPTARG" >&2; usage; exit 1 ;;
        :)  echo "La opción -$OPTARG requiere un argumento" >&2; usage; exit 1 ;;
    esac
done

shift $((OPTIND-1))

# Procesar opciones largas restantes
while [[ $# -gt 0 ]]; do
    case $1 in
        --file)
            if [[ -z "$2" ]]; then
                echo "Error: --file requiere un argumento" >&2; usage; exit 2
            fi
            INPUT_FILE="$2"
            shift 2
            ;;
        --file=*)
            INPUT_FILE="${1#*=}" 
            shift
            ;;
        --output)
            if [[ -z "$2" ]]; then
                echo "Error: --output requiere un argumento" >&2; usage; exit 2
            fi
            OUTPUT_FILE="$2"
            shift 2
            ;;
        --output=*)
            OUTPUT_FILE="${1#*=}" 
            shift
            ;;
        --help)
            usage; exit 0
            ;;
        *)  # argumento posicional (ej. archivos de entrada sin bandera)
            break
            ;;
    esac
done

Este segundo enfoque es más explícito y evita el uso de eval, pero tenés que recordar que cualquier opción corta que haya quedado sin ser reconocida por getopts (porque estaba mal escrita) quedará en $@ y será tratada como argumento posicional, lo cual puede generar confusiones. Por eso, la técnica de normalización suele ser más robusta.

Ejemplo completo: script que acepta -f/--file, -o/--output y -v/--verbose (opcional)

#!/usr/bin/env bash

usage() {
    cat <<EOF
Uso: $0 [OPCIONES]
Opciones:
  -f, --file   <archivo>   Archivo de entrada (obligatorio)
  -o, --output <archivo>   Archivo de salida (obligatorio)
  -v, --verbose            Modo verbose (opcional)
  -h, --help               Mostrar esta ayuda
EOF
}

INPUT_FILE=""
OUTPUT_FILE=""
VERBOSE=0

# ---------- Normalización de opciones largas a cortas ----------
normalize() {
    local out=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            --file)
                [[ -z "$2" ]] && { echo "--file necesita argumento" >&2; exit 2; }
                out+=(-f "$2\

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario