readline是最现实的选择,因其封装了终端输入、光标、历史、多字节字符、信号处理等复杂逻辑;windows需换用linenoise或editline,rl_attempted_completion_function须返回null结尾的动态分配字符串数组。

为什么 readline 是最现实的选择
直接手写补全逻辑在 C++ 里既低效又易错——你要处理终端原始输入、光标位置、历史回溯、多字节字符(如中文)、信号中断(Ctrl+C)、甚至不同 shell 的兼容性。Linux/macOS 下 readline 库已稳定存在三十多年,它把所有这些封装成几个回调函数,rl_attempted_completion_function 就是补全入口。别造轮子,用它。
注意:Windows 原生不带 readline,MinGW 可用,但 MSVC 需换 linenoise 或 editline;若坚持跨平台,linenoise 更轻量,API 也类似。
rl_attempted_completion_function 怎么写才不出错
这个函数签名固定:char **func(const char *text, int start, int end)。它不返回单个字符串,而是一组以 NULL 结尾的 char * 指针数组(char **),由 readline 自动管理内存(你 malloc,它 free)。
- 只对当前光标前的单词补全(
text是待补全片段,不是整行) - 必须返回动态分配的字符串副本,不能返回局部数组或字面量
- 返回
NULL表示无匹配,return NULL和return rl_completion_matches(...)效果不同:后者会触发默认文件名补全 - 常见错误:忘记在数组末尾加
NULL,导致readline越界读取,终端卡死
示例(补全预定义命令):
char **my_completion(const char *text, int start, int end) {
static const char *cmds[] = {"help", "load", "save", "quit", "exit"};
rl_attempted_completion_over = 1; // 关闭默认文件名补全
return rl_completion_matches(text, [](const char *text, int state) -> char * {
static int idx = 0;
if (state == 0) idx = 0;
while (idx <h3>如何让补全支持子命令和参数上下文</h3><p>单纯匹配命令名远远不够。用户输入 <code>load <tab></tab></code> 时,应补全文件名;输入 <code>save --format <tab></tab></code> 时,应补全 <code>json</code>、<code>xml</code> 等格式。这需要解析当前输入行,判断“光标前最后一个完整 token”是什么。</p>
- 用
rl_line_buffer获取整行,配合rl_point(光标位置)切分出当前单词和前置上下文 - 简单策略:从光标往前找空格,提取倒数第二个 token(即上一个命令或选项),再查表决定补全候选集
- 避免在回调里做 heavy parsing:预处理逻辑尽量轻量,否则补全延迟明显
- 注意引号和转义:如果用户输入
load "my file<tab></tab>,text是"my file,需手动去掉引号再匹配
初始化和清理的关键细节
readline 不是开箱即用。漏掉以下任一环节,补全可能静默失败或崩溃:
- 调用
rl_readline_name = "myapp"设置程序名,影响历史文件路径(如~/.myapp_history) - 必须设
rl_attempted_completion_function = my_completion,且要在第一次readline()调用前完成 - 启用历史:调用
using_history()+read_history(".myapp_history")(可选) - 退出前调用
write_history(".myapp_history"),否则历史丢失 - 链接时加
-lreadline -lcurses(部分系统需显式连curses)
最易忽略的是:如果你在补全回调里抛了 C++ 异常,readline 完全不捕获,直接 terminate。所有异常必须在回调内吞掉或转为返回 NULL。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











