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.
Frequently Asked Questions (FAQs) about PHP Command Line
How can I check the PHP version on the command line?
To check the PHP version on the command line, you need to type the command php -v
. This command will display the PHP version installed on your system, along with other details like the build system, date, and modules. It’s a quick and easy way to verify your PHP installation and its version.
What are the different options available with PHP command line?
PHP command line provides a variety of options for different purposes. Some of the commonly used options include -f
to parse and execute a file, -i
to display PHP information, -l
to perform syntax check of the given PHP code, -r
to run PHP code in command line code, and -v
to display the PHP version.
How can I execute a PHP script from the command line?
To execute a PHP script from the command line, you can use the php
command followed by the name of the script. For example, if you have a script named script.php
, you can run it by typing php script.php
in the command line. This will execute the script and display any output it produces.
How can I install PHP for command line use on Windows?
To install PHP for command line use on Windows, you first need to download the PHP Windows installer from the official PHP website. Once downloaded, run the installer and follow the on-screen instructions. During the installation process, make sure to select the option to add PHP to the system path. This will allow you to use PHP from the command line.
How can I find out the PHP version on Windows command line?
To find out the PHP version on Windows command line, you can use the php -v
command. This command will display the PHP version installed on your system. If PHP is not recognized as a command, it means that PHP is not installed or not added to the system path.
How can I run PHP code without a script file from the command line?
You can run PHP code directly from the command line without a script file using the -r
option. For example, php -r 'echo phpinfo();'
will execute the phpinfo()
function and display the output in the command line.
How can I perform a syntax check of PHP code from the command line?
To perform a syntax check of PHP code from the command line, you can use the -l
option. For example, php -l script.php
will check the syntax of the script.php
file and report any errors found.
How can I display PHP configuration information from the command line?
To display PHP configuration information from the command line, you can use the -i
option. For example, php -i
will display detailed information about your PHP installation, including configuration settings, loaded modules, environment variables, and more.
How can I parse and execute a PHP file from the command line?
To parse and execute a PHP file from the command line, you can use the -f
option. For example, php -f script.php
will parse and execute the script.php
file.
How can I use PHP interactive shell from the command line?
To use PHP interactive shell from the command line, you can use the -a
option. For example, php -a
will start the PHP interactive shell, allowing you to type and execute PHP code directly from the command line.
Harry Fuecks is the Engineering Project Lead at Tamedia and formerly the Head of Engineering at Squirro. He is a data-driven facilitator, leader, coach and specializes in line management, hiring software engineers, analytics, mobile, and marketing. Harry also enjoys writing and you can read his articles on SitePoint and Medium.