Completed
Push — master ( 2b1385...7c6a84 )
by Thomas
07:21
created

Application   F

Complexity

Total Complexity 172

Size/Duplication

Total Lines 1082
Duplicated Lines 6.28 %

Coupling/Cohesion

Components 1
Dependencies 28

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 172
c 1
b 0
f 0
lcom 1
cbo 28
dl 68
loc 1082
rs 0.6157

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Application often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Application, and based on these observations, apply Extract Interface, too.

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