GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( cc39b3...2b121f )
by Anton
04:25 queued 01:05
created

Application::run()   F

Complexity

Conditions 20
Paths 2280

Size

Total Lines 74
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 20
eloc 46
c 2
b 0
f 0
nc 2280
nop 2
dl 0
loc 74
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
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;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\ErrorHandler\ErrorHandler was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 <[email protected]>
72
 */
73
class Application implements ResetInterface
74
{
75
    private array $commands = [];
76
    private bool $wantHelps = false;
77
    private ?Command $runningCommand = null;
78
    private ?CommandLoaderInterface $commandLoader = null;
79
    private bool $catchExceptions = true;
80
    private bool $catchErrors = false;
81
    private bool $autoExit = true;
82
    private InputDefinition $definition;
83
    private HelperSet $helperSet;
84
    private ?EventDispatcherInterface $dispatcher = null;
85
    private Terminal $terminal;
86
    private string $defaultCommand;
87
    private bool $singleCommand = false;
88
    private bool $initialized = false;
89
    private ?SignalRegistry $signalRegistry = null;
90
    private array $signalsToDispatchEvent = [];
91
92
    public function __construct(
93
        private string $name = 'UNKNOWN',
94
        private string $version = 'UNKNOWN',
95
    ) {
96
        $this->terminal = new Terminal();
97
        $this->defaultCommand = 'list';
98
        if (\defined('SIGINT') && SignalRegistry::isSupported()) {
99
            $this->signalRegistry = new SignalRegistry();
100
            $this->signalsToDispatchEvent = [\SIGINT, \SIGQUIT, \SIGTERM, \SIGUSR1, \SIGUSR2];
101
        }
102
    }
103
104
    /**
105
     * @final
106
     */
107
    public function setDispatcher(EventDispatcherInterface $dispatcher): void
108
    {
109
        $this->dispatcher = $dispatcher;
110
    }
111
112
    public function setCommandLoader(CommandLoaderInterface $commandLoader): void
113
    {
114
        $this->commandLoader = $commandLoader;
115
    }
116
117
    public function getSignalRegistry(): SignalRegistry
118
    {
119
        if (!$this->signalRegistry) {
120
            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.');
121
        }
122
123
        return $this->signalRegistry;
124
    }
125
126
    public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void
127
    {
128
        $this->signalsToDispatchEvent = $signalsToDispatchEvent;
129
    }
130
131
    /**
132
     * Runs the current application.
133
     *
134
     * @return int 0 if everything went fine, or an error code
135
     *
136
     * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
137
     */
138
    public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
139
    {
140
        if (\function_exists('putenv')) {
141
            @putenv('LINES='.$this->terminal->getHeight());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for putenv(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

141
            /** @scrutinizer ignore-unhandled */ @putenv('LINES='.$this->terminal->getHeight());

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
142
            @putenv('COLUMNS='.$this->terminal->getWidth());
143
        }
144
145
        $input ??= new ArgvInput();
146
        $output ??= new ConsoleOutput();
147
148
        $renderException = function (\Throwable $e) use ($output) {
149
            if ($output instanceof ConsoleOutputInterface) {
150
                $this->renderThrowable($e, $output->getErrorOutput());
151
            } else {
152
                $this->renderThrowable($e, $output);
153
            }
154
        };
155
        if ($phpHandler = set_exception_handler($renderException)) {
156
            restore_exception_handler();
157
            if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) {
158
                $errorHandler = true;
159
            } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) {
160
                $phpHandler[0]->setExceptionHandler($errorHandler);
161
            }
162
        }
163
164
        try {
165
            $this->configureIO($input, $output);
166
167
            $exitCode = $this->doRun($input, $output);
168
        } catch (\Throwable $e) {
169
            if ($e instanceof \Exception && !$this->catchExceptions) {
170
                throw $e;
171
            }
172
            if (!$e instanceof \Exception && !$this->catchErrors) {
173
                throw $e;
174
            }
175
176
            $renderException($e);
177
178
            $exitCode = $e->getCode();
179
            if (is_numeric($exitCode)) {
0 ignored issues
show
introduced by
The condition is_numeric($exitCode) is always true.
Loading history...
180
                $exitCode = (int) $exitCode;
181
                if ($exitCode <= 0) {
182
                    $exitCode = 1;
183
                }
184
            } else {
185
                $exitCode = 1;
186
            }
187
        } finally {
188
            // if the exception handler changed, keep it
189
            // otherwise, unregister $renderException
190
            if (!$phpHandler) {
191
                if (set_exception_handler($renderException) === $renderException) {
192
                    restore_exception_handler();
193
                }
194
                restore_exception_handler();
195
            } elseif (!$errorHandler) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $errorHandler does not seem to be defined for all execution paths leading up to this point.
Loading history...
196
                $finalHandler = $phpHandler[0]->setExceptionHandler(null);
197
                if ($finalHandler !== $renderException) {
198
                    $phpHandler[0]->setExceptionHandler($finalHandler);
199
                }
200
            }
201
        }
