Por que a tecla Enter não envia EOL?

19

A EOL do Unix / Linux é LF, avanço de linha, ASCII 10, sequência de escape \n.

Aqui está um trecho de código Python para obter exatamente um pressionamento de tecla:

import sys, tty, termios
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
    tty.setraw(sys.stdin.fileno())
    ch = sys.stdin.read(1)
finally:
    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

Quando pressiono Entermeu teclado em resposta a esse trecho, ele fornece \rretorno de carro, ASCII 13.

No Windows , Enterenvia CR LF == 13 10. * nix não é Windows; por que Enterdar 13 em vez de 10?

gato
fonte
Tente ler dois bytes.
Michael Hampton
@MichaelHampton Não, não há nada esperando nesse descritor de arquivo após a leitura de um byte
cat

Respostas:

11

Enquanto a resposta de Thomas Dickey é bastante correta, Stéphane Chazelas mencionou corretamente em um comentário à resposta de Dickey que a conversão não é imutável; faz parte da disciplina de linha.

De fato, a tradução é completamente programável.

A página do manual man 3 termios contém basicamente todas as informações pertinentes. (O link leva para o projeto de páginas de manual do Linux , que menciona quais recursos são apenas para Linux e são comuns ao POSIX ou a outros sistemas; sempre verifique a seção Conforming to em cada página.)

Os iflagatributos do terminal ( old_settings[0]no código mostrado na pergunta em Python ) possuem três sinalizadores relevantes em todos os sistemas POSIXy:

  • INLCR: Se definido, traduza NL para CR na entrada
  • ICRNL: Se definido (e IGNCRnão estiver definido), traduza CR para NL na entrada
  • IGNCR: Ignorar CR na entrada

Da mesma forma, também existem configurações de saída relacionadas ( old_settings[1]):

  • OPOST: Ativar o processamento de saída.
  • OCRNL: Mapeie CR para NL na saída.
  • ONLCR: Mapeie NL para CR na saída. (XSI; não disponível em todos os sistemas POSIX ou Single-Unix-Specification.)
  • ONOCR: Ignore (não produza) CR na primeira coluna.
  • ONLRET: Ignorar (não produzir) CR.

Por exemplo, você pode evitar confiar no ttymódulo. A operação "makeraw" apenas limpa um conjunto de sinalizadores (e define o CS8oflag):

import sys
import termios

fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
ch = None

try:
    new_settings = termios.tcgetattr(fd)
    new_settings[0] = new_settings[0] & ~termios.IGNBRK
    new_settings[0] = new_settings[0] & ~termios.BRKINT
    new_settings[0] = new_settings[0] & ~termios.PARMRK
    new_settings[0] = new_settings[0] & ~termios.ISTRIP
    new_settings[0] = new_settings[0] & ~termios.INLCR
    new_settings[0] = new_settings[0] & ~termios.IGNCR
    new_settings[0] = new_settings[0] & ~termios.ICRNL
    new_settings[0] = new_settings[0] & ~termios.IXON
    new_settings[1] = new_settings[1] & ~termios.OPOST
    new_settings[2] = new_settings[2] & ~termios.CSIZE
    new_settings[2] = new_settings[2] | termios.CS8
    new_settings[2] = new_settings[2] & ~termios.PARENB
    new_settings[3] = new_settings[3] & ~termios.ECHO
    new_settings[3] = new_settings[3] & ~termios.ECHONL
    new_settings[3] = new_settings[3] & ~termios.ICANON
    new_settings[3] = new_settings[3] & ~termios.ISIG
    new_settings[3] = new_settings[3] & ~termios.IEXTEN
    termios.tcsetattr(fd, termios.TCSANOW, new_settings)
finally:
    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

return ch

