
Este script pega a entrada do usuário linha após linha e é executado myfunction
em cada linha
#!/bin/bash
SENTENCE=""
while read word
do
myfunction $word"
done
echo $SENTENCE
Para interromper a entrada, o usuário deve pressionar [ENTER]
e depois Ctrl+D
.
Como posso reconstruir meu script para terminar apenas Ctrl+D
e processar a linha onde Ctrl+D
foi pressionado.
Responder1
Para fazer isso, você teria que ler caractere por caractere, não linha por linha.
Por que? 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 retornar zero, significa que encontrou EOF (veja o read(2)
manual; man 2 read
). Observe que EOF não é um caractere mas sim uma condição, ou seja, a condição "não há mais nada para ser lido",fim do arquivo.
Ctrl+Denvia umcaractere de fim de transmissão
(EOT, código de caracteres ASCII 4, $'\04'
em bash
) para o driver do terminal. Isso tem o efeito de enviar tudo o que há para ser enviado para a read()
chamada em espera do shell.
Quando você pressiona Ctrl+Dno meio da inserção do texto em uma linha, tudo o que você digitou até agora é enviado para o shell 1 . Isso significa que se você digitar
Ctrl+Dduas vezes depois de digitar algo em uma linha, a primeira enviará alguns dados e a segunda enviaránada, e a read()
chamada retornará zero e o shell interpretará isso como EOF. Da mesma forma, se você pressionar Enterseguido de Ctrl+D, o shell obterá EOF imediatamente, pois não havia dados para enviar.
Então, como evitar ter que digitar Ctrl+Dduas vezes?
Como eu disse, leia caracteres únicos. Quando você usa o read
comando interno do shell, ele provavelmente possui um buffer de entrada e solicita read()
a leitura de no máximo essa quantidade de caracteres do fluxo de entrada (talvez 16 kb ou mais). Isso significa que o shell obterá vários pedaços de entrada de 16 kb, seguidos por um pedaço que pode ter menos de 16 kb, seguido por zero bytes (EOF). Ao encontrar o final da entrada (ou uma nova linha, ou um delimitador especificado), o controle é retornado ao script.
Se você usarread -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 lendo caractere por caractere, retornando o controle ao shell script após cada um.
O único problema read -n
é que ele define o terminal para o "modo bruto", o que significa que os caracteres são enviados como estão, sem qualquer interpretação. Por exemplo, se você pressionar Ctrl+D, obterá um caractere EOT literal em sua string. Então temos que verificar isso. Isso também tem o efeito colateral de que o usuário não conseguirá editar a linha antes de enviá-la ao script, por exemplo, pressionando Backspace, ou usando Ctrl+W(para excluir a palavra anterior) ouCtrl+U (para excluir o início da linha) .
Para resumir uma longa história:A seguir está o loop final que seu
bash
script precisa fazer para ler uma linha de entrada, ao mesmo tempo que permite ao usuário interromper a entrada a qualquer momento pressionando
Ctrl+D:
while true; do
line=''
while IFS= read -r -N 1 ch; do
case "$ch" in
$'\04') 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 aIFS
variável. Sem isso, não seríamos capazes de ler espaços. Eu usoread -N
em vez deread -n
, caso contrário não seríamos capazes de detectar novas linhas. A-r
opçãoread
nos permite ler as barras invertidas corretamente.A
case
instrução atua em cada caractere lido ($ch
). Se um EOT ($'\04'
) for detectado, ele será definidogot_eot
como 1 e então passará para abreak
instrução que o tira do loop interno. Se uma nova linha ($'\n'
) for detectada, ela simplesmente sairá do loop interno. Caso contrário, adiciona o caractere ao final daline
variável.Após o loop, a linha é impressa na saída padrão. Seria aqui que você chamaria seu script ou função que usa
"$line"
. Se chegamos aqui detectando 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+Dpara ver o que acontece na 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 for $'\n'
precisará mudar para um teste for $'\r'
. O shell também irá exibi-los como^M
.
Para contornar isso:
stty_saved="$(stty -g)" stty -echoctl #o loop vai aqui, com $'\n' substituído por $'\r' stty "$stty_saved"
Você também pode querer gerar uma nova linha explicitamente antes de break
obter exatamente o mesmo comportamento de bash
.
Responder2
No modo padrão do dispositivo terminal, a read()
chamada do sistema (quando chamada com um buffer grande o suficiente) levaria linhas completas. Os únicos momentos em que os dados lidos não terminariam com um caractere de nova linha seria quando você pressionasse Ctrl-D.
Em meus testes (no Linux, FreeBSD e Solaris), um single read()
só produz uma única linha, mesmo que o usuário tenha digitado mais no momento em que read()
é chamado. O único caso em que os dados lidos poderiam conter mais de uma linha seria quando o usuário insere uma nova linha como Ctrl+VCtrl+J(o próximo caractere literal seguido por um caractere de nova linha literal (em oposição a um retorno de carro convertido em nova linha quando você pressiona Enter)) .
O read
shell interno, entretanto, lê a entrada um byte por vez até ver um caractere de nova linha ou final de arquivo. Quefim do arquivoseria quando read(0, buf, 1)
retornasse 0, o que só pode acontecer quando você pressiona Ctrl-Duma linha vazia.
Aqui, você deseja fazer leituras grandes e detectar Ctrl-Dquando a entrada não termina em um caractere de nova linha.
Você não pode fazer isso com o read
builtin, mas pode fazer isso com o sysread
builtin do zsh
.
Se você quiser contabilizar o usuário digitando ^V^J
:
#! /bin/zsh -
zmodload zsh/system # for sysread
myfunction() printf 'Got: <%s>\n' "$1"
lines=('')
while (($#lines)); do
if (($#lines == 1)) && [[ $lines[1] == '' ]]; then
sysread
lines=("${(@f)REPLY}") # split on newline
continue
fi
# pop one line
line=$lines[1]
lines[1]=()
myfunction "$line"
done
Se você quiser considerar foo^V^Jbar
um único registro (com uma nova linha incorporada), suponha que cada um read()
retorne um registro:
#! /bin/zsh -
zmodload zsh/system # for sysread
myfunction() printf 'Got: <%s>\n' "$1"
finished=false
while ! $finished && sysread line; do
if [[ $line = *$'\n' ]]; then
line=${line%?} # strip the newline
else
finished=true
fi
myfunction "$line"
done
Alternativamente, com zsh
, você poderia usar zsh
o próprio editor de linha avançado para inserir os dados e mapeá ^D
-los para um widget que sinaliza o fim da entrada:
#! /bin/zsh -
myfunction() printf 'Got: <%s>\n' "$1"
finished=false
finish() {
finished=true
zle .accept-line
}
zle -N finish
bindkey '^D' finish
while ! $finished && line= && vared line; do
myfunction "$line"
done
Com bash
ou outros shells POSIX, para um equivalente da sysread
abordagem, você poderia fazer algo próximo usando dd
para fazer as read()
chamadas do sistema:
#! /bin/sh -
sysread() {
# add a . to preserve the trailing newlines
REPLY=$(dd bs=8192 count=1 2> /dev/null; echo .)
REPLY=${REPLY%?} # strip the .
[ -n "$REPLY" ]
}
myfunction() { printf 'Got: <%s>\n' "$1"; }
nl='
'
finished=false
while ! "$finished" && sysread; do
case $REPLY in
(*"$nl") line=${REPLY%?};; # strip the newline
(*) line=$REPLY finished=true
esac
myfunction "$line"
done
Responder3
Não estou muito claro sobre o que você está pedindo, mas se quiser que o usuário consiga inserir várias linhas e depois processar todas as linhas como um todo você pode usar o mapfile
. Ele recebe a entrada do usuário até que o EOF seja encontrado e então retorna uma matriz com cada linha sendo um item na matriz.
PROGRAMA.sh
#!/bin/bash
myfunction () {
echo "$@"
}
SENTANCE=''
echo "Enter your input, press ctrl+D when finished"
mapfile input #this takes user input until they terminate with ctrl+D
n=0
for line in "${input[@]}"
do
((n++))
SENTANCE+="\n$n\t$(myfunction $line)"
done
echo -e "$SENTANCE"
Exemplo
$: bash PROGRAM.sh
Enter your input, press ctrl+D when finished
this is line 1
this is line 2
this is not line 10
# I pushed ctrl+d here
1 this is line 1
2 this is line 2
3 this is not line 10