ConsoleOptionParser::parse()   F
last analyzed

Complexity

Conditions 19
Paths 224

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
nc 224
nop 1
dl 0
loc 45
rs 3.3833
c 0
b 0
f 0

How to fix   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
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         2.0.0
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Console;
16
17
use Cake\Console\Exception\ConsoleException;
18
use Cake\Utility\Inflector;
19
use LogicException;
20
21
/**
22
 * Handles parsing the ARGV in the command line and provides support
23
 * for GetOpt compatible option definition. Provides a builder pattern implementation
24
 * for creating shell option parsers.
25
 *
26
 * ### Options
27
 *
28
 * Named arguments come in two forms, long and short. Long arguments are preceded
29
 * by two - and give a more verbose option name. i.e. `--version`. Short arguments are
30
 * preceded by one - and are only one character long. They usually match with a long option,
31
 * and provide a more terse alternative.
32
 *
33
 * ### Using Options
34
 *
35
 * Options can be defined with both long and short forms. By using `$parser->addOption()`
36
 * you can define new options. The name of the option is used as its long form, and you
37
 * can supply an additional short form, with the `short` option. Short options should
38
 * only be one letter long. Using more than one letter for a short option will raise an exception.
39
 *
40
 * Calling options can be done using syntax similar to most *nix command line tools. Long options
41
 * cane either include an `=` or leave it out.
42
 *
43
 * `cake myshell command --connection default --name=something`
44
 *
45
 * Short options can be defined singly or in groups.
46
 *
47
 * `cake myshell command -cn`
48
 *
49
 * Short options can be combined into groups as seen above. Each letter in a group
50
 * will be treated as a separate option. The previous example is equivalent to:
51
 *
52
 * `cake myshell command -c -n`
53
 *
54
 * Short options can also accept values:
55
 *
56
 * `cake myshell command -c default`
57
 *
58
 * ### Positional arguments
59
 *
60
 * If no positional arguments are defined, all of them will be parsed. If you define positional
61
 * arguments any arguments greater than those defined will cause exceptions. Additionally you can
62
 * declare arguments as optional, by setting the required param to false.
63
 *
64
 * ```
65
 * $parser->addArgument('model', ['required' => false]);
66
 * ```
67
 *
68
 * ### Providing Help text
69
 *
70
 * By providing help text for your positional arguments and named arguments, the ConsoleOptionParser
71
 * can generate a help display for you. You can view the help for shells by using the `--help` or `-h` switch.
72
 */
