Bash: 如何通过按下任意键终止无限循环?

31

我需要写一个无限循环,当任何键被按下时停止。

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

请提供想法?

#!/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
6个回答

37
你需要将标准输入设置为非阻塞模式。这里有一个可以工作的示例:
#!/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:stty 命令中添加 -icrnl 标志以正确捕获回车键,使用 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" = ""?这样做有什么好处还是只是因为他们学习时就是这样做的? - Vala
2
@Thor84no 这是针对旧的、有缺陷的系统的保护措施:https://dev59.com/5Ww15IYBdhLWcg3wD3jp#6853353 - sam hocevar
适用于几乎所有按键:似乎无法检测到Return和Space键(这里是OS X 10.10)。有什么想法? - DavidD
感谢您的建议,我已经相应地更新了答案。 - sam hocevar
使用-g代替--save将在macOS和Linux上工作(分别测试了11.4和18.04)。 - mathandy
显示剩余2条评论

10
read 命令有两个参数 -n-t,与字符数和超时时间有关。
根据 bash 手册-n nchars 读取 nchars 个字符后返回,而不是等待完整的一行输入。如果在读取到 nchars 字符之前遇到分隔符,则会立即返回。 -t timeout 如果在 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

2
类似于 while ! read -t0; do echo -n .; done; read; echo Finished,但它不会在按下回车键(或Ctrl-d)之前结束,并且即使使用了可能的 -s 选项,它也会回显输入并且不会遵守可能的 -d 选项。(GNU bash,版本4.3.11) - jarno
1
它可能需要非零超时才能正常工作,就像这样:echo -n x | read -t0.001 -n1 && echo caught it - jarno
再次强调,@jarno是正确的,尽管这种解决方法会导致无限循环每次迭代减慢1ms。Paul,我建议你修改你的答案,展示真正的bash代码,以便你可以验证它是否有效。 - hackerb9
@hackerb9,我已经更新了手册条目,因为最近read命令的行为似乎发生了变化。我们可以利用这一点使用read -t 0来检查是否有数据可读取。我现在已经将此代码添加到答案中了。 - Paul
我想是这样的。您可以通过使用 stty --savestty --all 进行测试。您看到了什么样的差异?已经有几个答案更改终端设置... - jarno
显示剩余4条评论

3

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


这不会打破循环,它会打破整个脚本。 - InterLinked
@mouviciel:没错,但如果您添加一些关于使用“trap foo SIGINT”捕获^C而不退出整个脚本的信息会更好。 - hackerb9

2

:自动循环输入用户输入

我已经做到了这一点,而不必玩stty:

loop=true loopDelay=.05
while $loop; do
    trapKey=
    if IFS= read -d '' -rsn 1 -t $loopDelay 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;echo ;;
            * ) trapKey=${str@Q}   ;;
        esac
    fi
    if [ "$trapKey" ] ;then
        printf "\nDoing something with %s.\n" "$trapKey"
    fi
    echo -n .
done

这将

  • 以非常小的占用空间(最多2毫秒)循环执行
  • 响应键盘上的 光标左光标右光标上光标下
  • 通过按下 Esc 键或 q 键退出循环。

解释:

由于键盘不返回字符而是按下的键,某些键可能会发送多个字符,比如 Home 键应该发送一个转义序列\e[H,其中包含3个字符。

为了支持这一点,我使用了一个超小的超时read命令来构建一个循环:

if IFS= read -d '' -rsn 1 -t $LOOPDELAY str; then
    while IFS= read -d '' -rsn 1 -t .002 chr; do
        str+="$chr"
    done

如果在$LOOPDELAY之后没有读取任何字符,则表示没有读取键盘按键。否则,$str变量将由所有可以在不到0.002秒的时间内访问的read字符完成。

更好的版本,支持Fx键:

#!/bin/bash

loopDelay=.042

# printf -v shapes "%b " \\U28{01,08,10,20,80,40,04,02}
printf -v shapes "%b " \\U28{19,38,B0,e0,c4,46,07,0b}
shapes=($shapes)

declare -A csiKeys='( [15~]=F5 [17~]=F6 [18~]=F7 [19~]=F8 [20~]=F9 [21~]=F10
    [23~]=F11 [24~]=F12 [A]=UP [B]=DOWN [C]=RIGHT [D]=LEFT [H]=HOME [F]=END
    [2~]=INSERT [3~]=DELETE [5~]=PGUP [6~]=PGDOWN )' \
        escKeys='( [OP]=F1 [OQ]=F2 [OR]=F3 [OS]=F4 )'
loop=true
while $loop; do
    trapKey=
    if IFS= read -d '' -rsn 1 -t $loopDelay str; then
        while IFS= read -d '' -rsn 1 -t .002 chr; do str+="$chr" ; done
        if [[ ${str::2} == $'\e[' ]] && [[ -v "csiKeys['${str:2}']" ]] ;then
            trapKey="${csiKeys[${str:2}]}"
        elif [[ ${str::1} == $'\e' ]] && [[ -v "escKeys['${str:1}']" ]] ;then
            trapKey="${escKeys[${str:1}]}"            
        elif [[ ${str/$'\e'/q} == q ]];then
            printf '"%q" pressed, exit.\n' "$str"
            loop=false
        else
            trapKey=${str@Q}
        fi
    fi
    if [ "$trapKey" ] ;then
        printf "Doing something with %s.\n" "$trapKey"
    fi
    printf >&2 '%s\r' ${shapes[shcnt++%8]}
done

0
这是另一个解决方案。它适用于任何按键,包括空格、回车、箭头等。
原始解决方案在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变量,因为它是默认变量。

如果您的循环仅等待按键操作,建议在 while 循环中添加例如 sleep 0.1,以避免循环占用 CPU 核心的所有可用资源。 - jarno

0

我发现了这篇论坛帖子,并将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"

运行此脚本


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接