Skip to content

用readline库改进REPL交互体验

The GNU Readline library provides a set of functions for use by applications that allow users to edit command lines as they are typed in.

引言

GNU readline库是广泛使用的C语言库,在EmacsVi中都有使用到:

这篇将使用此库建立一个简单的REPL,具备一定的交互能力,然后通过默认配置文件以支持中文输入。

Python中的readline

在Python中就自带了这个库的对应版本:

/usr/local/python3/lib/python3.7/lib-dynload/readline.cpython-37m-x86_64-linux-gnu.so

而比Python官方REPL更好用的IPython最初是有一个类似readline功能的前端模块,后来替换为:

这两个库在readline库基础上让REPL和对应语言语法结合起来,有着更好的代码高亮、提示、补全、跨行输入等特性。

简陋的REPL

std::string line;
while (true) {
    std::cout << ">>";
    getline(std::cin, line);
    std::cout << line << std::endl;
}

这就是最简单的REPL了,有提示符,也可以获取输入,但存在问题:

  • 没有命令提示
  • 没有历史命令
  • 光标无法移动,只能挨个删除输入字符

引入readline库

#include <readline/readline.h>

std::string line;
while (true) {
    char *line_read = readline(">>");
    line = line_read;
    free(line_read);

    std::cout << line << std::endl;
}

通过调用readline获取输入,就自动有了光标移动等特性,每次获取输入后要通过free释放内存。

历史命令

REPL退出时将历史命令记录到文件中,然后每次启动的时候自动加载历史命令文件:

#include <readline/readline.h>
#include <readline/history.h>

std::string history = ".ledge_history";

stifle_history(100); // 最多记录100条

read_history(history.c_str());

std::string line;

while (true) {
    char *line_read = readline(">>");

    if (line_read && *line_read) {
        add_history(line_read);
    }

    line = line_read;
    free(line_read);

    std::cout << line << std::endl;
}

write_history(history.c_str());

通过3个历史命令api完成了读取、添加、保存历史命令文件功能。

自动补全

默认自动补全是按Tab键用本地文件或目录来补全,可以定制为根据已输入内容进行语法补全:

void initialize_readline() {
    // .inputrc里面可以使用类似语法
    // $if ledge ..... $endif
    rl_readline_name = "ledge";

    // 注册自动补全回调函数
    rl_attempted_completion_function = ledge_completion;
}

char **ledge_completion(const char *text, int start, [[maybe_unused]] int end) {
    char **matches;

    matches = nullptr;

    // 行首部分作为命令,剩下部分用本地文件名补全
    if (start == 0) {
        matches = rl_completion_matches(text, command_generator);
    }
    else {
        matches = rl_completion_matches(text, rl_filename_completion_function);
    }

    return (matches);
}    

// 不断尝试符合条件的命令,得到一个集合,到最后一个空字符串表示查找结束
char *command_generator(const char *text, int state) {
    static int list_index, len;
    std::string name;

    // 开始查找
    if (!state) {
        list_index = 0;
        len = strlen(text);
    }

    int size = commands.size();

    while (list_index < size) {
        name = commands[list_index];
        if (name == "") {// 结束查找
            return nullptr;
        }

        list_index++;

        if (strncmp(name.c_str(), text, len) == 0)
            return (dupstr(name.c_str()));
    }

    // 结束查找
    return nullptr;
}

自动补全后,还可以预定义回调函数进一步执行相应操作。

readline无法输入中文

readline库会自动读取配置文件(~/.inputrc):

# 允许8bit输入
set meta-flag on
set input-meta on

# 禁止自动去除最高位
set convert-meta off

# 允许最高位显示
set output-meta on

# 按键配置
"\eOA": history-search-backward
"\eOB": history-search-forward

"\e[A": history-search-backward
"\e[B": history-search-forward

"\eOC": forward-char
"\eOD": backward-char

"\e[C": forward-char
"\e[D": backward-char

vim无法显示中文

配置文件(~/.vimrc):

set encoding=utf8
set termencoding=utf-8
set fileencodings=utf-8,gbk

emacs无法显示中文

配置文件(~/.emacs.el):

(set-language-environment "UTF-8")
(set-locale-environment "UTF-8")