Search code examples
bashinterprocess

How do you connect the keyboard and a named pipe to STDIN in Bash?


I'm trying to find a way to invoke an interactive command-line program so that it takes input both directly from the keyboard and also from a named pipe. My goal is to get this working with MATLAB, but I think Bash works just as well as an example. So the idea is to start Bash and once it's running I can type commands, use up arrow for history, etc, and also send commands to a named pipe. I've been looking around and fiddling with this for a few days, but nothing I've tried so far has worked quite right.

For example, there's a helpful thread at https://serverfault.com/questions/171095/how-do-i-join-two-named-pipes-into-single-input-stream-in-linux that suggests doing something like this:

mkfifo alt_in
(tail -f alt_in & cat) | bash

This is almost exactly what I'm looking for, except that if you try to use backspace or arrow keys it doesn't work right. (I guess this is because cat is intercepting the keystrokes, which would normally be handled by the readline library in bash?) Does anyone have other suggestions?

Ultimately I'd like to have a way to launch a MATLAB process so that I can send commands to it from TextMate but also interact with it in the terminal. I use MATLAB for work, but am no great fan of its GUI or editor.

Edit December 19, 2010

Thanks for all the very helpful suggestions! I wanted to summarize the outcome of this for anyone who's interested. I looked into rlwrap, but since my goal was to get this working inside a TextMate bundle that anyone could use, I was hesitant to rely on a non-standard utility (which I should have mentioned in the first place). I also checked on expect and unbuffer briefly, but, well, they seem pretty complicated and I just didn't have the fortitude to dive in to that.

screen worked pretty well for this - as suggested below, I could open Terminal, start screen, start matlab -nodesktop, and then it was possible to have TextMate, for example, send the selected text to Matlab using screen -X ... inside a TextMate command. The shortcomings of this approach that I noticed were:

  1. Sending a block of text with more that 1,024 characters resulted in an error from `screen`; I figure there's some buffer somewhere that can't hold more than that, but didn't spend any time trying to track it down.
  2. Screen has its own scrollback buffer, making Terminal's scrollbar pretty useless. Maybe there's an option to change how this works?
  3. Control-A is `screen`'s command keystroke; you can't use it for moving to the beginning of the line unless you rebind the command key.

While I was playing around with screen, it occurred to me that although the question I asked is about connecting pipes and STDIN and so on, I actually only care about this for a very specific case: getting text from TextMate to Terminal. And that prompted me to try doing this with AppleScript, and surprisingly that turned out to be simpler and more robust than any of the other things I'd tried. Create a TextMate command that takes the selected text or current line as input, and contains

#!/usr/bin/env osascript

set input_command to do shell script "cat"

tell application "Terminal"
    do script input_command in window 1
end tell

And that works great, as long as MATLAB is in the frontmost Terminal window. (It's also possible to search for a Terminal tab that's running MATLAB, I just excluded that for clarity.)

The moral of this story is that I should ask more specific questions. Thanks again for all the help; I've learned a lot from wrestling with this!


Solution

  • Give this a try:

    mkfifo alt_in
    (tail -f alt_in & rlwrap cat) | bash
    

    You won't get a prompt, but you can enter commands and use arrow keys, etc., including history retrieval. You can echo commands from another terminal into the named pipe and they'll be executed at the receiving end, but they won't be in its history.

    Some other things to investigate:

    • expect
    • screen -x and screen -X

    For the latter, in one terminal start screen. You'll be able to interact with Bash there pretty much as you normally would. Now on another terminal, do

    screen -ls
    

    to determine the PID of the screen session you started. It will be the digits at the start of a line that looks like this:

    31544.pts-2.hostname     (01/01/2010 01:01:01 PM)        (Attached)
    

    Now you can do things like this:

    screen -S 31544 -X stuff $'echo Your ad here.\n'
    

    which will cause the echo command to be executed on the other terminal. The $'' causes the escape sequences within to be interpreted, in this case giving us a newline so we can "press" Enter.

    Let's say we did this:

    screen -S 31544 -X stuff $'top\n'
    

    Now top is running. We could go to that terminal and press q to quit, but where's the fun in that?

    screen -S 31544 -X stuff 'q'
    

    Notice this time we didn't need a newline.