LinuxQuestions.org
Visit Jeremy's Blog.
Go Back   LinuxQuestions.org > Blogs > Nominal Animal
User Name
Password

Notices

Rate this Entry

Bash scripts and keyboard input

Posted 01-29-2012 at 02:43 PM by Nominal Animal

You can use Bash to read individual keypresses. Here is an example:
Code:
#!/bin/bash

# Reset terminal to current state when we exit.
trap "stty $(stty -g)" EXIT

# Disable echo and special characters, set input timeout to 0.2 seconds.
stty -echo -icanon time 2 || exit $?

# String containing all keypresses.
KEYS=""

# Set field separator to BEL (should not occur in keypresses)
IFS=$'\a'

# Remind user to press ESC to quit.
echo "Press Esc to quit." >&2

# Input loop.
while [ 1 ]; do

    # Read more input from keyboard when necessary.
    while read -t 0 ; do
        read -s -r -d "" -N 1 -t 0.2 CHAR && KEYS="$KEYS$CHAR" || break
    done

    # If no keys to process, wait 0.05 seconds and retry.
    if [ -z "$KEYS" ]; then
        sleep 0.05
        continue
    fi

    # Check the first (next) keypress in the buffer.
    case "$KEYS" in
      $'\x1B\x5B\x41'*) # Up
        KEYS="${KEYS##???}"
        echo "Up"
        ;;
      $'\x1B\x5B\x42'*) # Down
        KEYS="${KEYS##???}"
        echo "Down"
        ;;
      $'\x1B\x5B\x44'*) # Down
        KEYS="${KEYS##???}"
        echo "Left"
        ;;
      $'\x1B\x5B\x43'*) # Down
        KEYS="${KEYS##???}"
        echo "Right"
        ;;
      $'\x1B\x4F\x48'*) # Home
        KEYS="${KEYS##???}"
        echo "Home"
        ;;
      $'\x1B\x5B\x31\x7E'*) # Home (Numpad)
        KEYS="${KEYS##????}"
        echo "Home (Numpad)"
        ;;
      $'\x1B\x4F\x46'*) # End
        KEYS="${KEYS##???}"
        echo "End"
        ;;
      $'\x1B\x5B\x34\x7E'*) # End (Numpad)
        KEYS="${KEYS##????}"
        echo "End (Numpad)"
        ;;
      $'\x1B\x5B\x45'*) # 5 (Numpad)
        KEYS="${KEYS#???}"
        echo "Center (Numpad)"
        ;;
      $'\x1B\x5B\x35\x7e'*) # PageUp
        KEYS="${KEYS##????}"
        echo "PageUp"
        ;;
      $'\x1B\x5B\x36\x7e'*) # PageDown
        KEYS="${KEYS##????}"
        echo "PageDown"
        ;;
      $'\x1B\x5B\x32\x7e'*) # Insert
        KEYS="${KEYS##????}"
        echo "Insert"
        ;;
      $'\x1B\x5B\x33\x7e'*) # Delete
        KEYS="${KEYS##????}"
        echo "Delete"
        ;;
      $'\n'*|$'\r'*) # Enter/Return
        KEYS="${KEYS##?}"
        echo "Enter or Return"
        ;;
      $'\t'*) # Tab
        KEYS="${KEYS##?}"
        echo "Tab"
        ;;
      $'\x1B') # Esc (without anything following!)
        KEYS="${KEYS##?}"
        echo "Esc - Quitting"
        exit 0
        ;;
      $'\x1B'*) # Unknown escape sequences
        echo -n "Unknown escape sequence (${#KEYS} chars): \$'"
        echo -n "$KEYS" | od --width=256 -t x1 | sed -e '2,99 d; s|^[0-9A-Fa-f]* ||; s| |\\x|g; s|$|'"'|"
        KEYS=""
        ;;
      [$'\x01'-$'\x1F'$'\x7F']*) # Consume control characters
        KEYS="${KEYS##?}"
        ;;
      *) # Printable characters.
        KEY="${KEYS:0:1}"
        KEYS="${KEYS#?}"
        echo "'$KEY'"
        ;;
    esac