202
203
        if ($this->autoExit) {
204
            if ($exitCode > 255) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $exitCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
205
                $exitCode = 255;
206
            }
207
208
            exit($exitCode);
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return integer. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
209
        }
210
211
        return $exitCode;
212
    }
213
214
    /**
215
     * Runs the current application.
216
     *
217
     * @return int 0 if everything went fine, or an error code
218
     */
219
    public function doRun(InputInterface $input, OutputInterface $output): int
220
    {
221
        if (true === $input->hasParameterOption(['--version', '-V'], true)) {
222
            $output->writeln($this->getLongVersion());
223
224
            return 0;
225
        }
226
227
        try {
228
            // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
229
            $input->bind($this->getDefinition());
230
        } catch (ExceptionInterface) {
231
            // Errors must be ignored, full binding/validation happens later when the command is known.
232
        }
233
234
        $name = $this->getCommandName($input);
235
        if (true === $input->hasParameterOption(['--help', '-h'], true)) {
236
            if (!$name) {
237
                $name = 'help';
238
                $input = new ArrayInput(['command_name' => $this->defaultCommand]);
239
            } else {
240
                $this->wantHelps = true;
241
            }
242
        }
243
244
        if (!$name) {
245
            $name = $this->defaultCommand;
246
            $definition = $this->getDefinition();
247
            $definition->setArguments(array_merge(
248
                $definition->getArguments(),
249
                [
250
                    'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name),
251
                ]
252
            ));
253
        }
254
255
        try {
256
            $this->runningCommand = null;
257
            // the command name MUST be the first element of the input
258
            $command = $this->find($name);
259
        } catch (\Throwable $e) {
260
            if (($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) && 1 === \count($alternatives = $e->getAlternatives()) && $input->isInteractive()) {
261
                $alternative = $alternatives[0];
262
263
                $style = new SymfonyStyle($input, $output);
264
                $output->writeln('');
265
                $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true);
266
                $output->writeln($formattedBlock);
267
                if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
268
                    if (null !== $this->dispatcher) {
269
                        $event = new ConsoleErrorEvent($input, $output, $e);
270
                        $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
271
272
                        return $event->getExitCode();
273
                    }
274
275
                    return 1;
276
                }
277
278
                $command = $this->find($alternative);
279
            } else {
280
                if (null !== $this->dispatcher) {
281
                    $event = new ConsoleErrorEvent($input, $output, $e);
282
                    $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
283
284
                    if (0 === $event->getExitCode()) {
285
                        return 0;
286
                    }
287
288
                    $e = $event->getError();
289
                }
290
291
                try {
292
                    if ($e instanceof CommandNotFoundException && $namespace = $this->findNamespace($name)) {
293
                        $helper = new DescriptorHelper();
294
                        $helper->describe($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output, $this, [
295
                            'format' => 'txt',
296
                            'raw_text' => false,
297
                            'namespace' => $namespace,
298
                            'short' => false,
299
                        ]);
300
301
                        return isset($event) ? $event->getExitCode() : 1;
302
                    }
303
304
                    throw $e;
305
                } catch (NamespaceNotFoundException) {
306
                    throw $e;
307
                }
308
            }
309
        }
