diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b70d5bb..88c088fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,11 @@ ## Not released yet +* Add a option `ignoreValidationErrors` on `AsTask` attribute to ignore + parameters & options validation errors +* Add a way to merge an application `box.json` config file used by `castor:repack`command * Deprecate `Context::withPath()` in favor of `Context::withCurrentDirectory()` * Deprecate `path` argument in `capture()`, `exit_code()`, `run()`, `with()` in favor of `currentDirectory` -* Add a way to merge an application `box.json` config file used by `castor:repack`command ## 0.14.0 (2024-03-08) diff --git a/bin/castor b/bin/castor index 873b8d6f..9512426e 100755 --- a/bin/castor +++ b/bin/castor @@ -2,6 +2,7 @@ run(); +ApplicationFactory::create()->run(new Input()); diff --git a/bin/generate-tests.php b/bin/generate-tests.php index af439573..db6c9065 100755 --- a/bin/generate-tests.php +++ b/bin/generate-tests.php @@ -116,19 +116,21 @@ add_test([], $class, '{{ base }}/tests/Examples/fixtures/broken/' . $dir->getRelativePath()); } -add_test(['parallel:sleep', '--sleep5', '0', '--sleep7', '0', '--sleep10', '0'], 'ParallelSleepTest'); -add_test(['context:context', '--context', 'run'], 'ContextContextRunTest'); +add_test(['args:passthru', 'a', 'b', '--no', '--foo', 'bar', '-x'], 'ArgPassthruExpanded'); +add_test(['context:context', '--context', 'dynamic'], 'ContextContextDynamicTest'); add_test(['context:context', '--context', 'my_default', '-v'], 'ContextContextMyDefaultTest'); add_test(['context:context', '--context', 'no_no_exist'], 'ContextContextDoNotExistTest'); -add_test(['context:context', '--context', 'production'], 'ContextContextProductionTest'); add_test(['context:context', '--context', 'path'], 'ContextContextPathTest'); -add_test(['context:context', '--context', 'dynamic'], 'ContextContextDynamicTest'); +add_test(['context:context', '--context', 'production'], 'ContextContextProductionTest'); +add_test(['context:context', '--context', 'run'], 'ContextContextRunTest'); add_test(['enabled:hello', '--context', 'production'], 'EnabledInProduction'); -add_test([], 'NewProjectTest', '/tmp'); +add_test(['parallel:sleep', '--sleep5', '0', '--sleep7', '0', '--sleep10', '0'], 'ParallelSleepTest'); +// In /tmp +add_test(['completion', 'bash'], 'NoConfigCompletionTest', '/tmp'); add_test(['init'], 'NewProjectInitTest', '/tmp'); -add_test(['unknown:task'], 'NoConfigUnknownTest', '/tmp'); add_test(['unknown:task', 'toto', '--foo', 1], 'NoConfigUnknownWithArgsTest', '/tmp'); -add_test(['completion', 'bash'], 'NoConfigCompletionTest', '/tmp'); +add_test(['unknown:task'], 'NoConfigUnknownTest', '/tmp'); +add_test([], 'NewProjectTest', '/tmp'); function add_test(array $args, string $class, ?string $cwd = null) { diff --git a/examples/args.php b/examples/args.php index f5827c0b..e9c63d75 100644 --- a/examples/args.php +++ b/examples/args.php @@ -4,6 +4,7 @@ use Castor\Attribute\AsArgument; use Castor\Attribute\AsOption; +use Castor\Attribute\AsRawTokens; use Castor\Attribute\AsTask; use function Castor\run; @@ -32,3 +33,12 @@ function another_args( ): void { run(['echo', $required, $test2]); } + +/** + * @param string[] $rawTokens + */ +#[AsTask(description: 'Dumps all arguments and options, without configuration nor validation', ignoreValidationErrors: true)] +function passthru(#[AsRawTokens] array $rawTokens): void +{ + var_dump($rawTokens); +} diff --git a/src/Attribute/AsRawTokens.php b/src/Attribute/AsRawTokens.php new file mode 100644 index 00000000..3071806e --- /dev/null +++ b/src/Attribute/AsRawTokens.php @@ -0,0 +1,8 @@ +input ?? throw new \LogicException('Input not available yet.'); } @@ -126,7 +127,9 @@ public function getCommand(bool $allowNull = false): ?Command // is registered public function doRun(InputInterface $input, OutputInterface $output): int { - $this->input = $input; + if ($input instanceof Input) { + $this->input = $input; + } $this->sectionOutput = new SectionOutput($output); $this->symfonyStyle = new SymfonyStyle($input, $output); $this->logger->pushHandler(new ConsoleHandler($output)); diff --git a/src/Console/Command/SymfonyTaskCommand.php b/src/Console/Command/SymfonyTaskCommand.php index 75b1c18a..bf064d9f 100644 --- a/src/Console/Command/SymfonyTaskCommand.php +++ b/src/Console/Command/SymfonyTaskCommand.php @@ -3,6 +3,7 @@ namespace Castor\Console\Command; use Castor\Attribute\AsSymfonyTask; +use Castor\Console\Input\Input; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -67,10 +68,12 @@ protected function configure(): void } } + /** + * @param Input $input + */ protected function execute(InputInterface $input, OutputInterface $output): int { - $r = new \ReflectionProperty($input, 'tokens'); - $extra = array_filter($r->getValue($input), fn ($item) => $item !== $this->taskAttribute->name); + $extra = array_filter($input->getRawTokens(), fn ($item) => $item !== $this->taskAttribute->name); $p = new Process([...$this->taskAttribute->console, $this->taskAttribute->originalName, ...$extra]); $p->run(fn ($type, $bytes) => print ($bytes)); diff --git a/src/Console/Command/TaskCommand.php b/src/Console/Command/TaskCommand.php index 54d415a6..a5ec276f 100644 --- a/src/Console/Command/TaskCommand.php +++ b/src/Console/Command/TaskCommand.php @@ -5,8 +5,10 @@ use Castor\Attribute\AsArgument; use Castor\Attribute\AsCommandArgument; use Castor\Attribute\AsOption; +use Castor\Attribute\AsRawTokens; use Castor\Attribute\AsTask; use Castor\Console\Application; +use Castor\Console\Input\Input; use Castor\Event\AfterExecuteTaskEvent; use Castor\Event\BeforeExecuteTaskEvent; use Castor\EventDispatcher; @@ -76,7 +78,15 @@ public function isEnabled(): bool protected function configure(): void { + if ($this->taskAttribute->ignoreValidationErrors) { + $this->ignoreValidationErrors(); + } + foreach ($this->function->getParameters() as $parameter) { + if ($parameter->getAttributes(AsRawTokens::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { + continue; + } + $taskArgumentAttribute = $parameter->getAttributes(AsCommandArgument::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null; if ($taskArgumentAttribute) { @@ -140,11 +150,33 @@ protected function configure(): void } } + /** + * @param Input $input + */ protected function execute(InputInterface $input, OutputInterface $output): int { $args = []; foreach ($this->function->getParameters() as $parameter) { + if ($parameter->getAttributes(AsRawTokens::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { + $parameters = []; + $keep = false; + foreach ($input->getRawTokens() as $value) { + if ($value === $input->getFirstArgument()) { + $keep = true; + + continue; + } + if ($keep) { + $parameters[] = $value; + } + } + + $args[] = $parameters; + + continue; + } + $name = $this->getParameterName($parameter); if ($input->hasArgument($name)) { $args[] = $input->getArgument($name); diff --git a/src/Console/Input/Input.php b/src/Console/Input/Input.php new file mode 100644 index 00000000..85efc40c --- /dev/null +++ b/src/Console/Input/Input.php @@ -0,0 +1,17 @@ + + */ + public function getRawTokens(): array + { + // @phpstan-ignore-next-line + return (fn () => $this->tokens)->bindTo($this, ArgvInput::class)(); + } +} diff --git a/src/GlobalHelper.php b/src/GlobalHelper.php index c6a210ab..11aed709 100644 --- a/src/GlobalHelper.php +++ b/src/GlobalHelper.php @@ -3,10 +3,10 @@ namespace Castor; use Castor\Console\Application; +use Castor\Console\Input\Input; use Monolog\Logger; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Filesystem\Filesystem; @@ -57,7 +57,7 @@ public static function getLogger(): Logger return self::getApplication()->logger; } - public static function getInput(): InputInterface + public static function getInput(): Input { return self::getApplication()->getInput(); } diff --git a/src/functions.php b/src/functions.php index b3407765..c303f678 100644 --- a/src/functions.php +++ b/src/functions.php @@ -4,6 +4,7 @@ use Castor\Attribute\AsContextGenerator; use Castor\Console\Application; +use Castor\Console\Input\Input; use Castor\Exception\ExecutableNotFoundException; use Castor\Exception\MinimumVersionRequirementNotMetException; use Castor\Exception\WaitFor\ExitedBeforeTimeoutException; @@ -631,7 +632,7 @@ function get_application(): Application return app(); } -function input(): InputInterface +function input(): Input { return GlobalHelper::getInput(); } diff --git a/tests/Examples/Generated/ArgPassthruExpanded.php b/tests/Examples/Generated/ArgPassthruExpanded.php new file mode 100644 index 00000000..05f989f2 --- /dev/null +++ b/tests/Examples/Generated/ArgPassthruExpanded.php @@ -0,0 +1,22 @@ +runTask(['args:passthru', 'a', 'b', '--no', '--foo', 'bar', '-x']); + + $this->assertSame(0, $process->getExitCode()); + $this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput()); + if (file_exists(__FILE__ . '.err.txt')) { + $this->assertStringEqualsFile(__FILE__ . '.err.txt', $process->getErrorOutput()); + } else { + $this->assertSame('', $process->getErrorOutput()); + } + } +} diff --git a/tests/Examples/Generated/ArgPassthruExpanded.php.output.txt b/tests/Examples/Generated/ArgPassthruExpanded.php.output.txt new file mode 100644 index 00000000..01aefae4 --- /dev/null +++ b/tests/Examples/Generated/ArgPassthruExpanded.php.output.txt @@ -0,0 +1,14 @@ +array(6) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" + [2]=> + string(4) "--no" + [3]=> + string(5) "--foo" + [4]=> + string(3) "bar" + [5]=> + string(2) "-x" +} diff --git a/tests/Examples/Generated/ArgsPassthruTest.php b/tests/Examples/Generated/ArgsPassthruTest.php new file mode 100644 index 00000000..3f6b2d02 --- /dev/null +++ b/tests/Examples/Generated/ArgsPassthruTest.php @@ -0,0 +1,22 @@ +runTask(['args:passthru']); + + $this->assertSame(0, $process->getExitCode()); + $this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput()); + if (file_exists(__FILE__ . '.err.txt')) { + $this->assertStringEqualsFile(__FILE__ . '.err.txt', $process->getErrorOutput()); + } else { + $this->assertSame('', $process->getErrorOutput()); + } + } +} diff --git a/tests/Examples/Generated/ArgsPassthruTest.php.output.txt b/tests/Examples/Generated/ArgsPassthruTest.php.output.txt new file mode 100644 index 00000000..63a30c0d --- /dev/null +++ b/tests/Examples/Generated/ArgsPassthruTest.php.output.txt @@ -0,0 +1,2 @@ +array(0) { +} diff --git a/tests/Examples/Generated/ListTest.php.output.txt b/tests/Examples/Generated/ListTest.php.output.txt index 8155299e..269a7c83 100644 --- a/tests/Examples/Generated/ListTest.php.output.txt +++ b/tests/Examples/Generated/ListTest.php.output.txt @@ -6,6 +6,7 @@ list List commands no-namespace Task without a namespace args:another-args Dumps all arguments and options, without configuration args:args Dumps all arguments and options, with custom configuration +args:passthru Dumps all arguments and options, without configuration not validation bar:bar Prints bar, but also executes foo cache:complex Cache with usage of CacheItemInterface cache:simple Cache a simple call