phpEnv 默认不启用 readline 扩展,必须编译时加 --with-readline 并安装系统依赖,否则 readline_list_history() 等函数失效;历史需手动添加与持久化,Tab 补全须注册回调。

phpEnv 默认不启用 readline 扩展,即使你用它管理多个 PHP 版本,readline 仍需手动编译进对应 PHP 实例 —— 否则 readline_list_history()、readline_write_history() 等函数会直接报错或返回空数组。
phpEnv 编译时必须加 --with-readline
phpEnv 本身不提供开关式启用扩展的功能,它只是封装了 PHP 源码编译流程。要让 readline 完整可用(支持历史列表、补全、持久化),不能只靠 phpenv install 默认参数。
- 系统级依赖必须先装:Ubuntu/Debian 运行
sudo apt install libreadline-dev;CentOS/RHEL 用sudo yum install readline-devel - phpEnv 安装 PHP 时需显式指定:
PHP_CONFIGURE_OPTS="--with-readline" phpenv install 8.3.5 - 如果已装好 PHP,无法热启扩展 —— 必须重装:
phpenv uninstall 8.3.5 && PHP_CONFIGURE_OPTS="--with-readline" phpenv install 8.3.5 - 漏掉
--with-readline时,readline()函数虽能调用,但底层走的是libedit,readline_list_history()始终返回空数组,且方向键、Ctrl-R 搜索等功能不可用
readline_add_history() 必须手动调用才生效
很多人以为输入后历史自动保存,其实 PHP 的 readline 不像 Bash 那样默认记录 —— 每次有效输入都得显式塞进历史栈。
- 只写
$line = readline("prompt> ");不够,必须紧跟if ($line !== false && trim($line) !== '') { readline_add_history($line); } -
readline_add_history()只影响内存历史,关掉脚本就丢,如需持久化,还得配readline_read_history()和readline_write_history() - 重复调用
readline_add_history("")会导致空行进历史,建议trim()后判断再添加
历史文件读写要注意路径和权限
用 readline_read_history() 或 readline_write_history() 时,路径错误或目录不可写是静默失败的最常见原因。
立即学习“PHP免费学习笔记(深入)”;
- 文件路径推荐用绝对路径:
$histFile = __DIR__ . '/.cli_history';,避免 CLI 工作目录变动导致写入失败 - 首次运行前检查目录可写:
if (!is_writable(dirname($histFile))) { die("历史目录不可写\n"); } -
readline_read_history()对不存在的文件不报错,但也不加载任何内容;readline_write_history()写失败时无 warning,需自行用file_exists()+filesize()验证是否真写进去了 - 历史文件是纯文本、每行一条命令,可直接用
cat查看,也方便调试
Tab 补全必须注册 readline_completion_function()
没有注册补全回调,Tab 键在 PHP CLI 中就是个普通字符,不会触发任何行为。
- 回调函数必须返回数组,哪怕空数组:
readline_completion_function(fn($input) => []);,否则 Tab 会卡住 - 输入为空时(
$input === ''),通常应返回全部候选,否则第一次按 Tab 没反应 - 补全逻辑里别用
echo或var_dump,会污染补全界面;调试建议写日志到临时文件 - 一个实用技巧:用
str_starts_with($keyword, $input)做前缀匹配,比strpos()更安全(避免误匹配子串)
最关键的遗漏点:很多人试完 readline() 能读输入就以为成了,但真正让 CLI 变得“像 Bash”的那几个函数 —— readline_list_history()、readline_write_history()、readline_completion_function() —— 全部依赖编译时的 --with-readline,缺了它,所有高级功能都是摆设。



