73
class ConsoleOptionParser
74
{
75
    /**
76
     * Description text - displays before options when help is generated
77
     *
78
     * @see \Cake\Console\ConsoleOptionParser::description()
79
     * @var string
80
     */
81
    protected $_description;
82
83
    /**
84
     * Epilog text - displays after options when help is generated
85
     *
86
     * @see \Cake\Console\ConsoleOptionParser::epilog()
87
     * @var string
88
     */
89
    protected $_epilog;
90
91
    /**
92
     * Option definitions.
93
     *
94
     * @see \Cake\Console\ConsoleOptionParser::addOption()
95
     * @var \Cake\Console\ConsoleInputOption[]
96
     */
97
    protected $_options = [];
98
99
    /**
100
     * Map of short -> long options, generated when using addOption()
101
     *
102
     * @var array
103
     */
104
    protected $_shortOptions = [];
105
106
    /**
107
     * Positional argument definitions.
108
     *
109
     * @see \Cake\Console\ConsoleOptionParser::addArgument()
110
     * @var \Cake\Console\ConsoleInputArgument[]
111
     */
112
    protected $_args = [];
113
114
    /**
115
     * Subcommands for this Shell.
116
     *
117
     * @see \Cake\Console\ConsoleOptionParser::addSubcommand()
118
     * @var \Cake\Console\ConsoleInputSubcommand[]
119
     */
120
    protected $_subcommands = [];
121
122
    /**
123
     * Subcommand sorting option
124
     *
125
     * @var bool
126
     */
127
    protected $_subcommandSort = true;
128
129
    /**
130
     * Command name.
131
     *
132
     * @var string
133
     */
134
    protected $_command = '';
135
136
    /**
137
     * Array of args (argv).
138
     *
139
     * @var array
140
     */
141
    protected $_tokens = [];
142
143
    /**
144
     * Root alias used in help output
145
     *
146
     * @see \Cake\Console\HelpFormatter::setAlias()
147
     * @var string
148
     */
149
    protected $rootName = 'cake';
150
151
    /**
152
     * Construct an OptionParser so you can define its behavior
153
     *
154
     * @param string|null $command The command name this parser is for. The command name is used for generating help.
155
     * @param bool $defaultOptions Whether you want the verbose and quiet options set. Setting
156
     *  this to false will prevent the addition of `--verbose` & `--quiet` options.
157
     */
158
    public function __construct($command = null, $defaultOptions = true)
159
    {
160
        $this->setCommand($command);
161
162
        $this->addOption('help', [
163
            'short' => 'h',
164
            'help' => 'Display this help.',
165
            'boolean' => true,
166
        ]);
167
168
        if ($defaultOptions) {
169
            $this->addOption('verbose', [
170
                'short' => 'v',
171
                'help' => 'Enable verbose output.',
172
                'boolean' => true,
173
            ])->addOption('quiet', [
174
                'short' => 'q',
175
                'help' => 'Enable quiet output.',
176
                'boolean' => true,
177
            ]);
178
        }
179
    }
180
181
    /**
182
     * Static factory method for creating new OptionParsers so you can chain methods off of them.
183
     *
184
     * @param string|null $command The command name this parser is for. The command name is used for generating help.
185
     * @param bool $defaultOptions Whether you want the verbose and quiet options set.
186
     * @return static
187
     */
188
    public static function create($command, $defaultOptions = true)
189
    {
190
        return new static($command, $defaultOptions);
191
    }
192
193
    /**
194
     * Build a parser from an array. Uses an array like
195
     *
196
     * ```
197
     * $spec = [
198
     *      'description' => 'text',
199
     *      'epilog' => 'text',
200
     *      'arguments' => [
201
     *          // list of arguments compatible with addArguments.
202
     *      ],
203
     *      'options' => [
204
     *          // list of options compatible with addOptions
205
     *      ],
206
     *      'subcommands' => [
207
     *          // list of subcommands to add.
208
     *      ]
209
     * ];
210
     * ```
211
     *
212
     * @param array $spec The spec to build the OptionParser with.
213
     * @param bool $defaultOptions Whether you want the verbose and quiet options set.
214
     * @return static
215
     */
216
    public static function buildFromArray($spec, $defaultOptions = true)
217
    {
218
        $parser = new static($spec['command'], $defaultOptions);
219
        if (!empty($spec['arguments'])) {
220
            $parser->addArguments($spec['arguments']);
221
        }
222
        if (!empty($spec['options'])) {
223
            $parser->addOptions($spec['options']);
224
        }
225
        if (!empty($spec['subcommands'])) {
226
            $parser->addSubcommands($spec['subcommands']);
227
        }
228
        if (!empty($spec['description'])) {
229
            $parser->setDescription($spec['description']);
230
        }
231
        if (!empty($spec['epilog'])) {
232
            $parser->setEpilog($spec['epilog']);
233
        }
234
235
        return $parser;
236
    }
237
238
    /**
239
     * Returns an array representation of this parser.
240
     *
241
     * @return array
242
     */
243
    public function toArray()
244
    {
245
        $result = [
246
            'command' => $this->_command,
247
            'arguments' => $this->_args,
248
            'options' => $this->_options,
249
            'subcommands' => $this->_subcommands,
250
            'description' => $this->_description,
251
            'epilog' => $this->_epilog,
252
        ];
253
254
        return $result;
255
    }
256
257
    /**
258
     * Get or set the command name for shell/task.
259
     *
260
     * @param array|\Cake\Console\ConsoleOptionParser $spec ConsoleOptionParser or spec to merge with.
261
     * @return $this
262
     */
263
    public function merge($spec)
264
    {
265
        if ($spec instanceof ConsoleOptionParser) {
266
            $spec = $spec->toArray();
267
        }
268
        if (!empty($spec['arguments'])) {
269
            $this->addArguments($spec['arguments']);
270
        }
271
        if (!empty($spec['options'])) {
272
            $this->addOptions($spec['options']);
273
        }
274
        if (!empty($spec['subcommands'])) {
275
            $this->addSubcommands($spec['subcommands']);
276
        }
277
        if (!empty($spec['description'])) {
278
            $this->setDescription($spec['description']);
279
        }
280
        if (!empty($spec['epilog'])) {
281
            $this->setEpilog($spec['epilog']);
282
        }
283
284
        return $this;
285
    }
286
287
    /**
288
     * Sets the command name for shell/task.
289
     *
290
     * @param string $text The text to set.
291
     * @return $this
292
     */
293
    public function setCommand($text)
294
    {
295
        $this->_command = Inflector::underscore($text);
296
297
        return $this;
298
    }
299
300
    /**
301
     * Gets the command name for shell/task.
302
     *
303
     * @return string The value of the command.
304
     */
305
    public function getCommand()
306
    {
307
        return $this->_command;
308
    }
309
310
    /**
311
     * Gets or sets the command name for shell/task.
312
     *
313
     * @deprecated 3.4.0 Use setCommand()/getCommand() instead.
314
     * @param string|null $text The text to set, or null if you want to read
315
     * @return string|$this If reading, the value of the command. If setting $this will be returned.
316
     */
317
    public function command($text = null)
318
    {
319
        deprecationWarning(
320
            'ConsoleOptionParser::command() is deprecated. ' .
321
            'Use ConsoleOptionParser::setCommand()/getCommand() instead.'
322
        );
323
        if ($text !== null) {
324
            return $this->setCommand($text);
325
        }
326
327
        return $this->getCommand();
328
    }
329
330
    /**
331
     * Sets the description text for shell/task.
332
     *
333
     * @param string|array $text The text to set. If an array the
334
     *   text will be imploded with "\n".
335
     * @return $this
336
     */
337
    public function setDescription($text)
338
    {
339
        if (is_array($text)) {
340
            $text = implode("\n", $text);
341
        }
342
        $this->_description = $text;
343
344
        return $this;
345
    }
346
347
    /**
348
     * Gets the description text for shell/task.
349
     *
350
     * @return string The value of the description
351
     */
352
    public function getDescription()
353
    {
354
        return $this->_description;
355
    }
356
357
    /**
358
     * Get or set the description text for shell/task.
359
     *
360
     * @deprecated 3.4.0 Use setDescription()/getDescription() instead.
361
     * @param string|array|null $text The text to set, or null if you want to read. If an array the
362
     *   text will be imploded with "\n".
363
     * @return string|$this If reading, the value of the description. If setting $this will be returned.
364
     */
365 View Code Duplication
    public function description($text = null)
366
    {
367
        deprecationWarning(
368
            'ConsoleOptionParser::description() is deprecated. ' .
369
            'Use ConsoleOptionParser::setDescription()/getDescription() instead.'
370
        );
371
        if ($text !== null) {
372
            return $this->setDescription($text);
373
        }
374
375
        return $this->getDescription();
376
    }
377
378
    /**
379
     * Sets an epilog to the parser. The epilog is added to the end of
380
     * the options and arguments listing when help is generated.
381
     *
382
     * @param string|array $text The text to set. If an array the text will
383
     *   be imploded with "\n".
384
     * @return $this
385
     */
386
    public function setEpilog($text)
387
    {
388
        if (is_array($text)) {
389
            $text = implode("\n", $text);
390
        }
391
        $this->_epilog = $text;
392
393
        return $this;
394
    }
395
396
    /**
397
     * Gets the epilog.
398
     *
399
     * @return string The value of the epilog.
400
     */
401
    public function getEpilog()
402
    {
403
        return $this->_epilog;
404
    }
405
406
    /**
407
     * Gets or sets an epilog to the parser. The epilog is added to the end of
408
     * the options and arguments listing when help is generated.
409
     *
410
     * @deprecated 3.4.0 Use setEpilog()/getEpilog() instead.
411
     * @param string|array|null $text Text when setting or null when reading. If an array the text will
412
     *   be imploded with "\n".
413
     * @return string|$this If reading, the value of the epilog. If setting $this will be returned.
414
     */
415
    public function epilog($text = null)
416
    {
417
        deprecationWarning(
418
            'ConsoleOptionParser::epliog() is deprecated. ' .
419
            'Use ConsoleOptionParser::setEpilog()/getEpilog() instead.'
420
        );
421
        if ($text !== null) {
422
            return $this->setEpilog($text);
423
        }
424
425
        return $this->getEpilog();
426
    }
427
428
    /**
429
     * Enables sorting of subcommands
430
     *
431
     * @param bool $value Whether or not to sort subcommands
432
     * @return $this
433
     */
434
    public function enableSubcommandSort($value = true)
435
    {
436
        $this->_subcommandSort = (bool)$value;
437
438
        return $this;
439
    }
440
441
    /**
442
     * Checks whether or not sorting is enabled for subcommands.
443
     *
444
     * @return bool
445
     */
446
    public function isSubcommandSortEnabled()
447
    {
448
        return $this->_subcommandSort;
449
    }
450
451
    /**
452
     * Add an option to the option parser. Options allow you to define optional or required
453
     * parameters for your console application. Options are defined by the parameters they use.
454
     *
455
     * ### Options
456
     *
457
     * - `short` - The single letter variant for this option, leave undefined for none.
458
     * - `help` - Help text for this option. Used when generating help for the option.
459
     * - `default` - The default value for this option. Defaults are added into the parsed params when the
460
     *    attached option is not provided or has no value. Using default and boolean together will not work.
461
     *    are added into the parsed parameters when the option is undefined. Defaults to null.
462
     * - `boolean` - The option uses no value, it's just a boolean switch. Defaults to false.
463
     *    If an option is defined as boolean, it will always be added to the parsed params. If no present
464
     *    it will be false, if present it will be true.
465
     * - `multiple` - The option can be provided multiple times. The parsed option
466
     *   will be an array of values when this option is enabled.
467
     * - `choices` A list of valid choices for this option. If left empty all values are valid..
468
     *   An exception will be raised when parse() encounters an invalid value.
469
     *
470
     * @param \Cake\Console\ConsoleInputOption|string $name The long name you want to the value to be parsed out as when options are parsed.
471
     *   Will also accept an instance of ConsoleInputOption
472
     * @param array $options An array of parameters that define the behavior of the option
473
     * @return $this
474
     */
475
    public function addOption($name, array $options = [])
476
    {
477
        if ($name instanceof ConsoleInputOption) {
478
            $option = $name;
479
            $name = $option->name();
480
        } else {
481
            $defaults = [
482
                'name' => $name,
483
                'short' => null,
484
                'help' => '',
485
                'default' => null,
486
                'boolean' => false,
487
                'choices' => [],
488
            ];
489
            $options += $defaults;
490
            $option = new ConsoleInputOption($options);
491
        }
492
        $this->_options[$name] = $option;
493
        asort($this->_options);
494
        if ($option->short() !== null) {
495
            $this->_shortOptions[$option->short()] = $name;
496
            asort($this->_shortOptions);
497
        }
498
499
        return $this;
500
    }
501
502
    /**
503
     * Remove an option from the option parser.
504
     *
505
     * @param string $name The option name to remove.
506
     * @return $this
507
     */
508
    public function removeOption($name)
509
    {
510
        unset($this->_options[$name]);
511
512
        return $this;
513
    }
514
515
    /**
516
     * Add a positional argument to the option parser.
517
     *
518
     * ### Params
519
     *
520
     * - `help` The help text to display for this argument.
521
     * - `required` Whether this parameter is required.
522
     * - `index` The index for the arg, if left undefined the argument will be put
523
     *   onto the end of the arguments. If you define the same index twice the first
524
     *   option will be overwritten.
525
     * - `choices` A list of valid choices for this argument. If left empty all values are valid..
526
     *   An exception will be raised when parse() encounters an invalid value.
527
     *
528
     * @param \Cake\Console\ConsoleInputArgument|string $name The name of the argument.
529
     *   Will also accept an instance of ConsoleInputArgument.
530
     * @param array $params Parameters for the argument, see above.
531
     * @return $this
532
     */
533
    public function addArgument($name, array $params = [])
534
    {
535
        if ($name instanceof ConsoleInputArgument) {
536
            $arg = $name;
537
            $index = count($this->_args);
538
        } else {
539
            $defaults = [
540
                'name' => $name,
541
                'help' => '',
542
                'index' => count($this->_args),
543
                'required' => false,
544
                'choices' => [],
545
            ];
546
            $options = $params + $defaults;
547
            $index = $options['index'];
548
            unset($options['index']);
549
            $arg = new ConsoleInputArgument($options);
550
        }
551
        foreach ($this->_args as $k => $a) {
552
            if ($a->isEqualTo($arg)) {
553
                return $this;
554
            }
555
            if (!empty($options['required']) && !$a->isRequired()) {
556
                throw new LogicException('A required argument cannot follow an optional one');
557
            }
558
        }
559
        $this->_args[$index] = $arg;
560
        ksort($this->_args);
561
562
        return $this;
563
    }
564
565
    /**
566
     * Add multiple arguments at once. Take an array of argument definitions.
567
     * The keys are used as the argument names, and the values as params for the argument.
568
     *
569
     * @param array $args Array of arguments to add.
570
     * @see \Cake\Console\ConsoleOptionParser::addArgument()
571
     * @return $this
572
     */
573
    public function addArguments(array $args)
574
    {
575
        foreach ($args as $name => $params) {
576
            if ($params instanceof ConsoleInputArgument) {
577
                $name = $params;
578
                $params = [];
579
            }
580
            $this->addArgument($name, $params);
581
        }
582
583
        return $this;
584
    }
585
586
    /**
587
     * Add multiple options at once. Takes an array of option definitions.
588
     * The keys are used as option names, and the values as params for the option.
589
     *
590
     * @param array $options Array of options to add.
591
     * @see \Cake\Console\ConsoleOptionParser::addOption()
592
     * @return $this
593
     */
594
    public function addOptions(array $options)
595
    {
596
        foreach ($options as $name => $params) {
597
            if ($params instanceof ConsoleInputOption) {
598
                $name = $params;
599
                $params = [];
600
            }
601
            $this->addOption($name, $params);
602
        }
603
604
        return $this;
605
    }
606
607
    /**
608
     * Append a subcommand to the subcommand list.
609
     * Subcommands are usually methods on your Shell, but can also be used to document Tasks.
610
     *
611
     * ### Options
612
     *
613
     * - `help` - Help text for the subcommand.
614
     * - `parser` - A ConsoleOptionParser for the subcommand. This allows you to create method
615
     *    specific option parsers. When help is generated for a subcommand, if a parser is present
616
     *    it will be used.
617
     *
618
     * @param \Cake\Console\ConsoleInputSubcommand|string $name Name of the subcommand. Will also accept an instance of ConsoleInputSubcommand
619
     * @param array $options Array of params, see above.
620
     * @return $this
621
     */
622
    public function addSubcommand($name, array $options = [])
623
    {
624
        if ($name instanceof ConsoleInputSubcommand) {
625
            $command = $name;
626
            $name = $command->name();
627
        } else {
628
            $name = Inflector::underscore($name);
629
            $defaults = [
630
                'name' => $name,
631
                'help' => '',
632
                'parser' => null,
633
            ];
634
            $options += $defaults;
635
636
            $command = new ConsoleInputSubcommand($options);
637
        }
638
        $this->_subcommands[$name] = $command;
639
        if ($this->_subcommandSort) {
640
            asort($this->_subcommands);
641
        }
642
643
        return $this;
644
    }
645
646
    /**
647
     * Remove a subcommand from the option parser.
648
     *
649
     * @param string $name The subcommand name to remove.
650
     * @return $this
651
     */
652
    public function removeSubcommand($name)
653
    {
654
        unset($this->_subcommands[$name]);
655
656
        return $this;
657
    }
658
659
    /**
660
     * Add multiple subcommands at once.
661
     *
662
     * @param array $commands Array of subcommands.
663
     * @return $this
664
     */
665
    public function addSubcommands(array $commands)
666
    {
667
        foreach ($commands as $name => $params) {
668
            if ($params instanceof ConsoleInputSubcommand) {
669
                $name = $params;
670
                $params = [];
671
            }
672
            $this->addSubcommand($name, $params);
673
        }
674
675
        return $this;
676
    }
677
678
    /**
679
     * Gets the arguments defined in the parser.
680
     *
681
     * @return \Cake\Console\ConsoleInputArgument[]
682
     */
683
    public function arguments()
684
    {
685
        return $this->_args;
686
    }
687
688
    /**
689
     * Get the list of argument names.
690
     *
691
     * @return string[]
692
     */
693
    public function argumentNames()
694
    {
695
        $out = [];
696
        foreach ($this->_args as $arg) {
697
            $out[] = $arg->name();
698
        }
699
700
        return $out;
701
    }
702
703
    /**
704
     * Get the defined options in the parser.
705
     *
706
     * @return \Cake\Console\ConsoleInputOption[]
707
     */
708
    public function options()
709
    {
710
        return $this->_options;
711
    }
712
713
    /**
714
     * Get the array of defined subcommands
715
     *
716
     * @return \Cake\Console\ConsoleInputSubcommand[]
717
     */
718
    public function subcommands()
719
    {
720
        return $this->_subcommands;
721
    }
722
723
    /**
724
     * Parse the argv array into a set of params and args. If $command is not null
725
     * and $command is equal to a subcommand that has a parser, that parser will be used
726
     * to parse the $argv
727
     *
728
     * @param array $argv Array of args (argv) to parse.
729
     * @return array [$params, $args]
730
     * @throws \Cake\Console\Exception\ConsoleException When an invalid parameter is encountered.
731
     */
732
    public function parse($argv)
733
    {
734
        $command = isset($argv[0]) ? Inflector::underscore($argv[0]) : null;
735
        if (isset($this->_subcommands[$command])) {
736
            array_shift($argv);
737
        }
738
        if (isset($this->_subcommands[$command]) && $this->_subcommands[$command]->parser()) {
739
            return $this->_subcommands[$command]->parser()->parse($argv);
740
        }
741
        $params = $args = [];
742
        $this->_tokens = $argv;
743
        while (($token = array_shift($this->_tokens)) !== null) {
744
            if (isset($this->_subcommands[$token])) {
745
                continue;
746
            }
747
            if (substr($token, 0, 2) === '--') {
748
                $params = $this->_parseLongOption($token, $params);
749
            } elseif (substr($token, 0, 1) === '-') {
750
                $params = $this->_parseShortOption($token, $params);
751
            } else {
752
                $args = $this->_parseArg($token, $args);
753
            }
754
        }
755
        foreach ($this->_args as $i => $arg) {
756
            if ($arg->isRequired() && !isset($args[$i]) && empty($params['help'])) {
757
                throw new ConsoleException(
758
                    sprintf('Missing required arguments. %s is required.', $arg->name())
759
                );
760
            }
761
        }
762
        foreach ($this->_options as $option) {
763
            $name = $option->name();
764
            $isBoolean = $option->isBoolean();
765
            $default = $option->defaultValue();
766
767
            if ($default !== null && !isset($params[$name]) && !$isBoolean) {
768
                $params[$name] = $default;
769
            }
770
            if ($isBoolean && !isset($params[$name])) {
771
                $params[$name] = false;
772
            }
773
        }
774
775
        return [$params, $args];
776
    }
777
778
    /**
779
     * Gets formatted help for this parser object.
780
     *
781
     * Generates help text based on the description, options, arguments, subcommands and epilog
782
     * in the parser.
783
     *
784
     * @param string|null $subcommand If present and a valid subcommand that has a linked parser.
785
     *    That subcommands help will be shown instead.
786
     * @param string $format Define the output format, can be text or xml
787
     * @param int $width The width to format user content to. Defaults to 72
788
     * @return string Generated help.
789
     */
790
    public function help($subcommand = null, $format = 'text', $width = 72)
791
    {
792
        if ($subcommand === null) {
793
            $formatter = new HelpFormatter($this);
794
            $formatter->setAlias($this->rootName);
795
796
            if ($format === 'text') {
797
                return $formatter->text($width);
798
            }
799
            if ($format === 'xml') {
800
                return (string)$formatter->xml();
801
            }
802
        }
803
804
        if (isset($this->_subcommands[$subcommand])) {
805
            $command = $this->_subcommands[$subcommand];
806
            $subparser = $command->parser();
807
808
            // Generate a parser as the subcommand didn't define one.
809
            if (!($subparser instanceof self)) {
810
                // $subparser = clone $this;
811
                $subparser = new self($subcommand);
812
                $subparser
813
                    ->setDescription($command->getRawHelp())
814
                    ->addOptions($this->options())
815
                    ->addArguments($this->arguments());
816
            }
817
            if (strlen($subparser->getDescription()) === 0) {
818
                $subparser->setDescription($command->getRawHelp());
819
            }
820
            $subparser->setCommand($this->getCommand() . ' ' . $subcommand);
821
            $subparser->setRootName($this->rootName);
822
823
            return $subparser->help(null, $format, $width);
824
        }
825
826
        return $this->getCommandError($subcommand);
827
    }
828
829
    /**
830
     * Set the alias used in the HelpFormatter
831
     *
832
     * @param string $alias The alias
833
     * @return void
834
     * @deprecated 3.5.0 Use setRootName() instead.
835
     */
836
    public function setHelpAlias($alias)
837
    {
838
        deprecationWarning(
839
            'ConsoleOptionParser::setHelpAlias() is deprecated. ' .
840
            'Use ConsoleOptionParser::setRootName() instead.'
841
        );
842
        $this->rootName = $alias;
843
    }
844
845
    /**
846
     * Set the root name used in the HelpFormatter
847
     *
848
     * @param string $name The root command name
849
     * @return $this
850
     */
851
    public function setRootName($name)
852
    {
853
        $this->rootName = (string)$name;
854
855
        return $this;
856
    }
857
858
    /**
859
     * Get the message output in the console stating that the command can not be found and tries to guess what the user
860
     * wanted to say. Output a list of available subcommands as well.
861
     *
862
     * @param string $command Unknown command name trying to be dispatched.
863
     * @return string The message to be displayed in the console.
864
     */
865
    protected function getCommandError($command)
866
    {
867
        $rootCommand = $this->getCommand();
868
        $subcommands = array_keys((array)$this->subcommands());
869
        $bestGuess = $this->findClosestItem($command, $subcommands);
870
871
        $out = [
872
            sprintf(
873
                'Unable to find the `%s %s` subcommand. See `bin/%s %s --help`.',
874
                $rootCommand,
875
                $command,
876
                $this->rootName,
877
                $rootCommand
878
            ),
879
            '',
880
        ];
881
882
        if ($bestGuess !== null) {
883
            $out[] = sprintf('Did you mean : `%s %s` ?', $rootCommand, $bestGuess);
884
            $out[] = '';
885
        }
886
        $out[] = sprintf('Available subcommands for the `%s` command are : ', $rootCommand);
887
        $out[] = '';
888
        foreach ($subcommands as $subcommand) {
889
            $out[] = ' - ' . $subcommand;
890
        }
891
892
        return implode("\n", $out);
893
    }
894
895
    /**
896
     * Get the message output in the console stating that the option can not be found and tries to guess what the user
897
     * wanted to say. Output a list of available options as well.
898
     *
899
     * @param string $option Unknown option name trying to be used.
900
     * @return string The message to be displayed in the console.
901
     */
902
    protected function getOptionError($option)
903
    {
904
        $availableOptions = array_keys($this->_options);
905
        $bestGuess = $this->findClosestItem($option, $availableOptions);
906
        $out = [
907
            sprintf('Unknown option `%s`.', $option),
908
            '',
909
        ];
910
911
        if ($bestGuess !== null) {
912
            $out[] = sprintf('Did you mean `%s` ?', $bestGuess);
913
            $out[] = '';
914
        }
915
916
        $out[] = 'Available options are :';
917
        $out[] = '';
918
        foreach ($availableOptions as $availableOption) {
919
            $out[] = ' - ' . $availableOption;
920
        }
921
922
        return implode("\n", $out);
923
    }
924
925
    /**
926
     * Get the message output in the console stating that the short option can not be found. Output a list of available
927
     * short options and what option they refer to as well.
928
     *
929
     * @param string $option Unknown short option name trying to be used.
930
     * @return string The message to be displayed in the console.
931
     */
932
    protected function getShortOptionError($option)
933
    {
934
        $out = [sprintf('Unknown short option `%s`', $option)];
935
        $out[] = '';
936
        $out[] = 'Available short options are :';
937
        $out[] = '';
938
939
        foreach ($this->_shortOptions as $short => $long) {
940
            $out[] = sprintf(' - `%s` (short for `--%s`)', $short, $long);
941
        }
942
943
        return implode("\n", $out);
944
    }
945
946
    /**
947
     * Tries to guess the item name the user originally wanted using the some regex pattern and the levenshtein
948
     * algorithm.
949
     *
950
     * @param string $needle Unknown item (either a subcommand name or an option for instance) trying to be used.
951
     * @param string[] $haystack List of items available for the type $needle belongs to.
952
     * @return string|null The closest name to the item submitted by the user.
953
     */
954
    protected function findClosestItem($needle, $haystack)
955
    {
956
        $bestGuess = null;
957
        foreach ($haystack as $item) {
958
            if (preg_match('/^' . $needle . '/', $item)) {
959
                return $item;
960
            }
961
        }
962
963
        foreach ($haystack as $item) {
964
            if (preg_match('/' . $needle . '/', $item)) {
965
                return $item;
966
            }
967
968
            $score = levenshtein($needle, $item);
969
970
            if (!isset($bestScore) || $score < $bestScore) {
971
                $bestScore = $score;
972
                $bestGuess = $item;
973
            }
974
        }
975
976
        return $bestGuess;
977
    }
978
979
    /**
980
     * Parse the value for a long option out of $this->_tokens. Will handle
981
     * options with an `=` in them.
982
     *
983
     * @param string $option The option to parse.
984
     * @param array $params The params to append the parsed value into
985
     * @return array Params with $option added in.
986
     */
987
    protected function _parseLongOption($option, $params)
988
    {
989
        $name = substr($option, 2);
990
        if (strpos($name, '=') !== false) {
991
            list($name, $value) = explode('=', $name, 2);
992
            array_unshift($this->_tokens, $value);
993
        }
994
995
        return $this->_parseOption($name, $params);
996
    }
997
998
    /**
999
     * Parse the value for a short option out of $this->_tokens
1000
     * If the $option is a combination of multiple shortcuts like -otf
1001
     * they will be shifted onto the token stack and parsed individually.
1002
     *
1003
     * @param string $option The option to parse.
1004
     * @param array $params The params to append the parsed value into
1005
     * @return array Params with $option added in.
1006
     * @throws \Cake\Console\Exception\ConsoleException When unknown short options are encountered.
1007
     */
1008
    protected function _parseShortOption($option, $params)
1009
    {
1010
        $key = substr($option, 1);
1011
        if (strlen($key) > 1) {
1012
            $flags = str_split($key);
1013
            $key = $flags[0];
1014
            for ($i = 1, $len = count($flags); $i < $len; $i++) {
1015
                array_unshift($this->_tokens, '-' . $flags[$i]);
1016
            }
1017
        }
1018
        if (!isset($this->_shortOptions[$key])) {
1019
            throw new ConsoleException($this->getShortOptionError($key));
1020
        }
1021
        $name = $this->_shortOptions[$key];
1022
1023
        return $this->_parseOption($name, $params);
1024
    }
1025
1026
    /**
1027
     * Parse an option by its name index.
1028
     *
1029
     * @param string $name The name to parse.
1030
     * @param array $params The params to append the parsed value into
1031
     * @return array Params with $option added in.
1032
     * @throws \Cake\Console\Exception\ConsoleException
1033
     */
1034
    protected function _parseOption($name, $params)
1035
    {
1036
        if (!isset($this->_options[$name])) {
1037
            throw new ConsoleException($this->getOptionError($name));
1038
        }
1039
        $option = $this->_options[$name];
1040
        $isBoolean = $option->isBoolean();
1041
        $nextValue = $this->_nextToken();
1042
        $emptyNextValue = (empty($nextValue) && $nextValue !== '0');
1043
        if (!$isBoolean && !$emptyNextValue && !$this->_optionExists($nextValue)) {
1044
            array_shift($this->_tokens);
1045
            $value = $nextValue;
1046
        } elseif ($isBoolean) {
1047
            $value = true;
1048
        } else {
1049
            $value = $option->defaultValue();
1050
        }
1051
        if ($option->validChoice($value)) {
1052
            if ($option->acceptsMultiple()) {
1053
                $params[$name][] = $value;
1054
            } else {
1055
                $params[$name] = $value;
1056
            }
1057
1058
            return $params;
1059
        }
1060
1061
        return [];
1062
    }
1063
1064
    /**
1065
     * Check to see if $name has an option (short/long) defined for it.
1066
     *
1067
     * @param string $name The name of the option.
1068
     * @return bool
1069
     */
1070
    protected function _optionExists($name)
1071
    {
1072
        if (substr($name, 0, 2) === '--') {
1073
            return isset($this->_options[substr($name, 2)]);
1074
        }
1075
        if ($name[0] === '-' && $name[1] !== '-') {
1076
            return isset($this->_shortOptions[$name[1]]);
1077
        }
1078
1079
        return false;
1080
    }
1081
1082
    /**
1083
     * Parse an argument, and ensure that the argument doesn't exceed the number of arguments
1084
     * and that the argument is a valid choice.
1085
     *
1086
     * @param string $argument The argument to append
1087
     * @param array $args The array of parsed args to append to.
1088
     * @return string[] Args
1089
     * @throws \Cake\Console\Exception\ConsoleException
1090
     */
1091
    protected function _parseArg($argument, $args)
1092
    {
1093
        if (empty($this->_args)) {
1094
            $args[] = $argument;
1095
1096
            return $args;
1097
        }
1098
        $next = count($args);
1099
        if (!isset($this->_args[$next])) {
1100
            throw new ConsoleException('Too many arguments.');
1101
        }
1102
1103
        if ($this->_args[$next]->validChoice($argument)) {
1104
            $args[] = $argument;
1105
1106
            return $args;
1107
        }
1108
    }
1109
1110
    /**
1111
     * Find the next token in the argv set.
1112
     *
1113
     * @return string next token or ''
1114
     */
1115
    protected function _nextToken()
1116
    {
1117
        return isset($this->_tokens[0]) ? $this->_tokens[0] : '';
1118
    }
1119
}
1120