Quando escrevemos scripts em Bash, uma das tarefas mais comuns é interpretar os argumentos passados pelo usuário na linha de comando. Sem uma ferramenta adequada, isso pode se tornar um código confuso e propenso a erros, especialmente quando temos muitas opções. É aqui que entra o getopts, um utilitário nativo do Bash que facilita o parsing de opções curtas (como -v ou -f arquivo). Nesta aula, vamos explorar como usar o getopts para criar scripts mais robustos e profissionais.

O getopts é um comando interno do Bash que processa os argumentos de forma sistemática, permitindo que você defina quais flags são válidas e se elas esperam um argumento adicional. Ele é especialmente útil para scripts que precisam de uma interface de linha de comando clara, seguindo as convenções do Unix. Ao longo desta aula, você verá como declarar flags simples, como lidar com argumentos obrigatórios, e como construir uma mensagem de uso que oriente o usuário.

Flags curtas

Flags curtas são opções de uma única letra precedidas por um hífen, como -a, -b, -c. O getopts permite que você defina quais dessas opções são reconhecidas pelo script. A sintaxe básica é:

getopts ":abc" opcao

Nesse exemplo, a string ":abc" define três opções: -a, -b e -c. O dois-pontos (:) no início é opcional e serve para suprimir mensagens de erro automáticas do getopts, permitindo que você mesmo trate os erros. Cada vez que o getopts é chamado, ele retorna a próxima opção na variável opcao (que você pode nomear como quiser). Você normalmente usa um loop while para processar todas as opções.

Vamos ver um exemplo simples de um script que aceita as flags -a, -b e -c e imprime qual foi fornecida:

#!/bin/bash

while getopts ":abc" opcao; do
  case $opcao in
    a) echo "Opção -a ativada";;
    b) echo "Opção -b ativada";;
    c) echo "Opção -c ativada";;
    \?) echo "Opção inválida: -$OPTARG" >&2; exit 1;;
  esac
done

Ao executar esse script com ./script -a -c, a saída será:

Opção -a ativada
Opção -c ativada

Note que usamos \? para capturar opções inválidas. A variável OPTARG contém o caractere inválido. Esse padrão é fundamental para criar scripts amigáveis.

Argumentos com valor

Muitas opções precisam de um valor associado, como -o arquivo.txt ou -n 10. No getopts, você indica que uma opção espera um argumento adicionando dois-pontos (:) após a letra na string de opções. Por exemplo, a string ":f:n" define que -f e -n exigem um argumento, enquanto -a não exige.

Quando a opção tem um argumento, o valor é armazenado na variável OPTARG. Veja um exemplo prático:

#!/bin/bash

while getopts ":f:n:" opcao; do
  case $opcao in
    f) echo "Arquivo de saída: $OPTARG";;
    n) echo "Número de iterações: $OPTARG";;
    \?) echo "Opção inválida: -$OPTARG" >&2; exit 1;;
  esac
done

Se você executar ./script -f saida.txt -n 5, a saída será:

Arquivo de saída: saida.txt
Número de iterações: 5

É importante notar que, se uma opção que espera argumento não receber um, o getopts retornará ? ou : dependendo de como você configurou. Para tratar isso, você pode adicionar um caso para : (quando falta o argumento) e \? (opção desconhecida). Vamos melhorar nosso script:

#!/bin/bash

while getopts ":f:n:" opcao; do
  case $opcao in
    f) echo "Arquivo de saída: $OPTARG";;
    n) echo "Número de iterações: $OPTARG";;
    :) echo "A opção -$OPTARG requer um argumento." >&2; exit 1;;
    \?) echo "Opção inválida: -$OPTARG" >&2; exit 1;;
  esac
done

Assim, se o usuário digitar -f sem valor, o script exibirá uma mensagem clara. Esse tratamento de erros é essencial para scripts profissionais.

Mensagem de uso

Uma boa prática em scripts de linha de comando é fornecer uma mensagem de uso (ou help) que explique como usar o script. O getopts não gera automaticamente essa mensagem, mas você pode criar uma função que a exiba quando o usuário usar -h ou quando houver erro. Vamos criar uma função uso() que imprime as instruções:

#!/bin/bash

