Several hints on writing CLI php scripts


Despite PHP is used mainly for web applications, it's a very handy language for writing command line applications too. Especially if you're used to, it can replace both bash and Perl for you. To demonstrate all of this, let's create a sample backup script.

The requirements

The script's input is a list of directories that should be archived and destination of the archives. For convenience we'll allow two input formats - combination of command line arguments and standard input or configuration file.

Message output

First we'll create simple logger class that outputs information and error messages to the appropriate streams:

class SimpleLogger {

    function info($message) {
        fprintf(STDOUT, date('[Y-m-d H:i:s] ') . $message . "\n");
    }

    function error($message) {
        fprintf(STDERR, date('[Y-m-d H:i:s] ') . $message . "\n");
    }
}

The main functionality

class BackupCreator {
    private $dirs;
    private $backup_dir;
    private $test_run;
    private $logger;

    function BackupCreator($dirs, $backup_dir, $test_run=false) {
        $this->dirs = $dirs;
        $this->backup_dir = $backup_dir;
        $this->test_run = $test_run;
        $this->logger = new SimpleLogger();
    }

    function backup() {
        if (!is_dir($this->backup_dir)) {
            $this->logger->error("Directory {$this->backup_dir} does not exist or is not accessible!");
            return -1;
        }

        $backup_timestamp = date("YmdHis");
        foreach ($this->dirs as $dir) {
            if (!file_exists($dir)) {
                $this->logger->error("Backup entry {$dir} does not exist or is not accessible!");
                continue;           
            }

            $archive_name = $this->backup_dir . '/' .$backup_timestamp . str_replace('/', '_', $dir);
            $folder = dirname($dir);
            $item = basename($dir);
            chdir($folder);
            $cmd = "tar -zcf '$archive_name' '$item'";
            $this->execute($cmd);           
        }

        return 0;
    }

    function execute($cmd) {
        $this->logger->info("Executing $cmd");
    }
   
}

Execution of external commands

We'll use tar + gzip to compress our backups. So here's a wrapper function for executing external commands that provides verbose output and option for test run - just output the command without executing it to verify that nothing bad will happen.

    function execute($cmd) {
        $this->logger->info("Executing: $cmd");
        if ($this->test_run) {
            $this->logger->info("Test mode - command is not executed");
            return;
        }
        exec($cmd, $output, $return_value);
        $this->logger->info("Command completed with exit code $return_value");
        if ($output) {
            $this->logger->info("Command output is:\n" . implode("\n", $output));
        }
    }

Reading arguments from the command line and standard input

Now that the main functionality is ready we'll begin argument parsing. Here the function getopt comes in hand. Our script accepts the following options:
    -d <dir> - location of backup dir is <dir>, and backup items list is read from STDIN, one on each line
    -c <file> - use configuration file (this option overrides -d)
    -t - test run

Notice the columns after options expecting argument

function usage() {
    echo "Available options:
    -d <dir> - location of backup dir is <dir>, and backup items list is read from STDIN, one on each line
    -c <file> - use configuration file (this option overrides -d)
    -t - test run
";
    exit(-1);
}

$opts = getopt('d:c:t');
if (!$opts) {
    usage();
}

$dirs = array();
$backup_dir = '';
$test_run = isset($opts['t']);
if (isset($opts['c']) && $opts['c']) {
    //TODO
} else if (isset($opts['d']) && $opts['d']) {
    $backup_dir = $opts['d'];
    while ($item = fgets(STDIN)) {
        $dirs[] = trim($item);   
    }
} else {
    usage();
}

if (!$dirs || !$backup_dir) {
    usage();
}

$backup = new BackupCreator($dirs, $backup_dir, $test_run);
$backup->backup();

Now we can test our script - just run

    php backup.php -d /tmp -t

and write a couple of directories, each on separate line. Finish with CTRL+D. You'll see similar output:

[2010-02-02 01:42:19] Executing: tar -zcf '/tmp/20100202014219_home_foo_Desktop' 'Desktop'
[2010-02-02 01:42:19] Test mode - command is not executed
[2010-02-02 01:42:19] Backup entry /home/xxx does not exist or is not accessible!

Reading ini files

Now let's complete the last item: reading configuration from ini file. It has the following format:

backup_dir=/tmp

dirs[]=/home/foo/Desktop
dirs[]=/home/bar/Documents
dirs[]=/home/xxx

The function that we'll use is parse_ini_file

if (isset($opts['c']) && $opts['c']) {
    $config = parse_ini_file($opts['c'], true);
    if (!$config) {
        fprintf(STDERR, "Error while parsing config file {$opts['c']}");
        exit(-1);
    }
    $backup_dir = $config['backup_dir'];
    $dirs = $config['dirs'];
}

Two sample use cases

Backup all files/folders that start with 'documents' in foo's home folder and put backups in /tmp:

find /home/foo -name documents* | php backup.php -d /tmp

Create regular backups on foo and bar's public_html using crontab:
First create config file public_html_backups.ini, containing

backup_dir=/backups
dirs[]=/home/foo/public_html
dirs[]=/home/bar/public_html

Then put the following line in root's crontab:

1 2 * * * php backup.php -c public_html_backups.ini

Now adding another user's public_html is just a matter of edditing config file.

Download full script

 

No comments yet

Back to articles list

This page was last modified on 2018-12-09 01:23:07