CompletionCommand.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP Project
  13. * @since 2.5.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Command;
  17. use Cake\Console\Arguments;
  18. use Cake\Console\CommandCollection;
  19. use Cake\Console\CommandCollectionAwareInterface;
  20. use Cake\Console\ConsoleIo;
  21. use Cake\Console\ConsoleOptionParser;
  22. use Cake\Console\Shell;
  23. use Cake\Utility\Inflector;
  24. use ReflectionClass;
  25. use ReflectionMethod;
  26. /**
  27. * Provide command completion shells such as bash.
  28. */
  29. class CompletionCommand extends Command implements CommandCollectionAwareInterface
  30. {
  31. /**
  32. * @var \Cake\Console\CommandCollection
  33. */
  34. protected $commands;
  35. /**
  36. * Set the command collection used to get completion data on.
  37. *
  38. * @param \Cake\Console\CommandCollection $commands The command collection
  39. * @return void
  40. */
  41. public function setCommandCollection(CommandCollection $commands): void
  42. {
  43. $this->commands = $commands;
  44. }
  45. /**
  46. * Gets the option parser instance and configures it.
  47. *
  48. * @param \Cake\Console\ConsoleOptionParser $parser The parser to build
  49. * @return \Cake\Console\ConsoleOptionParser
  50. */
  51. public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
  52. {
  53. $modes = [
  54. 'commands' => 'Output a list of available commands',
  55. 'subcommands' => 'Output a list of available sub-commands for a command',
  56. 'options' => 'Output a list of available options for a command and possible subcommand.',
  57. 'fuzzy' => 'Does nothing. Only for backwards compatibility',
  58. ];
  59. $modeHelp = '';
  60. foreach ($modes as $key => $help) {
  61. $modeHelp .= "- <info>{$key}</info> {$help}\n";
  62. }
  63. $parser->setDescription(
  64. 'Used by shells like bash to autocomplete command name, options and arguments'
  65. )->addArgument('mode', [
  66. 'help' => 'The type of thing to get completion on.',
  67. 'required' => true,
  68. 'choices' => array_keys($modes),
  69. ])->addArgument('command', [
  70. 'help' => 'The command name to get information on.',
  71. 'required' => false,
  72. ])->addArgument('subcommand', [
  73. 'help' => 'The sub-command related to command to get information on.',
  74. 'required' => false,
  75. ])->setEpilog([
  76. 'The various modes allow you to get help information on commands and their arguments.',
  77. 'The available modes are:',
  78. '',
  79. $modeHelp,
  80. '',
  81. 'This command is not intended to be called manually, and should be invoked from a ' .
  82. 'terminal completion script.',
  83. ]);
  84. return $parser;
  85. }
  86. /**
  87. * Main function Prints out the list of commands.
  88. *
  89. * @param \Cake\Console\Arguments $args The command arguments.
  90. * @param \Cake\Console\ConsoleIo $io The console io
  91. * @return int
  92. */
  93. public function execute(Arguments $args, ConsoleIo $io): ?int
  94. {
  95. $mode = $args->getArgument('mode');
  96. switch ($mode) {
  97. case 'commands':
  98. return $this->getCommands($args, $io);
  99. case 'subcommands':
  100. return $this->getSubcommands($args, $io);
  101. case 'options':
  102. return $this->getOptions($args, $io);
  103. case 'fuzzy':
  104. return static::CODE_SUCCESS;
  105. default:
  106. $io->err('Invalid mode chosen.');
  107. }
  108. return static::CODE_SUCCESS;
  109. }
  110. /**
  111. * Get the list of defined commands.
  112. *
  113. * @param \Cake\Console\Arguments $args The command arguments.
  114. * @param \Cake\Console\ConsoleIo $io The console io
  115. * @return int
  116. */
  117. protected function getCommands(Arguments $args, ConsoleIo $io): int
  118. {
  119. $options = [];
  120. foreach ($this->commands as $key => $value) {
  121. $parts = explode(' ', $key);
  122. $options[] = $parts[0];
  123. }
  124. $options = array_unique($options);
  125. $io->out(implode(' ', $options));
  126. return static::CODE_SUCCESS;
  127. }
  128. /**
  129. * Get the list of defined sub-commands.
  130. *
  131. * @param \Cake\Console\Arguments $args The command arguments.
  132. * @param \Cake\Console\ConsoleIo $io The console io
  133. * @return int
  134. */
  135. protected function getSubcommands(Arguments $args, ConsoleIo $io): int
  136. {
  137. $name = $args->getArgument('command');
  138. if ($name === null || !strlen($name)) {
  139. return static::CODE_SUCCESS;
  140. }
  141. $options = [];
  142. foreach ($this->commands as $key => $value) {
  143. $parts = explode(' ', $key);
  144. if ($parts[0] !== $name) {
  145. continue;
  146. }
  147. // Space separate command name, collect
  148. // hits as subcommands
  149. if (count($parts) > 1) {
  150. $options[] = implode(' ', array_slice($parts, 1));
  151. continue;
  152. }
  153. // Handle class strings
  154. if (is_string($value)) {
  155. $reflection = new ReflectionClass($value);
  156. $value = $reflection->newInstance();
  157. }
  158. if ($value instanceof Shell) {
  159. $shellCommands = $this->shellSubcommands($value);
  160. $options = array_merge($options, $shellCommands);
  161. }
  162. }
  163. $options = array_unique($options);
  164. $io->out(implode(' ', $options));
  165. return static::CODE_SUCCESS;
  166. }
  167. /**
  168. * Reflect the subcommands names out of a shell.
  169. *
  170. * @param \Cake\Console\Shell $shell The shell to get commands for
  171. * @return string[] A list of commands
  172. */
  173. protected function shellSubcommands(Shell $shell): array
  174. {
  175. $shell->initialize();
  176. $shell->loadTasks();
  177. $optionParser = $shell->getOptionParser();
  178. $subcommands = $optionParser->subcommands();
  179. $output = array_keys($subcommands);
  180. // If there are no formal subcommands all methods
  181. // on a shell are 'subcommands'
  182. if (count($subcommands) === 0) {
  183. /** @psalm-suppress DeprecatedClass */
  184. $coreShellReflection = new ReflectionClass(Shell::class);
  185. $reflection = new ReflectionClass($shell);
  186. foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
  187. if (
  188. $shell->hasMethod($method->getName())
  189. && !$coreShellReflection->hasMethod($method->getName())
  190. ) {
  191. $output[] = $method->getName();
  192. }
  193. }
  194. }
  195. $taskNames = array_map('Cake\Utility\Inflector::underscore', $shell->taskNames);
  196. $output = array_merge($output, $taskNames);
  197. return array_unique($output);
  198. }
  199. /**
  200. * Get the options for a command or subcommand
  201. *
  202. * @param \Cake\Console\Arguments $args The command arguments.
  203. * @param \Cake\Console\ConsoleIo $io The console io
  204. * @return int
  205. */
  206. protected function getOptions(Arguments $args, ConsoleIo $io): ?int
  207. {
  208. $name = $args->getArgument('command');
  209. $subcommand = $args->getArgument('subcommand');
  210. $options = [];
  211. foreach ($this->commands as $key => $value) {
  212. $parts = explode(' ', $key);
  213. if ($parts[0] !== $name) {
  214. continue;
  215. }
  216. if ($subcommand && !isset($parts[1])) {
  217. continue;
  218. }
  219. if ($subcommand && isset($parts[1]) && $parts[1] !== $subcommand) {
  220. continue;
  221. }
  222. // Handle class strings
  223. if (is_string($value)) {
  224. $reflection = new ReflectionClass($value);
  225. $value = $reflection->newInstance();
  226. }
  227. $parser = null;
  228. if ($value instanceof Command) {
  229. $parser = $value->getOptionParser();
  230. }
  231. if ($value instanceof Shell) {
  232. $value->initialize();
  233. $value->loadTasks();
  234. $parser = $value->getOptionParser();
  235. $subcommand = Inflector::camelize((string)$subcommand);
  236. if ($subcommand && $value->hasTask($subcommand)) {
  237. $parser = $value->{$subcommand}->getOptionParser();
  238. }
  239. }
  240. if ($parser) {
  241. foreach ($parser->options() as $name => $option) {
  242. $options[] = "--$name";
  243. $short = $option->short();
  244. if ($short) {
  245. $options[] = "-$short";
  246. }
  247. }
  248. }
  249. }
  250. $options = array_unique($options);
  251. $io->out(implode(' ', $options));
  252. return static::CODE_SUCCESS;
  253. }
  254. }