Contexto
Cobra e urfave/cli são as duas bibliotecas dominantes para construir aplicações de linha de comando em Go, e ambas resolvem o mesmo núcleo: parsear argumentos, organizar subcomandos, tratar flags e imprimir help. A diferença de fundo é de peso e de postura. Cobra é um framework robusto, com um gerador de scaffolding (cobra-cli), integração próxima com o Viper para configuração, geração de man pages e autocompletar para quatro shells — é a fundação por trás de kubectl, hugo, gh (GitHub CLI) e helm, o que faz dele o padrão de facto para CLIs grandes. urfave/cli é mais enxuto: você declara a aplicação inteira como uma struct cli.App, sem gerador e com menos superfície, o que a torna especialmente confortável para ferramentas de porte pequeno a médio.
A pergunta que decide não é "qual parseia flags melhor" — os dois fazem isso bem. É quanto da sua CLI vai crescer e quanta infraestrutura você quer de graça desde o começo. Uma árvore profunda de subcomandos com autocompletar polido, páginas de manual e configuração em camadas (flag, variável de ambiente e arquivo) é território natural de Cobra. Uma ferramenta focada, com meia dúzia de comandos e poucas dependências, tende a ficar mais legível e direta em urfave/cli, onde a definição inteira cabe numa struct que você lê de cima a baixo.
Quando escolher Cobra
Cobra se paga quando a CLI é grande ou tem ambição de crescer: muitos subcomandos, subcomandos aninhados e a necessidade de uma experiência de terminal polida. O cobra-cli gera o esqueleto do projeto e novos comandos por linha de comando, o autocompletar embutido cobre bash, zsh, fish e powershell, e a biblioteca gera man pages e Markdown a partir da própria árvore de comandos. Some a integração com o Viper (mesmo autor) e você tem configuração em camadas — flag sobrepõe variável de ambiente, que sobrepõe arquivo — sem escrever a lógica de precedência à mão.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
var verbose bool
root := &cobra.Command{
Use: "app",
Short: "app faz coisas",
}
root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "saída detalhada")
greet := &cobra.Command{
Use: "greet [nome]",
Short: "cumprimenta um usuário",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if verbose {
fmt.Fprintln(os.Stderr, "cumprimentando...")
}
fmt.Printf("olá, %s\n", args[0])
},
}
root.AddCommand(greet)
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
O exemplo mostra o padrão de Cobra: cada comando é um *cobra.Command que você compõe numa árvore com AddCommand, PersistentFlags propaga uma flag para todos os subcomandos, e validadores como cobra.ExactArgs(1) cuidam da aridade dos argumentos antes de o Run executar. É mais verboso que a alternativa, e essa é justamente a contrapartida: para uma CLI de dois comandos, o boilerplate parece exagero. O retorno aparece na escala — quando a árvore tem dezenas de comandos, o scaffolding, o autocompletar de quatro shells, as man pages e o Viper deixam de ser luxo e viram a infraestrutura que segura a complexidade. Não é coincidência que as maiores CLIs do ecossistema Go rodem sobre Cobra.
Quando escolher urfave/cli
urfave/cli é a escolha certa quando você quer expressar uma ferramenta inteira de forma declarativa, com o mínimo de cerimônia e poucas dependências. A aplicação é uma única struct cli.App: nome, uso, flags e comandos ficam todos num lugar que você lê de uma vez. Não há gerador de código porque não é preciso — a struct já é a especificação. Para uma ferramenta pequena ou média, essa densidade é uma vantagem real: menos arquivos, menos indireção, e a definição inteira visível numa tela.
package main
import (
"fmt"
"log"
"os"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "app",
Usage: "app faz coisas",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}},
},
Commands: []*cli.Command{
{
Name: "greet",
Usage: "cumprimenta um usuário",
Action: func(c *cli.Context) error {
if c.Bool("verbose") {
fmt.Fprintln(os.Stderr, "cumprimentando...")
}
fmt.Printf("olá, %s\n", c.Args().First())
return nil
},
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
A diferença de ergonomia é clara: em urfave/cli, Action retorna error (mais idiomático em Go do que o Run sem retorno de Cobra), as flags são structs tipadas com Aliases, e você mapeia variáveis de ambiente colocando EnvVars direto na definição da flag, sem trazer uma biblioteca de configuração à parte. O custo é o teto de recursos: o autocompletar cobre bash e zsh via script gerado (sem fish/powershell embutidos), não há um cobra-cli para gerar esqueleto, e a integração com configuração em camadas é mais manual. Para a maioria das ferramentas internas, scripts de build e CLIs de produto de porte moderado, nada disso pesa — e a economia de dependências e de boilerplate é um ganho concreto.
Veredito
A decisão acompanha o tamanho e a ambição da CLI. Para ferramentas grandes, com árvores profundas de subcomandos e expectativa de experiência de terminal de primeira — autocompletar em vários shells, man pages, configuração em camadas via Viper — Cobra é a escolha comprovada. O boilerplate inicial é o preço de entrada de uma infraestrutura que as maiores CLIs de Go (kubectl, Hugo, gh, Helm) usam justamente porque escala. Se você está construindo uma plataforma de linha de comando para durar e crescer, comece por Cobra.
Para ferramentas pequenas e médias, urfave/cli entrega mais com menos: uma struct declarativa que você lê de ponta a ponta, Action retornando error no estilo idiomático de Go, EnvVars na própria flag e um grafo de dependências enxuto. Você abre mão de geradores e de alguns recursos avançados, mas ganha simplicidade e velocidade de escrita onde elas importam mais. Regra prática: se você hesita entre os dois porque a CLI é pequena hoje mas pode virar grande, pergunte se os subcomandos vão realmente se multiplicar. Se sim, o investimento em Cobra se paga; se não, urfave/cli mantém o código mais leve e mais legível.