embora, para fins de compatibilidade, convém verificar se todas essas constantes existem primeiro no módulo termios (se você executar em sistemas não POSIX). Você também pode usar new_settings[6][termios.VMIN]e new_settings[6][termios.VTIME]definir se uma leitura será bloqueada se não houver dados pendentes e por quanto tempo (em número inteiro de segundos decisivos). (Normalmente, VMINé definido como 0 e VTIME0 se as leituras devem retornar imediatamente ou para um número positivo (décimo de segundos) quanto tempo a leitura deve esperar no máximo.)

Como você pode ver, o acima (e "makeraw" em geral) desabilita toda a tradução na entrada, o que explica o comportamento que o gato está vendo:

    new_settings[0] = new_settings[0] & ~termios.INLCR
    new_settings[0] = new_settings[0] & ~termios.ICRNL
    new_settings[0] = new_settings[0] & ~termios.IGNCR

Para obter um comportamento normal, omita as linhas que limpam essas três linhas e a conversão de entrada permanece inalterada, mesmo quando "bruta".

A new_settings[1] = new_settings[1] & ~termios.OPOSTlinha desativa todo o processamento de saída, independentemente do que dizem os outros sinalizadores de saída. Você pode simplesmente omiti-lo para manter intacto o processamento de saída. Isso mantém a saída "normal", mesmo no modo bruto. (Isso não afeta se a entrada é ecoada automaticamente ou não; isso é controlado pelo ECHOcflag in new_settings[3].)

Por fim, quando novos atributos são definidos, a chamada será bem-sucedida se alguma das novas configurações tiver sido definida. Se as configurações forem confidenciais - por exemplo, se você estiver solicitando uma senha na linha de comando -, deverá obter as novas configurações e verificar se os sinalizadores importantes estão definidos / desabilitados corretamente, para ter certeza.

Se você quiser ver as configurações atuais do terminal, execute

stty -a

Os sinalizadores de entrada geralmente estão na quarta linha e os sinalizadores de saída na quinta linha, com um -nome anterior ao sinalizador, se o sinalizador estiver desmarcado. Por exemplo, a saída pode ser

speed 38400 baud; rows 58; columns 205; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?; swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc ixany imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke

Nos pseudo-terminais e nos dispositivos USB TTY, a taxa de transmissão é irrelevante.

Se você escreve scripts Bash que desejam ler, por exemplo, senhas, considere o seguinte idioma:

#!/bin/bash
trap 'stty sane ; stty '"$(stty -g)" EXIT
stty -echo -echonl -imaxbel -isig -icanon min 1 time 0

A EXITarmadilha é executada sempre que o shell sai. Ele stty -glê as configurações atuais do terminal no início do script, para que as configurações atuais sejam restauradas quando o script sair automaticamente. Você pode até interromper o script com Ctrl+ C, e ele fará a coisa certa. (Em alguns casos de canto com sinais, eu descobri que o terminal às vezes fica preso às configurações brutas / não-canônicas (exigindo que você digite reset+ Entercegamente no terminal), mas a execução stty saneantes de restaurar as configurações originais reais curou todas as vezes É por isso que está lá; uma espécie de segurança adicional.)

Você pode ler as linhas de entrada (não associadas ao terminal) usando o readbash embutido ou até ler a entrada caractere a caractere usando

IFS=$'\0'
input=""
while read -N 1 c ; do
    [[ "$c" == "" || "$c" == $'\n' || "$c" == $'\r' ]] && break
    input="$input$c"
done

Se você não definir IFScomo ASCII NUL, o interno readconsumirá os separadores, de modo que celes estarão vazios. Armadilha para jovens jogadores.

