3 pontos por GN⁺ 2024-12-14 | 1 comentários | Compartilhar no WhatsApp
  • A forma como o terminal parece se comportar é resultado não só do sistema operacional, do shell e do emulador de terminal, mas também dos costumes de cada programa; e esses costumes são mais consistentes do que parecem
  • O POSIX trata principalmente da interação entre emulador de terminal, sistema operacional e shell, além de alguns utilitários centrais, mas não define até os detalhes de comportamento de programas como htop
  • Padrões como Ctrl-C, q, Ctrl-D, as 16 cores ANSI, atalhos de teclado do readline, desativar cores ao enviar saída para pipe e o significado de - como stdin/stdout se repetem
  • Comportamentos que parecem leis da natureza na verdade precisam ser implementados pelo programa ou pela biblioteca de entrada; exemplos disso são o encerramento com Ctrl-D no ipython e a interrupção de busca com Ctrl-C no prompt-toolkit
  • Ao criar um novo programa de terminal ou tentar entender o comportamento de uma ferramenta, é mais prático começar pelas expectativas padrão e pelas exceções do que pela ideia de que “tudo é possível”

Os quatro agentes que moldam o comportamento do terminal

  • O que acontece no terminal normalmente pode ser visto como o resultado conjunto de quatro agentes
    • sistema operacional
    • shell
    • emulador de terminal
    • o programa que está rodando no momento, como top, vim ou cat
  • Em combinações como bash, GNOME Terminal e Linux, os três primeiros elementos são relativamente bem conhecidos, e parte do comportamento é padronizada pelo POSIX
  • Pode parecer que cada programa individual pode fazer qualquer coisa, mas na experiência real de uso eles se comportam de forma bastante consistente

Há poucos padrões, mas existem costumes

  • Quase não há padrões de fato que definam de forma abrangente o comportamento de programas de terminal
  • Há duas referências próximas
    • POSIX: trata principalmente da interação entre emulador de terminal, sistema operacional e shell, e também define parte do comportamento de alguns utilitários centrais, como cp
    • command line interface guidelines: um guia sobre costumes de interfaces de linha de comando
  • Essas regras não são prescrições que autores de programas sejam obrigados a seguir, e sim algo mais próximo de costumes técnicos observados ao longo de 20 anos usando terminal
  • Há muitas exceções, e muitas vezes elas têm bons motivos

Algumas regras precisam ser implementadas diretamente pelo programa

  • A responsabilidade do programa é relativamente clara em coisas como a localização de arquivos de configuração, a saída de --help e a convenção de escrever a saída normal em stdout e erros em stderr
  • Já outros comportamentos parecem naturais, mas na prática precisam ser tratados diretamente pelo programa ou pela biblioteca de entrada
  • Encerrar um REPL com Ctrl-D é um exemplo representativo
    • Em cat, sem implementação extra, o sistema operacional retorna EOF em cooked mode
    • O ipython implementa o tratamento de Ctrl-D no código do prompt-toolkit
  • Entender essa responsabilidade de implementação torna menos surpreendente o fato de os detalhes variarem um pouco de programa para programa

Regra 1: programas não interativos encerram com Ctrl-C

  • Programas não interativos geralmente encerram quando se pressiona Ctrl-C
  • Isso acontece porque, se o programa não configurar um manipulador para o sinal SIGINT, o comportamento padrão ao receber Ctrl-C é terminar
  • Essa regra não se aplica da mesma forma a programas interativos como python3, bc e less
    • Em programas interativos, Ctrl-C pode interromper a tarefa atual em vez de encerrar o programa
    • Exemplos são uma busca no less ou código Python em execução no python3
  • A biblioteca de entrada prompt-toolkit, usada pelo ipython, tem código para interromper uma busca com Ctrl-C

Regra 2: TUIs normalmente encerram com q

  • Programas TUI como less e htop normalmente encerram quando se pressiona q
  • Essa regra não se aplica a programas em que encerrar com q não seria natural
    • tmux
    • editores de texto

Regra 3: REPLs encerram com Ctrl-D em uma linha vazia

  • REPLs como python3 e ed normalmente encerram quando se pressiona Ctrl-D em uma linha vazia
  • Ao executar um programa como cat em cooked mode, o sistema operacional retorna EOF para Ctrl-D em uma linha vazia
  • sqlite3, python3, fish, bash e outros não usam cooked mode de fato, mas implementam esse atalho para imitar o comportamento padrão
  • Há exemplos de implementação em dois lugares
  • O REPL de Erlang é uma exceção: ele não encerra com Ctrl-D

