【问题标题】:Bash: How to end infinite loop with any key pressed?Bash:如何在按下任意键的情况下结束无限循环?
【发布时间】: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

【问题讨论】:

    标签: bash while-loop


    【解决方案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 这是针对旧的、有缺陷的系统的保护措施:stackoverflow.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】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-10-30
        • 2019-06-22
        相关资源
        最近更新 更多