Commit 36f88033 authored by Andrew Robert Nicols's avatar Andrew Robert Nicols
Browse files

Add CLI Library (Bug #844607)



This commit adds a Command Line Interface to make writing CLI scripts
easier. It can be called in a basic fashion, or an extended fashion which
automatically generates help and applies verbosity, help, and argument
validation.

Change-Id: I9f13de74ba29e072e64e859f065bb6d754d6393b
Signed-off-by: default avatarAndrew Robert Nicols <andrew.nicols@luns.net.uk>
parent 056044c0
<?php
/**
* Mahara: Electronic portfolio, weblog, resume builder and social networking
* Copyright (C) 2011 Andrew Nicols <andrew.nicols@luns.net.uk>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package mahara
* @subpackage lib
* @author Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL
*/
/**
* Command Line Interface Class for mahara
*
* Two methods of use are currently supported:
* * basic; and
* * extended.
*
* In basic use, the CLI can be used to retrieve parameters passed on the
* command line. For example, when called as:
*
* php htdocs/admin/cli/example.php --argument=value --argument2 -x -q=n pony
*
* the library could be used to determine the argument options as follows:
*
* <?php
*
* define('CLI', true);
* define('INTERNAL', true);
* include(dirname(dirname(__FILE__)) . '/init.php');
*
* $cli = get_cli();
* $cli->set_cli_shortoptions(array('x' => 'argumentx', 'q' => 'question'));
* $argument = $cli->get_cli_param('argument');
* $argument = $cli->get_cli_param('argument2');
* $argument = $cli->get_cli_param('argumentx');
* $argument = $cli->get_cli_param('question');
* $argument = $cli->get_cli_unmatched();
*
*
* In the extended version, a greater degree of setup is required, but a
* number of other benefits are available as a result, including:
* * help and usage generation;
* * built-in verbosity support; and
* * built-in argument validation.
*
* The following sample code demonstrates how to use the extended version:
* <?php
*
* define('CLI', true);
* define('INTERNAL', true);
* include(dirname(dirname(__FILE__)) . '/init.php');
*
* $cli = get_cli();
*
* $options = array();
* $options['argument'] = new stdClass();
* $options['argument']->exampleValue = 'value';
* $options['argument']->description = 'This is an example description for argument';
*
* $options['argument2'] = new stdClass();
* $options['argument2']->description = 'This is an example description for argument2 - it takes no value';
*
* $options['argumentx'] = new stdClass();
* $options['argumentx']->description = 'This is an example description for argumentx - it takes no value and has an alias';
* $options['argumentx']->shortoptions = array('x');
*
* $options['question'] = new stdClass();
* $options['question']->exampleValue = 'value';
* $options['question']->description = 'This is an example description for question - it typicaly takes an argument and has an alias of q';
* $options['question']->shortoptions = array('q');
*
* $settings = new stdClass();
* $settings->options = $options;
* $settings->allowunmatched = true;
* $settings->info = 'Some information about what this script does';
*
* $cli->setup($settings);
*/
class cli {
/**
* Store the short option mapping information
*/
private $shortoptions = array();
/**
* Store default option values in a readily available format
*/
private $defaultvalues = array();
/**
* Store the arguments given on the CLI
*/
private $arguments = null;
/**
* Store any unmatched entries not recognised as valid arguments
*/
private $unmatched = null;
/**
* By default, allow unmatched text in the data stream to allow for
* simple use. This will be turned off by anyone calling setup()
*/
private $allowunmatched = true;
/**
* Store the settings passed in
*/
private $settings = null;
/**
* Set up the CLI interface correctly
*
* @param object settings The settings to work with
* @return void
*/
public function setup($settings) {
// Handle various options
$this->allowunmatched = (isset($settings->allowunmatched)) ? $settings->allowunmatched : false;
// Add verbosity and help options
$help = new stdClass();
$help->shortoptions = array('h');
$help->description = 'Display this help and usage information';
$settings->options['help'] = $help;
$verbose = new stdClass();
$verbose->shortoptions = array('v');
$verbose->description = 'Increase verbosity of the CLI script to show more information';
$settings->options['verbose'] = $verbose;
// Process longoption configuraiton
foreach ($settings->options as $name => $optionsettings) {
// Store the default value
$this->defaultvalues[$name] = (isset($optionsettings->defaultvalue)) ? $optionsettings->defaultvalue : false;
// By default this value isn't required
$optionsettings->required = (isset($optionsettings->required)) ? $optionsettings->required : false;
// Set the default description
if (!isset($optionsettings->description)) {
$optionsettings->description = '';
}
// Check all short options
if (isset($optionsettings->shortoptions)) {
foreach ($optionsettings->shortoptions AS $k => $shortoption) {
$this->shortoptions[$shortoption] = $name;
}
}
else {
$optionsettings->shortoptions = array();
}
}
// Store all settings for any access required later
$this->settings = $settings;
// Validate the options given
$this->validate_options();
// Process default arguments
$this->process_default_arguments();
}
/**
* Process the default arguments supplied if this script was called by
* using the extended method.
*
* If the verbose option is called, verbosity is increased to screen
* for all targets - dbg, info, warn, and environ.
* If the help option is called, then the help and usage information is
* printed using {@see cli_print_help}.
* @return void
*/
private function process_default_arguments() {
global $CFG;
// Check for verbosity
$verbose = $this->get_cli_param('verbose');
if ($verbose) {
$CFG->log_dbg_targets = LOG_TARGET_SCREEN | LOG_TARGET_ERRORLOG;
$CFG->log_info_targets = LOG_TARGET_SCREEN | LOG_TARGET_ERRORLOG;
$CFG->log_warn_targets = LOG_TARGET_SCREEN | LOG_TARGET_ERRORLOG;
$CFG->log_environ_targets = LOG_TARGET_SCREEN | LOG_TARGET_ERRORLOG;
}
// Check for usage/help request
$help = $this->get_cli_param('help');
if ($help) {
$this->cli_print_help();
}
}
/**
* Set the Short Option to Long Option mapping for basic usage
*
* @param array $shortoptions An associative array mapping the short
* option to the long option name
* @return void
*/
public function set_cli_shortoptions($shortoption) {
$this->shortoptions = $shortoption;
}
/**
* Compile the list of CLI arguments
*
* The following options are valid:
*
* --foo=bar
* -foo=bar
* --example-flag-without-content
* -example-flag-without-content
*
* It is not possible to have any whitespace between the = and any values
*
* Any other values are ignored and no warnings are issued
*
* @return array of specified variables
*/
public function _get_cli_params() {
global $argv;
if ($this->arguments && $this->unmatched) {
return array($this->arguments, $this->unmatched);
}
// We want to manipulate the arguments. Doing so on $argv would be
// pretty rude
$options = $argv;
// Remove the script name
unset ($options[0]);
// Trim off anything after a -- with no arguments
if (($key = array_search('--', $options)) !== false) {
$options = array_slice($options, 0, $key);
}
$this->arguments = array();
$this->unmatched = array();
foreach ($options as $argument) {
// Attempt to match arguments
preg_match('/^(-(-)?)([^=]*)(=(.*))?$/', $argument, $matches);
if (count($matches) && !empty($matches[3])) {
$argname = $matches[3];
if ($matches[1] == '-' && isset($this->shortoptions[$argname])) {
$argname = $this->shortoptions[$argname];
}
$argdata = isset($matches[5]) ? $matches[5] : true;
$this->arguments[$argname] = $argdata;
}
else {
// The argument didn't match a known setting so store it in
// case this was expected
$this->unmatched[] = $argument;
}
}
return array($this->arguments, $this->unmatched);
}
/**
* Retrieve the specified CLI argument
*
* @param string $name The name of the argument to retrieve
* @param mixed $default The default value for the parameter
* @return mixed the value of that parameter, or true if the value has no
* paramter but is set
*/
public function _get_cli_param($name) {
list($cliparams) = $this->_get_cli_params();
if (isset($cliparams[$name])) {
$value = $cliparams[$name];
}
else if (isset($this->defaultvalues[$name])) {
return array($this->defaultvalues[$name], true);
}
else if (func_num_args() == 2) {
$php_work_around = func_get_arg(1);
return array($php_work_around, true);
}
else {
throw new ParameterException("Missing parameter '$name' and no default supplied");
}
return array($value, false);
}
/**
* Retrieve the value of the command line argument for the specified
* setting.
*
* @param string $name The name of the argument to retrieve
* @param mixed $default The default value to use if the argument was not
* specified
* @return mixed
*/
public function get_cli_param($name) {
$args = func_get_args();
list ($value) = call_user_func_array(array($this, '_get_cli_param'), $args);
return $value;
}
/**
* Retrieve all data supplied on the command line which was not
* specified as an argument
*
* @return array All arguments specified, split on whitespace
*/
public function get_cli_unmatched() {
list($cliparams, $unmatched) = $this->_get_cli_params();
return $unmatched;
}
/**
* Validate all arguments supplied on the command line
*
* @return void
*/
function validate_options() {
$this->_get_cli_params();
// Check for unmatched data when allowunmatched is not set
if (count($this->unmatched) && !$this->allowunmatched) {
$this->cli_print_help(true);
}
// Check for invalid arguments
foreach ($this->arguments as $argument => $value) {
if (!isset($this->settings->options[$argument])) {
log_info('An invalid argument was specified: ' . $argument);
$this->cli_print_help(true);
}
}
// Check for missing arguments
foreach ($this->settings->options as $argument => $settings) {
if ($settings->required && !isset($this->arguments[$argument])) {
if (isset($settings->required_callback)) {
call_user_func($settings->required_callback);
$this->cli_print();
$this->cli_print_help(true);
}
else {
$this->cli_print('Missing option ' . $argument);
$this->cli_print();
$this->cli_print_help(true);
}
}
}
}
/**
* Exit the program with a message and set the exit status appropriately
*
* @param string $message The message to output
* @param mixed $error false if exiting normally; true or the expected
* error code if exiting abnormally. If true is used, then an exit code of
* 127 is used
* @return void
*/
public function cli_exit($message = null, $error = false) {
if ($message) {
print($message . "\n");
}
if ($error === false) {
$exitcode = 0;
}
else if ($error === true || !is_int($error)) {
$exitcode = 127;
}
exit($exitcode);
}
/**
* Print out a message formatted for the command line
*
* @param string $message The message to output
* @return void
*/
public function cli_print($message = '') {
print($message . "\n");
}
/**
* Print the help and usage information for this CLI script
*
* If a description is supplied for an argument, then this is
* word-wrapped to standard terminal lengths. All available options are
* also displayed.
*
* @param integer $exitcode The exit code to use, or true to indicate
* an error - {@see cli_exit}
*/
public function cli_print_help($exitcode = 0) {
// Display usage information
printf ("Usage: %s ", basename(__FILE__));
$options = array();
foreach ($this->settings->options as $option => $settings) {
$optiondisplay = '--' . $option;
if (isset($settings->examplevalue)) {
$optiondisplay .= '=' . $settings->examplevalue;
}
if (!$settings->required) {
$optiondisplay = '[' . $optiondisplay . ']';
}
$options[] = $optiondisplay;
}
print implode(' ', $options);
print "\n\n";
print $this->settings->info . "\n\n";
foreach ($this->settings->options as $option => $settings) {
// Line-wrap the description
$wrapped = wordwrap($settings->description, 48, '|||');
$lined = preg_split('/\|\|\|/', $wrapped);
// Merge the long option and short options
$alloptions = array('--' . $option);
foreach ($settings->shortoptions as $shortoption) {
$alloptions[] = '-' . $shortoption;
}
if (isset($settings->examplevalue)) {
foreach ($alloptions as &$option) {
$option = $option . '=' . $settings->examplevalue;
}
}
// Pad the arrays to make the loop easier
$total = max(count($alloptions), count($lined));
$lined = array_pad($lined, $total, '');
$alloptions = array_pad($alloptions, $total, '');
for ($i = 0; $i < $total; $i++) {
printf(" %-20s\t%s\n", $alloptions[$i], $lined[$i]);
}
print "\n";
}
$this->cli_exit(null, $exitcode);
}
}
/**
* Return a single CLI object
*
* This is stored in a static cache to ensure that only one instance of the
* CLI object is called
*
* @return CLI object
*/
function get_cli() {
static $cli = null;
if ($cli === null) {
$cli = new cli();
}
return $cli;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment