2 pontos por GN⁺ 2024-02-07 | 1 comentários | Compartilhar no WhatsApp
  • Uma referência em documento open source que organiza princípios de design de programas CLI e diretrizes concretas como uma releitura moderna da filosofia UNIX tradicional, tendo como principal público desenvolvedores que criam ferramentas de linha de comando
  • A CLI evoluiu de uma simples plataforma de scripts para uma interface de texto centrada em humanos, e os princípios de design também precisam ser atualizados para acompanhar essa mudança
  • Composability e facilidade de uso para humanos não são incompatíveis; seguindo convenções UNIX como entrada/saída padrão, pipes e códigos de saída, é possível alcançar ambas ao mesmo tempo
  • Oferece recomendações concretas até para detalhes frequentemente negligenciados na prática, como texto de ajuda, mensagens de erro, formato de saída, interatividade e sistema de configuração
  • A compatibilidade futura e a confiança do usuário em ferramentas CLI são definidas pela estabilidade da interface e pela transparência dos dados analíticos, e este guia apresenta essa linha de base

Filosofia (Philosophy)

Design centrado no ser humano

  • Os comandos UNIX tradicionais eram projetados principalmente supondo uso por outros programas, mas hoje a maior parte das CLIs é usada diretamente por pessoas, então é necessário um design com prioridade para humanos
  • No passado, a CLI era "machine-first", mas hoje evoluiu para uma UI textual "human-first"

Pequenos componentes combináveis

  • O núcleo da filosofia UNIX é combinar programas pequenos e simples para formar sistemas maiores, e isso continua válido hoje
  • stdin/stdout/stderr padrão, sinais e códigos de saída garantem a conexão entre programas, e JSON oferece suporte a troca de dados mais estruturada
  • Todo software inevitavelmente se torna parte de um sistema maior, e ser um componente que funciona bem ou não é algo decidido na fase de design

Consistência

  • Usuários de terminal já estão acostumados com convenções existentes, então recomenda-se que a CLI siga padrões já estabelecidos
  • Ainda assim, se a consistência prejudicar a usabilidade, é possível quebrar a convenção com cuidado

Quantidade adequada de informação

  • Se um comando fica esperando por vários minutos sem qualquer saída, há informação "de menos"; se despeja uma enorme quantidade de logs de debug, há informação "demais"
  • O equilíbrio na quantidade de informação é absolutamente importante para que o software apoie o usuário

Facilidade de descoberta (Ease of Discovery)

  • GUIs mostram todas as funções na tela, enquanto CLIs muitas vezes são mal interpretadas como algo que depende apenas de memória
  • Com texto de ajuda abrangente, exemplos ricos e sugestões do próximo comando, a CLI também pode ser fácil de aprender ao adotar técnicas de GUI

CLI como conversa

  • O uso da CLI tem uma estrutura de conversa baseada em tentativas e erros repetidos; sugestões para corrigir erros, indicação de estado intermediário e confirmação antes de ações perigosas são técnicas de design que aproveitam essa característica
  • A pior interação é uma conversa hostil que deixa o usuário impotente; a melhor é uma troca agradável que gera sensação de realização

Robustez (Robustness)

  • O software precisa ser robusto na prática e na percepção
  • Tratar entradas inesperadas com elegância, manter a idempotência, informar o progresso e evitar expor stack traces são pontos centrais
  • Reduzir casos especiais complexos e manter tudo simples aumenta a robustez

Empatia (Empathy)

  • Ferramentas CLI são instrumentos criativos para desenvolvedores e devem ser agradáveis de usar
  • O design deve ser pensado o suficiente para que o usuário sinta que a ferramenta está do seu lado

Caos (Chaos)

  • O mundo do terminal é cheio de inconsistências, mas esse caos também é fonte de criação livre
  • "Se um padrão for claramente prejudicial à produtividade ou à satisfação do usuário, abandone esse padrão" — Jef Raskin

Diretrizes — O básico (The Basics)

  • Use uma biblioteca de parsing de argumentos: entre as recomendadas por linguagem estão Go(Cobra, cli), Python(Click, Typer, Argparse), Rust(clap), Node(oclif) e várias outras
  • Retorne código de saída 0 em caso de sucesso e código diferente de 0 em caso de falha — esse é o critério usado por scripts para distinguir sucesso e erro
  • A saída padrão deve ir para stdout, enquanto mensagens como logs e erros devem ir para stderr

