1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Console\Command;
13
14use Symfony\Component\Console\Application;
15use Symfony\Component\Console\Attribute\AsCommand;
16use Symfony\Component\Console\Completion\CompletionInput;
17use Symfony\Component\Console\Completion\CompletionSuggestions;
18use Symfony\Component\Console\Exception\ExceptionInterface;
19use Symfony\Component\Console\Exception\InvalidArgumentException;
20use Symfony\Component\Console\Exception\LogicException;
21use Symfony\Component\Console\Helper\HelperSet;
22use Symfony\Component\Console\Input\InputArgument;
23use Symfony\Component\Console\Input\InputDefinition;
24use Symfony\Component\Console\Input\InputInterface;
25use Symfony\Component\Console\Input\InputOption;
26use Symfony\Component\Console\Output\OutputInterface;
27
28/**
29 * Base class for all commands.
30 *
31 * @author Fabien Potencier <fabien@symfony.com>
32 */
33class Command
34{
35    // see https://tldp.org/LDP/abs/html/exitcodes.html
36    public const SUCCESS = 0;
37    public const FAILURE = 1;
38    public const INVALID = 2;
39
40    /**
41     * @var string|null The default command name
42     */
43    protected static $defaultName;
44
45    /**
46     * @var string|null The default command description
47     */
48    protected static $defaultDescription;
49
50    private $application;
51    private $name;
52    private $processTitle;
53    private $aliases = [];
54    private $definition;
55    private $hidden = false;
56    private $help = '';
57    private $description = '';
58    private $fullDefinition;
59    private $ignoreValidationErrors = false;
60    private $code;
61    private $synopsis = [];
62    private $usages = [];
63    private $helperSet;
64
65    /**
66     * @return string|null
67     */
68    public static function getDefaultName()
69    {
70        $class = static::class;
71
72        if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
73            return $attribute[0]->newInstance()->name;
74        }
75
76        $r = new \ReflectionProperty($class, 'defaultName');
77
78        return $class === $r->class ? static::$defaultName : null;
79    }
80
81    public static function getDefaultDescription(): ?string
82    {
83        $class = static::class;
84
85        if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
86            return $attribute[0]->newInstance()->description;
87        }
88
89        $r = new \ReflectionProperty($class, 'defaultDescription');
90
91        return $class === $r->class ? static::$defaultDescription : null;
92    }
93
94    /**
95     * @param string|null $name The name of the command; passing null means it must be set in configure()
96     *
97     * @throws LogicException When the command name is empty
98     */
99    public function __construct(string $name = null)
100    {
101        $this->definition = new InputDefinition();
102
103        if (null === $name && null !== $name = static::getDefaultName()) {
104            $aliases = explode('|', $name);
105
106            if ('' === $name = array_shift($aliases)) {
107                $this->setHidden(true);
108                $name = array_shift($aliases);
109            }
110
111            $this->setAliases($aliases);
112        }
113
114        if (null !== $name) {
115            $this->setName($name);
116        }
117
118        if ('' === $this->description) {
119            $this->setDescription(static::getDefaultDescription() ?? '');
120        }
121
122        $this->configure();
123    }
124
125    /**
126     * Ignores validation errors.
127     *
128     * This is mainly useful for the help command.
129     */
130    public function ignoreValidationErrors()
131    {
132        $this->ignoreValidationErrors = true;
133    }
134
135    public function setApplication(Application $application = null)
136    {
137        $this->application = $application;
138        if ($application) {
139            $this->setHelperSet($application->getHelperSet());
140        } else {
141            $this->helperSet = null;
142        }
143
144        $this->fullDefinition = null;
145    }
146
147    public function setHelperSet(HelperSet $helperSet)
148    {
149        $this->helperSet = $helperSet;
150    }
151
152    /**
153     * Gets the helper set.
154     *
155     * @return HelperSet|null
156     */
157    public function getHelperSet()
158    {
159        return $this->helperSet;
160    }
161
162    /**
163     * Gets the application instance for this command.
164     *
165     * @return Application|null
166     */
167    public function getApplication()
168    {
169        return $this->application;
170    }
171
172    /**
173     * Checks whether the command is enabled or not in the current environment.
174     *
175     * Override this to check for x or y and return false if the command cannot
176     * run properly under the current conditions.
177     *
178     * @return bool
179     */
180    public function isEnabled()
181    {
182        return true;
183    }
184
185    /**
186     * Configures the current command.
187     */
188    protected function configure()
189    {
190    }
191
192    /**
193     * Executes the current command.
194     *
195     * This method is not abstract because you can use this class
196     * as a concrete class. In this case, instead of defining the
197     * execute() method, you set the code to execute by passing
198     * a Closure to the setCode() method.
199     *
200     * @return int 0 if everything went fine, or an exit code
201     *
202     * @throws LogicException When this abstract method is not implemented
203     *
204     * @see setCode()
205     */
206    protected function execute(InputInterface $input, OutputInterface $output)
207    {
208        throw new LogicException('You must override the execute() method in the concrete command class.');
209    }
210
211    /**
212     * Interacts with the user.
213     *
214     * This method is executed before the InputDefinition is validated.
215     * This means that this is the only place where the command can
216     * interactively ask for values of missing required arguments.
217     */
218    protected function interact(InputInterface $input, OutputInterface $output)
219    {
220    }
221
222    /**
223     * Initializes the command after the input has been bound and before the input
224     * is validated.
225     *
226     * This is mainly useful when a lot of commands extends one main command
227     * where some things need to be initialized based on the input arguments and options.
228     *
229     * @see InputInterface::bind()
230     * @see InputInterface::validate()
231     */
232    protected function initialize(InputInterface $input, OutputInterface $output)
233    {
234    }
235
236    /**
237     * Runs the command.
238     *
239     * The code to execute is either defined directly with the
240     * setCode() method or by overriding the execute() method
241     * in a sub-class.
242     *
243     * @return int The command exit code
244     *
245     * @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}.
246     *
247     * @see setCode()
248     * @see execute()
249     */
250    public function run(InputInterface $input, OutputInterface $output)
251    {
252        // add the application arguments and options
253        $this->mergeApplicationDefinition();
254
255        // bind the input against the command specific arguments/options
256        try {
257            $input->bind($this->getDefinition());
258        } catch (ExceptionInterface $e) {
259            if (!$this->ignoreValidationErrors) {
260                throw $e;
261            }
262        }
263
264        $this->initialize($input, $output);
265
266        if (null !== $this->processTitle) {
267            if (\function_exists('cli_set_process_title')) {
268                if (!@cli_set_process_title($this->processTitle)) {
269                    if ('Darwin' === \PHP_OS) {
270                        $output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
271                    } else {
272                        cli_set_process_title($this->processTitle);
273                    }
274                }
275            } elseif (\function_exists('setproctitle')) {
276                setproctitle($this->processTitle);
277            } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
278                $output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
279            }
280        }
281
282        if ($input->isInteractive()) {
283            $this->interact($input, $output);
284        }
285
286        // The command name argument is often omitted when a command is executed directly with its run() method.
287        // It would fail the validation if we didn't make sure the command argument is present,
288        // since it's required by the application.
289        if ($input->hasArgument('command') && null === $input->getArgument('command')) {
290            $input->setArgument('command', $this->getName());
291        }
292
293        $input->validate();
294
295        if ($this->code) {
296            $statusCode = ($this->code)($input, $output);
297        } else {
298            $statusCode = $this->execute($input, $output);
299
300            if (!\is_int($statusCode)) {
301                throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
302            }
303        }
304
305        return is_numeric($statusCode) ? (int) $statusCode : 0;
306    }
307
308    /**
309     * Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
310     */
311    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
312    {
313    }
314
315    /**
316     * Sets the code to execute when running this command.
317     *
318     * If this method is used, it overrides the code defined
319     * in the execute() method.
320     *
321     * @param callable $code A callable(InputInterface $input, OutputInterface $output)
322     *
323     * @return $this
324     *
325     * @throws InvalidArgumentException
326     *
327     * @see execute()
328     */
329    public function setCode(callable $code)
330    {
331        if ($code instanceof \Closure) {
332            $r = new \ReflectionFunction($code);
333            if (null === $r->getClosureThis()) {
334                set_error_handler(static function () {});
335                try {
336                    if ($c = \Closure::bind($code, $this)) {
337                        $code = $c;
338                    }
339                } finally {
340                    restore_error_handler();
341                }
342            }
343        }
344
345        $this->code = $code;
346
347        return $this;
348    }
349
350    /**
351     * Merges the application definition with the command definition.
352     *
353     * This method is not part of public API and should not be used directly.
354     *
355     * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
356     *
357     * @internal
358     */
359    public function mergeApplicationDefinition(bool $mergeArgs = true)
360    {
361        if (null === $this->application) {
362            return;
363        }
364
365        $this->fullDefinition = new InputDefinition();
366        $this->fullDefinition->setOptions($this->definition->getOptions());
367        $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
368
369        if ($mergeArgs) {
370            $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
371            $this->fullDefinition->addArguments($this->definition->getArguments());
372        } else {
373            $this->fullDefinition->setArguments($this->definition->getArguments());
374        }
375    }
376
377    /**
378     * Sets an array of argument and option instances.
379     *
380     * @param array|InputDefinition $definition An array of argument and option instances or a definition instance
381     *
382     * @return $this
383     */
384    public function setDefinition($definition)
385    {
386        if ($definition instanceof InputDefinition) {
387            $this->definition = $definition;
388        } else {
389            $this->definition->setDefinition($definition);
390        }
391
392        $this->fullDefinition = null;
393
394        return $this;
395    }
396
397    /**
398     * Gets the InputDefinition attached to this Command.
399     *
400     * @return InputDefinition
401     */
402    public function getDefinition()
403    {
404        return $this->fullDefinition ?? $this->getNativeDefinition();
405    }
406
407    /**
408     * Gets the InputDefinition to be used to create representations of this Command.
409     *
410     * Can be overridden to provide the original command representation when it would otherwise
411     * be changed by merging with the application InputDefinition.
412     *
413     * This method is not part of public API and should not be used directly.
414     *
415     * @return InputDefinition
416     */
417    public function getNativeDefinition()
418    {
419        if (null === $this->definition) {
420            throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
421        }
422
423        return $this->definition;
424    }
425
426    /**
427     * Adds an argument.
428     *
429     * @param int|null $mode    The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
430     * @param mixed    $default The default value (for InputArgument::OPTIONAL mode only)
431     *
432     * @throws InvalidArgumentException When argument mode is not valid
433     *
434     * @return $this
435     */
436    public function addArgument(string $name, int $mode = null, string $description = '', $default = null)
437    {
438        $this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
439        if (null !== $this->fullDefinition) {
440            $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default));
441        }
442
443        return $this;
444    }
445
446    /**
447     * Adds an option.
448     *
449     * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
450     * @param int|null          $mode     The option mode: One of the InputOption::VALUE_* constants
451     * @param mixed             $default  The default value (must be null for InputOption::VALUE_NONE)
452     *
453     * @throws InvalidArgumentException If option mode is invalid or incompatible
454     *
455     * @return $this
456     */
457    public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
458    {
459        $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
460        if (null !== $this->fullDefinition) {
461            $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
462        }
463
464        return $this;
465    }
466
467    /**
468     * Sets the name of the command.
469     *
470     * This method can set both the namespace and the name if
471     * you separate them by a colon (:)
472     *
473     *     $command->setName('foo:bar');
474     *
475     * @return $this
476     *
477     * @throws InvalidArgumentException When the name is invalid
478     */
479    public function setName(string $name)
480    {
481        $this->validateName($name);
482
483        $this->name = $name;
484
485        return $this;
486    }
487
488    /**
489     * Sets the process title of the command.
490     *
491     * This feature should be used only when creating a long process command,
492     * like a daemon.
493     *
494     * @return $this
495     */
496    public function setProcessTitle(string $title)
497    {
498        $this->processTitle = $title;
499
500        return $this;
501    }
502
503    /**
504     * Returns the command name.
505     *
506     * @return string|null
507     */
508    public function getName()
509    {
510        return $this->name;
511    }
512
513    /**
514     * @param bool $hidden Whether or not the command should be hidden from the list of commands
515     *                     The default value will be true in Symfony 6.0
516     *
517     * @return $this
518     *
519     * @final since Symfony 5.1
520     */
521    public function setHidden(bool $hidden /*= true*/)
522    {
523        $this->hidden = $hidden;
524
525        return $this;
526    }
527
528    /**
529     * @return bool whether the command should be publicly shown or not
530     */
531    public function isHidden()
532    {
533        return $this->hidden;
534    }
535
536    /**
537     * Sets the description for the command.
538     *
539     * @return $this
540     */
541    public function setDescription(string $description)
542    {
543        $this->description = $description;
544
545        return $this;
546    }
547
548    /**
549     * Returns the description for the command.
550     *
551     * @return string
552     */
553    public function getDescription()
554    {
555        return $this->description;
556    }
557
558    /**
559     * Sets the help for the command.
560     *
561     * @return $this
562     */
563    public function setHelp(string $help)
564    {
565        $this->help = $help;
566
567        return $this;
568    }
569
570    /**
571     * Returns the help for the command.
572     *
573     * @return string
574     */
575    public function getHelp()
576    {
577        return $this->help;
578    }
579
580    /**
581     * Returns the processed help for the command replacing the %command.name% and
582     * %command.full_name% patterns with the real values dynamically.
583     *
584     * @return string
585     */
586    public function getProcessedHelp()
587    {
588        $name = $this->name;
589        $isSingleCommand = $this->application && $this->application->isSingleCommand();
590
591        $placeholders = [
592            '%command.name%',
593            '%command.full_name%',
594        ];
595        $replacements = [
596            $name,
597            $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
598        ];
599
600        return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
601    }
602
603    /**
604     * Sets the aliases for the command.
605     *
606     * @param string[] $aliases An array of aliases for the command
607     *
608     * @return $this
609     *
610     * @throws InvalidArgumentException When an alias is invalid
611     */
612    public function setAliases(iterable $aliases)
613    {
614        $list = [];
615
616        foreach ($aliases as $alias) {
617            $this->validateName($alias);
618            $list[] = $alias;
619        }
620
621        $this->aliases = \is_array($aliases) ? $aliases : $list;
622
623        return $this;
624    }
625
626    /**
627     * Returns the aliases for the command.
628     *
629     * @return array
630     */
631    public function getAliases()
632    {
633        return $this->aliases;
634    }
635
636    /**
637     * Returns the synopsis for the command.
638     *
639     * @param bool $short Whether to show the short version of the synopsis (with options folded) or not
640     *
641     * @return string
642     */
643    public function getSynopsis(bool $short = false)
644    {
645        $key = $short ? 'short' : 'long';
646
647        if (!isset($this->synopsis[$key])) {
648            $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
649        }
650
651        return $this->synopsis[$key];
652    }
653
654    /**
655     * Add a command usage example, it'll be prefixed with the command name.
656     *
657     * @return $this
658     */
659    public function addUsage(string $usage)
660    {
661        if (!str_starts_with($usage, $this->name)) {
662            $usage = sprintf('%s %s', $this->name, $usage);
663        }
664
665        $this->usages[] = $usage;
666
667        return $this;
668    }
669
670    /**
671     * Returns alternative usages of the command.
672     *
673     * @return array
674     */
675    public function getUsages()
676    {
677        return $this->usages;
678    }
679
680    /**
681     * Gets a helper instance by name.
682     *
683     * @return mixed
684     *
685     * @throws LogicException           if no HelperSet is defined
686     * @throws InvalidArgumentException if the helper is not defined
687     */
688    public function getHelper(string $name)
689    {
690        if (null === $this->helperSet) {
691            throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
692        }
693
694        return $this->helperSet->get($name);
695    }
696
697    /**
698     * Validates a command name.
699     *
700     * It must be non-empty and parts can optionally be separated by ":".
701     *
702     * @throws InvalidArgumentException When the name is invalid
703     */
704    private function validateName(string $name)
705    {
706        if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
707            throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
708        }
709    }
710}
711