Nesta aula, vamos construir uma ferramenta de linha de comando (CLI) em Bash com subcomandos, uma técnica essencial para criar utilitários poderosos e extensíveis. Você aprenderá a usar getopts para processar opções, a organizar subcomandos de forma clara e a implementar um sistema de ajuda automática. Ao final, terá uma estrutura sólida que pode ser expandida para projetos reais.

Ferramentas como git e docker são exemplos clássicos de CLIs com subcomandos. Vamos replicar essa arquitetura usando apenas Bash, mostrando como planejar e implementar cada parte de forma incremental.

getopts

O getopts é um comando interno do Bash que facilita o parsing de opções de linha de comando. Ele processa opções curtas (como -v) e permite associar argumentos a elas (como -o arquivo). É mais portátil e seguro que usar shift manualmente.

O uso básico é: getopts "opções" variável. As opções são definidas em uma string, onde cada caractere representa uma opção. Se uma opção exigir um argumento, coloque dois pontos após ela. Por exemplo, "a:b" define que -a requer um argumento e -b não.

Exemplo simples:

#!/bin/bash

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

echo "Argumentos restantes: $*"

Quando você executa ./script -a valor -b arg1, o getopts processa as opções e deixa os argumentos restantes em $@ (após o loop, você pode usar shift $((OPTIND-1)) para removê-los).

É importante tratar o caso de opção desconhecida com \? e exibir uma mensagem de erro.

Subcomandos

Subcomandos são palavras-chave após o nome do programa que indicam uma ação específica. Por exemplo, git commit executa o subcomando commit. Em Bash, podemos implementar isso verificando o primeiro argumento ($1) e chamando funções específicas.

Vamos criar uma função para cada subcomando e uma função principal que decide qual chamar. Isso mantém o código organizado e facilita a adição de novos subcomandos.

#!/bin/bash

cmd_hello() {
  echo "Olá, mundo!"
}

cmd_goodbye() {
  echo "Até logo!"
}

main() {
  case "$1" in
    hello)
      cmd_hello
      ;;
    goodbye)
      cmd_goodbye
      ;;
    *)
      echo "Subcomando desconhecido: $1" >&2
      exit 1
      ;;
  esac
}

main "$@"

Observe que usamos main como ponto de entrada e passamos "$@" para que a função possa acessar todos os argumentos. Isso permite que cada subcomando receba seus próprios argumentos.

Ajuda

Um bom CLI deve ter uma opção -h ou --help que mostra instruções de uso. Vamos criar uma função usage que imprime a ajuda geral e, se um subcomando for especificado, a ajuda específica dele.

Podemos usar getopts para detectar -h e, para subcomandos, verificar se o primeiro argumento é help ou -h. Vamos implementar uma ajuda geral e uma ajuda por subcomando.

#!/bin/bash

usage() {
  cat <<EOF
Uso: $(basename "$0") [comando] [opções]

Comandos:
  hello   Exibe uma saudação
  goodbye Exibe uma despedida

Opções:
  -h, --help   Mostra esta ajuda
EOF
}

usage_hello() {
  echo "Uso: $(basename "$0") hello"
  echo "Exibe uma saudação amigável."
}

usage_goodbye() {
  echo "Uso: $(basename "$0") goodbye"
  echo "Exibe uma despedida."
}

cmd_hello() {
  echo "Olá, mundo!"
}

cmd_goodbye() {
  echo "Até logo!"
}

main() {
  if [ "$#" -eq 0 ]; then
    usage
    exit 0
  fi

  case "$1" in
    help|-h|--help)
      if [ -n "$2" ]; then
        case "$2" in
          hello) usage_hello ;;
          goodbye) usage_goodbye ;;
          *) echo "Subcomando desconhecido: $2" >&2; exit 1 ;;
        esac
      else
        usage
      fi
      ;;
    hello)
      cmd_hello
      ;;
    goodbye)
      cmd_goodbye
      ;;
    *)
      echo "Comando desconhecido: $1" >&2
      usage >&2
      exit 1
      ;;
  esac
}

main "$@"

Dessa forma, ./script help hello mostra a ajuda específica do subcomando hello.

