PHP on the Command Line – Part 2

In Part 1 of this tutorial, we had a look at PHP’s command line SAPI (server API). In this article, we’ll take things a step further, and see how you can hook up a command line PHP script with existing command line tools provided by your operating system.

In contrast to the last article, the focus of this article will be Unix-based systems — something which can’t be avoided. To steal a quote from Linux.com’s CLI for Noobies series:

"[Comparing] the command line environment of DOS/Windows with that of Linux, [is] like comparing a wheelbarrow with an 18-wheeler."

I’ll assume that you’re using PHP 4.3.x+ with the CLI SAPI to execute PHP scripts on the command line. The examples provided expect to find the CLI binary at /usr/local/bin/php. If your setup is different than this, modifying the first line should remedy the situation in each case. Note that you can download the complete code archive for this tutorial here.

Today’s Command Line options:

  • Shell Execution: run external programs from PHP
  • Security Issues: terrorists threaten the command line!
  • Working with the Environment: talk to Mother Operating System
  • Process Control: it kills me
Shell Execution

One of the advantages of working on Unix-based systems is the wealth of command line tools that are available to solve most any common problem you can think of.

This becomes particularly important when you’re writing PHP scripts for the command line. Performing a search and replace over a group of text files, for example, is best left to tools like grep and sed, both for performance and to save precious development hours. That’s not to say that you shouldn’t use PHP as a "front end" to perform search and replace operations, but that you should delegate the "real work" to existing tools whenever possible.

PHP provides a number of functions for executing external programs via the shell, namely shell_exec(), passthru(), exec(), popen(), all of which do essentially the same thing, but provide different "interfaces" to the external program. Each of these functions can only capture output written to the STDOUT pipe; each will spawn a child process within which the external program will be executed.

Two further functions exist to execute external programs: proc_open() and pcntl_exec(). Their use and behaviour differs from the preceding functions, so we’ll examine them separately.

What’s my Extension?

To give us a "known quantity" that we can execute with other PHP scripts, first up, we’ll look at a script that examines the PHP extensions available in any given installation. I’ll let the code explain itself; everything you’ll see here, you saw in the last article:

#!/usr/local/bin/php 
<?php
# Include PEAR::Console_Getopt
require_once 'Console/Getopt.php';

// Define error return codes
define('INVALID_PHP_SAPI',3);
define('UNKNOWN_EXTENSION',4);

//--------------------------------------------------------------------------------------
/**
* Displays the usage of this script
* Called when -h option specified or on illegal option
*/
function usage() {
 $usage = <<<EOD
Usage: ./extensions.php [OPTION]
Lists the loaded PHP extensions or shows the functions
a single extension makes available
 -e=EXTENSION  Name of extension to list functions for
 -h    Display usage

EOD;
 fwrite(STDOUT,$usage);
 exit(0);
}

//--------------------------------------------------------------------------------------
/**
* Gets the name of an extension from -e option
* (defaults to NULL)
*/
function getExtension() {

 $args = Console_Getopt::readPHPArgv();

 // Could be an error with older PHP versions and the CGI SAPI
 if ( PEAR::isError($args) ) {
   fwrite(STDERR,$args->getMessage()."n");
   exit(INVALID_PHP_SAPI);
 }

 // Compatibility between "php extensions.php" and "./extensions.php"
 if ( realpath($_SERVER['argv'][0]) == __FILE__ ) {
   $options = Console_Getopt::getOpt($args,'he:');
 } else {
   $options = Console_Getopt::getOpt2($args,'he:');
 }

 // Check for invalid options
 if ( PEAR::isError($options) ) {
   fwrite(STDERR,$options->getMessage()."n");
   usage();
 }

 // Set default length
 $extension = NULL;

 // Loop through the user provided options
 foreach ( $options[0] as $option ) {
   switch ( $option[0] ) {
     case 'h':
       usage();
     break;
     case 'e':
       $extension = $option[1];
     break;
   }
 }

 return $extension;
}

//--------------------------------------------------------------------------------------
// Get a list of extensions
$extensions = get_loaded_extensions();

$extension = getExtension();