310
311
        if ($command instanceof LazyCommand) {
312
            $command = $command->getCommand();
313
        }
314
315
        $this->runningCommand = $command;
316
        $exitCode = $this->doRunCommand($command, $input, $output);
317
        $this->runningCommand = null;
318
319
        return $exitCode;
320
    }
321
322
    public function reset(): void
323
    {
324
    }
325
326
    public function setHelperSet(HelperSet $helperSet): void
327
    {
328
        $this->helperSet = $helperSet;
329
    }
330
331
    /**
332
     * Get the helper set associated with the command.
333
     */
334
    public function getHelperSet(): HelperSet
335
    {
336
        return $this->helperSet ??= $this->getDefaultHelperSet();
337
    }
338
339
    public function setDefinition(InputDefinition $definition): void
340
    {
341
        $this->definition = $definition;
342
    }
343
344
    /**
345
     * Gets the InputDefinition related to this Application.
346
     */
347
    public function getDefinition(): InputDefinition
348
    {
349
        $this->definition ??= $this->getDefaultInputDefinition();
350
351
        if ($this->singleCommand) {
352
            $inputDefinition = $this->definition;
353
            $inputDefinition->setArguments();
354
355
            return $inputDefinition;
356
        }
357
358
        return $this->definition;
359
    }
360
361
    /**
362
     * Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
363
     */
364
    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
365
    {
366
        if (
367
            CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
368
            && 'command' === $input->getCompletionName()
369
        ) {
370
            foreach ($this->all() as $name => $command) {
371
                // skip hidden commands and aliased commands as they already get added below
372
                if ($command->isHidden() || $command->getName() !== $name) {
373
                    continue;
374
                }
375
                $suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription()));
376
                foreach ($command->getAliases() as $name) {
0 ignored issues
show
Comprehensibility Bug introduced by
$name is overwriting a variable from outer foreach loop.
Loading history...
377
                    $suggestions->suggestValue(new Suggestion($name, $command->getDescription()));
378
                }
379
            }
380
381
            return;
382
        }
383
384
        if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
385
            $suggestions->suggestOptions($this->getDefinition()->getOptions());
386
387
            return;
388
        }
389
    }
390
391
    /**
392
     * Gets the help message.
393
     */
394
    public function getHelp(): string
395
    {
396
        return $this->getLongVersion();
397
    }
398
399
    /**
400
     * Gets whether to catch exceptions or not during commands execution.
401
     */
402
    public function areExceptionsCaught(): bool
403
    {
404
        return $this->catchExceptions;
405
    }
406
407
    /**
408
     * Sets whether to catch exceptions or not during commands execution.
409
     */
410
    public function setCatchExceptions(bool $boolean): void
411
    {
412
        $this->catchExceptions = $boolean;
413
    }
414
415
    /**
416
     * Sets whether to catch errors or not during commands execution.
417
     */
418
    public function setCatchErrors(bool $catchErrors = true): void
419
    {
420
        $this->catchErrors = $catchErrors;
421
    }
422
423
    /**
424
     * Gets whether to automatically exit after a command execution or not.
425
     */
426
    public function isAutoExitEnabled(): bool
427
    {
428
        return $this->autoExit;
429
    }
430
431
    /**
432
     * Sets whether to automatically exit after a command execution or not.
433
     */
434
    public function setAutoExit(bool $boolean): void
435
    {
436
        $this->autoExit = $boolean;
437
    }
438
439
    /**
440
     * Gets the name of the application.
441
     */
442
    public function getName(): string
443
    {
444
        return $this->name;
445
    }
446
447
    /**
448
     * Sets the application name.
449
     */
450
    public function setName(string $name): void
451
    {
452
        $this->name = $name;
453
    }
454
455
    /**
456
     * Gets the application version.
457
     */
458
    public function getVersion(): string
459
    {
460
        return $this->version;
461
    }
462
463
    /**
464
     * Sets the application version.
465
     */
466
    public function setVersion(string $version): void
467
    {
468
        $this->version = $version;
469
    }
470
471
    /**
472
     * Returns the long version of the application.
473
     */
