Following Darrell Brogdon's previous article on using PHP as a shell scripting language, this article covers some advanced uses of the language and shows some tricks that make PHP an extremely useful language to work with.
Unlike Darrel's article however, this one will also cover the Windows version of PHP as well as the Linux/Unix one. First, however, there are a couple of differences you need to be aware of.
So what are the differences?
The differences between the two environments are mostly due to operating system differences as opposed to differences in PHP itself. The big one has to be case sensitivity in file names; this has nailed me on many an occasions. A very good habit to get into to avoid this, is to always use lowercase filenames no matter what. How you do it is up to you, but lowercase filenames have always worked for me. The other thing you need to be very aware of are the differences in the file system.
Windows uses a system of drive letters and separate disks, where as most posix based systems (this is the technical name for Linux and Unix variants) treat all file systems and disks as one large directory tree. If you're only working in one folder this should not present a problem, but if you’re working with globs and other file system functions, you need to be aware of this.
The 3rd most important thing is one of binary versus text when writing files. Under Linux/Unix, all files are handled identically and that is as a byte stream, whereas under Windows the situation is a little different. Windows treats text files as a separate type than binary files. Thankfully in the latest builds of PHP this is no longer an issue to be concerned with as the fopen calls now default to binary mode.
The last thing to be aware of is line endings. Under Windows these take the form \r\n as opposed to just \n on Linux/Unix.
So to sum up, watch out for position in the file system, check your filenames and be cautious of line endings.
Why are these differences important in a Shell environment?
First and foremost, the case situation will manifest itself when running scripts. A script named 'MyScript.php' will happily run from the command prompt using 'PHP -q myscript.php' on Windows, but will fail miserably on Linux. File system position will show up when using glob functions, and other directory scanning functions. Lastly the line ending issue is most likely to surprise you if your parsing files, text streams or the output from commands. From this point on, I’ll not mention these points again, but if something is not working as you expect, then keep them in mind.
So what neat things can we do with PHP in the shell?
Well, the answer to that is pretty much anything. I use PHP for a huge amount of different tasks, from dumping files to chucking together quick test models or proof of concept ideas. Over the years however, one of the great things I’ve found with PHP is its ability to hack together quick pipe filters.
For those of you who are not up to speed on pipe filters, a quick explanation is in order. When the predecessors of today’s modern OS's where developed back in the 60's the idea of one tool, many uses was a very common one. Shell programming spawned a whole generation of people who spent insane amounts of time writing ridiculously long command lines, to perform some quite long tasks. It’s for this very reason that today most OS's (Windows included) have a very rich set of separate programs that each do a small but efficient task.
Consider this example (running under Linux):
<code>ls -al | awk '{print $8,$5,$6}'</code>

This will take a standard long linux file listing, and re-order it so you get the file name, number of bytes and date of creation:

<code>
test 4096 2006-11-20
test1.php 0 2007-08-05
test.txt.gz 480 2006-04-09
tomcat 4096 2007-02-27
ukcode.php?number=07971899759 455 2006-12-04
unser.php 205 2007-01-14
</code>
If you install the Gawk package from the win32 GNU utilities page at http://gnuwin32.sourceforge.net/ then you can also do the following:
<code>dir | gawk "{print $4,$3,$1}"</code>
Which will give:
<code>
netset.txt 1,037 29/11/2007
ntent_a.xml 6,440 16/09/2008
ntent_ie.xml 1,654 16/09/2008
ntent_m.xml 5,862 16/09/2008
ntent_y.xml 5,816 16/09/2008
ntuser.dat 15,728,640 29/01/2009
persistent_state 16 04/08/2008
phone 47 22/05/2008
</code>
As you can see the outputs are very similar, but not identical, however this is not an article about shell programming in general, it's about putting PHP to some advanced uses. You can see from the examples above that commands are joined with the vertical bar character '|' as this is the pipe character, and to cut this short basically means take the output of one command and feed to the next via the standard input stream.
This means if we read the standard input stream using the php file stream 'php://stdin' then we can use PHP to construct blocks of code that can participate in the filter chain.
Enough talking already, give us some code!!!!
Ok ok, I hear you. So by way of an example to show what I’m on about, we'll put together a small example under Linux that shows what those file permission flags in a file listing mean.
First, if we do an 'ls -al' we should see something like this:
<code>
drwx------  2 shawty users      4096 2006-04-10 23:51 .ssh
-rw-r--r--  1 shawty users       193 2007-10-25 10:43 staaus.cap
-rw-r--r--  1 shawty users     11627 2007-10-27 14:05 staceymail2
-rw-r--r--  1 shawty users     18737 2007-10-28 00:24 staceymail3
-rw-r--r--  1 shawty users     34040 2007-10-28 16:10 staceymail4
-rw-r--r--  1 shawty users    182891 2007-10-26 09:28 stacymail
-rw-r--r--  1 shawty shawty     3642 2008-10-29 17:02 startup
-rw-r--r--  1 shawty shawty     4124 2008-10-29 17:03 startup-org
-rw-r--r--  1 shawty users        55 2006-06-03 01:51 suck.newrc
-rw-r--r--  1 shawty users        50 2006-06-03 01:47 sucknewsrc
-rw-r--r--  1 shawty users         0 2008-05-15 17:20 .sudo_as_admin_successful
-rw-r--r--  1 shawty users     23067 2006-10-12 22:34 telnet.pl
-rw-r--r--  1 shawty users   9231834 2006-04-18 14:21 termine
</code>
The first thing we need to do here is to read this into a PHP script.
<code>
<?php

  // Open PHP's standard input stream
  $input_stream = fopen("php://stdin","r");

  // Array to store the received data
  $lines = array();

  // Loop & process while we receive lines
  while($line = fgets($input_stream,4096)) // Note 4k lines, should be ok for most purposes
  {
    //Store each line in the array, ensuring we chop off the line ends
    $lines[] = trim($line);
  }

  fclose($input_stream);

  print_r($lines);

