CommandTask.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 2.5.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Shell\Task;
  16. use Cake\Console\Shell;
  17. use Cake\Core\App;
  18. use Cake\Core\Plugin;
  19. use Cake\Filesystem\Folder;
  20. use Cake\Utility\Hash;
  21. use Cake\Utility\Inflector;
  22. use ReflectionClass;
  23. use ReflectionMethod;
  24. /**
  25. * Base class for Shell Command reflection.
  26. */
  27. class CommandTask extends Shell
  28. {
  29. /**
  30. * Gets the shell command listing.
  31. *
  32. * @return array
  33. */
  34. public function getShellList()
  35. {
  36. $skipFiles = ['app'];
  37. $hiddenCommands = ['command_list', 'completion'];
  38. $plugins = Plugin::loaded();
  39. $shellList = array_fill_keys($plugins, null) + ['CORE' => null, 'app' => null];
  40. $appPath = App::path('Shell');
  41. $shellList = $this->_findShells($shellList, $appPath[0], 'app', $skipFiles);
  42. $appPath = App::path('Command');
  43. $shellList = $this->_findShells($shellList, $appPath[0], 'app', $skipFiles);
  44. $skipCore = array_merge($skipFiles, $hiddenCommands, $shellList['app']);
  45. $corePath = dirname(__DIR__);
  46. $shellList = $this->_findShells($shellList, $corePath, 'CORE', $skipCore);
  47. $corePath = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'Command';
  48. $shellList = $this->_findShells($shellList, $corePath, 'CORE', $skipCore);
  49. foreach ($plugins as $plugin) {
  50. $pluginPath = Plugin::classPath($plugin) . 'Shell';
  51. $shellList = $this->_findShells($shellList, $pluginPath, $plugin, []);
  52. }
  53. return array_filter($shellList);
  54. }
  55. /**
  56. * Find shells in $path and add them to $shellList
  57. *
  58. * @param array $shellList The shell listing array.
  59. * @param string $path The path to look in.
  60. * @param string $key The key to add shells to
  61. * @param array $skip A list of commands to exclude.
  62. * @return array The updated list of shells.
  63. */
  64. protected function _findShells($shellList, $path, $key, $skip)
  65. {
  66. $shells = $this->_scanDir($path);
  67. return $this->_appendShells($key, $shells, $shellList, $skip);
  68. }
  69. /**
  70. * Scan the provided paths for shells, and append them into $shellList
  71. *
  72. * @param string $type The type of object.
  73. * @param array $shells The shell name.
  74. * @param array $shellList List of shells.
  75. * @param array $skip List of command names to skip.
  76. * @return array The updated $shellList
  77. */
  78. protected function _appendShells($type, $shells, $shellList, $skip)
  79. {
  80. if (!isset($shellList[$type])) {
  81. $shellList[$type] = [];
  82. }
  83. foreach ($shells as $shell) {
  84. $name = Inflector::underscore(preg_replace('/(Shell|Command)$/', '', $shell));
  85. if (!in_array($name, $skip, true)) {
  86. $shellList[$type][] = $name;
  87. }
  88. }
  89. sort($shellList[$type]);
  90. return $shellList;
  91. }
  92. /**
  93. * Scan a directory for .php files and return the class names that
  94. * should be within them.
  95. *
  96. * @param string $dir The directory to read.
  97. * @return array The list of shell classnames based on conventions.
  98. */
  99. protected function _scanDir($dir)
  100. {
  101. $dir = new Folder($dir);
  102. $contents = $dir->read(true, true);
  103. if (empty($contents[1])) {
  104. return [];
  105. }
  106. $shells = [];
  107. foreach ($contents[1] as $file) {
  108. if (substr($file, -4) !== '.php') {
  109. continue;
  110. }
  111. $shells[] = substr($file, 0, -4);
  112. }
  113. return $shells;
  114. }
  115. /**
  116. * Return a list of all commands
  117. *
  118. * @return array
  119. */
  120. public function commands()
  121. {
  122. $shellList = $this->getShellList();
  123. $flatten = Hash::flatten($shellList);
  124. $duplicates = array_intersect($flatten, array_unique(array_diff_key($flatten, array_unique($flatten))));
  125. $duplicates = Hash::expand($duplicates);
  126. $options = [];
  127. foreach ($shellList as $type => $commands) {
  128. foreach ($commands as $shell) {
  129. $prefix = '';
  130. if (!in_array(strtolower($type), ['app', 'core']) &&
  131. isset($duplicates[$type]) &&
  132. in_array($shell, $duplicates[$type])
  133. ) {
  134. $prefix = $type . '.';
  135. }
  136. $options[] = $prefix . $shell;
  137. }
  138. }
  139. return $options;
  140. }
  141. /**
  142. * Return a list of subcommands for a given command
  143. *
  144. * @param string $commandName The command you want subcommands from.
  145. * @return string[]
  146. * @throws \ReflectionException
  147. */
  148. public function subCommands($commandName)
  149. {
  150. $Shell = $this->getShell($commandName);
  151. if (!$Shell) {
  152. return [];
  153. }
  154. $taskMap = $this->Tasks->normalizeArray((array)$Shell->tasks);
  155. $return = array_keys($taskMap);
  156. $return = array_map('Cake\Utility\Inflector::underscore', $return);
  157. $shellMethodNames = ['main', 'help', 'getOptionParser', 'initialize', 'runCommand'];
  158. $baseClasses = ['Object', 'Shell', 'AppShell'];
  159. $Reflection = new ReflectionClass($Shell);
  160. $methods = $Reflection->getMethods(ReflectionMethod::IS_PUBLIC);
  161. $methodNames = [];
  162. foreach ($methods as $method) {
  163. $declaringClass = $method->getDeclaringClass()->getShortName();
  164. if (!in_array($declaringClass, $baseClasses)) {
  165. $methodNames[] = $method->getName();
  166. }
  167. }
  168. $return = array_merge($return, array_diff($methodNames, $shellMethodNames));
  169. sort($return);
  170. return $return;
  171. }
  172. /**
  173. * Get Shell instance for the given command
  174. *
  175. * @param string $commandName The command you want.
  176. * @return \Cake\Console\Shell|bool Shell instance if the command can be found, false otherwise.
  177. */
  178. public function getShell($commandName)
  179. {
  180. list($pluginDot, $name) = pluginSplit($commandName, true);
  181. if (in_array(strtolower($pluginDot), ['app.', 'core.'])) {
  182. $commandName = $name;
  183. $pluginDot = '';
  184. }
  185. if (!in_array($commandName, $this->commands()) && (empty($pluginDot) && !in_array($name, $this->commands()))) {
  186. return false;
  187. }
  188. if (empty($pluginDot)) {
  189. $shellList = $this->getShellList();
  190. if (!in_array($commandName, $shellList['app']) && !in_array($commandName, $shellList['CORE'])) {
  191. unset($shellList['CORE'], $shellList['app']);
  192. foreach ($shellList as $plugin => $commands) {
  193. if (in_array($commandName, $commands)) {
  194. $pluginDot = $plugin . '.';
  195. break;
  196. }
  197. }
  198. }
  199. }
  200. $name = Inflector::camelize($name);
  201. $pluginDot = Inflector::camelize($pluginDot);
  202. $class = App::className($pluginDot . $name, 'Shell', 'Shell');
  203. if (!$class) {
  204. return false;
  205. }
  206. /* @var \Cake\Console\Shell $Shell */
  207. $Shell = new $class();
  208. $Shell->plugin = trim($pluginDot, '.');
  209. $Shell->initialize();
  210. return $Shell;
  211. }
  212. /**
  213. * Get options list for the given command or subcommand
  214. *
  215. * @param string $commandName The command to get options for.
  216. * @param string $subCommandName The subcommand to get options for. Can be empty to get options for the command.
  217. * If this parameter is used, the subcommand must be a valid subcommand of the command passed
  218. * @return array Options list for the given command or subcommand
  219. */
  220. public function options($commandName, $subCommandName = '')
  221. {
  222. $Shell = $this->getShell($commandName);
  223. if (!$Shell) {
  224. return [];
  225. }
  226. $parser = $Shell->getOptionParser();
  227. if (!empty($subCommandName)) {
  228. $subCommandName = Inflector::camelize($subCommandName);
  229. if ($Shell->hasTask($subCommandName)) {
  230. $parser = $Shell->{$subCommandName}->getOptionParser();
  231. } else {
  232. return [];
  233. }
  234. }
  235. $options = [];
  236. $array = $parser->options();
  237. /* @var \Cake\Console\ConsoleInputOption $obj */
  238. foreach ($array as $name => $obj) {
  239. $options[] = "--$name";
  240. $short = $obj->short();
  241. if ($short) {
  242. $options[] = "-$short";
  243. }
  244. }
  245. return $options;
  246. }
  247. }