Cake/Console/Shell.php

1 <?php
2 /**
3 * Base class for Shells
4 *
5 * PHP 5
6 *
7 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
8 * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
9 *
10 * Licensed under The MIT License
11 * Redistributions of files must retain the above copyright notice.
12 *
13 * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
14 * @link http://cakephp.org CakePHP(tm) Project
15 * @since CakePHP(tm) v 1.2.0.5012
16 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
17 */
18  
19 App::uses('TaskCollection', 'Console');
20 App::uses('ConsoleOutput', 'Console');
21 App::uses('ConsoleInput', 'Console');
22 App::uses('ConsoleInputSubcommand', 'Console');
23 App::uses('ConsoleOptionParser', 'Console');
24 App::uses('File', 'Utility');
25  
26 /**
27 * Base class for command-line utilities for automating programmer chores.
28 *
29 * @package Cake.Console
30 */
31 class Shell extends Object {
32  
33 /**
34 * Output constants for making verbose and quiet shells.
35 */
36 const VERBOSE = 2;
37 const NORMAL = 1;
38 const QUIET = 0;
39  
40 /**
41 * An instance of ConsoleOptionParser that has been configured for this class.
42 *
43 * @var ConsoleOptionParser
44 */
45 public $OptionParser;
46  
47 /**
48 * If true, the script will ask for permission to perform actions.
49 *
50 * @var boolean
51 */
52 public $interactive = true;
53  
54 /**
55 * Contains command switches parsed from the command line.
56 *
57 * @var array
58 */
59 public $params = array();
60  
61 /**
62 * The command (method/task) that is being run.
63 *
64 * @var string
65 */
66 public $command;
67  
68 /**
69 * Contains arguments parsed from the command line.
70 *
71 * @var array
72 */
73 public $args = array();
74  
75 /**
76 * The name of the shell in camelized.
77 *
78 * @var string
79 */
80 public $name = null;
81  
82 /**
83 * The name of the plugin the shell belongs to.
84 * Is automatically set by ShellDispatcher when a shell is constructed.
85 *
86 * @var string
87 */
88 public $plugin = null;
89  
90 /**
91 * Contains tasks to load and instantiate
92 *
93 * @var array
94 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::$tasks
95 */
96 public $tasks = array();
97  
98 /**
99 * Contains the loaded tasks
100 *
101 * @var array
102 */
103 public $taskNames = array();
104  
105 /**
106 * Contains models to load and instantiate
107 *
108 * @var array
109 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::$uses
110 */
111 public $uses = array();
112  
113 /**
114 * Task Collection for the command, used to create Tasks.
115 *
116 * @var TaskCollection
117 */
118 public $Tasks;
119  
120 /**
121 * Normalized map of tasks.
122 *
123 * @var string
124 */
125 protected $_taskMap = array();
126  
127 /**
128 * stdout object.
129 *
130 * @var ConsoleOutput
131 */
132 public $stdout;
133  
134 /**
135 * stderr object.
136 *
137 * @var ConsoleOutput
138 */
139 public $stderr;
140  
141 /**
142 * stdin object
143 *
144 * @var ConsoleInput
145 */
146 public $stdin;
147  
148 /**
149 * Constructs this Shell instance.
150 *
151 * @param ConsoleOutput $stdout A ConsoleOutput object for stdout.
152 * @param ConsoleOutput $stderr A ConsoleOutput object for stderr.
153 * @param ConsoleInput $stdin A ConsoleInput object for stdin.
154 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell
155 */
156 public function __construct($stdout = null, $stderr = null, $stdin = null) {
157 if ($this->name == null) {
158 $this->name = Inflector::camelize(str_replace(array('Shell', 'Task'), '', get_class($this)));
159 }
160 $this->Tasks = new TaskCollection($this);
161  
162 $this->stdout = $stdout;
163 $this->stderr = $stderr;
164 $this->stdin = $stdin;
165 if ($this->stdout == null) {
166 $this->stdout = new ConsoleOutput('php://stdout');
167 }
168 if ($this->stderr == null) {
169 $this->stderr = new ConsoleOutput('php://stderr');
170 }
171 if ($this->stdin == null) {
172 $this->stdin = new ConsoleInput('php://stdin');
173 }
174  
175 $parent = get_parent_class($this);
176 if ($this->tasks !== null && $this->tasks !== false) {
177 $this->_mergeVars(array('tasks'), $parent, true);
178 }
179 if ($this->uses !== null && $this->uses !== false) {
180 $this->_mergeVars(array('uses'), $parent, false);
181 }
182 }
183  
184 /**
185 * Initializes the Shell
186 * acts as constructor for subclasses
187 * allows configuration of tasks prior to shell execution
188 *
189 * @return void
190 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::initialize
191 */
192 public function initialize() {
193 $this->_loadModels();
194 }
195  
196 /**
197 * Starts up the Shell and displays the welcome message.
198 * Allows for checking and configuring prior to command or main execution
199 *
200 * Override this method if you want to remove the welcome information,
201 * or otherwise modify the pre-command flow.
202 *
203 * @return void
204 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::startup
205 */
206 public function startup() {
207 $this->_welcome();
208 }
209  
210 /**
211 * Displays a header for the shell
212 *
213 * @return void
214 */
215 protected function _welcome() {
216 $this->out();
217 $this->out(__d('cake_console', '<info>Welcome to CakePHP %s Console</info>', 'v' . Configure::version()));
218 $this->hr();
219 $this->out(__d('cake_console', 'App : %s', APP_DIR));
220 $this->out(__d('cake_console', 'Path: %s', APP));
221 $this->hr();
222 }
223  
224 /**
225 * If $uses = true
226 * Loads AppModel file and constructs AppModel class
227 * makes $this->AppModel available to subclasses
228 * If public $uses is an array of models will load those models
229 *
230 * @return boolean
231 */
232 protected function _loadModels() {
233 if ($this->uses === null || $this->uses === false) {
234 return;
235 }
236 App::uses('ClassRegistry', 'Utility');
237  
238 if ($this->uses !== true && !empty($this->uses)) {
239 $uses = is_array($this->uses) ? $this->uses : array($this->uses);
240  
241 $modelClassName = $uses[0];
242 if (strpos($uses[0], '.') !== false) {
243 list($plugin, $modelClassName) = explode('.', $uses[0]);
244 }
245 $this->modelClass = $modelClassName;
246  
247 foreach ($uses as $modelClass) {
248 list($plugin, $modelClass) = pluginSplit($modelClass, true);
249 $this->{$modelClass} = ClassRegistry::init($plugin . $modelClass);
250 }
251 return true;
252 }
253 return false;
254 }
255  
256 /**
257 * Loads tasks defined in public $tasks
258 *
259 * @return boolean
260 */
261 public function loadTasks() {
262 if ($this->tasks === true || empty($this->tasks) || empty($this->Tasks)) {
263 return true;
264 }
265 $this->_taskMap = TaskCollection::normalizeObjectArray((array)$this->tasks);
266 foreach ($this->_taskMap as $task => $properties) {
267 $this->taskNames[] = $task;
268 }
269 return true;
270 }
271  
272 /**
273 * Check to see if this shell has a task with the provided name.
274 *
275 * @param string $task The task name to check.
276 * @return boolean Success
277 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hasTask
278 */
279 public function hasTask($task) {
280 return isset($this->_taskMap[Inflector::camelize($task)]);
281 }
282  
283 /**
284 * Check to see if this shell has a callable method by the given name.
285 *
286 * @param string $name The method name to check.
287 * @return boolean
288 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hasMethod
289 */
290 public function hasMethod($name) {
291 try {
292 $method = new ReflectionMethod($this, $name);
293 if (!$method->isPublic() || substr($name, 0, 1) === '_') {
294 return false;
295 }
296 if ($method->getDeclaringClass()->name == 'Shell') {
297 return false;
298 }
299 return true;
300 } catch (ReflectionException $e) {
301 return false;
302 }
303 }
304  
305 /**
306 * Dispatch a command to another Shell. Similar to Object::requestAction()
307 * but intended for running shells from other shells.
308 *
309 * ### Usage:
310 *
311 * With a string command:
312 *
313 * `return $this->dispatchShell('schema create DbAcl');`
314 *
315 * Avoid using this form if you have string arguments, with spaces in them.
316 * The dispatched will be invoked incorrectly. Only use this form for simple
317 * command dispatching.
318 *
319 * With an array command:
320 *
321 * `return $this->dispatchShell('schema', 'create', 'i18n', '--dry');`
322 *
323 * @return mixed The return of the other shell.
324 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::dispatchShell
325 */
326 public function dispatchShell() {
327 $args = func_get_args();
328 if (is_string($args[0]) && count($args) == 1) {
329 $args = explode(' ', $args[0]);
330 }
331  
332 $Dispatcher = new ShellDispatcher($args, false);
333 return $Dispatcher->dispatch();
334 }
335  
336 /**
337 * Runs the Shell with the provided argv.
338 *
339 * Delegates calls to Tasks and resolves methods inside the class. Commands are looked
340 * up with the following order:
341 *
342 * - Method on the shell.
343 * - Matching task name.
344 * - `main()` method.
345 *
346 * If a shell implements a `main()` method, all missing method calls will be sent to
347 * `main()` with the original method name in the argv.
348 *
349 * @param string $command The command name to run on this shell. If this argument is empty,
350 * and the shell has a `main()` method, that will be called instead.
351 * @param array $argv Array of arguments to run the shell with. This array should be missing the shell name.
352 * @return void
353 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::runCommand
354 */
355 public function runCommand($command, $argv) {
356 $isTask = $this->hasTask($command);
357 $isMethod = $this->hasMethod($command);
358 $isMain = $this->hasMethod('main');
359  
360 if ($isTask || $isMethod && $command !== 'execute') {
361 array_shift($argv);
362 }
363  
364 try {
365 $this->OptionParser = $this->getOptionParser();
366 list($this->params, $this->args) = $this->OptionParser->parse($argv, $command);
367 } catch (ConsoleException $e) {
368 $this->out($this->OptionParser->help($command));
369 return false;
370 }
371  
372 $this->command = $command;
373 if (!empty($this->params['help'])) {
374 return $this->_displayHelp($command);
375 }
376  
377 if (($isTask || $isMethod || $isMain) && $command !== 'execute') {
378 $this->startup();
379 }
380  
381 if ($isTask) {
382 $command = Inflector::camelize($command);
383 return $this->{$command}->runCommand('execute', $argv);
384 }
385 if ($isMethod) {
386 return $this->{$command}();
387 }
388 if ($isMain) {
389 return $this->main();
390 }
391 $this->out($this->OptionParser->help($command));
392 return false;
393 }
394  
395 /**
396 * Display the help in the correct format
397 *
398 * @param string $command
399 * @return void
400 */
401 protected function _displayHelp($command) {
402 $format = 'text';
403 if (!empty($this->args[0]) && $this->args[0] == 'xml') {
404 $format = 'xml';
405 $this->stdout->outputAs(ConsoleOutput::RAW);
406 } else {
407 $this->_welcome();
408 }
409 return $this->out($this->OptionParser->help($command, $format));
410 }
411  
412 /**
413 * Gets the option parser instance and configures it.
414 * By overriding this method you can configure the ConsoleOptionParser before returning it.
415 *
416 * @return ConsoleOptionParser
417 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::getOptionParser
418 */
419 public function getOptionParser() {
420 $name = ($this->plugin ? $this->plugin . '.' : '') . $this->name;
421 $parser = new ConsoleOptionParser($name);
422 return $parser;
423 }
424  
425 /**
426 * Overload get for lazy building of tasks
427 *
428 * @param string $name
429 * @return Shell Object of Task
430 */
431 public function __get($name) {
432 if (empty($this->{$name}) && in_array($name, $this->taskNames)) {
433 $properties = $this->_taskMap[$name];
434 $this->{$name} = $this->Tasks->load($properties['class'], $properties['settings']);
435 $this->{$name}->args =& $this->args;
436 $this->{$name}->params =& $this->params;
437 $this->{$name}->initialize();
438 $this->{$name}->loadTasks();
439 }
440 return $this->{$name};
441 }
442  
443 /**
444 * Prompts the user for input, and returns it.
445 *
446 * @param string $prompt Prompt text.
447 * @param mixed $options Array or string of options.
448 * @param string $default Default input value.
449 * @return mixed Either the default value, or the user-provided input.
450 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::in
451 */
452 public function in($prompt, $options = null, $default = null) {
453 if (!$this->interactive) {
454 return $default;
455 }
456 $originalOptions = $options;
457 $in = $this->_getInput($prompt, $originalOptions, $default);
458  
459 if ($options && is_string($options)) {
460 if (strpos($options, ',')) {
461 $options = explode(',', $options);
462 } elseif (strpos($options, '/')) {
463 $options = explode('/', $options);
464 } else {
465 $options = array($options);
466 }
467 }
468 if (is_array($options)) {
469 $options = array_merge(
470 array_map('strtolower', $options),
471 array_map('strtoupper', $options),
472 $options
473 );
474 while ($in === '' || !in_array($in, $options)) {
475 $in = $this->_getInput($prompt, $originalOptions, $default);
476 }
477 }
478 return $in;
479 }
480  
481 /**
482 * Prompts the user for input, and returns it.
483 *
484 * @param string $prompt Prompt text.
485 * @param mixed $options Array or string of options.
486 * @param string $default Default input value.
487 * @return Either the default value, or the user-provided input.
488 */
489 protected function _getInput($prompt, $options, $default) {
490 if (!is_array($options)) {
491 $printOptions = '';
492 } else {
493 $printOptions = '(' . implode('/', $options) . ')';
494 }
495  
496 if ($default === null) {
497 $this->stdout->write('<question>' . $prompt . '</question>' . " $printOptions \n" . '> ', 0);
498 } else {
499 $this->stdout->write('<question>' . $prompt . '</question>' . " $printOptions \n" . "[$default] > ", 0);
500 }
501 $result = $this->stdin->read();
502  
503 if ($result === false) {
504 $this->_stop(1);
505 }
506 $result = trim($result);
507  
508 if ($default !== null && ($result === '' || $result === null)) {
509 return $default;
510 }
511 return $result;
512 }
513  
514 /**
515 * Wrap a block of text.
516 * Allows you to set the width, and indenting on a block of text.
517 *
518 * ### Options
519 *
520 * - `width` The width to wrap to. Defaults to 72
521 * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true.
522 * - `indent` Indent the text with the string provided. Defaults to null.
523 *
524 * @param string $text Text the text to format.
525 * @param mixed $options Array of options to use, or an integer to wrap the text to.
526 * @return string Wrapped / indented text
527 * @see String::wrap()
528 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::wrapText
529 */
530 public function wrapText($text, $options = array()) {
531 return String::wrap($text, $options);
532 }
533  
534 /**
535 * Outputs a single or multiple messages to stdout. If no parameters
536 * are passed outputs just a newline.
537 *
538 * ### Output levels
539 *
540 * There are 3 built-in output level. Shell::QUIET, Shell::NORMAL, Shell::VERBOSE.
541 * The verbose and quiet output levels, map to the `verbose` and `quiet` output switches
542 * present in most shells. Using Shell::QUIET for a message means it will always display.
543 * While using Shell::VERBOSE means it will only display when verbose output is toggled.
544 *
545 * @param mixed $message A string or a an array of strings to output
546 * @param integer $newlines Number of newlines to append
547 * @param integer $level The message's output level, see above.
548 * @return integer|boolean Returns the number of bytes returned from writing to stdout.
549 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::out
550 */
551 public function out($message = null, $newlines = 1, $level = Shell::NORMAL) {
552 $currentLevel = Shell::NORMAL;
553 if (!empty($this->params['verbose'])) {
554 $currentLevel = Shell::VERBOSE;
555 }
556 if (!empty($this->params['quiet'])) {
557 $currentLevel = Shell::QUIET;
558 }
559 if ($level <= $currentLevel) {
560 return $this->stdout->write($message, $newlines);
561 }
562 return true;
563 }
564  
565 /**
566 * Outputs a single or multiple error messages to stderr. If no parameters
567 * are passed outputs just a newline.
568 *
569 * @param mixed $message A string or a an array of strings to output
570 * @param integer $newlines Number of newlines to append
571 * @return void
572 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::err
573 */
574 public function err($message = null, $newlines = 1) {
575 $this->stderr->write($message, $newlines);
576 }
577  
578 /**
579 * Returns a single or multiple linefeeds sequences.
580 *
581 * @param integer $multiplier Number of times the linefeed sequence should be repeated
582 * @return string
583 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::nl
584 */
585 public function nl($multiplier = 1) {
586 return str_repeat(ConsoleOutput::LF, $multiplier);
587 }
588  
589 /**
590 * Outputs a series of minus characters to the standard output, acts as a visual separator.
591 *
592 * @param integer $newlines Number of newlines to pre- and append
593 * @param integer $width Width of the line, defaults to 63
594 * @return void
595 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::hr
596 */
597 public function hr($newlines = 0, $width = 63) {
598 $this->out(null, $newlines);
599 $this->out(str_repeat('-', $width));
600 $this->out(null, $newlines);
601 }
602  
603 /**
604 * Displays a formatted error message
605 * and exits the application with status code 1
606 *
607 * @param string $title Title of the error
608 * @param string $message An optional error message
609 * @return void
610 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::error
611 */
612 public function error($title, $message = null) {
613 $this->err(__d('cake_console', '<error>Error:</error> %s', $title));
614  
615 if (!empty($message)) {
616 $this->err($message);
617 }
618 $this->_stop(1);
619 }
620  
621 /**
622 * Clear the console
623 *
624 * @return void
625 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::clear
626 */
627 public function clear() {
628 if (empty($this->params['noclear'])) {
629 if (DS === '/') {
630 passthru('clear');
631 } else {
632 passthru('cls');
633 }
634 }
635 }
636  
637 /**
638 * Creates a file at given path
639 *
640 * @param string $path Where to put the file.
641 * @param string $contents Content to put in the file.
642 * @return boolean Success
643 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::createFile
644 */
645 public function createFile($path, $contents) {
646 $path = str_replace(DS . DS, DS, $path);
647  
648 $this->out();
649  
650 if (is_file($path) && $this->interactive === true) {
651 $this->out(__d('cake_console', '<warning>File `%s` exists</warning>', $path));
652 $key = $this->in(__d('cake_console', 'Do you want to overwrite?'), array('y', 'n', 'q'), 'n');
653  
654 if (strtolower($key) == 'q') {
655 $this->out(__d('cake_console', '<error>Quitting</error>.'), 2);
656 $this->_stop();
657 } elseif (strtolower($key) != 'y') {
658 $this->out(__d('cake_console', 'Skip `%s`', $path), 2);
659 return false;
660 }
661 } else {
662 $this->out(__d('cake_console', 'Creating file %s', $path));
663 }
664  
665 $File = new File($path, true);
666 if ($File->exists() && $File->writable()) {
667 $data = $File->prepare($contents);
668 $File->write($data);
669 $this->out(__d('cake_console', '<success>Wrote</success> `%s`', $path));
670 return true;
671 } else {
672 $this->err(__d('cake_console', '<error>Could not write to `%s`</error>.', $path), 2);
673 return false;
674 }
675 }
676  
677 /**
678 * Action to create a Unit Test
679 *
680 * @return boolean Success
681 */
682 protected function _checkUnitTest() {
683 if (App::import('Vendor', 'phpunit', array('file' => 'PHPUnit' . DS . 'Autoload.php'))) {
684 return true;
685 }
686 if (@include 'PHPUnit' . DS . 'Autoload.php') {
687 return true;
688 }
689 $prompt = __d('cake_console', 'PHPUnit is not installed. Do you want to bake unit test files anyway?');
690 $unitTest = $this->in($prompt, array('y', 'n'), 'y');
691 $result = strtolower($unitTest) == 'y' || strtolower($unitTest) == 'yes';
692  
693 if ($result) {
694 $this->out();
695 $this->out(__d('cake_console', 'You can download PHPUnit from %s', 'http://phpunit.de'));
696 }
697 return $result;
698 }
699  
700 /**
701 * Makes absolute file path easier to read
702 *
703 * @param string $file Absolute file path
704 * @return string short path
705 * @link http://book.cakephp.org/2.0/en/console-and-shells.html#Shell::shortPath
706 */
707 public function shortPath($file) {
708 $shortPath = str_replace(ROOT, null, $file);
709 $shortPath = str_replace('..' . DS, '', $shortPath);
710 return str_replace(DS . DS, DS, $shortPath);
711 }
712  
713 /**
714 * Creates the proper controller path for the specified controller class name
715 *
716 * @param string $name Controller class name
717 * @return string Path to controller
718 */
719 protected function _controllerPath($name) {
720 return Inflector::underscore($name);
721 }
722  
723 /**
724 * Creates the proper controller plural name for the specified controller class name
725 *
726 * @param string $name Controller class name
727 * @return string Controller plural name
728 */
729 protected function _controllerName($name) {
730 return Inflector::pluralize(Inflector::camelize($name));
731 }
732  
733 /**
734 * Creates the proper model camelized name (singularized) for the specified name
735 *
736 * @param string $name Name
737 * @return string Camelized and singularized model name
738 */
739 protected function _modelName($name) {
740 return Inflector::camelize(Inflector::singularize($name));
741 }
742  
743 /**
744 * Creates the proper underscored model key for associations
745 *
746 * @param string $name Model class name
747 * @return string Singular model key
748 */
749 protected function _modelKey($name) {
750 return Inflector::underscore($name) . '_id';
751 }
752  
753 /**
754 * Creates the proper model name from a foreign key
755 *
756 * @param string $key Foreign key
757 * @return string Model name
758 */
759 protected function _modelNameFromKey($key) {
760 return Inflector::camelize(str_replace('_id', '', $key));
761 }
762  
763 /**
764 * creates the singular name for use in views.
765 *
766 * @param string $name
767 * @return string $name
768 */
769 protected function _singularName($name) {
770 return Inflector::variable(Inflector::singularize($name));
771 }
772  
773 /**
774 * Creates the plural name for views
775 *
776 * @param string $name Name to use
777 * @return string Plural name for views
778 */
779 protected function _pluralName($name) {
780 return Inflector::variable(Inflector::pluralize($name));
781 }
782  
783 /**
784 * Creates the singular human name used in views
785 *
786 * @param string $name Controller name
787 * @return string Singular human name
788 */
789 protected function _singularHumanName($name) {
790 return Inflector::humanize(Inflector::underscore(Inflector::singularize($name)));
791 }
792  
793 /**
794 * Creates the plural human name used in views
795 *
796 * @param string $name Controller name
797 * @return string Plural human name
798 */
799 protected function _pluralHumanName($name) {
800 return Inflector::humanize(Inflector::underscore($name));
801 }
802  
803 /**
804 * Find the correct path for a plugin. Scans $pluginPaths for the plugin you want.
805 *
806 * @param string $pluginName Name of the plugin you want ie. DebugKit
807 * @return string $path path to the correct plugin.
808 */
809 protected function _pluginPath($pluginName) {
810 if (CakePlugin::loaded($pluginName)) {
811 return CakePlugin::path($pluginName);
812 }
813 return current(App::path('plugins')) . $pluginName . DS;
814 }
815  
816 }
817  
818