// If it's not null, the -e option was used
if ( !is_null($extension) ) {

 // Does the extension actually exist?
 if ( in_array($extension,$extensions) ) {

   // Display the list of functionsthe extension provides
   $funcs = get_extension_funcs($extension);
   foreach ( $funcs as $func ) {
     fwrite(STDOUT,$func."n");
   }

 } else {

   fwrite(STDERR,"Unknown extension $extensionn");
   exit(UNKNOWN_EXTENSION);

 }
 
} else {

 // Display the list of extension
 foreach ( $extensions as $extension ) {
   fwrite(STDOUT, $extension."n");
 }
 
}

exit(0);
?>

Filename: extensions.php

The code is executed using this command:

$ ./extensions.php

When executed, the above script displays a list of loaded PHP extensions by default. However, if the command line ‘-e’ option is provided, along with an extension name, it lists the functions the extension provides.

$ ./extensions.php -e mysql

The above command displays a list of the functions provided by the MySQL extension (or whichever extension name you provided).

Don’t forget that you need to make the script executable by setting the correction permissions as follows:

$ chmod +x extensions.php

Now that I’ve got a simple program to work with, I can execute it from another script.

shell_exec()

The shell_exec() command is actually an alias for the backtick operator. It allows you to execute an external program via the shell and have the results returned to you as a string.

Whether you use the backtick operator or the shell_exec() command is up to you.

Personally, I prefer the latter as it clearly states what your code is doing and is easy to see in the source code.

Here’s a basic example:

#!/usr/local/bin/php 
<?php
$result = shell_exec('./extensions.php');
fwrite(STDOUT,$result);
exit(0);
?>

Filename: shell_exec1.php

As you can see, using shell_exec(), I can simply name an external program that I want to execute, and get the results back in the returned variable. There’s one important requirement for the above example to work — the shell_exec1.php script must execute from the directory in which extensions.php resides. We’ll look at this point further when we consider the environment below.

We can also pass command line options via shell_exec(). Let’s look at an example:

#!/usr/local/bin/php 
<?php
$result = shell_exec('./extensions.php -e mysql');
fwrite(STDOUT,$result);
exit(0);
?>

Filename: shell_exec2.php

In other words, the command we tell shell_exec() to execute can be anything we can type ourselves, from the command line. For example, I can pipe the output of extensions.php through grep, filtering for all extensions that begin with the letter ‘p’:

#!/usr/local/bin/php 
<?php
$result = shell_exec('./extensions.php | grep -w "^p.*"');
fwrite(STDOUT,$result);
exit(0);
?>

Filename: shell_exec3.php

This returns the following list (from my system):

posix 
pgsql
pcre
pcntl

One important thing to note about shell_exec() is that it only returns output from STDOUT. Take a look at this example:

#!/usr/local/bin/php 
<?php
$result = shell_exec('./extensions.php -e hair 2>/dev/null');

// Assumes that nothing was written to STDOUT if there was an error
if ( !empty($result) ) {
 fwrite(STDOUT,$result);
} else {
 fwrite(STDERR,"An error occurredn");
}
exit(0);
?>

Filename: shell_exec4.php

Here, I’ve redirected the STDERR stream to Unix’s NULL device (I’m throwing it away). If I execute my extensions.php script with an extension name that doesn’t exist, nothing is written to STDOUT, so the above script can display a (somewhat useless) message that an error occurred.

Note that if I don’t redirect STDERR to the null device, I will still see the error message ("Unknown extension hair") from my shell, the error having "bypassed" shell_exec4.php.

passthru()

The passthru() function allows you to execute an external program and display its results directly:

#!/usr/local/bin/php 
<?php
passthru('./extensions.php');
exit(0);
?>

Filename: passthru1.php

That’s all you need to do (passthru() doesn’t return anything).

It does take a second argument, though — a variable that’s populated with the return code of the external program. For example:

#!/usr/local/bin/php 
<?php
$return_code = 0;

passthru('./extensions.php -e hair 2>/dev/null',$return_code);

if ( $return_code != 0 ) {
 fwrite(STDERR,"There was an error!n");
}
exit(0);
?>

Filename: passthru2.php

This provides me with a more accurate mechanism to check for errors than did shell_exec(), with which I was forced to assume that nothing would be written to STDOUT if an error occurred.

exec()

The exec() function provides another variation on the theme. It returns the last line of output from the external program, but also (optionally) populates an array with the full output and makes the return code available. If I assume that most programs will return a single-line error message, if indeed there was one, exec() can be pretty handy. Let’s see another example:

