Search code examples
pythonbashpipeprompt

prompt command that works in bash pipe


I'm looking for a prompt command that will allow you to use readline bindings in the middle of a bash pipe, like so:

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

And I get to edit with readline bindings with text prefilled like this:

prompt> prefill|

And then the output of whatever you typed is sent to cat.

I have written a python program that does almost all of this:

#!/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()

This works great for when it's used on its own. prompt -i 'prefill' -p 'prompt> ' works perfectly how I want it to. However, when I put it in a pipe like this prompt -i 'prefill' -p 'prompt> ' | cat it won't work. I understand that is because the prompt is sending it's stdout to the FIFO pipe to cat, so the readline library can't work correctly because it usually interfaces with stdout. So I thought that the solution would be to redirect the file desciptors like this:

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

This will create a 3rd file descriptor, point it to where stdout used to be (the FIFO pipe to cat), points stdout to stderr (terminal), points stderr to the FIFO pipe, and closes the 3rd file descriptor. So, at the end of this we have prompt's stderr going to the FIFO, and prompt's stdout going to the console. If we tweak the python program's last line to print(response, file=sys.stderr), then this should work. But, it doesn't. There are 2 prompts printed, and no prefill. What is wrong?


Solution

  • This seems to be an issue with python's readline implementation: Python module "readline" can't handle output redirection https://bugs.python.org/issue24829

    I wrote this C program which does what I want. You can compile it on mac with clang file.c -L/opt/homebrew/opt/readline/lib -lreadline -o prompt

    #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;
    }