- 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
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
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.
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
Ainda assim, é possível oferecer cores personalizadas.
Em geral, é mais uma consolidação de coisas que se pode presumir que todos os programas de terminal já seguem.
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.Incluem materiais sobre POSIX, GNU e Unix, o guia de CLI da Heroku e o guia de apps CLI 12-factor.
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.
Ele tem 6 fluxos diferentes, e cada um pode ser interceptado separadamente ou configurado de forma diferente: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_output_streams?view=powershell-7.4
grepnão funcionarão.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
rlwrape 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 comrlwrap 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.
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
:qAí o Ctrl-D coloca um caractere invisível no começo da entrada, causando o erro “lexical error at character '\EOT'”, e preciso digitar
:qde novo, sem o prefixo invisível, para o GHCi encerrarNem sei muito bem qual seria o outro jeito; existe alguma configuração que mude esse comportamento?
Uma coisa que ficou faltando é o comportamento de tratar
~como o diretório homeIsso parece ser uma funcionalidade do shell, não uma API POSIX. Em Go,
os.ReadFile("~/.bashrc")não funciona, mas no shellecho ~/~/~é expandido para algo como/home/jrockway/~/~yourprogram ~/path/to/filesempre 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”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/
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~namenãoEm 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/passwdEm 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
Surgem questões como o que fazer se existir um arquivo chamado
"~", o que fazer com"~username", como escapar/citar, o que fazer se$HOMEnão existir ou for diferente da home real, e como lidar com WindowsUm 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./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 acasoMas, 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_thisem vez de--hyphens-like-normal-peopleEspecialmente porque
..é tratado no nível da API do SO, é fácil esperar que no POSIX~também seja assimO 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”
Outros sistemas operacionais não conseguiram consolidar uma tecla meta para atalhos de GUI, o que gera muito mais conflitos como
ctrl-cOs 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
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
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 errosTambém não dava para limpar a tela com Ctrl-L; em vez disso, era preciso executar
clear scrmanualmente. Parece que usar simplesmenteclearcomo comando teria sido óbvio demaisEu gostaria de acrescentar que processos de longa duração devem reler a configuração ao receber SIGHUP
A diferença é grande e, infelizmente, este item não é universal o suficiente para entrar na lista original
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”
É 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