#!/usr/local/bin/php 
<?php
$return_code = 0;
$result = array();

$error = exec('./extensions.php -e hair 2>&1',$result, $return_code);

if ( $return_code != 0 ) {
 fwrite(STDERR,"Error: $errorn");
} else {
 $result = implode("n",$result);
 fwrite(STDOUT,$result);
}
exit(0);
?>

Filename: exec1.php

First, I redirect the STDERR stream to STDOUT, so that I can capture both from my script. I use the $error variable only if $return_code is non-zero. Otherwise, I can display the contents of the $result array.

system()

The system() function provides yet another variation on the theme — something between passthru() and exec(). Like passthru(), it also outputs directly anything it receives from the external program, on the STDOUT stream. However, like exec(), it also returns the last line output, and makes the return code available. For example:

#!/usr/local/bin/php 
<?php
$return_code = 0;
$result = array();

$error = system('./extensions.php -e hair 2>&1',$return_code);

if ( $return_code != 0 ) {
 // Fictional logger
 # Logger::log($error);
}
exit(0);
?>

Filename: system1.php

An important difference in this code is that, though the error message will still be displayed immediately to the shell, I also have the opportunity to log the error message.

popen()

The popen() function allows me to work with an external program as if I were working with a file. For example:

#!/usr/local/bin/php 
<?php
$fp = popen('./extensions.php','r');

while ( !feof($fp) ) {
 fwrite(STDOUT, fgets($fp));
}

pclose($fp);
exit(0);
?>

Filename: popen1.php

As with all the preceding functions, popen() only reads from STDOUT. As such, the following example will display nothing:

#!/usr/local/bin/php 
<?php
$fp = popen('./extensions.php -e hair 2>/dev/null','r');

while ( !feof($fp) ) {
 fwrite(STDOUT, fgets($fp));
}

pclose($fp);
exit(0);
?>

Filename: popen2.php

One thing that’s special about popen() is it also allows you to write to the program’s STDIN stream, in the way we saw in the last tutorial.

Let’s imagine I have a simple script like this:

#!/usr/local/bin/php 
<?php
while ( trim($line = fgets(STDIN)) != 'exit' ) {

 fwrite(STDOUT,$line);

}

exit(0);
?>

Filename: reader.php

If I execute this script myself, it simply displays everything I type in, line by line, until I enter the string ‘exit’.

I can now write to this program with popen() like so:

#!/usr/local/bin/php 
<?php
$colors = array('red','green','blue');

// Open program in write mode
$fp = popen('./reader.php','w');

foreach ( $colors as $color ) {
 fwrite($fp,$color."n");
}

fwrite($fp,"exitn");

pclose($fp);
exit(0);
?>

Filename: popen3.php

If I run this script, the three colors in the $colors array will be displayed on new lines in my terminal. Because I opened the program with popen() in write mode, instead of capturing the STDOUT stream, it uses STDIN instead, which llows the results to remain visible.

Note the popen() is uni-directional. You can only read or write to an external program, not both (although the PHP manual has some interesting submitted hacks to get round this).

With PHP 4.3.x+ the function proc_open() allows you to work with more than one IO pipe at once, as we’ll see later.

Security Issues

Programming just wouldn’t be any fun if there weren’t the constant threat of "haXor ownZ you", would it? Well, the good news is that there’s plenty of opportunity, when executing external programs from PHP, to give the world access to your inner sanctum.

Command Injection

Executing external programs with PHP, in which user input affects the program executed, has parallels with the execution of SQL statements: you run the risk of command injection.

Consider the follow example of how not to do it!

#!/usr/local/bin/php  
<?php  
fwrite(STDOUT,"This is insecurity in action!n");  
 
fwrite(STDOUT,"Enter a filename to list: ");  
 
$file = trim(fgets(STDIN));  
 
$result = shell_exec("ls -l $file");  
 
fwrite(STDOUT, $result);  
 
exit(0);  
?>

Filename: security1.php

See the problem? Imagine that I, as the user of the script, enter something like this at the "Enter a filename to list: " prompt:

Enter a filename to list: whatever.php; rm -rf ./;

Whoops — no more files in this directory! The command I’ve executed with shell_exec() will actually look like this:

