You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

864 lines
23 KiB

  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | TopThink [ WE CAN DO IT JUST THINK IT ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2015 http://www.topthink.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Author: zhangyajun <448901948@qq.com>
  8. // +----------------------------------------------------------------------
  9. namespace think;
  10. use think\console\Command;
  11. use think\console\command\Help as HelpCommand;
  12. use think\console\Input;
  13. use think\console\input\Argument as InputArgument;
  14. use think\console\input\Definition as InputDefinition;
  15. use think\console\input\Option as InputOption;
  16. use think\console\Output;
  17. use think\console\output\driver\Buffer;
  18. class Console
  19. {
  20. /**
  21. * @var string 命令名称
  22. */
  23. private $name;
  24. /**
  25. * @var string 命令版本
  26. */
  27. private $version;
  28. /**
  29. * @var Command[] 命令
  30. */
  31. private $commands = [];
  32. /**
  33. * @var bool 是否需要帮助信息
  34. */
  35. private $wantHelps = false;
  36. /**
  37. * @var bool 是否捕获异常
  38. */
  39. private $catchExceptions = true;
  40. /**
  41. * @var bool 是否自动退出执行
  42. */
  43. private $autoExit = true;
  44. /**
  45. * @var InputDefinition 输入定义
  46. */
  47. private $definition;
  48. /**
  49. * @var string 默认执行的命令
  50. */
  51. private $defaultCommand;
  52. /**
  53. * @var array 默认提供的命令
  54. */
  55. private static $defaultCommands = [
  56. "think\\console\\command\\Help",
  57. "think\\console\\command\\Lists",
  58. "think\\console\\command\\Build",
  59. "think\\console\\command\\Clear",
  60. "think\\console\\command\\make\\Controller",
  61. "think\\console\\command\\make\\Model",
  62. "think\\console\\command\\optimize\\Autoload",
  63. "think\\console\\command\\optimize\\Config",
  64. "think\\console\\command\\optimize\\Route",
  65. "think\\console\\command\\optimize\\Schema",
  66. ];
  67. /**
  68. * Console constructor.
  69. * @access public
  70. * @param string $name 名称
  71. * @param string $version 版本
  72. * @param null|string $user 执行用户
  73. */
  74. public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN', $user = null)
  75. {
  76. $this->name = $name;
  77. $this->version = $version;
  78. if ($user) {
  79. $this->setUser($user);
  80. }
  81. $this->defaultCommand = 'list';
  82. $this->definition = $this->getDefaultInputDefinition();
  83. foreach ($this->getDefaultCommands() as $command) {
  84. $this->add($command);
  85. }
  86. }
  87. /**
  88. * 设置执行用户
  89. * @param $user
  90. */
  91. public function setUser($user)
  92. {
  93. $user = posix_getpwnam($user);
  94. if ($user) {
  95. posix_setuid($user['uid']);
  96. posix_setgid($user['gid']);
  97. }
  98. }
  99. /**
  100. * 初始化 Console
  101. * @access public
  102. * @param bool $run 是否运行 Console
  103. * @return int|Console
  104. */
  105. public static function init($run = true)
  106. {
  107. static $console;
  108. if (!$console) {
  109. $config = Config::get('console');
  110. // 实例化 console
  111. $console = new self($config['name'], $config['version'], $config['user']);
  112. // 读取指令集
  113. if (is_file(CONF_PATH . 'command' . EXT)) {
  114. $commands = include CONF_PATH . 'command' . EXT;
  115. if (is_array($commands)) {
  116. foreach ($commands as $command) {
  117. class_exists($command) &&
  118. is_subclass_of($command, "\\think\\console\\Command") &&
  119. $console->add(new $command()); // 注册指令
  120. }
  121. }
  122. }
  123. }
  124. return $run ? $console->run() : $console;
  125. }
  126. /**
  127. * 调用命令
  128. * @access public
  129. * @param string $command
  130. * @param array $parameters
  131. * @param string $driver
  132. * @return Output
  133. */
  134. public static function call($command, array $parameters = [], $driver = 'buffer')
  135. {
  136. $console = self::init(false);
  137. array_unshift($parameters, $command);
  138. $input = new Input($parameters);
  139. $output = new Output($driver);
  140. $console->setCatchExceptions(false);
  141. $console->find($command)->run($input, $output);
  142. return $output;
  143. }
  144. /**
  145. * 执行当前的指令
  146. * @access public
  147. * @return int
  148. * @throws \Exception
  149. */
  150. public function run()
  151. {
  152. $input = new Input();
  153. $output = new Output();
  154. $this->configureIO($input, $output);
  155. try {
  156. $exitCode = $this->doRun($input, $output);
  157. } catch (\Exception $e) {
  158. if (!$this->catchExceptions) throw $e;
  159. $output->renderException($e);
  160. $exitCode = $e->getCode();
  161. if (is_numeric($exitCode)) {
  162. $exitCode = ((int) $exitCode) ?: 1;
  163. } else {
  164. $exitCode = 1;
  165. }
  166. }
  167. if ($this->autoExit) {
  168. if ($exitCode > 255) $exitCode = 255;
  169. exit($exitCode);
  170. }
  171. return $exitCode;
  172. }
  173. /**
  174. * 执行指令
  175. * @access public
  176. * @param Input $input 输入
  177. * @param Output $output 输出
  178. * @return int
  179. */
  180. public function doRun(Input $input, Output $output)
  181. {
  182. // 获取版本信息
  183. if (true === $input->hasParameterOption(['--version', '-V'])) {
  184. $output->writeln($this->getLongVersion());
  185. return 0;
  186. }
  187. $name = $this->getCommandName($input);
  188. // 获取帮助信息
  189. if (true === $input->hasParameterOption(['--help', '-h'])) {
  190. if (!$name) {
  191. $name = 'help';
  192. $input = new Input(['help']);
  193. } else {
  194. $this->wantHelps = true;
  195. }
  196. }
  197. if (!$name) {
  198. $name = $this->defaultCommand;
  199. $input = new Input([$this->defaultCommand]);
  200. }
  201. return $this->doRunCommand($this->find($name), $input, $output);
  202. }
  203. /**
  204. * 设置输入参数定义
  205. * @access public
  206. * @param InputDefinition $definition 输入定义
  207. * @return $this;
  208. */
  209. public function setDefinition(InputDefinition $definition)
  210. {
  211. $this->definition = $definition;
  212. return $this;
  213. }
  214. /**
  215. * 获取输入参数定义
  216. * @access public
  217. * @return InputDefinition
  218. */
  219. public function getDefinition()
  220. {
  221. return $this->definition;
  222. }
  223. /**
  224. * 获取帮助信息
  225. * @access public
  226. * @return string
  227. */
  228. public function getHelp()
  229. {
  230. return $this->getLongVersion();
  231. }
  232. /**
  233. * 设置是否捕获异常
  234. * @access public
  235. * @param bool $boolean 是否捕获
  236. * @return $this
  237. */
  238. public function setCatchExceptions($boolean)
  239. {
  240. $this->catchExceptions = (bool) $boolean;
  241. return $this;
  242. }
  243. /**
  244. * 设置是否自动退出
  245. * @access public
  246. * @param bool $boolean 是否自动退出
  247. * @return $this
  248. */
  249. public function setAutoExit($boolean)
  250. {
  251. $this->autoExit = (bool) $boolean;
  252. return $this;
  253. }
  254. /**
  255. * 获取名称
  256. * @access public
  257. * @return string
  258. */
  259. public function getName()
  260. {
  261. return $this->name;
  262. }
  263. /**
  264. * 设置名称
  265. * @access public
  266. * @param string $name 名称
  267. * @return $this
  268. */
  269. public function setName($name)
  270. {
  271. $this->name = $name;
  272. return $this;
  273. }
  274. /**
  275. * 获取版本
  276. * @access public
  277. * @return string
  278. */
  279. public function getVersion()
  280. {
  281. return $this->version;
  282. }
  283. /**
  284. * 设置版本
  285. * @access public
  286. * @param string $version 版本信息
  287. * @return $this
  288. */
  289. public function setVersion($version)
  290. {
  291. $this->version = $version;
  292. return $this;
  293. }
  294. /**
  295. * 获取完整的版本号
  296. * @access public
  297. * @return string
  298. */
  299. public function getLongVersion()
  300. {
  301. if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) {
  302. return sprintf(
  303. '<info>%s</info> version <comment>%s</comment>',
  304. $this->getName(),
  305. $this->getVersion()
  306. );
  307. }
  308. return '<info>Console Tool</info>';
  309. }
  310. /**
  311. * 注册一个指令
  312. * @access public
  313. * @param string $name 指令名称
  314. * @return Command
  315. */
  316. public function register($name)
  317. {
  318. return $this->add(new Command($name));
  319. }
  320. /**
  321. * 批量添加指令
  322. * @access public
  323. * @param Command[] $commands 指令实例
  324. * @return $this
  325. */
  326. public function addCommands(array $commands)
  327. {
  328. foreach ($commands as $command) $this->add($command);
  329. return $this;
  330. }
  331. /**
  332. * 添加一个指令
  333. * @access public
  334. * @param Command $command 命令实例
  335. * @return Command|bool
  336. */
  337. public function add(Command $command)
  338. {
  339. if (!$command->isEnabled()) {
  340. $command->setConsole(null);
  341. return false;
  342. }
  343. $command->setConsole($this);
  344. if (null === $command->getDefinition()) {
  345. throw new \LogicException(
  346. sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))
  347. );
  348. }
  349. $this->commands[$command->getName()] = $command;
  350. foreach ($command->getAliases() as $alias) {
  351. $this->commands[$alias] = $command;
  352. }
  353. return $command;
  354. }
  355. /**
  356. * 获取指令
  357. * @access public
  358. * @param string $name 指令名称
  359. * @return Command
  360. * @throws \InvalidArgumentException
  361. */
  362. public function get($name)
  363. {
  364. if (!isset($this->commands[$name])) {
  365. throw new \InvalidArgumentException(
  366. sprintf('The command "%s" does not exist.', $name)
  367. );
  368. }
  369. $command = $this->commands[$name];
  370. if ($this->wantHelps) {
  371. $this->wantHelps = false;
  372. /** @var HelpCommand $helpCommand */
  373. $helpCommand = $this->get('help');
  374. $helpCommand->setCommand($command);
  375. return $helpCommand;
  376. }
  377. return $command;
  378. }
  379. /**
  380. * 某个指令是否存在
  381. * @access public
  382. * @param string $name 指令名称
  383. * @return bool
  384. */
  385. public function has($name)
  386. {
  387. return isset($this->commands[$name]);
  388. }
  389. /**
  390. * 获取所有的命名空间
  391. * @access public
  392. * @return array
  393. */
  394. public function getNamespaces()
  395. {
  396. $namespaces = [];
  397. foreach ($this->commands as $command) {
  398. $namespaces = array_merge(
  399. $namespaces, $this->extractAllNamespaces($command->getName())
  400. );
  401. foreach ($command->getAliases() as $alias) {
  402. $namespaces = array_merge(
  403. $namespaces, $this->extractAllNamespaces($alias)
  404. );
  405. }
  406. }
  407. return array_values(array_unique(array_filter($namespaces)));
  408. }
  409. /**
  410. * 查找注册命名空间中的名称或缩写
  411. * @access public
  412. * @param string $namespace
  413. * @return string
  414. * @throws \InvalidArgumentException
  415. */
  416. public function findNamespace($namespace)
  417. {
  418. $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
  419. return preg_quote($matches[1]) . '[^:]*';
  420. }, $namespace);
  421. $allNamespaces = $this->getNamespaces();
  422. $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces);
  423. if (empty($namespaces)) {
  424. $message = sprintf(
  425. 'There are no commands defined in the "%s" namespace.', $namespace
  426. );
  427. if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
  428. if (1 == count($alternatives)) {
  429. $message .= "\n\nDid you mean this?\n ";
  430. } else {
  431. $message .= "\n\nDid you mean one of these?\n ";
  432. }
  433. $message .= implode("\n ", $alternatives);
  434. }
  435. throw new \InvalidArgumentException($message);
  436. }
  437. $exact = in_array($namespace, $namespaces, true);
  438. if (count($namespaces) > 1 && !$exact) {
  439. throw new \InvalidArgumentException(
  440. sprintf(
  441. 'The namespace "%s" is ambiguous (%s).',
  442. $namespace,
  443. $this->getAbbreviationSuggestions(array_values($namespaces)))
  444. );
  445. }
  446. return $exact ? $namespace : reset($namespaces);
  447. }
  448. /**
  449. * 查找指令
  450. * @access public
  451. * @param string $name 名称或者别名
  452. * @return Command
  453. * @throws \InvalidArgumentException
  454. */
  455. public function find($name)
  456. {
  457. $expr = preg_replace_callback('{([^:]+|)}', function ($matches) {
  458. return preg_quote($matches[1]) . '[^:]*';
  459. }, $name);
  460. $allCommands = array_keys($this->commands);
  461. $commands = preg_grep('{^' . $expr . '}', $allCommands);
  462. if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) {
  463. if (false !== ($pos = strrpos($name, ':'))) {
  464. $this->findNamespace(substr($name, 0, $pos));
  465. }
  466. $message = sprintf('Command "%s" is not defined.', $name);
  467. if ($alternatives = $this->findAlternatives($name, $allCommands)) {
  468. if (1 == count($alternatives)) {
  469. $message .= "\n\nDid you mean this?\n ";
  470. } else {
  471. $message .= "\n\nDid you mean one of these?\n ";
  472. }
  473. $message .= implode("\n ", $alternatives);
  474. }
  475. throw new \InvalidArgumentException($message);
  476. }
  477. if (count($commands) > 1) {
  478. $commandList = $this->commands;
  479. $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) {
  480. $commandName = $commandList[$nameOrAlias]->getName();
  481. return $commandName === $nameOrAlias || !in_array($commandName, $commands);
  482. });
  483. }
  484. $exact = in_array($name, $commands, true);
  485. if (count($commands) > 1 && !$exact) {
  486. $suggestions = $this->getAbbreviationSuggestions(array_values($commands));
  487. throw new \InvalidArgumentException(
  488. sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)
  489. );
  490. }
  491. return $this->get($exact ? $name : reset($commands));
  492. }
  493. /**
  494. * 获取所有的指令
  495. * @access public
  496. * @param string $namespace 命名空间
  497. * @return Command[]
  498. */
  499. public function all($namespace = null)
  500. {
  501. if (null === $namespace) return $this->commands;
  502. $commands = [];
  503. foreach ($this->commands as $name => $command) {
  504. $ext = $this->extractNamespace($name, substr_count($namespace, ':') + 1);
  505. if ($ext === $namespace) $commands[$name] = $command;
  506. }
  507. return $commands;
  508. }
  509. /**
  510. * 获取可能的指令名
  511. * @access public
  512. * @param array $names 指令名
  513. * @return array
  514. */
  515. public static function getAbbreviations($names)
  516. {
  517. $abbrevs = [];
  518. foreach ($names as $name) {
  519. for ($len = strlen($name); $len > 0; --$len) {
  520. $abbrev = substr($name, 0, $len);
  521. $abbrevs[$abbrev][] = $name;
  522. }
  523. }
  524. return $abbrevs;
  525. }
  526. /**
  527. * 配置基于用户的参数和选项的输入和输出实例
  528. * @access protected
  529. * @param Input $input 输入实例
  530. * @param Output $output 输出实例
  531. * @return void
  532. */
  533. protected function configureIO(Input $input, Output $output)
  534. {
  535. if (true === $input->hasParameterOption(['--ansi'])) {
  536. $output->setDecorated(true);
  537. } elseif (true === $input->hasParameterOption(['--no-ansi'])) {
  538. $output->setDecorated(false);
  539. }
  540. if (true === $input->hasParameterOption(['--no-interaction', '-n'])) {
  541. $input->setInteractive(false);
  542. }
  543. if (true === $input->hasParameterOption(['--quiet', '-q'])) {
  544. $output->setVerbosity(Output::VERBOSITY_QUIET);
  545. } else {
  546. if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) {
  547. $output->setVerbosity(Output::VERBOSITY_DEBUG);
  548. } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) {
  549. $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE);
  550. } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {
  551. $output->setVerbosity(Output::VERBOSITY_VERBOSE);
  552. }
  553. }
  554. }
  555. /**
  556. * 执行指令
  557. * @access protected
  558. * @param Command $command 指令实例
  559. * @param Input $input 输入实例
  560. * @param Output $output 输出实例
  561. * @return int
  562. * @throws \Exception
  563. */
  564. protected function doRunCommand(Command $command, Input $input, Output $output)
  565. {
  566. return $command->run($input, $output);
  567. }
  568. /**
  569. * 获取指令的名称
  570. * @access protected
  571. * @param Input $input 输入实例
  572. * @return string
  573. */
  574. protected function getCommandName(Input $input)
  575. {
  576. return $input->getFirstArgument();
  577. }
  578. /**
  579. * 获取默认输入定义
  580. * @access protected
  581. * @return InputDefinition
  582. */
  583. protected function getDefaultInputDefinition()
  584. {
  585. return new InputDefinition([
  586. new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
  587. new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
  588. new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'),
  589. new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
  590. new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
  591. new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
  592. new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
  593. new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
  594. ]);
  595. }
  596. /**
  597. * 获取默认命令
  598. * @access protected
  599. * @return Command[]
  600. */
  601. protected function getDefaultCommands()
  602. {
  603. $defaultCommands = [];
  604. foreach (self::$defaultCommands as $class) {
  605. if (class_exists($class) && is_subclass_of($class, "think\\console\\Command")) {
  606. $defaultCommands[] = new $class();
  607. }
  608. }
  609. return $defaultCommands;
  610. }
  611. /**
  612. * 添加默认指令
  613. * @access public
  614. * @param array $classes 指令
  615. * @return void
  616. */
  617. public static function addDefaultCommands(array $classes)
  618. {
  619. self::$defaultCommands = array_merge(self::$defaultCommands, $classes);
  620. }
  621. /**
  622. * 获取可能的建议
  623. * @access private
  624. * @param array $abbrevs
  625. * @return string
  626. */
  627. private function getAbbreviationSuggestions($abbrevs)
  628. {
  629. return sprintf(
  630. '%s, %s%s',
  631. $abbrevs[0],
  632. $abbrevs[1],
  633. count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''
  634. );
  635. }
  636. /**
  637. * 返回指令的命名空间部分
  638. * @access public
  639. * @param string $name 指令名称
  640. * @param string $limit 部分的命名空间的最大数量
  641. * @return string
  642. */
  643. public function extractNamespace($name, $limit = null)
  644. {
  645. $parts = explode(':', $name);
  646. array_pop($parts);
  647. return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit));
  648. }
  649. /**
  650. * 查找可替代的建议
  651. * @access private
  652. * @param string $name 指令名称
  653. * @param array|\Traversable $collection 建议集合
  654. * @return array
  655. */
  656. private function findAlternatives($name, $collection)
  657. {
  658. $threshold = 1e3;
  659. $alternatives = [];
  660. $collectionParts = [];
  661. foreach ($collection as $item) {
  662. $collectionParts[$item] = explode(':', $item);
  663. }
  664. foreach (explode(':', $name) as $i => $subname) {
  665. foreach ($collectionParts as $collectionName => $parts) {
  666. $exists = isset($alternatives[$collectionName]);
  667. if (!isset($parts[$i]) && $exists) {
  668. $alternatives[$collectionName] += $threshold;
  669. continue;
  670. } elseif (!isset($parts[$i])) {
  671. continue;
  672. }
  673. $lev = levenshtein($subname, $parts[$i]);
  674. if ($lev <= strlen($subname) / 3 ||
  675. '' !== $subname &&
  676. false !== strpos($parts[$i], $subname)
  677. ) {
  678. $alternatives[$collectionName] = $exists ?
  679. $alternatives[$collectionName] + $lev :
  680. $lev;
  681. } elseif ($exists) {
  682. $alternatives[$collectionName] += $threshold;
  683. }
  684. }
  685. }
  686. foreach ($collection as $item) {
  687. $lev = levenshtein($name, $item);
  688. if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
  689. $alternatives[$item] = isset($alternatives[$item]) ?
  690. $alternatives[$item] - $lev :
  691. $lev;
  692. }
  693. }
  694. $alternatives = array_filter($alternatives, function ($lev) use ($threshold) {
  695. return $lev < 2 * $threshold;
  696. });
  697. asort($alternatives);
  698. return array_keys($alternatives);
  699. }
  700. /**
  701. * 设置默认的指令
  702. * @access public
  703. * @param string $commandName 指令名称
  704. * @return $this
  705. */
  706. public function setDefaultCommand($commandName)
  707. {
  708. $this->defaultCommand = $commandName;
  709. return $this;
  710. }
  711. /**
  712. * 返回所有的命名空间
  713. * @access private
  714. * @param string $name 指令名称
  715. * @return array
  716. */
  717. private function extractAllNamespaces($name)
  718. {
  719. $namespaces = [];
  720. foreach (explode(':', $name, -1) as $part) {
  721. if (count($namespaces)) {
  722. $namespaces[] = end($namespaces) . ':' . $part;
  723. } else {
  724. $namespaces[] = $part;
  725. }
  726. }
  727. return $namespaces;
  728. }
  729. }