Diretrizes — Ajuda (Help)

  • Exiba texto de ajuda detalhado com a flag -h ou --help, e aplique o mesmo a subcomandos
  • Ao executar sem argumentos, mostre uma ajuda concisa (incluindo descrição, 1–2 exemplos, explicação das flags e indicação de --help)
    • jq é citado como um bom exemplo dessa implementação
  • Dê suporte a várias formas de pedir ajuda, como --help, -h e help subcommand
  • No topo do texto de ajuda, forneça link para a documentação web e caminho para feedback
  • Mostre os exemplos primeiro — recomenda-se uma progressão em forma de história até chegar a casos de uso mais complexos
  • Coloque flags e comandos mais usados no topo do texto de ajuda (consulte a organização do git)
  • Use formatação como títulos em negrito para facilitar a leitura rápida, mas de forma independente do terminal
  • Quando o usuário digitar algo errado, é possível inferir a intenção e sugerir correções — porém a execução automática deve ser decidida com cautela
    • Uma entrada errada pode não ser só um typo, mas um erro lógico; além disso, ao corrigir automaticamente, surge o ônus de dar suporte permanente à sintaxe em questão

Diretrizes — Documentação (Documentation)

  • Forneça documentação baseada na web — essencial para busca e compartilhamento de links
  • Forneça documentação no terminal — sincronizada com a versão instalada e acessível offline
  • Considere fornecer páginas man — podem ser geradas com ferramentas como ronn, e recomenda-se permitir acesso por subcomando, como em npm help ls

Diretrizes — Saída (Output)

  • Legibilidade para humanos em primeiro lugar — use TTY para determinar se a saída será lida por pessoas
  • Fluxos de texto são a interface universal do UNIX, então também deve haver suporte a saída legível por máquina
  • Se uma saída amigável para humanos prejudicar a compatibilidade com pipes, forneça saída em texto simples com a flag --plain
  • Quando a flag --json for passada, ofereça saída em formato JSON
  • Em caso de sucesso, a saída deve ser concisa e, se não for necessária, ausente — para scripts, ofereça a opção -q para suprimir a saída
  • Avise o usuário quando houver mudança de estadogit push é um bom exemplo ao exibir o estado do branch remoto
  • Estruture a saída para que seja fácil verificar o estado atual do sistema e também saber o próximo passo, como em git status
  • Use cores de forma intencional e desative-as obrigatoriamente em condições como pipe, NO_COLOR, TERM=dumb e --no-color
  • Em ambientes sem TTY, não mostre animações nem spinners (para evitar poluir logs de CI)
  • Use emoji e símbolos apenas quando eles aumentarem a clareza (yubikey-agent é citado como exemplo)
  • Informações compreensíveis só para desenvolvedores devem ficar fora da saída padrão e aparecer apenas no modo verbose
  • Não use stderr como se fosse um arquivo de log — em geral, evite rótulos de nível de log como ERR e WARN
  • Em saídas longas, considere usar um pager como less — ative apenas em TTY e recomenda-se a opção less -FIRX

Diretrizes — Erros (Errors)

  • Reescreva erros previsíveis em mensagens compreensíveis para humanos (ex.: "é preciso executar chmod +w file.txt")
  • Mantenha uma boa relação sinal/ruído — erros do mesmo tipo devem ser agrupados sob um único cabeçalho
  • Coloque informações importantes no final da saída — texto vermelho deve ser usado de forma intencional e rara
  • Em caso de erro inesperado, inclua informações de debug e instruções para enviar um bug report
  • Monte a URL do bug report com informações preenchidas automaticamente para facilitar o envio

Diretrizes — Argumentos e flags (Arguments and Flags)

  • Argumentos (args) são posicionais, flags são nomeadas — prefira flags a argumentos
  • Forneça uma versão com nome completo para todas as flags (ex.: suporte simultâneo a -h e --help)
  • Flags de um único caractere devem ser limitadas às mais usadas
  • Quando houver padrão, use nomes de flags padronizados (-f/--force, -q/--quiet, -v, --json etc.)
  • Defina valores padrão que sejam adequados para a maioria dos usuários
  • Se argumentos ou flags não forem fornecidos, peça a entrada via prompt, mas nunca force prompt em ambientes não interativos
  • Antes de ações perigosas, peça confirmação — conforme o nível de risco, use confirmação y/n, ofereça dry-run ou exija digitação direta de texto
    • O risco é dividido em mild (exclusão de arquivo), moderate (exclusão de diretório, alteração de recurso remoto) e severe (exclusão de um servidor inteiro)
  • Em entrada e saída de arquivos, aceite - para ler de stdin e escrever em stdout (ex.: curl ... | tar xvf -)
  • Não receba segredos diretamente por flags — recomenda-se usar uma flag como --password-file ou stdin (devido ao risco de exposição em saída de ps e no histórico do shell)

