[ Team LiB ] Previous Section Next Section

Opening Pipes to and from Processes with popen() and proc_open()

Just as you open a file for writing or reading with fopen(), you can open a pipe to a process with popen(). popen() requires the path to a command and a string representing a mode (read or write). It returns a file pointer that can be used similarly to the file pointer returned by fopen(). You can pass popen() one of two mode flags: "w" to write to the process and "r" to read from it. You cannot, however, both read and write to a process in the same connection.

When you have finished working with the file handle returned by popen(), you must close the connection by calling pclose(), which requires a valid file handler.

Reading from popen() is useful when you want to parse the output from a process on a line-by-line basis. Listing 21.1 opens a connection to the GNU version of the who command and parses its output, adding a mailto link to each username.

Listing 21.1 Using popen() to Read the Output of the Unix who Command
 2:   "-//W3C//DTD XHTML 1.0 Strict//EN"
 3:   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 4: <html>
 5: <head>
 6: <title>Listing 21.1 Using popen() to Read the
 7:     Output of the Unix 'who' Command</title>
 8: </head>
 9: <body>
10: <div>
11: <h1>Administrators currently logged on to the server</h1>
12: <?php
13: $ph = popen( "who", "r" )
14:     or die( "Couldn't open connection to 'who' command" );
15: $host="corrosive.co.uk";
16: while ( ! feof( $ph ) ) {
17:   $line = fgets( $ph, 1024 );
18:   if ( strlen( $line ) <= 1 ) {
19:     continue;
20:   }
21:   $line = preg_replace( "/^(\S+).*/",
22:       "<a href=\"mailto:$1@$host\">$1</a><br />\n",
23:       $line );
24:   print "$line";
25: }
26: pclose( $ph );
27: ?>
28: </div>
29: </body>
30: </html>

We acquire a file pointer from popen() on line 13 and then use a while statement on line 16 to read each line of output from the process. If the output is a single character, we skip the rest of the current iteration (lines 14 and 15). Otherwise, we use preg_replace() on line 21 to add an HTML link to the string before printing the line on line 24. Finally, we close the connection with pclose() on line 26. Figure 21.1 shows sample output from Listing 21.1.

Figure 21.1. Reading the output of the Unix who command.


You can also use a connection established with popen() to write to a process. This is useful for commands that accept data from standard input in addition to command-line arguments. Listing 21.2 opens a connection to the column application using popen().

Listing 21.2 Using popen() to Pass Data to the column Application
 2:   "-//W3C//DTD XHTML 1.0 Strict//EN"
 3:   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 4: <html>
 5: <head>
 6: <title>Listing 21.2 Using popen() to Pass
 7:    Data to the 'column' Command</title>
 8: </head>
 9: <body>
10: <div>
11: <?php
12: $products = array(
13:     array( "HAL 2000", 2, "red" ),
14:     array( "Tricorder", 3, "blue" ),
15:     array( "ORAC AI", 1, "pink" ),
16:     array( "Sonic Screwdriver", 1, "orange" )
17:     );
18: $ph = popen( "column -tc 3 -s / > purchases/user3.txt", "w" )
19:   or die( "Couldn't open connection to 'column' command" );
20: foreach ( $products as $prod ) {
21:   fputs( $ph, join('/';, $prod). "\n");
22: }
23: pclose( $ph );
24: ?>
25: </div>
26: </body>
27: </html>

The purpose of the script in Listing 21.2 is to take the elements of a multidimensional array (defined on line 12) and output them to a file as an ASCII table. We open a connection to the column command on line 18, adding some command-line arguments. -t requires that the output should be formatted as a table, -c 3 determines the number of columns we require, and -s / sets the "/" character as the field delimiter. We ensure that the results will be written to a file called user3.txt. Note that the purchases directory must exist on your system and that your script must be capable of writing to it.

Notice that we are doing more than one thing with this command. We are calling the column command and writing its output to a file. In fact, we are issuing commands to a noninteractive shell. This means that, in addition to piping content to a process, we can initiate other processes as well. We could even have the output of the column command mailed to someone, like so:

popen( "column -tc 3 -s / | mail matt@corrosive.co.uk", "w" )

This level of flexibility can open your system to a grave threat if you ever pass user input to a PHP function that issues shell commands. We will look at precautions you can take later in the hour.

Having acquired a pipe resource, we loop through the $product array on line 20. Each value is itself an array, which we convert to a string using the join() function on line 21. Rather than joining on a space character, we join on the delimiter we established as part of our command-line arguments. Using the "/" character to join the array is necessary because the spaces in the product array would otherwise confuse the column command. Having joined the array, we pass the resultant string and a newline character to the fputs() function.

Finally, we close the connection. Taking a peek into the user3.txt file, we should see the table neatly formatted:

HAL 2000     2 red
Tricorder    3 blue
ORAC AI     1 pink
Sonic Screwdriver 1 orange

We could have made the code more portable by formatting the text using the sprintf() function. This would be the preferred approach. Listing 21.2 illustrates a technique that can be useful either for working with third-party commands that have no equivalent within PHP or when you want to build a quick, nonportable script that uses system commands.

In some situations, you will need finer control of a child process. The proc_open() function allows you to spawn a process, write to it, read from it, and read its error output. proc_open() requires a string representing the process to start, an array of descriptors that define modes of communication with the process, and an array that's populated with pipes.