Animal Nominal
fonte
1
Oh, pelo amor de Deus, nada é sempre simples :(
gato
Estou aceitando esta resposta, porque é mais útil para mim como um dev Python, mesmo que o outro é grande
cat
2
@cat: Embora isso possa ser muito útil para você, eu ainda diria que a resposta de Thomas Dickey está mais correta . Prefiro que você aceite isso.
Animal Nominal
4
Embora você esteja disposto a renunciar ao seu representante de +15, o @cat está certo. Se uma resposta é aceita ou não, não há indicação de que seja a "mais correta" das respostas postadas. Significa apenas que esse é o OP preferido por quaisquer razões pessoais. O "mais correto" é geralmente o mais votado. Aceitar uma resposta é uma preferência pessoal, se o OP preferir o seu, não há razão para não aceitá-lo.
terdon
1
@terdon: Ok, estou corrigido, então.
Animal Nominal
30

Essencialmente "porque tem sido feito dessa maneira desde as máquinas de escrever manuais". Sério.

Uma máquina de escrever manual tinha uma carruagem na qual o papel era alimentado e avançava enquanto você digitava (carregando uma mola) e tinha uma alavanca ou chave que liberaria a carruagem, permitindo que a mola retornasse a carruagem à margem esquerda.

À medida que a entrada eletrônica de dados (teletipo, etc.) foi introduzida, eles a levaram adiante. Portanto, a Enterchave em muitos terminais seria rotulada Return.

As alimentações de linha ocorreram (no processo manual) após o retorno do carro à margem esquerda. Novamente, os dispositivos eletrônicos imitaram os dispositivos manuais, fazendo uma line-feedoperação separada .

Ambas as operações são codificadas (para permitir que o teletipo seja mais do que um dispositivo independente, criando um tipo de papel), então temos CR(retorno de carro) e LF(alimentação de linha). Esta imagem do Teletype Information do ASR 33 mostra o teclado, com Returno lado direito e Line-Feedlogo à esquerda. Estar à direita , era a chave principal:

insira a descrição da imagem aqui

O Unix apareceu mais tarde. Seus desenvolvedores gostavam de encurtar as coisas (veja todas as abreviações, mesmo creatpara "criar"). Diante de um processo possivelmente em duas partes, eles decidiram que os feeds de linha só faziam sentido se fossem precedidos por retornos de carro. Então, eles retiraram os retornos de carro explícitos dos arquivos e converteram a Returnchave do terminal para enviar o feed de linha correspondente. Apenas para evitar confusão, eles se referiram ao feed de linha como "nova linha".

Ao escrever texto no terminal, o Unix se traduz na outra direção: um avanço de linha se torna retorno de carro / avanço de linha.

(Ou seja, "normalmente": o chamado "modo cozido", em contraste com o modo "bruto", onde nenhuma tradução é feita).

Resumo:

  • retorno de carro / avanço de linha é a sequência 13 10
  • o dispositivo envia 13 (já que "para sempre" nos seus termos)
  • Sistemas tipo Unix mudam isso para 13 10
  • Outros sistemas não armazenam necessariamente apenas 10 (o Windows aceita apenas 10 ou 13 10, dependendo da importância da compatibilidade).
Thomas Dickey
fonte
1
Procurei uma bela imagem para mostrar as alavancas de uma máquina de escrever manual, mas encontrei apenas imagens de baixa resolução.
Thomas Dickey
3
Se você tivesse que digitar em um desses, também abreviaria tudo!
Michael Hampton
3
Em relação à parte da história: as máquinas de escrever manuais que usei no meu uso, semelhantes a esta, tinham apenas uma alavanca. Quando você o puxava, ele girava primeiro o rolo (alimentação de linha) e depois puxava o carro. E foi essa atração que carregou a primavera. Cada letra digitada ou aba pressionada liberaria um pouco a mola, movendo o carro de volta para a posição "descarregada", que estava no final da linha, não no início.
RealSkeptic
2
Na entrada, o CR é convertido (pela disciplina tty line) para LF, não CR LF. Está na saída (incluindo o eco da entrada) que LF é traduzida para CR LF. Quando você digita foo<Return>no modo cozido, o aplicativo lê foo\ne foo\r\né enviado de volta pela disciplina de linha para eco no terminal.
Stéphane Chazelas 22/02
2
Vídeo do uso de "retorno de carro"
RedGrittyBrick 22/02