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