Application.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console;
  11. use Symfony\Component\Console\Exception\ExceptionInterface;
  12. use Symfony\Component\Console\Formatter\OutputFormatter;
  13. use Symfony\Component\Console\Helper\DebugFormatterHelper;
  14. use Symfony\Component\Console\Helper\Helper;
  15. use Symfony\Component\Console\Helper\ProcessHelper;
  16. use Symfony\Component\Console\Helper\QuestionHelper;
  17. use Symfony\Component\Console\Input\InputInterface;
  18. use Symfony\Component\Console\Input\StreamableInputInterface;
  19. use Symfony\Component\Console\Input\ArgvInput;
  20. use Symfony\Component\Console\Input\ArrayInput;
  21. use Symfony\Component\Console\Input\InputDefinition;
  22. use Symfony\Component\Console\Input\InputOption;
  23. use Symfony\Component\Console\Input\InputArgument;
  24. use Symfony\Component\Console\Input\InputAwareInterface;
  25. use Symfony\Component\Console\Output\OutputInterface;
  26. use Symfony\Component\Console\Output\ConsoleOutput;
  27. use Symfony\Component\Console\Output\ConsoleOutputInterface;
  28. use Symfony\Component\Console\Command\Command;
  29. use Symfony\Component\Console\Command\HelpCommand;
  30. use Symfony\Component\Console\Command\ListCommand;
  31. use Symfony\Component\Console\Helper\HelperSet;
  32. use Symfony\Component\Console\Helper\FormatterHelper;
  33. use Symfony\Component\Console\Event\ConsoleCommandEvent;
  34. use Symfony\Component\Console\Event\ConsoleErrorEvent;
  35. use Symfony\Component\Console\Event\ConsoleExceptionEvent;
  36. use Symfony\Component\Console\Event\ConsoleTerminateEvent;
  37. use Symfony\Component\Console\Exception\CommandNotFoundException;
  38. use Symfony\Component\Console\Exception\LogicException;
  39. use Symfony\Component\Debug\Exception\FatalThrowableError;
  40. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  41. /**
  42. * An Application is the container for a collection of commands.
  43. *
  44. * It is the main entry point of a Console application.
  45. *
  46. * This class is optimized for a standard CLI environment.
  47. *
  48. * Usage:
  49. *
  50. * $app = new Application('myapp', '1.0 (stable)');
  51. * $app->add(new SimpleCommand());
  52. * $app->run();
  53. *
  54. * @author Fabien Potencier <fabien@symfony.com>
  55. */
  56. class Application
  57. {
  58. private $commands = array();
  59. private $wantHelps = false;
  60. private $runningCommand;
  61. private $name;
  62. private $version;
  63. private $catchExceptions = true;
  64. private $autoExit = true;
  65. private $definition;
  66. private $helperSet;
  67. private $dispatcher;
  68. private $terminal;
  69. private $defaultCommand;
  70. private $singleCommand;
  71. private $initialized;
  72. /**
  73. * @param string $name The name of the application
  74. * @param string $version The version of the application
  75. */
  76. public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
  77. {
  78. $this->name = $name;
  79. $this->version = $version;
  80. $this->terminal = new Terminal();
  81. $this->defaultCommand = 'list';
  82. }
  83. public function setDispatcher(EventDispatcherInterface $dispatcher)
  84. {
  85. $this->dispatcher = $dispatcher;
  86. }
  87. /**
  88. * Runs the current application.
  89. *
  90. * @param InputInterface $input An Input instance
  91. * @param OutputInterface $output An Output instance
  92. *
  93. * @return int 0 if everything went fine, or an error code
  94. *
  95. * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
  96. */
  97. public function run(InputInterface $input = null, OutputInterface $output = null)
  98. {
  99. putenv('LINES='.$this->terminal->getHeight());
  100. putenv('COLUMNS='.$this->terminal->getWidth());
  101. if (null === $input) {
  102. $input = new ArgvInput();
  103. }
  104. if (null === $output) {
  105. $output = new ConsoleOutput();
  106. }
  107. if (null !== $this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) {
  108. @trigger_error(sprintf('The "ConsoleEvents::EXCEPTION" event is deprecated since Symfony 3.3 and will be removed in 4.0. Listen to the "ConsoleEvents::ERROR" event instead.'), E_USER_DEPRECATED);
  109. }
  110. $this->configureIO($input, $output);
  111. try {
  112. $e = null;
  113. $exitCode = $this->doRun($input, $output);
  114. } catch (\Exception $x) {
  115. $e = $x;
  116. } catch (\Throwable $x) {
  117. $e = new FatalThrowableError($x);
  118. }
  119. if (null !== $e) {
  120. if (!$this->catchExceptions || !$x instanceof \Exception) {
  121. throw $x;
  122. }
  123. if ($output instanceof ConsoleOutputInterface) {
  124. $this->renderException($e, $output->getErrorOutput());
  125. } else {
  126. $this->renderException($e, $output);
  127. }
  128. $exitCode = $e->getCode();
  129. if (is_numeric($exitCode)) {
  130. $exitCode = (int) $exitCode;
  131. if (0 === $exitCode) {
  132. $exitCode = 1;
  133. }
  134. } else {
  135. $exitCode = 1;
  136. }
  137. }
  138. if ($this->autoExit) {
  139. if ($exitCode > 255) {
  140. $exitCode = 255;
  141. }
  142. exit($exitCode);
  143. }
  144. return $exitCode;
  145. }
  146. /**
  147. * Runs the current application.
  148. *
  149. * @param InputInterface $input An Input instance
  150. * @param OutputInterface $output An Output instance
  151. *
  152. * @return int 0 if everything went fine, or an error code
  153. */
  154. public function doRun(InputInterface $input, OutputInterface $output)
  155. {
  156. if (true === $input->hasParameterOption(array('--version', '-V'), true)) {
  157. $output->writeln($this->getLongVersion());
  158. return 0;
  159. }
  160. $name = $this->getCommandName($input);
  161. if (true === $input->hasParameterOption(array('--help', '-h'), true)) {
  162. if (!$name) {
  163. $name = 'help';
  164. $input = new ArrayInput(array('command_name' => $this->defaultCommand));
  165. } else {
  166. $this->wantHelps = true;
  167. }
  168. }
  169. if (!$name) {
  170. $name = $this->defaultCommand;
  171. $definition = $this->getDefinition();
  172. $definition->setArguments(array_merge(
  173. $definition->getArguments(),
  174. array(
  175. 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
  176. )
  177. ));
  178. }
  179. try {
  180. $e = $this->runningCommand = null;
  181. // the command name MUST be the first element of the input
  182. $command = $this->find($name);
  183. } catch (\Exception $e) {
  184. } catch (\Throwable $e) {
  185. }
  186. if (null !== $e) {
  187. if (null !== $this->dispatcher) {
  188. $event = new ConsoleErrorEvent($input, $output, $e);
  189. $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
  190. $e = $event->getError();
  191. if (0 === $event->getExitCode()) {
  192. return 0;
  193. }
  194. }
  195. throw $e;
  196. }
  197. $this->runningCommand = $command;
  198. $exitCode = $this->doRunCommand($command, $input, $output);
  199. $this->runningCommand = null;
  200. return $exitCode;
  201. }
  202. /**
  203. * Set a helper set to be used with the command.
  204. *
  205. * @param HelperSet $helperSet The helper set
  206. */
  207. public function setHelperSet(HelperSet $helperSet)
  208. {
  209. $this->helperSet = $helperSet;
  210. }
  211. /**
  212. * Get the helper set associated with the command.
  213. *
  214. * @return HelperSet The HelperSet instance associated with this command
  215. */
  216. public function getHelperSet()
  217. {
  218. if (!$this->helperSet) {
  219. $this->helperSet = $this->getDefaultHelperSet();
  220. }
  221. return $this->helperSet;
  222. }
  223. /**
  224. * Set an input definition to be used with this application.
  225. *
  226. * @param InputDefinition $definition The input definition
  227. */
  228. public function setDefinition(InputDefinition $definition)
  229. {
  230. $this->definition = $definition;
  231. }
  232. /**
  233. * Gets the InputDefinition related to this Application.
  234. *
  235. * @return InputDefinition The InputDefinition instance
  236. */
  237. public function getDefinition()
  238. {
  239. if (!$this->definition) {
  240. $this->definition = $this->getDefaultInputDefinition();
  241. }
  242. if ($this->singleCommand) {
  243. $inputDefinition = $this->definition;
  244. $inputDefinition->setArguments();
  245. return $inputDefinition;
  246. }
  247. return $this->definition;
  248. }
  249. /**
  250. * Gets the help message.
  251. *
  252. * @return string A help message
  253. */
  254. public function getHelp()
  255. {
  256. return $this->getLongVersion();
  257. }
  258. /**
  259. * Gets whether to catch exceptions or not during commands execution.
  260. *
  261. * @return bool Whether to catch exceptions or not during commands execution
  262. */
  263. public function areExceptionsCaught()
  264. {
  265. return $this->catchExceptions;
  266. }
  267. /**
  268. * Sets whether to catch exceptions or not during commands execution.
  269. *
  270. * @param bool $boolean Whether to catch exceptions or not during commands execution
  271. */
  272. public function setCatchExceptions($boolean)
  273. {
  274. $this->catchExceptions = (bool) $boolean;
  275. }
  276. /**
  277. * Gets whether to automatically exit after a command execution or not.
  278. *
  279. * @return bool Whether to automatically exit after a command execution or not
  280. */
  281. public function isAutoExitEnabled()
  282. {
  283. return $this->autoExit;
  284. }
  285. /**
  286. * Sets whether to automatically exit after a command execution or not.
  287. *
  288. * @param bool $boolean Whether to automatically exit after a command execution or not
  289. */
  290. public function setAutoExit($boolean)
  291. {
  292. $this->autoExit = (bool) $boolean;
  293. }
  294. /**
  295. * Gets the name of the application.
  296. *
  297. * @return string The application name
  298. */
  299. public function getName()
  300. {
  301. return $this->name;
  302. }
  303. /**
  304. * Sets the application name.
  305. *
  306. * @param string $name The application name
  307. */
  308. public function setName($name)
  309. {
  310. $this->name = $name;
  311. }
  312. /**
  313. * Gets the application version.
  314. *
  315. * @return string The application version
  316. */
  317. public function getVersion()
  318. {
  319. return $this->version;
  320. }
  321. /**
  322. * Sets the application version.
  323. *
  324. * @param string $version The application version
  325. */
  326. public function setVersion($version)
  327. {
  328. $this->version = $version;
  329. }
  330. /**
  331. * Returns the long version of the application.
  332. *
  333. * @return string The long application version
  334. */
  335. public function getLongVersion()
  336. {
  337. if ('UNKNOWN' !== $this->getName()) {
  338. if ('UNKNOWN' !== $this->getVersion()) {
  339. return sprintf('%s <info>%s</info>', $this->getName(), $this->getVersion());
  340. }
  341. return $this->getName();
  342. }
  343. return 'Console Tool';
  344. }
  345. /**
  346. * Registers a new command.
  347. *
  348. * @param string $name The command name
  349. *
  350. * @return Command The newly created command
  351. */
  352. public function register($name)
  353. {
  354. return $this->add(new Command($name));
  355. }
  356. /**
  357. * Adds an array of command objects.
  358. *
  359. * If a Command is not enabled it will not be added.
  360. *
  361. * @param Command[] $commands An array of commands
  362. */
  363. public function addCommands(array $commands)
  364. {
  365. foreach ($commands as $command) {
  366. $this->add($command);
  367. }
  368. }
  369. /**
  370. * Adds a command object.
  371. *
  372. * If a command with the same name already exists, it will be overridden.
  373. * If the command is not enabled it will not be added.
  374. *
  375. * @param Command $command A Command object
  376. *
  377. * @return Command|null The registered command if enabled or null
  378. */
  379. public function add(Command $command)
  380. {
  381. $this->init();
  382. $command->setApplication($this);
  383. if (!$command->isEnabled()) {
  384. $command->setApplication(null);
  385. return;
  386. }
  387. if (null === $command->getDefinition()) {
  388. throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command)));
  389. }
  390. $this->commands[$command->getName()] = $command;
  391. foreach ($command->getAliases() as $alias) {
  392. $this->commands[$alias] = $command;
  393. }
  394. return $command;
  395. }
  396. /**
  397. * Returns a registered command by name or alias.
  398. *
  399. * @param string $name The command name or alias
  400. *
  401. * @return Command A Command object
  402. *
  403. * @throws CommandNotFoundException When given command name does not exist
  404. */
  405. public function get($name)
  406. {
  407. $this->init();
  408. if (!isset($this->commands[$name])) {
  409. throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
  410. }
  411. $command = $this->commands[$name];
  412. if ($this->wantHelps) {
  413. $this->wantHelps = false;
  414. $helpCommand = $this->get('help');
  415. $helpCommand->setCommand($command);
  416. return $helpCommand;
  417. }
  418. return $command;
  419. }
  420. /**
  421. * Returns true if the command exists, false otherwise.
  422. *
  423. * @param string $name The command name or alias
  424. *
  425. * @return bool true if the command exists, false otherwise
  426. */
  427. public function has($name)
  428. {
  429. $this->init();
  430. return isset($this->commands[$name]);
  431. }
  432. /**
  433. * Returns an array of all unique namespaces used by currently registered commands.
  434. *
  435. * It does not return the global namespace which always exists.
  436. *
  437. * @return string[] An array of namespaces
  438. */
  439. public function getNamespaces()
  440. {
  441. $namespaces = array();
  442. foreach ($this->all() as $command) {
  443. $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName()));
  444. foreach ($command->getAliases() as $alias) {
  445. $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias));
  446. }
  447. }
  448. return array_values(array_unique(array_filter($namespaces)));
  449. }
  450. /**
  451. * Finds a registered namespace by a name or an abbreviation.
  452. *
  453. * @param string $namespace A namespace or abbreviation to search for
  454. *
  455. * @return string A registered namespace
  456. *
  457. * @throws CommandNotFoundException When namespace is incorrect or ambiguous
  458. */
  459. public function findNamespace($namespace)
  460. {
  461. $allNamespaces = $this->getNamespaces();
  462. $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace);
  463. $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
  464. if (empty($namespaces)) {
  465. $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
  466. if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
  467. if (1 == count($alternatives)) {
  468. $message .= "\n\nDid you mean this?\n ";
  469. } else {
  470. $message .= "\n\nDid you mean one of these?\n ";
  471. }
  472. $message .= implode("\n ", $alternatives);
  473. }
  474. throw new CommandNotFoundException($message, $alternatives);
  475. }
  476. $exact = in_array($namespace, $namespaces, true);
  477. if (count($namespaces) > 1 && !$exact) {
  478. throw new CommandNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
  479. }
  480. return $exact ? $namespace : reset($namespaces);
  481. }
  482. /**
  483. * Finds a command by name or alias.
  484. *
  485. * Contrary to get, this command tries to find the best
  486. * match if you give it an abbreviation of a name or alias.
  487. *
  488. * @param string $name A command name or a command alias
  489. *
  490. * @return Command A Command instance
  491. *
  492. * @throws CommandNotFoundException When command name is incorrect or ambiguous
  493. */
  494. public function find($name)
  495. {
  496. $this->init();
  497. $allCommands = array_keys($this->commands);
  498. $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
  499. $commands = preg_grep('{^'.$expr.'}', $allCommands);
  500. if (empty($commands) || count(preg_grep('{^'.$expr.'$}', $commands)) < 1) {
  501. if (false !== $pos = strrpos($name, ':')) {
  502. // check if a namespace exists and contains commands
  503. $this->findNamespace(substr($name, 0, $pos));
  504. }
  505. $message = sprintf('Command "%s" is not defined.', $name);
  506. if ($alternatives = $this->findAlternatives($name, $allCommands)) {
  507. if (1 == count($alternatives)) {
  508. $message .= "\n\nDid you mean this?\n ";
  509. } else {
  510. $message .= "\n\nDid you mean one of these?\n ";
  511. }
  512. $message .= implode("\n ", $alternatives);
  513. }
  514. throw new CommandNotFoundException($message, $alternatives);
  515. }
  516. // filter out aliases for commands which are already on the list
  517. if (count($commands) > 1) {
  518. $commandList = $this->commands;
  519. $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) {
  520. $commandName = $commandList[$nameOrAlias]->getName();
  521. return $commandName === $nameOrAlias || !in_array($commandName, $commands);
  522. });
  523. }
  524. $exact = in_array($name, $commands, true);
  525. if (count($commands) > 1 && !$exact) {
  526. $usableWidth = $this->terminal->getWidth() - 10;
  527. $abbrevs = array_values($commands);
  528. $maxLen = 0;
  529. foreach ($abbrevs as $abbrev) {
  530. $maxLen = max(Helper::strlen($abbrev), $maxLen);
  531. }
  532. $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) {
  533. $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
  534. return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
  535. }, array_values($commands));
  536. $suggestions = $this->getAbbreviationSuggestions($abbrevs);
  537. throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
  538. }
  539. return $this->get($exact ? $name : reset($commands));
  540. }
  541. /**
  542. * Gets the commands (registered in the given namespace if provided).
  543. *
  544. * The array keys are the full names and the values the command instances.
  545. *
  546. * @param string $namespace A namespace name
  547. *
  548. * @return Command[] An array of Command instances
  549. */
  550. public function all($namespace = null)
  551. {
  552. $this->init();
  553. if (null === $namespace) {
  554. return $this->commands;
  555. }
  556. $commands = array();
  557. foreach ($this->commands as $name => $command) {
  558. if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) {
  559. $commands[$name] = $command;
  560. }
  561. }
  562. return $commands;
  563. }
  564. /**
  565. * Returns an array of possible abbreviations given a set of names.
  566. *
  567. * @param array $names An array of names
  568. *
  569. * @return array An array of abbreviations
  570. */
  571. public static function getAbbreviations($names)
  572. {
  573. $abbrevs = array();
  574. foreach ($names as $name) {
  575. for ($len = strlen($name); $len > 0; --$len) {
  576. $abbrev = substr($name, 0, $len);
  577. $abbrevs[$abbrev][] = $name;
  578. }
  579. }
  580. return $abbrevs;
  581. }
  582. /**
  583. * Renders a caught exception.
  584. *
  585. * @param \Exception $e An exception instance
  586. * @param OutputInterface $output An OutputInterface instance
  587. */
  588. public function renderException(\Exception $e, OutputInterface $output)
  589. {
  590. $output->writeln('', OutputInterface::VERBOSITY_QUIET);
  591. do {
  592. $title = sprintf(
  593. ' [%s%s] ',
  594. get_class($e),
  595. $output->isVerbose() && 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''
  596. );
  597. $len = Helper::strlen($title);
  598. $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;
  599. // HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327
  600. if (defined('HHVM_VERSION') && $width > 1 << 31) {
  601. $width = 1 << 31;
  602. }
  603. $lines = array();
  604. foreach (preg_split('/\r?\n/', trim($e->getMessage())) as $line) {
  605. foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
  606. // pre-format lines to get the right string length
  607. $lineLength = Helper::strlen($line) + 4;
  608. $lines[] = array($line, $lineLength);
  609. $len = max($lineLength, $len);
  610. }
  611. }
  612. $messages = array();
  613. $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
  614. $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::strlen($title))));
  615. foreach ($lines as $line) {
  616. $messages[] = sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
  617. }
  618. $messages[] = $emptyLine;
  619. $messages[] = '';
  620. $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
  621. if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
  622. $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET);
  623. // exception related properties
  624. $trace = $e->getTrace();
  625. array_unshift($trace, array(
  626. 'function' => '',
  627. 'file' => null !== $e->getFile() ? $e->getFile() : 'n/a',
  628. 'line' => null !== $e->getLine() ? $e->getLine() : 'n/a',
  629. 'args' => array(),
  630. ));
  631. for ($i = 0, $count = count($trace); $i < $count; ++$i) {
  632. $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
  633. $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
  634. $function = $trace[$i]['function'];
  635. $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
  636. $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
  637. $output->writeln(sprintf(' %s%s%s() at <info>%s:%s</info>', $class, $type, $function, $file, $line), OutputInterface::VERBOSITY_QUIET);
  638. }
  639. $output->writeln('', OutputInterface::VERBOSITY_QUIET);
  640. }
  641. } while ($e = $e->getPrevious());
  642. if (null !== $this->runningCommand) {
  643. $output->writeln(sprintf('<info>%s</info>', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET);
  644. $output->writeln('', OutputInterface::VERBOSITY_QUIET);
  645. }
  646. }
  647. /**
  648. * Tries to figure out the terminal width in which this application runs.
  649. *
  650. * @return int|null
  651. *
  652. * @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
  653. */
  654. protected function getTerminalWidth()
  655. {
  656. @trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), E_USER_DEPRECATED);
  657. return $this->terminal->getWidth();
  658. }
  659. /**
  660. * Tries to figure out the terminal height in which this application runs.
  661. *
  662. * @return int|null
  663. *
  664. * @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
  665. */
  666. protected function getTerminalHeight()
  667. {
  668. @trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), E_USER_DEPRECATED);
  669. return $this->terminal->getHeight();
  670. }
  671. /**
  672. * Tries to figure out the terminal dimensions based on the current environment.
  673. *
  674. * @return array Array containing width and height
  675. *
  676. * @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead.
  677. */
  678. public function getTerminalDimensions()
  679. {
  680. @trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), E_USER_DEPRECATED);
  681. return array($this->terminal->getWidth(), $this->terminal->getHeight());
  682. }
  683. /**
  684. * Sets terminal dimensions.
  685. *
  686. * Can be useful to force terminal dimensions for functional tests.
  687. *
  688. * @param int $width The width
  689. * @param int $height The height
  690. *
  691. * @return $this
  692. *
  693. * @deprecated since version 3.2, to be removed in 4.0. Set the COLUMNS and LINES env vars instead.
  694. */
  695. public function setTerminalDimensions($width, $height)
  696. {
  697. @trigger_error(sprintf('%s is deprecated as of 3.2 and will be removed in 4.0. Set the COLUMNS and LINES env vars instead.', __METHOD__), E_USER_DEPRECATED);
  698. putenv('COLUMNS='.$width);
  699. putenv('LINES='.$height);
  700. return $this;
  701. }
  702. /**
  703. * Configures the input and output instances based on the user arguments and options.
  704. *
  705. * @param InputInterface $input An InputInterface instance
  706. * @param OutputInterface $output An OutputInterface instance
  707. */
  708. protected function configureIO(InputInterface $input, OutputInterface $output)
  709. {
  710. if (true === $input->hasParameterOption(array('--ansi'), true)) {
  711. $output->setDecorated(true);
  712. } elseif (true === $input->hasParameterOption(array('--no-ansi'), true)) {
  713. $output->setDecorated(false);
  714. }
  715. if (true === $input->hasParameterOption(array('--no-interaction', '-n'), true)) {
  716. $input->setInteractive(false);
  717. } elseif (function_exists('posix_isatty')) {
  718. $inputStream = null;
  719. if ($input instanceof StreamableInputInterface) {
  720. $inputStream = $input->getStream();
  721. }
  722. // This check ensures that calling QuestionHelper::setInputStream() works
  723. // To be removed in 4.0 (in the same time as QuestionHelper::setInputStream)
  724. if (!$inputStream && $this->getHelperSet()->has('question')) {
  725. $inputStream = $this->getHelperSet()->get('question')->getInputStream(false);
  726. }
  727. if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) {
  728. $input->setInteractive(false);
  729. }
  730. }
  731. if (true === $input->hasParameterOption(array('--quiet', '-q'), true)) {
  732. $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
  733. $input->setInteractive(false);
  734. } else {
  735. if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
  736. $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
  737. } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
  738. $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
  739. } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
  740. $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
  741. }
  742. }
  743. }
  744. /**
  745. * Runs the current command.
  746. *
  747. * If an event dispatcher has been attached to the application,
  748. * events are also dispatched during the life-cycle of the command.
  749. *
  750. * @param Command $command A Command instance
  751. * @param InputInterface $input An Input instance
  752. * @param OutputInterface $output An Output instance
  753. *
  754. * @return int 0 if everything went fine, or an error code
  755. */
  756. protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
  757. {
  758. foreach ($command->getHelperSet() as $helper) {
  759. if ($helper instanceof InputAwareInterface) {
  760. $helper->setInput($input);
  761. }
  762. }
  763. if (null === $this->dispatcher) {
  764. return $command->run($input, $output);
  765. }
  766. // bind before the console.command event, so the listeners have access to input options/arguments
  767. try {
  768. $command->mergeApplicationDefinition();
  769. $input->bind($command->getDefinition());
  770. } catch (ExceptionInterface $e) {
  771. // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
  772. }
  773. $event = new ConsoleCommandEvent($command, $input, $output);
  774. $e = null;
  775. try {
  776. $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
  777. if ($event->commandShouldRun()) {
  778. $exitCode = $command->run($input, $output);
  779. } else {
  780. $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
  781. }
  782. } catch (\Exception $e) {
  783. } catch (\Throwable $e) {
  784. }
  785. if (null !== $e) {
  786. if ($this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) {
  787. $x = $e instanceof \Exception ? $e : new FatalThrowableError($e);
  788. $event = new ConsoleExceptionEvent($command, $input, $output, $x, $x->getCode());
  789. $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event);
  790. if ($x !== $event->getException()) {
  791. $e = $event->getException();
  792. }
  793. }
  794. $event = new ConsoleErrorEvent($input, $output, $e, $command);
  795. $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
  796. $e = $event->getError();
  797. if (0 === $exitCode = $event->getExitCode()) {
  798. $e = null;
  799. }
  800. }
  801. $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
  802. $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
  803. if (null !== $e) {
  804. throw $e;
  805. }
  806. return $event->getExitCode();
  807. }
  808. /**
  809. * Gets the name of the command based on input.
  810. *
  811. * @param InputInterface $input The input interface
  812. *
  813. * @return string The command name
  814. */
  815. protected function getCommandName(InputInterface $input)
  816. {
  817. return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
  818. }
  819. /**
  820. * Gets the default input definition.
  821. *
  822. * @return InputDefinition An InputDefinition instance
  823. */
  824. protected function getDefaultInputDefinition()
  825. {
  826. return new InputDefinition(array(
  827. new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
  828. new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
  829. new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
  830. 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'),
  831. new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
  832. new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'),
  833. new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
  834. new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
  835. ));
  836. }
  837. /**
  838. * Gets the default commands that should always be available.
  839. *
  840. * @return Command[] An array of default Command instances
  841. */
  842. protected function getDefaultCommands()
  843. {
  844. return array(new HelpCommand(), new ListCommand());
  845. }
  846. /**
  847. * Gets the default helper set with the helpers that should always be available.
  848. *
  849. * @return HelperSet A HelperSet instance
  850. */
  851. protected function getDefaultHelperSet()
  852. {
  853. return new HelperSet(array(
  854. new FormatterHelper(),
  855. new DebugFormatterHelper(),
  856. new ProcessHelper(),
  857. new QuestionHelper(),
  858. ));
  859. }
  860. /**
  861. * Returns abbreviated suggestions in string format.
  862. *
  863. * @param array $abbrevs Abbreviated suggestions to convert
  864. *
  865. * @return string A formatted string of abbreviated suggestions
  866. */
  867. private function getAbbreviationSuggestions($abbrevs)
  868. {
  869. return ' '.implode("\n ", $abbrevs);
  870. }
  871. /**
  872. * Returns the namespace part of the command name.
  873. *
  874. * This method is not part of public API and should not be used directly.
  875. *
  876. * @param string $name The full name of the command
  877. * @param string $limit The maximum number of parts of the namespace
  878. *
  879. * @return string The namespace of the command
  880. */
  881. public function extractNamespace($name, $limit = null)
  882. {
  883. $parts = explode(':', $name);
  884. array_pop($parts);
  885. return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit));
  886. }
  887. /**
  888. * Finds alternative of $name among $collection,
  889. * if nothing is found in $collection, try in $abbrevs.
  890. *
  891. * @param string $name The string
  892. * @param array|\Traversable $collection The collection
  893. *
  894. * @return string[] A sorted array of similar string
  895. */
  896. private function findAlternatives($name, $collection)
  897. {
  898. $threshold = 1e3;
  899. $alternatives = array();
  900. $collectionParts = array();
  901. foreach ($collection as $item) {
  902. $collectionParts[$item] = explode(':', $item);
  903. }
  904. foreach (explode(':', $name) as $i => $subname) {
  905. foreach ($collectionParts as $collectionName => $parts) {
  906. $exists = isset($alternatives[$collectionName]);
  907. if (!isset($parts[$i]) && $exists) {
  908. $alternatives[$collectionName] += $threshold;
  909. continue;
  910. } elseif (!isset($parts[$i])) {
  911. continue;
  912. }
  913. $lev = levenshtein($subname, $parts[$i]);
  914. if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
  915. $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
  916. } elseif ($exists) {
  917. $alternatives[$collectionName] += $threshold;
  918. }
  919. }
  920. }
  921. foreach ($collection as $item) {
  922. $lev = levenshtein($name, $item);
  923. if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) {
  924. $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
  925. }
  926. }
  927. $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
  928. ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
  929. return array_keys($alternatives);
  930. }
  931. /**
  932. * Sets the default Command name.
  933. *
  934. * @param string $commandName The Command name
  935. * @param bool $isSingleCommand Set to true if there is only one command in this application
  936. *
  937. * @return self
  938. */
  939. public function setDefaultCommand($commandName, $isSingleCommand = false)
  940. {
  941. $this->defaultCommand = $commandName;
  942. if ($isSingleCommand) {
  943. // Ensure the command exist
  944. $this->find($commandName);
  945. $this->singleCommand = true;
  946. }
  947. return $this;
  948. }
  949. private function splitStringByWidth($string, $width)
  950. {
  951. // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
  952. // additionally, array_slice() is not enough as some character has doubled width.
  953. // we need a function to split string not by character count but by string width
  954. if (false === $encoding = mb_detect_encoding($string, null, true)) {
  955. return str_split($string, $width);
  956. }
  957. $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
  958. $lines = array();
  959. $line = '';
  960. foreach (preg_split('//u', $utf8String) as $char) {
  961. // test if $char could be appended to current line
  962. if (mb_strwidth($line.$char, 'utf8') <= $width) {
  963. $line .= $char;
  964. continue;
  965. }
  966. // if not, push current line to array and make new line
  967. $lines[] = str_pad($line, $width);
  968. $line = $char;
  969. }
  970. $lines[] = count($lines) ? str_pad($line, $width) : $line;
  971. mb_convert_variables($encoding, 'utf8', $lines);
  972. return $lines;
  973. }
  974. /**
  975. * Returns all namespaces of the command name.
  976. *
  977. * @param string $name The full name of the command
  978. *
  979. * @return string[] The namespaces of the command
  980. */
  981. private function extractAllNamespaces($name)
  982. {
  983. // -1 as third argument is needed to skip the command short name when exploding
  984. $parts = explode(':', $name, -1);
  985. $namespaces = array();
  986. foreach ($parts as $part) {
  987. if (count($namespaces)) {
  988. $namespaces[] = end($namespaces).':'.$part;
  989. } else {
  990. $namespaces[] = $part;
  991. }
  992. }
  993. return $namespaces;
  994. }
  995. private function init()
  996. {
  997. if ($this->initialized) {
  998. return;
  999. }
  1000. $this->initialized = true;
  1001. foreach ($this->getDefaultCommands() as $command) {
  1002. $this->add($command);
  1003. }
  1004. }
  1005. }