done
The crucial lines to get individual characters and not just complete input lines are the two lines
Code:
trap "stty $(stty -g)" EXIT
stty -echo -icanon time 2 || exit $?
The first one sets an exit trap, so that the terminal is restored to current mode whenever the script exits. The second one disables input echoing, special character parsing, and sets the input timeout to 2 deciseconds = 0.2 seconds.

One decisecond may be too short for some keyboards or machines; I've seen "partial" keypresses read when using 1. When using 2, the loop seems to catch all keypresses intact.

(Technically, read should just attempt a large read, but return whatever it receives from the first attempt, and only do one attempt. Unfortunately, Bash does not support that for its read built-in, so we need this loopy whing as a workaround. Fortunately, the above seems to be rock solid on all machines I've tried it on.)

You'll also need to set the field separator, IFS, to a value that does not interfere with your character comparisons. I used the beep, ASCII BEL, \a, as it should never occur in input. If you do not, you will have issues in trying to match the keypresses in a case..esac statement.

When reading keyboard input, the special keys are provided as multi-character strings. (The above script will tell you what they look like to the script.) To handle that, we need a simple loop:
Code:
    # Read more input from keyboard when necessary.
    while read -t 0 ; do
        read -s -r -d "" -N 1 -t 0.2 CHAR && KEYS="$KEYS$CHAR" || break
    done
The while loop will continue for as long as Bash thinks it has input to read. read -t 0 does not actually read anything, it will just return success if there is input available right now, and failure otherwise.

The read -s -r -d "" -N 1 -t 0.2 CHAR command tells Bash to read one character, storing it into the CHAR variable. The flags tell Bash to not echo it, to not consider backslashes special, to consider ASCII NUL (empty string) as the line separator, only read one character, and not wait more than 0.2 seconds for input.

If the read succeeds, we add it to the key buffer, and see if there is more. If it fails, we should have a full keypress, and we can break out from the keyboard loop.

In most cases, this loop should work without any delays. The timeouts are there to stop it from "hanging" in the corner cases. At worst, it might drop a character -- but I don't think it will, it's pretty carefully designed to avoid that.

Because the keyboard input is essentially delayless, and I don't want to waste CPU, I delay for 1/20th of a second before re-checking for input. It should be short enough to make it feel snappy to the user, but long enough to not waste CPU. If you disagree, just change the sleep 0.05 to suit your preference!

When keyboard input is received, it is easiest to use a case..esac statement in Bash to determine which one it is. I've included most of the interesting keypresses in the statement above, but it'll also display the ones it does not recognize, so you should not have problems in supporting any keypress in your own programs. (Just try to match the longest ones first, and you should have no problems.)

It is also important that you note that the keyboard input buffer may contain more than one character. You cannot compare the input exactly, you need to check if the start of the buffer matches, and if so, remove those from the string. You can use either KEYS="${KEYS##?}" to remove the matching prefix (with as many ? as there are chars in the keypress). KEYS="${KEYS:1}" would work exactly the same (with 1 being the number of chars to remove).

Because of the exit trap, you can let Ctrl+C interrupt the script, or exit using exit, and the trap will return the terminal to its previous working state.
Posted in Uncategorized
Views 5691 Comments 0
« Prev     Main     Next »
Total Comments 0

Comments

 

  



All times are GMT -5. The time now is 01:33 PM.

Main Menu
Advertisement

My LQ
Write for LQ
LinuxQuestions.org is looking for people interested in writing Editorials, Articles, Reviews, and more. If you'd like to contribute content, let us know.
Main Menu
Syndicate
RSS1  Latest Threads
RSS1  LQ News
Twitter: @linuxquestions
identi.ca: @linuxquestions
Facebook: linuxquestions Google+: linuxquestions
Open Source Consulting | Domain Registration