?>
</code>
    
If you run this from a Linux command line using 'ls -al | php -q fancydir.php' (replace php file name as required), you'll get something like the following:
<code>
shawty@poweredge:~/Articles_PHP$ ls -al ../ | php -q fancydir.php
Array
(
    [0] => total 54676
    [1] => drwxr-xr-x 46 shawty users      4096 2009-01-29 20:01 .
    [2] => drwxr-xr-x 14 root   root       4096 2009-01-13 17:17 ..
    [3] => -rw-r--r--  1 shawty users         1 2006-11-15 02:24 2
    [4] => drwxr-xr-x 10 shawty users      4096 2008-05-14 17:11 20gdrive
    [5] => drwxr-x---  6 shawty users      4096 2007-04-30 16:14 4grec1
    [6] => drwx------  2 shawty users      4096 2006-05-14 17:31 a.b.p.v.files
    [7] => -rw-r--r--  1 shawty users   1863991 2006-03-06 14:24 abs-guide.pdf
    [8] => -rw-r--r--  1 shawty users         0 2006-05-23 22:42 .addressbook
    [9] => -rw-------  1 shawty users      2285 2006-05-23 22:42 .addressbook.lu
    [10] => drwx------  2 shawty users      4096 2008-05-15 17:42 .aptitude
    [11] => drwxr-xr-x  2 shawty shawty     4096 2009-01-29 20:01 Articles_PHP
    [12] => drwxr-xr-x  3 shawty users      4096 2006-06-11 01:34 baks
    [13] => -rw-------  1 shawty users     15190 2009-01-29 12:56 .bash_history
    [14] => -rw-r--r--  1 shawty users       220 2008-05-15 16:41 .bash_logout
    [15] => -rw-r--r--  1 shawty users      2298 2008-05-15 16:41 .bashrc
    [16] => drwxr-xr-x  2 shawty users      4096 2006-08-07 14:39 belkinbt
    [17] => drwx------  2 shawty users      4096 2006-08-22 21:16 .cedit
  ==SNIP==
</code>
    
As you can see, the directory listing is in an array, line by line as sent to the script. We can now use this array to go over each line and redisplay it, something like this:
<code>
$count = 1;
foreach($lines as $line)
{
  print $count . " : " . $line . "\n";
  $count++;
}
</code>
    
This would give you a number listing something like the following:
<code>
1 : drwxr-xr-x 46 shawty users      4096 2009-01-29 20:01 .
2 : drwxr-xr-x 14 root   root       4096 2009-01-13 17:17 ..
3 : -rw-r--r--  1 shawty users         1 2006-11-15 02:24 2
4 : drwxr-xr-x 10 shawty users      4096 2008-05-14 17:11 20gdrive
5 : drwxr-x---  6 shawty users      4096 2007-04-30 16:14 4grec1
6 : drwx------  2 shawty users      4096 2006-05-14 17:31 a.b.p.v.files
7 : -rw-r--r--  1 shawty users   1863991 2006-03-06 14:24 abs-guide.pdf
8 : -rw-r--r--  1 shawty users         0 2006-05-23 22:42 .addressbook
</code>
    
As part of this article, I’ve provided a slightly more lengthy example that allows you to use the pipe techniques above to produce a list like this:
<code>
Array
(
    [0] => -rwxr-xr-x
    [1] => 1
    [2] => shawty
    [3] => users
    [4] => 2804
    [5] => 2006-04-01
    [6] => 23:18
    [7] => .xsession
)
File Name is .xsession
Size is 2804 Bytes
It belongs to 'shawty' in the 'users' Group
and was created on 2006-04-01 at 23:18
File is Readable,Writable,Executable by shawty
File is Readable,Not Writable,Executable by members of the shawty group
File is Readable,Not Writable,Executable by everyone

Array
(
    [0] => -rw-r--r--
    [1] => 1
    [2] => shawty
    [3] => users
    [4] => 119
    [5] => 2006-04-01
    [6] => 23:18
    [7] => .xtalkrc
)
File Name is .xtalkrc
Size is 119 Bytes
It belongs to 'shawty' in the 'users' Group
and was created on 2006-04-01 at 23:18
File is Readable,Writable,Not Executable by shawty
File is Readable,Not Writable,Not Executable by members of the shawty group
File is Readable,Not Writable,Not Executable by everyone

etc.....

</code>
    
What about Windows?
I said I’d mention Windows too, and just like it's Linux counterpart, the Windows command line can use pipes too, and in the same manner, and as I mentioned before if you go to the Gnu WIn32 project you can combine your scripts with all the usual 'awk','grep','sort' and other command lines you're used to.
However...that’s just the start, under Windows we are fortunate to have 2 sets of tools at our disposal than can find out some clever information. These tools come in the form of 'Windows Power Shell' and 'PS-Tools'.
These tools can query and retrieve all manner of system information, usually not just from a local machine but from a remote one also. As an example, we'll consider the output from 'PSinfo' , if you type PSinfo and press return you'll be presented with the following:
C:\Documents and Settings\Shawty>psinfo

PsInfo v1.75 - Local and remote system information viewer
Copyright (C) 2001-2007 Mark Russinovich
Sysinternals - www.sysinternals.com

<code>
System information for \\*******:
Uptime:                    0 days 10 hours 9 minutes 31 seconds
Kernel version:            Microsoft Windows XP, Multiprocessor Free
Product type:              Professional
Product version:           5.1
Service pack:              2
Kernel build number:       2600
Registered organization:   ********************
Registered owner:          **********
Install date:              29/11/2007, 16:05:53
Activation status:         ********************
IE version:                7.0000
System root:               C:\WINDOWS
Processors:                2
Processor speed:           2.4 GHz
Processor type:            Intel(R) Core(TM)2 Duo CPU     E4600  @
Physical memory:           3582 MB
Video driver:              NVIDIA GeForce 8500 GT
</code>
I’ve removed some of the key details for reasons of security, but you can see that it provides a basic overview of your PC. This output could be piped into a PHP filter, saved into a database, and then printed to the console or any other destination, maybe even another pipe. Within the PStools suite, we have the possibility to display Running process lists, Running services lists, Event log entry’s and many many more variables.
On top of that, once we start to use powershell, the possibilities become enormous.
A final Idea....
So you can use PHP to construct some very advanced pipe filters, but is that all you can do? Not by any stretch of the imagination. Let's suppose we now start thinking about using PHP's built in Exec functions for running processes. We could then use the file handling commands to read in a list of servers, then exec a given set of commands on those servers and collect the output. This output could then be piped out to another PHP script, and sent to a database, or turned into an XML feed. I’ll leave a final solution to this for you the reader to work out, but as a starting point, you could use something like this:
servers.txt
<code>
server1
server2
server3
</code>

list_installs.php
<code>
<?php

  $biff = array();

  $serverlist = file("servers.txt");
  foreach($serverlist as $server)
  {
  	unset($output);
    exec('psinfo \\\\' . trim($server),$output);
    foreach($output as $line)
    {
      if(preg_match('/Install\sdate:\s+(.*),\s(.*)/',$line,$matches))
      {
        $instdate = $matches[1];
        $insttime = $matches[2];
      }
    }
    print trim($server) . "," . $instdate . "," . $insttime . "\n";
  }
?>
</code>

output_xml.php
<code>
<?php
  $input_stream = fopen("php://stdin","r");
  $lines = array();
  while($line = fgets($input_stream,4096))
  {
    $lines[] = trim($line);
  }
  fclose($input_stream);

  print "<server_installs>\n";
  foreach($lines as $line)
  {
    $temp = explode(",",$line);
    print "\t<server name=\"". $temp[0] . "\">\n";
    print "\t\t<installdate>" . $temp[1] . "</installdate>\n";
    print "\t\t<installtime>" . $temp[2] . "</installtime>\n";
    print "\t</server>\n";
  }
  print "</server_installs>\n";
?>
</code>
Make sure all 3 files are in the same folder, and that psinfo is in your path; also remember to alter servers.txt to a list of your own Windows servers then run:
<code>
php -q list_installs.php | php -q output_xml.php
</code>
From the windows command prompt, the result should be something similar to the following:
<code>

<server_installs>
        <server name="server1">
                <installdate>20/09/2008</installdate>
                <installtime>22:12:26</installtime>
        </server>
        <server name="server2">
                <installdate>29/11/2007</installdate>
                <installtime>16:05:53</installtime>
        </server>
</server_installs>

</code>
In Summary
With a little imagination, and armed with a few techniques, PHP can be a powerful ally, you can connect things to other things and the only limits are your PHP ability’s and your imagination. Note also, everything we've discussed here will work across SSH links, telnet links, secure tunnels. With piped commands such as 'Netcat' you can pipe the output from your routers and switches, you can list and report on remote FTP directories if you use these techniques with wget and lynx, and with a little patience you can make scripts that run un-modified on both Windows and most Linux/Unix variants.
I hope you've learned some tricks that have made your life easier; I know I have over the years. You'll very quickly find that you build up a rather large library of small chunks of PHP code, all of which can be chained together in some way, like a giant digital Lego set.
Enjoy.
P.S. You can download a zip file below containing all the files we have discussed above.