Diretrizes — Interatividade (Interactivity)

  • Prompts e elementos interativos devem ser exibidos somente quando stdin for TTY
  • Quando --no-input for passado, desative todos os prompts
  • Ao inserir senha, desative o echo (não mostrar o conteúdo digitado na tela)
  • Oriente claramente para que o usuário possa sair a qualquer momento — Ctrl-C deve sempre continuar funcionando

Diretrizes — Subcomandos (Subcommands)

  • Mantenha consistência nos nomes de flags e no formato de saída entre subcomandos
  • Ferramentas complexas devem usar uma estrutura de subcomandos em dois níveis no formato noun verb ou verb noun (ex.: docker container create)
  • Evite subcomandos com nomes ambíguos ou parecidos (ex.: evitar usar update e upgrade ao mesmo tempo)

Diretrizes — Robustez (Robustness Guidelines)

  • Faça a validação de entrada logo no início e encerre cedo com erro compreensível quando os dados forem inválidos
  • Responsividade é mais importante que velocidade — mostre alguma coisa em até 100 ms
  • Para tarefas demoradas, forneça uma barra de progresso (progress bar) — é possível usar bibliotecas como Python(tqdm), Go(schollz/progressbar) e Node(node-progress)
  • Ao processar em paralelo, tome cuidado para que a saída não se embaralhe
  • Configure timeouts de rede — incluindo valores padrão, para evitar espera infinita
  • Depois de um erro temporário, o design deve permitir que uma nova tentativa retome do estado anterior
  • Design crash-only — estrutura capaz de encerrar imediatamente sem etapa de limpeza, garantindo idempotência

Diretrizes — Compatibilidade futura (Future-proofing)

  • Mantenha mudanças em formato aditivo e compatível com versões anteriores
  • Antes de mudanças que quebrem compatibilidade, mostre avisos prévios dentro do programa
  • Mudanças na saída para humanos geralmente são aceitáveis — para scripts, incentive o uso de --plain e --json
  • Proíba subcomandos catch-all — depois eles impedem a adição de subcomandos reais com aquele nome
  • Não permita automaticamente abreviações de subcomandos — só aliases explícitos, mantidos de forma estável
  • Proíba “bombas-relógio” — minimize dependências externas para que a ferramenta ainda possa funcionar daqui a 20 anos

Diretrizes — Sinais e caracteres de controle (Signals)

  • Ao receber Ctrl-C (sinal INT), encerre imediatamente e defina timeout para rotinas de limpeza
  • Durante a limpeza, deixe claro que um novo Ctrl-C pode forçar o encerramento (ver o exemplo do Docker Compose)
  • O programa deve ser projetado assumindo que pode iniciar sem que uma limpeza anterior tenha sido concluída

Diretrizes — Configuração (Configuration)

Prioridade de aplicação da configuração (maior → menor):

  • flags → variáveis de ambiente do shell atual → configuração no nível do projeto (.env) → configuração no nível do usuário → configuração de todo o sistema

Recomendações por tipo de configuração:

  • Configurações que mudam a cada chamada (nível de debug, dry-run): use flags

  • Configurações que variam por projeto ou máquina (caminho, cor, proxy HTTP): combinação de flags + variáveis de ambiente

  • Configurações compartilhadas por todo o projeto (tipo Makefile, package.json): use arquivos versionados

  • Siga a especificação XDG Base Directory — recomenda-se caminho de configuração baseado em ~/.config (com suporte em yarn, fish, neovim, tmux etc.)

  • Ao modificar automaticamente o arquivo de configuração de outro programa, é obrigatório obter o consentimento do usuário


Diretrizes — Variáveis de ambiente (Environment Variables)

  • Variáveis de ambiente são adequadas para comportamentos que mudam conforme o contexto de execução
  • Nos nomes, use apenas letras maiúsculas, números e underscore, sem começar por número
  • Recomenda-se valor em uma única linha — múltiplas linhas podem gerar incompatibilidade com o comando env
  • Verifique primeiro variáveis de ambiente genéricas como NO_COLOR, DEBUG, EDITOR, HTTP_PROXY, SHELL, TMPDIR, HOME, PAGER
  • Recomenda-se suportar leitura de arquivo .env por projeto — mas .env não é substituto de um arquivo de configuração formal
    • Limitações de .env: não entra em controle de versão, não tem histórico, usa apenas o tipo string e é vulnerável a problemas de codificação
  • Não leia segredos de variáveis de ambiente — eles se propagam para todos os processos, podem vazar em logs e ficar expostos em Docker inspect ou systemctl show
    • Segredos devem ser recebidos apenas por arquivos de credenciais, pipes, sockets AF_UNIX ou serviços de gerenciamento de segredos

