* @link http://www.yiiframework.com/ * @copyright 2008-2013 Yii Software LLC * @license http://www.yiiframework.com/license/ */ /** * CConsoleCommand represents an executable console command. * * It works like {@link CController} by parsing command line options and dispatching * the request to a specific action with appropriate option values. * * Users call a console command via the following command format: *
 * yiic CommandName ActionName --Option1=Value1 --Option2=Value2 ...
 * 
* * Child classes mainly needs to implement various action methods whose name must be * prefixed with "action". The parameters to an action method are considered as options * for that specific action. The action specified as {@link defaultAction} will be invoked * when a user does not specify the action name in his command. * * Options are bound to action parameters via parameter names. For example, the following * action method will allow us to run a command with yiic sitemap --type=News: *
 * class SitemapCommand extends CConsoleCommand {
 *     public function actionIndex($type) {
 *         ....
 *     }
 * }
 * 
* * Since version 1.1.11 the return value of action methods will be used as application exit code if it is an integer value. * * @property string $name The command name. * @property CConsoleCommandRunner $commandRunner The command runner instance. * @property string $help The command description. Defaults to 'Usage: php entry-script.php command-name'. * @property array $optionHelp The command option help information. Each array element describes * the help information for a single action. * * @author Qiang Xue * @package system.console * @since 1.0 */ abstract class CConsoleCommand extends CComponent { /** * @var string the name of the default action. Defaults to 'index'. * @since 1.1.5 */ public $defaultAction='index'; private $_name; private $_runner; /** * Constructor. * @param string $name name of the command * @param CConsoleCommandRunner $runner the command runner */ public function __construct($name,$runner) { $this->_name=$name; $this->_runner=$runner; $this->attachBehaviors($this->behaviors()); } /** * Initializes the command object. * This method is invoked after a command object is created and initialized with configurations. * You may override this method to further customize the command before it executes. * @since 1.1.6 */ public function init() { } /** * Returns a list of behaviors that this command should behave as. * The return value should be an array of behavior configurations indexed by * behavior names. Each behavior configuration can be either a string specifying * the behavior class or an array of the following structure: *
	 * 'behaviorName'=>array(
	 *     'class'=>'path.to.BehaviorClass',
	 *     'property1'=>'value1',
	 *     'property2'=>'value2',
	 * )
	 * 
* * Note, the behavior classes must implement {@link IBehavior} or extend from * {@link CBehavior}. Behaviors declared in this method will be attached * to the controller when it is instantiated. * * For more details about behaviors, see {@link CComponent}. * @return array the behavior configurations (behavior name=>behavior configuration) * @since 1.1.11 */ public function behaviors() { return array(); } /** * Executes the command. * The default implementation will parse the input parameters and * dispatch the command request to an appropriate action with the corresponding * option values * @param array $args command line parameters for this command. * @return integer application exit code, which is returned by the invoked action. 0 if the action did not return anything. * (return value is available since version 1.1.11) */ public function run($args) { list($action, $options, $args)=$this->resolveRequest($args); $methodName='action'.$action; if(!preg_match('/^\w+$/',$action) || !method_exists($this,$methodName)) $this->usageError("Unknown action: ".$action); $method=new ReflectionMethod($this,$methodName); $params=array(); // named and unnamed options foreach($method->getParameters() as $i=>$param) { $name=$param->getName(); if(isset($options[$name])) { if($param->isArray()) $params[]=is_array($options[$name]) ? $options[$name] : array($options[$name]); elseif(!is_array($options[$name])) $params[]=$options[$name]; else $this->usageError("Option --$name requires a scalar. Array is given."); } elseif($name==='args') $params[]=$args; elseif($param->isDefaultValueAvailable()) $params[]=$param->getDefaultValue(); else $this->usageError("Missing required option --$name."); unset($options[$name]); } // try global options if(!empty($options)) { $class=new ReflectionClass(get_class($this)); foreach($options as $name=>$value) { if($class->hasProperty($name)) { $property=$class->getProperty($name); if($property->isPublic() && !$property->isStatic()) { $this->$name=$value; unset($options[$name]); } } } } if(!empty($options)) $this->usageError("Unknown options: ".implode(', ',array_keys($options))); $exitCode=0; if($this->beforeAction($action,$params)) { $exitCode=$method->invokeArgs($this,$params); $exitCode=$this->afterAction($action,$params,is_int($exitCode)?$exitCode:0); } return $exitCode; } /** * This method is invoked right before an action is to be executed. * You may override this method to do last-minute preparation for the action. * @param string $action the action name * @param array $params the parameters to be passed to the action method. * @return boolean whether the action should be executed. */ protected function beforeAction($action,$params) { if($this->hasEventHandler('onBeforeAction')) { $event = new CConsoleCommandEvent($this,$params,$action); $this->onBeforeAction($event); return !$event->stopCommand; } else { return true; } } /** * This method is invoked right after an action finishes execution. * You may override this method to do some postprocessing for the action. * @param string $action the action name * @param array $params the parameters to be passed to the action method. * @param integer $exitCode the application exit code returned by the action method. * @return integer application exit code (return value is available since version 1.1.11) */ protected function afterAction($action,$params,$exitCode=0) { $event=new CConsoleCommandEvent($this,$params,$action,$exitCode); if($this->hasEventHandler('onAfterAction')) $this->onAfterAction($event); return $event->exitCode; } /** * Parses the command line arguments and determines which action to perform. * @param array $args command line arguments * @return array the action name, named options (name=>value), and unnamed options * @since 1.1.5 */ protected function resolveRequest($args) { $options=array(); // named parameters $params=array(); // unnamed parameters foreach($args as $arg) { if(preg_match('/^--(\w+)(=(.*))?$/',$arg,$matches)) // an option { $name=$matches[1]; $value=isset($matches[3]) ? $matches[3] : true; if(isset($options[$name])) { if(!is_array($options[$name])) $options[$name]=array($options[$name]); $options[$name][]=$value; } else $options[$name]=$value; } elseif(isset($action)) $params[]=$arg; else $action=$arg; } if(!isset($action)) $action=$this->defaultAction; return array($action,$options,$params); } /** * @return string the command name. */ public function getName() { return $this->_name; } /** * @return CConsoleCommandRunner the command runner instance */ public function getCommandRunner() { return $this->_runner; } /** * Provides the command description. * This method may be overridden to return the actual command description. * @return string the command description. Defaults to 'Usage: php entry-script.php command-name'. */ public function getHelp() { $help='Usage: '.$this->getCommandRunner()->getScriptName().' '.$this->getName(); $options=$this->getOptionHelp(); if(empty($options)) return $help."\n"; if(count($options)===1) return $help.' '.$options[0]."\n"; $help.=" \nActions:\n"; foreach($options as $option) $help.=' '.$option."\n"; return $help; } /** * Provides the command option help information. * The default implementation will return all available actions together with their * corresponding option information. * @return array the command option help information. Each array element describes * the help information for a single action. * @since 1.1.5 */ public function getOptionHelp() { $options=array(); $class=new ReflectionClass(get_class($this)); foreach($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { $name=$method->getName(); if(!strncasecmp($name,'action',6) && strlen($name)>6) { $name=substr($name,6); $name[0]=strtolower($name[0]); $help=$name; foreach($method->getParameters() as $param) { $optional=$param->isDefaultValueAvailable(); $defaultValue=$optional ? $param->getDefaultValue() : null; if(is_array($defaultValue)) { $defaultValue = str_replace(array("\r\n", "\n", "\r"), "", print_r($defaultValue, true)); } $name=$param->getName(); if($name==='args') continue; if($optional) $help.=" [--$name=$defaultValue]"; else $help.=" --$name=value"; } $options[]=$help; } } return $options; } /** * Displays a usage error. * This method will then terminate the execution of the current application. * @param string $message the error message */ public function usageError($message) { echo "Error: $message\n\n".$this->getHelp()."\n"; exit(1); } /** * Copies a list of files from one place to another. * @param array $fileList the list of files to be copied (name=>spec). * The array keys are names displayed during the copy process, and array values are specifications * for files to be copied. Each array value must be an array of the following structure: * * @see buildFileList */ public function copyFiles($fileList) { $overwriteAll=false; foreach($fileList as $name=>$file) { $source=strtr($file['source'],'/\\',DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR); $target=strtr($file['target'],'/\\',DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR); $callback=isset($file['callback']) ? $file['callback'] : null; $params=isset($file['params']) ? $file['params'] : null; if(is_dir($source)) { $this->ensureDirectory($target); continue; } if($callback!==null) $content=call_user_func($callback,$source,$params); else $content=file_get_contents($source); if(is_file($target)) { if($content===file_get_contents($target)) { echo " unchanged $name\n"; continue; } if($overwriteAll) echo " overwrite $name\n"; else { echo " exist $name\n"; echo " ...overwrite? [Yes|No|All|Quit] "; $answer=trim(fgets(STDIN)); if(!strncasecmp($answer,'q',1)) return; elseif(!strncasecmp($answer,'y',1)) echo " overwrite $name\n"; elseif(!strncasecmp($answer,'a',1)) { echo " overwrite $name\n"; $overwriteAll=true; } else { echo " skip $name\n"; continue; } } } else { $this->ensureDirectory(dirname($target)); echo " generate $name\n"; } file_put_contents($target,$content); } } /** * Builds the file list of a directory. * This method traverses through the specified directory and builds * a list of files and subdirectories that the directory contains. * The result of this function can be passed to {@link copyFiles}. * @param string $sourceDir the source directory * @param string $targetDir the target directory * @param string $baseDir base directory * @param array $ignoreFiles list of the names of files that should * be ignored in list building process. Argument available since 1.1.11. * @param array $renameMap hash array of file names that should be * renamed. Example value: array('1.old.txt'=>'2.new.txt'). * Argument available since 1.1.11. * @return array the file list (see {@link copyFiles}) */ public function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) { $list=array(); $handle=opendir($sourceDir); while(($file=readdir($handle))!==false) { if(in_array($file,array('.','..','.svn','.gitignore')) || in_array($file,$ignoreFiles)) continue; $sourcePath=$sourceDir.DIRECTORY_SEPARATOR.$file; $targetPath=$targetDir.DIRECTORY_SEPARATOR.strtr($file,$renameMap); $name=$baseDir===''?$file : $baseDir.'/'.$file; $list[$name]=array('source'=>$sourcePath, 'target'=>$targetPath); if(is_dir($sourcePath)) $list=array_merge($list,$this->buildFileList($sourcePath,$targetPath,$name,$ignoreFiles,$renameMap)); } closedir($handle); return $list; } /** * Creates all parent directories if they do not exist. * @param string $directory the directory to be checked */ public function ensureDirectory($directory) { if(!is_dir($directory)) { $this->ensureDirectory(dirname($directory)); echo " mkdir ".strtr($directory,'\\','/')."\n"; mkdir($directory); } } /** * Renders a view file. * @param string $_viewFile_ view file path * @param array $_data_ optional data to be extracted as local view variables * @param boolean $_return_ whether to return the rendering result instead of displaying it * @return mixed the rendering result if required. Null otherwise. */ public function renderFile($_viewFile_,$_data_=null,$_return_=false) { if(is_array($_data_)) extract($_data_,EXTR_PREFIX_SAME,'data'); else $data=$_data_; if($_return_) { ob_start(); ob_implicit_flush(false); require($_viewFile_); return ob_get_clean(); } else require($_viewFile_); } /** * Converts a word to its plural form. * @param string $name the word to be pluralized * @return string the pluralized word */ public function pluralize($name) { $rules=array( '/(m)ove$/i' => '\1oves', '/(f)oot$/i' => '\1eet', '/(c)hild$/i' => '\1hildren', '/(h)uman$/i' => '\1umans', '/(m)an$/i' => '\1en', '/(s)taff$/i' => '\1taff', '/(t)ooth$/i' => '\1eeth', '/(p)erson$/i' => '\1eople', '/([m|l])ouse$/i' => '\1ice', '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', '/([^aeiouy]|qu)y$/i' => '\1ies', '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', '/(shea|lea|loa|thie)f$/i' => '\1ves', '/([ti])um$/i' => '\1a', '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', '/(bu)s$/i' => '\1ses', '/(ax|test)is$/i' => '\1es', '/s$/' => 's', ); foreach($rules as $rule=>$replacement) { if(preg_match($rule,$name)) return preg_replace($rule,$replacement,$name); } return $name.'s'; } /** * Reads input via the readline PHP extension if that's available, or fgets() if readline is not installed. * * @param string $message to echo out before waiting for user input * @param string $default the default string to be returned when user does not write anything. * Defaults to null, means that default string is disabled. This parameter is available since version 1.1.11. * @return mixed line read as a string, or false if input has been closed * * @since 1.1.9 */ public function prompt($message,$default=null) { if($default!==null) $message.=" [$default] "; else $message.=' '; if(extension_loaded('readline')) { $input=readline($message); if($input!==false) readline_add_history($input); } else { echo $message; $input=fgets(STDIN); } if($input===false) return false; else{ $input=trim($input); return ($input==='' && $default!==null) ? $default : $input; } } /** * Asks user to confirm by typing y or n. * * @param string $message to echo out before waiting for user input * @param boolean $default this value is returned if no selection is made. This parameter has been available since version 1.1.11. * @return boolean whether user confirmed * * @since 1.1.9 */ public function confirm($message,$default=false) { echo $message.' (yes|no) [' . ($default ? 'yes' : 'no') . ']:'; $input = trim(fgets(STDIN)); return empty($input) ? $default : !strncasecmp($input,'y',1); } /** * This event is raised before an action is to be executed. * @param CConsoleCommandEvent $event the event parameter * @since 1.1.11 */ public function onBeforeAction($event) { $this->raiseEvent('onBeforeAction',$event); } /** * This event is raised after an action finishes execution. * @param CConsoleCommandEvent $event the event parameter * @since 1.1.11 */ public function onAfterAction($event) { $this->raiseEvent('onAfterAction',$event); } }