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