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

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

Este script pega a entrada do usuário linha após linha e é executado myfunctionem 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+De processar a linha onde Ctrl+Dfoi 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 readcomando 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 bashscript 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 a IFSvariável. Sem isso, não seríamos capazes de ler espaços. Eu uso read -Nem vez de read -n, caso contrário não seríamos capazes de detectar novas linhas. A -ropção readnos permite ler as barras invertidas corretamente.

  • A caseinstrução atua em cada caractere lido ( $ch). Se um EOT ( $'\04') for detectado, ele será definido got_eotcomo 1 e então passará para a breakinstruçã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 da linevariá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 >fileem um terminal e tail -f fileem outro e, em seguida, insira uma linha parcial no cate pressione Ctrl+Dpara ver o que acontece na saída de tail.


Para ksh93usuá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 breakobter 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 readshell 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 readbuiltin, mas pode fazer isso com o sysreadbuiltin 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^Jbarum ú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 zsho 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 bashou outros shells POSIX, para um equivalente da sysreadabordagem, você poderia fazer algo próximo usando ddpara 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

informação relacionada