Search code examples
bashpromptreadlineread-eval-print-loop

How can I wrap a command with a line-buffered prompt?


I'm looking for a command that first spawns a process by a given command, then prompts the user using a given prompt string to enter a line (with readline functionality), pipes the entered line into that process, and repeats. Any output of the process is printed on the lines above the line of the prompt in order to prevent a mess, so that the prompt is always the last line on screen, but the process can output something at any time.

For example, a prompt command such that prompt -p "> " cat runs cat with a prompt before every line to be entered. It would look something like this:

$ prompt -p "> " cat
> hello
hello
> every time it's time for me to type, there's a prompt!
every time it's time for me to type, there's a prompt!
> for sure
for sure

Maybe you can also specify a prompt for the output of the command like this:

$ prompt -p "[IN] " -o "[OUT] " grep hi
[IN] hello
[IN] this is another example
[OUT] this is another example
[IN] it sure is, i'm glad you know

I found rlwrap (https://github.com/hanslub42/rlwrap) and it seems to do the line buffering with readline functionality, but without the input prompt.

Basically, I want a command that can turn any command that operates on an input stream into a friendly repl.

This almost works, but whenever the process outputs something, the cursor ends up in the wrong position:

CMD="grep hi" # as an example

prompt () {
    while true
    do
        printf "> \033\067"
        read -e line || break
        echo $line > $1
    done
}

prompt >(stdbuf -oL $CMD |
    stdbuf -oL sed 's/^/< /' |
    stdbuf -oL sed 's/^/'`echo -ne "\033[0;$(expr $(tput lines) - 1)r\033[$(expr $(tput lines) - 1);0H\033E"`'/;s/$/'`echo -ne "\033[0;$(tput lines)r\033\070\033M"`'/')

Here is another example for clarity. Imagine a simple irc client command that reads commands from stdin and outputs simple messages to stdout. It has no kind of interface or even a prompt, it simply reads, and prints directly from stdin and to stdout:

$ irc someserver
NOTICE (*): *** Looking up your hostname...
NOTICE (*): *** Found your hostname
001 (madeline): Welcome to someserver IRC!! madeline!madeline@somewhere
(...)
/join #box
JOIN (): #box
353 (madeline = #box): madeline @framboos
366 (madeline #box): End of /NAMES list.
hello!
<madeline> hello!
(5 seconds later)
<framboos> hii

Using the prompt command it would look something more like this:

$ prompt -p "[IN] " -o "[OUT] " irc someserver
[OUT] NOTICE (*): *** Looking up your hostname...
[OUT] NOTICE (*): *** Found your hostname
[OUT] 001 (madeline): Welcome to someserver IRC!! madeline!madeline@somewhere
(...)
[IN] /join #box
[OUT] JOIN (): #box
[OUT] 353 (madeline = #box): madeline @framboos
[OUT] 366 (madeline #box): End of /NAMES list.
[IN] hello!
[OUT] <madeline> hello!
(5 seconds later)
[OUT] <framboos> hii
[IN] 

The point being that a single process is spawned and every line you enter is piped into that same process, it doesn't spawn a new process for each line. Also notice how the [IN] prompt is not clobbered by the message from framboos, but rather the message is printed on the line above the prompt. The rlwrap program mentioned above does this properly. The only thing it's missing from what I can tell is the prompt string (s).


Solution

  • First of all, I was wrong about rlwrap, you can use a prompt with it:

    rlwrap -S "> " grep hi
    

    And it works pretty well. But it leaves artifacts of the prompt if you have something typed when something is printed by the process.

    Then I found socat, and it can do essentially the same thing as the above (among other things), but it doesn't leave those artifacts (by blocking stdout while you type until you press enter and the line is clear again):

    socat READLINE,prompt="[IN] " EXEC:"stdbuf -oL grep hi"
    [IN] hello
    [IN] this is another example
    this is another example
    [IN] it sure is, i'm glad you know
    

    Then I can just use sed to add a prompt to the output:

    socat READLINE,prompt="[IN] " SYSTEM:"stdbuf -oL grep hi | stdbuf -oL sed \'s/^/[OUT] /\'"
    [IN] hello
    [IN] this is another example
    [OUT] this is another example
    [IN] it sure is, i'm glad you know