$result = shell_exec("ls -l whatever.php; rm -rf ./;");

In fact, I’m executing two commands!

One way to prevent this problem occurring in this example would be to place the $file variable in single quotes, and to strip any single quotes the user may have added, like so:

$file = str_replace(''','',$file);  
$result = shell_exec("ls -l '$file'");

Important Note: do not use double quotes to escape the user-submitted value, because this allows the insertion of environment variables and the like. See When is a command line not a line? for a discussion of parameter expansion and the general CLI security issues involved in mixing with user input.

A much better approach is to use the PHP function escapeshellarg, which ensures that all single quotes in a string are properly escaped, and places a single quote at the start and end of the string.

If we take a script like this:

<?php  
$arg = "foo 'bar' foo";  
echo escapeshellarg($arg)."n";  
?>

The output displayed will look like that shown below:

'foo '''bar''' foo'

You can use this output safely as a single argument to an external program you’re executing.

In other words, the script security1.php above should become:

#!/usr/local/bin/php  
<?php  
fwrite(STDOUT,"Enter a filename to list: ");  
 
$file = trim(fgets(STDIN));  
 
$file = escapeshellarg($file);  
 
$result = shell_exec("ls -l $file");  
 
fwrite(STDOUT, $result);  
 
exit(0);  
?>

Filename: security2.php

Another function provided by PHP is escapeshellcmd(), which escapes meta characters such as ; with a backslash. This provides an alternative to using single quotes, which may be required in some circumstances. In general, though, it’s best to use escapeshellarg() unless you have a specific need.

Warning to Windows Users: with PHP versions 4.3.6 and below it is not secure to use escapeshellarg() and escapeshellcmd(). See this alert on Security Tracker.

More generally, as with Web applications, we mustn’t simply rely on the escape functions here; we must restrict users to entering only that content that they’re required to provide. For example, if they should be entering only a string that contains characters from the alphabet, validate the input with a simple regular expression, like this:

if ( !preg_match('/^[a-zA-Z]*$/', $userinput) ) {  
 exit(INVALID_INPUT);  
}

Shared Hosts

If you’re not feeling enough paranoia already, consider what can happen when the arguments you pass to command line script contain sensitive information. For example, consider executing the following:

$ mysqldump --user=harryf -password=secret somedatabase > somedatabase.sql

This makes my username and password available in the list of processes running on the system, and visible to all via the ps command. It’s not hard for someone to write a program that harvests this kind of information automatically.

The same problem also applies when we execute programs from a PHP script.

To demonstrate, here’s an example the generates a "long wait" (the first assignment for most trainee mechanics is to go ask their supervisor for a long wait…); basically this script will "hang around" long enough for you to see it with the ps command. It’s also a nice opportunity to see PEAR::Console_ProgressBar in action:

#!/usr/local/bin/php  
<?php  
# Include PEAR::Console_Getopt  
require_once 'Console/Getopt.php';  
 
# Include PEAR::Console_ProgressBar  
require_once 'Console/ProgressBar.php';  
 
# Exit codes  
define('INVALID_PHP_SAPI',3);  
define('INVALID_LOGIN',4);  
 
# Valid username / password  
define('USERNAME','harryf');  
define('PASSWORD','secret');  
 
//--------------------------------------------------------------------------------------  
/**  
* Displays the usage of this script  
* Called when -h option specified or on illegal option  
*/  
function usage() {  
 $usage = <<<EOD  
Usage: ./longweight.php [OPTION]  
Builds long weights.  
 -l=LENGTH  Length of weight  
 -u=USERNAME  Username  
 -p=PASSWORD  Password  
 -h    Display usage  
 
EOD;  
 fwrite(STDOUT,$usage);  
 exit(0);  
}  
 
//--------------------------------------------------------------------------------------  
/**  
* Gets the options specified by the user  
*/  
function getOptions() {  
 
 $args = Console_Getopt::readPHPArgv();  
 
 // Could be an error with older PHP versions and the CGI SAPI  
 if ( PEAR::isError($args) ) {  
   fwrite(STDERR,$args->getMessage()."n");  
   exit(INVALID_PHP_SAPI);  
 }  
 
 // Compatibility between "php longweight.php" and "./longweight.php"  
 if ( realpath($_SERVER['argv'][0]) == __FILE__ ) {  
   $options = Console_Getopt::getOpt($args,'hl:u:p:');  
 } else {  
   $options = Console_Getopt::getOpt2($args,'hl:u:p:');  
 }  
 
 // Check for invalid options  
 if ( PEAR::isError($options) ) {  
   fwrite(STDERR,$options->getMessage()."n");  
   usage();  
 }  
 
 // Set defaults for length, username and password  
 $ret_options = array(  
   'length' => 10,  
   'username'=> NULL,  
   'password'=> NULL  
 );  
 
 // Loop through the user provided options  
 foreach ( $options[0] as $option ) {  
   switch ( $option[0] ) {  
     case 'h':  
       usage();  
     break;  
     case 'l':  
       $ret_options['length'] = $option[1];  
     break;  
     case 'u':  
       $ret_options['username'] = $option[1];  
     break;  
     case 'p':  
       $ret_options['password'] = $option[1];  
     break;  
   }  
 }  
 
 return $ret_options;  
}  
 
//--------------------------------------------------------------------------------------  
/**  
* Validates the user. If not username and password was provided  
* by options, prompts user to enter them  
*/  
function validateUser($username,$password) {  
 if (is_null($username) ) {  
   fwrite(STDOUT,'Enter username: ');  
   $username = trim(fgets(STDIN));  
 }  
 if (is_null($password) ) {  
   fwrite(STDOUT,'Enter password: ');  
   $password = trim(fgets(STDIN));  
 }  
 if ( !(($username == USERNAME) & ($password == PASSWORD)) ) {  
   exit(INVALID_LOGIN);  
 }  
}  
 
//--------------------------------------------------------------------------------------  
/**  
* Returns an instance of PEAR::Console_ProgressBar  
*/  
function & getProgressBar($length) {  
 // Progress bar display (see documentation)  
 $display = 'Weight preparation %fraction% [%bar%] %percent% complete';  
 
 // Indicator for progress  
 $progress = '+';  
 
 // Indicator for what's remaining  
 $remaining = '-';  
 
 // Width of the progress bar  
 $width = '50';  
 
 return new Console_ProgressBar($display,$progress,$remaining,$width,$length);  
};  
 
//--------------------------------------------------------------------------------------  
// Get the command line options  
$options = getOptions();  
 
// Make sure we have a valid user (exits if not)  
validateUser($options['username'],$options['password']);  
 
fwrite(STDOUT, "Preparing Long Weight of length {$options['length']}n");  
 
$PBar = & getProgressBar($options['length']);  
 
// Loop for the length of the weight  
for ($i=0;$i<$options['length'];$i++) {  
 
 // Update the progress bar  
 $PBar->update($i+1);  
 
 sleep(1);  
}  
 
fwrite(STDOUT,"nDid you enjoy your long weight?n");  
 
exit(0);  
?>

Filename: longweight.php

Should you feel the need for a long wait, you can now get one by entering the following from the command line:

$ ./longweight.php -l 30 -u harryf -p secret

The script will hang around in the process list for 30 seconds, with the username and password available for all to see.

I now execute this command with another script, such as:

#!/usr/local/bin/php  
<?php  
shell_exec('./longweight.php -l 30 -u harryf -p secret');  
fwrite(STDOUT,"Long weight finishedn");  
exit(0);  
?>

Filename: execlongweight1.php

Now, from the command line, run the script in the background and examine the process list:

$ ./execlongweight1.php &  
$ ps -eo pid,ppid,cmd | grep longweight

I get output like this:

26466  1589 /usr/local/bin/php ./execlongweight1.php  
26467 26466 /usr/local/bin/php ./longweight.php -l 30 -u harryf -p secret

The way I’ve formatted the process list displays the process ID in the first column, the parent process id in the second column, and the executing command in the third column. You can see that the execlongweight1.php script was given process ID 26466. You can also see that longweight.php has 26466 as its parent process ID; it was spawned by the execlongweight1.php script. And there’s the username and password in full view for anyone who’s looking…

Because my longweight.php script also accepts the entering of a username and password interactively, via STDIN, a better approach is to execute the script using popen() in write mode:

#!/usr/local/bin/php  
<?php  
$fp = popen('./longweight.php -l 30','w');  
 
fwrite($fp,"harryfn");  
fwrite($fp,"secretn");  
 
fclose($fp);  
 
exit(0);  
?>

Filename: execlongweight2.php

Now, when we execute the cps command, we see the following:

26512  1589 /usr/local/bin/php ./execlongweight2.php  
26513 26512 /usr/local/bin/php ./longweight.php -l 30

The username and password are no longer visible.

Unfortunately, the same won’t work with mysqldump, which reads user input from a different source than STDIN. The next-best solution is to place a file called .my.cnf, containing your MySQL username and password, in your Unix account home directory.

[client]  
user=harryf  
password=secret

Filename: ~/.my.cnf

The mysqldump utility will read the username and password automatically from .my.cnf, saving you from having to expose them on the command line.

Working with the Environment

No PHP script is an island, particularly when it’s used to execute other programs. A Unix shell provides its own "memory" in which you can store environment variables. A number of variables are predefined and some, such as the PATH variable, are essential to allow your PHP script to execute external programs "naively".

The excellent LINUX: Rute User’s Tutorial and Exposition provides a summary of common environment variables here.

When you execute a PHP script from the command line, it inherits the environment variables defined in your shell. That means you can set an environment variable using the export command like so:

$ export SOMEVAR='Hello World!'

Note we use single quotes — not double quotes!

Now, let’s execute the following PHP script:

#!/usr/local/bin/php   
<?php  
fwrite(STDOUT,"The value of SOMEVAR is ".getenv('SOMEVAR')."n");  
 
exit(0);  
?>

Filename: env1.php

You will see the value you assigned to SOMEVAR. The getenv() function allows you to read environment variables.

You can also create or modify environment variables from a PHP script using the putenv() function, but with one important caveat: you can’t modify the parent processes environment, only the local copy to which your script has access.

Continuing from the last example, let’s execute the following PHP script:

#!/usr/local/bin/php   
<?php  
fwrite(STDOUT,"The value of SOMEVAR is ".getenv('SOMEVAR')."n");  
 
putenv("SOMEVAR='Goodbye World!'");  
 
fwrite(STDOUT,"The value of SOMEVAR is ".getenv('SOMEVAR')."n");  
 
exit(0);  
?>

Filename: env2.php

On executing this code, we’ll see the modified value of SOMEVAR on the second call to fwrite(). But, if I type the following from my shell once the script finishes execution, I see the original value "Hello World!"

$ echo $SOMEVAR

As first glance you might think that makes putenv() fairly useless. Where it becomes important is when your PHP script needs to set up the environment for another program that it will be executing. Look at this example:

#!/usr/local/bin/php   
<?php  
putenv("SOMEVAR='Goodbye World!'");  
 
fwrite(STDOUT,"Executing env1.phpn");  
fwrite(STDOUT,shell_exec('./env1.php'));  
 
exit(0);  
?>

Filename: env3.php

Because the script env1.php inherits the environment from its parent process, env1.php, it sees the modified value of SOMEVAR and displays "Goodbye World!"

In other words, environment variables can be used to communicate information between programs. You might define an environment variable INSTALL_DIR, which points to a directory into which multiple scripts should install something. Some main script, perhaps install.php, sets this variable, then executes other scripts that perform further installation tasks, such as extracting source code archives into the installation directory, or creating API documentation with phpDocumentor.

It’s also important to be aware of environment variables when executing external programs from PHP scripts running under Apache. Typically, the scripts will run with the environment of a special Unix user ("nobody", "wwwrun" or similar), with very few filesystem privileges and perhaps a non-standard user environment.

This may mean that, for example, when you attempt to execute under Apache a program with which you’d normally find your own PATH, nothing visible happens. It may be that you just need to specify the full path to the program or update the PATH variable temporarily using putenv(). It could also be a permissions issue, which is a different story.

Note that if you control the server and need to execute external programs from PHP scripts running under Apache, it may be worth investigating sudo, which provides a mechanism through which one user can execute a single command with the same environment and permissions as another user (obviously, you need to be extremely cautious when doing this).

Process Control

Earlier, we saw the popen() function, which allows us either to read or write to an external program. Sometimes, you need to do both, which is where the proc_open() function comes in handy.
Here’s a simple program that reads input line by line from STDIN and converts the first letters of each word to upper-case, then writes the line back to STDOUT. Because it’s a somewhat temperamental program, it doesn’t like to get words in all upper-case. If it finds an upper-case word, it complains to STDERR and removes that word:

#!/usr/local/bin/php   
<?php  
while ( ($line = trim(fgets(STDIN))) != 'exit' ) {  
 $words = explode(' ',$line);  
 foreach ( $words as $index => $word ) {  
   if ( strcmp($word,STRTOUPPER($word)) != 0 ) {  
     $words[$index] = ucfirst($word);  
   } else {  
     fwrite(STDERR, "Bleugh! $word is all upper case!n");  
     unset($words[$index]);  
   }  
 }  
 $line = implode(' ',$words);  
 fwrite(STDOUT, $line."n");  
}  
exit(0);  
?>

Filename: ucfirst.php

Being able to talk to this program from another PHP script is obviously trickier. We need to connect to STDIN, STDOUT and STDERR at the same time. Enter: proc_open

#!/usr/local/bin/php   
<?php  
// Describes how proc_open should open the external program  
$Spec = array (  
 0 => array('pipe','r'), /* STDIN */  
 1 => array('pipe','w'), /* STDOUT */  
 2 => array('file','./badwords.log','a'), /* STDERR */  
);

First, I need to define a "descriptor spec", which is an array with a special structure. It tells proc_open() how to connect to the STDIN, STDOUT and STDERR of the external program. It should be an array of arrays, the parent array containing three elements that correspond to STDIN, STDOUT and STDERR respectively. Each of the child arrays specifies how each of these should be used, identifying a "resource type": either a "pipe" for shell IO, or a "file", and further parameters that describe how the pipe or file should be used. This may seem a little "clunky" at first, but you’ll get used to it.

Here, I’ve defined a descriptor spec that says, "make STDIN available to the external program for reading (the spec is seen from the perspective of the external program, even though this script will be writing to the pipe), make STDOUT available, so the external program can write output to it, then point STDERR to the file ‘badwords.log’ and append any error messages to it."

Let’s move on to the rest of the program, where the purpose of the "descriptor spec" should become clearer:

// Handles to external programs STDIN, STDOUT and STDERR placed here   
$handles = array();  
 
// Open the process using the descriptor spec  
$process = proc_open('./ucfirst.php', $Spec, $handles);  
 
// Some lines to input to ucfirst.php  
$lines = array (  
 'hello world!',  
 'a bad EXAMPLE of upper case.',  
 'goodbye world!',  
);  
 
foreach ( $lines as $line ) {  
 // Write the line to the STDIN of ucfirst.php  
 fwrite($handles[0],$line."n");  
 
 $response = fgets($handles[1]);  
 
 // Display the response to the user  
 fwrite(STDOUT, $response);  
}  
 
// Issue the exit command to the ucfirst.php program  
fwrite($handles[0],"exitn");  
 
// Clean up  
fclose($handles[0]);  
fclose($handles[1]);  
proc_close($process);  
 
exit(0);  
?>

Filename: proc_open.php

There are a few things to notice here. Executing proc_open() returns a resource that represents the process, but to interact with the process you need to read from or write to handles stored in the $handles array, the elements of the array corresponding to those in the descriptor spec array.

Note that you need to be particularly careful to close handles to pipes, as I did in the clean up section above. A smarter approach might be to register a shutdown function to take care of cleaning up — see register_shutdown_function().

Now, when we execute proc_open.php, the output looks like that shown below:

Hello World!   
A Bad Of Upper Case.  
Goodbye World!

The badwords.log file contains:

Bleugh! EXAMPLE is all upper case!

With PHP5, proc_open() supports a new descriptor, "pty", which will allow it to control programs that read from other locations than STDIN and STDOUT (this should solve the problem with sending password to mysqldump, for example).

PHP5 also introduces three more functions: proc_get_status(), which provides information about what the external program is currently doing (most importantly, providing its process ID), proc_terminate(), which allows you to send the process a termination signal (more on that below) and proc_nice(), which allows you change the priority the operating system assigns to the program (how much processor time it gets); normally this is a number between 20 (lowest priority) and -20 (highest priority). Note that proc_nice() is actually not related to proc_open(); it’s meant to control the priority of the current PHP script, not the priority of an external program. From the shell, the renice command can be used to the same effect.

Signals

The Posix functions provide various tools to get useful information from the operating system (note the Posix extension is not available on Windows). Perhaps most useful, from the point of view of working with external programs, is the posix_kill() function, which allows you to send signals to a program while it’s executing.

If you’ve used Unix more than a little, you’ve probably run into the kill command, perhaps in its most famous incarnation:

$ kill -9 <pid>

The "-9" says send the "SIGKILL" signal (exit immediately) to the process identified by its ID. The Rute Users Tutorial provides some common signals here (in fact, it’s worth reading the whole chapter on Processes and Environment Variables).

The posix_kill() function works in the same way as the kill command: you identify a process by its process ID and send it a signal. Normally, you should send signal number 15 (SIGTERM), which gives the program you’re killing a chance to clean up before it exits. The SIGKILL signal (9) cannot be intercepted by a program; it dies immediately, which may mean it leaves a mess behind.

For more information on signals, it’s also worth glancing at PHP’s Process Control Extension (disabled by default), which provides functions to allow your scripts to work with signals (among other things).

Warning: do not use the pcntl functions when running PHP under Apache! Weird things will happen if you do.

Here’s a script that "catches" the SIGTERM signal:

#!/usr/local/bin/php   
<?php  
// Required from PHP 4.3.0+: see manual  
declare(ticks = 1);  
 
function cleanUp() {  
 fwrite(STDOUT,"Performing clean up...n");  
 exit(0);  
}  
 
// Map the SIGTERM signal to the cleanup callback function  
pcntl_signal(SIGTERM, "cleanUp");  
 
// Illegal - you cant catch the KILL signal  
# pcntl_signal(SIGKILL, "cleanUp");  
 
while (1) {  
 // Loop forever  
}  
?>

Filename: pcntl1.php

I execute this from the command line like so:

$ ./pcntl1.php &

This instructs the shell to run the process in the background. Immediately after execution, it tells me the process ID under which my script is running, for example:

[6] 2840

The "2840" being the process ID. I now type the following:

$ kill 2840

And I see the below:

Performing clean up...

The process then exits (note the kill command defaults to desired SIGTERM signal, in case you were wondering).

You may find it useful to catch signals like SIGINT, which corresponds to a user pressing CTRL+C to quit your program; this would give you to opportunity to roll back anything your script has done.

More PCNTL Tricks

One further function of note is pcntl_exec(). Unlike any of the program execution functions we’ve seen so far, the use of pcntl_exec() to execute an external program causes the program to replace the script by which it was executed, instead of running as a child process.

For example, imagine I execute the following script:

#!/usr/local/bin/php   
<?php  
fwrite(STDOUT,"My process ID is ".posix_getpid()."n");  
pcntl_exec('./longweight.php',array('-l15','-uharryf','-psecret'));  
?>

Filename: pcntl2.php

I then execute the code below from a separate shell:

$ ps -ef | grep longweight.php

The process ID of the longweight.php is the same as the process ID reported from pcntl2.php. What’s more, if I execute the following, I see nothing:

$ ps -ef | grep pcntl2.php

The reason I see nothing it because the script was replaced by longweight.php.

This behaviour can be useful when writing "wrapper" scripts that set up an environment for another script, then quietly disappear.

Another area in which the PCNTL functions are useful is if you’re writing some kind of "daemon" process with PHP (a server). Believe it or not, there are a number of HTTP servers written entirely in PHP, such as Nanoweb and PEAR::HTTP_Server.

Using the pcntl_fork() function, you can write a script that acts as a server but creates child processes to handle incoming requests in much that same way as Apache Web server (1.x). This allows my server to cope with multiple tasks in parallel, reducing delays. Unfortunately, pxntl_fork() doesn’t lend itself to a short discussion, so I’ll leave it for you to investigate further, should you need it.

Over to You

You should now have a pretty good idea of how to get your command line PHP scripts to "tune in" to the wealth of tools and utilities available on Unix-based systems. There’s a lot to learn if you’re new to Unix, and in this short article I’ve only been able to introduce the topics. Hopefully, though, you have a feeling for how much power is at your disposal.

If you’re interested in finding out more, I highly recommend Paul Sheer’s Rute User’s Tutorial and Exposition, also available from Amazon.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.