Regra 4: não ir além das 16 cores básicas

  • Programas de terminal em geral evitam usar cores fora das 16 cores ANSI básicas
  • Definir cores hex diretamente aumenta muito a chance de conflito com a cor de fundo do usuário
    • Por exemplo, texto #EEEEEE quase não aparece sobre fundo branco, mas pode ficar bom em fundo escuro
  • Ao usar as 16 cores básicas, é mais provável que o usuário já tenha configurado no emulador de terminal cores que combinem bem com o fundo
  • Isso também reduz as suposições sobre as cores suportadas pelo emulador de terminal
  • Há exceções, como editores de texto
    • O Helix usa por padrão um fundo roxo que não faz parte das cores ANSI básicas
    • A ideia é que o Helix não é um programa “essencial” e que, se o usuário não gostar do esquema de cores, pode trocar o tema

Regra 5: suporte parcial a atalhos do readline

  • Quase todo programa, quando faz sentido, oferece algum nível de suporte a atalhos de teclado do readline
  • Muitos programas não usam readline diretamente, mas imitam atalhos no estilo emacs/readline
  • Nem todos os programas imitam isso exatamente da mesma forma
    • O atuin parece usar Ctrl-A como prefixo e, por isso, Ctrl-A não leva ao início da linha
  • Esses programas podem implementar seu próprio buffer de recortar e colar, permitindo apagar a linha com Ctrl-U e depois colar com Ctrl-Y
  • Também há exceções
    • Programas como git, cat e nc não oferecem edição de linha além de backspace, Ctrl-W e Ctrl-U
    • Editores de texto têm cada um seu próprio modo de edição
  • O tema relacionado é tratado com mais profundidade em entering text in the terminal is complicated

Regra 5.1: Ctrl-W apaga a última palavra

  • Fora de editores de texto, a autora não se lembra de ter visto programas em que Ctrl-W não apague a última palavra
  • Em cooked mode, o sistema operacional por padrão apaga a última palavra ao pressionar Ctrl-W e apaga a linha inteira ao pressionar Ctrl-U
  • Muitos programas imitam esse comportamento padrão
  • Fora editores de texto, não vêm à mente outras exceções

Regra 6: desativar cores ao escrever para um pipe

  • A maioria dos programas desativa cores quando a saída vai para um pipe
  • Exemplos
    • rg blah destaca blah ao escrever no terminal, mas desativa o destaque ao escrever para um pipe ou arquivo
    • ls --color=auto usa cores ao escrever no terminal, mas não usa cores ao escrever para um pipe
  • O formato também pode mudar conforme o destino da saída seja ou não um terminal
    • ls organiza arquivos em colunas no terminal
    • o ripgrep agrupa correspondências com títulos
  • Se você quiser forçar a visualização das cores, pode usar unbuffer para fazer a saída do programa parecer um tty
unbuffer rg blah |  less -R
  • Alguns programas permitem forçar cores com a flag --color
    • O exemplo acima também pode ser feito com rg --color=always | less -R

Regra 7: - significa stdin ou stdout

  • Em geral, quando se passa - no lugar de um nome de arquivo, o programa lê da stdin ou escreve na stdout, conforme o sentido fizer sentido
  • Para formatar código Python que está na área de transferência com black e depois copiá-lo de volta, dá para fazer assim
pbpaste | black - | pbcopy
  • pbpaste é um programa do Mac; no Linux, dá para fazer algo parecido com xclip
  • Aparentemente a maioria dos programas implementa isso quando faz sentido, embora possa haver muitas exceções

Por que leva tanto tempo para aprender essas regras

  • Essas regras normalmente são aprendidas em várias etapas, e não de forma explícita de uma vez só
    • Primeiro se aprende que a regra existe, como em “Ctrl-C encerra o programa”
    • Depois se descobrem exceções, como o fato de find encerrar e less não
    • Em seguida se percebe, quase sem notar, o padrão de que programas não interativos encerram, enquanto programas interativos podem apenas interromper a tarefa atual
    • Só mais tarde isso consegue ser organizado como uma regra explícita
  • Grande parte da compreensão sobre terminais ainda fica no nível de reconhecimento inconsciente de padrões
  • Colocar essas regras por escrito ajuda outras pessoas a aprender o comportamento do terminal um pouco mais rápido