474
    public function getLongVersion(): string
475
    {
476
        if ('UNKNOWN' !== $this->getName()) {
477
            if ('UNKNOWN' !== $this->getVersion()) {
478
                return sprintf('%s <info>%s</info>', $this->getName(), $this->getVersion());
479
            }
480
481
            return $this->getName();
482
        }
483
484
        return 'Console Tool';
485
    }
486
487
    /**
488
     * Registers a new command.
489
     */
490
    public function register(string $name): Command
491
    {
492
        return $this->add(new Command($name));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->add(new Sy...Command\Command($name)) could return the type null which is incompatible with the type-hinted return Symfony\Component\Console\Command\Command. Consider adding an additional type-check to rule them out.
Loading history...
493
    }
494
495
    /**
496
     * Adds an array of command objects.
497
     *
498
     * If a Command is not enabled it will not be added.
499
     *
500
     * @param Command[] $commands An array of commands
501
     */
502
    public function addCommands(array $commands): void
503
    {
504
        foreach ($commands as $command) {
505
            $this->add($command);
506
        }
507
    }
508
509
    /**
510
     * Adds a command object.
511
     *
512
     * If a command with the same name already exists, it will be overridden.
513
     * If the command is not enabled it will not be added.
514
     */
515
    public function add(Command $command): ?Command
516
    {
517
        $this->init();
518
519
        $command->setApplication($this);
520
521
        if (!$command->isEnabled()) {
522
            $command->setApplication(null);
523
524
            return null;
525
        }
526
527
        if (!$command instanceof LazyCommand) {
528
            // Will throw if the command is not correctly initialized.
529
            $command->getDefinition();
530
        }
531
532
        if (!$command->getName()) {
533
            throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
534
        }
535
536
        $this->commands[$command->getName()] = $command;
537
538
        foreach ($command->getAliases() as $alias) {
539
            $this->commands[$alias] = $command;
540
        }
541
542
        return $command;
543
    }
544
545
    /**
546
     * Returns a registered command by name or alias.
547
     *
548
     * @throws CommandNotFoundException When given command name does not exist
549
     */
550
    public function get(string $name): Command
551
    {
552
        $this->init();
553
554
        if (!$this->has($name)) {
555
            throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
556
        }
557
558
        // When the command has a different name than the one used at the command loader level
559
        if (!isset($this->commands[$name])) {
560
            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));
561
        }
562
563
        $command = $this->commands[$name];
564
565
        if ($this->wantHelps) {
566
            $this->wantHelps = false;
567
568
            $helpCommand = $this->get('help');
569
            $helpCommand->setCommand($command);
0 ignored issues
show
Bug introduced by
The method setCommand() does not exist on Symfony\Component\Console\Command\Command. Did you maybe mean setCode()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

569
            $helpCommand->/** @scrutinizer ignore-call */ 
570
                          setCommand($command);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
570
571
            return $helpCommand;
572
        }
573
574
        return $command;
575
    }
576
577
    /**
578
     * Returns true if the command exists, false otherwise.
579
     */
580
    public function has(string $name): bool
581
    {
582
        $this->init();
583
584
        return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->get($name)));
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

584
        return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->/** @scrutinizer ignore-call */ get($name)));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
585
    }
586
587
    /**
588
     * Returns an array of all unique namespaces used by currently registered commands.
589
     *
590
     * It does not return the global namespace which always exists.
591
     *
592
     * @return string[]
593
     */
594
    public function getNamespaces(): array
595
    {
596
        $namespaces = [];
597
        foreach ($this->all() as $command) {
598
            if ($command->isHidden()) {
599
                continue;
600
            }
601
602
            $namespaces[] = $this->extractAllNamespaces($command->getName());
603
604
            foreach ($command->getAliases() as $alias) {
605
                $namespaces[] = $this->extractAllNamespaces($alias);
606
            }
607
        }
608
609
        return array_values(array_unique(array_filter(array_merge([], ...$namespaces))));
610
    }
611
612
    /**
613
     * Finds a registered namespace by a name or an abbreviation.
614
     *
615
     * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
616
     */
