在 bash 管道中工作的提示命令

prompt command that works in bash pipe

我正在寻找一个提示命令,它允许您在 bash 管道的中间使用 readline 绑定,如下所示:

$ prompt -i 'prefill' -p 'prompt> ' | cat

然后我开始使用预填充文本的 readline 绑定进行编辑:

prompt> prefill|

然后您输入的任何内容的输出都会发送到 cat

我写了一个 python 程序几乎可以完成所有这些工作:

#!/usr/bin/env python3

import readline
import sys

def input_with_prefill(prompt, text):
    def hook():
        readline.insert_text(text)
        readline.redisplay()
    readline.set_pre_input_hook(hook)
    result = input(prompt)
    readline.set_pre_input_hook()
    return result

def arg(flag):
    arg = ""
    if flag in sys.argv:
        tuple_matches_flag = lambda x: x[1] == flag
        # select first instance of "<flag>", 
        arg_ind = next(filter(tuple_matches_flag, enumerate(sys.argv)))[0]
        # add one to the index for the value of the arg
        arg = sys.argv[arg_ind + 1]
    return arg

def main():
    prompt = arg("-p")
    prefill = arg("-i")
    response = input_with_prefill(prompt, prefill)
    print(response)

main()

这在单独使用时效果很好。 prompt -i 'prefill' -p 'prompt> ' 完美地按照我想要的方式工作。 但是,当我把它放在这样的管道中时 prompt -i 'prefill' -p 'prompt> ' | cat 它不会起作用。我知道这是因为提示正在将它的标准输出发送到 FIFO 管道到 cat,所以 readline 库无法正常工作,因为它通常与标准输出接口。所以我认为解决方案是像这样重定向文件描述符:

prompt -i 'prefill' -p 'p> ' 3>&1 1>&2 2>&3 3>&- | cat

这将创建第三个文件描述符,将其指向 stdout 曾经所在的位置(到 cat 的 FIFO 管道),将 stdout 指向 stderr(终端),将 stderr 指向 FIFO 管道,并关闭第三个文件描述符.因此,在这结束时,我们将 prompt 的 stderr 发送到 FIFO,将 prompt 的 stdout 发送到控制台。如果我们将 python 程序的最后一行调整为 print(response, file=sys.stderr),那么这应该可以工作。但是,事实并非如此。打印了 2 个提示,没有预填充。怎么了?

这似乎是 python 的 readline 实现的问题: Python module "readline" can't handle output redirection https://bugs.python.org/issue24829

我写了这个 C 程序,它可以做我想做的事。您可以使用 clang file.c -L/opt/homebrew/opt/readline/lib -lreadline -o prompt

在 mac 上编译它
#include <string.h>
#include <unistd.h>
#include <readline/readline.h>

char *prefill = "";

int startup_hook() {
    if (prefill) {
        return rl_insert_text(prefill);
    } 
    return 0;
}

// get value for flag
char *arg(int argc, char **argv, char *flag) {
    for (int i = 0; i < argc; i++) {
        if (!strcmp(argv[i], flag)) {
            if (i < argc) {
                return argv[i+1];
            }
        }
    }
    return "";
}

int main(int argc, char** argv)
{
    // prompt and prefill from commandline
    char *prompt = arg(argc, argv, "-p");
    prefill = arg(argc, argv, "-i");

    // cast because functions hooks are supposed to take (char *, int)
    rl_startup_hook = (Function *)startup_hook;

    // use stderr if stdout redirected
    if(!isatty(STDIN_FILENO)) {
        rl_outstream = stderr;
    }

    char *line = readline(prompt);
    puts(line);
    return 0;
}