Diretrizes — Nomenclatura (Naming)

  • Use palavras simples e fáceis de lembrar — se forem genéricas demais, há risco de conflito com outros comandos
  • Use apenas minúsculas e, quando necessário, hífens (curl é um bom exemplo, DownloadURL é um mau exemplo)
  • Mantenha nomes curtos, mas nomes extremamente curtos como cd, ls e ps ficam reservados para utilitários genéricos
  • O caso de renomeação do antecessor do Docker Compose, plumfigdocker compose, mostra na prática que a facilidade de digitação é um critério importante de naming

Diretrizes — Distribuição (Distribution)

  • Sempre que possível, distribua como binário único — usando ferramentas como PyInstaller
  • Se binário único não for viável, use instaladores nativos da plataforma
  • Explique como desinstalar no final das instruções de instalação

Diretrizes — Dados analíticos (Analytics)

  • Não envie dados de uso nem dados de crash sem o consentimento do usuário
  • Ao coletar, divulgue claramente o que será coletado, por quê, como será anonimizado e por quanto tempo será retido
  • Recomenda-se opt-in por padrão — se usar opt-out, isso deve ser informado claramente na primeira execução ou no site
    • São apresentados três exemplos: Angular.js (opt-in explícito), Homebrew (Google Analytics, FAQ pública) e Next.js (estatísticas anônimas ativadas por padrão)
  • Como alternativas à análise, é possível usar instrumentação da documentação web, medição de downloads e entrevistas diretas com usuários

1 comentários

 
GN⁺ 2024-02-07
Comentários do Hacker News
  • Hoje em dia, muitas pessoas não sabem o que é a linha de comando e também não têm interesse em saber por que deveriam usá-la.

    • Nos anos 1980 a situação era a mesma, mas hoje há mais pessoas que conhecem a linha de comando do que nunca. Dá para dizer que é a era de ouro da CLI (interface de linha de comando).
  • Em scripts, não permita abreviações arbitrárias de subcomandos. Por exemplo, se você permitir mycmd ins ou mycmd i em vez de mycmd install, não será mais possível adicionar um novo comando que comece com i.

    • Em scripts, deve-se evitar o uso de argumentos curtos. Argumentos curtos oferecem a conveniência de reduzir a digitação para humanos, mas em scripts o custo de escrever explicitamente é baixo e, considerando a proporção entre leitura e escrita, isso é preferível.
  • Considere a opção --dry-run. A capacidade de mostrar antecipadamente o que será feito sem realizar mudanças de fato é muito útil para aprender a ferramenta e verificar se opções complexas foram configuradas corretamente.

  • Quando stdout não for um terminal interativo, não mostre animações. Isso evita que barras de progresso transformem logs de CI em uma árvore de Natal.

    • Nunca mostre animações em stdout. stderr é para logging, informação etc., e stdout deve fornecer uma saída útil independentemente de ser tty ou não.
  • Use símbolos e emojis apenas quando eles aumentarem a clareza.

    • Símbolos e emojis podem não ser renderizados de forma consistente entre terminais e podem dividir opiniões conforme o gosto do usuário, então devem ser usados com muito cuidado.
  • A linha de comando Unix hoje é, por um lado, “surpreendentemente útil” e, por outro, tem “falhas de design”.

    • O motivo de a linha de comando Unix ser útil fica claro quando se pensa no tempo necessário para fazer a mesma coisa em C ou Rust.
    • As falhas de design vêm do fato de que a interface de linha de comando precisa ser legível ao mesmo tempo por humanos e por máquinas. Não existe uma solução canônica para esse problema.
  • Exceto quando a CLI é muito grande e exige aninhamento (como aws), a maioria dos apps prefere imprimir todas as opções na ajuda e deixar que o usuário use less para encontrar o que precisa.

  • Tradicionalmente, os comandos UNIX eram escritos assumindo que seriam usados principalmente por outros programas.

    • Na prática, eles foram pensados para uso interativo dentro de um shell de login interativo. Havia uma divisão entre programas que geravam saída e filtros de texto “silenciosos”, e programas complexos eram escritos em C.
  • Não leia senhas a partir de variáveis de ambiente.

    • Senhas só devem ser recebidas por meio de arquivos de credenciais, pipes, sockets AF_UNIX, serviços de gerenciamento de segredos ou outros mecanismos de IPC.
  • O livro mais abrangente sobre diretrizes de CLI é o de Eric Raymond.

    • Já faz bastante tempo, mas ao dar uma olhada em clig.dev dá para ver que as opiniões mudaram bastante com o passar do tempo.