¿Cómo hacer lo siguiente?
- Tengo un script en bucle.
- Quiero detenerlo cuando presiono una tecla; de lo contrario, continuará en 5 segundos.
Respuesta1
Algo a tener en cuenta es que los shells o cualquier aplicación que normalmente ejecuta en una terminal no interactúan con el teclado ni la pantalla.
Toman información de su entrada estándar como un flujo de bytes. Cuando esa entrada estándar proviene de un archivo normal, los bytes provienen de allí, cuando es una tubería, son datos generalmente enviados por otro proceso, cuando es algún archivo de dispositivo que puede llegar a dispositivos físicos conectados a la computadora. Por ejemplo, cuando se trata de un dispositivo de caracteres tty, se trata de datos enviados a través de alguna línea serie, normalmente por un terminal. Una terminal es algún tipo de dispositivo que transforma los eventos del teclado en secuencias de bytes.
Ahí es donde reside todo el poder de las aplicaciones de terminal. Se les ha abstraído el mecanismo de entrada, por lo que se pueden utilizar de forma interactiva o automática en scripts.
Aquí, si va a emitir este tipo de mensaje y espera unallaveevento de prensa, probablemente desee que su aplicación (su script) sea únicamente interactiva. Espere que la entrada estándar sea una terminal o reciba información de la terminal independientemente de en qué entrada estándar esté abierta.
Ahora, como se vio arriba, todas las aplicaciones que ven son flujos de bytes, esa es la función del terminal (o emulador de terminal) y de la disciplina de línea del dispositivo tty para traducir la tecla presionada en secuencias de bytes. Algunos ejemplos:
- cuando presiona la atecla, los terminales ASCII envían un byte 0x61,
- cuando presionas la £tecla, los terminales UTF-8 envían los dos bytes 0xc2 y 0xa3.
- Cuando presiona la Entertecla, los terminales ASCII envían un byte 0x0d que las disciplinas de línea tty en sistemas basados en ASCII como Linux generalmente se traducen a 0x0a.
- Cuando presiona Ctrlsolo, los terminales no envían nada, pero si lo presiona con C, los terminales envían un byte 0x03 que la disciplina de línea tty intercepta para enviar una señal SIGINT a la tarea de primer plano.
- Cuando presiona Left, los terminales generalmente envían una secuencia de bytes (varía según el terminal, las aplicaciones pueden consultar la base de datos terminfo para traducirla), el primero de los cuales es 0x1b. Por ejemplo, dependiendo del modo en el que se encuentre
xterm
, en sistemas basados en ASCII, enviará 0x1b 0x4f 0x44 o 0x1b 0x5b 0x44 (<ESC>[A
o<ESC>OA
).
Entonces aquí, las preguntas que haría serían:
- ¿Aún desea preguntarle al usuario si la entrada estándar no es una terminal?
- Si la respuesta a 1 es sí, ¿desea avisar al usuario en el terminal o mediante stdin/stdout?
- Si la respuesta a 1 es no, ¿aún deseas esperar 5 segundos entre cada iteración?
- Si la respuesta a 2 esa través de la terminal, ¿debería cancelarse el script si no puede detectar un terminal de control o volver a un modo no terminal?
- ¿Quiere considerar solo las teclas presionadas después de haber emitido el mensaje? IOW, si el usuario escribe accidentalmente una clave antes de que se emita el mensaje.
- ¿Hasta dónde está dispuesto a llegar para asegurarse de leer solo los bytes emitidos al presionar una sola tecla?
Aquí, supongo que desea que su script sea solo una aplicación interactiva de terminal e interactúe solo a través del terminal de control, dejando solo stdin/stdout.
#! /bin/sh -
# ":" being a special builtin, POSIX requires it to exit if a
# redirection fails, which makes this a way to easily check if a
# controlling terminal is present and readable:
:</dev/tty
# if using bash however not in POSIX conformance mode, you'll need to
# change it to something like:
exec 3< /dev/tty 3<&- || exit
read_key_with_timeout() (
timeout=$1 prompt=$2
saved_tty_settings=$(stty -g) || exit
# if we're killed, restore the tty settings, the convoluted part about
# killing the subshell process is to work around a problem in shells
# like bash that ignore a SIGINT if the current command being run handles
# it.
for sig in INT TERM QUIT; do
trap '
stty "$saved_tty_settings"
trap - '"$sig"'
pid=$(exec sh -c '\''echo "$PPID"'\'')
kill -s '"$sig"' "$pid"
# fall back if kill failed above
exit 2' "$sig"
done
# drain the tty's buffer
stty -icanon min 0 time 0; cat > /dev/null
printf '%s\n' "$prompt"
# use the tty line discipline features to say the next read()
# should wait at most the given number of deciseconds (limited to 255)
stty time "$((timeout * 10))" -echo
# do one read and count the bytes returned
count=$(dd 2> /dev/null count=1 | wc -c)
# If the user pressed a key like the £ or Home ones described above
# it's likely all the corresponding bytes will have been read by dd
# above, but not guaranteed, so we may want to drain the tty buffer
# again to make sure we don't leave part of the sequence sent by a
# key press to be read by the next thing that reads from the tty device
# thereafter. Here allowing the terminal to send bytes as slow as 10
# per second. Doing so however, we may end up reading the bytes sent
# upon subsequent key presses though.
stty time 1; cat > /dev/null
stty "$saved_tty_settings"
# return whether at least one byte was read:
[ "$(($count))" -gt 0 ]
) <> /dev/tty >&0 2>&0
until
echo "Hello World"
sleep 1
echo "Done greeting the world"
read_key_with_timeout 5 "Press any key to stop"
do
continue
done
Respuesta2
while true; do
echo 'Looping, press Ctrl+C to exit'
sleep 5
done
No hay necesidad de hacerlo más complicado que eso.
Se requiere lo siguiente bash
:
while true; do
echo 'Press any key to exit, or wait 5 seconds'
if read -r -N 1 -t 5; then
break
fi
done
Si read
falla (por tiempo de espera), el ciclo continúa.