1 comentários

 
GN⁺ 2024-12-14
Opiniões no Hacker News
  • Além disso, programas de terminal devem respeitar as cores padrão de primeiro plano/plano de fundo do usuário e não alterá-las sem motivo.
    Mesmo ao usar cores, o conteúdo deve continuar legível quaisquer que sejam o fundo/primeiro plano padrão e a paleta de cores do terminal; se o significado for transmitido apenas por cor, ele desaparece ao copiar/colar ou em terminais sem suporte a cores.
    Emojis e caracteres não ASCII desnecessários também devem ser usados com cuidado; em uma saída padrão que rola pela tela, é melhor não apagar informações importantes como alertas exibidos temporariamente ou nomes de arquivos.
    Se você quiser usar muitas cores chamativas e animações, no mínimo ofereça uma opção de configuração que permita desativá-las facilmente.

    • Alguns anos atrás, no Docker, encontrei um bug muito irritante em que, quando um script imprimia um emoji na saída padrão, o terminal interativo travava e até o contêiner era encerrado.
      Não sei se a causa raiz foi corrigida, mas acabei tendo de criar PRs de contorno em vários projetos open source, abrindo mão de um pouco da intenção artística original: https://github.com/docker-archive/toolbox/issues/695
    • Tornar cores legíveis quaisquer que sejam o fundo/primeiro plano padrão e a paleta de cores do terminal é, na prática, quase impossível.
      Ainda assim, é possível oferecer cores personalizadas.
    • Como o texto original deixa claro, essas regras são observações técnicas, não prescrições.
      Em geral, é mais uma consolidação de coisas que se pode presumir que todos os programas de terminal já seguem.
    • Há uma tentativa de padronizar isso: https://bixense.com/clicolors/
    • Emojis e caracteres não ASCII desnecessários precisam ser mais enfatizados.
      Hoje em dia, alguns autores criam ferramentas assumindo que o usuário usa uma fonte com patches, o que pode prejudicar bastante a usabilidade.
  • Bom texto.
    Como foi dito que é difícil aprender essas convenções, também recomendo o Command Line Interface Guidelines, que é minha referência preferida ao pensar em CLIs: https://clig.dev
    Ele inclui regras do texto original, como encerrar com Ctrl-C, aceitar - na entrada padrão e desativar cores em pipes, além de muito mais.

    • Esse material já é referenciado logo depois do sumário, no início do texto.
    • As leituras adicionais do CLIG também valem a recomendação: https://clig.dev/#further-reading
      Incluem materiais sobre POSIX, GNU e Unix, o guia de CLI da Heroku e o guia de apps CLI 12-factor.
    • Há também isto: https://usage.jdx.dev/
  • O ponto de que “a saída normal deve ir para stdout, e erros para stderr” é realmente importante.
    A saída padrão deve ser reservada apenas para os dados que o programa foi solicitado a gerar. Se o usuário pediu dados JSON, a saída padrão deve conter exatamente aquele objeto JSON, e absolutamente nada mais.
    O nome erro padrão é, na verdade, meio mal escolhido; ele é mais como um “fluxo padrão do usuário” para onde devem ir todas as mensagens que o usuário precisa ler no terminal. Isso inclui não só mensagens de erro, mas também mensagens de status e saída detalhada.
    Assim, a saída do programa pode ser encaminhada por pipe para outros programas sem atritos, enquanto informações que não são dados ainda vão para o terminal ou podem ser redirecionadas para outro lugar.
    Seria bom se um programa pudesse criar facilmente descritores de arquivo de conexão de terminal para finalidades específicas e documentar os números no manual, como faz com códigos de saída. Para compatibilidade, o padrão poderia ser enviar para stdout ou stderr, e estou pensando em experimentar um pouco com isso.

  • Mais alguns pontos: se Ctrl-E e Ctrl-W, atalhos de teclado do readline/emacs, são novidade para você, é bom saber que a maioria das fontes de entrada do macOS usa esses atalhos.
    No macOS, você pode testar Ctrl-E, Ctrl-W e Ctrl-U agora mesmo na barra de endereço do navegador.
    Se estiver usando um programa de linha de comando que não oferece nenhuma edição de linha, você pode instalar rlwrap e executar o REPL por baixo dele. Por exemplo, o Standard ML of New Jersey tem um REPL, mas não tem edição de linha, então dá para complementar com rlwrap smlnj.
    Sobre “não use mais de 16 cores”, eu iria além e diria “não use mais de 8 cores ou, no mínimo, torne as cores configuráveis”. Paletas populares como Solarized e o Base 16 padrão às vezes usam as cores “claras” para vários tons de cinza, então o verde claro que o autor imaginou pode, na prática, ser o mesmo cinza do texto comum.

    • Ao pressionar Ctrl-W na barra de endereço do macOS, na maioria dos navegadores que já usei, a aba atual é fechada.
      O mesmo acontece no emulador de terminal que uso, o Alacritty, e na maioria dos exploradores de arquivos com abas. Se me lembro bem, o Windows Explorer também funciona assim agora, mas faz bastante tempo que não uso Windows.
  • A convenção de encerrar com Ctrl-D em um REPL sempre me pega no GHCi
    Normalmente aperto Ctrl-D, fico confuso porque nada acontece, então lembro que no GHCi não funciona e executo :q
    Aí o Ctrl-D coloca um caractere invisível no começo da entrada, causando o erro “lexical error at character '\EOT'”, e preciso digitar :q de novo, sem o prefixo invisível, para o GHCi encerrar

    • Tente sair do vi em um teclado de um país desconhecido e você vai achar isso ainda mais difícil
    • Eu sempre encerro o GHCi com Ctrl-D
      Nem sei muito bem qual seria o outro jeito; existe alguma configuração que mude esse comportamento?
    • Você está usando Windows?
  • Uma coisa que ficou faltando é o comportamento de tratar ~ como o diretório home
    Isso parece ser uma funcionalidade do shell, não uma API POSIX. Em Go, os.ReadFile("~/.bashrc") não funciona, mas no shell echo ~/~/~ é expandido para algo como /home/jrockway/~/~
    yourprogram ~/path/to/file sempre funciona, mas em um REPL que pede um nome de arquivo pode não funcionar. Na prática, muitos softwares dão suporte a isso, então acho que se encaixa em “o que a maioria dos programas TUI faz”

    • A expansão de til é parte da expansão de palavras do shell: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06_01
      Na expansão de palavras, a expansão de til é a primeira etapa, seguida por divisão de campos, expansão de nomes de caminho e remoção de aspas
      A especificação relacionada do shell está em https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_06, e a especificação completa pode ser vista em https://pubs.opengroup.org/onlinepubs/9799919799/
    • Acho que isso deve continuar sendo uma funcionalidade do shell
      Se uma ferramenta adicionar expansão de til, ela precisa saber qual é o diretório home do usuário atual, o que complica a implementação, e pode acabar com uma implementação incompleta em que ~ funciona, mas ~name não
      Em shells POSIX, ~name é expandido para o diretório home daquele usuário, então é preciso consultar o diretório home de usuários arbitrários, incluindo usuários do sistema. Dependendo da configuração do NSS, isso pode ser muito mais complexo do que ler /etc/passwd
      Em uma TUI interativa, implementar expansão de til pode ser útil, mas nesse caso ela deveria ser tão completa quanto a do shell. Programas CLI fazem melhor em não realizar esse tipo de expansão mágica
    • Se você for colocar isso no seu próprio REPL, precisa levar documentação, escapes e tratamento de aspas realmente a sério
      Surgem questões como o que fazer se existir um arquivo chamado "~", o que fazer com "~username", como escapar/citar, o que fazer se $HOME não existir ou for diferente da home real, e como lidar com Windows
      Um fato pouco conhecido: no ssh, se você digitar "~." depois de uma quebra de linha, o cliente força o fechamento da conexão e o servidor não pode fazer nada; então é melhor não incorporar isso ao seu fluxo de trabalho
    • Um caso em que esse problema sempre me incomoda é algo como ./myprogram --config=~/.config/myprogram
      É claro que muitos parsers de flags permitem usar a forma que o shell expande, ./myprogram --config ~/.config/myprogram, então você acaba sabendo disso ou dando suporte por acaso
      Mas, por memória muscular, sempre acabo digitando =. Acho que é um hábito vindo de uma biblioteca de parsing de flags de um emprego antigo e, pelo mesmo motivo, também sempre uso --underscores_like_this em vez de --hyphens-like-normal-people
    • Boa observação
      Especialmente porque .. é tratado no nível da API do SO, é fácil esperar que no POSIX ~ também seja assim
  • O principal motivo de eu gostar muito mais de CLI do que de GUI, e às vezes até de TUI, é que ela parece muito mais consistente
    Existem convenções, e seguir todas as convenções na CLI é muito mais fácil do que projetar uma boa GUI. Por isso, no fim, a qualidade tende a ser maior
    Penso muito em como trazer essa propriedade para GUIs, mas até agora as melhores respostas são algo como “muito esforço” ou “baixar as expectativas”

    • Acho que um grande motivo é que, com exceção do macOS, as convenções de GUI não são fortemente implementadas pelo SO nem fáceis de adotar
      Outros sistemas operacionais não conseguiram consolidar uma tecla meta para atalhos de GUI, o que gera muito mais conflitos como ctrl-c
      Os padrões do framework de aplicações que serve de base para apps nativos do macOS oferecem muitas operações comuns de teclado. Se você usa a classe padrão da barra de menus e os comandos padrão, Command O, N, Q, X, C, V etc. vêm quase de graça; e, se usa campos de texto padrão, a navegação com Command/Option/Fn + setas também aparece automaticamente
      O Windows também é bastante bom nesse aspecto, mas o fato de a tecla modificadora padrão ser Ctrl pode atrapalhar o uso de apps baseados em terminal, e vários frameworks de UI dentro do próprio SO parecem ter padrões diferentes
      No mundo Linux, acho que isso só seria possível se alguém projetasse um framework de aplicações completo, as distribuições o padronizassem e os apps fossem portados para ele. Gerenciadores de janelas não querem ditar o comportamento dentro das janelas, toolkits de GUI não querem definir padrões para o SO inteiro, e ambientes de desktop e distribuições também não querem assumir a tarefa de impor um framework de UI ou portar apps
  • Programas não devem adicionar arquivos ao diretório home e devem respeitar configurações como XDG_CONFIG_HOME

    • Concordo, mas este texto, como o autor claramente tenta deixar claro, é mais descritivo do que prescritivo
      Não é um texto tentando convencer sobre como aplicações deveriam se comportar, e sim listando comportamentos comumente vistos em aplicações
  • Uma exceção à regra de que “Ctrl-W deve apagar a última palavra” é o mysql(1)
    Como o mysql é linkado à editline em vez de readline, por padrão o Ctrl-W apaga tudo até o início da linha, não apenas a última palavra
    Quando precisei usar isso por um tempo, uma query que eu tinha escrito com todo cuidado desaparecia inteira, e eu ficava realmente enlouquecido

    • Poderia ser pior
      Na faculdade, tive o azar de decidir usar o sqlplus para me conectar a um banco de dados Oracle; atalhos de terminal como Ctrl-W simplesmente não funcionavam, e além disso não dava para mover o cursor para trás dentro da linha nem havia navegação pelo histórico para voltar a comandos anteriores
      Bastava um único erro de digitação e era preciso digitar tudo de novo desde o começo; digitando apenas / dava para executar exatamente o último comando, mas isso não ajudava a corrigir erros
      Também não dava para limpar a tela com Ctrl-L; em vez disso, era preciso executar clear scr manualmente. Parece que usar simplesmente clear como comando teria sido óbvio demais
  • Eu gostaria de acrescentar que processos de longa duração devem reler a configuração ao receber SIGHUP

    • Esta não é uma lista de “coisas que deveriam ser feitas”, mas de “coisas que em geral são assim”
      A diferença é grande e, infelizmente, este item não é universal o suficiente para entrar na lista original
    • Estritamente falando, isso provavelmente fica fora do escopo deste texto
      Processos de longa duração que reagem a SIGHUP normalmente rodam desacoplados, sem TTY de controle, então é meio forçado chamá-los de “programas de terminal”
    • Esse comportamento não é difícil de implementar na prática?
      É preciso tomar cuidado para que a aplicação não faça cache por engano não só do estado da configuração, mas também do estado derivado dela; caso contrário, eles ficam inconsistentes entre si
      Em apps simples isso pode ser fácil, mas a maioria dos apps que precisam de um arquivo de configuração não é tão simples assim