617
    public function findNamespace(string $namespace): string
618
    {
619
        $allNamespaces = $this->getNamespaces();
620
        $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*';
621
        $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
622
623
        if (!$namespaces) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $namespaces of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
624
            $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
625
626
            if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
627
                if (1 == \count($alternatives)) {
628
                    $message .= "\n\nDid you mean this?\n    ";
629
                } else {
630
                    $message .= "\n\nDid you mean one of these?\n    ";
631
                }
632
633
                $message .= implode("\n    ", $alternatives);
634
            }
635
636
            throw new NamespaceNotFoundException($message, $alternatives);
637
        }
638
639
        $exact = \in_array($namespace, $namespaces, true);
640
        if (\count($namespaces) > 1 && !$exact) {
641
            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));
642
        }
643
644
        return $exact ? $namespace : reset($namespaces);
645
    }
646
647
    /**
648
     * Finds a command by name or alias.
649
     *
650
     * Contrary to get, this command tries to find the best
651
     * match if you give it an abbreviation of a name or alias.
652
     *
653
     * @throws CommandNotFoundException When command name is incorrect or ambiguous
654
     */
655
    public function find(string $name): Command
656
    {
657
        $this->init();
658
659
        $aliases = [];
660
661
        foreach ($this->commands as $command) {
662
            foreach ($command->getAliases() as $alias) {
663
                if (!$this->has($alias)) {
664
                    $this->commands[$alias] = $command;
665
                }
666
            }
667
        }
668
669
        if ($this->has($name)) {
670
            return $this->get($name);
671
        }
672
673
        $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
674
        $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*';
675
        $commands = preg_grep('{^'.$expr.'}', $allCommands);
676
677
        if (!$commands) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $commands of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
678
            $commands = preg_grep('{^'.$expr.'}i', $allCommands);
679
        }
680
681
        // if no commands matched or we just matched namespaces
682
        if (!$commands || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $commands of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
683
            if (false !== $pos = strrpos($name, ':')) {
684
                // check if a namespace exists and contains commands
685
                $this->findNamespace(substr($name, 0, $pos));
686
            }
687
688
            $message = sprintf('Command "%s" is not defined.', $name);
689
690
            if ($alternatives = $this->findAlternatives($name, $allCommands)) {
691
                // remove hidden commands
692
                $alternatives = array_filter($alternatives, fn ($name) => !$this->get($name)->isHidden());
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, true);
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) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $commandList does not seem to be defined for all execution paths leading up to this point.
Loading history...
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): array
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]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message);
844
            }
845
846
            $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
847
            $lines = [];
848
            foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
849
                foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
0 ignored issues
show
Comprehensibility Bug introduced by
$line is overwriting a variable from outer foreach loop.
Loading history...
850
                    // pre-format lines to get the right string length
851
                    $lineLength = Helper::width($line) + 4;
852
                    $lines[] = [$line, $lineLength];
853
854
                    $len = max($lineLength, $len);
855
                }
856
            }
857
858
            $messages = [];
859
            if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
860
                $messages[] = sprintf('<comment>%s</comment>', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
861
            }
862
            $messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
863
            if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
864
                $messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $title does not seem to be defined for all execution paths leading up to this point.
Loading history...
865
            }
866
            foreach ($lines as $line) {
867
                $messages[] = sprintf('<error>  %s  %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
868
            }
869
            $messages[] = $emptyLine;
870
            $messages[] = '';
871
872
            $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
873
874
            if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
875
                $output->writeln('<comment>Exception trace:</comment>', OutputInterface::VERBOSITY_QUIET);
876
877
                // exception related properties
878
                $trace = $e->getTrace();
879
880
                array_unshift($trace, [
881
                    'function' => '',
882
                    'file' => $e->getFile() ?: 'n/a',
883
                    'line' => $e->getLine() ?: 'n/a',
884
                    'args' => [],
885
                ]);
886
887
                for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
888
                    $class = $trace[$i]['class'] ?? '';
889
                    $type = $trace[$i]['type'] ?? '';
890
                    $function = $trace[$i]['function'] ?? '';
891
                    $file = $trace[$i]['file'] ?? 'n/a';
892
                    $line = $trace[$i]['line'] ?? 'n/a';
893
894
                    $output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
895
                }
