From 5b71176408a54344c63e78790af126ffbb4ba4a3 Mon Sep 17 00:00:00 2001 From: Hafiz Muhammad Moaz Date: Mon, 7 Sep 2026 14:17:34 +0500 Subject: [PATCH] fix: reject typed options passed without a value, add flag type (fixes #38) --- src/Command.php | 16 ++++++++++++++-- tests/ValidationTest.php | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Command.php b/src/Command.php index 6552d77..cf39eb3 100644 --- a/src/Command.php +++ b/src/Command.php @@ -61,7 +61,9 @@ abstract class Command * Option definitions. * * @var array> Definitions keyed by option name, - * each with optional "type", "required" and "default" keys + * each with optional "type" ("string", "int", "float", "numeric" or + * "flag"), "required" and "default" keys. Only options declared as + * "flag" accept being passed without a value */ protected array $optionDefinitions = []; /** @@ -369,7 +371,17 @@ protected function validateDefinitions(array $definitions, array $values, string if (!\is_string($type)) { $type = 'string'; } - if ($type === 'string' || !\is_string($value)) { + if ($type === 'flag') { + continue; + } + if (!\is_string($value) || $type === 'string') { + if (!\is_string($value)) { + // A boolean (true) means the option was passed without a + // value, which is only valid for "flag" definitions + $errors[] = $translator + ? $translator->render('cli', 'validation.type', [$label, (string) $key, $type]) + : $label . ' "' . $key . '" must be of type ' . $type . '.'; + } continue; } $valid = match ($type) { diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 7a3e6bc..c31fb5f 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -94,6 +94,41 @@ public function testMissingRequiredOptionReportsAnError() : void self::assertStringContainsString('option "count" is required', Stderr::getContents()); } + public function testTypedOptionWithoutValueReportsAnError() : void + { + $command = new ValidatedCommandMock($this->console); + $command->setOptionDefinitions([ + 'limit' => ['type' => 'int'], + ]); + $this->console->addCommand($command); + $this->console->exec('validated 5 --limit'); + self::assertStringContainsString('option "limit" must be of type int', Stderr::getContents()); + self::assertStringNotContainsString('ran', Stdout::getContents()); + } + + public function testFlagOptionAllowsBeingPassedWithoutValue() : void + { + $command = new ValidatedCommandMock($this->console); + $command->setOptionDefinitions([ + 'all' => ['type' => 'flag'], + ]); + $this->console->addCommand($command); + $this->console->exec('validated 5 --all'); + self::assertStringContainsString('ran', Stdout::getContents()); + self::assertTrue($this->console->getOption('all')); + } + + public function testTypedOptionWithValueStillValidates() : void + { + $command = new ValidatedCommandMock($this->console); + $command->setOptionDefinitions([ + 'limit' => ['type' => 'int'], + ]); + $this->console->addCommand($command); + $this->console->exec('validated 5 --limit=abc'); + self::assertStringContainsString('option "limit" must be of type int', Stderr::getContents()); + } + public function testValidationErrorsAreTranslatedToSpanish() : void { $console = new ConsoleMock(new Language('es'));