Bash:如何在按下任意键的情况下结束无限循环?

Posted

技术标签:

【中文标题】Bash:如何在按下任意键的情况下结束无限循环?【英文标题】:Bash: How to end infinite loop with any key pressed? 【发布时间】:2011-07-14 22:26:02 【问题描述】:

我需要编写一个无限循环,在按下任意键时停止。

不幸的是,这个只有在按下一个键时才会循环。

请给点意见?

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

【问题讨论】:

【参考方案1】:

您需要将标准输入置于非阻塞模式。这是一个有效的示例:

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

编辑 2014/12/09:-icrnl 标志添加到 stty 以正确捕获 Return 键,使用 cat -v 而不是 read 以捕获空格。

如果输入数据足够快,cat 可能会读取多个字符;如果不是所需的行为,请将cat -v 替换为dd bs=1 count=1 status=none | cat -v

编辑 2019/09/05: 使用 stty --save 恢复 TTY 设置。

【讨论】:

我知道这有点离题了,但是为什么人们在 bash 中使用"x$variable" = "x" 之类的条件而不是更简单的"$variable" = ""?有什么好处吗?还是人们这样做只是因为他们是这样学习的? @Thor84no 这是针对旧的、有缺陷的系统的保护措施:***.com/a/6853353/111461 适用于几乎任何键:显然未检测到 Return 和 Space 键(此处为 OS X 10.10)。想法? 如何恢复标准输入模式? 哦,不过我觉得还是用stty --save保存原来的状态,以后再恢复比较好。【参考方案2】:

read 有多个字符参数-n 和一个可以使用的超时参数-t

来自bash manual:

-n nchars read 在读取 nchars 个字符后返回,而不是等待完整的输入行,但如果在分隔符之前读取的 nchars 个字符少于此,则使用分隔符。

-t 超时

如果在 timeout 秒内没有读取完整的输入行(或指定数量的字符),则导致 read 超时并返回失败。 timeout 可以是一个小数,小数点后面有一个小数部分。此选项仅在 read 正在从终端、管道或其他特殊文件读取输入时有效;从常规文件读取时它没有效果。如果读取超时,读取将读取的任何部分输入保存到指定的变量名称中。如果 timeout 为 0,read 立即返回,而不尝试读取任何数据。如果输入在指定的文件描述符上可用,则退出状态为 0,否则为非零。超过超时退出状态大于128。

但是,内置的 read 使用具有自己设置的终端。因此,正如其他答案所指出的,我们需要使用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

【讨论】:

像这样while ! read -t0; do echo -n .; done; read; echo Finished,但直到按下 Enter(或 Ctrl-d)才完成,即使有可能的 -s 选项,它也会回显输入,并且不尊重可能的 -d选项。 (GNU bash,版本 4.3.11) 正如@jarno 提到的,这个答案是不正确的,因为read -t 0 只有在按下 Enter 键后才会看到输入可用。 @hackerb9 谢谢,我添加了 -n 参数以避免需要输入键。 它可能需要非零超时才能工作,如下所示:echo -n x | read -t0.001 -n1 && echo caught it 再一次,@jarno 是正确的,尽管该解决方案的副作用是无限循环每次迭代都会减慢 1 毫秒。 Paul,我建议更改您的答案以显示实际的 bash 而不是伪代码,以便您验证它是否有效。【参考方案3】:

通常我不介意用简单的 CTRL-C 打破 bash 无限循环。例如,这是终止 tail -f 的传统方式。

【讨论】:

这不会破坏循环,它会破坏整个脚本 @mouviciel: 是的,但如果您要添加一些有关使用 'trap foo SIGINT' 捕获 ^C 而不退出整个脚本的信息会更好。【参考方案4】:

纯bash:无人参与的用户循环输入

我已经完成了这项工作,而无需使用 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

这会

循环占用空间非常小(最长 2 毫秒) 对 cursor leftcursor rightcursor upcursor down 键做出反应 使用 Escapeq 键退出循环。

【讨论】:

【参考方案5】:

这是另一种解决方案。它适用于任何按下的键,包括空格、回车、箭头等。

在 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

在 bash 和 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

在 bash 中,您甚至可以为 read 命令省略 REPLY 变量,因为它是那里的默认变量。

【讨论】:

如果您的循环除了等待按键之外什么都不做,最好添加例如sleep 0.1 在 while 循环中,这样循环不会占用 CPU 内核的所有可用资源。【参考方案6】:

我找到了this forum post 并将era 的帖子改写成这种非常通用的格式:

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

运行this script

【讨论】:

以上是关于Bash:如何在按下任意键的情况下结束无限循环?的主要内容,如果未能解决你的问题,请参考以下文章

Pygame:如何在不按任何键的情况下使精灵面对相同的方向

Qt C++:如何在按下按钮之前进行for循环

vim和bash

Bash参数解析逻辑陷入无限循环[重复]

shiro登陆,在登陆操作循环了10次才结束,这种情况怎么解决,相当于请求了10次登陆,但是我只登陆了一次

颤振:如何在按下后退按钮时不退出应用程序