896
897
                $output->writeln('', OutputInterface::VERBOSITY_QUIET);
898
            }
899
        } while ($e = $e->getPrevious());
900
    }
901
902
    /**
903
     * Configures the input and output instances based on the user arguments and options.
904
     */
905
    protected function configureIO(InputInterface $input, OutputInterface $output): void
906
    {
907
        if (true === $input->hasParameterOption(['--ansi'], true)) {
908
            $output->setDecorated(true);
909
        } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) {
910
            $output->setDecorated(false);
911
        }
912
913
        if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) {
914
            $input->setInteractive(false);
915
        }
916
917
        switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) {
918
            case -1:
919
                $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
920
                break;
921
            case 1:
922
                $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
923
                break;
924
            case 2:
925
                $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
926
                break;
927
            case 3:
928
                $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
929
                break;
930
            default:
931
                $shellVerbosity = 0;
932
                break;
933
        }
934
935
        if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
936
            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
937
            $shellVerbosity = -1;
938
        } else {
939
            if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) {
940
                $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);
941
                $shellVerbosity = 3;
942
            } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) {
943
                $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);
944
                $shellVerbosity = 2;
945
            } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) {
946
                $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
947
                $shellVerbosity = 1;
948
            }
949
        }
950
951
        if (-1 === $shellVerbosity) {
952
            $input->setInteractive(false);
953
        }
954
955
        if (\function_exists('putenv')) {
956
            @putenv('SHELL_VERBOSITY='.$shellVerbosity);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for putenv(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

956
            /** @scrutinizer ignore-unhandled */ @putenv('SHELL_VERBOSITY='.$shellVerbosity);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
957
        }
958
        $_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
959
        $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
960
    }
961
962
    /**
963
     * Runs the current command.
964
     *
965
     * If an event dispatcher has been attached to the application,
966
     * events are also dispatched during the life-cycle of the command.
967
     *
968
     * @return int 0 if everything went fine, or an error code
969
     */
970
    protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int
971
    {
972
        foreach ($command->getHelperSet() as $helper) {
973
            if ($helper instanceof InputAwareInterface) {
974
                $helper->setInput($input);
975
            }
976
        }
977
978
        $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
979
        if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->signalsToDispatchEvent of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
980
            if (!$this->signalRegistry) {
981
                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.');
982
            }
983
984
            if (Terminal::hasSttyAvailable()) {
985
                $sttyMode = shell_exec('stty -g');
986
987
                foreach ([\SIGINT, \SIGQUIT, \SIGTERM] as $signal) {
988
                    $this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
989
                }
990
            }
991
992
            if ($this->dispatcher) {
993
                // We register application signals, so that we can dispatch the event
994
                foreach ($this->signalsToDispatchEvent as $signal) {
995
                    $event = new ConsoleSignalEvent($command, $input, $output, $signal);
996
997
                    $this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
998
                        $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
0 ignored issues
show
Bug introduced by
The method dispatch() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

998
                        $this->dispatcher->/** @scrutinizer ignore-call */ 
999
                                           dispatch($event, ConsoleEvents::SIGNAL);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
999
                        $exitCode = $event->getExitCode();
1000
1001
                        // If the command is signalable, we call the handleSignal() method
1002
                        if (\in_array($signal, $commandSignals, true)) {
1003
                            $exitCode = $command->handleSignal($signal, $exitCode);
0 ignored issues
show
Bug introduced by
The method handleSignal() does not exist on Symfony\Component\Console\Command\Command. It seems like you code against a sub-type of Symfony\Component\Console\Command\Command such as Symfony\Component\Console\Command\TraceableCommand. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1003
                            /** @scrutinizer ignore-call */ 
1004
                            $exitCode = $command->handleSignal($signal, $exitCode);
Loading history...
1004
                        }
1005
1006
                        if (false !== $exitCode) {
1007
                            $event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal);
1008
                            $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
1009
1010
                            exit($event->getExitCode());
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1011
                        }
1012
                    });
