Para fazer isso, você teria que ler caractere por caractere, não linha por linha.
Por quê? O shell provavelmente usa a função da biblioteca C padrão read()
para ler os dados que o usuário está digitando, e essa função retorna
o número de bytes realmente lidos. Se retorna zero, isso significa que tem
EOF encontrado (consulte o read(2)
manual; man 2 read
). Note que EOF
não é um personagem, mas uma condição, ou seja, a condição "não há nada
mais para ser lido ", fim de arquivo .
Ctrl + D envia um caractere de fim de transmissão
(EOT, código de caracteres ASCII 4, $''
in bash
) para o terminal
motorista. Isto tem o efeito de enviar o que há para enviar para o
esperando read()
chamada do shell.
Quando você pressiona Ctrl + D na metade
digitando o texto em uma linha, o que você digitou até agora é
enviado para o shell 1 . Isso significa que, se você digitar
Ctrl + D duas vezes depois de digitar algo
uma linha, o primeiro enviará alguns dados e o segundo
enviar nada , e a chamada read()
retornará zero e o shell
interpretar isso como EOF. Da mesma forma, se você pressionar Enter seguido
por Ctrl + D , o shell recebe o EOF de uma vez
não havia dados para enviar.
Então, como evitar ter que digitar Ctrl + D duas vezes?
Como eu disse, leia caracteres únicos. Quando você usa o shell read
comando embutido, provavelmente tem um buffer de entrada e pergunta read()
para
ler um máximo de muitos caracteres do fluxo de entrada (talvez 16
kb ou mais). Isso significa que o shell terá um monte de pedaços de 16 kb
de entrada, seguido por um pedaço que pode ser inferior a 16 kb, seguido por
zero bytes (EOF). Depois de encontrar o final da entrada (ou uma nova linha ou um
delimitador especificado), o controle é retornado ao script.
Se você usar read -n 1
para ler um único caractere, o shell usará
um buffer de um único byte em sua chamada para read()
, ou seja, ele ficará em
um loop apertado de leitura de caractere por caractere, retornando o controle para o
script de shell depois de cada um.
O único problema com read -n
é que ele define o terminal como "raw
mode ", o que significa que os caracteres são enviados como são, sem qualquer
interpretação. Por exemplo, se você pressionar Ctrl + D ,
você receberá um caractere EOT literal na sua string. Então nós temos que checar
por isso. Isto também tem o efeito colateral que o usuário será incapaz de editar a linha antes de submetê-la ao script, por exemplo pressionando Backspace , ou usando Ctrl + W (para apagar a palavra anterior) ou Ctrl + U (para apagar até o começo da linha).
Para encurtar a história: O seguinte é o ciclo final que o seu
bash
script precisa fazer para ler uma linha de entrada, enquanto ao mesmo tempo
permitindo que o usuário interrompa a entrada a qualquer momento pressionando
Ctrl + D :
while true; do
line=''
while IFS= read -r -N 1 ch; do
case "$ch" in
$'') got_eot=1 ;&
$'\n') break ;;
*) line="$line$ch" ;;
esac
done
printf 'line: "%s"\n' "$line"
if (( got_eot )); then
break
fi
done
Sem entrar em muitos detalhes sobre isso:
-
IFS=
limpa a variávelIFS
. Sem isso, não poderíamos ler espaços. Eu usoread -N
em vez deread -n
, caso contrário, não conseguiríamos detectar novas linhas. A opção-r
pararead
nos permite ler as barras invertidas corretamente. -
A instrução
case
age em cada caractere de leitura ($ch
). Se uma EOT ($''
) for detectada, ela definirágot_eot
para 1 e, em seguida, passará para a instruçãobreak
, que a extrairá do loop interno. Se uma nova linha ($'\n'
) for detectada, ela sairá do loop interno. Caso contrário, ele adiciona o caractere ao final da variávelline
. -
Após o loop, a linha é impressa na saída padrão. Isso seria onde você chama seu script ou função que usa
"$line"
. Se chegamos aqui ao detectar um EOT, saímos do loop mais externo.
1 Você pode testar isso executando cat >file
em um terminal
e tail -f file
em outro e, em seguida, insira uma linha parcial no
cat
e pressione Ctrl + D para ver o que acontece no
saída de tail
.
Para ksh93
usuários: O loop acima lerá um caractere de retorno de carro em vez de um caractere de nova linha em ksh93
, o que significa que o teste de $'\n'
precisará ser alterado para um teste de $'\r'
. O shell também os exibirá como ^M
.
Para contornar isso:
stty_saved="$( stty -g )" stty -echoctl # the loop goes here, with $'\n' replaced by $'\r' stty "$stty_saved"
Você também pode querer gerar uma nova linha explicitamente antes do break
para obter exatamente o mesmo comportamento de bash
.