O Contrato da CLI: O Que Torna uma Ferramenta de Linha de Comando um Bom Cidadão
argparse, Click e Typer parecem três maneiras diferentes de construir uma ferramenta de linha de comando, mas na verdade são três ergonomias diferentes sobrepostas ao mesmo contrato subjacente: um programa que lê da entrada padrão, escreve resultados na saída padrão, escreve diagnósticos no erro padrão e relata sucesso ou falha através de seu código de saída. Esse contrato precede todas as três bibliotecas por décadas, e é a razão pela qual uma CLI Python bem construída compõe-se de forma limpa com grep, xargs e scripts shell escritos por pessoas que nunca ouviram falar da sua ferramenta.
Esta página explica esse contrato diretamente, porque Noções Básicas de CLI e as páginas específicas de framework - argparse, Click, Typer - assumem que você já entende o que uma CLI "bem-comportada" deve fazer, e gastam seu espaço em como implementá-la em vez de por que isso importa.
Resumo
- A interface real de uma CLI são seus streams (stdout/stderr), seu código de saída e sua gramática de argumentos - a biblioteca de parsing que você escolhe é um detalhe de implementação sobre esse contrato.
- Por Que Importa: Uma ferramenta que viola o contrato - misturando erros no stdout, sempre saindo com 0, ignorando pipes - quebra o scripting e a composição, mesmo que funcione bem quando um humano a executa diretamente.
- Conceitos Chave: Separação stdout/stderr, códigos de saída, gramática de argumento vs. opção, detecção de TTY, precedência de configuração.
- Quando Usar Este Modelo: Projetando o formato de saída de uma nova CLI, decidindo o que deve ser um argumento em vez de uma flag, ou depurando por que uma ferramenta se comporta de maneira diferente em um script do que em um terminal.
- Limitações / Trade-offs: Seguir o contrato adiciona uma pequena quantidade de disciplina (modos de saída estruturados, separação cuidadosa de streams) que um script rápido e único pode pular - mas pular isso é exatamente o que torna uma ferramenta inutilizável em automação posteriormente.
- Tópicos Relacionados: Pipelines Unix, convenções de scripting shell, detecção de capacidade de terminal, logging estruturado.
Fundamentos
Todo processo tem três streams padrão disponíveis para ele: stdin (entrada), stdout (saída normal) e stderr (diagnósticos e erros).
A separação entre stdout e stderr existe para que um chamador possa redirecionar um sem o outro - canalizando a saída real de um comando para outro programa enquanto ainda permite que as mensagens de erro cheguem ao terminal.
Uma ferramenta que imprime uma mensagem de erro no stdout polui qualquer coisa que consuma essa saída, porque não há como a jusante distinguir "aqui estão seus dados" de "aqui está o que deu errado" uma vez que ambos os streams são mesclados.
A segunda metade do contrato é o código de saída: 0 significa sucesso, qualquer valor não zero significa falha, e shells e scripts ramificam com base nesse número, não no texto que um comando imprimiu.
Uma CLI que sempre sai com 0 - mesmo quando falhou - quebra todos os padrões if mytool; then ou mytool && next-step que um chamador pode escrever razoavelmente, transformando silenciosamente falhas em sucessos do ponto de vista do shell.
A terceira peça é a gramática de argumentos: argumentos posicionais são valores obrigatórios em uma ordem fixa (cp source dest), enquanto opções/flags são nomeadas e opcionais (--verbose, -o output.txt), seguindo convenções POSIX/GNU estabelecidas há muito tempo - flags curtas de um único traço, opções longas de dois traços, -- para encerrar a análise de opções.
argparse, Click e Typer implementam essa mesma gramática; a diferença entre eles é quanto código boilerplate você escreve para chegar lá, não como a linha de comando resultante se parece para um usuário.
Mecânicas e Interações
A detecção de TTY é o mecanismo por trás de um comportamento que todo usuário de CLI já viu sem necessariamente nomeá-lo: o mesmo comando parece diferente quando sua saída vai para um terminal versus para um arquivo ou outro processo.
Um processo pode verificar se seu stdout está conectado a um terminal interativo ou a um pipe/arquivo, e ferramentas bem-comportadas usam essa verificação para decidir se devem mostrar cores, barras de progresso e prompts.
import sys
if sys.stdout.isatty():
print("\033[32mDone\033[0m") # códigos de escape de cor - bom para um humano
else:
print("Done") # texto puro - seguro para um arquivo de log ou outro programaSaída Rica e Prompts Interativos ambos se baseiam nessa verificação: Rich detecta um alvo não terminal e volta automaticamente para texto puro, e uma biblioteca de prompts interativos precisa detectar um contexto não interativo (um pipeline de CI, uma entrada canalizada) e falhar claramente em vez de travar para sempre esperando por uma entrada de teclado que nunca virá.
A precedência de configuração é a mesma ideia aplicada a uma pergunta diferente: não "como devo exibir isso", mas "qual valor vence quando uma configuração é fornecida em mais de um lugar". A convenção quase universal - flags de linha de comando substituem variáveis de ambiente, que substituem um arquivo de configuração, que substituem padrões embutidos - existe porque corresponde à especificidade de uma fonte: uma flag é a instrução mais explícita e única que um usuário pode dar, enquanto um padrão é o fallback menos específico.
Configuração e Ambiente cobre a implementação concreta dessa cadeia de precedência; a razão pela qual a ordem é sempre flags-then-env-then-file-then-defaults, e não alguma outra ordem, é que invertê-la significaria que um arquivo de configuração obsoleto poderia substituir silenciosamente uma flag explícita que alguém digitou momentos atrás.
Considerações Avançadas e Aplicações
O contrato descrito até agora explica por que "esta ferramenta funciona quando a executo manualmente" é uma barra mais baixa do que "esta ferramenta funciona quando outra coisa a chama".
Uma CLI destinada apenas ao uso interativo pode se safar com prompts, cores e barras de progresso como sua interface principal; uma CLI destinada a ser scriptável precisa de um modo de saída legível por máquina (--json, --quiet), códigos de saída distintos e significativos além de apenas 0/1, e streams que nunca assumem que um humano está assistindo.
Melhores Práticas de CLI enumera orientações concretas aqui, mas a razão subjacente para cada diretriz remonta a este contrato: --json existe porque stdout precisa ser analisável por algo mais do que um olho humano, códigos de saída distintos existem porque um chamador precisa distinguir "entrada inválida" de "falha de rede" sem analisar o texto de erro, e uma flag --quiet existe porque a saída de progresso que é útil interativamente é ruído em um arquivo de log.
Empacotamento e Distribuição de CLIs é uma preocupação relacionada, mas distinta - trata de como a ferramenta chega ao PATH de um usuário, não sobre o contrato que a ferramenta honra uma vez invocada; uma ferramenta lindamente empacotada que viola a separação de streams ainda é um mau cidadão depois de instalada.
| Escolha de design | Ferramenta apenas interativa | Ferramenta scriptável/componível |
|---|---|---|
| Formato de saída | Tabelas ricas, cores, prosa legível por humanos | Texto puro ou --json sob demanda, amigável para parsing |
| Prompts | Usados livremente para entrada ausente | Deve falhar claramente (ou aceitar flags/stdin) em contextos não interativos |
| Códigos de saída | 0 e 1 geralmente suficientes | Códigos distintos por categoria de falha, documentados |
| Indicadores de progresso | Barras de progresso, spinners | Suprimidos automaticamente quando não é um TTY, ou via --quiet |
| Fonte de configuração | Flags e prompts | Precedência completa de flags > env > arquivo > padrões, tudo scriptável |
Acertar isso desde o início é mais importante para uma CLI do que para a maioria dos outros softwares, porque os consumidores de uma CLI incluem scripts futuros e pipelines de CI que nunca fizeram parte da conversa de design original - uma ferramenta que só é executada pela pessoa que a escreveu pode esconder violações de contrato indefinidamente, até que alguém tente chamá-la do cron ou de um trabalho de CI e ela trave esperando por um prompt que nunca virá.
Conceitos Errôneos Comuns
- "Imprimir tudo no stdout está bem, desde que seja legível." Legibilidade para um humano e capacidade de parsing para um pipeline são objetivos diferentes - texto de erro misturado no stdout quebra qualquer chamador que tenta separar a saída real dos diagnósticos, mesmo que um humano lendo o terminal nunca perceba.
- "Código de saída 1 é suficiente para todas as falhas." Códigos de saída distintos permitem que um chamador ramifique por categoria de falha (entrada inválida versus um timeout de rede versus um erro de permissão) sem analisar a saída de texto, o que importa no momento em que a ferramenta é chamada de um script em vez de manualmente.
- "Os decoradores do Click/Typer são uma sintaxe de argumento diferente do argparse." Todos eles, em última análise, produzem a mesma linha de comando no estilo POSIX/GNU para o usuário final; os decoradores mudam a quantidade de código que o autor escreve, não a gramática que o usuário digita.
- "Cores e barras de progresso são sempre um toque agradável." Eles são um toque agradável apenas quando stdout é um terminal - mostrados incondicionalmente, códigos de escape e barras de progresso em movimento corrompem arquivos de log e confundem qualquer programa que analise essa saída.
- "A ordem de precedência da configuração realmente não importa, desde que todas as fontes sejam suportadas." A ordem codifica a intenção - uma flag digitada agora deve sempre vencer um arquivo de configuração escrito meses atrás - e inverter a ordem significaria que uma fonte obsoleta e menos específica poderia substituir silenciosamente a mais explícita.
FAQs
Por que uma CLI precisa de streams stdout e stderr separados?
Para que um chamador possa redirecionar a saída real para um arquivo ou outro programa, enquanto as mensagens de erro ainda chegam ao terminal (ou vice-versa) - mesclá-las remove a capacidade de um consumidor a jusante de dizer "aqui estão seus dados" de "aqui está o que deu errado".
Qual é a diferença real entre um argumento e uma opção/flag?
Um argumento é posicional e geralmente obrigatório, identificado por sua ordem na linha de comando (cp source dest); uma opção ou flag é nomeada (--verbose, -o file.txt) e opcional, e pode aparecer em qualquer ordem - todas as três bibliotecas de CLI Python implementam essa mesma distinção POSIX/GNU.
Por que os códigos de saída importam se um humano apenas lê a saída impressa?
Porque shells e sistemas de CI ramificam com base no código de saída numérico, não no texto impresso - um script que encadeia comandos com && ou verifica $? precisa de um código não zero correto em caso de falha, ou continuará como se a etapa anterior tivesse sido bem-sucedida.
Como um programa sabe se está sendo executado interativamente ou canalizado?
Verificando se seu stdout (ou stdin) está conectado a um terminal através de uma verificação isatty(); essa única verificação é o que permite que uma ferramenta mostre cores e barras de progresso para um humano enquanto emite texto puro automaticamente quando canalizado para um arquivo ou outro processo.
Por que a precedência de configuração é sempre flags, depois ambiente, depois arquivo, depois padrões?
Porque essa ordem corresponde à especificidade - uma flag é a instrução mais explícita e imediata que um usuário pode dar agora, enquanto um padrão é o fallback menos específico - inverter a ordem permitiria que uma fonte obsoleta e menos específica vencesse silenciosamente uma explícita.
argparse, Click e Typer produzem experiências de linha de comando diferentes para usuários finais?
Não fundamentalmente - todos os três seguem a mesma gramática de argumento/opção que um usuário final já conhece de outras ferramentas Unix; eles diferem na quantidade de código Python que o autor escreve para chegar lá, não no que o usuário digita.
Por que uma biblioteca de prompts interativos não pode simplesmente sempre pedir a entrada ausente?
Porque um contexto não interativo - um trabalho de CI, um script canalizado - não tem um humano disponível para responder, então um prompt que assume um ficará travado indefinidamente; uma ferramenta bem-comportada detecta esse contexto e falha com uma mensagem clara ou volta para flags/stdin.
Um flag de saída `--json` é apenas um recurso extra agradável?
É mais próximo de um requisito para scriptabilidade - prosa legível por humanos é genuinamente difícil de analisar de forma confiável por outro programa, então um modo legível por máquina estável e documentado é o que permite que outras ferramentas dependam da saída da sua CLI sem raspar texto.
O que realmente quebra se uma CLI ignora este contrato, mas funciona bem quando executada manualmente?
Nada quebra até que alguém tente chamá-la de um script, job cron ou pipeline de CI - nesse ponto, um stdout/stderr mesclado, um código de saída sempre zero ou um prompt interativo inesperado se transforma em uma falha silenciosa ou um travamento, muitas vezes longe de onde a decisão de design original foi tomada.
Empacotar bem uma CLI (pipx, entry points) também significa que ela honra este contrato?
Não - o empacotamento determina como uma ferramenta chega ao PATH de um usuário e é instalada, o que é uma preocupação separada de se o comportamento real de tempo de execução da ferramenta (streams, códigos de saída, conhecimento de TTY) é bem-comportado uma vez invocado.
Por que isso importa mais para CLIs do que para, digamos, um serviço web típico?
Porque os consumidores de uma CLI incluem rotineiramente outros scripts e automação que nunca fizeram parte da conversa de design original - um serviço web tem um conjunto mais controlado de chamadores, enquanto uma CLI pode acabar sendo canalizada para xargs ou chamada de cron por alguém que nunca leu seu código.
Relacionados
- Noções Básicas de CLI - introdução prática à construção de uma primeira ferramenta de linha de comando
- argparse - a implementação da biblioteca padrão da gramática de argumentos POSIX/GNU
- Saída Rica - formatação ciente de TTY que volta para texto puro
- Configuração e Ambiente - implementação da cadeia de precedência flags-sobre-env-sobre-arquivo
- Prompts Interativos - solicitar com segurança sem quebrar chamadores não interativos
- Melhores Práticas de CLI - orientações concretas baseadas neste mesmo contrato
Versões da Stack: Esta página é conceitual e não está vinculada a uma versão específica da stack.