Search code examples
bashstdoutstdin

Is it possible for bash to "interactively" alternate between reading from and writing to a child process via stdin/stdout?


This question has been asked for many languages but I have yet to find the bash-flavored duplicate.

Suppose I have a program that alternates between writing stdout and reading stdin.

#include <stdio.h>

/*
 * Control_D to exit.
 */
int main(int argc, char** argv){
  char init = 'C';
  if (argc > 1) {
    init = *argv[1];
  }
  putchar(init);
  putchar('\n');

  while (1) {
    int c = getchar();
    if (c == -1) {
      return 0;
    }
    putchar(c);
    putchar('\n');
  }
}

I would like to write a bash script that reads what the program wrote and then decides what to write standard input, and does this repeatedly. That is, something like this:

myProgram &

for i in $(seq 1 10);
do
output=$(# magic command to read myProgram stdout)
if [[ $output = "C" ]]; then
# Magic command to write 'C' to myProgram input
fi
if [[ $output = "D" ]]; then
# Magic command to write 'E' to myProgram input
done

I initially tried to do this with named pipes but this did not work because pipes require both ends to be open before starting and using various exec tricks did not manage to workaround these limitations. I am not ruling them out as a solution, merely pointing out that I was not sufficiently clever to get them to work.

Do these magic commands exist in bash, or do I have to switch to another language?

Let's assume for the sake of this question that I have no control over myProgram and cannot dictate how it communicates; it only understands stdin and stdout because it was intended to be used interactively by a user.


Solution

  • I think you are looking for the coproc builtin. It allows you run commands asynchronously and provides you file descriptors to interact with i.e. a pair of fd's, connected to both stdin and stdout of the command

    coproc myProgram 
    

    The built-in returns the fd pair in the array named COPROC if no name is provided by default. You need something like

    To write to the program

    printf 'foo' >&${COPROC[1]}
    

    To read from the program

    read -u "${COPROC[0]}" var
    

    So your whole program would look like below. Assuming myprogram is the executable available in the current path.

    coproc ./myProgram 
    
    for ((i=1; i<=10; i++)); do
        read -u "${COPROC[0]}" var
        if [[ $var = "C" ]]; then
            printf 'C' >&${COPROC[1]}
        elif [[ $var = "D" ]]; then
            printf 'E' >&${COPROC[1]}
        fi
    done   
    

    Like running a background job using & provides the process id in $! running the program using coproc automatically updates the process id in COPROC_PID variable, so that you can do below, when you are done with the program

    kill "$COPROC_PID"
    

    Untested, but I think you might need to flush out stdout as its not line buffered by default. Use fflush(stdout) from your C program or run the executable with stdbuf -oL