src/Library/CommandLine/AbstractCommandLineController.php
<?php
/**
* This file is part of the Library package.
*
* Copyleft (ↄ) 2013-2016 Pierre Cassat <me@e-piwi.fr> and contributors
*
* 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/>.
*
* The source code of this package is available online at
* <http://github.com/atelierspierrot/library>.
*/
namespace Library\CommandLine;
use \Library\CodeParser;
/**
* Basic command line controller
*
* Any command line controller must extend this abstract class.
*
* It defines some basic command line options that you must not overwrite in your child class:
*
* - "h | help" : get a usage information,
* - "v | verbose" : increase verbosity of the execution (written strings must be handled in your scripts),
* - "x | debug" : execute the script in "debug" mode (must write actions before executing them),
* - "q | quiet" : turn verbosity totally off during the execution (only errors and informations will be written),
* - "f | force" : force some actions (avoid interactions),
* - "i | interactive" : increase interactivity of the execution,
* - "version" : get some version information about running environment.
*
* @author piwi <me@e-piwi.fr>
*/
abstract class AbstractCommandLineController
implements CommandLineControllerInterface
{
/**
* This must be over-written in any child class
*
* @var string The name of the script
*/
public static $_name = 'Command line interface';
/**
* This must be over-written in any child class
*
* @var string The version number of the script
*/
public static $_version = '1.0.0';
/**
* @var string The name of the current called script
*/
protected $script;
/**
* @var array The array of command line parameters
*/
protected $params;
/**
* @var \Library\CommandLine\Formater The formater instance
*/
protected $formater;
/**
* @var \Library\CommandLine\Stream The stream instance
*/
protected $stream;
/**
* @var bool Default if `false`, option `-v` puts it `true`
*/
protected $verbose = false;
/**
* @var bool Default if `false`, option `-x` puts it `true`
*/
protected $debug = false;
/**
* @var bool Default if `false`, option `-f` puts it `true`
*/
protected $force = false;
/**
* @var bool Default if `false`, option `-i` puts it `true`
*/
protected $interactive = false;
/**
* @var array Stack of executed methods during script execution
*/
protected $done_methods = array();
/**
* @var bool
*/
protected $written = false;
/**
* The default CLI options
*
* @var array
*/
protected $basic_options = array(
'argv_options'=>array(
'h' => 'help',
'v' => 'verbose',
'x' => 'debug',
'q' => 'quiet',
'f' => 'force',
'i' => 'interactive',
),
'argv_long_options'=>array(
'help' => 'help',
'verbose' => 'verbose',
'debug' => 'debug',
'quiet' => 'quiet',
'force' => 'force',
'interactive'=> 'interactive',
),
'commands'=>array(
'version' => 'version',
),
'aliases'=>array(
'vers' => 'version',
),
);
/**
* Default CLI & presentation options
*
* Must be over-written in any child class, defining:
*
* - "title" item: the title of the child class,
* - "title_options" item: an array for the title presentation options,
* - "argv_options" item: an array of child class short options like `letter => command` pairs,
* - "argv_long_options" item: an array of child class long options like `option => command` pairs,
* - "commands" item: an array of child class commands,
* - "aliases" item: an array of child class aliases for commands,
*
* @var array
*/
protected $options = array(
'title' => '',
'title_options' => array(
'foreground' => 'cyan',
'background' => 'blue',
'text_options' => 'bold',
'autospaced' => false
),
'argv_options' => array(),
'argv_long_options' => array(),
'commands' => array(),
'aliases' => array(),
);
// ------------------------------------
// Construction
// ------------------------------------
public function __construct(array $options = array())
{
// if (!isset($this->options)) $this->options = array();
$this
->_initOptions()
->_overWriteOptions($this->basic_options)
->_overWriteOptions($options);
// $this->options = array_merge_recursive($this->basic_options, $this->options, $options);
if (!empty($options)) {
foreach($options as $optn=>$optv) {
if (array_key_exists($optn, $this->options)) {
if (is_array($this->options[$optn]) OR is_array($optv)) {
if (!is_array($this->options[$optn])) $this->options[$optn] = array( $this->options[$optn] );
if (!is_array($optv)) $optv = array( $optv );
$this->options[$optn] = array_merge($this->options[$optn], $optv);
} else
$this->options[$optn] = $optv;
}
}
}
if (empty($argv)) {
$argv = $_SERVER['argv'];
}
$this->setScript( array_shift($argv) );
$this->setParameters( $argv );
$this->formater = new Formater;
foreach($this->options as $_optn=>$_optv) {
$this->formater->addOption( $_optn, $_optv );
}
$this->formater->setAutospaced(true);
$this->stream = new Stream;
// self::_treatBasicOptions();
}
/**
* Magic distribution when printing object
*
* @return string
*/
public function __toString()
{
$this->distribute();
return '';
}
/**
* Distribution of the work
*/
public function distribute()
{
if (empty($this->params)) {
return self::writeNothingToDo();
}
if ($this->written===false && $this->verbose===true) {
self::writeIntro();
$this->written=true;
}
self::_treatOptions();
}
// ------------------------------------
// Setters / Getters
// ------------------------------------
/**
* @param bool $dbg
* @return self
*/
public function setDebug($dbg = true)
{
$this->verbose = $dbg;
$this->debug = $dbg;
return $this;
}
/**
* @param bool $vbr
* @return $this
*/
public function setVerbose($vbr = true)
{
$this->verbose = $vbr;
return $this;
}
/**
* @param bool $frc
* @return $this
*/
public function setForce($frc = true)
{
$this->force = $frc;
return $this;
}
/**
* @param bool $frc
* @return $this
*/
public function setInteractive($frc = true)
{
$this->interactive = $frc;
return $this;
}
/**
* @return $this
* @see self::setVerbose()
* @see self::setDebug()
*/
public function setQuiet()
{
self::setVerbose(false);
self::setDebug(false);
return $this;
}
/**
* @param string $_cls_meth
* @return $this
*/
public function addDoneMethod($_cls_meth)
{
$this->done_methods[] = $_cls_meth;
return $this;
}
/**
* @return array
*/
public function getDoneMethods()
{
return $this->done_methods;
}
/**
* Set the current command line script called
*
* @param string $script_name The script name
* @return self
*/
public function setScript($script_name)
{
$this->script = $script_name;
return $this;
}
/**
* Get the current command line script called
*
* @return string
*/
public function getScript()
{
return $this->script;
}
/**
* Set the command line parameters
*
* @param array $params The collection of parameters
* @return self
*/
public function setParameters(array $params)
{
$this->params = $params;
return $this;
}
/**
* Get the parameters collection
*
* @return array
*/
public function getParameters()
{
return $this->params;
}
// ------------------------------------
// CLI METHODS
// ------------------------------------
// The docblocks comments of any `run...` method is used to build the "help" info of the script
/**
* List of all options and features of the command line tool ; for some commands, a specific help can be available, running <var>--command --help</var>
* Some command examples are purposed running <var>--console --help</var>
*/
public function runHelpCommand($opt = null)
{
if (!empty($opt)) {
if (!is_array($opt)) $opt = array( $opt=>'' );
$opt_keys = array_keys($opt);
$ok=false;
while ($ok===false) {
if (count($opt_keys)==0) break;
$current_option = array_shift( $opt_keys );
$ok = self::runArgumentHelp( $current_option );
}
$this->debugWrite( '>> [help] displaying global help' );
$help_str = Helper::getHelpInfo($this->options, $this->formater, $this);
self::write( $this->formater->parse($help_str) );
self::writeStop();
} else {
self::usage();
}
}
/**
* Run the command line in <bold>verbose</bold> mode, writing some information on screen (default is <option>OFF</option>)
*/
public function runVerboseCommand()
{
self::setVerbose();
}
/**
* Run the command line in <bold>quiet</bold> mode, trying to not write anything on screen (default is <option>OFF</option>)
*/
public function runQuietCommand()
{
self::setQuiet();
}
/**
* Run the command line in <bold>debug</bold> mode, writing some scripts information during runtime (default is <option>OFF</option>)
*/
public function runDebugCommand()
{
self::setDebug();
}
/**
* Run the command line in <bold>forced</bold> mode ; any choice will be set on default value if so
*/
public function runForceCommand()
{
self::setForce();
}
/**
* Run the command line in <bold>interactive</bold> mode ; any choice will be prompted if possible
*/
public function runInteractiveCommand()
{
self::setInteractive();
}
/**
* Get versions of system environment
*/
public function runVersionCommand()
{
$str = '';
$_cls = get_class($this);
if (!empty($_cls::$_name)) {
$str .= $_cls::$_name;
}
if (!empty($_cls::$_version)) {
$str .= ' - v. '.$_cls::$_version;
}
if (strlen($str)) {
$this->writeInfo('Interface version: <bold>'.$str.'</bold>');
}
$this->writeInfo('PHP version: <bold>'.phpversion().'</bold>');
$this->writeInfo('Server software: <bold>'.php_uname().'</bold>');
$this->writeStop();
}
// ------------------------------------
// CLI WRITING METHODS
// ------------------------------------
/**
* Format and write a string to STDOUT
*
* @param null $str
* @param bool $new_line
* @return $this
*/
public function write($str = null, $new_line = true)
{
$this->stream->write( $this->formater->message($str), $new_line );
return $this;
}
/**
* Format and write an error message to STDOUT (or STDERR) and exits with status `$status`
*
* @param null $str
* @param int $status
* @param bool $new_line
* @return $this
*/
public function error($str = null, $status = 1, $new_line = true)
{
$this->stream->error( $this->formater->message($str), $status, $new_line );
return $this;
}
/**
* Parse, format and write a message to STDOUT
*
* @param string $str
* @param null $type
* @param bool $spaced
* @return $this
*/
public function parseAndWrite($str, $type = null, $spaced = false)
{
if ($this->verbose!==true) return self::write( $str );
if ($spaced!==false)
$str = $this->formater->spacedStr( $str, $type, true );
elseif (!is_null($type))
$str = $this->formater->buildTaggedString( $str, $type );
self::write( $this->formater->parse( $str ) );
return $this;
}
/**
* @param string $str
* @return $this
*/
public function writeError($str)
{
if ($this->verbose===true) self::writeBreak();
self::parseAndWrite( $str, 'error', true );
return $this;
}
/**
* @param string $str
* @return $this
*/
public function writeThinError($str)
{
if ($this->verbose===true) self::writeBreak();
self::parseAndWrite( $str, 'error_str' );
if ($this->verbose===true) self::writeBreak();
return $this;
}
/**
* @param string $str
* @return $this
*/
public function writeInfo($str)
{
self::parseAndWrite( $str, 'info' );
return $this;
}
/**
* @param string $str
* @return $this
*/
public function writeComment($str)
{
if ($this->verbose===true)
self::parseAndWrite( $str, 'comment' );
return $this;
}
/**
* @param string $str
* @return $this
*/
public function writeHighlight($str)
{
self::parseAndWrite( $str, 'highlight' );
return $this;
}
/**
* @return $this
*/
public function writeBreak()
{
if ($this->verbose===true) $this->stream->write(PHP_EOL);
return $this;
}
/**
* @return $this
*/
public function writeStop()
{
if ($this->verbose!==true) {
$this->stream->__exit();
} else {
$this->debugWrite( '>> [writeStop] exit with no error' );
$this->stream->__exit(
$this->formater->message( '<info>-- out --</info>' )
);
}
return $this;
}
/**
* Write a string only in verbose mode
*
* @param null $str
* @param bool $new_line
* @return $this
*/
public function verboseWrite($str = null, $new_line = true)
{
if ($this->verbose===true) $this->write( $str, $new_line );
return $this;
}
/**
* Write a string only in debug mode
*
* @param null $str
* @param bool $new_line
* @return $this
*/
public function debugWrite($str = null, $new_line = true )
{
if ($this->debug===true) $this->write( $str, $new_line );
return $this;
}
/**
* Prompt user input
*
* @param string|null $str
* @param mixed|null $default
* @return string
*/
public function prompt($str = null, $default = null)
{
$this->stream->prompt(
$this->formater->prompt($str, $default)
);
return $this->stream->getUserResponse();
}
/**
* Get last user input
*
* @return string
*/
public function getPrompt()
{
return $this->stream->getUserResponse();
}
// ------------------------------------
// CONTROLLER METHODS
// ------------------------------------
private function __init()
{
if ($this->written===false && $this->verbose===true) {
self::writeIntro();
$this->written=true;
}
}
public function writeIntro()
{
$str = !empty($this->options['title']) ? $this->options['title'] : $this->getVersionString();
$this->stream->write(
$this->formater->parse(
$this->formater->spacedStr($str, 'title', true)
).PHP_EOL
);
}
public function getVersionString()
{
$str = '';
$_cls = get_class($this);
if (!empty($_cls::$_name)) {
$str .= $_cls::$_name;
}
if (!empty($_cls::$_version)) {
$str .= ' - v. '.$_cls::$_version;
}
return $str;
}
public function writeNothingToDo( )
{
self::__init();
self::writeThinError( '> Nothing to do ! (run "--help" option to see help)' );
$this->stream->__exit();
}
public function runArgumentHelp($arg = null)
{
$help_descr = $this->getOptionHelp( $arg );
if ($help_descr!=$arg) {
$this->debugWrite( ">> [help] displaying help for option \"$arg\"" );
$help_ctt = Helper::formatHelpString( ucfirst($arg), $help_descr, $this->formater );
self::write( $this->formater->parse($help_ctt) );
self::writeStop();
}
$this->debugWrite( ">> [help] no help found for option \"$arg\"" );
return false;
}
public function usage($opt = null)
{
if (!empty($opt)) {
if (!is_array($opt)) $opt = array( $opt=>'' );
$opt_keys = array_keys($opt);
$ok=false;
while ($ok===false) {
if (count($opt_keys)==0) break;
$current_option = array_shift( $opt_keys );
$ok = self::runArgumentHelp( $current_option );
}
}
$this->debugWrite( '>> [help] displaying global help' );
$help_str = Helper::getHelpInfo($this->options, $this->formater, $this);
self::write( $this->formater->parse($help_str) );
self::writeStop();
}
// --------------------
// PROCESS
// --------------------
protected function _initOptions()
{
if (!isset($this->options)) $this->options = array();
return $this;
}
protected function _overWriteOptions(array $options)
{
if (!empty($options)) {
foreach ($options as $name=>$stack) {
if (is_array($stack)) {
if (!isset($this->options[$name])) {
$this->options[$name] = array();
}
$this->options[$name] = array_merge($this->options[$name], $stack);
} else {
$this->options[$name] = $stack;
}
}
}
return $this;
}
protected function _treatOptions()
{
$this->params = self::getopt();
$this->debugWrite( ">> Command line arguments are [".var_export($this->params,1)."]" );
$_meths=array();
foreach($this->params as $_opt_name=>$_opt_val) {
$new_opt_names = array(
$_opt_name, $_opt_name.':', $_opt_name.'::'
);
foreach($new_opt_names as $_opt_new_name) {
if (array_key_exists($_opt_new_name, $this->options['argv_options'])) {
$_ind = $this->options['argv_options'][$_opt_new_name];
$_meths[ $_ind ] = $_opt_val;
}
if (array_key_exists($_opt_new_name, $this->options['argv_long_options'])) {
$_ind = $this->options['argv_long_options'][$_opt_new_name];
$_meths[ $_ind ] = $_opt_val;
}
if (array_key_exists($_opt_new_name, $this->options['commands'])) {
$_ind = $this->options['commands'][$_opt_new_name];
$_meths[ $_ind ] = $_opt_val;
}
if (array_key_exists($_opt_new_name, $this->options['aliases'])) {
$_ind = $this->options['aliases'][$_opt_new_name];
$_meths[ $_ind ] = $_opt_val;
}
}
}
if (!empty($_meths)) {
if (array_key_exists('help', $_meths)) {
$_cls_meth = 'runHelpCommand';
$args = $_meths;
unset($args['help']);
self::addDoneMethod( $_cls_meth );
self::__init();
$this->$_cls_meth( $args );
} else {
foreach($_meths as $_meth=>$_args) {
$_cls_meth = 'run'.ucfirst($_meth).'Command';
if (method_exists($this, $_cls_meth) AND !in_array($_cls_meth, $this->done_methods)) {
self::addDoneMethod( $_cls_meth );
self::__init();
$this->$_cls_meth( $_args );
}
}
}
}
}
protected function _treatBasicOptions()
{
return Helper::treatOptions($this->basic_options, $this);
}
public function getopt()
{
return Helper::getopt( array_merge(
$this->basic_options, $this->options
) );
}
public function getOptionMethod($arg = null)
{
return Helper::getOptionMethod(array_merge(
$this->basic_options, $this->options
), $this);
}
public function getOptionDescription($arg = null)
{
return Helper::getOptionDescription($arg, $this);
}
public function getOptionHelp($arg = null)
{
return Helper::getOptionHelp($arg, $this);
}
}