Getopts: parsing de opções como um profissional

1. Introdução ao Getopts e por que usá-lo

Desenvolver scripts Bash que aceitam opções de linha de comando é uma necessidade frequente. Muitos iniciantes recorrem ao parsing manual com $1, $2 e estruturas case para interpretar argumentos. Essa abordagem, embora funcional para scripts simples, rapidamente se torna frágil e difícil de manter quando o número de opções cresce.

# Abordagem manual problemática
case "$1" in
    -v) VERBOSE=true; shift ;;
    -o) OUTPUT="$2"; shift 2 ;;
    *) echo "Opção inválida"; exit 1 ;;
esac

O getopts resolve esses problemas oferecendo uma solução padronizada e robusta. Suas principais vantagens incluem: parsing automático de opções agrupadas (ex: -abc), detecção de opções ausentes, suporte a argumentos obrigatórios e tratamento de erros integrado.

A sintaxe básica do getopts segue este padrão:

while getopts "opcoes" var; do
    case "$var" in
        opcao) ação ;;
    esac
done

2. Estrutura fundamental do loop com getopts

O getopts utiliza variáveis especiais para controlar o processo de parsing. A variável OPTIND (option index) armazena o índice do próximo argumento a ser processado. Já OPTARG contém o valor do argumento quando a opção espera um parâmetro.

A string de opções define o comportamento esperado. Opções seguidas de : indicam que esperam um argumento obrigatório:

while getopts "vo:" opt; do
    case "$opt" in
        v) echo "Modo verboso ativado" ;;
        o) echo "Arquivo de saída: $OPTARG" ;;
    esac
done

Neste exemplo, v não espera argumento, enquanto o espera um valor. A diferença é crucial: sem os : adequados, o script pode falhar silenciosamente ou interpretar argumentos incorretamente.

3. Tipos de opções: sem argumento, com argumento obrigatório e opcional

Opções booleanas são as mais simples, funcionando como flags de ativação/desativação:

while getopts "vd" opt; do
    case "$opt" in
        v) VERBOSE=true ;;
        d) DEBUG=true ;;
    esac
done

Opções com argumento obrigatório exigem o : após a letra na string de opções:

while getopts "o:u:" opt; do
    case "$opt" in
        o) ARQUIVO="$OPTARG" ;;
        u) USUARIO="$OPTARG" ;;
    esac
done

Uma limitação importante do getopts nativo é a ausência de suporte a argumentos opcionais. Para contornar isso, é possível implementar uma lógica manual dentro do case:

while getopts "c:" opt; do
    case "$opt" in
        c) if [[ "$OPTARG" =~ ^- ]]; then
               CONFIG="default.cfg"
               OPTIND=$((OPTIND - 1))
           else
               CONFIG="$OPTARG"
           fi ;;
    esac
done

4. Tratamento de erros e validação de entrada

Por padrão, o getopts exibe mensagens de erro do shell para opções inválidas. Para suprimir essas mensagens e implementar um tratamento personalizado, adicione : no início da string de opções:

while getopts ":vo:" opt; do
    case "$opt" in
        v) VERBOSE=true ;;
        o) OUTPUT="$OPTARG" ;;
        \?) echo "Opção inválida: -$OPTARG" >&2 ;;
        :) echo "Opção -$OPTARG requer um argumento" >&2 ;;
    esac
done

A validação de valores recebidos é essencial para scripts robustos:

case "$opt" in
    n) if ! [[ "$OPTARG" =~ ^[0-9]+$ ]]; then
           echo "Erro: -n requer um número" >&2
           exit 1
       fi
       NUMERO="$OPTARG" ;;
esac

5. Combinando opções e argumentos posicionais

Após processar todas as opções com getopts, use shift $((OPTIND - 1)) para remover as opções processadas e deixar apenas os argumentos posicionais:

while getopts "vo:" opt; do
    case "$opt" in
        v) VERBOSE=true ;;
        o) OUTPUT="$OPTARG" ;;
    esac
done

shift $((OPTIND - 1))

# Agora $@ contém apenas argumentos posicionais
for arquivo in "$@"; do
    echo "Processando: $arquivo"
