clue / stdio-react
基于ReactPHP的异步、事件驱动的控制台输入输出(STDIN,STDOUT)的真实交互式CLI应用程序
Requires
- php: >=5.3
- clue/term-react: ^1.0 || ^0.1.1
- clue/utf8-react: ^1.0 || ^0.1
- react/event-loop: ^1.2
- react/stream: ^1.2
Requires (Dev)
- clue/arguments: ^2.0
- clue/commander: ^1.2
- phpunit/phpunit: ^9.3 || ^5.7 || ^4.8.35
Suggests
- ext-mbstring: Using ext-mbstring should provide slightly better performance for handling I/O
README
基于ReactPHP的异步、事件驱动和UTF-8感知控制台输入输出(STDIN,STDOUT)的真实交互式CLI应用程序。
您可以使用此库构建真正交互式和响应式的命令行(CLI)应用程序,当用户输入一行或按下特定键时,应用程序会立即做出反应。受ext-readline
的启发,但支持UTF-8和交织I/O(在输出打印时输入),历史记录和自动完成支持,并在内部处理适当的TTY设置,无需任何扩展或特殊安装。
目录
支持我们
我们投入了大量时间开发、维护和更新我们的优秀开源项目。您可以通过在GitHub上成为赞助商来帮助我们保持工作的高质量。赞助商将获得许多回报,有关详细信息,请参阅我们的赞助页面。
让我们一起将这些项目提升到新的水平!🚀
快速入门示例
一旦安装,您可以使用以下代码在CLI程序中显示提示
<?php require __DIR__ . '/vendor/autoload.php'; $stdio = new Clue\React\Stdio\Stdio(); $stdio->setPrompt('Input > '); $stdio->on('data', function ($line) use ($stdio) { $line = rtrim($line, "\r\n"); $stdio->write('Your input: ' . $line . PHP_EOL); if ($line === 'quit') { $stdio->end(); } });
另请参阅示例。
用法
Stdio
Stdio
是此库的主要接口。它通过注册和转发相应的事件来负责协调输入和输出流。
$stdio = new Clue\React\Stdio\Stdio();
此类接受一个可选的LoopInterface|null $loop
参数,可以用来传递用于此对象的事件循环实例。您可以使用null
值,以便使用默认循环。除非您确定要显式使用给定的事件循环实例,否则不应提供此值。
下面是等待用户输入和写入输出的示例。Stdio
类是一个行为良好的双向流(实现ReactPHP的DuplexStreamInterface
),它将每行完整内容作为data
事件发出,包括尾随换行符。
输出
Stdio
是一个行为良好的可写流,实现ReactPHP的WritableStreamInterface
。
可以使用write($text)
方法将给定的文本字符打印到控制台输出。如果需要更多控制或希望输出单个字节或二进制输出,这很有用。
$stdio->write('hello'); $stdio->write(" world\n");
由于Stdio
是一个行为良好的可写流,因此您也可以将任何可读流pipe()
到这个流中。
$logger->pipe($stdio);
输入
Stdio
是一个行为良好的可读流,实现ReactPHP的ReadableStreamInterface
。
它将为从控制台输入中读取的每一行发出一个data
事件。事件将包含输入缓冲区,包括尾随换行符。您可以注册任何数量的事件处理程序,如下所示
$stdio->on('data', function ($line) { if ($line === "start\n") { doSomething(); } });
请注意,这个类负责缓冲不完整的行,并且只输出完整的行。这意味着行通常以换行符结尾。如果流在没有换行符的情况下结束,它将不会出现在 data
事件中。因此,通常建议在处理此类命令行输入之前删除尾随的换行符。
$stdio->on('data', function ($line) { $line = rtrim($line, "\r\n"); if ($line === "start") { doSomething(); } });
同样,如果您复制并粘贴大量文本,它将正确地输出多行,每行使用单独的 data
事件。
因为 Stdio
是一个表现良好的可读流,它会以原样输出传入的数据,所以您也可以使用它将这个流 pipe()
到其他可写流中。
$stdio->pipe($logger);
您可以通过这个接口控制控制台输入的各个方面,所以请继续阅读。
提示
提示符 将写入到 用户输入行 的开始处,在 用户输入缓冲区 之前。
可以使用 setPrompt($prompt)
方法来更改输入提示。提示符将按原样打印到 用户输入行,因此您可能想要在末尾添加一个空格。
$stdio->setPrompt('Input: ');
默认输入提示为空,即 用户输入行 仅包含实际的 用户输入缓冲区。您可以通过传递一个空提示来恢复此行为。
$stdio->setPrompt('');
可以使用 getPrompt()
方法来获取当前输入提示。除非您已设置其他内容,否则它将返回一个空字符串。
assert($stdio->getPrompt() === '');
回显
回显模式 控制实际 用户输入缓冲区 在 用户输入行 中的显示方式。
可以使用 setEcho($echo)
方法来控制回显模式。默认情况下,按原样打印 用户输入缓冲区。
您可以禁用打印 用户输入缓冲区,例如用于密码提示。用户仍然可以输入,但不会收到任何关于当前 用户输入缓冲区 的指示。请注意,这通常会导致不良的用户体验,因为用户甚至看不到他们的光标位置。只需像这样传递一个布尔值 false
。
$stdio->setEcho(false);
或者,您也可以使用替换字符 隐藏 用户输入缓冲区。对于每个 用户输入缓冲区 中的字符,将打印一个替换字符。这对于密码提示很有用,可以给用户提供按键已被记录的指示。这通常提供更好的用户体验,并允许用户控制他们的光标位置。只需传递一个字符串替换字符,像这样。
$stdio->setEcho('*');
要恢复每个字符按原样出现的原始行为,只需传递一个布尔值 true
。
$stdio->setEcho(true);
输入缓冲区
用户输入的每一项都将缓存在当前的 用户输入缓冲区 中。一旦用户按下回车,用户输入缓冲区 将被处理并清除。
可以使用 addInput($input)
方法将文本添加到当前的 用户输入缓冲区 中的光标位置。给定文本将被插入,就像用户在文本中输入一样,因此相应地调整当前光标位置。用户可以在任何时间删除和/或重写缓冲区。更改 用户输入缓冲区 对于向用户展示预设输入(如上次密码尝试)很有用。只需传递一个输入字符串,像这样。
$stdio->addInput('hello');
setInput($buffer)
方法可以用来控制 用户输入缓冲区。给定的文本将用来替换整个当前的 用户输入缓冲区,并相应地将当前光标位置调整到新缓冲区的末尾。用户可以在任何时间删除和/或重写缓冲区。改变 用户输入缓冲区 对于向用户展示预设输入(如最后一次密码尝试)非常有用。只需传递一个输入字符串,如下所示
$stdio->setInput('lastpass');
getInput()
方法可以用来访问当前的 用户输入缓冲区。如果你想在当前的 用户输入缓冲区 后面追加一些输入,这会很有用。你可以简单地像这样访问缓冲区
$buffer = $stdio->getInput();
光标
默认情况下,用户可以通过键盘上的箭头键来控制他们的(水平)光标位置。此外,每个按下的键盘字符都会推进光标位置。
setMove($toggle)
方法可以用来控制用户是否可以使用他们的箭头键。要禁用左右箭头键,只需传递一个布尔值 false
,如下所示
$stdio->setMove(false);
要恢复默认行为,其中用户可以使用左右箭头键,只需传递一个布尔值 true
,如下所示
$stdio->setMove(true);
getCursorPosition()
方法可以用来访问当前光标位置,以字符数来衡量。如果你想要获取当前 用户输入缓冲区 的子字符串,这会很有用。只需像这样调用它
$position = $stdio->getCursorPosition();
getCursorCell()
方法可以用来获取当前光标位置,以等宽单元格数来衡量。大多数 普通 字符(纯ASCII和大多数多字节UTF-8序列)占用一个等宽单元格。然而,有一些字符没有可视表示(完全不占用单元格)或者字符不能适应一个单元格(如一些亚洲象形文字)。这个方法主要用于计算屏幕上的可视光标位置,但你也可以像这样调用它
$cell = $stdio->getCursorCell();
moveCursorTo($position)
方法可以用来将当前光标位置设置为给定的绝对字符位置。例如,要将光标移动到 用户输入缓冲区 的开头,只需调用
$stdio->moveCursorTo(0);
moveCursorBy($offset)
方法可以用来通过相对于当前位置的给定字符数来改变光标位置。正数将光标向右移动 - 负数将光标向左移动。例如,要将光标向左移动一个字符,只需调用
$stdio->moveCursorBy(-1);
历史记录
默认情况下,用户可以通过键盘上的上箭头键和下箭头键来访问之前命令的历史记录。历史记录将从空状态开始,因此这个功能实际上是禁用的,因为那时上箭头键和下箭头键没有功能。
请注意,历史记录不是自动维护的。用户通过按回车键提交的任何输入都不会自动添加到历史记录中。一开始这可能看起来不方便,但实际上这给了你更多控制权,决定了哪些(以及何时)行应该被添加到历史记录中。如果你想要自动将用户输入的所有内容添加到历史记录中,你可能想要使用类似这样的方法
$stdio->on('data', function ($line) use ($stdio) { $line = rtrim($line); $all = $stdio->listHistory(); // skip empty line and duplicate of previous line if ($line !== '' && $line !== end($all)) { $stdio->addHistory($line); } });
listHistory(): string[]
方法可以用来返回包含历史记录中所有行的数组。这将是一个空数组,直到你通过 addHistory()
添加新的条目。
$list = $stdio->listHistory(); assert(count($list) === 0);
addHistory(string $line): void
方法可以用来将新行添加到(历史记录的底部位置)中。随后的 listHistory()
调用将返回这个作为最后一个元素。
$stdio->addHistory('a'); $stdio->addHistory('b'); $list = $stdio->listHistory(); assert($list === array('a', 'b'));
clearHistory(): void
方法可以用来清除完整的历史记录列表。随后的 listHistory()
调用将返回一个空数组,直到你再次通过 addHistory()
添加新条目。请注意,如果历史记录为空,历史记录功能实际上将被禁用,因为那时上箭头键和下箭头键没有功能。
$stdio->clearHistory(); $list = $stdio->listHistory(); assert(count($list) === 0);
limitHistory(?int $limit): void
方法可以用来设置保留在内存中的历史行数限制。默认情况下,只会保留最后500行,其余的都会被丢弃。你可以使用一个整数值来限制为给定数量的条目,或者使用 null
来表示无限制(不建议,因为所有内容都会保留在RAM中)。如果你将限制设置为 0
(整数零),则历史记录实际上会被禁用,因为不能向历史列表添加或从历史列表中返回任何行。如果你正在构建一个命令行界面(CLI)应用程序,你可能还希望使用类似于下面的方法来遵守 HISTSIZE
环境变量。
$limit = getenv('HISTSIZE'); if ($limit === '' || $limit < 0) { // empty string or negative value means unlimited $stdio->limitHistory(null); } elseif ($limit !== false) { // apply any other value if given $stdio->limitHistory($limit); }
没有 readHistory()
或 writeHistory()
方法,因为文件系统操作本质上会阻塞,因此超出了这个库的范围。使用你喜欢的文件系统API和适当的 addHistory()
或单独的 listHistory()
调用应该是相当直接的,这留作本文档读者的练习(即 你)。
自动完成
默认情况下,用户可以通过在键盘上按TAB键来使用自动完成。自动完成功能默认未注册,因此此功能实际上是被禁用的,因为TAB键此时没有任何功能。
可以使用 setAutocomplete(?callable $autocomplete): void
方法来注册一个新的自动完成处理程序。在其最简单的形式中,你不需要分配任何参数,可以直接从像这样的可调用对象返回可能匹配的单词数组
$stdio->setAutocomplete(function () { return array( 'exit', 'echo', 'help', ); });
如果用户输入 he [TAB]
,则前两个匹配将被跳过,因为它们与当前单词前缀不匹配,最后一个将被自动选中,从而使输入缓冲区的结果为 hello
。
如果用户输入 e [TAB]
,那么这将匹配多个条目,用户将看到最多8个可用的单词完成选项,如下所示
> e [TAB] exit echo > e
除非另有说明,否则匹配将针对输入缓冲区中的当前单词边界进行。这意味着如果用户输入 hello [SPACE] ex [TAB]
,则结果输入缓冲区将是 hello exit
,这取决于你的特定用例,可能或可能不是你所需要的。
为了提供更多控制,自动完成功能实际上接收三个参数(类似于 ext-readline
的 readline_completion_function()
):第一个参数将是根据当前光标位置和单词边界的不完整单词,而第二个和第三个参数将是该单词在完整输入缓冲区中的起始和结束偏移量,以(Unicode)字符为单位。上述示例将分别调用 $fn('he', 0, 2)
、$fn('e', 0, 1)
和 $fn('ex', 6, 8)
。你可能想将此用作 $offset
参数来检查当前单词是否是参数或根命令,并将 $word
参数用于自动完成部分文件名匹配,如下所示
$stdio->setAutocomplete(function ($word, $offset) { if ($offset <= 1) { // autocomplete root commands at offset=0/1 only return array('cat', 'rm', 'stat'); } else { // autocomplete all command arguments as glob pattern return glob($word . '*', GLOB_MARK); } });
请注意,用户还可以使用引号和/或根命令周围的前导空白,例如
"hell [TAB]
,在这种情况下,偏移量将相应增加,这将调用$fn('hell', 1, 4)
。除非你使用更复杂的参数解析器,否则使用$offset <= 1
来检查这是一个根命令的合理近似。
如果你需要对自动完成有更多的控制,你也可以直接访问和/或操作输入缓冲区和光标,如下所示
$stdio->setAutocomplete(function () use ($stdio) { if ($stdio->getInput() === 'run') { $stdio->setInput('run --test --value=42'); $stdio->moveCursorBy(-2); } // return empty array so normal autocompletion doesn't kick in return array(); });
您可以使用null
值来再次移除自动完成功能,从而禁用自动完成功能。
$stdio->setAutocomplete(null);
键盘
Readline
类负责从STDIN
读取用户输入并注册相应的按键事件。默认情况下,Readline
使用类似于常见终端中通常找到的硬编码按键映射。这意味着正常Unicode字符键(“a”和“b”,以及“?”、“ä”、“µ”等)将被处理为用户输入,而特殊控制键可用于光标移动、历史记录和自动完成功能。未知特殊键将被忽略,默认情况下不会作为用户输入的一部分进行处理。
此外,您可以将自定义函数绑定到您想要的任何键码。如果将自定义函数绑定到某个键码,则默认行为将不再触发。这允许您将完全新的函数注册到键上或覆盖现有行为。
例如,您可以使用以下代码在用户按下某个键时打印一些帮助文本
$stdio->on('?', function () use ($stdio) { $stdio->write('Here\'s some help: …' . PHP_EOL); });
类似地,这也可以用来在用户按下某个键时操纵用户输入并替换一些输入
$stdio->on('ä', function () use ($stdio) { $stdio->addInput('a'); });
Readline
使用终端发出的原始二进制键码。这意味着您可以使用正常UTF-8字符表示法来表示正常Unicode字符。特殊键使用二进制控制代码序列(有关更多详细信息,请参阅ANSI / VT100控制代码)。例如,以下代码可以将自定义函数注册到向上箭头光标键
$stdio->on("\033[A", function () use ($stdio) { $stdio->setInput(strtoupper($stdio->getInput())); });
铃声
默认情况下,当用户尝试执行其他情况下已禁用的功能时(例如,在行首使用左箭头或退格键时),此项目将发出可听/可视的BELL信号。
BELL是否可听/可视取决于终端及其设置,即一些终端可能会“嘟嘟”或闪烁屏幕或发出短暂的振动。
可以使用setBell(bool $bell): void
方法来启用或禁用在使用禁用功能时发出BELL信号。
$stdio->setBell(false);
Readline
自v2.3.0以来已弃用,请参阅
Stdio
。
已弃用的Readline
类负责响应用户输入并向用户显示提示。它是通过从输入流中读取单个字节并将当前用户输入行写入输出流来实现的。
已弃用的Readline
类仅用于内部,不应再从消费项目中引用。
您可以通过Stdio
访问当前实例。
// deprecated $readline = $stdio->getReadline();
现在在Readline
实例上可用的所有方法现在都可在Stdio
类上使用。出于向后兼容性的原因,它们在下一个主要版本之前仍然可在Readline
类上使用,有关更多详细信息,请参阅上面。
// deprecated $readline->setPrompt('> '); // new $stdio->setPrompt('> ');
内部,Readline
也是一个表现良好的可读流(实现了ReactPHP的ReadableStreamInterface
),它会将每行完整的输入作为data
事件发出,包括尾随换行符。这被认为是高级用法。
陷阱
Stdio
必须在写入STDOUT
时重绘当前用户输入行。因此,确保任何输出都以这种方式写入,而不是使用echo
语句非常重要。
// echo 'hello world!' . PHP_EOL; $stdio->write('hello world!' . PHP_EOL);
根据您的程序,替换所有此类出现的做法可能合理或不合理。作为替代,您可以使用输出缓冲区,它将自动将所有写入事件转发到Stdio
实例,如下所示
ob_start(function ($chunk) use ($stdio) { // forward write event to Stdio instead $stdio->write($chunk); // discard data from normal output handling return ''; }, 1);
安装
推荐安装此库的方法是通过Composer。你刚开始使用Composer吗?需要了解Composer?
本项目遵循SemVer规范。这将安装最新支持的版本。
composer require clue/stdio-react:^2.6
有关版本升级的详细信息,请参阅变更日志。
本项目旨在在任何平台上运行,因此不要求任何PHP扩展,并支持在旧版PHP 5.3到当前PHP 8+和HHVM上运行。强烈建议为该项目使用最新支持的PHP版本。
内部,它将使用ext-mbstring
来计算和测量字符串大小。如果此扩展不存在,则此库将使用稍微慢一点的正则表达式解决方案,但应该同样有效。强烈建议安装ext-mbstring
。
内部,它将使用ext-readline
来启用原始终端输入模式。如果此扩展不存在,则此库将在启动时手动设置所需的TTY设置,并在退出时尝试恢复先前设置。输入行编辑完全在本库内部处理,不依赖于ext-readline
。安装ext-readline
是完全可选的。
请注意,Microsoft Windows不受支持。由于平台限制,PHP在Windows上无法提供读取标准控制台输入而不阻塞的支持。遗憾的是,由于底层PHP功能请求尚未实现(这不太可能在近期内发生),我们在此库中能做的事情很少。然而,此包在Windows Subsystem for Linux(或WSL)上运行时没有问题。我们建议当你想在Windows上运行此包时安装WSL。有关更多详细信息,请参阅#18。
测试
要运行测试套件,您首先需要克隆此存储库,然后通过Composer安装所有依赖项。
composer install
要运行测试套件,请转到项目根目录并运行
vendor/bin/phpunit
许可证
本项目以宽松的MIT许可发布。
你知道吗?我提供定制开发服务,并为发布赞助和贡献开具发票。如果您想了解详细信息,请联系我(@clue)。
更多
-
您想了解更多关于处理数据流的信息吗?请参阅底层react/stream组件的文档。
-
如果您构建了一个从STDIN读取命令行的交互式CLI工具,您可能想使用clue/arguments来将此字符串拆分成其单个参数,然后使用clue/commander来路由到已注册的命令及其所需的参数。