PHP on the Command Line – Part 1 Article

Multiple Choice

You can also use STDIN to handle choices:

<?php
fwrite(STDOUT, "Pick some colors (enter the letter and press return)n");

// An array of choice to color
$colors = array (
'a'=>'Red',
'b'=>'Green',
'c'=>'Blue',
);

fwrite(STDOUT, "Enter 'q' to quitn");

// Display the choices
foreach ( $colors as $choice => $color ) {
fwrite(STDOUT, "t$choice: $colorn");
}

// Loop until they enter 'q' for Quit
do {
// A character from STDIN, ignoring whitespace characters
do {
$selection = fgetc(STDIN);
} while ( trim($selection) == '' );

if ( array_key_exists($selection,$colors) ) {
fwrite(STDOUT, "You picked {$colors[$selection]}n");
}

} while ( $selection != 'q' );

exit(0);
?>

Filename: pickacolor.php

The end user sees something like this:

Pick some colors (enter the letter and press return)
Enter 'q' to quit
a: Red
b: Green
c: Blue
b
You picked Green
c
You picked Blue
q

Using a loop, I can get my script to continue execution until the user enters ‘q’ to quit. Another approach would be a while loop that continues forever, but contains a break instruction when some condition is met. It may take you some time to get familiar with this, particularly if you’ve been using PHP in a Web application in which execution normally halts automatically after 30 seconds, and user input is provided in a single bundle at the start of a request. The main thing is to pay close attention to your code when you insert “never ending” loops like this, so you’re not inadvertently executing a database query over and over, for example.

Now, let’s get back to the example above. Have a look at what’s happening here:

// A character from STDIN, ignoring whitespace characters
do {
$selection = fgetc(STDIN);
} while ( trim($selection) == '' );

The fgetc() function pulls a single character of the STDIN stream that’s coming from the user. When execution reaches this point, there’s a pause while the terminal waits for the user to enter a character and press return. Both the character I enter, such as the letter ‘a’, and the new line character are placed on the STDIN stream. Both will eventually be read by fgetc(), but on consecutive loop iterations. As a result, I need to ignore the new line characters which, in this case, I have done using the trim() function, comparing what it returns to an empty string.

If that doesn’t make sense, try commenting out the do { and } while ( trim($selection) == '' ); lines in this inner loop, before and after fgetc(). Then, see what happens when you run the script.

Also, right now, users of this script can enter multiple characters between presses of the return key. See if you can work out a way to prevent that by confirming that return was pressed between the user’s choices.

Errors and Pipes

Earlier on, I mentioned that you could write errors to a different output location defined by the STDERR constant in PHP. This is achieved in the same way as writing to STDOUT:

<?php
// A constant to be used as an error return status
define ('DB_CONNECTION_FAILED',1);

// Try connecting to MySQL
if ( !@mysql_connect('localhost','user','pass') ) {
// Write to STDERR
fwrite(STDERR,mysql_error()."n");
exit(DB_CONNECTION_FAILED);
}

fwrite(STDOUT,"Connected to databasen");
exit(0);
?>

Filename: failedconnection.php

Any connection errors will now be reported using STDERR. What this script doesn’t do is demonstrate why this functionality can be useful. For that, consider the following:

<?php
// A custom error handler
function CliErrorHandler($errno, $errstr, $errfile, $errline) {
fwrite(STDERR,"$errstr in $errfile on $errlinen");
}
// Tell PHP to use the error handler
set_error_handler('CliErrorHandler');

fwrite(STDOUT,"Opening file foobar.logn");

// File does not exist - error is generated
if ( $fp = fopen('foobar.log','r') ) {
// do something with the file here
fclose($fp);
}

fwrite(STDOUT,"Job finishedn");
exit(0);
?>

Filename: outwitherrors.php

Now, when you execute this script normally (assuming the file foobar.log doesn’t exist in the same directory as the script), it produces output like this:

Opening file foobar.log
fopen(foobar.log): failed to open stream: No such file or directory in /home/harryf/outwitherrors.php on 11
Job finished

The error messages are mixed with the normal output as before. But by piping the output from the script, I can split the errors from the normal output:

$ php outwitherrors.php 2> errors.log

This time, you’ll only see these messages:

Opening file foobar.log
Job finished

But, if you look into the directory in which you ran the script, a new file called errors.log will have been created, containing the error message. The number 2 is the command line handle used to identify STDERR. Note that 1 is handle for STDOUT, while 0 is the handle for STDERR. Using the > symbol from the command line, you can direct output to a particular location.

Although this may not seem very exciting, it’s a very handy tool for system administration. A simple application, running some script from cron, would be the following:

$ php outwitherrors.php >> transaction.log 2>> errors.log

Using ‘>>‘, I tell the terminal to append new messages to the existing log (rather than overwrite it). The normal operational messages are now logged to transaction.log, which I might peruse once a month, just to check that everything’s OK. Meanwhile, any errors that need a quicker response end up in errors.log, which some other cron job might email me on a daily basis (or more frequently) as required.