1013
                }
1014
1015
                // then we register command signals, but not if already handled after the dispatcher
1016
                $commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent);
1017
            }
1018
1019
            foreach ($commandSignals as $signal) {
1020
                $this->signalRegistry->register($signal, function (int $signal) use ($command): void {
1021
                    if (false !== $exitCode = $command->handleSignal($signal)) {
1022
                        exit($exitCode);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1023
                    }
1024
                });
1025
            }
1026
        }
1027
1028
        if (null === $this->dispatcher) {
1029
            return $command->run($input, $output);
1030
        }
1031
1032
        // bind before the console.command event, so the listeners have access to input options/arguments
1033
        try {
1034
            $command->mergeApplicationDefinition();
1035
            $input->bind($command->getDefinition());
1036
        } catch (ExceptionInterface) {
1037
            // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
1038
        }
1039
1040
        $event = new ConsoleCommandEvent($command, $input, $output);
1041
        $e = null;
1042
1043
        try {
1044
            $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND);
1045
1046
            if ($event->commandShouldRun()) {
1047
                $exitCode = $command->run($input, $output);
1048
            } else {
1049
                $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
1050
            }
1051
        } catch (\Throwable $e) {
1052
            $event = new ConsoleErrorEvent($input, $output, $e, $command);
1053
            $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
1054
            $e = $event->getError();
1055
1056
            if (0 === $exitCode = $event->getExitCode()) {
1057
                $e = null;
1058
            }
1059
        }
1060
1061
        $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
1062
        $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
1063
1064
        if (null !== $e) {
1065
            throw $e;
1066
        }
1067
1068
        return $event->getExitCode();
1069
    }
1070
1071
    /**
1072
     * Gets the name of the command based on input.
1073
     */
1074
    protected function getCommandName(InputInterface $input): ?string
1075
    {
1076
        return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
1077
    }
1078
1079
    /**
1080
     * Gets the default input definition.
1081
     */
1082
    protected function getDefaultInputDefinition(): InputDefinition
1083
    {
1084
        return new InputDefinition([
1085
            new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
1086
            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'),
1087
            new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
1088
            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'),
1089
            new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
1090
            new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null),
1091
            new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
1092
        ]);
1093
    }
1094
1095
    /**
1096
     * Gets the default commands that should always be available.
1097
     *
1098
     * @return Command[]
1099
     */
1100
    protected function getDefaultCommands(): array
1101
    {
1102
        return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
1103
    }
1104
1105
    /**
1106
     * Gets the default helper set with the helpers that should always be available.
1107
     */
1108
    protected function getDefaultHelperSet(): HelperSet
1109
    {
1110
        return new HelperSet([
1111
            new FormatterHelper(),
1112
            new DebugFormatterHelper(),
1113
            new ProcessHelper(),
1114
            new QuestionHelper(),
1115
        ]);
1116
    }
1117
1118
    /**
1119
     * Returns abbreviated suggestions in string format.
1120
     */
1121
    private function getAbbreviationSuggestions(array $abbrevs): string
1122
    {
1123
        return '    '.implode("\n    ", $abbrevs);
1124
    }
1125
1126
    /**
1127
     * Returns the namespace part of the command name.
1128
     *
1129
     * This method is not part of public API and should not be used directly.
1130
     */
1131
    public function extractNamespace(string $name, ?int $limit = null): string
1132
    {
1133
        $parts = explode(':', $name, -1);
1134
1135
        return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit));
1136
    }
1137
1138
    /**
1139
     * Finds alternative of $name among $collection,
1140
     * if nothing is found in $collection, try in $abbrevs.
1141
     *
1142
     * @return string[]
1143
     */
1144
    private function findAlternatives(string $name, iterable $collection): array
