diff --git a/UPGRADE-5.4.md b/UPGRADE-5.4.md index b3ec3bc0aa686..d747e969717fe 100644 --- a/UPGRADE-5.4.md +++ b/UPGRADE-5.4.md @@ -10,6 +10,7 @@ Console ------- * Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement + * Add blacklisting of reserved shortcuts for `InputOptions` in commands. See `getDefaultInputDefinition()` in `Symfony\Component\Console\Application` Finder ------ diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index fedb08823e15b..0483372d1775e 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 5.4 --- + * Add blacklisting of reserved shortcuts for InputOptions in commands * Add `TesterTrait::assertCommandIsSuccessful()` to test command * Deprecate `HelperSet::setCommand()` and `getCommand()` without replacement diff --git a/src/Symfony/Component/Console/Input/InputOption.php b/src/Symfony/Component/Console/Input/InputOption.php index 2bec34fe1a395..8dc26dff56b4f 100644 --- a/src/Symfony/Component/Console/Input/InputOption.php +++ b/src/Symfony/Component/Console/Input/InputOption.php @@ -52,6 +52,10 @@ class InputOption private $default; private $description; + public const RESERVED_NAMES = ['help', 'quiet', 'version', 'ansi', 'no-ansi', 'no-interaction', 'env', 'no-debug', 'verbose']; + public const RESERVED_COMMANDS = ['about', 'help', 'list']; + public const RESERVED_SHORTCUTS = ['h', 'q', 'V', 'n', 'e', 'v', 'vv', 'vvv']; + /** * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int|null $mode The option mode: One of the VALUE_* constants @@ -79,6 +83,10 @@ public function __construct(string $name, $shortcut = null, int $mode = null, st } $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); $shortcuts = array_filter($shortcuts); + $used_reserved_shortcuts = array_intersect($shortcuts, self::RESERVED_SHORTCUTS); + if ($used_reserved_shortcuts && !\in_array($name, array_merge(self::RESERVED_NAMES, self::RESERVED_COMMANDS))) { + throw new InvalidArgumentException(sprintf('An option shortcut cannot include a reserved shortcut (%s).', implode('|', $used_reserved_shortcuts))); + } $shortcut = implode('|', $shortcuts); if (empty($shortcut)) { diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 81dfe7e9a2b5b..e1d440786b2cc 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -1167,19 +1167,19 @@ public function testRunDispatchesExitCodeOneForExceptionCodeZero() public function testAddingOptionWithDuplicateShortcut() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('An option with shortcut "e" already exists.'); + $this->expectExceptionMessage('An option with shortcut "t" already exists.'); $dispatcher = new EventDispatcher(); $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions(false); $application->setDispatcher($dispatcher); - $application->getDefinition()->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'Environment')); + $application->getDefinition()->addOption(new InputOption('--test', '-t', InputOption::VALUE_REQUIRED, 'Test')); $application ->register('foo') ->setAliases(['f']) - ->setDefinition([new InputOption('survey', 'e', InputOption::VALUE_REQUIRED, 'My option with a shortcut.')]) + ->setDefinition([new InputOption('survey', 't', InputOption::VALUE_REQUIRED, 'My option with a shortcut.')]) ->setCode(function (InputInterface $input, OutputInterface $output) {}) ; @@ -1198,6 +1198,7 @@ public function testAddingAlreadySetDefinitionElementData($def) $application = new Application(); $application->setAutoExit(false); $application->setCatchExceptions(false); + $application ->register('foo') ->setDefinition([$def]) @@ -1214,7 +1215,7 @@ public function getAddingAlreadySetDefinitionElementData() return [ [new InputArgument('command', InputArgument::REQUIRED)], [new InputOption('quiet', '', InputOption::VALUE_NONE)], - [new InputOption('query', 'q', InputOption::VALUE_NONE)], + //[new InputOption('quiet', 'q', InputOption::VALUE_NONE)], \InvalidArgumentException::class); ]; } diff --git a/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php b/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php index 8ab83d036fe05..5c44e01db7194 100644 --- a/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php +++ b/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php @@ -57,6 +57,41 @@ public function testShortcut() $this->assertNull($option->getShortcut(), '__construct() makes the shortcut null by default'); } + public function testInvalidShortcut() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (e).'); + $option = new InputOption('foo', 'e'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (v).'); + $option = new InputOption('foo', 'x|v|z'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (v).'); + $option = new InputOption('foo', ['x' | 'v' | 'z']); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (V).'); + $option = new InputOption('foo', 'V'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (vvv).'); + $option = new InputOption('foo', 'vvv'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (help).'); + $option = new InputOption('foo', 'help'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (ansi).'); + $option = new InputOption('foo', 'ansi'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('An option shortcut cannot include a reserved shortcut (no-ansi).'); + $option = new InputOption('foo', 'no-ansi'); + } + public function testModes() { $option = new InputOption('foo', 'f'); diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command.php b/src/Symfony/Component/Runtime/Tests/phpt/command.php index f8ba65133e7db..1fe7a93c077e8 100644 --- a/src/Symfony/Component/Runtime/Tests/phpt/command.php +++ b/src/Symfony/Component/Runtime/Tests/phpt/command.php @@ -8,7 +8,7 @@ require __DIR__.'/autoload.php'; return function (Command $command, InputInterface $input, OutputInterface $output, array $context) { - $command->addOption('hello', 'e', InputOption::VALUE_REQUIRED, 'How should I greet?', 'OK'); + $command->addOption('hello', 'he', InputOption::VALUE_REQUIRED, 'How should I greet?', 'OK'); return $command->setCode(function () use ($input, $output, $context) { $output->write($input->getOption('hello').' Command '.$context['SOME_VAR']); diff --git a/src/Symfony/Component/Yaml/Command/LintCommand.php b/src/Symfony/Component/Yaml/Command/LintCommand.php index 92c54d997c133..735190f0e27c6 100644 --- a/src/Symfony/Component/Yaml/Command/LintCommand.php +++ b/src/Symfony/Component/Yaml/Command/LintCommand.php @@ -58,7 +58,7 @@ protected function configure() ->setDescription(self::$defaultDescription) ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format') - ->addOption('exclude', 'e', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to exclude') + ->addOption('exclude', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to exclude') ->addOption('parse-tags', null, InputOption::VALUE_NEGATABLE, 'Parse custom tags', null) ->setHelp(<<%command.name% command lints a YAML file and outputs to STDOUT