There’s one difference between the UNIX and Windows command lines, when it comes to piping output, of which you should be aware. On UNIX, you can merge the STDOUT and STDERR streams to a single destination, for example:

$ php outwitherrors.php > everything.log 2>&1

What this does is re-route the STDERR to STDOUT, meaning that both get written to the log file everything.log.

Unfortunately, Windows doesn’t support this capability, although you can find tools (like StdErr that can help you achieve the same ends.

In general, the subject of piping IO is one that I can’t do full justice to. On UNIX, it’s almost a (black?) art, and usually comes into play when you start using other command line tools like sed, awk and xargs (O’Reilly have dedicated entire books to the subject, including the Loris Book). What you should be getting a feeling for, however, is that, by conforming to the standard conventions for shell scripting, you have the chance to tap into a powerful system administration “framework” that provides all sorts of other tools.

Coping with Arguments

You’ve already seen some examples of how to read user input from STDIN, once a script has already begun execution. What about being able to pass information to the script at the point it is executed?

If you’ve ever programmed in a language like C or Java, you’ll no doubt be familiar with variables called “argc” and “argv”. With PHP, the same naming convention is used, with the integer variable $argc containing the number of arguments passed to the script, while $argv contains an indexed array of the arguments, the “delimiter” between each argument being a space character.

To see how they work, try the following script:

<?php
// Correct English grammar...
$argc > 1 ? $plural = 's' : $plural = '';

// Display the number of arguments received
fwrite(STDOUT,"Got $argc argument$pluraln");

// Write out the contents of the $argv array
foreach ( $argv as $key => $value ) {
fwrite(STDOUT,"$key => $valuen");
}
?>

Filename: arguments.php

I execute this script as normal:

$ php arguments.php

The result is:

Got 1 argument
0 => arguments.php

The first and only argument is the name of the script being executed.

Now, if I execute the following:

$ php arguments.php Hello World!

The code returns:

Got 3 arguments
0 => arguments.php
1 => Hello
2 => World!

The strings “Hello” and “World!” are treated as two separate arguments.

If, instead, I use quotes:

$ php arguments.php "Hello World!"

The output is as follows:

Got 2 arguments
0 => arguments.php
1 => Hello World!

As you can see, passing basic arguments to a PHP script in this manner is very easy. Of course, there are always a few “gotchas” to watch out for.

The variables $argc and $argv are made available in PHP’s global scope to those using the PHP 4.3.0+ version of the CLI binary. They are not available outside the global scope (such as inside a function). When you use a CGI binary with register_globals off, $argc and $argv will not be available at all. Instead, it’s best to access them via the $_SERVER array you’re well used to, using $_SERVER['argv'] and $_SERVER['argc']. They will be available here irrespective of SAPI and register_globals, so long as (wait for it…) the 'register_argc_argv' ini setting is switched on in php.ini (which, thankfully, it has been, by default, since PHP 4.0.0).

The other main “gotcha” is that first argument you saw above — the name of the script itself. This happened because I named, in the command line, the PHP binary to execute the script, so arguments are calculated relative to the PHP binary, not to the script it’s executing. Meanwhile, as you saw above, it’s possible to execute a script directly by identifying the binary with the “SheBang” on UNIX, or by file association on Windows. Doing so will mean the script’s name no longer appears in the list of arguments. In other words, you need to be careful to check what the first argument contains and whether it matches the contents of $_SERVER['SCRIPT_NAME']. Of course, life would be too easy if $_SERVER['SCRIPT_NAME'] was always available, which it generally won’t be if you’re using the CGI binary. A more reliable mechanism is to compare the contents of the __FILE__ constant with the contents of $_SERVER[argv][0]:

<?php
if ( realpath($_SERVER['argv'][0]) == __FILE__ ) {
fwrite(STDOUT,'You executed "$ php whereami.php"');
} else {
fwrite(STDOUT,'You executed "$ whereami.php"');
}
?>

Filename: whereami.php

If you’ve had any experience executing commands using a UNIX shell, you’ll have no doubt run into the use of command line options such as:

$ ls -lt

This lists the contents of a directory in “long” format, the files being ordered by modification time. Another example you may know is as follows:

$ mysql --user=harryf --password=secret

The above is used to log in to MySQL.

These are known as command line options, the first example having the “short option” syntax, while the second uses the “long option” syntax.

If I try passing arguments like this to my arguments.php script above:

$ php arguments.php --user=harry --password=secret

I get:

Got 3 arguments
0 => arguments.php
1 => --user=harry
2 => --password=secret

The $argv array is ignorant to the options syntax, restricting itself to breaking up arguments using the space character between them. It’s up to you to parse the contents of each argument to handle “options” like this within your script.

Go to page: 1 | 2 | 3

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.