Projeto: ferramenta CLI com subcomandos
LidoNesta aula, você aprenderá a construir uma ferramenta de linha de comando (CLI) em Bash com subcomandos, utilizando getopts para processar opções, implementando um sistema de ajuda e organizando a estrutura do código de forma modular e profissional.
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 subcomandoslib/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 -eouset -upara 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
- Bash Builtins (getopts)
- Bash Shell Functions
- Bash Conditional Constructs
- TLDP - getopts
- ShellCheck - analisador de scripts
- Bash Variables
Exercícios
Crie um script que use
getoptspara aceitar as opções-v(verbose) e-n nome(nome). Se-vfor passado, imprima "Modo verboso ativado"; se-nfor 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 doneModifique o script da aula para adicionar um subcomando
versionque 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ãoImplemente uma função
mainque aceite subcomandos e useshiftpara 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 }Crie uma função de ajuda
usageque liste os subcomandos e opções globais, e faça com que a opção-ha 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 fiEstruture um projeto CLI com pelo menos três arquivos:
bin/meuapp,lib/comandos.shelib/ajuda.sh. Escreva o conteúdo de cada arquivo para um subcomandostatusque 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" }