2
\$\begingroup\$
/**
 * Execute an external program, display raw output in real-time, and buffer output pipes (stdout, stderr) for further processing.
 *
 * @param string|array $cmd Command to execute (not escaped)
 * @param string[] $output_buffers Buffers to capture
 * @return int Exit status code
 */
function tee($cmd, &...$output_buffers) {
    $proc = proc_open($cmd, array_fill(1, count($output_buffers), ['pipe', 'w']), $pipes);
    if($pipes) {
        $filePointers = [];
        foreach($pipes as $p => $pipe) {
            stream_set_blocking($pipe, 0);
            $filePointers[$p] = fopen("php://fd/$p", 'w');
        }
        foreach(array_keys($output_buffers) as $b) {
            $output_buffers[$b] = '';
        }
        $sleep = 10*1000;
        while($output_buffers) {
            $readSuccess = false;
            foreach(array_keys($output_buffers) as $b) {
                $p = $b+1;
                if(feof($pipes[$p])) {
                    fclose($pipes[$p]);
                    unset($pipes[$p]);
                    unset($output_buffers[$b]);
                } else {
                    $line = fgets($pipes[$b + 1]);
                    if(strlen($line)) {
                        $readSuccess = true;
                        fwrite($filePointers[$p], $line);
                        $output_buffers[$b] .= $line;
                    }
                }
            }
            if($readSuccess) {
                $sleep = 10*1000;
            } else {
                usleep($sleep);
                if($sleep < 150*1000) {
                    $sleep *= 2;
                }
            }
        }

        foreach($filePointers as $fp) {
            fclose($fp);
        }
    }
    return proc_close($proc);
}

I wrote this function because I write a lot of PHP-CLI scripts that interface with other shell programs. Often passthru works well for this, but sometimes you also want to capture STDERR in the event that the status code doesn't give enough information. This function lets you do that.

This commenter says that we have to put the pipes into non-blocking mode or the process can hang while it's waiting for you to read the other pipes, so that's what I've done.

Example usage:

tee("echo 'hello'; (>&2 echo \"error\"); echo 'world'", $stdout, $stderr);
dump($stdout);
dump($stderr);

Output:

hello
error
world
"""
hello\n
world\n
"""
"error\n"
\$\endgroup\$
  • \$\begingroup\$ Turns out this doesn't work so well. If a process asks for input, it'll just hang not display anything until you answer the question. Which is pretty weird, because this script works with sleep so I don't know why it wouldn't display the question. \$\endgroup\$ – mpen May 7 '16 at 17:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.