PluginProbe
Depicter — Popup & Slider Builder / trunk
Depicter — Popup & Slider Builder vtrunk
4.8.1 trunk 1.0.0 1.1.0 1.1.2 1.1.4 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.5 1.3.8 1.5.0 1.5.1 1.5.2 1.5.5 1.6.0 1.6.1 1.6.2 1.7.0 All 76 releases
depicter / vendor / symfony / console / Application.php

Application.php in Depicter — Popup & Slider Builder trunk, at vendor/symfony/console/Application.php

1,302 lines 43.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\Console;
13
14 use Symfony\Component\Console\Command\Command;
15 use Symfony\Component\Console\Command\CompleteCommand;
16 use Symfony\Component\Console\Command\DumpCompletionCommand;
17 use Symfony\Component\Console\Command\HelpCommand;
18 use Symfony\Component\Console\Command\LazyCommand;
19 use Symfony\Component\Console\Command\ListCommand;
20 use Symfony\Component\Console\Command\SignalableCommandInterface;
21 use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
22 use Symfony\Component\Console\Completion\CompletionInput;
23 use Symfony\Component\Console\Completion\CompletionSuggestions;
24 use Symfony\Component\Console\Event\ConsoleCommandEvent;
25 use Symfony\Component\Console\Event\ConsoleErrorEvent;
26 use Symfony\Component\Console\Event\ConsoleSignalEvent;
27 use Symfony\Component\Console\Event\ConsoleTerminateEvent;
28 use Symfony\Component\Console\Exception\CommandNotFoundException;
29 use Symfony\Component\Console\Exception\ExceptionInterface;
30 use Symfony\Component\Console\Exception\LogicException;
31 use Symfony\Component\Console\Exception\NamespaceNotFoundException;
32 use Symfony\Component\Console\Exception\RuntimeException;
33 use Symfony\Component\Console\Formatter\OutputFormatter;
34 use Symfony\Component\Console\Helper\DebugFormatterHelper;
35 use Symfony\Component\Console\Helper\FormatterHelper;
36 use Symfony\Component\Console\Helper\Helper;
37 use Symfony\Component\Console\Helper\HelperSet;
38 use Symfony\Component\Console\Helper\ProcessHelper;
39 use Symfony\Component\Console\Helper\QuestionHelper;
40 use Symfony\Component\Console\Input\ArgvInput;
41 use Symfony\Component\Console\Input\ArrayInput;
42 use Symfony\Component\Console\Input\InputArgument;
43 use Symfony\Component\Console\Input\InputAwareInterface;
44 use Symfony\Component\Console\Input\InputDefinition;
45 use Symfony\Component\Console\Input\InputInterface;
46 use Symfony\Component\Console\Input\InputOption;
47 use Symfony\Component\Console\Output\ConsoleOutput;
48 use Symfony\Component\Console\Output\ConsoleOutputInterface;
49 use Symfony\Component\Console\Output\OutputInterface;
50 use Symfony\Component\Console\SignalRegistry\SignalRegistry;
51 use Symfony\Component\Console\Style\SymfonyStyle;
52 use Symfony\Component\ErrorHandler\ErrorHandler;
53 use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
54 use Symfony\Contracts\Service\ResetInterface;
55
56 /**
57 * An Application is the container for a collection of commands.
58 *
59 * It is the main entry point of a Console application.
60 *
61 * This class is optimized for a standard CLI environment.
62 *
63 * Usage:
64 *
65 * $app = new Application('myapp', '1.0 (stable)');
66 * $app->add(new SimpleCommand());
67 * $app->run();
68 *
69 * @author Fabien Potencier <fabien@symfony.com>
70 */
71 class Application implements ResetInterface
72 {
73 private $commands = [];
74 private $wantHelps = false;
75 private $runningCommand;
76 private $name;
77 private $version;
78 private $commandLoader;
79 private $catchExceptions = true;
80 private $autoExit = true;
81 private $definition;
82 private $helperSet;
83 private $dispatcher;
84 private $terminal;
85 private $defaultCommand;
86 private $singleCommand = false;
87 private $initialized;
88 private $signalRegistry;
89 private $signalsToDispatchEvent = [];
90
91 public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
92 {
93 $this->name = $name;
94 $this->version = $version;
95 $this->terminal = new Terminal();
96 $this->defaultCommand = 'list';
97 if (\defined('SIGINT') && SignalRegistry::isSupported()) {
98 $this->signalRegistry = new SignalRegistry();
99 $this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2];
100 }
101 }
102
103 /**
104 * @final
105 */
106 public function setDispatcher(EventDispatcherInterface $dispatcher)
107 {
108 $this->dispatcher = $dispatcher;
109 }
110
111 public function setCommandLoader(CommandLoaderInterface $commandLoader)
112 {
113 $this->commandLoader = $commandLoader;
114 }
115
116 public function getSignalRegistry(): SignalRegistry
117 {
118 if (!$this->signalRegistry) {
119 throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
120 }
121
122 return $this->signalRegistry;
123 }
124
125 public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
126 {
127 $this->signalsToDispatchEvent = $signalsToDispatchEvent;
128 }
129
130 /**
131 * Runs the current application.
132 *
133 * @return int 0 if everything went fine, or an error code
134 *
135 * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
136 */
137 public function run(?InputInterface $input = null, ?OutputInterface $output = null)
138 {
139 if (\function_exists('putenv')) {
140 @putenv('LINES='.$this->terminal->getHeight());
141 @putenv('COLUMNS='.$this->terminal->getWidth());
142 }
143
144 if (null === $input) {
145 $input = new ArgvInput();
146 }
147
148 if (null === $output) {
149 $output = new ConsoleOutput();
150 }
151
152 $renderException = function (\Throwable $e) use ($output) {
153 if ($output instanceof ConsoleOutputInterface) {
154 $this->renderThrowable($e, $output->getErrorOutput());
155 } else {
156 $this->renderThrowable($e, $output);
157 }
158 };
159 if ($phpHandler = set_exception_handler($renderException)) {
160 restore_exception_handler();
161 if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) {
162 $errorHandler = true;
163 } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) {
164 $phpHandler[0]->setExceptionHandler($errorHandler);
165 }
166 }
167
168 try {
169 $this->configureIO($input, $output);
170
171 $exitCode = $this->doRun($input, $output);
172 } catch (\Exception $e) {
173 if (!$this->catchExceptions) {
174 throw $e;
175 }
176
177 $renderException($e);
178
179 $exitCode = $e->getCode();
180 if (is_numeric($exitCode)) {
181 $exitCode = (int) $exitCode;
182 if ($exitCode <= 0) {
183 $exitCode = 1;
184 }
185 } else {
186 $exitCode = 1;
187 }
188 } finally {
189 // if the exception handler changed, keep it
190 // otherwise, unregister $renderException
191 if (!$phpHandler) {
192 if (set_exception_handler($renderException) === $renderException) {
193 restore_exception_handler();
194 }
195 restore_exception_handler();
196 } elseif (!$errorHandler) {
197 $finalHandler = $phpHandler[0]->setExceptionHandler(null);
198 if ($finalHandler !== $renderException) {
199 $phpHandler[0]->setExceptionHandler($finalHandler);
200 }
201 }
202 }
203
204 if ($this->autoExit) {
205 if ($exitCode > 255) {
206 $exitCode = 255;
207 }
208
209 exit($exitCode);
210 }
211
212 return $exitCode;
213 }
214
215 /**
216 * Runs the current application.
217 *
218 * @return int 0 if everything went fine, or an error code
219 */
220 public function doRun(InputInterface $input, OutputInterface $output)
221 {
222 if (true === $input->hasParameterOption(['--version', '-V'], true)) {
223 $output->writeln($this->getLongVersion());
224
225 return 0;
226 }
227
228 try {
229 // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
230 $input->bind($this->getDefinition());
231 } catch (ExceptionInterface $e) {
232 // Errors must be ignored, full binding/validation happens later when the command is known.
233 }
234
235 $name = $this->getCommandName($input);
236 if (true === $input->hasParameterOption(['--help', '-h'], true)) {
237 if (!$name) {
238 $name = 'help';
239 $input = new ArrayInput(['command_name' => $this->defaultCommand]);
240 } else {
241 $this->wantHelps = true;
242 }
243 }
244
245 if (!$name) {
246 $name = $this->defaultCommand;
247 $definition = $this->getDefinition();
248 $definition->setArguments(array_merge(
249 $definition->getArguments(),
250 [
251 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
252 ]
253 ));
254 }
255
256 try {
257 $this->runningCommand = null;
258 // the command name MUST be the first element of the input
259 $command = $this->find($name);
260 } catch (\Throwable $e) {
261 if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) {
262 if (null !== $this->dispatcher) {
263 $event = new ConsoleErrorEvent($input, $output, $e);
264 $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
265
266 if (0 === $event->getExitCode()) {
267 return 0;
268 }
269
270 $e = $event->getError();
271 }
272
273 throw $e;
274 }
275
276 $alternative = $alternatives[0];
277
278 $style = new SymfonyStyle($input, $output);
279 $output->writeln('');
280 $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true);
281 $output->writeln($formattedBlock);
282 if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
283 if (null !== $this->dispatcher) {
284 $event = new ConsoleErrorEvent($input, $output, $e);
285 $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
286
287 return $event->getExitCode();
288 }
289
290 return 1;
291 }
292
293 $command = $this->find($alternative);
294 }
295
296 if ($command instanceof LazyCommand) {
297 $command = $command->getCommand();
298 }
299
300 $this->runningCommand = $command;
301 $exitCode = $this->doRunCommand($command, $input, $output);
302 $this->runningCommand = null;
303
304 return $exitCode;
305 }
306
307 /**
308 * {@inheritdoc}
309 */
310 public function reset()
311 {
312 }
313
314 public function setHelperSet(HelperSet $helperSet)
315 {
316 $this->helperSet = $helperSet;
317 }
318
319 /**
320 * Get the helper set associated with the command.
321 *
322 * @return HelperSet
323 */
324 public function getHelperSet()
325 {
326 if (!$this->helperSet) {
327 $this->helperSet = $this->getDefaultHelperSet();
328 }
329
330 return $this->helperSet;
331 }
332
333 public function setDefinition(InputDefinition $definition)
334 {
335 $this->definition = $definition;
336 }
337
338 /**
339 * Gets the InputDefinition related to this Application.
340 *
341 * @return InputDefinition
342 */
343 public function getDefinition()
344 {
345 if (!$this->definition) {
346 $this->definition = $this->getDefaultInputDefinition();
347 }
348
349 if ($this->singleCommand) {
350 $inputDefinition = $this->definition;
351 $inputDefinition->setArguments();
352
353 return $inputDefinition;
354 }
355
356 return $this->definition;
357 }
358
359 /**
360 * Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
361 */
362 public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
363 {
364 if (
365 CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
366 && 'command' === $input->getCompletionName()
367 ) {
368 $commandNames = [];
369 foreach ($this->all() as $name => $command) {
370 // skip hidden commands and aliased commands as they already get added below
371 if ($command->isHidden() || $command->getName() !== $name) {
372 continue;
373 }
374 $commandNames[] = $command->getName();
375 foreach ($command->getAliases() as $name) {
376 $commandNames[] = $name;
377 }
378 }
379 $suggestions->suggestValues(array_filter($commandNames));
380
381 return;
382 }
383
384 if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
385 $suggestions->suggestOptions($this->getDefinition()->getOptions());
386
387 return;
388 }
389 }
390
391 /**
392 * Gets the help message.
393 *
394 * @return string
395 */
396 public function getHelp()
397 {
398 return $this->getLongVersion();
399 }
400
401 /**
402 * Gets whether to catch exceptions or not during commands execution.
403 *
404 * @return bool
405 */
406 public function areExceptionsCaught()
407 {
408 return $this->catchExceptions;
409 }
410
411 /**
412 * Sets whether to catch exceptions or not during commands execution.
413 */
414 public function setCatchExceptions(bool $boolean)
415 {
416 $this->catchExceptions = $boolean;
417 }
418
419 /**
420 * Gets whether to automatically exit after a command execution or not.
421 *
422 * @return bool
423 */
424 public function isAutoExitEnabled()
425 {
426 return $this->autoExit;
427 }
428
429 /**
430 * Sets whether to automatically exit after a command execution or not.
431 */
432 public function setAutoExit(bool $boolean)
433 {
434 $this->autoExit = $boolean;
435 }
436
437 /**
438 * Gets the name of the application.
439 *
440 * @return string
441 */
442 public function getName()
443 {
444 return $this->name;
445 }
446
447 /**
448 * Sets the application name.
449 **/
450 public function setName(string $name)
451 {
452 $this->name = $name;
453 }
454
455 /**
456 * Gets the application version.
457 *
458 * @return string
459 */
460 public function getVersion()
461 {
462 return $this->version;
463 }
464
465 /**
466 * Sets the application version.
467 */
468 public function setVersion(string $version)
469 {
470 $this->version = $version;
471 }
472
473 /**
474 * Returns the long version of the application.
475 *
476 * @return string
477 */
478 public function getLongVersion()
479 {
480 if ('UNKNOWN' !== $this->getName()) {
481 if ('UNKNOWN' !== $this->getVersion()) {
482 return sprintf('%s <info>%s</info>', $this->getName(), $this->getVersion());
483 }
484
485 return $this->getName();
486 }
487
488 return 'Console Tool';
489 }
490
491 /**
492 * Registers a new command.
493 *
494 * @return Command
495 */
496 public function register(string $name)
497 {
498 return $this->add(new Command($name));
499 }
500
501 /**
502 * Adds an array of command objects.
503 *
504 * If a Command is not enabled it will not be added.
505 *
506 * @param Command[] $commands An array of commands
507 */
508 public function addCommands(array $commands)
509 {
510 foreach ($commands as $command) {
511 $this->add($command);
512 }
513 }
514
515 /**
516 * Adds a command object.
517 *
518 * If a command with the same name already exists, it will be overridden.
519 * If the command is not enabled it will not be added.
520 *
521 * @return Command|null
522 */
523 public function add(Command $command)
524 {
525 $this->init();
526
527 $command->setApplication($this);
528
529 if (!$command->isEnabled()) {
530 $command->setApplication(null);
531
532 return null;
533 }
534
535 if (!$command instanceof LazyCommand) {
536 // Will throw if the command is not correctly initialized.
537 $command->getDefinition();
538 }
539
540 if (!$command->getName()) {
541 throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
542 }
543
544 $this->commands[$command->getName()] = $command;
545
546 foreach ($command->getAliases() as $alias) {
547 $this->commands[$alias] = $command;
548 }
549
550 return $command;
551 }
552
553 /**
554 * Returns a registered command by name or alias.
555 *
556 * @return Command
557 *
558 * @throws CommandNotFoundException When given command name does not exist
559 */
560 public function get(string $name)
561 {
562 $this->init();
563
564 if (!$this->has($name)) {
565 throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
566 }
567
568 // When the command has a different name than the one used at the command loader level
569 if (!isset($this->commands[$name])) {
570 throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
571 }
572
573 $command = $this->commands[$name];
574
575 if ($this->wantHelps) {
576 $this->wantHelps = false;
577
578 $helpCommand = $this->get('help');
579 $helpCommand->setCommand($command);
580
581 return $helpCommand;
582 }
583
584 return $command;
585 }
586
587 /**
588 * Returns true if the command exists, false otherwise.
589 *
590 * @return bool
591 */
592 public function has(string $name)
593 {
594 $this->init();
595
596 return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name)));
597 }
598
599 /**
600 * Returns an array of all unique namespaces used by currently registered commands.
601 *
602 * It does not return the global namespace which always exists.
603 *
604 * @return string[]
605 */
606 public function getNamespaces()
607 {
608 $namespaces = [];
609 foreach ($this->all() as $command) {
610 if ($command->isHidden()) {
611 continue;
612 }
613
614 $namespaces[] = $this->extractAllNamespaces($command->getName());
615
616 foreach ($command->getAliases() as $alias) {
617 $namespaces[] = $this->extractAllNamespaces($alias);
618 }
619 }
620
621 return array_values(array_unique(array_filter(array_merge([], ...$namespaces))));
622 }
623
624 /**
625 * Finds a registered namespace by a name or an abbreviation.
626 *
627 * @return string
628 *
629 * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
630 */
631 public function findNamespace(string $namespace)
632 {
633 $allNamespaces = $this->getNamespaces();
634 $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*';
635 $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
636
637 if (empty($namespaces)) {
638 $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
639
640 if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
641 if (1 == \count($alternatives)) {
642 $message .= "\n\nDid you mean this?\n ";
643 } else {
644 $message .= "\n\nDid you mean one of these?\n ";
645 }
646
647 $message .= implode("\n ", $alternatives);
648 }
649
650 throw new NamespaceNotFoundException($message, $alternatives);
651 }
652
653 $exact = \in_array($namespace, $namespaces, true);
654 if (\count($namespaces) > 1 && !$exact) {
655 throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
656 }
657
658 return $exact ? $namespace : reset($namespaces);
659 }
660
661 /**
662 * Finds a command by name or alias.
663 *
664 * Contrary to get, this command tries to find the best
665 * match if you give it an abbreviation of a name or alias.
666 *
667 * @return Command
668 *
669 * @throws CommandNotFoundException When command name is incorrect or ambiguous
670 */
671 public function find(string $name)
672 {
673 $this->init();
674
675 $aliases = [];
676
677 foreach ($this->commands as $command) {
678 foreach ($command->getAliases() as $alias) {
679 if (!$this->has($alias)) {
680 $this->commands[$alias] = $command;
681 }
682 }
683 }
684
685 if ($this->has($name)) {
686 return $this->get($name);
687 }
688
689 $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
690 $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*';
691 $commands = preg_grep('{^'.$expr.'}', $allCommands);
692
693 if (empty($commands)) {
694 $commands = preg_grep('{^'.$expr.'}i', $allCommands);
695 }
696
697 // if no commands matched or we just matched namespaces
698 if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) {
699 if (false !== $pos = strrpos($name, ':')) {
700 // check if a namespace exists and contains commands
701 $this->findNamespace(substr($name, 0, $pos));
702 }
703
704 $message = sprintf('Command "%s" is not defined.', $name);
705
706 if ($alternatives = $this->findAlternatives($name, $allCommands)) {
707 // remove hidden commands
708 $alternatives = array_filter($alternatives, function ($name) {
709 return !$this->get($name)->isHidden();
710 });
711
712 if (1 == \count($alternatives)) {
713 $message .= "\n\nDid you mean this?\n ";
714 } else {
715 $message .= "\n\nDid you mean one of these?\n ";
716 }
717 $message .= implode("\n ", $alternatives);
718 }
719
720 throw new CommandNotFoundException($message, array_values($alternatives));
721 }
722
723 // filter out aliases for commands which are already on the list
724 if (\count($commands) > 1) {
725 $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands;
726 $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) {
727 if (!$commandList[$nameOrAlias] instanceof Command) {
728 $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias);
729 }
730
731 $commandName = $commandList[$nameOrAlias]->getName();
732
733 $aliases[$nameOrAlias] = $commandName;
734
735 return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
736 }));
737 }
738
739 if (\count($commands) > 1) {
740 $usableWidth = $this->terminal->getWidth() - 10;
741 $abbrevs = array_values($commands);
742 $maxLen = 0;
743 foreach ($abbrevs as $abbrev) {
744 $maxLen = max(Helper::width($abbrev), $maxLen);
745 }
746 $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
747 if ($commandList[$cmd]->isHidden()) {
748 unset($commands[array_search($cmd, $commands)]);
749
750 return false;
751 }
752
753 $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
754
755 return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
756 }, array_values($commands));
757
758 if (\count($commands) > 1) {
759 $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
760
761 throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
762 }
763 }
764
765 $command = $this->get(reset($commands));
766
767 if ($command->isHidden()) {
768 throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
769 }
770
771 return $command;
772 }
773
774 /**
775 * Gets the commands (registered in the given namespace if provided).
776 *
777 * The array keys are the full names and the values the command instances.
778 *
779 * @return Command[]
780 */
781 public function all(?string $namespace = null)
782 {
783 $this->init();
784
785 if (null === $namespace) {
786 if (!$this->commandLoader) {
787 return $this->commands;
788 }
789
790 $commands = $this->commands;
791 foreach ($this->commandLoader->getNames() as $name) {
792 if (!isset($commands[$name]) && $this->has($name)) {
793 $commands[$name] = $this->get($name);
794 }
795 }
796
797 return $commands;
798 }
799
800 $commands = [];
801 foreach ($this->commands as $name => $command) {
802 if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) {
803 $commands[$name] = $command;
804 }
805 }
806
807 if ($this->commandLoader) {
808 foreach ($this->commandLoader->getNames() as $name) {
809 if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) {
810 $commands[$name] = $this->get($name);
811 }
812 }
813 }
814
815 return $commands;
816 }
817
818 /**
819 * Returns an array of possible abbreviations given a set of names.
820 *
821 * @return string[][]
822 */
823 public static function getAbbreviations(array $names)
824 {
825 $abbrevs = [];
826 foreach ($names as $name) {
827 for ($len = \strlen($name); $len > 0; --$len) {
828 $abbrev = substr($name, 0, $len);
829 $abbrevs[$abbrev][] = $name;
830 }
831 }
832
833 return $abbrevs;
834 }
835
836 public function renderThrowable(\Throwable $e, OutputInterface $output): void
837 {
838 $output->writeln('', OutputInterface::VERBOSITY_QUIET);
839
840 $this->doRenderThrowable($e, $output);
841
842 if (null !== $this->runningCommand) {
843 $output->writeln(sprintf('<info>%s</info>', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
844 $output->writeln('', OutputInterface::VERBOSITY_QUIET);
845 }
846 }
847
848 protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void
849 {
850 do {
851 $message = trim($e->getMessage());
852 if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
853 $class = get_debug_type($e);
854 $title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
855 $len = Helper::width($title);
856 } else {
857 $len = 0;
858 }
859
860 if (str_contains($message, "@anonymous\0")) {
861 $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', function ($m) {
862 return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
863 }, $message);
864 }
865
866 $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
867 $lines = [];
868 foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
869 foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
870 // pre-format lines to get the right string length
871 $lineLength = Helper::width($line) + 4;
872 $lines[] = [$line, $lineLength];
873
874 $len = max($lineLength, $len);
875 }
876 }
877
878 $messages = [];
879 if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
880 $messages[] = sprintf('<comment>%s</comment>', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
881 }
882 $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
883 if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
884 $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
885 }
886 foreach ($lines as $line) {
887 $messages[] = sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
888 }
889 $messages[] = $emptyLine;
890 $messages[] = '';
891
892 $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
893
894 if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
895 $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET);
896
897 // exception related properties
898 $trace = $e->getTrace();
899
900 array_unshift($trace, [
901 'function' => '',
902 'file' => $e->getFile() ?: 'n/a',
903 'line' => $e->getLine() ?: 'n/a',
904 'args' => [],
905 ]);
906
907 for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
908 $class = $trace[$i]['class'] ?? '';
909 $type = $trace[$i]['type'] ?? '';
910 $function = $trace[$i]['function'] ?? '';
911 $file = $trace[$i]['file'] ?? 'n/a';
912 $line = $trace[$i]['line'] ?? 'n/a';
913
914 $output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
915 }
916
917 $output->writeln('', OutputInterface::VERBOSITY_QUIET);
918 }
919 } while ($e = $e->getPrevious());
920 }
921
922 /**
923 * Configures the input and output instances based on the user arguments and options.
924 */
925 protected function configureIO(InputInterface $input, OutputInterface $output)
926 {
927 if (true === $input->hasParameterOption(['--ansi'], true)) {
928 $output->setDecorated(true);
929 } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) {
930 $output->setDecorated(false);
931 }
932
933 if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) {
934 $input->setInteractive(false);
935 }
936
937 switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) {
938 case -1:
939 $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
940 break;
941 case 1:
942 $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
943 break;
944 case 2:
945 $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
946 break;
947 case 3:
948 $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
949 break;
950 default:
951 $shellVerbosity = 0;
952 break;
953 }
954
955 if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
956 $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
957 $shellVerbosity = -1;
958 } else {
959 if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
960 $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
961 $shellVerbosity = 3;
962 } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
963 $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
964 $shellVerbosity = 2;
965 } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
966 $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
967 $shellVerbosity = 1;
968 }
969 }
970
971 if (-1 === $shellVerbosity) {
972 $input->setInteractive(false);
973 }
974
975 if (\function_exists('putenv')) {
976 @putenv('SHELL_VERBOSITY='.$shellVerbosity);
977 }
978 $_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
979 $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
980 }
981
982 /**
983 * Runs the current command.
984 *
985 * If an event dispatcher has been attached to the application,
986 * events are also dispatched during the life-cycle of the command.
987 *
988 * @return int 0 if everything went fine, or an error code
989 */
990 protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
991 {
992 foreach ($command->getHelperSet() as $helper) {
993 if ($helper instanceof InputAwareInterface) {
994 $helper->setInput($input);
995 }
996 }
997
998 if ($this->signalsToDispatchEvent) {
999 $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
1000
1001 if ($commandSignals || null !== $this->dispatcher) {
1002 if (!$this->signalRegistry) {
1003 throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
1004 }
1005
1006 if (Terminal::hasSttyAvailable()) {
1007 $sttyMode = shell_exec('stty -g');
1008
1009 foreach ([\SIGINT, \SIGTERM] as $signal) {
1010 $this->signalRegistry->register($signal, static function () use ($sttyMode) {
1011 shell_exec('stty '.$sttyMode);
1012 });
1013 }
1014 }
1015 }
1016
1017 if (null !== $this->dispatcher) {
1018 foreach ($this->signalsToDispatchEvent as $signal) {
1019 $event = new ConsoleSignalEvent($command, $input, $output, $signal);
1020
1021 $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) {
1022 $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
1023
1024 // No more handlers, we try to simulate PHP default behavior
1025 if (!$hasNext) {
1026 if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) {
1027 exit(0);
1028 }
1029 }
1030 });
1031 }
1032 }
1033
1034 foreach ($commandSignals as $signal) {
1035 $this->signalRegistry->register($signal, [$command, 'handleSignal']);
1036 }
1037 }
1038
1039 if (null === $this->dispatcher) {
1040 return $command->run($input, $output);
1041 }
1042
1043 // bind before the console.command event, so the listeners have access to input options/arguments
1044 try {
1045 $command->mergeApplicationDefinition();
1046 $input->bind($command->getDefinition());
1047 } catch (ExceptionInterface $e) {
1048 // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
1049 }
1050
1051 $event = new ConsoleCommandEvent($command, $input, $output);
1052 $e = null;
1053
1054 try {
1055 $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND);
1056
1057 if ($event->commandShouldRun()) {
1058 $exitCode = $command->run($input, $output);
1059 } else {
1060 $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
1061 }
1062 } catch (\Throwable $e) {
1063 $event = new ConsoleErrorEvent($input, $output, $e, $command);
1064 $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
1065 $e = $event->getError();
1066
1067 if (0 === $exitCode = $event->getExitCode()) {
1068 $e = null;
1069 }
1070 }
1071
1072 $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
1073 $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
1074
1075 if (null !== $e) {
1076 throw $e;
1077 }
1078
1079 return $event->getExitCode();
1080 }
1081
1082 /**
1083 * Gets the name of the command based on input.
1084 *
1085 * @return string|null
1086 */
1087 protected function getCommandName(InputInterface $input)
1088 {
1089 return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
1090 }
1091
1092 /**
1093 * Gets the default input definition.
1094 *
1095 * @return InputDefinition
1096 */
1097 protected function getDefaultInputDefinition()
1098 {
1099 return new InputDefinition([
1100 new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
1101 new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the <info>'.$this->defaultCommand.'</info> command'),
1102 new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
1103 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'),
1104 new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
1105 new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null),
1106 new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
1107 ]);
1108 }
1109
1110 /**
1111 * Gets the default commands that should always be available.
1112 *
1113 * @return Command[]
1114 */
1115 protected function getDefaultCommands()
1116 {
1117 return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
1118 }
1119
1120 /**
1121 * Gets the default helper set with the helpers that should always be available.
1122 *
1123 * @return HelperSet
1124 */
1125 protected function getDefaultHelperSet()
1126 {
1127 return new HelperSet([
1128 new FormatterHelper(),
1129 new DebugFormatterHelper(),
1130 new ProcessHelper(),
1131 new QuestionHelper(),
1132 ]);
1133 }
1134
1135 /**
1136 * Returns abbreviated suggestions in string format.
1137 */
1138 private function getAbbreviationSuggestions(array $abbrevs): string
1139 {
1140 return ' '.implode("\n ", $abbrevs);
1141 }
1142
1143 /**
1144 * Returns the namespace part of the command name.
1145 *
1146 * This method is not part of public API and should not be used directly.
1147 *
1148 * @return string
1149 */
1150 public function extractNamespace(string $name, ?int $limit = null)
1151 {
1152 $parts = explode(':', $name, -1);
1153
1154 return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit));
1155 }
1156
1157 /**
1158 * Finds alternative of $name among $collection,
1159 * if nothing is found in $collection, try in $abbrevs.
1160 *
1161 * @return string[]
1162 */
1163 private function findAlternatives(string $name, iterable $collection): array
1164 {
1165 $threshold = 1e3;
1166 $alternatives = [];
1167
1168 $collectionParts = [];
1169 foreach ($collection as $item) {
1170 $collectionParts[$item] = explode(':', $item);
1171 }
1172
1173 foreach (explode(':', $name) as $i => $subname) {
1174 foreach ($collectionParts as $collectionName => $parts) {
1175 $exists = isset($alternatives[$collectionName]);
1176 if (!isset($parts[$i]) && $exists) {
1177 $alternatives[$collectionName] += $threshold;
1178 continue;
1179 } elseif (!isset($parts[$i])) {
1180 continue;
1181 }
1182
1183 $lev = levenshtein($subname, $parts[$i]);
1184 if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) {
1185 $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
1186 } elseif ($exists) {
1187 $alternatives[$collectionName] += $threshold;
1188 }
1189 }
1190 }
1191
1192 foreach ($collection as $item) {
1193 $lev = levenshtein($name, $item);
1194 if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
1195 $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
1196 }
1197 }
1198
1199 $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
1200 ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
1201
1202 return array_keys($alternatives);
1203 }
1204
1205 /**
1206 * Sets the default Command name.
1207 *
1208 * @return $this
1209 */
1210 public function setDefaultCommand(string $commandName, bool $isSingleCommand = false)
1211 {
1212 $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];
1213
1214 if ($isSingleCommand) {
1215 // Ensure the command exist
1216 $this->find($commandName);
1217
1218 $this->singleCommand = true;
1219 }
1220
1221 return $this;
1222 }
1223
1224 /**
1225 * @internal
1226 */
1227 public function isSingleCommand(): bool
1228 {
1229 return $this->singleCommand;
1230 }
1231
1232 private function splitStringByWidth(string $string, int $width): array
1233 {
1234 // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
1235 // additionally, array_slice() is not enough as some character has doubled width.
1236 // we need a function to split string not by character count but by string width
1237 if (false === $encoding = mb_detect_encoding($string, null, true)) {
1238 return str_split($string, $width);
1239 }
1240
1241 $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
1242 $lines = [];
1243 $line = '';
1244
1245 $offset = 0;
1246 while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) {
1247 $offset += \strlen($m[0]);
1248
1249 foreach (preg_split('//u', $m[0]) as $char) {
1250 // test if $char could be appended to current line
1251 if (mb_strwidth($line.$char, 'utf8') <= $width) {
1252 $line .= $char;
1253 continue;
1254 }
1255 // if not, push current line to array and make new line
1256 $lines[] = str_pad($line, $width);
1257 $line = $char;
1258 }
1259 }
1260
1261 $lines[] = \count($lines) ? str_pad($line, $width) : $line;
1262
1263 mb_convert_variables($encoding, 'utf8', $lines);
1264
1265 return $lines;
1266 }
1267
1268 /**
1269 * Returns all namespaces of the command name.
1270 *
1271 * @return string[]
1272 */
1273 private function extractAllNamespaces(string $name): array
1274 {
1275 // -1 as third argument is needed to skip the command short name when exploding
1276 $parts = explode(':', $name, -1);
1277 $namespaces = [];
1278
1279 foreach ($parts as $part) {
1280 if (\count($namespaces)) {
1281 $namespaces[] = end($namespaces).':'.$part;
1282 } else {
1283 $namespaces[] = $part;
1284 }
1285 }
1286
1287 return $namespaces;
1288 }
1289
1290 private function init()
1291 {
1292 if ($this->initialized) {
1293 return;
1294 }
1295 $this->initialized = true;
1296
1297 foreach ($this->getDefaultCommands() as $command) {
1298 $this->add($command);
1299 }
1300 }
1301 }
1302