开发者

Bash: How to end infinite loop with any key pressed?

开发者 https://www.devze.com 2023-02-16 12:45 出处:网络
I need to write an infinite loop that stops when any key is pressed. Unfortunately this one loops only whe开发者_如何学Gon a key is pressed.

I need to write an infinite loop that stops when any key is pressed.

Unfortunately this one loops only whe开发者_如何学Gon a key is pressed.

Ideas please?

#!/bin/bash

count=0
while : ; do

    # dummy action
    echo -n "$a "
    let "a+=1"

    # detect any key  press
    read -n 1 keypress
    echo $keypress

done
echo "Thanks for using this script."
exit 0


You need to put the standard input in non-blocking mode. Here is an example that works:

#!/bin/bash

if [ -t 0 ]; then
  SAVED_STTY="`stty --save`"
  stty -echo -icanon -icrnl time 0 min 0
fi

count=0
keypress=''
while [ "x$keypress" = "x" ]; do
  let count+=1
  echo -ne $count'\r'
  keypress="`cat -v`"
done

if [ -t 0 ]; then stty "$SAVED_STTY"; fi

echo "You pressed '$keypress' after $count loop iterations"
echo "Thanks for using this script."
exit 0

Edit 2014/12/09: Add the -icrnl flag to stty to properly catch the Return key, use cat -v instead of read in order to catch Space.

It is possible that cat reads more than one character if it is fed data fast enough; if not the desired behaviour, replace cat -v with dd bs=1 count=1 status=none | cat -v.

Edit 2019/09/05: Use stty --save to restore the TTY settings.


read has a number of characters parameter -n and a timeout parameter -t which could be used.

From bash manual:

-n nchars read returns after reading nchars characters rather than waiting for a complete line of input, but honors a delimiter if fewer than nchars characters are read before the delimiter.

-t timeout

Cause read to time out and return failure if a complete line of input (or a specified number of characters) is not read within timeout seconds. timeout may be a decimal number with a fractional portion following the decimal point. This option is only effective if read is reading input from a terminal, pipe, or other special file; it has no effect when reading from regular files. If read times out, read saves any partial input read into the specified variable name. If timeout is 0, read returns immediately, without trying to read any data. The exit status is 0 if input is available on the specified file descriptor, non-zero otherwise. The exit status is greater than 128 if the timeout is exceeded.

However, the read builtin uses the terminal which has its own settings. So as other answers have pointed out we need to set the flags for the terminal using stty.

#!/bin/bash
old_tty=$(stty --save)

# Minimum required changes to terminal.  Add -echo to avoid output to screen.
stty -icanon min 0;

while true ; do
    if read -t 0; then # Input ready
        read -n 1 char
        echo -e "\nRead: ${char}\n"
        break
    else # No input
        echo -n '.'
        sleep 1
    fi       
done

stty $old_tty


Usually I don't mind breaking a bash infinite loop with a simple CTRL-C. This is the traditional way for terminating a tail -f for instance.


Pure bash: unattended user input over loop

I've done this without having to play with stty:

loop=true
while $loop; do
    trapKey=
    if IFS= read -d '' -rsn 1 -t .002 str; then
        while IFS= read -d '' -rsn 1 -t .002 chr; do
            str+="$chr"
        done
        case $str in
            $'\E[A') trapKey=UP    ;;
            $'\E[B') trapKey=DOWN  ;;
            $'\E[C') trapKey=RIGHT ;;
            $'\E[D') trapKey=LEFT  ;;
            q | $'\E') loop=false  ;;
        esac
    fi
    if [ "$trapKey" ] ;then
        printf "\nDoing something with '%s'.\n" $trapKey
    fi
    echo -n .
done

This will

  • loop with a very small footprint (max 2 millisecond)
  • react to keys cursor left, cursor right, cursor up and cursor down
  • exit loop with key Escape or q.


Here is another solution. It works for any key pressed, including space, enter, arrows, etc.

The original solution tested in bash:

IFS=''
if [ -t 0 ]; then stty -echo -icanon raw time 0 min 0; fi
while [ -z "$key" ]; do
    read key
done
if [ -t 0 ]; then stty sane; fi

An improved solution tested in bash and dash:

if [ -t 0 ]; then
   old_tty=$(stty --save)
   stty raw -echo min 0
fi
while
   IFS= read -r REPLY
   [ -z "$REPLY" ]
do :; done
if [ -t 0 ]; then stty "$old_tty"; fi

In bash you could even leave out REPLY variable for the read command, because it is the default variable there.


I found this forum post and rewrote era's post into this pretty general use format:

# stuff before main function
printf "INIT\n\n"; sleep 2

INIT(){
  starting="MAIN loop starting"; ending="MAIN loop success"
  runMAIN=1; i=1; echo "0"
}; INIT

# exit script when MAIN is done, if ever (in this case counting out 4 seconds)
exitScript(){
    trap - SIGINT SIGTERM SIGTERM # clear the trap
    kill -- -$$ # Send SIGTERM to child/sub processes
    kill $( jobs -p ) # kill any remaining processes
}; trap exitScript SIGINT SIGTERM # set trap

MAIN(){
  echo "$starting"
  sleep 1

  echo "$i"; let "i++"
  if (($i > 4)); then printf "\nexiting\n"; exitScript; fi

  echo "$ending"; echo
}

# main loop running in subshell due to the '&'' after 'done'
{ while ((runMAIN)); do
  if ! MAIN; then runMain=0; fi
done; } &

# --------------------------------------------------
tput smso
# echo "Press any key to return \c"
tput rmso
oldstty=`stty -g`
stty -icanon -echo min 1 time 0
dd bs=1 count=1 >/dev/null 2>&1
stty "$oldstty"
# --------------------------------------------------

# everything after this point will occur after user inputs any key
printf "\nYou pressed a key!\n\nGoodbye!\n"

Run this script

0

精彩评论

暂无评论...
验证码 换一张
取 消