The array of descriptors should consist of three elements. Each element should itself take the form of an array containing the string 'pipe' or 'file'. If the first element is 'pipe', this descriptor represents a read or write pipe between the process and the script and the second element should be either of 'r' for read or 'w' for write. If the first element is 'file', the descriptor represents a read from, or write to, a file and the second element should be a path to a file. The third element should be 'r' for read, 'w' for write, or 'a' for append. Here's an example:

$descriptors = array( 0 => array( "pipe", r ),
            1 => array( "pipe", w ),
            2 => array( "file", "errors.txt", a )
$proc = proc_open( "my_cmd", $descriptors, $pipes );

The $descriptors array in the previous fragment initializes two pipes. The first pipe represents the standard input from which the process reads, and the second pipe represents the standard output to which the process writes. The third element represents a standard error. In this case, the process appends errors to file called errors.txt. Notice that reading, writing, and appending are all defined from the point of view of the process and not the script. We call proc_open(), passing it a string pointing to a command called 'my_cmd', the $descriptor array, and an as-yet-empty variable called $pipes. The proc_open() function returns a resource and populates $pipes with an array of resources that mirror the $descriptors array. We can write to $pipes[0] and read from $pipes[1] just as we would with file resources. Errors are written to the 'errors.txt' file without our intervention:

fwrite( $pipes[0], "some input text" );
while ( ! feof( $pipes[1] ) ) {
  print fgets( $pipes[1], 1024 );

After we have written to our command and read from it we should close any open pipes (in this case, the read and write pipes) before calling proc_close. proc_close() requires a single argument: the resource returned by proc_open(). Here's the code:

fclose( $pipes[0] );
fclose( $pipes[1] );
proc_close( $proc );

Listing 21.3 creates a small class called Grepper that uses proc_open() to work with the standard Unix grep command.

Listing 21.3 A Class That Uses proc_open()
 1: <?php
 3: class Grepper {
 4:   private static $descriptors = array( 0 => array( "pipe", r ),
 5:                      1 => array( "pipe", w ),
 6:                      2 => array( "pipe", w )
 7:                  );
 9:  static function grep ( $in, $arg ) {
10:    $proc = proc_open( "grep $arg", self::$descriptors, $pipes );
11:    if ( ! is_resource( $proc ) ) {
12:      throw new Exception( "proc_open did not return a resource" );
13:    }
14:    fwrite( $pipes[0], $in );
15:    fclose( $pipes[0] );
16:    while ( ! feof( $pipes[1] ) ) {
17:      $ret .= fgets($pipes[1], 1024);
18:    }
19:    fclose( $pipes[1] );
20:    try {
21:      self::checkError( $pipes[2] );
22:    } catch( Exception $e ) {
23:      throw $e;
24:    }
25:    proc_close( $proc );
26:    return $ret;
27:  }
29:  static private function checkError( $pipe ) {
30:    $ret = "";
31:    while ( ! feof( $pipe ) ) {
32:      $ret .= fgets( $pipe );
33:    }
34:    fclose( $pipe );
35:    if ( $ret ) {
36:      throw new Exception( $ret );
37:    }
38:    return false;
39:  }
40: }
42: $string = "mary had a little lamb\n";
43: $string .= "it's fleece was white as snow\n";
44: $string .= "and everywhere that mary went\n";
45: $string .= "the lamb was sure to go\n";
47: try {
48:   print ( Grepper::grep( $string, "mary" ));
49: } catch ( Exception $e ) {
50:   print "error: ".$e->getMessage();
51: }
52: ?>

The Grepper class uses proc_open() to call the Unix command grep, which performs a fast search for patterns in strings or files. We set up our descriptors array on line 3, making the $descriptors property private and static. We make $descriptors static because we are going to allow our class to be called statically (that is without the need for creating a Grepper object). Notice that in this example, we are using a pipe for standard error rather than a file. The class has only one public method—grep()—which starts on line 9. The method requires a string to be searched, $in, and a string representing the pattern to find, $arg. Notice that we have declared it static. This means that client coders can call the grep() method using the Grepper class rather than a Grepper object, like this:

Grepper::grep( "search for gold", "gold" );

We call proc_open() on line 10, passing it the grep command and the client-supplied argument in a single string. We pass it the $descriptors property and an uninitialized $pipes variable.

We check that proc_open() returned a valid resource on line 11. If not, we throw an exception and thereby end method execution.

On line 14, we write the string to be searched to $pipe[0], passing the data to the grep command. We have nothing more to say to grep, so we close the pipe on line 15.

On lines 16–18 we read any output from grep, storing it in a return variable before closing the read pipe.

We call a method named checkError() on line 21, passing it our last remaining pipe: the error descriptor. The checkError() method simply reads from the pipe it is supplied (line 31). If it finds any content, it instantiates and throws an Exception object, which would be rethrown in the grep() method (line 23). In fact, we do not need to manually rethrow an exception. By failing to catch an exception in a method, we implicitly throw it back to the calling code. In Listing 21.3 we catch any Exception object thrown by checkError() and throw it manually to make our code clearer.

Assuming that checkError() does not throw an exception, the grep() method calls proc_close() on line 25 and returns the output it has gathered.

We test the class on line 42, creating a nursery rhyme string and passing it to the grep() method together with the search string "mary". We wrap the call to grep() in a try clause. If an exception is thrown, we output its message to the browser on line 50; otherwise, we print the results of the grep() method on line 48.

    [ Team LiB ] Previous Section Next Section