7

I want to know how I can interact with a program that I run in a command line PHP script. The scenario is:

  1. Start executing a program.
  2. Read the output until a question is being asked (by reading STDOUT I guess).
  3. Type the answer and press Enter (by writing to STDIN I guess). The user is not to input this, the script already knows what to answer by reading and interpreting the output from step 2.
  4. Again read the output until a new question is being asked.
  5. Again type the answer and press Enter. Again, the script knows it all, no user input is to take place.
  6. This question/answer scenario repeats x number of times until the program is finished.

How can I write a PHP script that does this? I'm thinking that I probably want to use proc_open() but I can't figure out how. I'm thinking it would be something like this but it doesn't work of course:

$descriptorspec = array(
    0 => array('pipe', 'r'),  //STDIN
    1 => array('pipe', 'w'),  //STDOUT
    2 => array('pipe', 'r'),  //STDERR
);
$process = proc_open('mycommand', $descriptorspec, $pipes, null, null);
if (is_resource($process)) {
    // Get output until first question is asked
    while ($buffer = fgets($pipes[1])) {
        echo $buffer;
    }
    if (strpos($buffer, 'STEP 1:') !== false) {
        fwrite($pipes[0], "My first answer\n");  //enter the answer
    } else {
        die('Unexpected last line before question');
    }

    // Get output until second question is asked
    while ($buffer = fgets($pipes[1])) {
        echo $buffer;
    }
    if (strpos($buffer, 'STEP 2:') !== false) {
        fwrite($pipes[0], "My second answer\n");  //enter the answer
    } else {
        die('Unexpected last line before question');
    }

    // ...and so we continue...
} else {
    echo 'Not a resource';
}

UPDATE: I figured out that the program outputs the questions to STDERR (because it writes STDOUT to a file).

TheStoryCoder
  • 3,403
  • 6
  • 34
  • 64
  • check this out: https://stackoverflow.com/questions/2929629/how-do-i-write-a-command-line-interactive-php-script – jsxqf Mar 17 '16 at 02:03
  • @jsxqf You didn't read my question. It's not about me interacting with PHP. It's about PHP interacting with another program (without any input from me). – TheStoryCoder Mar 17 '16 at 11:16
  • Does your external program output a linefeed ("\n") immediately after "STEP 1:" and "STEP 2:" and before expecting the response? Also, are the questions always the same and in the same order? – Matt Raines Mar 17 '16 at 21:21
  • i think socket will be good option for the same? – Chetan Ameta Mar 18 '16 at 09:59
  • @MattRaines No, it asks the question and leaves the cursor at the end of the line, so I guess it doesn't. But the questions and the order are also the same, so you just need to look for the given text before (programmatically) entering the answer. – TheStoryCoder Mar 18 '16 at 11:01
  • 1
    What operating system are you running the code on? – Matt Raines Mar 18 '16 at 11:28

2 Answers2

1

You are certainly on the right track.

It might be best to start with the glaring issue in your code:

while ($buffer = fgets($pipes[1])) {
    echo $buffer;
}

This loop will never exit. At some point the program is going to ask you a question, but your code at this point is still executing the (blocking) fgets call.

As to how to write code that works properly....

The most obvious solution is not to bother waiting for the prompt before providing your answer. This will work as long as:

  1. You don't need to adapt your response based on the preceding output of the program

  2. The program is reading from its stdin and does not clear the buffer at any point

In fact, you don't even need a controlling process to this:

program <input.txt >output.txt 2>errors.txt

But assuming 1 and/or 2 do not apply, and given that it is redirecting its stdout already (which kind of suggests there's more to the story than we know about) then,

...
if (is_resource($process)) {
   while ($buffer=fgets($pipes[2]) { // returns false at EOF/program termination
       if (strpos($buffer, 'STEP 1:') !== false) {
           fwrite($pipes[0], "My first answer\n");      
       } else if (strpos($buffer, 'STEP 2:') !== false) {
           fwrite($pipes[0], "My second answer\n");  //enter the answer
       }
   }
}

Implementing checks for out of sequence questions and branching in the request/response cycle are left as an exercise for the reader.

symcbean
  • 47,736
  • 6
  • 59
  • 94
0

Hopefully the following example based on your code above will be enough to get you started.

I've tested it on Linux although you didn't specify which operating system you were using, so your mileage may vary if you're running something else.

Because the command is piped to another process, the output will be buffered. This means we won't receive the prompts because the buffer waits forever for a linefeed before sending the current line and the prompts are not followed by linefeeds.

Thanks to Chris and ignomueller.net's answers to Trick an application into thinking its stdin is interactive, not a pipe we can confuse the command into thinking it's talking to a terminal by passing it as an argument to script, which generates a typescript of the command's output. By saving the typescript to /dev/null, flushing output after each write (-f) and excluding start and done messages (-q) this means we can read the prompts as they are output.

You specified the STDOUT from the command should be sent to a file, whereas the questions are prompted for on STDERR. This is an added complication because the logic I've used doesn't seem to work reading from STDERR. However, if you redirect STDOUT to a file within the -c parameter to script, and then redirect script's own STDERR to STDOUT, it all seems to work OK.

$descriptorspec = array(
    0 => array('pipe', 'r'),  //STDIN
    1 => array('pipe', 'w'),  //STDOUT
    2 => array('pipe', 'r'),  //STDERR
);
// Avoid buffering by passing the command through "script"
$process = proc_open(
    'script -qfc "mycommand >mycommand.out" /dev/null 2>&1',
    $descriptorspec, $pipes, null, null);
if (is_resource($process)) {
    $buffer = "";
    // Read from the command's STDOUT until it's closed.
    while (!feof($pipes[1])) {
        $input = fread($pipes[1], 8192);
        $buffer .= $input;

        // Output what we've read for debugging. You'd want to add some logic here
        // instead to handle the input and work out the answers to the questions.
        echo $input;

        // Answer the questions when appropriate.
        // This won't work if your output ever includes "STEP 1:" other than when
        // prompting for a question. You might need some more robust logic here.
        if (preg_match("/\nSTEP 1:$/", $buffer)) {
            fwrite($pipes[0], "My first answer\n");
        } elseif (preg_match("/\nSTEP 2:$/", $buffer)) {
            fwrite($pipes[0], "My second answer\n");
        }
    }
    proc_close($process);
} else {
    echo 'Not a resource';
}

I've written the code this way because you specified that the answers to the prompts required "reading and interpreting the output". If the answers were all known in advance of running the program, the solution is much simpler. In this case, you can simply output all the answers straight away before starting to read the response. The command won't care that you didn't wait for the prompt before providing input. You might need to call stream_set_blocking($pipes[0], false); first in this case, I'm not 100% sure.

Community
  • 1
  • 1
Matt Raines
  • 4,149
  • 8
  • 31
  • 34