Estrutura

Para projetos maiores, é recomendável organizar o código em múltiplos arquivos. Vamos estruturar nossa CLI da seguinte forma:

  • bin/meuprog - script principal (executável)
  • lib/comandos.sh - funções dos subcomandos
  • lib/ajuda.sh - funções de ajuda

O script principal carrega as bibliotecas usando source e chama a função principal. Isso facilita a manutenção e o teste.

Exemplo de bin/meuprog:

#!/bin/bash

# Carrega bibliotecas
source "$(dirname "$0")/../lib/comandos.sh"
source "$(dirname "$0")/../lib/ajuda.sh"

main "$@"

E lib/comandos.sh contém as funções cmd_hello e cmd_goodbye. lib/ajuda.sh contém as funções de ajuda.

Essa separação permite que você adicione novos subcomandos criando novas funções e atualizando a função main para reconhecê-los.

Boas práticas e observações finais

Ao construir CLIs em Bash, lembre-se de:

  • Sempre use set -e ou set -u para evitar erros silenciosos.
  • Valide entradas e trate erros adequadamente.
  • Documente cada função e o uso geral.
  • Teste cada subcomando isoladamente.
  • Use nomes de variáveis em minúsculas para variáveis locais.

Com essas técnicas, você pode criar ferramentas robustas e profissionais usando apenas Bash.

Referências

Exercícios

  1. Crie um script que use getopts para aceitar as opções -v (verbose) e -n nome (nome). Se -v for passado, imprima "Modo verboso ativado"; se -n for passado, imprima "Nome: <valor>".

    ✓ Resposta:
    #!/bin/bash
    
    while getopts "vn:" opt; do
      case $opt in
        v)
          echo "Modo verboso ativado"
          ;;
        n)
          echo "Nome: $OPTARG"
          ;;
        \?)
          echo "Opção inválida" >&2
          exit 1
          ;;
      esac
    done
  2. Modifique o script da aula para adicionar um subcomando version que imprime "Versão 1.0".

    ✓ Resposta:
    #!/bin/bash
    
    cmd_version() {
      echo "Versão 1.0"
    }
    
    # Na função main, adicione:
    # version)
    #   cmd_version
    #   ;;
    # E na ajuda, adicione a linha: version  Exibe a versão
  3. Implemente uma função main que aceite subcomandos e use shift para passar os argumentos restantes para a função do subcomando.

    ✓ Resposta:
    main() {
      case "$1" in
        hello)
          shift
          cmd_hello "$@"
          ;;
        goodbye)
          shift
          cmd_goodbye "$@"
          ;;
        *)
          echo "Comando desconhecido" >&2
          exit 1
          ;;
      esac
    }
  4. Crie uma função de ajuda usage que liste os subcomandos e opções globais, e faça com que a opção -h a exiba.

    ✓ Resposta:
    usage() {
      cat <<EOF
    Uso: $(basename "$0") [comando] [opções]
    
    Comandos:
      hello   Exibe uma saudação
      goodbye Exibe uma despedida
    
    Opções:
      -h, --help   Mostra esta ajuda
    EOF
    }
    
    # No main, antes de processar subcomandos, verifique -h:
    if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
      usage
      exit 0
    fi
  5. Estruture um projeto CLI com pelo menos três arquivos: bin/meuapp, lib/comandos.sh e lib/ajuda.sh. Escreva o conteúdo de cada arquivo para um subcomando status que imprime "Status: OK".

    ✓ Resposta:
    # bin/meuapp
    #!/bin/bash
    source "$(dirname "$0")/../lib/comandos.sh"
    source "$(dirname "$0")/../lib/ajuda.sh"
    main "$@"
    
    # lib/comandos.sh
    cmd_status() {
      echo "Status: OK"
    }
    
    main() {
      case "$1" in
        status) cmd_status ;;
        *) echo "Comando desconhecido" >&2; exit 1 ;;
      esac
    }
    
    # lib/ajuda.sh
    usage() {
      echo "Uso: meuapp [comando]"
      echo "Comandos:"
      echo "  status   Mostra o status"
    }