done

A ordem correta de uso é: opções primeiro, argumentos posicionais depois. O script deve ser chamado como ./script.sh -v -o saida.txt arquivo1.txt arquivo2.txt.

6. Padrões de design e boas práticas

Definir valores padrão antes do loop de parsing é uma prática recomendada:

VERBOSE=false
OUTPUT_FILE=""
CONFIG_FILE="default.conf"

while getopts ":vo:c:h" opt; do
    case "$opt" in
        v) VERBOSE=true ;;
        o) OUTPUT_FILE="$OPTARG" ;;
        c) CONFIG_FILE="$OPTARG" ;;
        h) mostrar_ajuda; exit 0 ;;
    esac
done

Implementar uma função de ajuda melhora a usabilidade:

mostrar_ajuda() {
    cat << EOF
Uso: $0 [OPÇÕES] [ARQUIVOS...]

Opções:
  -v        Modo verboso
  -o ARQ    Arquivo de saída
  -c ARQ    Arquivo de configuração
  -h        Mostra esta ajuda
EOF
}

7. Limitações do getopts e alternativas avançadas

A principal limitação do getopts é a ausência de suporte nativo a opções longas como --verbose. Uma solução híbrida combina getopts para opções curtas com parsing manual para longas:

while [[ $# -gt 0 ]]; do
    case "$1" in
        --verbose) VERBOSE=true; shift ;;
        --output=*) OUTPUT="${1#*=}"; shift ;;
        -*) while getopts "vo:" opt "$1"; do
                case "$opt" in
                    v) VERBOSE=true ;;
                    o) OUTPUT="$OPTARG" ;;
                esac
            done
            shift ;;
        *) break ;;
    esac
done

Para projetos mais complexos, considere ferramentas externas como o getopt GNU (que suporta opções longas) ou bibliotecas como argbash para geração automática de código de parsing.

8. Exemplo completo: script profissional com getopts

#!/bin/bash

# Script de backup profissional com getopts
# Uso: ./backup.sh [-v] [-d DESTINO] [-c CONFIG] ARQUIVO...

# Valores padrão
VERBOSE=false
DESTINO="./backup"
CONFIG="backup.conf"

# Função de ajuda
mostrar_ajuda() {
    cat << EOF
Uso: $0 [OPÇÕES] ARQUIVO...

Opções:
  -v        Modo verboso
  -d DIR    Diretório de destino (padrão: ./backup)
  -c ARQ    Arquivo de configuração (padrão: backup.conf)
  -h        Mostra esta ajuda

Exemplo:
  $0 -v -d /tmp/backup -c meu.conf arquivo1.txt arquivo2.txt
EOF
}

# Parsing de opções com tratamento de erros
while getopts ":vd:c:h" opt; do
    case "$opt" in
        v) VERBOSE=true ;;
        d) DESTINO="$OPTARG" ;;
        c) CONFIG="$OPTARG" ;;
        h) mostrar_ajuda; exit 0 ;;
        \?) echo "Erro: opção inválida -$OPTARG" >&2; exit 1 ;;
        :) echo "Erro: opção -$OPTARG requer argumento" >&2; exit 1 ;;
    esac
done

shift $((OPTIND - 1))

# Validação de argumentos posicionais
if [[ $# -eq 0 ]]; then
    echo "Erro: nenhum arquivo especificado" >&2
    mostrar_ajuda
    exit 1
fi

# Validação do destino
if [[ ! -d "$DESTINO" ]]; then
    $VERBOSE && echo "Criando diretório: $DESTINO"
    mkdir -p "$DESTINO" || { echo "Erro ao criar destino" >&2; exit 1; }
fi

# Processamento dos arquivos
for arquivo in "$@"; do
    if [[ ! -f "$arquivo" ]]; then
        echo "Aviso: arquivo não encontrado: $arquivo" >&2
        continue
    fi

    $VERBOSE && echo "Copiando: $arquivo -> $DESTINO"
    cp "$arquivo" "$DESTINO/" || echo "Erro ao copiar $arquivo" >&2
done

$VERBOSE && echo "Backup concluído em $DESTINO"

Referências