如何使用PHP-CLI获取光标位置?

问题描述 投票:2回答:2

使用在CLI模式下运行的PHP脚本,我希望以可移植的方式获取光标位置。

随着代码:

// Query Cursor Position
echo "\033[6n";

在终端中,此代码报告光标位置,如

wb ?> ./script.php 
^[[5;1R
wb ?> ;1R 

但是,我无法在代码中检索这两个值(行:5,列:1)。

在使用输出缓冲进行一些测试之后:

ob_start();
echo "\033[6n";
$s = ob_get_contents();
file_put_contents('cpos.txt',$s);

我在cpos.txt文件中“\ 033 [6n”,而不是设备答案。

并阅读STDIN:

$timeout = 2;
$sent = false;
$t = microtime(true);
$buf = '';
stream_set_blocking(STDIN,false);
while(true){
    $buf .= fread(STDIN,8);
    if(!$sent){
        echo "\033[6n";
        $sent = true;
    }
    if($t+$timeout<microtime(true))
        break;
}
var_dump($buf);

缓冲区为空,但终端显示设备应答:

wb ?> ./script.php 
^[[5;1R
string(0) ""
wb ?>

有没有办法,没有诅咒,获得光标位置?

php terminal command-line-interface
2个回答
0
投票

你到目前为止所使用的代码几乎可以工作,你会发现按下enter并等待你的超时完成会产生一个包含答案的字符串,但最后会有一个\n字符。 (注意字符串长度为7而不是0。)

$ php foo.php
^[[2;1R                           
string(7) "
"

这里的问题是stream_set_blocking不会阻止终端逐行缓冲输入,因此终端在按下回车键之前不会向程序的stdin发送任何内容。

要使终端在没有行缓冲的情况下立即向您的程序发送字符,您需要将终端设置为"non-canonical"模式。这会禁用任何行编辑功能,例如按退格键删除字符的功能,而是立即将字符发送到输入缓冲区。在PHP中执行此操作的最简单方法是调用Unix实用程序stty

<?php
system('stty -icanon');

echo "\033[6n";
$buf = fread(STDIN, 16);

var_dump($buf);

此代码成功捕获终端对$buf的响应。

$ php foo.php
^[[2;1Rstring(6) ""

但是,此代码有几个问题。首先,它在终端完成后不会重新启用终端中的规范模式。当您尝试在程序中稍后从stdin输入时,或者在程序退出后在shell中输入时,这可能会导致问题。其次,来自终端^[[2;1R的响应代码仍然会回显到终端,当你想要做的就是把它读成一个变量时,你的程序输出看起来很乱。

要解决输入回显问题,我们可以将-echo添加到stty参数以禁用终端中的输入回显。要在我们更改之前将终端重置为其状态,我们可以调用stty -g输出当前终端设置的列表,稍后可以将其传递给stty以重置终端。

<?php
// Save terminal settings.
$ttyprops = trim(`stty -g`);

// Disable canonical input and disable echo.
system('stty -icanon -echo');

echo "\033[6n";
$buf = fread(STDIN, 16);

// Restore terminal settings.
system("stty '$ttyprops'");

var_dump($buf);

现在运行程序时,我们看不到终端中显示的任何垃圾:

$ php foo.php 
string(6) ""

我们可以做的最后一个改进是允许在stdout重定向到另一个进程/文件时运行程序。这对你的应用程序来说可能是必需的,也可能不是,但是目前运行php foo.php > /tmp/outfile是行不通的,因为echo "\033[6n";将直接写入输出文件而不是终端,让你的程序等待字符被发送到stdin,因为终端是从未发送任何转义序列,因此不会回复它。解决方法是写入/dev/tty而不是stdout,如下所示:

$term = fopen('/dev/tty', 'w');
fwrite($term, "\033[6n");
fclose($term); // Flush and close the file.

把这一切放在一起,并使用bin2hex()而不是var_dump()来获取$buf中的字符列表,我们得到以下结果:

<?php
$ttyprops = trim(`stty -g`);
system('stty -icanon -echo');

$term = fopen('/dev/tty', 'w');
fwrite($term, "\033[6n");
fclose($term);

$buf = fread(STDIN, 16);

system("stty '$ttyprops'");

echo bin2hex($buf) . "\n";

我们可以看到该程序正常工作如下:

$ php foo.php > /tmp/outfile
$ cat /tmp/outfile
1b5b323b3152
$ xxd -p -r /tmp/outfile | xxd
00000000: 1b5b 323b 3152                           .[2;1R

这表明$buf包含^[[2;1R,表示当查询其位置时光标位于第2行和第1列。

所以现在剩下要做的就是在PHP中解析这个字符串并提取由分号分隔的行和列。这可以使用正则表达式完成。

<?php
// Example response string.
$buf = "\033[123;456R";

$matches = [];
preg_match('/^\033\[(\d+);(\d+)R$/', $buf, $matches);

$row = intval($matches[1]);
$col = intval($matches[2]);
echo "Row: $row, Col: $col\n";

这给出了以下输出:

Row: 123, Col: 456

值得注意的是,所有这些代码只能移植到类Unix操作系统和ANSI / VT100兼容终端。除非您在Cygwin / MSYS2下运行该程序,否则此代码可能无法在Windows上运行。我还建议您在此代码中添加一些错误处理,以防您因任何原因未从终端获得响应。


0
投票

(这真是一个评论,但它有点长)

使用硬编码终端序列距“便携式”还有很长的路要走。虽然目前大多数终端仿真器都支持ANSI,vt100或xterm代码,这些代码具有共同的基础,但是有一个非常明确的API用于访问称为“curses”的交互式终端。一个PHP extension is available in pecl。这只是curses系统的存根接口 - 存在于任何Unix / Linux系统上。虽然可以在mswindows上使用cygwin或pdcurses进行设置,但这并不容易。您没有提到您正在使用的操作系统。 (mswindows console uses ANSI序列)

有一个基于termcap(curses的前身)的工具包(hoa)可能很有用。

要“检索”您只需要从stdin读取的数据(尽管建议使用非阻塞)。

© www.soinside.com 2019 - 2024. All rights reserved.