Cómo leer la entrada del usuario línea por línea hasta Ctrl+D e incluir la línea donde se escribió Ctrl+D

Cómo leer la entrada del usuario línea por línea hasta Ctrl+D e incluir la línea donde se escribió Ctrl+D

Este script toma la entrada del usuario línea tras línea y se ejecuta myfunctionen cada línea.

#!/bin/bash
SENTENCE=""

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

Para detener la entrada, el usuario debe presionar [ENTER]y luego Ctrl+D.

¿Cómo puedo reconstruir mi script para que termine solo Ctrl+Dy procese la línea donde Ctrl+Dse presionó?

Respuesta1

Para hacer eso, tendrías que leer carácter por carácter, no línea por línea.

¿Por qué? Es muy probable que el shell utilice la función de biblioteca estándar de C read() para leer los datos que el usuario está escribiendo, y esa función devuelve el número de bytes realmente leídos. Si devuelve cero, significa que ha encontrado EOF (consulte el read(2)manual; man 2 read). Tenga en cuenta que EOF no es un carácter sino una condición, es decir, la condición "no hay nada más que leer",fin del documento.

Ctrl+Denvía uncarácter de fin de transmisión (EOT, código de caracteres ASCII 4, $'\04'en bash) al controlador del terminal. Esto tiene el efecto de enviar todo lo que haya que enviar a la read()llamada en espera del shell.

Cuando presionas Ctrl+Da la mitad de ingresar el texto en una línea, todo lo que hayas escrito hasta ahora se envía al shell 1 . Esto significa que si ingresas Ctrl+Ddos veces después de haber escrito algo en una línea, la primera enviará algunos datos y la segunda enviaránada, y la read()llamada devolverá cero y el shell lo interpretará como EOF. Del mismo modo, si presiona Enterseguido de Ctrl+D, el shell obtiene EOF de inmediato ya que no había ningún dato para enviar.

Entonces, ¿cómo evitar tener que escribir Ctrl+Ddos veces?

Como dije, lea caracteres individuales. Cuando utiliza el readcomando integrado del shell, probablemente tenga un búfer de entrada y solicite read()leer un máximo de esa cantidad de caracteres del flujo de entrada (tal vez 16 kb aproximadamente). Esto significa que el shell obtendrá un montón de fragmentos de entrada de 16 kb, seguidos de un fragmento que puede tener menos de 16 kb, seguido de cero bytes (EOF). Una vez que se encuentra el final de la entrada (o una nueva línea, o un delimitador específico), el control regresa al script.

Si usa read -n 1para leer un solo carácter, el shell usará un buffer de un solo byte en su llamada a read(), es decir, se ubicará en un bucle cerrado leyendo carácter por carácter, devolviendo el control al script del shell después de cada uno.

El único problema read -nes que configura el terminal en "modo sin formato", lo que significa que los caracteres se envían tal como están sin ninguna interpretación. Por ejemplo, si presiona Ctrl+D, obtendrá un carácter EOT literal en su cadena. Así que tenemos que comprobarlo. Esto también tiene el efecto secundario de que el usuario no podrá editar la línea antes de enviarla al script, por ejemplo presionando Backspaceo usando Ctrl+W(para eliminar la palabra anterior) o Ctrl+U(para eliminar al principio de la línea). .

Para acortar una historia larga:El siguiente es el bucle final que su bashsecuencia de comandos debe realizar para leer una línea de entrada y, al mismo tiempo, permite al usuario interrumpir la entrada en cualquier momento presionando 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

Sin entrar en demasiados detalles sobre esto:

  • IFS=borra la IFSvariable. Sin esto, no podríamos leer espacios. Lo uso read -Nen lugar de read -n, de lo contrario no podríamos detectar nuevas líneas. La -ropción readnos permite leer las barras invertidas correctamente.

  • La casedeclaración actúa sobre cada carácter leído ( $ch). Si se detecta un EOT ( $'\04'), se establece got_eoten 1 y luego pasa a la breakdeclaración que lo saca del bucle interno. Si se detecta una nueva línea ( $'\n'), simplemente sale del bucle interno. De lo contrario, agrega el carácter al final de la linevariable.

  • Después del bucle, la línea se imprime en la salida estándar. Aquí sería donde llamarías a tu script o función que usa "$line". Si llegamos hasta aquí detectando un EOT, salimos del bucle más externo.

1 Puede probar esto ejecutando cat >fileen una terminal y tail -f fileen otra, y luego ingresar una línea parcial en caty presionar Ctrl+Dpara ver qué sucede en la salida de tail.


Para ksh93los usuarios: el bucle anterior leerá un carácter de retorno de carro en lugar de un carácter de nueva línea en ksh93, lo que significa que la prueba para $'\n'deberá cambiar a una prueba para $'\r'. El shell también los mostrará como ^M.

Para solucionar esto:

stty_saved="$(stty -g)"
stty-echoctl

#el bucle va aquí, con $'\n' reemplazado por $'\r'

stty "$stty_saved"

También es posible que desee generar una nueva línea explícitamente justo antes para breakobtener exactamente el mismo comportamiento que en bash.

Respuesta2

En el modo predeterminado del dispositivo terminal, la read()llamada al sistema (cuando se llama con un búfer lo suficientemente grande) conduciría líneas completas. Las únicas ocasiones en las que los datos leídos no terminarían en un carácter de nueva línea serían cuando presiona Ctrl-D.

En mis pruebas (en Linux, FreeBSD y Solaris), un único read()solo produce una sola línea, incluso si el usuario ha ingresado más cuando read()se llama. El único caso en el que los datos leídos podrían contener más de una línea sería cuando el usuario ingresa una nueva línea como Ctrl+VCtrl+J(el carácter literal siguiente seguido de un carácter de nueva línea literal (a diferencia de un retorno de carro convertido en nueva línea cuando presiona Enter)) .

Sin embargo, el readshell incorporado lee la entrada un byte a la vez hasta que ve un carácter de nueva línea o el final del archivo. Esofin del documentosería cuando read(0, buf, 1)devuelve 0, lo que solo puede suceder cuando presionas Ctrl-Den una línea vacía.

Aquí, querrás hacer lecturas grandes y detectar cuándo Ctrl-Dla entrada no termina en un carácter de nueva línea.

No puedes hacer eso con la readfunción incorporada, pero puedes hacerlo con la sysreadfunción incorporada de zsh.

Si desea dar cuenta del usuario que escribe ^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

Si desea considerarlo foo^V^Jbarcomo un registro único (con una nueva línea incrustada), suponga que cada uno read()devuelve un 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, con zsh, puede usar zshel editor de línea avanzado de 's para ingresar los datos y asignarlos ^Da un widget que señala el final de la 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

Con bashu otros shells POSIX, para un equivalente del sysreadenfoque, podría hacer algo parecido usando ddpara realizar las read()llamadas al 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

Respuesta3

No tengo muy claro lo que estás pidiendo, pero si quieres que el usuario pueda ingresar varias líneas y luego procesar todas las líneas en su conjunto, puedes usar mapfile. Toma la entrada del usuario hasta que se encuentra EOF y luego devuelve una matriz en la que cada línea es un elemento de la 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"

Ejemplo

$: 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

información relacionada