【问题标题】:PHP CLI: How to read a single character of input from the TTY (without waiting for the enter key)?PHP CLI:如何从 TTY 读取单个字符的输入(无需等待回车键)?
【发布时间】:2011-04-10 16:58:00
【问题描述】:

我想从 PHP 中的命令行一次读取一个字符,但似乎有某种输入缓冲从某处阻止了这一点。

考虑这段代码:

#!/usr/bin/php
<?php
echo "input# ";
while ($c = fread(STDIN, 1)) {
    echo "Read from STDIN: " . $c . "\ninput# ";
}
?>

输入“foo”作为输入(并按回车键),我得到的输出是:

input# foo
Read from STDIN: f
input# Read from STDIN: o
input# Read from STDIN: o
input# Read from STDIN: 

input# 

期待的输出是:

input# f
input# Read from STDIN: f

input# o
input# Read from STDIN: o

input# o
input# Read from STDIN: o

input# 
input# Read from STDIN: 

input# 

(也就是说,字符在输入时被读取和处理)。

然而,目前,每个字符只有在按下回车后才会被读取。我怀疑 TTY 正在缓冲输入。

最终我希望能够阅读诸如向上箭头、向下箭头等按键。

【问题讨论】:

    标签: php stdin command-line-interface buffering tty


    【解决方案1】:

    我的解决方案是在 TTY 上设置-icanon 模式(使用stty)。例如:

    stty -icanon
    

    所以,现在可以运行的代码是:

    #!/usr/bin/php
    <?php
    system("stty -icanon");
    echo "input# ";
    while ($c = fread(STDIN, 1)) {
        echo "Read from STDIN: " . $c . "\ninput# ";
    }
    ?>
    

    输出:

    input# fRead from STDIN: f
    input# oRead from STDIN: o
    input# oRead from STDIN: o
    input# 
    Read from STDIN: 
    
    input# 
    

    对此处给出的答案的支持:
    Is there a way to wait for and get a key press from a (remote) terminal session?

    欲了解更多信息,请参阅:
    http://www.faqs.org/docs/Linux-HOWTO/Serial-Programming-HOWTO.html#AEN92

    完成后不要忘记恢复 TTY...

    恢复 tty 配置

    可以通过在对终端进行更改之前保存 tty 状态来将终端重置回原来的状态。然后,您可以在完成后恢复到该状态。

    例如:

    <?php
    
    // Save existing tty configuration
    $term = `stty -g`;
    
    // Make lots of drastic changes to the tty
    system("stty raw opost -ocrnl onlcr -onocr -onlret icrnl -inlcr -echo isig intr undef");
    
    // Reset the tty back to the original configuration
    system("stty '" . $term . "'");
    
    ?>
    

    这是保存 tty 并将其恢复为用户在您开始之前的状态的唯一方法。

    请注意,如果您不担心保留原始状态,只需执行以下操作即可将其重置为默认的“正常”配置:

    <?php
    
    // Make lots of drastic changes to the tty
    system("stty raw opost -ocrnl onlcr -onocr -onlret icrnl -inlcr -echo isig intr undef");
    
    // Reset the tty back to sane defaults
    system("stty sane");
    
    ?>
    

    【讨论】:

    • "完成后不要忘记重置 TTY!" -- 我们如何重置它?
    • Mark:最基本的你可以做到stty sane。但是,为了保证您将 tty 重置为之前的确切状态,您应该首先保存它的状态。我已经扩展了答案以包括如何做到这一点的示例。
    【解决方案2】:

    这是一种适用于我的 readline 和流函数的方法,而无需弄乱 tty 的东西。

    readline_callback_handler_install('', function() { });
    while (true) {
      $r = array(STDIN);
      $w = NULL;
      $e = NULL;
      $n = stream_select($r, $w, $e, null);
      if ($n && in_array(STDIN, $r)) {
        $c = stream_get_contents(STDIN, 1);
        echo "Char read: $c\n";
        break;
      }
    }
    

    在 OSX 上使用 PHP 5.5.8 测试。

    【讨论】:

    • readline 需要编译成 PHP。使用以下命令检查 --with-readline=/opt/local 的配置:php -i | grep readline
    • 使用(最近更新的)Arch Linux 和 PHP 7.0.5。 break 导致它读取一个字符然后停止;删除它使一切正常。我还将select 末尾的0 更改为NULL,现在PHP 不使用100% CPU(!)。 根据初始测试,此功能在我的机器上似乎可以完美运行(捕获 \033s 和所有内容)。
    • @i336_ 感谢您指出这一点,我编辑了答案并将 stream_select 的 tv_sec 设置为 null 以解决 CPU 使用问题。但是,我无法重现任何停顿。
    • 啊,太好了!关于停顿,我发现这个问题可以在 ideone 上重现! ideone.com/EkJwIC(取消/评论 break 以进行测试)。我怀疑这是因为 OS X 的默认 tty 设置与 Linux 不同。
    • 请注意,使用此方法无法区分元键等。实际上,任何与输出字符不对应的键(箭头键、F1、F2 等)都会在 $c 中返回 x1B 或 ESC,但至少在我的终端模拟器中,例如 CTRL+W 确实返回“^W”或 0x13我
    【解决方案3】:

    下面的函数是@seb 答案的简化版本,可用于捕获单个字符。它不需要stream_select,并使用readline_callback_handler_install 的固有阻塞而不是创建一个while 循环。它还删除了处理程序以允许正常的进一步输入(例如 readline)。

    function readchar($prompt)
    {
        readline_callback_handler_install($prompt, function() {});
        $char = stream_get_contents(STDIN, 1);
        readline_callback_handler_remove();
        return $char;
    }
    
    // example:
    if (!in_array(
        readchar('Continue? [Y/n] '), ["\n", 'y', 'Y']
        // enter/return key ("\n") for default 'Y'
    )) die("Good Bye\n");
    $name = readline("Name: ");
    echo "Hello {$name}.\n";
    

    【讨论】:

      【解决方案4】:
      <?php
      `stty -icanon`;
      // this will do it
      stream_set_blocking(STDIN, 0);
      echo "Press 'Q' to quit\n";
      while(1){
         if (ord(fgetc(STDIN)) == 113) {
             echo "QUIT detected...";
             break;
         }
         echo "we are waiting for something...";
      }
      

      【讨论】:

      • 它似乎不起作用。根据stream_set_blocking手册说This function works for any stream that supports non-blocking mode (currently, regular files and socket streams).恐怕STDIN不属于那里。
      • 感谢您提供此代码 sn-p,它可能会提供一些有限的即时帮助。 proper explanation 将通过展示为什么这是解决问题的好方法,并使其对有其他类似问题的未来读者更有用,从而大大提高其长期价值。请edit您的回答添加一些解释,包括您所做的假设。 ref
      猜你喜欢
      • 1970-01-01
      • 2016-05-13
      • 1970-01-01
      • 2017-02-19
      • 2020-01-26
      • 2015-07-30
      相关资源
      最近更新 更多