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: 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: