How to do the following?

  • I have a loop script.
  • I want to stop it when i press a key,otherwise it will continue in 5 seconds.

Something to bear in mind is that shells or any applications that you typically run in a terminal don't interact with a keyboard and screen.

They take input from their stdin as a stream of bytes. When that stdin comes from a regular file, the bytes come from there, when it's a pipe, that's data typically sent by another process, when it's some device file that can come to physical devices attached to the computer. For instance, when it's a tty character device, that's data sent over some serial line typically by a terminal. A terminal is some form of device that transforms keyboard events into sequences of bytes.

That's where the whole power of terminal applications resides. The input mechanism has been abstracted for them, so they can be used interactively or automatically in scripts.

Here, if you're going to issue this kind of prompt and expecting a key press event, you probably want your application (your script) to be an interactive one only. Either expect stdin to be a terminal, or take input from the terminal regardless of what stdin is open on.

Now, as seen above, all applications see are streams of bytes, that's the terminal's (or terminal emulator) and the tty device line discipline's role to translate key pressed into sequences of bytes. A few examples:

  • when you press the a key, ASCII terminals send a 0x61 byte,
  • when you press the £ key, UTF-8 terminals send the two bytes 0xc2 and 0xa3.
  • When you press the Enter key, ASCII terminals send a 0x0d byte which tty line disciplines on ASCII-based systems like Linux typically translate to 0x0a
  • When you press Ctrl alone, terminals don't send anything, but if you press it with C, terminals send a 0x03 byte which the tty line discipline intercept to send a SIGINT signal to the foreground task
  • When you press Left, terminals typically send a sequence of bytes (varies with the terminal, applications can query the terminfo database to translate it) the first of which is 0x1b. For instance, depending on the mode it's in, xterm, on ASCII-based systems, will either send 0x1b 0x4f 0x44 or 0x1b 0x5b 0x44 (<ESC>[A or <ESC>OA).

So here, the questions I'd ask would be:

  1. Do you still want to prompt the user if stdin is not a terminal
  2. If the answer to 1 is yes, then do you want to prompt the user on the terminal, or through stdin/stdout?
  3. If the answer to 1 is no, do you still want to wait 5 seconds between each iteration?
  4. If the answer to 2 is through the terminal, should the script abort if it cannot detect a controlling terminal or fall back to a non-terminal mode?
  5. Do you want to consider only the keys pressed after you have issued the prompt. IOW, if the user accidentally type a key before the prompt is issued.
  6. To what length are you willing to go to make sure you only read the bytes issued for a single key press?

Here, I assume you want your script to be a terminal interactive application only and interact only through the controlling terminal leaving stdin/stdout alone.

#! /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:

# 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"

  # 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

  echo "Hello World"
  sleep 1
  echo "Done greeting the world"
  read_key_with_timeout 5 "Press any key to stop"
while true; do
    echo 'Looping, press Ctrl+C to exit'
    sleep 5

There is no need to make it more complicated than that.

The following requires bash:

while true; do
    echo 'Press any key to exit, or wait 5 seconds'
    if read -r -N 1 -t 5; then

If the read fails (by timing out), the loop continues.

  • Is there a way to make it press ANY key to exit? May 29 '17 at 6:45
  • @TheBlueKingLP Updated the answer.
    – Kusalananda
    May 29 '17 at 6:51
  • You'd want -r so you can also break with the backslash key. May 29 '17 at 7:35
  • @StéphaneChazelas But... that's a specific key, not any key... ;-) Ok, I'll fix it.
    – Kusalananda
    May 29 '17 at 7:46
  • Note that it also fails on ^@, but that's a known limitation of bash that can't handle NUL bytes. You can use zsh's read -sk -r5 instead. See also my answer for other considerations/limitations. May 29 '17 at 11:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.