Bash:读取输入是否可用(是任意键按下了吗?)

3

问题

我正在使用Bash 5,有一个长时间运行的循环,需要偶尔检查用户可能按下的各种键。我知道如何使用 stty 来实现这一点 - 请参见我下面的答案 - 但它比应该更丑陋。

基本上,我正在寻找一种简洁的方法来实现这一点:

keyhit-p() {
  if "read -n1 would block"; then
    return false
  else
    return true
  fi
}

非解决方案: read -t 0

我已经阅读了 bash 手册,知道了 read -t 0 命令。但它并不能满足我的要求,即检测是否有任何输入可用。相反,它只会在用户按下 ENTER 键(完整的一行输入)时返回 true。

例如:

while true; do
  if read -n1 -t0; then
    echo "This only works if you hit enter"
    break
  fi
done

尽管这似乎是有道理的,但请注意,添加“-n1”并没有帮助通知bash我打算逐个字符而不是逐行读取。也许这应该成为一个bash功能请求? - hackerb9
你说的“丑”是什么意思? - oguz ismail
1
“丑陋”指的是“将stty状态保存在变量中,然后希望在用户按下^C之前恢复它,从而使他们的终端崩溃”。这应该是一个原子操作。我知道我可以使用trap EXIT 'stty sane',但这对用户的终端做出了不总是有效的假设。 - hackerb9
1个回答

2

一个可用但不太美观的答案

尽管以下方法可行,但我希望有更好的解决方案。

#!/bin/bash
# Reset terminal's stty to previous values on exit.
trap 'stty $(stty --save)' EXIT

keyhit-p() {
    # Return true if input is available on stdin (any key has been hit).
    local sttysave=$(stty --save)
    stty -icanon min 1 time 0   # ⎫
    read -t0                    # ⎬ Ugly: This ought to be atomic so the
    local status=$?             # ⎪ terminal's stty is always restored.
    stty $sttysave              # ⎭
    return $status
}

while true; do
    echo -n .
    if ! keyhit-p; then
        continue
    else
        while keyhit-p; do
            read -n1
            echo Key: $REPLY
        done
        break
    fi
done

这会在read之前更改用户的终端设置(stty),并尝试在之后写回,但是这样做是非原子性的。脚本可能会被中断并使用户的终端处于不正确的状态。我希望看到一个解决这个问题的答案,最好只使用bash内置的工具。

更快、更丑陋的答案

上述例程中的另一个缺陷是它需要大量的CPU时间来尝试做到完美。它需要调用外部程序(stty)三次才能检查是否发生了任何事情。在循环中进行分支可能会很昂贵。如果我们放弃正确性,我们可以得到一个运行速度比原来快两个数量级(256×)的例程。

#!/bin/bash

# Reset terminal's stty to previous values on exit.
trap 'stty $(stty --save)' EXIT

# Set one character at a time input for the whole script.
stty -icanon min 1 time 0

while true; do
    echo -n .
    # We save time by presuming `read -t0` no longer waits for lines.
    # This may cause problems and can be wrong, for example, with ^Z.
    if ! read -t0; then
        continue
    else
        while read -t0; do
            read -n1
            echo Key: $REPLY
        done
        break
    fi
done

这个脚本不是在读取测试期间才切换到非规范模式,而是在开始时设置一次,并在脚本退出时使用异常处理程序来撤消它。

虽然我喜欢代码看起来更干净的样子,但原始版本的原子性缺陷因为未处理 SUSPEND 信号而加剧。如果用户的 shell 是 bash,则当进程被挂起时启用 icanon,但在进程被前台化时不会禁用 icanon。这使得 read -t0 即使按下键(除 Enter 外)也会返回 FALSE。其他用户 shell 可能不像 bash 那样在 ^Z 上启用 icanon,但这更糟糕,因为输入命令将不再像平常那样工作。

此外,要求非规范模式始终保持开启可能会导致其他问题,特别是当脚本变得比这个简单示例更长时。文档中没有说明非规范模式应该如何影响 read 和其他 bash 内置命令。在我的测试中似乎可以工作,但它总是有效吗?调用或被外部程序调用时遇到问题的机会会增加。也许没有问题,但需要进行繁琐的测试。


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