1: <?php
2: /**
3: * CConsoleCommand class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CConsoleCommand represents an executable console command.
13: *
14: * It works like {@link CController} by parsing command line options and dispatching
15: * the request to a specific action with appropriate option values.
16: *
17: * Users call a console command via the following command format:
18: * <pre>
19: * yiic CommandName ActionName --Option1=Value1 --Option2=Value2 ...
20: * </pre>
21: *
22: * Child classes mainly needs to implement various action methods whose name must be
23: * prefixed with "action". The parameters to an action method are considered as options
24: * for that specific action. The action specified as {@link defaultAction} will be invoked
25: * when a user does not specify the action name in his command.
26: *
27: * Options are bound to action parameters via parameter names. For example, the following
28: * action method will allow us to run a command with <code>yiic sitemap --type=News</code>:
29: * <pre>
30: * class SitemapCommand extends CConsoleCommand {
31: * public function actionIndex($type) {
32: * ....
33: * }
34: * }
35: * </pre>
36: *
37: * Since version 1.1.11 the return value of action methods will be used as application exit code if it is an integer value.
38: *
39: * @property string $name The command name.
40: * @property CConsoleCommandRunner $commandRunner The command runner instance.
41: * @property string $help The command description. Defaults to 'Usage: php entry-script.php command-name'.
42: * @property array $optionHelp The command option help information. Each array element describes
43: * the help information for a single action.
44: *
45: * @author Qiang Xue <qiang.xue@gmail.com>
46: * @package system.console
47: * @since 1.0
48: */
49: abstract class CConsoleCommand extends CComponent
50: {
51: /**
52: * @var string the name of the default action. Defaults to 'index'.
53: * @since 1.1.5
54: */
55: public $defaultAction='index';
56:
57: private $_name;
58: private $_runner;
59:
60: /**
61: * Constructor.
62: * @param string $name name of the command
63: * @param CConsoleCommandRunner $runner the command runner
64: */
65: public function __construct($name,$runner)
66: {
67: $this->_name=$name;
68: $this->_runner=$runner;
69: $this->attachBehaviors($this->behaviors());
70: }
71:
72: /**
73: * Initializes the command object.
74: * This method is invoked after a command object is created and initialized with configurations.
75: * You may override this method to further customize the command before it executes.
76: * @since 1.1.6
77: */
78: public function init()
79: {
80: }
81:
82: /**
83: * Returns a list of behaviors that this command should behave as.
84: * The return value should be an array of behavior configurations indexed by
85: * behavior names. Each behavior configuration can be either a string specifying
86: * the behavior class or an array of the following structure:
87: * <pre>
88: * 'behaviorName'=>array(
89: * 'class'=>'path.to.BehaviorClass',
90: * 'property1'=>'value1',
91: * 'property2'=>'value2',
92: * )
93: * </pre>
94: *
95: * Note, the behavior classes must implement {@link IBehavior} or extend from
96: * {@link CBehavior}. Behaviors declared in this method will be attached
97: * to the controller when it is instantiated.
98: *
99: * For more details about behaviors, see {@link CComponent}.
100: * @return array the behavior configurations (behavior name=>behavior configuration)
101: * @since 1.1.11
102: */
103: public function behaviors()
104: {
105: return array();
106: }
107:
108: /**
109: * Executes the command.
110: * The default implementation will parse the input parameters and
111: * dispatch the command request to an appropriate action with the corresponding
112: * option values
113: * @param array $args command line parameters for this command.
114: * @return integer application exit code, which is returned by the invoked action. 0 if the action did not return anything.
115: * (return value is available since version 1.1.11)
116: */
117: public function run($args)
118: {
119: list($action, $options, $args)=$this->resolveRequest($args);
120: $methodName='action'.$action;
121: if(!preg_match('/^\w+$/',$action) || !method_exists($this,$methodName))
122: $this->usageError("Unknown action: ".$action);
123:
124: $method=new ReflectionMethod($this,$methodName);
125: $params=array();
126: // named and unnamed options
127: foreach($method->getParameters() as $i=>$param)
128: {
129: $name=$param->getName();
130: if(isset($options[$name]))
131: {
132: if($param->isArray())
133: $params[]=is_array($options[$name]) ? $options[$name] : array($options[$name]);
134: elseif(!is_array($options[$name]))
135: $params[]=$options[$name];
136: else
137: $this->usageError("Option --$name requires a scalar. Array is given.");
138: }
139: elseif($name==='args')
140: $params[]=$args;
141: elseif($param->isDefaultValueAvailable())
142: $params[]=$param->getDefaultValue();
143: else
144: $this->usageError("Missing required option --$name.");
145: unset($options[$name]);
146: }
147:
148: // try global options
149: if(!empty($options))
150: {
151: $class=new ReflectionClass(get_class($this));
152: foreach($options as $name=>$value)
153: {
154: if($class->hasProperty($name))
155: {
156: $property=$class->getProperty($name);
157: if($property->isPublic() && !$property->isStatic())
158: {
159: $this->$name=$value;
160: unset($options[$name]);
161: }
162: }
163: }
164: }
165:
166: if(!empty($options))
167: $this->usageError("Unknown options: ".implode(', ',array_keys($options)));
168:
169: $exitCode=0;
170: if($this->beforeAction($action,$params))
171: {
172: $exitCode=$method->invokeArgs($this,$params);
173: $exitCode=$this->afterAction($action,$params,is_int($exitCode)?$exitCode:0);
174: }
175: return $exitCode;
176: }
177:
178: /**
179: * This method is invoked right before an action is to be executed.
180: * You may override this method to do last-minute preparation for the action.
181: * @param string $action the action name
182: * @param array $params the parameters to be passed to the action method.
183: * @return boolean whether the action should be executed.
184: */
185: protected function beforeAction($action,$params)
186: {
187: if($this->hasEventHandler('onBeforeAction'))
188: {
189: $event = new CConsoleCommandEvent($this,$params,$action);
190: $this->onBeforeAction($event);
191: return !$event->stopCommand;
192: }
193: else
194: {
195: return true;
196: }
197: }
198:
199: /**
200: * This method is invoked right after an action finishes execution.
201: * You may override this method to do some postprocessing for the action.
202: * @param string $action the action name
203: * @param array $params the parameters to be passed to the action method.
204: * @param integer $exitCode the application exit code returned by the action method.
205: * @return integer application exit code (return value is available since version 1.1.11)
206: */
207: protected function afterAction($action,$params,$exitCode=0)
208: {
209: $event=new CConsoleCommandEvent($this,$params,$action,$exitCode);
210: if($this->hasEventHandler('onAfterAction'))
211: $this->onAfterAction($event);
212: return $event->exitCode;
213: }
214:
215: /**
216: * Parses the command line arguments and determines which action to perform.
217: * @param array $args command line arguments
218: * @return array the action name, named options (name=>value), and unnamed options
219: * @since 1.1.5
220: */
221: protected function resolveRequest($args)
222: {
223: $options=array(); // named parameters
224: $params=array(); // unnamed parameters
225: foreach($args as $arg)
226: {
227: if(preg_match('/^--(\w+)(=(.*))?$/',$arg,$matches)) // an option
228: {
229: $name=$matches[1];
230: $value=isset($matches[3]) ? $matches[3] : true;
231: if(isset($options[$name]))
232: {
233: if(!is_array($options[$name]))
234: $options[$name]=array($options[$name]);
235: $options[$name][]=$value;
236: }
237: else
238: $options[$name]=$value;
239: }
240: elseif(isset($action))
241: $params[]=$arg;
242: else
243: $action=$arg;
244: }
245: if(!isset($action))
246: $action=$this->defaultAction;
247:
248: return array($action,$options,$params);
249: }
250:
251: /**
252: * @return string the command name.
253: */
254: public function getName()
255: {
256: return $this->_name;
257: }
258:
259: /**
260: * @return CConsoleCommandRunner the command runner instance
261: */
262: public function getCommandRunner()
263: {
264: return $this->_runner;
265: }
266:
267: /**
268: * Provides the command description.
269: * This method may be overridden to return the actual command description.
270: * @return string the command description. Defaults to 'Usage: php entry-script.php command-name'.
271: */
272: public function getHelp()
273: {
274: $help='Usage: '.$this->getCommandRunner()->getScriptName().' '.$this->getName();
275: $options=$this->getOptionHelp();
276: if(empty($options))
277: return $help."\n";
278: if(count($options)===1)
279: return $help.' '.$options[0]."\n";
280: $help.=" <action>\nActions:\n";
281: foreach($options as $option)
282: $help.=' '.$option."\n";
283: return $help;
284: }
285:
286: /**
287: * Provides the command option help information.
288: * The default implementation will return all available actions together with their
289: * corresponding option information.
290: * @return array the command option help information. Each array element describes
291: * the help information for a single action.
292: * @since 1.1.5
293: */
294: public function getOptionHelp()
295: {
296: $options=array();
297: $class=new ReflectionClass(get_class($this));
298: foreach($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
299: {
300: $name=$method->getName();
301: if(!strncasecmp($name,'action',6) && strlen($name)>6)
302: {
303: $name=substr($name,6);
304: $name[0]=strtolower($name[0]);
305: $help=$name;
306:
307: foreach($method->getParameters() as $param)
308: {
309: $optional=$param->isDefaultValueAvailable();
310: $defaultValue=$optional ? $param->getDefaultValue() : null;
311: if(is_array($defaultValue)) {
312: $defaultValue = str_replace(array("\r\n", "\n", "\r"), "", print_r($defaultValue, true));
313: }
314: $name=$param->getName();
315:
316: if($name==='args')
317: continue;
318:
319: if($optional)
320: $help.=" [--$name=$defaultValue]";
321: else
322: $help.=" --$name=value";
323: }
324: $options[]=$help;
325: }
326: }
327: return $options;
328: }
329:
330: /**
331: * Displays a usage error.
332: * This method will then terminate the execution of the current application.
333: * @param string $message the error message
334: */
335: public function usageError($message)
336: {
337: echo "Error: $message\n\n".$this->getHelp()."\n";
338: exit(1);
339: }
340:
341: /**
342: * Copies a list of files from one place to another.
343: * @param array $fileList the list of files to be copied (name=>spec).
344: * The array keys are names displayed during the copy process, and array values are specifications
345: * for files to be copied. Each array value must be an array of the following structure:
346: * <ul>
347: * <li>source: required, the full path of the file/directory to be copied from</li>
348: * <li>target: required, the full path of the file/directory to be copied to</li>
349: * <li>callback: optional, the callback to be invoked when copying a file. The callback function
350: * should be declared as follows:
351: * <pre>
352: * function foo($source,$params)
353: * </pre>
354: * where $source parameter is the source file path, and the content returned
355: * by the function will be saved into the target file.</li>
356: * <li>params: optional, the parameters to be passed to the callback</li>
357: * </ul>
358: * @see buildFileList
359: */
360: public function copyFiles($fileList)
361: {
362: $overwriteAll=false;
363: foreach($fileList as $name=>$file)
364: {
365: $source=strtr($file['source'],'/\\',DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR);
366: $target=strtr($file['target'],'/\\',DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR);
367: $callback=isset($file['callback']) ? $file['callback'] : null;
368: $params=isset($file['params']) ? $file['params'] : null;
369:
370: if(is_dir($source))
371: {
372: $this->ensureDirectory($target);
373: continue;
374: }
375:
376: if($callback!==null)
377: $content=call_user_func($callback,$source,$params);
378: else
379: $content=file_get_contents($source);
380: if(is_file($target))
381: {
382: if($content===file_get_contents($target))
383: {
384: echo " unchanged $name\n";
385: continue;
386: }
387: if($overwriteAll)
388: echo " overwrite $name\n";
389: else
390: {
391: echo " exist $name\n";
392: echo " ...overwrite? [Yes|No|All|Quit] ";
393: $answer=trim(fgets(STDIN));
394: if(!strncasecmp($answer,'q',1))
395: return;
396: elseif(!strncasecmp($answer,'y',1))
397: echo " overwrite $name\n";
398: elseif(!strncasecmp($answer,'a',1))
399: {
400: echo " overwrite $name\n";
401: $overwriteAll=true;
402: }
403: else
404: {
405: echo " skip $name\n";
406: continue;
407: }
408: }
409: }
410: else
411: {
412: $this->ensureDirectory(dirname($target));
413: echo " generate $name\n";
414: }
415: file_put_contents($target,$content);
416: }
417: }
418:
419: /**
420: * Builds the file list of a directory.
421: * This method traverses through the specified directory and builds
422: * a list of files and subdirectories that the directory contains.
423: * The result of this function can be passed to {@link copyFiles}.
424: * @param string $sourceDir the source directory
425: * @param string $targetDir the target directory
426: * @param string $baseDir base directory
427: * @param array $ignoreFiles list of the names of files that should
428: * be ignored in list building process. Argument available since 1.1.11.
429: * @param array $renameMap hash array of file names that should be
430: * renamed. Example value: array('1.old.txt'=>'2.new.txt').
431: * Argument available since 1.1.11.
432: * @return array the file list (see {@link copyFiles})
433: */
434: public function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array())
435: {
436: $list=array();
437: $handle=opendir($sourceDir);
438: while(($file=readdir($handle))!==false)
439: {
440: if(in_array($file,array('.','..','.svn','.gitignore')) || in_array($file,$ignoreFiles))
441: continue;
442: $sourcePath=$sourceDir.DIRECTORY_SEPARATOR.$file;
443: $targetPath=$targetDir.DIRECTORY_SEPARATOR.strtr($file,$renameMap);
444: $name=$baseDir===''?$file : $baseDir.'/'.$file;
445: $list[$name]=array('source'=>$sourcePath, 'target'=>$targetPath);
446: if(is_dir($sourcePath))
447: $list=array_merge($list,$this->buildFileList($sourcePath,$targetPath,$name,$ignoreFiles,$renameMap));
448: }
449: closedir($handle);
450: return $list;
451: }
452:
453: /**
454: * Creates all parent directories if they do not exist.
455: * @param string $directory the directory to be checked
456: */
457: public function ensureDirectory($directory)
458: {
459: if(!is_dir($directory))
460: {
461: $this->ensureDirectory(dirname($directory));
462: echo " mkdir ".strtr($directory,'\\','/')."\n";
463: mkdir($directory);
464: }
465: }
466:
467: /**
468: * Renders a view file.
469: * @param string $_viewFile_ view file path
470: * @param array $_data_ optional data to be extracted as local view variables
471: * @param boolean $_return_ whether to return the rendering result instead of displaying it
472: * @return mixed the rendering result if required. Null otherwise.
473: */
474: public function renderFile($_viewFile_,$_data_=null,$_return_=false)
475: {
476: if(is_array($_data_))
477: extract($_data_,EXTR_PREFIX_SAME,'data');
478: else
479: $data=$_data_;
480: if($_return_)
481: {
482: ob_start();
483: ob_implicit_flush(false);
484: require($_viewFile_);
485: return ob_get_clean();
486: }
487: else
488: require($_viewFile_);
489: }
490:
491: /**
492: * Converts a word to its plural form.
493: * @param string $name the word to be pluralized
494: * @return string the pluralized word
495: */
496: public function pluralize($name)
497: {
498: $rules=array(
499: '/(m)ove$/i' => '\1oves',
500: '/(f)oot$/i' => '\1eet',
501: '/(c)hild$/i' => '\1hildren',
502: '/(h)uman$/i' => '\1umans',
503: '/(m)an$/i' => '\1en',
504: '/(s)taff$/i' => '\1taff',
505: '/(t)ooth$/i' => '\1eeth',
506: '/(p)erson$/i' => '\1eople',
507: '/([m|l])ouse$/i' => '\1ice',
508: '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es',
509: '/([^aeiouy]|qu)y$/i' => '\1ies',
510: '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
511: '/(shea|lea|loa|thie)f$/i' => '\1ves',
512: '/([ti])um$/i' => '\1a',
513: '/(tomat|potat|ech|her|vet)o$/i' => '\1oes',
514: '/(bu)s$/i' => '\1ses',
515: '/(ax|test)is$/i' => '\1es',
516: '/s$/' => 's',
517: );
518: foreach($rules as $rule=>$replacement)
519: {
520: if(preg_match($rule,$name))
521: return preg_replace($rule,$replacement,$name);
522: }
523: return $name.'s';
524: }
525:
526: /**
527: * Reads input via the readline PHP extension if that's available, or fgets() if readline is not installed.
528: *
529: * @param string $message to echo out before waiting for user input
530: * @param string $default the default string to be returned when user does not write anything.
531: * Defaults to null, means that default string is disabled. This parameter is available since version 1.1.11.
532: * @return mixed line read as a string, or false if input has been closed
533: *
534: * @since 1.1.9
535: */
536: public function prompt($message,$default=null)
537: {
538: if($default!==null)
539: $message.=" [$default] ";
540: else
541: $message.=' ';
542:
543: if(extension_loaded('readline'))
544: {
545: $input=readline($message);
546: if($input!==false)
547: readline_add_history($input);
548: }
549: else
550: {
551: echo $message;
552: $input=fgets(STDIN);
553: }
554:
555: if($input===false)
556: return false;
557: else{
558: $input=trim($input);
559: return ($input==='' && $default!==null) ? $default : $input;
560: }
561: }
562:
563: /**
564: * Asks user to confirm by typing y or n.
565: *
566: * @param string $message to echo out before waiting for user input
567: * @param boolean $default this value is returned if no selection is made. This parameter has been available since version 1.1.11.
568: * @return boolean whether user confirmed
569: *
570: * @since 1.1.9
571: */
572: public function confirm($message,$default=false)
573: {
574: echo $message.' (yes|no) [' . ($default ? 'yes' : 'no') . ']:';
575:
576: $input = trim(fgets(STDIN));
577: return empty($input) ? $default : !strncasecmp($input,'y',1);
578: }
579:
580: /**
581: * This event is raised before an action is to be executed.
582: * @param CConsoleCommandEvent $event the event parameter
583: * @since 1.1.11
584: */
585: public function onBeforeAction($event)
586: {
587: $this->raiseEvent('onBeforeAction',$event);
588: }
589:
590: /**
591: * This event is raised after an action finishes execution.
592: * @param CConsoleCommandEvent $event the event parameter
593: * @since 1.1.11
594: */
595: public function onAfterAction($event)
596: {
597: $this->raiseEvent('onAfterAction',$event);
598: }
599: }
600: