Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

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