
Este script toma la entrada del usuario línea tras línea y se ejecuta myfunction
en 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+D
y procese la línea donde Ctrl+D
se 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 read
comando 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 1
para 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 -n
es 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
bash
secuencia 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 laIFS
variable. Sin esto, no podríamos leer espacios. Lo usoread -N
en lugar deread -n
, de lo contrario no podríamos detectar nuevas líneas. La-r
opciónread
nos permite leer las barras invertidas correctamente.La
case
declaración actúa sobre cada carácter leído ($ch
). Si se detecta un EOT ($'\04'
), se establecegot_eot
en 1 y luego pasa a labreak
declaració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 laline
variable.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 >file
en una terminal y tail -f file
en otra, y luego ingresar una línea parcial en
cat
y presionar Ctrl+Dpara ver qué sucede en la salida de tail
.
Para ksh93
los 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 break
obtener 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 read
shell 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 read
función incorporada, pero puedes hacerlo con la sysread
funció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^Jbar
como 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 zsh
el editor de línea avanzado de 's para ingresar los datos y asignarlos ^D
a 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 bash
u otros shells POSIX, para un equivalente del sysread
enfoque, podría hacer algo parecido usando dd
para 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