1145
    {
1146
        $threshold = 1e3;
1147
        $alternatives = [];
1148
1149
        $collectionParts = [];
1150
        foreach ($collection as $item) {
1151
            $collectionParts[$item] = explode(':', $item);
1152
        }
1153
1154
        foreach (explode(':', $name) as $i => $subname) {
1155
            foreach ($collectionParts as $collectionName => $parts) {
1156
                $exists = isset($alternatives[$collectionName]);
1157
                if (!isset($parts[$i]) && $exists) {
1158
                    $alternatives[$collectionName] += $threshold;
1159
                    continue;
1160
                } elseif (!isset($parts[$i])) {
1161
                    continue;
1162
                }
1163
1164
                $lev = levenshtein($subname, $parts[$i]);
1165
                if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) {
1166
                    $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
1167
                } elseif ($exists) {
1168
                    $alternatives[$collectionName] += $threshold;
1169
                }
1170
            }
1171
        }
1172
1173
        foreach ($collection as $item) {
1174
            $lev = levenshtein($name, $item);
1175
            if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
1176
                $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
1177
            }
1178
        }
1179
1180
        $alternatives = array_filter($alternatives, fn ($lev) => $lev < 2 * $threshold);
1181
        ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
1182
1183
        return array_keys($alternatives);
1184
    }
1185
1186
    /**
1187
     * Sets the default Command name.
1188
     *
1189
     * @return $this
1190
     */
1191
    public function setDefaultCommand(string $commandName, bool $isSingleCommand = false): static
1192
    {
1193
        $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];
1194
1195
        if ($isSingleCommand) {
1196
            // Ensure the command exist
1197
            $this->find($commandName);
1198
1199
            $this->singleCommand = true;
1200
        }
1201
1202
        return $this;
1203
    }
1204
1205
    /**
1206
     * @internal
1207
     */
1208
    public function isSingleCommand(): bool
1209
    {
1210
        return $this->singleCommand;
1211
    }
1212
1213
    private function splitStringByWidth(string $string, int $width): array
1214
    {
1215
        // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
1216
        // additionally, array_slice() is not enough as some character has doubled width.
1217
        // we need a function to split string not by character count but by string width
1218
        if (false === $encoding = mb_detect_encoding($string, null, true)) {
1219
            return str_split($string, $width);
0 ignored issues
show
Bug Best Practice introduced by
The expression return str_split($string, $width) could return the type true which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
1220
        }
1221
1222
        $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
1223
        $lines = [];
1224
        $line = '';
1225
1226
        $offset = 0;
1227
        while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) {
0 ignored issues
show
Bug introduced by
It seems like $utf8String can also be of type array; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1227
        while (preg_match('/.{1,10000}/u', /** @scrutinizer ignore-type */ $utf8String, $m, 0, $offset)) {
Loading history...
1228
            $offset += \strlen($m[0]);
1229
1230
            foreach (preg_split('//u', $m[0]) as $char) {
1231
                // test if $char could be appended to current line
1232
                if (mb_strwidth($line.$char, 'utf8') <= $width) {
1233
                    $line .= $char;
1234
                    continue;
1235
                }
1236
                // if not, push current line to array and make new line
1237
                $lines[] = str_pad($line, $width);
1238
                $line = $char;
1239
            }
1240
        }
1241
1242
        $lines[] = \count($lines) ? str_pad($line, $width) : $line;
1243
1244
        mb_convert_variables($encoding, 'utf8', $lines);
1245
1246
        return $lines;
1247
    }
1248
1249
    /**
1250
     * Returns all namespaces of the command name.
1251
     *
1252
     * @return string[]
1253
     */
1254
    private function extractAllNamespaces(string $name): array
1255
    {
1256
        // -1 as third argument is needed to skip the command short name when exploding
1257
        $parts = explode(':', $name, -1);
1258
        $namespaces = [];
1259
1260
        foreach ($parts as $part) {
1261
            if (\count($namespaces)) {
1262
                $namespaces[] = end($namespaces).':'.$part;
1263
            } else {
1264
                $namespaces[] = $part;
1265
            }
1266
        }
1267
1268
        return $namespaces;
1269
    }
1270
1271
    private function init(): void
1272
    {
1273
        if ($this->initialized) {
1274
            return;
1275
        }
1276
        $this->initialized = true;
1277
1278
        foreach ($this->getDefaultCommands() as $command) {
1279
            $this->add($command);
1280
        }
1281
    }
1282
}
1283