uso() {
  cat <<EOF
Uso: $(basename "$0") [OPÇÕES]

OPÇÕES:
  -f ARQUIVO   Define o arquivo de saída.
  -n NÚMERO    Define o número de iterações.
  -h           Exibe esta mensagem de ajuda.
EOF
}

while getopts ":f:n:h" opcao; do
  case $opcao in
    f) echo "Arquivo de saída: $OPTARG";;
    n) echo "Número de iterações: $OPTARG";;
    h) uso; exit 0;;
    :) echo "A opção -$OPTARG requer um argumento." >&2; uso; exit 1;;
    \?) echo "Opção inválida: -$OPTARG" >&2; uso; exit 1;;
  esac
done

Com isso, se o usuário executar ./script -h, verá a mensagem de ajuda. Se ele usar uma opção inválida, também verá a mensagem, além do erro. Isso torna o script muito mais utilizável.

Você pode também exibir a mensagem de uso quando nenhum argumento for passado, verificando se $# é igual a zero. Essa é uma prática comum para evitar que o script execute sem parâmetros.

Exemplos

Vamos montar um exemplo completo que combina tudo o que vimos: um script que processa um arquivo de entrada, um número de repetições e uma flag de verbosidade. O script terá uma função de uso e tratará erros adequadamente.

#!/bin/bash

# Função de uso
uso() {
  cat <<EOF
Uso: $(basename "$0") [OPÇÕES]

OPÇÕES:
  -i ARQUIVO   Arquivo de entrada (obrigatório).
  -n NÚMERO    Número de repetições (padrão: 1).
  -v           Modo verboso.
  -h           Exibe esta mensagem de ajuda.
EOF
}

# Variáveis padrão
arquivo=""
reps=1
verboso=0

# Parsing de opções
while getopts ":i:n:vh" opcao; do
  case $opcao in
    i) arquivo="$OPTARG";;
    n) reps="$OPTARG";;
    v) verboso=1;;
    h) uso; exit 0;;
    :) echo "A opção -$OPTARG requer um argumento." >&2; uso; exit 1;;
    \?) echo "Opção inválida: -$OPTARG" >&2; uso; exit 1;;
  esac
done

# Verifica se o arquivo foi fornecido
if [[ -z "$arquivo" ]]; then
  echo "Erro: o arquivo de entrada é obrigatório." >&2
  uso
  exit 1
fi

# Verifica se o arquivo existe
if [[ ! -f "$arquivo" ]]; then
  echo "Erro: o arquivo '$arquivo' não existe." >&2
  exit 1
fi

# Loop de repetições
for ((i=1; i<=reps; i++)); do
  if [[ $verboso -eq 1 ]]; then
    echo "Processando repetição $i..."
  fi
  # Simula processamento
  cat "$arquivo"
done

echo "Concluído."

Nesse exemplo, usamos -i para o arquivo de entrada, -n para o número de repetições, -v para modo verboso e -h para ajuda. O script valida se o arquivo foi fornecido e se existe. Essa estrutura é típica de scripts mais complexos e mostra como o getopts pode ser integrado a outras lógicas.

Para testar, você pode criar um arquivo dados.txt e executar:

./script.sh -i dados.txt -n 3 -v

Isso exibirá o conteúdo do arquivo três vezes, com mensagens verbosas. Essa abordagem modular facilita a manutenção e a expansão do script.

Boas práticas e observações finais

Ao usar getopts, lembre-se de que ele suporta apenas opções curtas. Para opções longas (como --help), você precisaria usar getopt (com uma letra a mais) ou implementar um parser manual. O getopts também não reordena os argumentos, ou seja, ele processa as opções na ordem em que aparecem, mas ignora os argumentos não-opção (como nomes de arquivos) que são deixados intactos após o loop. Você pode acessá-los via $@ após o shift.

Outra dica é sempre usar o dois-pontos inicial na string de opções para controlar as mensagens de erro, e sempre incluir um caso para : e \?. Isso evita que o script falhe silenciosamente. Além disso, a função de uso deve ser chamada em qualquer erro de sintaxe, para que o usuário saiba como corrigir.

Por fim, teste seu script com várias combinações de argumentos, incluindo casos inválidos, para garantir que o comportamento seja intuitivo. O getopts é uma ferramenta poderosa, mas a qualidade do script depende de como você trata os erros e comunica as opções.

