Como ler a entrada do usuário linha a linha até Ctrl + D e incluir a linha onde o Ctrl + D foi digitado

7

Esse script leva a linha de entrada do usuário após a linha e executa myfunction em cada linha

#!/bin/bash
SENTENCE=""

while read word
do
    myfunction $word"
done
echo $SENTENCE

Para interromper a entrada, o usuário precisa pressionar [ENTER] e, em seguida, Ctrl+D .

Como posso recriar meu script para terminar apenas com Ctrl+D e processar a linha em que Ctrl+D foi pressionado.

    
por user123456 13.10.2016 / 20:58

2 respostas

4

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ável IFS . Sem isso, não poderíamos ler espaços. Eu uso read -N em vez de read -n , caso contrário, não conseguiríamos detectar novas linhas. A opção -r para read 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ção break , 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ável line .

  • 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 .

    
por 24.01.2017 / 10:21
1

No modo padrão do dispositivo terminal, a chamada do sistema read() (quando chamada com um buffer grande o suficiente) levaria linhas completas. Os únicos momentos em que os dados lidos não terminariam em um caractere de nova linha seriam quando você pressionar Ctrl-D .

Nos meus testes (no Linux, FreeBSD e Solaris), um único read() produz apenas uma única linha, mesmo que o usuário tenha digitado mais quando read() for chamado. O único caso em que os dados lidos poderiam conter mais de uma linha seria quando o usuário inserir uma nova linha como Ctrl + V Ctrl + J (o caractere literal seguinte seguido por um caractere de nova linha literal (ao contrário de um retorno de carro convertido para nova linha quando você pressiona Enter )).

No entanto, o shell read incorporado lê um byte de entrada de cada vez até ver um caractere de nova linha ou fim de arquivo. Esse fim de arquivo seria quando read(0, buf, 1) retornasse 0, o que só pode acontecer quando você pressionar Ctrl-D em uma linha vazia.

Aqui, você deseja fazer leituras grandes e detectar o Ctrl-D quando a entrada não terminar em um caractere de nova linha.

Você não pode fazer isso com o read incorporado, mas pode fazê-lo com o sysread incorporado em zsh .

Se você deseja contabilizar o usuário que está 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 como um único registro (com uma nova linha incorporada), suponha que 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

Como alternativa, com zsh , você pode usar o próprio editor de linha avançada do zsh para inserir os dados e mapear ^D lá para um widget que sinalize o final 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 abordagem sysread , você poderia fazer algo se aproximando usando dd para fazer as chamadas do sistema read() :

#! /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
    
por 24.01.2017 / 12:00