shell script respond to keypress
Asked Answered
S

6

35

I have a shell script that essentially says something like

while true; do
    read -r input
    if ["$input" = "a"]; then 
        echo "hello world"           
    fi
done

That is all well, and good, but I just realized having to hit ENTER presents a serious problem in this situation. What I need is for the script to respond when a key is pressed, without having to hit enter.

Is there a way to achieve this functionality within a shell script?

Superego answered 3/6, 2014 at 13:7 Comment(5)
In case this is your real code, note you missed spaces around [ and ] in the if condition.Musette
nah this isnt my real code. but thanks for the tipSuperego
The application for the script is in a display. the former control panel was made using a mouse, and clicks as the operation mechanism. But they want to add a coin-op for data storage. The original computer died, and had been running windows 98, and none of the hardware or software would run on a new windows OS. So i deduced the individual operations, and re-wired a keyboard to serve the IO functionality. but when I came home to write the rest of the script, an login remotely, it seemed to not be working. The display audience is children. There is a substantial risk of random IOSuperego
possible duplicate of How do I prompt for input in a Linux shell script?Tubular
special keys subset: #22843396Selfacting
M
33
read -rsn1

Expect only one letter (and don't wait for submitting) and be silent (don't write that letter back).

Mikvah answered 3/6, 2014 at 13:12 Comment(8)
man, i tried that last night and it seemed like it wasnt working. but latency could have been an issue. Ill try again.Superego
@Superego Do you want to continue in the loop as this will block until something is input ?Cordoba
I want it to ask users: over and over why they want to do, after doing each step. unfortunately, Im still at home, on a windows computer.Superego
If you need to check whether input is available, try adding -t 0.01 to wait for a fraction of a second, then give up if no keypress is pending. Smaller numbers still are probably perfectly okay if you want a really tight loop. Informal experimentation suggests 0.00001 works but 0.000001 is too small.Scurlock
I used this command to create a self-updating status information script which can accept key inputs to change display. read -rsn1 -t 5 key reads one key to $key and exits, otherwise it waits 5 seconds to re-render the status information. Very handy, thanks.Chez
read: bad option: -1 on zsh also, it still waits for <Enter>Whorehouse
@Whorehouse As read is a shell built-in, this answer is shell dependent. Use -rsk1 on zsh.Mikvah
I expounded upon your answer in mine here. At first, I couldn't figure out how to store the typed char into a variable, so I demoed that.Immesh
M
20

so the final working snippet is the following:

#!/bin/bash

while true; do
read -rsn1 input
if [ "$input" = "a" ]; then
    echo "hello world"
fi
done
Mistrot answered 2/6, 2015 at 18:25 Comment(0)
C
8

Another way of doing it, in a non blocking way(not sure if its what you want). You can use stty to set the min read time to 0.(bit dangerous if stty sane is not used after)

stty -icanon time 0 min 0

Then just run your loop like normal. No need for -r.

while true; do
    read input

    if ["$input" = "a"]; then 
        echo "hello world"           
    fi
done

IMPORTANT! After you have finished with non blocking you must remember to set stty back to normal using

stty sane

If you dont you will not be able to see anything on the terminal and it will appear to hang.

You will probably want to inlcude a trap for ctrl-C as if the script is quit before you revert stty back to normal you will not be able to see anything you type and it will appear the terminal has frozen.

trap control_c SIGINT

control_c()
{
    stty sane
}

P.S Also you may want to put a sleep statement in your script so you dont use up all your CPU as this will just continuously run as fast as it can.

sleep 0.1

P.S.S It appears that the hanging issue was only when i had used -echo as i used to so is probably not needed. Im going to leave it in the answer though as it is still good to reset stty to its default to avoid future problems. You can use -echo if you dont want what you have typed to appear on screen.

Cordoba answered 3/6, 2014 at 13:52 Comment(0)
P
2

You can use this getkey function:

getkey() {
    old_tty_settings=$(stty -g)   # Save old settings.
    stty -icanon
    Keypress=$(head -c1)
    stty "$old_tty_settings"      # Restore old settings.
}

It temporarily turns off "canonical mode" in the terminal settings (stty -icanon) then returns the input of "head" (a shell built-in) with the -c1 option which is returning ONE byte of standard input. If you don't include the "stty -icanon" then the script echoes the letter of the key pressed and then waits for RETURN (not what we want). Both "head" and "stty" are shell built-in commands. It is important to save and restore the old terminal settings after the key-press is received.

Then getkey() can be used in combination with a "case / esac" statement for interactive one-key selection from a list of entries: example:

case $Keypress in
   [Rr]*) Command response for "r" key ;;
   [Ww]*) Command response for "w" key ;;
   [Qq]*) Quit or escape command ;;  
esac

This getkey()/case-esac combination can be used to make many shell scripts interactive. I hope this helps.

Progestational answered 17/12, 2017 at 23:39 Comment(1)
Availability of getkey() varies.Papilla
I
2

How to read a single key press into variable c, and print it out. This prints out the key you pressed instantly, withOUT you having to press Enter first:

read -n1 c && printf "%s" "$c"

Or, with a little more "prettifying" in the output print:

read -n1 c && printf "\nYou Pressed: %s\n" "$c"

Example output of the latter command:

$ read -n1 c && printf "\nYou Pressed: %s\n" "$c"
M
You Pressed: M

To suppress your initial keypress from being echoed to the screen, add the -s option as well, which says from the read --help menu:

-s    do not echo input coming from a terminal

Here is the final command:

read -sn1 c && printf "You Pressed: %s\n" "$c"

And a demo:

$ read -sn1 c && printf "You Pressed: %s\n" "$c"
You Pressed: p

You can also optionally separate the -sn1 argument into two arguments (-s -n1) for clarity.

References:

  1. I learned about read -n1 from @pacholik here.

See also:

  1. read_keypress.sh in my eRCaGuy_hello_world repo.
  2. I use this bash cmd in C and C++ system calls to read keys here:
    1. [my answer] Capture characters from standard input without waiting for enter to be pressed
    2. [my answer] Read Key pressings in C ex. Arrow keys, Enter key
Immesh answered 3/2, 2022 at 22:45 Comment(0)
O
1

I have a way to do this in my project: https://sourceforge.net/p/playshell/code/ci/master/tree/source/keys.sh

It reads a single key everytime key_readonce is called. For special keys, a special parsing loop would run to also be able to parse them.

This is the crucial part of it:

if read -rn 1 -d '' "${T[@]}" "${S[@]}" K; then
    KEY[0]=$K

    if [[ $K == $'\e' ]]; then
        if [[ BASH_VERSINFO -ge 4 ]]; then
            T=(-t 0.05)
        else
            T=(-t 1)
        fi

        if read -rn 1 -d '' "${T[@]}" "${S[@]}" K; then
            case "$K" in
            \[)
                KEY[1]=$K

                local -i I=2

                while
                    read -rn 1 -d '' "${T[@]}" "${S[@]}" "KEY[$I]" && \
                    [[ ${KEY[I]} != [[:upper:]~] ]]
                do
                    (( ++I ))
                done
                ;;
            O)
                KEY[1]=$K
                read -rn 1 -d '' "${T[@]}" 'KEY[2]'
                ;;
            [[:print:]]|$'\t'|$'\e')
                KEY[1]=$K
                ;;
            *)
                __V1=$K
                ;;
            esac
        fi
    fi

    utils_implode KEY __V0
Onceover answered 3/6, 2014 at 13:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.