Referências

Exercícios

  1. Crie um script que use getopts para aceitar as opções -a, -b e -c (sem argumentos). Para cada opção, imprima uma mensagem indicando que ela foi ativada. Teste com diferentes combinações.

    ✓ Resposta:
    #!/bin/bash
    
    while getopts ":abc" opcao; do
      case $opcao in
        a) echo "Opção -a ativada";;
        b) echo "Opção -b ativada";;
        c) echo "Opção -c ativada";;
        \?) echo "Opção inválida: -$OPTARG" >&2; exit 1;;
      esac
    done
    
  2. Modifique o script anterior para que as opções -a e -b exijam um argumento (por exemplo, -a valor). Imprima o valor fornecido.

    ✓ Resposta:
    #!/bin/bash
    
    while getopts ":a:b:" opcao; do
      case $opcao in
        a) echo "Valor de -a: $OPTARG";;
        b) echo "Valor de -b: $OPTARG";;
        :) echo "A opção -$OPTARG requer um argumento." >&2; exit 1;;
        \?) echo "Opção inválida: -$OPTARG" >&2; exit 1;;
      esac
    done
    
  3. Escreva um script que aceite uma opção -o arquivo (obrigatória) e uma opção -v (verboso). Se a opção -o não for fornecida, exiba uma mensagem de erro e a mensagem de uso, e saia com código 1.

    ✓ Resposta:
    #!/bin/bash
    
    uso() {
      cat <<EOF
    Uso: $(basename "$0") -o ARQUIVO [-v]
    
      -o ARQUIVO   Arquivo de saída (obrigatório).
      -v           Modo verboso.
    EOF
    }
    
    arquivo=""
    verboso=0
    
    while getopts ":o:v" opcao; do
      case $opcao in
        o) arquivo="$OPTARG";;
        v) verboso=1;;
        :) echo "A opção -$OPTARG requer um argumento." >&2; uso; exit 1;;
        \?) echo "Opção inválida: -$OPTARG" >&2; uso; exit 1;;
      esac
    done
    
    if [[ -z "$arquivo" ]]; then
      echo "Erro: a opção -o é obrigatória." >&2
      uso
      exit 1
    fi
    
    if [[ $verboso -eq 1 ]]; then
      echo "Processando arquivo: $arquivo"
    fi
    
  4. Implemente um script que use getopts para aceitar as opções -n (número, com valor) e -h (ajuda). Se -h for usada, exiba a mensagem de uso e saia. Se -n não for um número, exiba um erro.

    ✓ Resposta:
    #!/bin/bash
    
    uso() {
      cat <<EOF
    Uso: $(basename "$0") -n NÚMERO
    
      -n NÚMERO   Define o número (obrigatório).
      -h          Exibe esta ajuda.
    EOF
    }
    
    numero=""
    
    while getopts ":n:h" opcao; do
      case $opcao in
        n) numero="$OPTARG";;
        h) uso; exit 0;;
        :) echo "A opção -$OPTARG requer um argumento." >&2; uso; exit 1;;
        \?) echo "Opção inválida: -$OPTARG" >&2; uso; exit 1;;
      esac
    done
    
    if [[ -z "$numero" ]]; then
      echo "Erro: a opção -n é obrigatória." >&2
      uso
      exit 1
    fi
    
    if [[ ! "$numero" =~ ^[0-9]+$ ]]; then
      echo "Erro: '$numero' não é um número válido." >&2
      exit 1
    fi
    
    echo "Número fornecido: $numero"
    
  5. Crie um script que aceite múltiplas opções -f (cada uma com um argumento) e armazene todos os valores em um array. Ao final, imprima todos os valores.

    ✓ Resposta:
    #!/bin/bash
    
    # Array para armazenar os valores
    valores=()
    
    while getopts ":f:" opcao; do
      case $opcao in
        f) valores+=("$OPTARG");;
        :) echo "A opção -$OPTARG requer um argumento." >&2; exit 1;;
        \?) echo "Opção inválida: -$OPTARG" >&2; exit 1;;
      esac
    done
    
    echo "Valores fornecidos:"
    for valor in "${valores[@]}"; do
      echo "- $valor"
    done