One Hat Cyber Team
Your IP:
216.73.216.176
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
proc
/
thread-self
/
root
/
proc
/
self
/
cwd
/
Edit File:
Command.tar
MailerTestCommand.php 0000644 00000005015 15111153305 0010621 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Mailer\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Mailer\Transport\TransportInterface; use Symfony\Component\Mime\Email; /** * A console command to test Mailer transports. */ #[AsCommand(name: 'mailer:test', description: 'Test Mailer transports by sending an email')] final class MailerTestCommand extends Command { public function __construct(private TransportInterface $transport) { $this->transport = $transport; parent::__construct(); } protected function configure(): void { $this ->addArgument('to', InputArgument::REQUIRED, 'The recipient of the message') ->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'from@example.org') ->addOption('subject', null, InputOption::VALUE_OPTIONAL, 'The subject of the message', 'Testing transport') ->addOption('body', null, InputOption::VALUE_OPTIONAL, 'The body of the message', 'Testing body') ->addOption('transport', null, InputOption::VALUE_OPTIONAL, 'The transport to be used') ->setHelp(<<<'EOF' The <info>%command.name%</info> command tests a Mailer transport by sending a simple email message: <info>php %command.full_name% to@example.com</info> You can also specify a specific transport: <info>php %command.full_name% to@example.com --transport=transport_name</info> Note that this command bypasses the Messenger bus if configured. EOF ); } protected function execute(InputInterface $input, OutputInterface $output): int { $message = (new Email()) ->to($input->getArgument('to')) ->from($input->getOption('from')) ->subject($input->getOption('subject')) ->text($input->getOption('body')) ; if ($transport = $input->getOption('transport')) { $message->getHeaders()->addTextHeader('X-Transport', $transport); } $this->transport->send($message); return 0; } } error_log 0000644 00000007625 15111153305 0006466 0 ustar 00 [20-Nov-2025 02:26:54 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPullCommand.php:33 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPullCommand.php on line 33 [20-Nov-2025 02:31:17 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php:36 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php on line 36 [20-Nov-2025 02:33:50 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php:33 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php on line 33 [20-Nov-2025 06:13:19 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php:36 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php on line 36 [20-Nov-2025 06:28:18 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php:33 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php on line 33 [20-Nov-2025 06:30:11 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPullCommand.php:33 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPullCommand.php on line 33 [25-Nov-2025 06:29:32 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php:33 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php on line 33 [25-Nov-2025 06:30:11 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php:36 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php on line 36 [25-Nov-2025 06:30:38 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPullCommand.php:33 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPullCommand.php on line 33 [26-Nov-2025 01:20:03 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php:33 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/TranslationPushCommand.php on line 33 [26-Nov-2025 02:05:17 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Console\Command\Command" not found in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php:36 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/translation/Command/XliffLintCommand.php on line 36 GenerateUuidCommand.php 0000644 00000016453 15111172406 0011144 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\Factory\UuidFactory; use Symfony\Component\Uid\Uuid; #[AsCommand(name: 'uuid:generate', description: 'Generate a UUID')] class GenerateUuidCommand extends Command { private UuidFactory $factory; public function __construct(UuidFactory $factory = null) { $this->factory = $factory ?? new UuidFactory(); parent::__construct(); } protected function configure(): void { $this ->setDefinition([ new InputOption('time-based', null, InputOption::VALUE_REQUIRED, 'The timestamp, to generate a time-based UUID: a parsable date/time string'), new InputOption('node', null, InputOption::VALUE_REQUIRED, 'The UUID whose node part should be used as the node of the generated UUID'), new InputOption('name-based', null, InputOption::VALUE_REQUIRED, 'The name, to generate a name-based UUID'), new InputOption('namespace', null, InputOption::VALUE_REQUIRED, 'The UUID to use at the namespace for named-based UUIDs, predefined namespaces keywords "dns", "url", "oid" and "x500" are accepted'), new InputOption('random-based', null, InputOption::VALUE_NONE, 'To generate a random-based UUID'), new InputOption('count', 'c', InputOption::VALUE_REQUIRED, 'The number of UUID to generate', 1), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, sprintf('The UUID output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'rfc4122'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> generates a UUID. <info>php %command.full_name%</info> To generate a time-based UUID: <info>php %command.full_name% --time-based=now</info> To specify a time-based UUID's node: <info>php %command.full_name% --time-based=@1613480254 --node=fb3502dc-137e-4849-8886-ac90d07f64a7</info> To generate a name-based UUID: <info>php %command.full_name% --name-based=foo</info> To specify a name-based UUID's namespace: <info>php %command.full_name% --name-based=bar --namespace=fb3502dc-137e-4849-8886-ac90d07f64a7</info> To generate a random-based UUID: <info>php %command.full_name% --random-based</info> To generate several UUIDs: <info>php %command.full_name% --count=10</info> To output a specific format: <info>php %command.full_name% --format=base58</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); $time = $input->getOption('time-based'); $node = $input->getOption('node'); $name = $input->getOption('name-based'); $namespace = $input->getOption('namespace'); $random = $input->getOption('random-based'); if (false !== ($time ?? $name ?? $random) && 1 < ((null !== $time) + (null !== $name) + $random)) { $io->error('Only one of "--time-based", "--name-based" or "--random-based" can be provided at a time.'); return 1; } if (null === $time && null !== $node) { $io->error('Option "--node" can only be used with "--time-based".'); return 1; } if (null === $name && null !== $namespace) { $io->error('Option "--namespace" can only be used with "--name-based".'); return 1; } switch (true) { case null !== $time: if (null !== $node) { try { $node = Uuid::fromString($node); } catch (\InvalidArgumentException $e) { $io->error(sprintf('Invalid node "%s": %s', $node, $e->getMessage())); return 1; } } try { new \DateTimeImmutable($time); } catch (\Exception $e) { $io->error(sprintf('Invalid timestamp "%s": %s', $time, str_replace('DateTimeImmutable::__construct(): ', '', $e->getMessage()))); return 1; } $create = fn (): Uuid => $this->factory->timeBased($node)->create(new \DateTimeImmutable($time)); break; case null !== $name: if ($namespace && !\in_array($namespace, ['dns', 'url', 'oid', 'x500'], true)) { try { $namespace = Uuid::fromString($namespace); } catch (\InvalidArgumentException $e) { $io->error(sprintf('Invalid namespace "%s": %s', $namespace, $e->getMessage())); return 1; } } $create = function () use ($namespace, $name): Uuid { try { $factory = $this->factory->nameBased($namespace); } catch (\LogicException) { throw new \InvalidArgumentException('Missing namespace: use the "--namespace" option or configure a default namespace in the underlying factory.'); } return $factory->create($name); }; break; case $random: $create = $this->factory->randomBased()->create(...); break; default: $create = $this->factory->create(...); break; } $formatOption = $input->getOption('format'); if (\in_array($formatOption, $this->getAvailableFormatOptions())) { $format = 'to'.ucfirst($formatOption); } else { $io->error(sprintf('Invalid format "%s", supported formats are "%s".', $formatOption, implode('", "', $this->getAvailableFormatOptions()))); return 1; } $count = (int) $input->getOption('count'); try { for ($i = 0; $i < $count; ++$i) { $output->writeln($create()->$format()); } } catch (\Exception $e) { $io->error($e->getMessage()); return 1; } return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormatOptions()); } } private function getAvailableFormatOptions(): array { return [ 'base32', 'base58', 'rfc4122', ]; } } InspectUlidCommand.php 0000644 00000004177 15111172406 0011006 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\Ulid; #[AsCommand(name: 'ulid:inspect', description: 'Inspect a ULID')] class InspectUlidCommand extends Command { protected function configure(): void { $this ->setDefinition([ new InputArgument('ulid', InputArgument::REQUIRED, 'The ULID to inspect'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> displays information about a ULID. <info>php %command.full_name% 01EWAKBCMWQ2C94EXNN60ZBS0Q</info> <info>php %command.full_name% 1BVdfLn3ERmbjYBLCdaaLW</info> <info>php %command.full_name% 01771535-b29c-b898-923b-b5a981f5e417</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); try { $ulid = Ulid::fromString($input->getArgument('ulid')); } catch (\InvalidArgumentException $e) { $io->error($e->getMessage()); return 1; } $io->table(['Label', 'Value'], [ ['toBase32 (canonical)', (string) $ulid], ['toBase58', $ulid->toBase58()], ['toRfc4122', $ulid->toRfc4122()], ['toHex', $ulid->toHex()], new TableSeparator(), ['Time', $ulid->getDateTime()->format('Y-m-d H:i:s.v \U\T\C')], ]); return 0; } } GenerateUlidCommand.php 0000644 00000007336 15111172406 0011133 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\Factory\UlidFactory; #[AsCommand(name: 'ulid:generate', description: 'Generate a ULID')] class GenerateUlidCommand extends Command { private UlidFactory $factory; public function __construct(UlidFactory $factory = null) { $this->factory = $factory ?? new UlidFactory(); parent::__construct(); } protected function configure(): void { $this ->setDefinition([ new InputOption('time', null, InputOption::VALUE_REQUIRED, 'The ULID timestamp: a parsable date/time string'), new InputOption('count', 'c', InputOption::VALUE_REQUIRED, 'The number of ULID to generate', 1), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, sprintf('The ULID output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'base32'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> command generates a ULID. <info>php %command.full_name%</info> To specify the timestamp: <info>php %command.full_name% --time="2021-02-16 14:09:08"</info> To generate several ULIDs: <info>php %command.full_name% --count=10</info> To output a specific format: <info>php %command.full_name% --format=rfc4122</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); if (null !== $time = $input->getOption('time')) { try { $time = new \DateTimeImmutable($time); } catch (\Exception $e) { $io->error(sprintf('Invalid timestamp "%s": %s', $time, str_replace('DateTimeImmutable::__construct(): ', '', $e->getMessage()))); return 1; } } $formatOption = $input->getOption('format'); if (\in_array($formatOption, $this->getAvailableFormatOptions())) { $format = 'to'.ucfirst($formatOption); } else { $io->error(sprintf('Invalid format "%s", supported formats are "%s".', $formatOption, implode('", "', $this->getAvailableFormatOptions()))); return 1; } $count = (int) $input->getOption('count'); try { for ($i = 0; $i < $count; ++$i) { $output->writeln($this->factory->create($time)->$format()); } } catch (\Exception $e) { $io->error($e->getMessage()); return 1; } return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormatOptions()); } } private function getAvailableFormatOptions(): array { return [ 'base32', 'base58', 'rfc4122', ]; } } InspectUuidCommand.php 0000644 00000005173 15111172406 0011014 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Uid\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Uid\MaxUuid; use Symfony\Component\Uid\NilUuid; use Symfony\Component\Uid\TimeBasedUidInterface; use Symfony\Component\Uid\Uuid; #[AsCommand(name: 'uuid:inspect', description: 'Inspect a UUID')] class InspectUuidCommand extends Command { protected function configure(): void { $this ->setDefinition([ new InputArgument('uuid', InputArgument::REQUIRED, 'The UUID to inspect'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</info> displays information about a UUID. <info>php %command.full_name% a7613e0a-5986-11eb-a861-2bf05af69e52</info> <info>php %command.full_name% MfnmaUvvQ1h8B14vTwt6dX</info> <info>php %command.full_name% 57C4Z0MPC627NTGR9BY1DFD7JJ</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); try { /** @var Uuid $uuid */ $uuid = Uuid::fromString($input->getArgument('uuid')); } catch (\InvalidArgumentException $e) { $io->error($e->getMessage()); return 1; } if (new NilUuid() == $uuid) { $version = 'nil'; } elseif (new MaxUuid() == $uuid) { $version = 'max'; } else { $version = uuid_type($uuid); } $rows = [ ['Version', $version], ['toRfc4122 (canonical)', (string) $uuid], ['toBase58', $uuid->toBase58()], ['toBase32', $uuid->toBase32()], ['toHex', $uuid->toHex()], ]; if ($uuid instanceof TimeBasedUidInterface) { $rows[] = new TableSeparator(); $rows[] = ['Time', $uuid->getDateTime()->format('Y-m-d H:i:s.u \U\T\C')]; } $io->table(['Label', 'Value'], $rows); return 0; } } ReflectingCommand.php 0000644 00000025736 15111273343 0010653 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use PhpParser\NodeTraverser; use PhpParser\PrettyPrinter\Standard as Printer; use Psy\CodeCleaner\NoReturnValue; use Psy\Context; use Psy\ContextAware; use Psy\Exception\ErrorException; use Psy\Exception\RuntimeException; use Psy\Exception\UnexpectedTargetException; use Psy\Reflection\ReflectionClassConstant; use Psy\Reflection\ReflectionConstant_; use Psy\Sudo\SudoVisitor; use Psy\Util\Mirror; /** * An abstract command with helpers for inspecting the current context. */ abstract class ReflectingCommand extends Command implements ContextAware { const CLASS_OR_FUNC = '/^[\\\\\w]+$/'; const CLASS_MEMBER = '/^([\\\\\w]+)::(\w+)$/'; const CLASS_STATIC = '/^([\\\\\w]+)::\$(\w+)$/'; const INSTANCE_MEMBER = '/^(\$\w+)(::|->)(\w+)$/'; /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; private $parser; private $traverser; private $printer; /** * {@inheritdoc} */ public function __construct($name = null) { $this->parser = new CodeArgumentParser(); $this->traverser = new NodeTraverser(); $this->traverser->addVisitor(new SudoVisitor()); $this->printer = new Printer(); parent::__construct($name); } /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * Get the target for a value. * * @throws \InvalidArgumentException when the value specified can't be resolved * * @param string $valueName Function, class, variable, constant, method or property name * * @return array (class or instance name, member name, kind) */ protected function getTarget(string $valueName): array { $valueName = \trim($valueName); $matches = []; switch (true) { case \preg_match(self::CLASS_OR_FUNC, $valueName, $matches): return [$this->resolveName($matches[0], true), null, 0]; case \preg_match(self::CLASS_MEMBER, $valueName, $matches): return [$this->resolveName($matches[1]), $matches[2], Mirror::CONSTANT | Mirror::METHOD]; case \preg_match(self::CLASS_STATIC, $valueName, $matches): return [$this->resolveName($matches[1]), $matches[2], Mirror::STATIC_PROPERTY | Mirror::PROPERTY]; case \preg_match(self::INSTANCE_MEMBER, $valueName, $matches): if ($matches[2] === '->') { $kind = Mirror::METHOD | Mirror::PROPERTY; } else { $kind = Mirror::CONSTANT | Mirror::METHOD; } return [$this->resolveObject($matches[1]), $matches[3], $kind]; default: return [$this->resolveObject($valueName), null, 0]; } } /** * Resolve a class or function name (with the current shell namespace). * * @throws ErrorException when `self` or `static` is used in a non-class scope * * @param string $name * @param bool $includeFunctions (default: false) */ protected function resolveName(string $name, bool $includeFunctions = false): string { $shell = $this->getApplication(); // While not *technically* 100% accurate, let's treat `self` and `static` as equivalent. if (\in_array(\strtolower($name), ['self', 'static'])) { if ($boundClass = $shell->getBoundClass()) { return $boundClass; } if ($boundObject = $shell->getBoundObject()) { return \get_class($boundObject); } $msg = \sprintf('Cannot use "%s" when no class scope is active', \strtolower($name)); throw new ErrorException($msg, 0, \E_USER_ERROR, "eval()'d code", 1); } if (\substr($name, 0, 1) === '\\') { return $name; } // Check $name against the current namespace and use statements. if (self::couldBeClassName($name)) { try { $name = $this->resolveCode($name.'::class'); } catch (RuntimeException $e) { // /shrug } } if ($namespace = $shell->getNamespace()) { $fullName = $namespace.'\\'.$name; if (\class_exists($fullName) || \interface_exists($fullName) || ($includeFunctions && \function_exists($fullName))) { return $fullName; } } return $name; } /** * Check whether a given name could be a class name. */ protected function couldBeClassName(string $name): bool { // Regex based on https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class return \preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/', $name) === 1; } /** * Get a Reflector and documentation for a function, class or instance, constant, method or property. * * @param string $valueName Function, class, variable, constant, method or property name * * @return array (value, Reflector) */ protected function getTargetAndReflector(string $valueName): array { list($value, $member, $kind) = $this->getTarget($valueName); return [$value, Mirror::get($value, $member, $kind)]; } /** * Resolve code to a value in the current scope. * * @throws RuntimeException when the code does not return a value in the current scope * * @param string $code * * @return mixed Variable value */ protected function resolveCode(string $code) { try { // Add an implicit `sudo` to target resolution. $nodes = $this->traverser->traverse($this->parser->parse($code)); $sudoCode = $this->printer->prettyPrint($nodes); $value = $this->getApplication()->execute($sudoCode, true); } catch (\Throwable $e) { // Swallow all exceptions? } if (!isset($value) || $value instanceof NoReturnValue) { throw new RuntimeException('Unknown target: '.$code); } return $value; } /** * Resolve code to an object in the current scope. * * @throws UnexpectedTargetException when the code resolves to a non-object value * * @param string $code * * @return object Variable instance */ private function resolveObject(string $code) { $value = $this->resolveCode($code); if (!\is_object($value)) { throw new UnexpectedTargetException($value, 'Unable to inspect a non-object'); } return $value; } /** * @deprecated Use `resolveCode` instead * * @param string $name * * @return mixed Variable instance */ protected function resolveInstance(string $name) { @\trigger_error('`resolveInstance` is deprecated; use `resolveCode` instead.', \E_USER_DEPRECATED); return $this->resolveCode($name); } /** * Get a variable from the current shell scope. * * @param string $name * * @return mixed */ protected function getScopeVariable(string $name) { return $this->context->get($name); } /** * Get all scope variables from the current shell scope. * * @return array */ protected function getScopeVariables(): array { return $this->context->getAll(); } /** * Given a Reflector instance, set command-scope variables in the shell * execution context. This is used to inject magic $__class, $__method and * $__file variables (as well as a handful of others). * * @param \Reflector $reflector */ protected function setCommandScopeVariables(\Reflector $reflector) { $vars = []; switch (\get_class($reflector)) { case \ReflectionClass::class: case \ReflectionObject::class: $vars['__class'] = $reflector->name; if ($reflector->inNamespace()) { $vars['__namespace'] = $reflector->getNamespaceName(); } break; case \ReflectionMethod::class: $vars['__method'] = \sprintf('%s::%s', $reflector->class, $reflector->name); $vars['__class'] = $reflector->class; $classReflector = $reflector->getDeclaringClass(); if ($classReflector->inNamespace()) { $vars['__namespace'] = $classReflector->getNamespaceName(); } break; case \ReflectionFunction::class: $vars['__function'] = $reflector->name; if ($reflector->inNamespace()) { $vars['__namespace'] = $reflector->getNamespaceName(); } break; case \ReflectionGenerator::class: $funcReflector = $reflector->getFunction(); $vars['__function'] = $funcReflector->name; if ($funcReflector->inNamespace()) { $vars['__namespace'] = $funcReflector->getNamespaceName(); } if ($fileName = $reflector->getExecutingFile()) { $vars['__file'] = $fileName; $vars['__line'] = $reflector->getExecutingLine(); $vars['__dir'] = \dirname($fileName); } break; case \ReflectionProperty::class: case \ReflectionClassConstant::class: case ReflectionClassConstant::class: $classReflector = $reflector->getDeclaringClass(); $vars['__class'] = $classReflector->name; if ($classReflector->inNamespace()) { $vars['__namespace'] = $classReflector->getNamespaceName(); } // no line for these, but this'll do if ($fileName = $reflector->getDeclaringClass()->getFileName()) { $vars['__file'] = $fileName; $vars['__dir'] = \dirname($fileName); } break; case ReflectionConstant_::class: if ($reflector->inNamespace()) { $vars['__namespace'] = $reflector->getNamespaceName(); } break; } if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) { if ($fileName = $reflector->getFileName()) { $vars['__file'] = $fileName; $vars['__line'] = $reflector->getStartLine(); $vars['__dir'] = \dirname($fileName); } } $this->context->setCommandScopeVariables($vars); } } HelpCommand.php 0000644 00000004756 15111273343 0007460 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * HelpCommand displays the help for a given command. * * @author Fabien Potencier <fabien@symfony.com> */ class HelpCommand extends Command { private Command $command; /** * @return void */ protected function configure() { $this->ignoreValidationErrors(); $this ->setName('help') ->setDefinition([ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), ]) ->setDescription('Display help for a command') ->setHelp(<<<'EOF' The <info>%command.name%</info> command displays help for a given command: <info>%command.full_name% list</info> You can also output the help in other formats by using the <comment>--format</comment> option: <info>%command.full_name% --format=xml list</info> To display the list of available commands, please use the <info>list</info> command. EOF ) ; } /** * @return void */ public function setCommand(Command $command) { $this->command = $command; } protected function execute(InputInterface $input, OutputInterface $output): int { $this->command ??= $this->getApplication()->find($input->getArgument('command_name')); $helper = new DescriptorHelper(); $helper->describe($output, $this->command, [ 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), ]); unset($this->command); return 0; } } PsyVersionCommand.php 0000644 00000001671 15111273343 0010702 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * A dumb little command for printing out the current Psy Shell version. */ class PsyVersionCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('version') ->setDefinition([]) ->setDescription('Show Psy Shell version.') ->setHelp('Show Psy Shell version.'); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln($this->getApplication()->getVersion()); return 0; } } ClearCommand.php 0000644 00000002150 15111273343 0007600 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Clear the Psy Shell. * * Just what it says on the tin. */ class ClearCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('clear') ->setDefinition([]) ->setDescription('Clear the Psy Shell screen.') ->setHelp( <<<'HELP' Clear the Psy Shell screen. Pro Tip: If your PHP has readline support, you should be able to use ctrl+l too! HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $output->write(\sprintf('%c[2J%c[0;0f', 27, 27)); return 0; } } TimeitCommand.php 0000644 00000011677 15111273343 0010023 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use PhpParser\NodeTraverser; use PhpParser\PrettyPrinter\Standard as Printer; use Psy\Command\TimeitCommand\TimeitVisitor; use Psy\Input\CodeArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Class TimeitCommand. */ class TimeitCommand extends Command { const RESULT_MSG = '<info>Command took %.6f seconds to complete.</info>'; const AVG_RESULT_MSG = '<info>Command took %.6f seconds on average (%.6f median; %.6f total) to complete.</info>'; // All times stored as nanoseconds! private static $useHrtime; private static $start = null; private static $times = []; private $parser; private $traverser; private $printer; /** * {@inheritdoc} */ public function __construct($name = null) { // @todo Remove microtime use after we drop support for PHP < 7.3 self::$useHrtime = \function_exists('hrtime'); $this->parser = new CodeArgumentParser(); $this->traverser = new NodeTraverser(); $this->traverser->addVisitor(new TimeitVisitor()); $this->printer = new Printer(); parent::__construct($name); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('timeit') ->setDefinition([ new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Number of iterations.'), new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'), ]) ->setDescription('Profiles with a timer.') ->setHelp( <<<'HELP' Time profiling for functions and commands. e.g. <return>>>> timeit sleep(1)</return> <return>>>> timeit -n1000 $closure()</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $code = $input->getArgument('code'); $num = (int) ($input->getOption('num') ?: 1); $shell = $this->getApplication(); $instrumentedCode = $this->instrumentCode($code); self::$times = []; do { $_ = $shell->execute($instrumentedCode); $this->ensureEndMarked(); } while (\count(self::$times) < $num); $shell->writeReturnValue($_); $times = self::$times; self::$times = []; if ($num === 1) { $output->writeln(\sprintf(self::RESULT_MSG, $times[0] / 1e+9)); } else { $total = \array_sum($times); \rsort($times); $median = $times[\round($num / 2)]; $output->writeln(\sprintf(self::AVG_RESULT_MSG, ($total / $num) / 1e+9, $median / 1e+9, $total / 1e+9)); } return 0; } /** * Internal method for marking the start of timeit execution. * * A static call to this method will be injected at the start of the timeit * input code to instrument the call. We will use the saved start time to * more accurately calculate time elapsed during execution. */ public static function markStart() { self::$start = self::$useHrtime ? \hrtime(true) : (\microtime(true) * 1e+6); } /** * Internal method for marking the end of timeit execution. * * A static call to this method is injected by TimeitVisitor at the end * of the timeit input code to instrument the call. * * Note that this accepts an optional $ret parameter, which is used to pass * the return value of the last statement back out of timeit. This saves us * a bunch of code rewriting shenanigans. * * @param mixed $ret * * @return mixed it just passes $ret right back */ public static function markEnd($ret = null) { self::$times[] = (self::$useHrtime ? \hrtime(true) : (\microtime(true) * 1e+6)) - self::$start; self::$start = null; return $ret; } /** * Ensure that the end of code execution was marked. * * The end *should* be marked in the instrumented code, but just in case * we'll add a fallback here. */ private function ensureEndMarked() { if (self::$start !== null) { self::markEnd(); } } /** * Instrument code for timeit execution. * * This inserts `markStart` and `markEnd` calls to ensure that (reasonably) * accurate times are recorded for just the code being executed. */ private function instrumentCode(string $code): string { return $this->printer->prettyPrint($this->traverser->traverse($this->parser->parse($code))); } } WtfCommand.php 0000644 00000007414 15111273343 0007322 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Context; use Psy\ContextAware; use Psy\Input\FilterOptions; use Psy\Output\ShellOutput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Show the last uncaught exception. */ class WtfCommand extends TraceCommand implements ContextAware { /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('wtf') ->setAliases(['last-exception', 'wtf?']) ->setDefinition([ new InputArgument('incredulity', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Number of lines to show.'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show entire backtrace.'), $grep, $insensitive, $invert, ]) ->setDescription('Show the backtrace of the most recent exception.') ->setHelp( <<<'HELP' Shows a few lines of the backtrace of the most recent exception. If you want to see more lines, add more question marks or exclamation marks: e.g. <return>>>> wtf ?</return> <return>>>> wtf ?!???!?!?</return> To see the entire backtrace, pass the -a/--all flag: e.g. <return>>>> wtf -a</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->filter->bind($input); $incredulity = \implode('', $input->getArgument('incredulity')); if (\strlen(\preg_replace('/[\\?!]/', '', $incredulity))) { throw new \InvalidArgumentException('Incredulity must include only "?" and "!"'); } $exception = $this->context->getLastException(); $count = $input->getOption('all') ? \PHP_INT_MAX : \max(3, \pow(2, \strlen($incredulity) + 1)); $shell = $this->getApplication(); if ($output instanceof ShellOutput) { $output->startPaging(); } do { $traceCount = \count($exception->getTrace()); $showLines = $count; // Show the whole trace if we'd only be hiding a few lines if ($traceCount < \max($count * 1.2, $count + 2)) { $showLines = \PHP_INT_MAX; } $trace = $this->getBacktrace($exception, $showLines); $moreLines = $traceCount - \count($trace); $output->writeln($shell->formatException($exception)); $output->writeln('--'); $output->write($trace, true, ShellOutput::NUMBER_LINES); $output->writeln(''); if ($moreLines > 0) { $output->writeln(\sprintf( '<aside>Use <return>wtf -a</return> to see %d more lines</aside>', $moreLines )); $output->writeln(''); } } while ($exception = $exception->getPrevious()); if ($output instanceof ShellOutput) { $output->stopPaging(); } return 0; } } ThrowUpCommand.php 0000644 00000007240 15111273343 0010167 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use PhpParser\Node\Arg; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Throw_; use PhpParser\PrettyPrinter\Standard as Printer; use Psy\Context; use Psy\ContextAware; use Psy\Exception\ThrowUpException; use Psy\Input\CodeArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Throw an exception or error out of the Psy Shell. */ class ThrowUpCommand extends Command implements ContextAware { private $parser; private $printer; /** * {@inheritdoc} */ public function __construct($name = null) { $this->parser = new CodeArgumentParser(); $this->printer = new Printer(); parent::__construct($name); } /** * @deprecated throwUp no longer needs to be ContextAware * * @param Context $context */ public function setContext(Context $context) { // Do nothing } /** * {@inheritdoc} */ protected function configure() { $this ->setName('throw-up') ->setDefinition([ new CodeArgument('exception', CodeArgument::OPTIONAL, 'Exception or Error to throw.'), ]) ->setDescription('Throw an exception or error out of the Psy Shell.') ->setHelp( <<<'HELP' Throws an exception or error out of the current the Psy Shell instance. By default it throws the most recent exception. e.g. <return>>>> throw-up</return> <return>>>> throw-up $e</return> <return>>>> throw-up new Exception('WHEEEEEE!')</return> <return>>>> throw-up "bye!"</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code * * @throws \InvalidArgumentException if there is no exception to throw */ protected function execute(InputInterface $input, OutputInterface $output) { $args = $this->prepareArgs($input->getArgument('exception')); $throwStmt = new Throw_(new New_(new FullyQualifiedName(ThrowUpException::class), $args)); $throwCode = $this->printer->prettyPrint([$throwStmt]); $shell = $this->getApplication(); $shell->addCode($throwCode, !$shell->hasCode()); return 0; } /** * Parse the supplied command argument. * * If no argument was given, this falls back to `$_e` * * @throws \InvalidArgumentException if there is no exception to throw * * @param string $code * * @return Arg[] */ private function prepareArgs(string $code = null): array { if (!$code) { // Default to last exception if nothing else was supplied return [new Arg(new Variable('_e'))]; } $nodes = $this->parser->parse($code); if (\count($nodes) !== 1) { throw new \InvalidArgumentException('No idea how to throw this'); } $node = $nodes[0]; // Make this work for PHP Parser v3.x $expr = isset($node->expr) ? $node->expr : $node; $args = [new Arg($expr, false, false, $node->getAttributes())]; // Allow throwing via a string, e.g. `throw-up "SUP"` if ($expr instanceof String_) { return [new New_(new FullyQualifiedName(\Exception::class), $args)]; } return $args; } } EditCommand.php 0000644 00000013343 15111273343 0007445 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Context; use Psy\ContextAware; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class EditCommand extends Command implements ContextAware { /** * @var string */ private $runtimeDir = ''; /** * @var Context */ private $context; /** * Constructor. * * @param string $runtimeDir The directory to use for temporary files * @param string|null $name The name of the command; passing null means it must be set in configure() * * @throws \Symfony\Component\Console\Exception\LogicException When the command name is empty */ public function __construct($runtimeDir, $name = null) { parent::__construct($name); $this->runtimeDir = $runtimeDir; } protected function configure() { $this ->setName('edit') ->setDefinition([ new InputArgument('file', InputArgument::OPTIONAL, 'The file to open for editing. If this is not given, edits a temporary file.', null), new InputOption( 'exec', 'e', InputOption::VALUE_NONE, 'Execute the file content after editing. This is the default when a file name argument is not given.', null ), new InputOption( 'no-exec', 'E', InputOption::VALUE_NONE, 'Do not execute the file content after editing. This is the default when a file name argument is given.', null ), ]) ->setDescription('Open an external editor. Afterwards, get produced code in input buffer.') ->setHelp('Set the EDITOR environment variable to something you\'d like to use.'); } /** * @param InputInterface $input * @param OutputInterface $output * * @return int 0 if everything went fine, or an exit code * * @throws \InvalidArgumentException when both exec and no-exec flags are given or if a given variable is not found in the current context * @throws \UnexpectedValueException if file_get_contents on the edited file returns false instead of a string */ protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('exec') && $input->getOption('no-exec')) { throw new \InvalidArgumentException('The --exec and --no-exec flags are mutually exclusive'); } $filePath = $this->extractFilePath($input->getArgument('file')); $execute = $this->shouldExecuteFile( $input->getOption('exec'), $input->getOption('no-exec'), $filePath ); $shouldRemoveFile = false; if ($filePath === null) { $filePath = \tempnam($this->runtimeDir, 'psysh-edit-command'); $shouldRemoveFile = true; } $editedContent = $this->editFile($filePath, $shouldRemoveFile); if ($execute) { $this->getApplication()->addInput($editedContent); } return 0; } /** * @param bool $execOption * @param bool $noExecOption * @param string|null $filePath */ private function shouldExecuteFile(bool $execOption, bool $noExecOption, string $filePath = null): bool { if ($execOption) { return true; } if ($noExecOption) { return false; } // By default, code that is edited is executed if there was no given input file path return $filePath === null; } /** * @param string|null $fileArgument * * @return string|null The file path to edit, null if the input was null, or the value of the referenced variable * * @throws \InvalidArgumentException If the variable is not found in the current context */ private function extractFilePath(string $fileArgument = null) { // If the file argument was a variable, get it from the context if ($fileArgument !== null && $fileArgument !== '' && $fileArgument[0] === '$') { $fileArgument = $this->context->get(\preg_replace('/^\$/', '', $fileArgument)); } return $fileArgument; } /** * @param string $filePath * @param bool $shouldRemoveFile * * @throws \UnexpectedValueException if file_get_contents on $filePath returns false instead of a string */ private function editFile(string $filePath, bool $shouldRemoveFile): string { $escapedFilePath = \escapeshellarg($filePath); $editor = (isset($_SERVER['EDITOR']) && $_SERVER['EDITOR']) ? $_SERVER['EDITOR'] : 'nano'; $pipes = []; $proc = \proc_open("{$editor} {$escapedFilePath}", [\STDIN, \STDOUT, \STDERR], $pipes); \proc_close($proc); $editedContent = @\file_get_contents($filePath); if ($shouldRemoveFile) { @\unlink($filePath); } if ($editedContent === false) { throw new \UnexpectedValueException("Reading {$filePath} returned false"); } return $editedContent; } /** * Set the Context reference. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } } TimeitCommand/TimeitVisitor.php 0000644 00000010120 15111273343 0012614 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\TimeitCommand; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\FunctionLike; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Return_; use PhpParser\NodeVisitorAbstract; use Psy\CodeCleaner\NoReturnValue; use Psy\Command\TimeitCommand; /** * A node visitor for instrumenting code to be executed by the `timeit` command. * * Injects `TimeitCommand::markStart()` at the start of code to be executed, and * `TimeitCommand::markEnd()` at the end, and on top-level return statements. */ class TimeitVisitor extends NodeVisitorAbstract { private $functionDepth; /** * {@inheritdoc} * * @return Node[]|null Array of nodes */ public function beforeTraverse(array $nodes) { $this->functionDepth = 0; } /** * {@inheritdoc} * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { // keep track of nested function-like nodes, because they can have // returns statements... and we don't want to call markEnd for those. if ($node instanceof FunctionLike) { $this->functionDepth++; return; } // replace any top-level `return` statements with a `markEnd` call if ($this->functionDepth === 0 && $node instanceof Return_) { return new Return_($this->getEndCall($node->expr), $node->getAttributes()); } } /** * {@inheritdoc} * * @return int|Node|Node[]|null Replacement node (or special return value) */ public function leaveNode(Node $node) { if ($node instanceof FunctionLike) { $this->functionDepth--; } } /** * {@inheritdoc} * * @return Node[]|null Array of nodes */ public function afterTraverse(array $nodes) { // prepend a `markStart` call \array_unshift($nodes, $this->maybeExpression($this->getStartCall())); // append a `markEnd` call (wrapping the final node, if it's an expression) $last = $nodes[\count($nodes) - 1]; if ($last instanceof Expr) { \array_pop($nodes); $nodes[] = $this->getEndCall($last); } elseif ($last instanceof Expression) { \array_pop($nodes); $nodes[] = new Expression($this->getEndCall($last->expr), $last->getAttributes()); } elseif ($last instanceof Return_) { // nothing to do here, we're already ending with a return call } else { $nodes[] = $this->maybeExpression($this->getEndCall()); } return $nodes; } /** * Get PhpParser AST nodes for a `markStart` call. * * @return \PhpParser\Node\Expr\StaticCall */ private function getStartCall(): StaticCall { return new StaticCall(new FullyQualifiedName(TimeitCommand::class), 'markStart'); } /** * Get PhpParser AST nodes for a `markEnd` call. * * Optionally pass in a return value. * * @param Expr|null $arg */ private function getEndCall(Expr $arg = null): StaticCall { if ($arg === null) { $arg = NoReturnValue::create(); } return new StaticCall(new FullyQualifiedName(TimeitCommand::class), 'markEnd', [new Arg($arg)]); } /** * Compatibility shim for PHP Parser 3.x. * * Wrap $expr in a PhpParser\Node\Stmt\Expression if the class exists. * * @param \PhpParser\Node $expr * @param array $attrs * * @return \PhpParser\Node\Expr|\PhpParser\Node\Stmt\Expression */ private function maybeExpression(Node $expr, array $attrs = []) { return \class_exists(Expression::class) ? new Expression($expr, $attrs) : $expr; } } DumpCommand.php 0000644 00000005070 15111273343 0007463 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Input\CodeArgument; use Psy\VarDumper\Presenter; use Psy\VarDumper\PresenterAware; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Dump an object or primitive. * * This is like var_dump but *way* awesomer. */ class DumpCommand extends ReflectingCommand implements PresenterAware { private $presenter; /** * PresenterAware interface. * * @param Presenter $presenter */ public function setPresenter(Presenter $presenter) { $this->presenter = $presenter; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('dump') ->setDefinition([ new CodeArgument('target', CodeArgument::REQUIRED, 'A target object or primitive to dump.'), new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'), ]) ->setDescription('Dump an object or primitive.') ->setHelp( <<<'HELP' Dump an object or primitive. This is like var_dump but <strong>way</strong> awesomer. e.g. <return>>>> dump $_</return> <return>>>> dump $someVar</return> <return>>>> dump $stuff->getAll()</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $depth = $input->getOption('depth'); $target = $this->resolveCode($input->getArgument('target')); $output->page($this->presenter->present($target, $depth, $input->getOption('all') ? Presenter::VERBOSE : 0)); if (\is_object($target)) { $this->setCommandScopeVariables(new \ReflectionObject($target)); } return 0; } /** * @deprecated Use `resolveCode` instead * * @param string $name * * @return mixed */ protected function resolveTarget(string $name) { @\trigger_error('`resolveTarget` is deprecated; use `resolveCode` instead.', \E_USER_DEPRECATED); return $this->resolveCode($name); } } DocCommand.php 0000644 00000022041 15111273343 0007260 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Formatter\DocblockFormatter; use Psy\Formatter\SignatureFormatter; use Psy\Input\CodeArgument; use Psy\Output\ShellOutput; use Psy\Reflection\ReflectionClassConstant; use Psy\Reflection\ReflectionConstant_; use Psy\Reflection\ReflectionLanguageConstruct; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Read the documentation for an object, class, constant, method or property. */ class DocCommand extends ReflectingCommand { const INHERIT_DOC_TAG = '{@inheritdoc}'; /** * {@inheritdoc} */ protected function configure() { $this ->setName('doc') ->setAliases(['rtfm', 'man']) ->setDefinition([ new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show documentation for superclasses as well as the current class.'), new CodeArgument('target', CodeArgument::REQUIRED, 'Function, class, instance, constant, method or property to document.'), ]) ->setDescription('Read the documentation for an object, class, constant, method or property.') ->setHelp( <<<HELP Read the documentation for an object, class, constant, method or property. It's awesome for well-documented code, not quite as awesome for poorly documented code. e.g. <return>>>> doc preg_replace</return> <return>>>> doc Psy\Shell</return> <return>>>> doc Psy\Shell::debug</return> <return>>>> \$s = new Psy\Shell</return> <return>>>> doc \$s->run</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $value = $input->getArgument('target'); if (ReflectionLanguageConstruct::isLanguageConstruct($value)) { $reflector = new ReflectionLanguageConstruct($value); $doc = $this->getManualDocById($value); } else { list($target, $reflector) = $this->getTargetAndReflector($value); $doc = $this->getManualDoc($reflector) ?: DocblockFormatter::format($reflector); } $db = $this->getApplication()->getManualDb(); if ($output instanceof ShellOutput) { $output->startPaging(); } // Maybe include the declaring class if ($reflector instanceof \ReflectionMethod || $reflector instanceof \ReflectionProperty) { $output->writeln(SignatureFormatter::format($reflector->getDeclaringClass())); } $output->writeln(SignatureFormatter::format($reflector)); $output->writeln(''); if (empty($doc) && !$db) { $output->writeln('<warning>PHP manual not found</warning>'); $output->writeln(' To document core PHP functionality, download the PHP reference manual:'); $output->writeln(' https://github.com/bobthecow/psysh/wiki/PHP-manual'); } else { $output->writeln($doc); } // Implicit --all if the original docblock has an {@inheritdoc} tag. if ($input->getOption('all') || \stripos($doc, self::INHERIT_DOC_TAG) !== false) { $parent = $reflector; foreach ($this->getParentReflectors($reflector) as $parent) { $output->writeln(''); $output->writeln('---'); $output->writeln(''); // Maybe include the declaring class if ($parent instanceof \ReflectionMethod || $parent instanceof \ReflectionProperty) { $output->writeln(SignatureFormatter::format($parent->getDeclaringClass())); } $output->writeln(SignatureFormatter::format($parent)); $output->writeln(''); if ($doc = $this->getManualDoc($parent) ?: DocblockFormatter::format($parent)) { $output->writeln($doc); } } } if ($output instanceof ShellOutput) { $output->stopPaging(); } // Set some magic local variables $this->setCommandScopeVariables($reflector); return 0; } private function getManualDoc($reflector) { switch (\get_class($reflector)) { case \ReflectionClass::class: case \ReflectionObject::class: case \ReflectionFunction::class: $id = $reflector->name; break; case \ReflectionMethod::class: $id = $reflector->class.'::'.$reflector->name; break; case \ReflectionProperty::class: $id = $reflector->class.'::$'.$reflector->name; break; case \ReflectionClassConstant::class: case ReflectionClassConstant::class: // @todo this is going to collide with ReflectionMethod ids // someday... start running the query by id + type if the DB // supports it. $id = $reflector->class.'::'.$reflector->name; break; case ReflectionConstant_::class: $id = $reflector->name; break; default: return false; } return $this->getManualDocById($id); } /** * Get all all parent Reflectors for a given Reflector. * * For example, passing a Class, Object or TraitReflector will yield all * traits and parent classes. Passing a Method or PropertyReflector will * yield Reflectors for the same-named method or property on all traits and * parent classes. * * @return \Generator a whole bunch of \Reflector instances */ private function getParentReflectors($reflector): \Generator { $seenClasses = []; switch (\get_class($reflector)) { case \ReflectionClass::class: case \ReflectionObject::class: foreach ($reflector->getTraits() as $trait) { if (!\in_array($trait->getName(), $seenClasses)) { $seenClasses[] = $trait->getName(); yield $trait; } } foreach ($reflector->getInterfaces() as $interface) { if (!\in_array($interface->getName(), $seenClasses)) { $seenClasses[] = $interface->getName(); yield $interface; } } while ($reflector = $reflector->getParentClass()) { yield $reflector; foreach ($reflector->getTraits() as $trait) { if (!\in_array($trait->getName(), $seenClasses)) { $seenClasses[] = $trait->getName(); yield $trait; } } foreach ($reflector->getInterfaces() as $interface) { if (!\in_array($interface->getName(), $seenClasses)) { $seenClasses[] = $interface->getName(); yield $interface; } } } return; case \ReflectionMethod::class: foreach ($this->getParentReflectors($reflector->getDeclaringClass()) as $parent) { if ($parent->hasMethod($reflector->getName())) { $parentMethod = $parent->getMethod($reflector->getName()); if (!\in_array($parentMethod->getDeclaringClass()->getName(), $seenClasses)) { $seenClasses[] = $parentMethod->getDeclaringClass()->getName(); yield $parentMethod; } } } return; case \ReflectionProperty::class: foreach ($this->getParentReflectors($reflector->getDeclaringClass()) as $parent) { if ($parent->hasProperty($reflector->getName())) { $parentProperty = $parent->getProperty($reflector->getName()); if (!\in_array($parentProperty->getDeclaringClass()->getName(), $seenClasses)) { $seenClasses[] = $parentProperty->getDeclaringClass()->getName(); yield $parentProperty; } } } break; } } private function getManualDocById($id) { if ($db = $this->getApplication()->getManualDb()) { $result = $db->query(\sprintf('SELECT doc FROM php_manual WHERE id = %s', $db->quote($id))); if ($result !== false) { return $result->fetchColumn(0); } } } } TraceCommand.php 0000644 00000005210 15111273343 0007610 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Formatter\TraceFormatter; use Psy\Input\FilterOptions; use Psy\Output\ShellOutput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Show the current stack trace. */ class TraceCommand extends Command { protected $filter; /** * {@inheritdoc} */ public function __construct($name = null) { $this->filter = new FilterOptions(); parent::__construct($name); } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('trace') ->setDefinition([ new InputOption('include-psy', 'p', InputOption::VALUE_NONE, 'Include Psy in the call stack.'), new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Only include NUM lines.'), $grep, $insensitive, $invert, ]) ->setDescription('Show the current call stack.') ->setHelp( <<<'HELP' Show the current call stack. Optionally, include PsySH in the call stack by passing the <info>--include-psy</info> option. e.g. <return>> trace -n10</return> <return>> trace --include-psy</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->filter->bind($input); $trace = $this->getBacktrace(new \Exception(), $input->getOption('num'), $input->getOption('include-psy')); $output->page($trace, ShellOutput::NUMBER_LINES); return 0; } /** * Get a backtrace for an exception or error. * * Optionally limit the number of rows to include with $count, and exclude * Psy from the trace. * * @param \Throwable $e The exception or error with a backtrace * @param int $count (default: PHP_INT_MAX) * @param bool $includePsy (default: true) * * @return array Formatted stacktrace lines */ protected function getBacktrace(\Throwable $e, int $count = null, bool $includePsy = true): array { return TraceFormatter::formatTrace($e, $this->filter, $count, $includePsy); } } ListCommand/MethodEnumerator.php 0000644 00000007616 15111273343 0012763 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Symfony\Component\Console\Input\InputInterface; /** * Method Enumerator class. */ class MethodEnumerator extends Enumerator { /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // only list methods when a Reflector is present. if ($reflector === null) { return []; } // We can only list methods on actual class (or object) reflectors. if (!$reflector instanceof \ReflectionClass) { return []; } // only list methods if we are specifically asked if (!$input->getOption('methods')) { return []; } $showAll = $input->getOption('all'); $noInherit = $input->getOption('no-inherit'); $methods = $this->prepareMethods($this->getMethods($showAll, $reflector, $noInherit)); if (empty($methods)) { return []; } $ret = []; $ret[$this->getKindLabel($reflector)] = $methods; return $ret; } /** * Get defined methods for the given class or object Reflector. * * @param bool $showAll Include private and protected methods * @param \ReflectionClass $reflector * @param bool $noInherit Exclude inherited methods * * @return array */ protected function getMethods(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array { $className = $reflector->getName(); $methods = []; foreach ($reflector->getMethods() as $name => $method) { // For some reason PHP reflection shows private methods from the parent class, even // though they're effectively worthless. Let's suppress them here, like --no-inherit if (($noInherit || $method->isPrivate()) && $method->getDeclaringClass()->getName() !== $className) { continue; } if ($showAll || $method->isPublic()) { $methods[$method->getName()] = $method; } } \ksort($methods, \SORT_NATURAL | \SORT_FLAG_CASE); return $methods; } /** * Prepare formatted method array. * * @param array $methods * * @return array */ protected function prepareMethods(array $methods): array { // My kingdom for a generator. $ret = []; foreach ($methods as $name => $method) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => $this->getVisibilityStyle($method), 'value' => $this->presentSignature($method), ]; } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector */ protected function getKindLabel(\ReflectionClass $reflector): string { if ($reflector->isInterface()) { return 'Interface Methods'; } elseif (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) { return 'Trait Methods'; } else { return 'Class Methods'; } } /** * Get output style for the given method's visibility. * * @param \ReflectionMethod $method */ private function getVisibilityStyle(\ReflectionMethod $method): string { if ($method->isPublic()) { return self::IS_PUBLIC; } elseif ($method->isProtected()) { return self::IS_PROTECTED; } else { return self::IS_PRIVATE; } } } ListCommand/PropertyEnumerator.php 0000644 00000011536 15111273343 0013363 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Symfony\Component\Console\Input\InputInterface; /** * Property Enumerator class. */ class PropertyEnumerator extends Enumerator { /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // only list properties when a Reflector is present. if ($reflector === null) { return []; } // We can only list properties on actual class (or object) reflectors. if (!$reflector instanceof \ReflectionClass) { return []; } // only list properties if we are specifically asked if (!$input->getOption('properties')) { return []; } $showAll = $input->getOption('all'); $noInherit = $input->getOption('no-inherit'); $properties = $this->prepareProperties($this->getProperties($showAll, $reflector, $noInherit), $target); if (empty($properties)) { return []; } $ret = []; $ret[$this->getKindLabel($reflector)] = $properties; return $ret; } /** * Get defined properties for the given class or object Reflector. * * @param bool $showAll Include private and protected properties * @param \ReflectionClass $reflector * @param bool $noInherit Exclude inherited properties * * @return array */ protected function getProperties(bool $showAll, \ReflectionClass $reflector, bool $noInherit = false): array { $className = $reflector->getName(); $properties = []; foreach ($reflector->getProperties() as $property) { if ($noInherit && $property->getDeclaringClass()->getName() !== $className) { continue; } if ($showAll || $property->isPublic()) { $properties[$property->getName()] = $property; } } \ksort($properties, \SORT_NATURAL | \SORT_FLAG_CASE); return $properties; } /** * Prepare formatted property array. * * @param array $properties * * @return array */ protected function prepareProperties(array $properties, $target = null): array { // My kingdom for a generator. $ret = []; foreach ($properties as $name => $property) { if ($this->showItem($name)) { $fname = '$'.$name; $ret[$fname] = [ 'name' => $fname, 'style' => $this->getVisibilityStyle($property), 'value' => $this->presentValue($property, $target), ]; } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector */ protected function getKindLabel(\ReflectionClass $reflector): string { if (\method_exists($reflector, 'isTrait') && $reflector->isTrait()) { return 'Trait Properties'; } else { return 'Class Properties'; } } /** * Get output style for the given property's visibility. * * @param \ReflectionProperty $property */ private function getVisibilityStyle(\ReflectionProperty $property): string { if ($property->isPublic()) { return self::IS_PUBLIC; } elseif ($property->isProtected()) { return self::IS_PROTECTED; } else { return self::IS_PRIVATE; } } /** * Present the $target's current value for a reflection property. * * @param \ReflectionProperty $property * @param mixed $target */ protected function presentValue(\ReflectionProperty $property, $target): string { if (!$target) { return ''; } // If $target is a class or trait (try to) get the default // value for the property. if (!\is_object($target)) { try { $refl = new \ReflectionClass($target); $props = $refl->getDefaultProperties(); if (\array_key_exists($property->name, $props)) { $suffix = $property->isStatic() ? '' : ' <aside>(default)</aside>'; return $this->presentRef($props[$property->name]).$suffix; } } catch (\Throwable $e) { // Well, we gave it a shot. } return ''; } $property->setAccessible(true); $value = $property->getValue($target); return $this->presentRef($value); } } ListCommand/GlobalVariableEnumerator.php 0000644 00000003771 15111273343 0014407 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Symfony\Component\Console\Input\InputInterface; /** * Global Variable Enumerator class. */ class GlobalVariableEnumerator extends Enumerator { /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // only list globals when no Reflector is present. if ($reflector !== null || $target !== null) { return []; } // only list globals if we are specifically asked if (!$input->getOption('globals')) { return []; } $globals = $this->prepareGlobals($this->getGlobals()); if (empty($globals)) { return []; } return [ 'Global Variables' => $globals, ]; } /** * Get defined global variables. * * @return array */ protected function getGlobals(): array { global $GLOBALS; $names = \array_keys($GLOBALS); \natcasesort($names); $ret = []; foreach ($names as $name) { $ret[$name] = $GLOBALS[$name]; } return $ret; } /** * Prepare formatted global variable array. * * @param array $globals * * @return array */ protected function prepareGlobals(array $globals): array { // My kingdom for a generator. $ret = []; foreach ($globals as $name => $value) { if ($this->showItem($name)) { $fname = '$'.$name; $ret[$fname] = [ 'name' => $fname, 'style' => self::IS_GLOBAL, 'value' => $this->presentRef($value), ]; } } return $ret; } } ListCommand/ClassConstantEnumerator.php 0000644 00000006256 15111273343 0014321 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Psy\Reflection\ReflectionClassConstant; use Symfony\Component\Console\Input\InputInterface; /** * Class Constant Enumerator class. */ class ClassConstantEnumerator extends Enumerator { /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // only list constants when a Reflector is present. if ($reflector === null) { return []; } // We can only list constants on actual class (or object) reflectors. if (!$reflector instanceof \ReflectionClass) { // @todo handle ReflectionExtension as well return []; } // only list constants if we are specifically asked if (!$input->getOption('constants')) { return []; } $noInherit = $input->getOption('no-inherit'); $constants = $this->prepareConstants($this->getConstants($reflector, $noInherit)); if (empty($constants)) { return []; } $ret = []; $ret[$this->getKindLabel($reflector)] = $constants; return $ret; } /** * Get defined constants for the given class or object Reflector. * * @param \ReflectionClass $reflector * @param bool $noInherit Exclude inherited constants * * @return array */ protected function getConstants(\ReflectionClass $reflector, bool $noInherit = false): array { $className = $reflector->getName(); $constants = []; foreach ($reflector->getConstants() as $name => $constant) { $constReflector = ReflectionClassConstant::create($reflector->name, $name); if ($noInherit && $constReflector->getDeclaringClass()->getName() !== $className) { continue; } $constants[$name] = $constReflector; } \ksort($constants, \SORT_NATURAL | \SORT_FLAG_CASE); return $constants; } /** * Prepare formatted constant array. * * @param array $constants * * @return array */ protected function prepareConstants(array $constants): array { // My kingdom for a generator. $ret = []; foreach ($constants as $name => $constant) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => self::IS_CONSTANT, 'value' => $this->presentRef($constant->getValue()), ]; } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector */ protected function getKindLabel(\ReflectionClass $reflector): string { if ($reflector->isInterface()) { return 'Interface Constants'; } else { return 'Class Constants'; } } } ListCommand/FunctionEnumerator.php 0000644 00000006004 15111273343 0013316 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Psy\Reflection\ReflectionNamespace; use Symfony\Component\Console\Input\InputInterface; /** * Function Enumerator class. */ class FunctionEnumerator extends Enumerator { /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // if we have a reflector, ensure that it's a namespace reflector if (($target !== null || $reflector !== null) && !$reflector instanceof ReflectionNamespace) { return []; } // only list functions if we are specifically asked if (!$input->getOption('functions')) { return []; } if ($input->getOption('user')) { $label = 'User Functions'; $functions = $this->getFunctions('user'); } elseif ($input->getOption('internal')) { $label = 'Internal Functions'; $functions = $this->getFunctions('internal'); } else { $label = 'Functions'; $functions = $this->getFunctions(); } $prefix = $reflector === null ? null : \strtolower($reflector->getName()).'\\'; $functions = $this->prepareFunctions($functions, $prefix); if (empty($functions)) { return []; } $ret = []; $ret[$label] = $functions; return $ret; } /** * Get defined functions. * * Optionally limit functions to "user" or "internal" functions. * * @param string|null $type "user" or "internal" (default: both) * * @return array */ protected function getFunctions(string $type = null): array { $funcs = \get_defined_functions(); if ($type) { return $funcs[$type]; } else { return \array_merge($funcs['internal'], $funcs['user']); } } /** * Prepare formatted function array. * * @param array $functions * @param string $prefix * * @return array */ protected function prepareFunctions(array $functions, string $prefix = null): array { \natcasesort($functions); // My kingdom for a generator. $ret = []; foreach ($functions as $name) { if ($prefix !== null && \strpos(\strtolower($name), $prefix) !== 0) { continue; } if ($this->showItem($name)) { try { $ret[$name] = [ 'name' => $name, 'style' => self::IS_FUNCTION, 'value' => $this->presentSignature($name), ]; } catch (\Throwable $e) { // Ignore failures. } } } return $ret; } } ListCommand/VariableEnumerator.php 0000644 00000006707 15111273343 0013270 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Psy\Context; use Psy\VarDumper\Presenter; use Symfony\Component\Console\Input\InputInterface; /** * Variable Enumerator class. */ class VariableEnumerator extends Enumerator { // n.b. this array is the order in which special variables will be listed private static $specialNames = [ '_', '_e', '__out', '__function', '__method', '__class', '__namespace', '__file', '__line', '__dir', ]; private $context; /** * Variable Enumerator constructor. * * Unlike most other enumerators, the Variable Enumerator needs access to * the current scope variables, so we need to pass it a Context instance. * * @param Presenter $presenter * @param Context $context */ public function __construct(Presenter $presenter, Context $context) { $this->context = $context; parent::__construct($presenter); } /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // only list variables when no Reflector is present. if ($reflector !== null || $target !== null) { return []; } // only list variables if we are specifically asked if (!$input->getOption('vars')) { return []; } $showAll = $input->getOption('all'); $variables = $this->prepareVariables($this->getVariables($showAll)); if (empty($variables)) { return []; } return [ 'Variables' => $variables, ]; } /** * Get scope variables. * * @param bool $showAll Include special variables (e.g. $_) * * @return array */ protected function getVariables(bool $showAll): array { $scopeVars = $this->context->getAll(); \uksort($scopeVars, function ($a, $b) { $aIndex = \array_search($a, self::$specialNames); $bIndex = \array_search($b, self::$specialNames); if ($aIndex !== false) { if ($bIndex !== false) { return $aIndex - $bIndex; } return 1; } if ($bIndex !== false) { return -1; } return \strnatcasecmp($a, $b); }); $ret = []; foreach ($scopeVars as $name => $val) { if (!$showAll && \in_array($name, self::$specialNames)) { continue; } $ret[$name] = $val; } return $ret; } /** * Prepare formatted variable array. * * @param array $variables * * @return array */ protected function prepareVariables(array $variables): array { // My kingdom for a generator. $ret = []; foreach ($variables as $name => $val) { if ($this->showItem($name)) { $fname = '$'.$name; $ret[$fname] = [ 'name' => $fname, 'style' => \in_array($name, self::$specialNames) ? self::IS_PRIVATE : self::IS_PUBLIC, 'value' => $this->presentRef($val), ]; } } return $ret; } } ListCommand/ConstantEnumerator.php 0000644 00000011211 15111273343 0013316 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Psy\Reflection\ReflectionNamespace; use Symfony\Component\Console\Input\InputInterface; /** * Constant Enumerator class. */ class ConstantEnumerator extends Enumerator { // Because `Json` is ugly. private static $categoryLabels = [ 'libxml' => 'libxml', 'openssl' => 'OpenSSL', 'pcre' => 'PCRE', 'sqlite3' => 'SQLite3', 'curl' => 'cURL', 'dom' => 'DOM', 'ftp' => 'FTP', 'gd' => 'GD', 'gmp' => 'GMP', 'iconv' => 'iconv', 'json' => 'JSON', 'ldap' => 'LDAP', 'mbstring' => 'mbstring', 'odbc' => 'ODBC', 'pcntl' => 'PCNTL', 'pgsql' => 'pgsql', 'posix' => 'POSIX', 'mysqli' => 'mysqli', 'soap' => 'SOAP', 'exif' => 'EXIF', 'sysvmsg' => 'sysvmsg', 'xml' => 'XML', 'xsl' => 'XSL', ]; /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // if we have a reflector, ensure that it's a namespace reflector if (($target !== null || $reflector !== null) && !$reflector instanceof ReflectionNamespace) { return []; } // only list constants if we are specifically asked if (!$input->getOption('constants')) { return []; } $user = $input->getOption('user'); $internal = $input->getOption('internal'); $category = $input->getOption('category'); if ($category) { $category = \strtolower($category); if ($category === 'internal') { $internal = true; $category = null; } elseif ($category === 'user') { $user = true; $category = null; } } $ret = []; if ($user) { $ret['User Constants'] = $this->getConstants('user'); } if ($internal) { $ret['Internal Constants'] = $this->getConstants('internal'); } if ($category) { $caseCategory = \array_key_exists($category, self::$categoryLabels) ? self::$categoryLabels[$category] : \ucfirst($category); $label = $caseCategory.' Constants'; $ret[$label] = $this->getConstants($category); } if (!$user && !$internal && !$category) { $ret['Constants'] = $this->getConstants(); } if ($reflector !== null) { $prefix = \strtolower($reflector->getName()).'\\'; foreach ($ret as $key => $names) { foreach (\array_keys($names) as $name) { if (\strpos(\strtolower($name), $prefix) !== 0) { unset($ret[$key][$name]); } } } } return \array_map([$this, 'prepareConstants'], \array_filter($ret)); } /** * Get defined constants. * * Optionally restrict constants to a given category, e.g. "date". If the * category is "internal", include all non-user-defined constants. * * @param string $category * * @return array */ protected function getConstants(string $category = null): array { if (!$category) { return \get_defined_constants(); } $consts = \get_defined_constants(true); if ($category === 'internal') { unset($consts['user']); return \array_merge(...\array_values($consts)); } foreach ($consts as $key => $value) { if (\strtolower($key) === $category) { return $value; } } return []; } /** * Prepare formatted constant array. * * @param array $constants * * @return array */ protected function prepareConstants(array $constants): array { // My kingdom for a generator. $ret = []; $names = \array_keys($constants); \natcasesort($names); foreach ($names as $name) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => self::IS_CONSTANT, 'value' => $this->presentRef($constants[$name]), ]; } } return $ret; } } ListCommand/Enumerator.php 0000644 00000005205 15111273344 0011613 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Psy\Formatter\SignatureFormatter; use Psy\Input\FilterOptions; use Psy\Util\Mirror; use Psy\VarDumper\Presenter; use Symfony\Component\Console\Input\InputInterface; /** * Abstract Enumerator class. */ abstract class Enumerator { // Output styles const IS_PUBLIC = 'public'; const IS_PROTECTED = 'protected'; const IS_PRIVATE = 'private'; const IS_GLOBAL = 'global'; const IS_CONSTANT = 'const'; const IS_CLASS = 'class'; const IS_FUNCTION = 'function'; private $filter; private $presenter; /** * Enumerator constructor. * * @param Presenter $presenter */ public function __construct(Presenter $presenter) { $this->filter = new FilterOptions(); $this->presenter = $presenter; } /** * Return a list of categorized things with the given input options and target. * * @param InputInterface $input * @param \Reflector|null $reflector * @param mixed $target * * @return array */ public function enumerate(InputInterface $input, \Reflector $reflector = null, $target = null): array { $this->filter->bind($input); return $this->listItems($input, $reflector, $target); } /** * Enumerate specific items with the given input options and target. * * Implementing classes should return an array of arrays: * * [ * 'Constants' => [ * 'FOO' => [ * 'name' => 'FOO', * 'style' => 'public', * 'value' => '123', * ], * ], * ] * * @param InputInterface $input * @param \Reflector|null $reflector * @param mixed $target * * @return array */ abstract protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array; protected function showItem($name) { return $this->filter->match($name); } protected function presentRef($value) { return $this->presenter->presentRef($value); } protected function presentSignature($target) { // This might get weird if the signature is actually for a reflector. Hrm. if (!$target instanceof \Reflector) { $target = Mirror::get($target); } return SignatureFormatter::format($target); } } ListCommand/ClassEnumerator.php 0000644 00000007426 15111273344 0012610 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command\ListCommand; use Psy\Reflection\ReflectionNamespace; use Symfony\Component\Console\Input\InputInterface; /** * Class Enumerator class. */ class ClassEnumerator extends Enumerator { /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null): array { // if we have a reflector, ensure that it's a namespace reflector if (($target !== null || $reflector !== null) && !$reflector instanceof ReflectionNamespace) { return []; } $internal = $input->getOption('internal'); $user = $input->getOption('user'); $prefix = $reflector === null ? null : \strtolower($reflector->getName()).'\\'; $ret = []; // only list classes, interfaces and traits if we are specifically asked if ($input->getOption('classes')) { $ret = \array_merge($ret, $this->filterClasses('Classes', \get_declared_classes(), $internal, $user, $prefix)); } if ($input->getOption('interfaces')) { $ret = \array_merge($ret, $this->filterClasses('Interfaces', \get_declared_interfaces(), $internal, $user, $prefix)); } if ($input->getOption('traits')) { $ret = \array_merge($ret, $this->filterClasses('Traits', \get_declared_traits(), $internal, $user, $prefix)); } return \array_map([$this, 'prepareClasses'], \array_filter($ret)); } /** * Filter a list of classes, interfaces or traits. * * If $internal or $user is defined, results will be limited to internal or * user-defined classes as appropriate. * * @param string $key * @param array $classes * @param bool $internal * @param bool $user * @param string $prefix * * @return array */ protected function filterClasses(string $key, array $classes, bool $internal, bool $user, string $prefix = null): array { $ret = []; if ($internal) { $ret['Internal '.$key] = \array_filter($classes, function ($class) use ($prefix) { if ($prefix !== null && \strpos(\strtolower($class), $prefix) !== 0) { return false; } $refl = new \ReflectionClass($class); return $refl->isInternal(); }); } if ($user) { $ret['User '.$key] = \array_filter($classes, function ($class) use ($prefix) { if ($prefix !== null && \strpos(\strtolower($class), $prefix) !== 0) { return false; } $refl = new \ReflectionClass($class); return !$refl->isInternal(); }); } if (!$user && !$internal) { $ret[$key] = \array_filter($classes, function ($class) use ($prefix) { return $prefix === null || \strpos(\strtolower($class), $prefix) === 0; }); } return $ret; } /** * Prepare formatted class array. * * @param array $classes * * @return array */ protected function prepareClasses(array $classes): array { \natcasesort($classes); // My kingdom for a generator. $ret = []; foreach ($classes as $name) { if ($this->showItem($name)) { $ret[$name] = [ 'name' => $name, 'style' => self::IS_CLASS, 'value' => $this->presentSignature($name), ]; } } return $ret; } } CodeArgumentParser.php 0000644 00000002742 15111273344 0011015 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use PhpParser\Parser; use Psy\Exception\ParseErrorException; use Psy\ParserFactory; /** * Class CodeArgumentParser. */ class CodeArgumentParser { private $parser; public function __construct(Parser $parser = null) { $this->parser = $parser ?? (new ParserFactory())->createParser(); } /** * Lex and parse a string of code into statements. * * This is intended for code arguments, so the code string *should not* start with <?php * * @throws ParseErrorException * * @return array Statements */ public function parse(string $code): array { $code = '<?php '.$code; try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw ParseErrorException::fromParseError($e); } // If we got an unexpected EOF, let's try it again with a semicolon. try { return $this->parser->parse($code.';'); } catch (\PhpParser\Error $_e) { // Throw the original error, not the semicolon one. throw ParseErrorException::fromParseError($e); } } } } ExitCommand.php 0000644 00000002201 15111273344 0007461 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Exception\BreakException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Exit the Psy Shell. * * Just what it says on the tin. */ class ExitCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('exit') ->setAliases(['quit', 'q']) ->setDefinition([]) ->setDescription('End the current session and return to caller.') ->setHelp( <<<'HELP' End the current session and return to caller. e.g. <return>>>> exit</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { throw new BreakException('Goodbye'); } } Command.php 0000644 00000054664 15111273344 0006653 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Helper\HelperInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Base class for all commands. * * @author Fabien Potencier <fabien@symfony.com> */ class Command { // see https://tldp.org/LDP/abs/html/exitcodes.html public const SUCCESS = 0; public const FAILURE = 1; public const INVALID = 2; /** * @var string|null The default command name * * @deprecated since Symfony 6.1, use the AsCommand attribute instead */ protected static $defaultName; /** * @var string|null The default command description * * @deprecated since Symfony 6.1, use the AsCommand attribute instead */ protected static $defaultDescription; private ?Application $application = null; private ?string $name = null; private ?string $processTitle = null; private array $aliases = []; private InputDefinition $definition; private bool $hidden = false; private string $help = ''; private string $description = ''; private ?InputDefinition $fullDefinition = null; private bool $ignoreValidationErrors = false; private ?\Closure $code = null; private array $synopsis = []; private array $usages = []; private ?HelperSet $helperSet = null; public static function getDefaultName(): ?string { $class = static::class; if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->name; } $r = new \ReflectionProperty($class, 'defaultName'); if ($class !== $r->class || null === static::$defaultName) { return null; } trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); return static::$defaultName; } public static function getDefaultDescription(): ?string { $class = static::class; if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->description; } $r = new \ReflectionProperty($class, 'defaultDescription'); if ($class !== $r->class || null === static::$defaultDescription) { return null; } trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); return static::$defaultDescription; } /** * @param string|null $name The name of the command; passing null means it must be set in configure() * * @throws LogicException When the command name is empty */ public function __construct(string $name = null) { $this->definition = new InputDefinition(); if (null === $name && null !== $name = static::getDefaultName()) { $aliases = explode('|', $name); if ('' === $name = array_shift($aliases)) { $this->setHidden(true); $name = array_shift($aliases); } $this->setAliases($aliases); } if (null !== $name) { $this->setName($name); } if ('' === $this->description) { $this->setDescription(static::getDefaultDescription() ?? ''); } $this->configure(); } /** * Ignores validation errors. * * This is mainly useful for the help command. * * @return void */ public function ignoreValidationErrors() { $this->ignoreValidationErrors = true; } /** * @return void */ public function setApplication(Application $application = null) { if (1 > \func_num_args()) { trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); } $this->application = $application; if ($application) { $this->setHelperSet($application->getHelperSet()); } else { $this->helperSet = null; } $this->fullDefinition = null; } /** * @return void */ public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; } /** * Gets the helper set. */ public function getHelperSet(): ?HelperSet { return $this->helperSet; } /** * Gets the application instance for this command. */ public function getApplication(): ?Application { return $this->application; } /** * Checks whether the command is enabled or not in the current environment. * * Override this to check for x or y and return false if the command cannot * run properly under the current conditions. * * @return bool */ public function isEnabled() { return true; } /** * Configures the current command. * * @return void */ protected function configure() { } /** * Executes the current command. * * This method is not abstract because you can use this class * as a concrete class. In this case, instead of defining the * execute() method, you set the code to execute by passing * a Closure to the setCode() method. * * @return int 0 if everything went fine, or an exit code * * @throws LogicException When this abstract method is not implemented * * @see setCode() */ protected function execute(InputInterface $input, OutputInterface $output) { throw new LogicException('You must override the execute() method in the concrete command class.'); } /** * Interacts with the user. * * This method is executed before the InputDefinition is validated. * This means that this is the only place where the command can * interactively ask for values of missing required arguments. * * @return void */ protected function interact(InputInterface $input, OutputInterface $output) { } /** * Initializes the command after the input has been bound and before the input * is validated. * * This is mainly useful when a lot of commands extends one main command * where some things need to be initialized based on the input arguments and options. * * @see InputInterface::bind() * @see InputInterface::validate() * * @return void */ protected function initialize(InputInterface $input, OutputInterface $output) { } /** * Runs the command. * * The code to execute is either defined directly with the * setCode() method or by overriding the execute() method * in a sub-class. * * @return int The command exit code * * @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}. * * @see setCode() * @see execute() */ public function run(InputInterface $input, OutputInterface $output): int { // add the application arguments and options $this->mergeApplicationDefinition(); // bind the input against the command specific arguments/options try { $input->bind($this->getDefinition()); } catch (ExceptionInterface $e) { if (!$this->ignoreValidationErrors) { throw $e; } } $this->initialize($input, $output); if (null !== $this->processTitle) { if (\function_exists('cli_set_process_title')) { if (!@cli_set_process_title($this->processTitle)) { if ('Darwin' === \PHP_OS) { $output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE); } else { cli_set_process_title($this->processTitle); } } } elseif (\function_exists('setproctitle')) { setproctitle($this->processTitle); } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) { $output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>'); } } if ($input->isInteractive()) { $this->interact($input, $output); } // The command name argument is often omitted when a command is executed directly with its run() method. // It would fail the validation if we didn't make sure the command argument is present, // since it's required by the application. if ($input->hasArgument('command') && null === $input->getArgument('command')) { $input->setArgument('command', $this->getName()); } $input->validate(); if ($this->code) { $statusCode = ($this->code)($input, $output); } else { $statusCode = $this->execute($input, $output); if (!\is_int($statusCode)) { throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode))); } } return is_numeric($statusCode) ? (int) $statusCode : 0; } /** * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $definition = $this->getDefinition(); if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) { $definition->getOption($input->getCompletionName())->complete($input, $suggestions); } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) { $definition->getArgument($input->getCompletionName())->complete($input, $suggestions); } } /** * Sets the code to execute when running this command. * * If this method is used, it overrides the code defined * in the execute() method. * * @param callable $code A callable(InputInterface $input, OutputInterface $output) * * @return $this * * @throws InvalidArgumentException * * @see execute() */ public function setCode(callable $code): static { if ($code instanceof \Closure) { $r = new \ReflectionFunction($code); if (null === $r->getClosureThis()) { set_error_handler(static function () {}); try { if ($c = \Closure::bind($code, $this)) { $code = $c; } } finally { restore_error_handler(); } } } else { $code = $code(...); } $this->code = $code; return $this; } /** * Merges the application definition with the command definition. * * This method is not part of public API and should not be used directly. * * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments * * @internal */ public function mergeApplicationDefinition(bool $mergeArgs = true): void { if (null === $this->application) { return; } $this->fullDefinition = new InputDefinition(); $this->fullDefinition->setOptions($this->definition->getOptions()); $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions()); if ($mergeArgs) { $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments()); $this->fullDefinition->addArguments($this->definition->getArguments()); } else { $this->fullDefinition->setArguments($this->definition->getArguments()); } } /** * Sets an array of argument and option instances. * * @return $this */ public function setDefinition(array|InputDefinition $definition): static { if ($definition instanceof InputDefinition) { $this->definition = $definition; } else { $this->definition->setDefinition($definition); } $this->fullDefinition = null; return $this; } /** * Gets the InputDefinition attached to this Command. */ public function getDefinition(): InputDefinition { return $this->fullDefinition ?? $this->getNativeDefinition(); } /** * Gets the InputDefinition to be used to create representations of this Command. * * Can be overridden to provide the original command representation when it would otherwise * be changed by merging with the application InputDefinition. * * This method is not part of public API and should not be used directly. */ public function getNativeDefinition(): InputDefinition { return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); } /** * Adds an argument. * * @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL * @param $default The default value (for InputArgument::OPTIONAL mode only) * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion * * @return $this * * @throws InvalidArgumentException When argument mode is not valid */ public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static { $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); } $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); return $this; } /** * Adds an option. * * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param $mode The option mode: One of the InputOption::VALUE_* constants * @param $default The default value (must be null for InputOption::VALUE_NONE) * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion * * @return $this * * @throws InvalidArgumentException If option mode is invalid or incompatible */ public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static { $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); } $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); return $this; } /** * Sets the name of the command. * * This method can set both the namespace and the name if * you separate them by a colon (:) * * $command->setName('foo:bar'); * * @return $this * * @throws InvalidArgumentException When the name is invalid */ public function setName(string $name): static { $this->validateName($name); $this->name = $name; return $this; } /** * Sets the process title of the command. * * This feature should be used only when creating a long process command, * like a daemon. * * @return $this */ public function setProcessTitle(string $title): static { $this->processTitle = $title; return $this; } /** * Returns the command name. */ public function getName(): ?string { return $this->name; } /** * @param bool $hidden Whether or not the command should be hidden from the list of commands * * @return $this */ public function setHidden(bool $hidden = true): static { $this->hidden = $hidden; return $this; } /** * @return bool whether the command should be publicly shown or not */ public function isHidden(): bool { return $this->hidden; } /** * Sets the description for the command. * * @return $this */ public function setDescription(string $description): static { $this->description = $description; return $this; } /** * Returns the description for the command. */ public function getDescription(): string { return $this->description; } /** * Sets the help for the command. * * @return $this */ public function setHelp(string $help): static { $this->help = $help; return $this; } /** * Returns the help for the command. */ public function getHelp(): string { return $this->help; } /** * Returns the processed help for the command replacing the %command.name% and * %command.full_name% patterns with the real values dynamically. */ public function getProcessedHelp(): string { $name = $this->name; $isSingleCommand = $this->application?->isSingleCommand(); $placeholders = [ '%command.name%', '%command.full_name%', ]; $replacements = [ $name, $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name, ]; return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription()); } /** * Sets the aliases for the command. * * @param string[] $aliases An array of aliases for the command * * @return $this * * @throws InvalidArgumentException When an alias is invalid */ public function setAliases(iterable $aliases): static { $list = []; foreach ($aliases as $alias) { $this->validateName($alias); $list[] = $alias; } $this->aliases = \is_array($aliases) ? $aliases : $list; return $this; } /** * Returns the aliases for the command. */ public function getAliases(): array { return $this->aliases; } /** * Returns the synopsis for the command. * * @param bool $short Whether to show the short version of the synopsis (with options folded) or not */ public function getSynopsis(bool $short = false): string { $key = $short ? 'short' : 'long'; if (!isset($this->synopsis[$key])) { $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); } return $this->synopsis[$key]; } /** * Add a command usage example, it'll be prefixed with the command name. * * @return $this */ public function addUsage(string $usage): static { if (!str_starts_with($usage, $this->name)) { $usage = sprintf('%s %s', $this->name, $usage); } $this->usages[] = $usage; return $this; } /** * Returns alternative usages of the command. */ public function getUsages(): array { return $this->usages; } /** * Gets a helper instance by name. * * @return HelperInterface * * @throws LogicException if no HelperSet is defined * @throws InvalidArgumentException if the helper is not defined */ public function getHelper(string $name): mixed { if (null === $this->helperSet) { 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)); } return $this->helperSet->get($name); } /** * Validates a command name. * * It must be non-empty and parts can optionally be separated by ":". * * @throws InvalidArgumentException When the name is invalid */ private function validateName(string $name): void { if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); } } } SudoCommand.php 0000644 00000006067 15111273344 0007500 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use PhpParser\NodeTraverser; use PhpParser\PrettyPrinter\Standard as Printer; use Psy\Input\CodeArgument; use Psy\Readline\Readline; use Psy\Sudo\SudoVisitor; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Evaluate PHP code, bypassing visibility restrictions. */ class SudoCommand extends Command { private $readline; private $parser; private $traverser; private $printer; /** * {@inheritdoc} */ public function __construct($name = null) { $this->parser = new CodeArgumentParser(); $this->traverser = new NodeTraverser(); $this->traverser->addVisitor(new SudoVisitor()); $this->printer = new Printer(); parent::__construct($name); } /** * Set the Shell's Readline service. * * @param Readline $readline */ public function setReadline(Readline $readline) { $this->readline = $readline; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('sudo') ->setDefinition([ new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'), ]) ->setDescription('Evaluate PHP code, bypassing visibility restrictions.') ->setHelp( <<<'HELP' Evaluate PHP code, bypassing visibility restrictions. e.g. <return>>>> $sekret->whisper("hi")</return> <return>PHP error: Call to private method Sekret::whisper() from context '' on line 1</return> <return>>>> sudo $sekret->whisper("hi")</return> <return>=> "hi"</return> <return>>>> $sekret->word</return> <return>PHP error: Cannot access private property Sekret::$word on line 1</return> <return>>>> sudo $sekret->word</return> <return>=> "hi"</return> <return>>>> $sekret->word = "please"</return> <return>PHP error: Cannot access private property Sekret::$word on line 1</return> <return>>>> sudo $sekret->word = "please"</return> <return>=> "please"</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $code = $input->getArgument('code'); // special case for !! if ($code === '!!') { $history = $this->readline->listHistory(); if (\count($history) < 2) { throw new \InvalidArgumentException('No previous command to replay'); } $code = $history[\count($history) - 2]; } $nodes = $this->traverser->traverse($this->parser->parse($code)); $sudoCode = $this->printer->prettyPrint($nodes); $shell = $this->getApplication(); $shell->addCode($sudoCode, !$shell->hasCode()); return 0; } } HistoryCommand.php 0000644 00000017011 15111273344 0010216 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Input\FilterOptions; use Psy\Output\ShellOutput; use Psy\Readline\Readline; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Psy Shell history command. * * Shows, searches and replays readline history. Not too shabby. */ class HistoryCommand extends Command { private $filter; private $readline; /** * {@inheritdoc} */ public function __construct($name = null) { $this->filter = new FilterOptions(); parent::__construct($name); } /** * Set the Shell's Readline service. * * @param Readline $readline */ public function setReadline(Readline $readline) { $this->readline = $readline; } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('history') ->setAliases(['hist']) ->setDefinition([ new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines.'), new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'), new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'), $grep, $insensitive, $invert, new InputOption('no-numbers', 'N', InputOption::VALUE_NONE, 'Omit line numbers.'), new InputOption('save', '', InputOption::VALUE_REQUIRED, 'Save history to a file.'), new InputOption('replay', '', InputOption::VALUE_NONE, 'Replay.'), new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the history.'), ]) ->setDescription('Show the Psy Shell history.') ->setHelp( <<<'HELP' Show, search, save or replay the Psy Shell history. e.g. <return>>>> history --grep /[bB]acon/</return> <return>>>> history --show 0..10 --replay</return> <return>>>> history --clear</return> <return>>>> history --tail 1000 --save somefile.txt</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->validateOnlyOne($input, ['show', 'head', 'tail']); $this->validateOnlyOne($input, ['save', 'replay', 'clear']); $history = $this->getHistorySlice( $input->getOption('show'), $input->getOption('head'), $input->getOption('tail') ); $highlighted = false; $this->filter->bind($input); if ($this->filter->hasFilter()) { $matches = []; $highlighted = []; foreach ($history as $i => $line) { if ($this->filter->match($line, $matches)) { if (isset($matches[0])) { $chunks = \explode($matches[0], $history[$i]); $chunks = \array_map([__CLASS__, 'escape'], $chunks); $glue = \sprintf('<urgent>%s</urgent>', self::escape($matches[0])); $highlighted[$i] = \implode($glue, $chunks); } } else { unset($history[$i]); } } } if ($save = $input->getOption('save')) { $output->writeln(\sprintf('Saving history in %s...', $save)); \file_put_contents($save, \implode(\PHP_EOL, $history).\PHP_EOL); $output->writeln('<info>History saved.</info>'); } elseif ($input->getOption('replay')) { if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) { throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying'); } $count = \count($history); $output->writeln(\sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : '')); $this->getApplication()->addInput($history); } elseif ($input->getOption('clear')) { $this->clearHistory(); $output->writeln('<info>History cleared.</info>'); } else { $type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES; if (!$highlighted) { $type = $type | OutputInterface::OUTPUT_RAW; } $output->page($highlighted ?: $history, $type); } return 0; } /** * Extract a range from a string. * * @param string $range * * @return array [ start, end ] */ private function extractRange(string $range): array { if (\preg_match('/^\d+$/', $range)) { return [$range, $range + 1]; } $matches = []; if ($range !== '..' && \preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) { $start = $matches[1] ? (int) $matches[1] : 0; $end = $matches[2] ? (int) $matches[2] + 1 : \PHP_INT_MAX; return [$start, $end]; } throw new \InvalidArgumentException('Unexpected range: '.$range); } /** * Retrieve a slice of the readline history. * * @param string|null $show * @param string|null $head * @param string|null $tail * * @return array A slice of history */ private function getHistorySlice($show, $head, $tail): array { $history = $this->readline->listHistory(); // don't show the current `history` invocation \array_pop($history); if ($show) { list($start, $end) = $this->extractRange($show); $length = $end - $start; } elseif ($head) { if (!\preg_match('/^\d+$/', $head)) { throw new \InvalidArgumentException('Please specify an integer argument for --head'); } $start = 0; $length = (int) $head; } elseif ($tail) { if (!\preg_match('/^\d+$/', $tail)) { throw new \InvalidArgumentException('Please specify an integer argument for --tail'); } $start = \count($history) - $tail; $length = (int) $tail + 1; } else { return $history; } return \array_slice($history, $start, $length, true); } /** * Validate that only one of the given $options is set. * * @param InputInterface $input * @param array $options */ private function validateOnlyOne(InputInterface $input, array $options) { $count = 0; foreach ($options as $opt) { if ($input->getOption($opt)) { $count++; } } if ($count > 1) { throw new \InvalidArgumentException('Please specify only one of --'.\implode(', --', $options)); } } /** * Clear the readline history. */ private function clearHistory() { $this->readline->clearHistory(); } public static function escape(string $string): string { return OutputFormatter::escape($string); } } ShowCommand.php 0000644 00000023221 15111273344 0007475 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Exception\RuntimeException; use Psy\Exception\UnexpectedTargetException; use Psy\Formatter\CodeFormatter; use Psy\Formatter\SignatureFormatter; use Psy\Input\CodeArgument; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Show the code for an object, class, constant, method or property. */ class ShowCommand extends ReflectingCommand { private $lastException; private $lastExceptionIndex; /** * @param string|null $colorMode (deprecated and ignored) */ public function __construct($colorMode = null) { parent::__construct(); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('show') ->setDefinition([ new CodeArgument('target', CodeArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'), new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1), ]) ->setDescription('Show the code for an object, class, constant, method or property.') ->setHelp( <<<HELP Show the code for an object, class, constant, method or property, or the context of the last exception. <return>cat --ex</return> defaults to showing the lines surrounding the location of the last exception. Invoking it more than once travels up the exception's stack trace, and providing a number shows the context of the given index of the trace. e.g. <return>>>> show \$myObject</return> <return>>>> show Psy\Shell::debug</return> <return>>>> show --ex</return> <return>>>> show --ex 3</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { // n.b. As far as I can tell, InputInterface doesn't want to tell me // whether an option with an optional value was actually passed. If you // call `$input->getOption('ex')`, it will return the default, both when // `--ex` is specified with no value, and when `--ex` isn't specified at // all. // // So we're doing something sneaky here. If we call `getOptions`, it'll // return the default value when `--ex` is not present, and `null` if // `--ex` is passed with no value. /shrug $opts = $input->getOptions(); // Strict comparison to `1` (the default value) here, because `--ex 1` // will come in as `"1"`. Now we can tell the difference between // "no --ex present", because it's the integer 1, "--ex with no value", // because it's `null`, and "--ex 1", because it's the string "1". if ($opts['ex'] !== 1) { if ($input->getArgument('target')) { throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")'); } $this->writeExceptionContext($input, $output); return 0; } if ($input->getArgument('target')) { $this->writeCodeContext($input, $output); return 0; } throw new RuntimeException('Not enough arguments (missing: "target")'); } private function writeCodeContext(InputInterface $input, OutputInterface $output) { try { list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target')); } catch (UnexpectedTargetException $e) { // If we didn't get a target and Reflector, maybe we got a filename? $target = $e->getTarget(); if (\is_string($target) && \is_file($target) && $code = @\file_get_contents($target)) { $file = \realpath($target); if ($file !== $this->context->get('__file')) { $this->context->setCommandScopeVariables([ '__file' => $file, '__dir' => \dirname($file), ]); } $output->page(CodeFormatter::formatCode($code)); return; } else { throw $e; } } // Set some magic local variables $this->setCommandScopeVariables($reflector); try { $output->page(CodeFormatter::format($reflector)); } catch (RuntimeException $e) { $output->writeln(SignatureFormatter::format($reflector)); throw $e; } } private function writeExceptionContext(InputInterface $input, OutputInterface $output) { $exception = $this->context->getLastException(); if ($exception !== $this->lastException) { $this->lastException = null; $this->lastExceptionIndex = null; } $opts = $input->getOptions(); if ($opts['ex'] === null) { if ($this->lastException && $this->lastExceptionIndex !== null) { $index = $this->lastExceptionIndex + 1; } else { $index = 0; } } else { $index = \max(0, (int) $input->getOption('ex') - 1); } $trace = $exception->getTrace(); \array_unshift($trace, [ 'file' => $exception->getFile(), 'line' => $exception->getLine(), ]); if ($index >= \count($trace)) { $index = 0; } $this->lastException = $exception; $this->lastExceptionIndex = $index; $output->writeln($this->getApplication()->formatException($exception)); $output->writeln('--'); $this->writeTraceLine($output, $trace, $index); $this->writeTraceCodeSnippet($output, $trace, $index); $this->setCommandScopeVariablesFromContext($trace[$index]); } private function writeTraceLine(OutputInterface $output, array $trace, $index) { $file = isset($trace[$index]['file']) ? $this->replaceCwd($trace[$index]['file']) : 'n/a'; $line = isset($trace[$index]['line']) ? $trace[$index]['line'] : 'n/a'; $output->writeln(\sprintf( 'From <info>%s:%d</info> at <strong>level %d</strong> of backtrace (of %d):', OutputFormatter::escape($file), OutputFormatter::escape($line), $index + 1, \count($trace) )); } private function replaceCwd(string $file): string { if ($cwd = \getcwd()) { $cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; } if ($cwd === false) { return $file; } else { return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file); } } private function writeTraceCodeSnippet(OutputInterface $output, array $trace, $index) { if (!isset($trace[$index]['file'])) { return; } $file = $trace[$index]['file']; if ($fileAndLine = $this->extractEvalFileAndLine($file)) { list($file, $line) = $fileAndLine; } else { if (!isset($trace[$index]['line'])) { return; } $line = $trace[$index]['line']; } if (\is_file($file)) { $code = @\file_get_contents($file); } if (empty($code)) { return; } $startLine = \max($line - 5, 0); $endLine = $line + 5; $output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $line), false); } private function setCommandScopeVariablesFromContext(array $context) { $vars = []; if (isset($context['class'])) { $vars['__class'] = $context['class']; if (isset($context['function'])) { $vars['__method'] = $context['function']; } try { $refl = new \ReflectionClass($context['class']); if ($namespace = $refl->getNamespaceName()) { $vars['__namespace'] = $namespace; } } catch (\Throwable $e) { // oh well } } elseif (isset($context['function'])) { $vars['__function'] = $context['function']; try { $refl = new \ReflectionFunction($context['function']); if ($namespace = $refl->getNamespaceName()) { $vars['__namespace'] = $namespace; } } catch (\Throwable $e) { // oh well } } if (isset($context['file'])) { $file = $context['file']; if ($fileAndLine = $this->extractEvalFileAndLine($file)) { list($file, $line) = $fileAndLine; } elseif (isset($context['line'])) { $line = $context['line']; } if (\is_file($file)) { $vars['__file'] = $file; if (isset($line)) { $vars['__line'] = $line; } $vars['__dir'] = \dirname($file); } } $this->context->setCommandScopeVariables($vars); } private function extractEvalFileAndLine(string $file) { if (\preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) { return [$matches[1], $matches[2]]; } } } WhereamiCommand.php 0000644 00000010416 15111273344 0010320 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Formatter\CodeFormatter; use Psy\Output\ShellOutput; use Psy\Shell; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Show the context of where you opened the debugger. */ class WhereamiCommand extends Command { private $backtrace; /** * @param string|null $colorMode (deprecated and ignored) */ public function __construct($colorMode = null) { $this->backtrace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); parent::__construct(); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('whereami') ->setDefinition([ new InputOption('num', 'n', InputOption::VALUE_OPTIONAL, 'Number of lines before and after.', '5'), new InputOption('file', 'f|a', InputOption::VALUE_NONE, 'Show the full source for the current file.'), ]) ->setDescription('Show where you are in the code.') ->setHelp( <<<'HELP' Show where you are in the code. Optionally, include the number of lines before and after you want to display, or --file for the whole file. e.g. <return>> whereami </return> <return>> whereami -n10</return> <return>> whereami --file</return> HELP ); } /** * Obtains the correct stack frame in the full backtrace. * * @return array */ protected function trace(): array { foreach (\array_reverse($this->backtrace) as $stackFrame) { if ($this->isDebugCall($stackFrame)) { return $stackFrame; } } return \end($this->backtrace); } private static function isDebugCall(array $stackFrame): bool { $class = isset($stackFrame['class']) ? $stackFrame['class'] : null; $function = isset($stackFrame['function']) ? $stackFrame['function'] : null; return ($class === null && $function === 'Psy\\debug') || ($class === Shell::class && \in_array($function, ['__construct', 'debug'])); } /** * Determine the file and line based on the specific backtrace. * * @return array */ protected function fileInfo(): array { $stackFrame = $this->trace(); if (\preg_match('/eval\(/', $stackFrame['file'])) { \preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches); $file = $matches[1][0]; $line = (int) $matches[2][0]; } else { $file = $stackFrame['file']; $line = $stackFrame['line']; } return \compact('file', 'line'); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $info = $this->fileInfo(); $num = $input->getOption('num'); $lineNum = $info['line']; $startLine = \max($lineNum - $num, 1); $endLine = $lineNum + $num; $code = \file_get_contents($info['file']); if ($input->getOption('file')) { $startLine = 1; $endLine = null; } if ($output instanceof ShellOutput) { $output->startPaging(); } $output->writeln(\sprintf('From <info>%s:%s</info>:', $this->replaceCwd($info['file']), $lineNum)); $output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $lineNum), false); if ($output instanceof ShellOutput) { $output->stopPaging(); } return 0; } /** * Replace the given directory from the start of a filepath. * * @param string $file */ private function replaceCwd(string $file): string { $cwd = \getcwd(); if ($cwd === false) { return $file; } $cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file); } } BufferCommand.php 0000644 00000004665 15111273344 0010001 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use Psy\Exception\RuntimeException; use Psy\Output\ShellOutput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Interact with the current code buffer. * * Shows and clears the buffer for the current multi-line expression. */ class BufferCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('buffer') ->setAliases(['buf']) ->setDefinition([ new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the current buffer.'), ]) ->setDescription('Show (or clear) the contents of the code input buffer.') ->setHelp( <<<'HELP' Show the contents of the code buffer for the current multi-line expression. Optionally, clear the buffer by passing the <info>--clear</info> option. HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $app = $this->getApplication(); if (!$app instanceof \Psy\Shell) { throw new RuntimeException('Buffer command requires a \Psy\Shell application'); } $buf = $app->getCodeBuffer(); if ($input->getOption('clear')) { $app->resetCodeBuffer(); $output->writeln($this->formatLines($buf, 'urgent'), ShellOutput::NUMBER_LINES); } else { $output->writeln($this->formatLines($buf), ShellOutput::NUMBER_LINES); } return 0; } /** * A helper method for wrapping buffer lines in `<urgent>` and `<return>` formatter strings. * * @param array $lines * @param string $type (default: 'return') * * @return array Formatted strings */ protected function formatLines(array $lines, string $type = 'return'): array { $template = \sprintf('<%s>%%s</%s>', $type, $type); return \array_map(function ($line) use ($template) { return \sprintf($template, $line); }, $lines); } } ParseCommand.php 0000644 00000010037 15111273344 0007630 0 ustar 00 <?php /* * This file is part of Psy Shell. * * (c) 2012-2023 Justin Hileman * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Psy\Command; use PhpParser\Node; use PhpParser\Parser; use Psy\Context; use Psy\ContextAware; use Psy\Input\CodeArgument; use Psy\ParserFactory; use Psy\VarDumper\Presenter; use Psy\VarDumper\PresenterAware; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarDumper\Caster\Caster; /** * Parse PHP code and show the abstract syntax tree. */ class ParseCommand extends Command implements ContextAware, PresenterAware { /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; private $presenter; private $parserFactory; private $parsers; /** * {@inheritdoc} */ public function __construct($name = null) { $this->parserFactory = new ParserFactory(); $this->parsers = []; parent::__construct($name); } /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * PresenterAware interface. * * @param Presenter $presenter */ public function setPresenter(Presenter $presenter) { $this->presenter = clone $presenter; $this->presenter->addCasters([ Node::class => function (Node $node, array $a) { $a = [ Caster::PREFIX_VIRTUAL.'type' => $node->getType(), Caster::PREFIX_VIRTUAL.'attributes' => $node->getAttributes(), ]; foreach ($node->getSubNodeNames() as $name) { $a[Caster::PREFIX_VIRTUAL.$name] = $node->$name; } return $a; }, ]); } /** * {@inheritdoc} */ protected function configure() { $kindMsg = 'One of PhpParser\\ParserFactory constants: ' .\implode(', ', ParserFactory::getPossibleKinds()) ." (default is based on current interpreter's version)."; $this ->setName('parse') ->setDefinition([ new CodeArgument('code', CodeArgument::REQUIRED, 'PHP code to parse.'), new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse.', 10), new InputOption('kind', '', InputOption::VALUE_REQUIRED, $kindMsg, $this->parserFactory->getDefaultKind()), ]) ->setDescription('Parse PHP code and show the abstract syntax tree.') ->setHelp( <<<'HELP' Parse PHP code and show the abstract syntax tree. This command is used in the development of PsySH. Given a string of PHP code, it pretty-prints the PHP Parser parse tree. See https://github.com/nikic/PHP-Parser It prolly won't be super useful for most of you, but it's here if you want to play. HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $code = $input->getArgument('code'); $parserKind = $input->getOption('kind'); $depth = $input->getOption('depth'); $nodes = $this->getParser($parserKind)->parse($code); $output->page($this->presenter->present($nodes, $depth)); $this->context->setReturnValue($nodes); return 0; } /** * Get (or create) the Parser instance. * * @param string|null $kind One of Psy\ParserFactory constants (only for PHP parser 2.0 and above) */ private function getParser(string $kind = null): CodeArgumentParser { if (!\array_key_exists($kind, $this->parsers)) { $this->parsers[$kind] = new CodeArgumentParser($this->parserFactory->createParser($kind)); } return $this->parsers[$kind]; } } ListCommand.php 0000644 00000005030 15111273344 0007466 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * ListCommand displays the list of all available commands for the application. * * @author Fabien Potencier <fabien@symfony.com> */ class ListCommand extends Command { /** * @return void */ protected function configure() { $this ->setName('list') ->setDefinition([ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), ]) ->setDescription('List commands') ->setHelp(<<<'EOF' The <info>%command.name%</info> command lists all commands: <info>%command.full_name%</info> You can also display the commands for a specific namespace: <info>%command.full_name% test</info> You can also output the information in other formats by using the <comment>--format</comment> option: <info>%command.full_name% --format=xml</info> It's also possible to get raw list of commands (useful for embedding command runner): <info>%command.full_name% --raw</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $helper = new DescriptorHelper(); $helper->describe($output, $this->getApplication(), [ 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), 'namespace' => $input->getArgument('namespace'), 'short' => $input->getOption('short'), ]); return 0; } } Descriptor/HtmlDescriptor.php 0000644 00000007174 15111330411 0012335 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command\Descriptor; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\HtmlDumper; /** * Describe collected data clones for html output. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> * * @final */ class HtmlDescriptor implements DumpDescriptorInterface { private HtmlDumper $dumper; private bool $initialized = false; public function __construct(HtmlDumper $dumper) { $this->dumper = $dumper; } public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void { if (!$this->initialized) { $styles = file_get_contents(__DIR__.'/../../Resources/css/htmlDescriptor.css'); $scripts = file_get_contents(__DIR__.'/../../Resources/js/htmlDescriptor.js'); $output->writeln("<style>$styles</style><script>$scripts</script>"); $this->initialized = true; } $title = '-'; if (isset($context['request'])) { $request = $context['request']; $controller = "<span class='dumped-tag'>{$this->dumper->dump($request['controller'], true, ['maxDepth' => 0])}</span>"; $title = sprintf('<code>%s</code> <a href="%s">%s</a>', $request['method'], $uri = $request['uri'], $uri); $dedupIdentifier = $request['identifier']; } elseif (isset($context['cli'])) { $title = '<code>$ </code>'.$context['cli']['command_line']; $dedupIdentifier = $context['cli']['identifier']; } else { $dedupIdentifier = uniqid('', true); } $sourceDescription = ''; if (isset($context['source'])) { $source = $context['source']; $projectDir = $source['project_dir'] ?? null; $sourceDescription = sprintf('%s on line %d', $source['name'], $source['line']); if (isset($source['file_link'])) { $sourceDescription = sprintf('<a href="%s">%s</a>', $source['file_link'], $sourceDescription); } } $isoDate = $this->extractDate($context, 'c'); $tags = array_filter([ 'controller' => $controller ?? null, 'project dir' => $projectDir ?? null, ]); $output->writeln(<<<HTML <article data-dedup-id="$dedupIdentifier"> <header> <div class="row"> <h2 class="col">$title</h2> <time class="col text-small" title="$isoDate" datetime="$isoDate"> {$this->extractDate($context)} </time> </div> {$this->renderTags($tags)} </header> <section class="body"> <p class="text-small"> $sourceDescription </p> {$this->dumper->dump($data, true)} </section> </article> HTML ); } private function extractDate(array $context, string $format = 'r'): string { return date($format, (int) $context['timestamp']); } private function renderTags(array $tags): string { if (!$tags) { return ''; } $renderedTags = ''; foreach ($tags as $key => $value) { $renderedTags .= sprintf('<li><span class="badge">%s</span>%s</li>', $key, $value); } return <<<HTML <div class="row"> <ul class="tags"> $renderedTags </ul> </div> HTML; } } Descriptor/DumpDescriptorInterface.php 0000644 00000001142 15111330412 0014145 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command\Descriptor; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> */ interface DumpDescriptorInterface { public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void; } Descriptor/CliDescriptor.php 0000644 00000005144 15111330412 0012134 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command\Descriptor; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\CliDumper; /** * Describe collected data clones for cli output. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> * * @final */ class CliDescriptor implements DumpDescriptorInterface { private CliDumper $dumper; private mixed $lastIdentifier = null; public function __construct(CliDumper $dumper) { $this->dumper = $dumper; } public function describe(OutputInterface $output, Data $data, array $context, int $clientId): void { $io = $output instanceof SymfonyStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output); $this->dumper->setColors($output->isDecorated()); $rows = [['date', date('r', (int) $context['timestamp'])]]; $lastIdentifier = $this->lastIdentifier; $this->lastIdentifier = $clientId; $section = "Received from client #$clientId"; if (isset($context['request'])) { $request = $context['request']; $this->lastIdentifier = $request['identifier']; $section = sprintf('%s %s', $request['method'], $request['uri']); if ($controller = $request['controller']) { $rows[] = ['controller', rtrim($this->dumper->dump($controller, true), "\n")]; } } elseif (isset($context['cli'])) { $this->lastIdentifier = $context['cli']['identifier']; $section = '$ '.$context['cli']['command_line']; } if ($this->lastIdentifier !== $lastIdentifier) { $io->section($section); } if (isset($context['source'])) { $source = $context['source']; $sourceInfo = sprintf('%s on line %d', $source['name'], $source['line']); if ($fileLink = $source['file_link'] ?? null) { $sourceInfo = sprintf('<href=%s>%s</>', $fileLink, $sourceInfo); } $rows[] = ['source', $sourceInfo]; $file = $source['file_relative'] ?? $source['file']; $rows[] = ['file', $file]; } $io->table([], $rows); $this->dumper->dump($data); $io->newLine(); } } ServerDumpCommand.php 0000644 00000007426 15111330412 0010650 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Command\Descriptor\CliDescriptor; use Symfony\Component\VarDumper\Command\Descriptor\DumpDescriptorInterface; use Symfony\Component\VarDumper\Command\Descriptor\HtmlDescriptor; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\Server\DumpServer; /** * Starts a dump server to collect and output dumps on a single place with multiple formats support. * * @author Maxime Steinhausser <maxime.steinhausser@gmail.com> * * @final */ #[AsCommand(name: 'server:dump', description: 'Start a dump server that collects and displays dumps in a single place')] class ServerDumpCommand extends Command { private DumpServer $server; /** @var DumpDescriptorInterface[] */ private array $descriptors; public function __construct(DumpServer $server, array $descriptors = []) { $this->server = $server; $this->descriptors = $descriptors + [ 'cli' => new CliDescriptor(new CliDumper()), 'html' => new HtmlDescriptor(new HtmlDumper()), ]; parent::__construct(); } protected function configure(): void { $this ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format (%s)', implode(', ', $this->getAvailableFormats())), 'cli') ->setHelp(<<<'EOF' <info>%command.name%</info> starts a dump server that collects and displays dumps in a single place for debugging you application: <info>php %command.full_name%</info> You can consult dumped data in HTML format in your browser by providing the <comment>--format=html</comment> option and redirecting the output to a file: <info>php %command.full_name% --format="html" > dump.html</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $format = $input->getOption('format'); if (!$descriptor = $this->descriptors[$format] ?? null) { throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $format)); } $errorIo = $io->getErrorStyle(); $errorIo->title('Symfony Var Dumper Server'); $this->server->start(); $errorIo->success(sprintf('Server listening on %s', $this->server->getHost())); $errorIo->comment('Quit the server with CONTROL-C.'); $this->server->listen(function (Data $data, array $context, int $clientId) use ($descriptor, $io) { $descriptor->describe($io, $data, $context, $clientId); }); return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormats()); } } private function getAvailableFormats(): array { return array_keys($this->descriptors); } } LintCommand.php 0000644 00000023465 15111446542 0007477 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Yaml; /** * Validates YAML files syntax and outputs encountered errors. * * @author Grégoire Pineau <lyrixx@lyrixx.info> * @author Robin Chalas <robin.chalas@gmail.com> */ #[AsCommand(name: 'lint:yaml', description: 'Lint a YAML file and outputs encountered errors')] class LintCommand extends Command { private Parser $parser; private ?string $format = null; private bool $displayCorrectFiles; private ?\Closure $directoryIteratorProvider; private ?\Closure $isReadableProvider; public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null) { parent::__construct($name); $this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...); $this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...); } /** * @return void */ protected function configure() { $this ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions()))) ->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(<<<EOF The <info>%command.name%</info> command lints a YAML file and outputs to STDOUT the first encountered syntax error. You can validates YAML contents passed from STDIN: <info>cat filename | php %command.full_name% -</info> You can also validate the syntax of a file: <info>php %command.full_name% filename</info> Or of a whole directory: <info>php %command.full_name% dirname</info> <info>php %command.full_name% dirname --format=json</info> You can also exclude one or more specific files: <info>php %command.full_name% dirname --exclude="dirname/foo.yaml" --exclude="dirname/bar.yaml"</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $filenames = (array) $input->getArgument('filename'); $excludes = $input->getOption('exclude'); $this->format = $input->getOption('format'); $flags = $input->getOption('parse-tags'); if (null === $this->format) { // Autodetect format according to CI environment $this->format = class_exists(GithubActionReporter::class) && GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'; } $flags = $flags ? Yaml::PARSE_CUSTOM_TAGS : 0; $this->displayCorrectFiles = $output->isVerbose(); if (['-'] === $filenames) { return $this->display($io, [$this->validate(file_get_contents('php://stdin'), $flags)]); } if (!$filenames) { throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; foreach ($filenames as $filename) { if (!$this->isReadable($filename)) { throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); } foreach ($this->getFiles($filename) as $file) { if (!\in_array($file->getPathname(), $excludes, true)) { $filesInfo[] = $this->validate(file_get_contents($file), $flags, $file); } } } return $this->display($io, $filesInfo); } private function validate(string $content, int $flags, string $file = null): array { $prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use (&$prevErrorHandler) { if (\E_USER_DEPRECATED === $level) { throw new ParseException($message, $this->getParser()->getRealCurrentLineNb() + 1); } return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false; }); try { $this->getParser()->parse($content, Yaml::PARSE_CONSTANT | $flags); } catch (ParseException $e) { return ['file' => $file, 'line' => $e->getParsedLine(), 'valid' => false, 'message' => $e->getMessage()]; } finally { restore_error_handler(); } return ['file' => $file, 'valid' => true]; } private function display(SymfonyStyle $io, array $files): int { return match ($this->format) { 'txt' => $this->displayTxt($io, $files), 'json' => $this->displayJson($io, $files), 'github' => $this->displayTxt($io, $files, true), default => throw new InvalidArgumentException(sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))), }; } private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int { $countFiles = \count($filesInfo); $erroredFiles = 0; $suggestTagOption = false; if ($errorAsGithubAnnotations) { $githubReporter = new GithubActionReporter($io); } foreach ($filesInfo as $info) { if ($info['valid'] && $this->displayCorrectFiles) { $io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$erroredFiles; $io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); $io->text(sprintf('<error> >> %s</error>', $info['message'])); if (str_contains($info['message'], 'PARSE_CUSTOM_TAGS')) { $suggestTagOption = true; } if ($errorAsGithubAnnotations) { $githubReporter->error($info['message'], $info['file'] ?? 'php://stdin', $info['line']); } } } if (0 === $erroredFiles) { $io->success(sprintf('All %d YAML files contain valid syntax.', $countFiles)); } else { $io->warning(sprintf('%d YAML files have valid syntax and %d contain errors.%s', $countFiles - $erroredFiles, $erroredFiles, $suggestTagOption ? ' Use the --parse-tags option if you want parse custom tags.' : '')); } return min($erroredFiles, 1); } private function displayJson(SymfonyStyle $io, array $filesInfo): int { $errors = 0; array_walk($filesInfo, function (&$v) use (&$errors) { $v['file'] = (string) $v['file']; if (!$v['valid']) { ++$errors; } if (isset($v['message']) && str_contains($v['message'], 'PARSE_CUSTOM_TAGS')) { $v['message'] .= ' Use the --parse-tags option if you want parse custom tags.'; } }); $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); return min($errors, 1); } private function getFiles(string $fileOrDirectory): iterable { if (is_file($fileOrDirectory)) { yield new \SplFileInfo($fileOrDirectory); return; } foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { if (!\in_array($file->getExtension(), ['yml', 'yaml'])) { continue; } yield $file; } } private function getParser(): Parser { return $this->parser ??= new Parser(); } private function getDirectoryIterator(string $directory): iterable { $default = fn ($directory) => new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), \RecursiveIteratorIterator::LEAVES_ONLY ); if (null !== $this->directoryIteratorProvider) { return ($this->directoryIteratorProvider)($directory, $default); } return $default($directory); } private function isReadable(string $fileOrDirectory): bool { $default = is_readable(...); if (null !== $this->isReadableProvider) { return ($this->isReadableProvider)($fileOrDirectory, $default); } return $default($fileOrDirectory); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormatOptions()); } } private function getAvailableFormatOptions(): array { return ['txt', 'json', 'github']; } } DumpCompletionCommand.php 0000644 00000012500 15111727621 0011514 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; /** * Dumps the completion script for the current shell. * * @author Wouter de Jong <wouter@wouterj.nl> */ #[AsCommand(name: 'completion', description: 'Dump the shell completion script')] final class DumpCompletionCommand extends Command { /** * @deprecated since Symfony 6.1 */ protected static $defaultName = 'completion'; /** * @deprecated since Symfony 6.1 */ protected static $defaultDescription = 'Dump the shell completion script'; private array $supportedShells; protected function configure(): void { $fullCommand = $_SERVER['PHP_SELF']; $commandName = basename($fullCommand); $fullCommand = @realpath($fullCommand) ?: $fullCommand; $shell = $this->guessShell(); [$rcFile, $completionFile] = match ($shell) { 'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"], 'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName], default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"], }; $supportedShells = implode(', ', $this->getSupportedShells()); $this ->setHelp(<<<EOH The <info>%command.name%</> command dumps the shell completion script required to use shell autocompletion (currently, {$supportedShells} completion are supported). <comment>Static installation -------------------</> Dump the script to a global completion file and restart your shell: <info>%command.full_name% {$shell} | sudo tee {$completionFile}</> Or dump the script to a local file and source it: <info>%command.full_name% {$shell} > completion.sh</> <comment># source the file whenever you use the project</> <info>source completion.sh</> <comment># or add this line at the end of your "{$rcFile}" file:</> <info>source /path/to/completion.sh</> <comment>Dynamic installation --------------------</> Add this to the end of your shell configuration file (e.g. <info>"{$rcFile}"</>): <info>eval "$({$fullCommand} completion {$shell})"</> EOH ) ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...)) ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $commandName = basename($_SERVER['argv'][0]); if ($input->getOption('debug')) { $this->tailDebugLog($commandName, $output); return 0; } $shell = $input->getArgument('shell') ?? self::guessShell(); $completionFile = __DIR__.'/../Resources/completion.'.$shell; if (!file_exists($completionFile)) { $supportedShells = $this->getSupportedShells(); if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } if ($shell) { $output->writeln(sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").</>', $shell, implode('", "', $supportedShells))); } else { $output->writeln(sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells))); } return 2; } $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile))); return 0; } private static function guessShell(): string { return basename($_SERVER['SHELL'] ?? ''); } private function tailDebugLog(string $commandName, OutputInterface $output): void { $debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log'; if (!file_exists($debugFile)) { touch($debugFile); } $process = new Process(['tail', '-f', $debugFile], null, null, null, 0); $process->run(function (string $type, string $line) use ($output): void { $output->write($line); }); } /** * @return string[] */ private function getSupportedShells(): array { if (isset($this->supportedShells)) { return $this->supportedShells; } $shells = []; foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) { if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) { $shells[] = $file->getExtension(); } } sort($shells); return $this->supportedShells = $shells; } } LazyCommand.php 0000644 00000013713 15111727621 0007503 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Application; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Helper\HelperInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @author Nicolas Grekas <p@tchwork.com> */ final class LazyCommand extends Command { private \Closure|Command $command; private ?bool $isEnabled; public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true) { $this->setName($name) ->setAliases($aliases) ->setHidden($isHidden) ->setDescription($description); $this->command = $commandFactory; $this->isEnabled = $isEnabled; } public function ignoreValidationErrors(): void { $this->getCommand()->ignoreValidationErrors(); } public function setApplication(Application $application = null): void { if (1 > \func_num_args()) { trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); } if ($this->command instanceof parent) { $this->command->setApplication($application); } parent::setApplication($application); } public function setHelperSet(HelperSet $helperSet): void { if ($this->command instanceof parent) { $this->command->setHelperSet($helperSet); } parent::setHelperSet($helperSet); } public function isEnabled(): bool { return $this->isEnabled ?? $this->getCommand()->isEnabled(); } public function run(InputInterface $input, OutputInterface $output): int { return $this->getCommand()->run($input, $output); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $this->getCommand()->complete($input, $suggestions); } public function setCode(callable $code): static { $this->getCommand()->setCode($code); return $this; } /** * @internal */ public function mergeApplicationDefinition(bool $mergeArgs = true): void { $this->getCommand()->mergeApplicationDefinition($mergeArgs); } public function setDefinition(array|InputDefinition $definition): static { $this->getCommand()->setDefinition($definition); return $this; } public function getDefinition(): InputDefinition { return $this->getCommand()->getDefinition(); } public function getNativeDefinition(): InputDefinition { return $this->getCommand()->getNativeDefinition(); } /** * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion */ public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static { $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues); return $this; } /** * @param array|\Closure(CompletionInput,CompletionSuggestions):list<string|Suggestion> $suggestedValues The values used for input completion */ public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static { $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues); return $this; } public function setProcessTitle(string $title): static { $this->getCommand()->setProcessTitle($title); return $this; } public function setHelp(string $help): static { $this->getCommand()->setHelp($help); return $this; } public function getHelp(): string { return $this->getCommand()->getHelp(); } public function getProcessedHelp(): string { return $this->getCommand()->getProcessedHelp(); } public function getSynopsis(bool $short = false): string { return $this->getCommand()->getSynopsis($short); } public function addUsage(string $usage): static { $this->getCommand()->addUsage($usage); return $this; } public function getUsages(): array { return $this->getCommand()->getUsages(); } public function getHelper(string $name): HelperInterface { return $this->getCommand()->getHelper($name); } public function getCommand(): parent { if (!$this->command instanceof \Closure) { return $this->command; } $command = $this->command = ($this->command)(); $command->setApplication($this->getApplication()); if (null !== $this->getHelperSet()) { $command->setHelperSet($this->getHelperSet()); } $command->setName($this->getName()) ->setAliases($this->getAliases()) ->setHidden($this->isHidden()) ->setDescription($this->getDescription()); // Will throw if the command is not correctly initialized. $command->getDefinition(); return $command; } } SignalableCommandInterface.php 0000644 00000001530 15111727621 0012440 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; /** * Interface for command reacting to signal. * * @author Grégoire Pineau <lyrixx@lyrix.info> */ interface SignalableCommandInterface { /** * Returns the list of signals to subscribe. */ public function getSubscribedSignals(): array; /** * The method will be called when the application is signaled. * * @param int|false $previousExitCode * @return int|false The exit code to return or false to continue the normal execution */ public function handleSignal(int $signal, /* int|false $previousExitCode = 0 */); } CompleteCommand.php 0000644 00000022052 15111727621 0010330 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Completion\Output\BashCompletionOutput; use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; use Symfony\Component\Console\Completion\Output\FishCompletionOutput; use Symfony\Component\Console\Completion\Output\ZshCompletionOutput; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Responsible for providing the values to the shell completion. * * @author Wouter de Jong <wouter@wouterj.nl> */ #[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')] final class CompleteCommand extends Command { public const COMPLETION_API_VERSION = '1'; /** * @deprecated since Symfony 6.1 */ protected static $defaultName = '|_complete'; /** * @deprecated since Symfony 6.1 */ protected static $defaultDescription = 'Internal command to provide shell completion suggestions'; private $completionOutputs; private $isDebug = false; /** * @param array<string, class-string<CompletionOutputInterface>> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value */ public function __construct(array $completionOutputs = []) { // must be set before the parent constructor, as the property value is used in configure() $this->completionOutputs = $completionOutputs + [ 'bash' => BashCompletionOutput::class, 'fish' => FishCompletionOutput::class, 'zsh' => ZshCompletionOutput::class, ]; parent::__construct(); } protected function configure(): void { $this ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")') ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)') ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)') ->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script') ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated') ; } protected function initialize(InputInterface $input, OutputInterface $output): void { $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL); } protected function execute(InputInterface $input, OutputInterface $output): int { try { // "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1 $version = $input->getOption('symfony') ? '1' : $input->getOption('api-version'); if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) { $message = sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION); $this->log($message); $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); return 126; } $shell = $input->getOption('shell'); if (!$shell) { throw new \RuntimeException('The "--shell" option must be set.'); } if (!$completionOutput = $this->completionOutputs[$shell] ?? false) { throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs)))); } $completionInput = $this->createCompletionInput($input); $suggestions = new CompletionSuggestions(); $this->log([ '', '<comment>'.date('Y-m-d H:i:s').'</>', '<info>Input:</> <comment>("|" indicates the cursor position)</>', ' '.(string) $completionInput, '<info>Command:</>', ' '.(string) implode(' ', $_SERVER['argv']), '<info>Messages:</>', ]); $command = $this->findCommand($completionInput, $output); if (null === $command) { $this->log(' No command found, completing using the Application class.'); $this->getApplication()->complete($completionInput, $suggestions); } elseif ( $completionInput->mustSuggestArgumentValuesFor('command') && $command->getName() !== $completionInput->getCompletionValue() && !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true) ) { $this->log(' No command found, completing using the Application class.'); // expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear") $suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases()))); } else { $command->mergeApplicationDefinition(); $completionInput->bind($command->getDefinition()); if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) { $this->log(' Completing option names for the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> command.'); $suggestions->suggestOptions($command->getDefinition()->getOptions()); } else { $this->log([ ' Completing using the <comment>'.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'</> class.', ' Completing <comment>'.$completionInput->getCompletionType().'</> for <comment>'.$completionInput->getCompletionName().'</>', ]); if (null !== $compval = $completionInput->getCompletionValue()) { $this->log(' Current value: <comment>'.$compval.'</>'); } $command->complete($completionInput, $suggestions); } } /** @var CompletionOutputInterface $completionOutput */ $completionOutput = new $completionOutput(); $this->log('<info>Suggestions:</>'); if ($options = $suggestions->getOptionSuggestions()) { $this->log(' --'.implode(' --', array_map(fn ($o) => $o->getName(), $options))); } elseif ($values = $suggestions->getValueSuggestions()) { $this->log(' '.implode(' ', $values)); } else { $this->log(' <comment>No suggestions were provided</>'); } $completionOutput->write($suggestions, $output); } catch (\Throwable $e) { $this->log([ '<error>Error!</error>', (string) $e, ]); if ($output->isDebug()) { throw $e; } return 2; } return 0; } private function createCompletionInput(InputInterface $input): CompletionInput { $currentIndex = $input->getOption('current'); if (!$currentIndex || !ctype_digit($currentIndex)) { throw new \RuntimeException('The "--current" option must be set and it must be an integer.'); } $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex); try { $completionInput->bind($this->getApplication()->getDefinition()); } catch (ExceptionInterface) { } return $completionInput; } private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command { try { $inputName = $completionInput->getFirstArgument(); if (null === $inputName) { return null; } return $this->getApplication()->find($inputName); } catch (CommandNotFoundException) { } return null; } private function log($messages): void { if (!$this->isDebug) { return; } $commandName = basename($_SERVER['argv'][0]); file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND); } } LockableTrait.php 0000644 00000003333 15111727621 0010002 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; use Symfony\Component\Lock\Store\FlockStore; use Symfony\Component\Lock\Store\SemaphoreStore; /** * Basic lock feature for commands. * * @author Geoffrey Brier <geoffrey.brier@gmail.com> */ trait LockableTrait { private ?LockInterface $lock = null; /** * Locks a command. */ private function lock(string $name = null, bool $blocking = false): bool { if (!class_exists(SemaphoreStore::class)) { throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".'); } if (null !== $this->lock) { throw new LogicException('A lock is already in place.'); } if (SemaphoreStore::isSupported()) { $store = new SemaphoreStore(); } else { $store = new FlockStore(); } $this->lock = (new LockFactory($store))->createLock($name ?: $this->getName()); if (!$this->lock->acquire($blocking)) { $this->lock = null; return false; } return true; } /** * Releases the command lock if there is one. */ private function release(): void { if ($this->lock) { $this->lock->release(); $this->lock = null; } } } XliffLintCommand.php 0000644 00000025125 15111745545 0010470 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Util\XliffUtils; /** * Validates XLIFF files syntax and outputs encountered errors. * * @author Grégoire Pineau <lyrixx@lyrixx.info> * @author Robin Chalas <robin.chalas@gmail.com> * @author Javier Eguiluz <javier.eguiluz@gmail.com> */ #[AsCommand(name: 'lint:xliff', description: 'Lint an XLIFF file and outputs encountered errors')] class XliffLintCommand extends Command { private string $format; private bool $displayCorrectFiles; private ?\Closure $directoryIteratorProvider; private ?\Closure $isReadableProvider; private bool $requireStrictFileNames; public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null, bool $requireStrictFileNames = true) { parent::__construct($name); $this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...); $this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...); $this->requireStrictFileNames = $requireStrictFileNames; } /** * @return void */ protected function configure() { $this ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions()))) ->setHelp(<<<EOF The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT the first encountered syntax error. You can validates XLIFF contents passed from STDIN: <info>cat filename | php %command.full_name% -</info> You can also validate the syntax of a file: <info>php %command.full_name% filename</info> Or of a whole directory: <info>php %command.full_name% dirname</info> <info>php %command.full_name% dirname --format=json</info> EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $filenames = (array) $input->getArgument('filename'); $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'); $this->displayCorrectFiles = $output->isVerbose(); if (['-'] === $filenames) { return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); } if (!$filenames) { throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; foreach ($filenames as $filename) { if (!$this->isReadable($filename)) { throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); } foreach ($this->getFiles($filename) as $file) { $filesInfo[] = $this->validate(file_get_contents($file), $file); } } return $this->display($io, $filesInfo); } private function validate(string $content, string $file = null): array { $errors = []; // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input if ('' === trim($content)) { return ['file' => $file, 'valid' => true]; } $internal = libxml_use_internal_errors(true); $document = new \DOMDocument(); $document->loadXML($content); if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) { $normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/')); // strict file names require translation files to be named '____.locale.xlf' // otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed // also, the regexp matching must be case-insensitive, as defined for 'target-language' values // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language $expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern); if (0 === preg_match($expectedFilenamePattern, basename($file))) { $errors[] = [ 'line' => -1, 'column' => -1, 'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage), ]; } } foreach (XliffUtils::validateSchema($document) as $xmlError) { $errors[] = [ 'line' => $xmlError['line'], 'column' => $xmlError['column'], 'message' => $xmlError['message'], ]; } libxml_clear_errors(); libxml_use_internal_errors($internal); return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors]; } private function display(SymfonyStyle $io, array $files): int { return match ($this->format) { 'txt' => $this->displayTxt($io, $files), 'json' => $this->displayJson($io, $files), 'github' => $this->displayTxt($io, $files, true), default => throw new InvalidArgumentException(sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))), }; } private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int { $countFiles = \count($filesInfo); $erroredFiles = 0; $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null; foreach ($filesInfo as $info) { if ($info['valid'] && $this->displayCorrectFiles) { $io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$erroredFiles; $io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); $io->listing(array_map(function ($error) use ($info, $githubReporter) { // general document errors have a '-1' line number $line = -1 === $error['line'] ? null : $error['line']; $githubReporter?->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null); return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']); }, $info['messages'])); } } if (0 === $erroredFiles) { $io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles)); } else { $io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); } return min($erroredFiles, 1); } private function displayJson(SymfonyStyle $io, array $filesInfo): int { $errors = 0; array_walk($filesInfo, function (&$v) use (&$errors) { $v['file'] = (string) $v['file']; if (!$v['valid']) { ++$errors; } }); $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); return min($errors, 1); } /** * @return iterable<\SplFileInfo> */ private function getFiles(string $fileOrDirectory): iterable { if (is_file($fileOrDirectory)) { yield new \SplFileInfo($fileOrDirectory); return; } foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) { continue; } yield $file; } } /** * @return iterable<\SplFileInfo> */ private function getDirectoryIterator(string $directory): iterable { $default = fn ($directory) => new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), \RecursiveIteratorIterator::LEAVES_ONLY ); if (null !== $this->directoryIteratorProvider) { return ($this->directoryIteratorProvider)($directory, $default); } return $default($directory); } private function isReadable(string $fileOrDirectory): bool { $default = fn ($fileOrDirectory) => is_readable($fileOrDirectory); if (null !== $this->isReadableProvider) { return ($this->isReadableProvider)($fileOrDirectory, $default); } return $default($fileOrDirectory); } private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string { foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) { if ('target-language' === $attribute->nodeName) { return $attribute->nodeValue; } } return null; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormatOptions()); } } private function getAvailableFormatOptions(): array { return ['txt', 'json', 'github']; } } TranslationTrait.php 0000644 00000004525 15111745545 0010575 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; use Symfony\Component\Translation\TranslatorBag; /** * @internal */ trait TranslationTrait { private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag { $bag = new TranslatorBag(); foreach ($locales as $locale) { $catalogue = new MessageCatalogue($locale); foreach ($transPaths as $path) { $this->reader->read($path, $catalogue); } if ($domains) { foreach ($domains as $domain) { $bag->addCatalogue($this->filterCatalogue($catalogue, $domain)); } } else { $bag->addCatalogue($catalogue); } } return $bag; } private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue { $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); // extract intl-icu messages only $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; if ($intlMessages = $catalogue->all($intlDomain)) { $filteredCatalogue->add($intlMessages, $intlDomain); } // extract all messages and subtract intl-icu messages if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { $filteredCatalogue->add($messages, $domain); } foreach ($catalogue->getResources() as $resource) { $filteredCatalogue->addResource($resource); } if ($metadata = $catalogue->getMetadata('', $intlDomain)) { foreach ($metadata as $k => $v) { $filteredCatalogue->setMetadata($k, $v, $intlDomain); } } if ($metadata = $catalogue->getMetadata('', $domain)) { foreach ($metadata as $k => $v) { $filteredCatalogue->setMetadata($k, $v, $domain); } } return $filteredCatalogue; } } TranslationPushCommand.php 0000644 00000016763 15111745545 0011737 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Provider\FilteringProvider; use Symfony\Component\Translation\Provider\TranslationProviderCollection; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\TranslatorBag; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ #[AsCommand(name: 'translation:push', description: 'Push translations to a given provider.')] final class TranslationPushCommand extends Command { use TranslationTrait; private TranslationProviderCollection $providers; private TranslationReaderInterface $reader; private array $transPaths; private array $enabledLocales; public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = []) { $this->providers = $providers; $this->reader = $reader; $this->transPaths = $transPaths; $this->enabledLocales = $enabledLocales; parent::__construct(); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('provider')) { $suggestions->suggestValues($this->providers->keys()); return; } if ($input->mustSuggestOptionValuesFor('domains')) { $provider = $this->providers->get($input->getArgument('provider')); if ($provider && method_exists($provider, 'getDomains')) { $domains = $provider->getDomains(); $suggestions->suggestValues($domains); } return; } if ($input->mustSuggestOptionValuesFor('locales')) { $suggestions->suggestValues($this->enabledLocales); } } protected function configure(): void { $keys = $this->providers->keys(); $defaultProvider = 1 === \count($keys) ? $keys[0] : null; $this ->setDefinition([ new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider), new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'), new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'), new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'), new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales), ]) ->setHelp(<<<'EOF' The <info>%command.name%</> command pushes translations to the given provider. Only new translations are pushed, existing ones are not overwritten. You can overwrite existing translations by using the <comment>--force</> flag: <info>php %command.full_name% --force provider</> You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag: <info>php %command.full_name% --delete-missing provider</> Full example: <info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</> This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale. Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case. Provider translations for others domains and locales are ignored. EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $provider = $this->providers->get($input->getArgument('provider')); if (!$this->enabledLocales) { throw new InvalidArgumentException(sprintf('You must define "framework.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME))); } $io = new SymfonyStyle($input, $output); $domains = $input->getOption('domains'); $locales = $input->getOption('locales'); $force = $input->getOption('force'); $deleteMissing = $input->getOption('delete-missing'); if (!$domains && $provider instanceof FilteringProvider) { $domains = $provider->getDomains(); } // Reading local translations must be done after retrieving the domains from the provider // in order to manage only translations from configured domains $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); if (!$domains) { $domains = $this->getDomainsFromTranslatorBag($localTranslations); } if (!$deleteMissing && $force) { $provider->write($localTranslations); $io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } $providerTranslations = $provider->read($domains, $locales); if ($deleteMissing) { $provider->delete($providerTranslations->diff($localTranslations)); $io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); // Read provider translations again, after missing translations deletion, // to avoid push freshly deleted translations. $providerTranslations = $provider->read($domains, $locales); } $translationsToWrite = $localTranslations->diff($providerTranslations); if ($force) { $translationsToWrite->addBag($localTranslations->intersect($providerTranslations)); } $provider->write($translationsToWrite); $io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array { $domains = []; foreach ($translatorBag->getCatalogues() as $catalogue) { $domains += $catalogue->getDomains(); } return array_unique($domains); } } TranslationPullCommand.php 0000644 00000016701 15111745545 0011724 0 ustar 00 <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Provider\TranslationProviderCollection; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Writer\TranslationWriterInterface; /** * @author Mathieu Santostefano <msantostefano@protonmail.com> */ #[AsCommand(name: 'translation:pull', description: 'Pull translations from a given provider.')] final class TranslationPullCommand extends Command { use TranslationTrait; private TranslationProviderCollection $providerCollection; private TranslationWriterInterface $writer; private TranslationReaderInterface $reader; private string $defaultLocale; private array $transPaths; private array $enabledLocales; public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = []) { $this->providerCollection = $providerCollection; $this->writer = $writer; $this->reader = $reader; $this->defaultLocale = $defaultLocale; $this->transPaths = $transPaths; $this->enabledLocales = $enabledLocales; parent::__construct(); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('provider')) { $suggestions->suggestValues($this->providerCollection->keys()); return; } if ($input->mustSuggestOptionValuesFor('domains')) { $provider = $this->providerCollection->get($input->getArgument('provider')); if (method_exists($provider, 'getDomains')) { $suggestions->suggestValues($provider->getDomains()); } return; } if ($input->mustSuggestOptionValuesFor('locales')) { $suggestions->suggestValues($this->enabledLocales); return; } if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']); } } protected function configure(): void { $keys = $this->providerCollection->keys(); $defaultProvider = 1 === \count($keys) ? $keys[0] : null; $this ->setDefinition([ new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider), new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'), new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'), new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'), new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'), new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'), ]) ->setHelp(<<<'EOF' The <info>%command.name%</> command pulls translations from the given provider. Only new translations are pulled, existing ones are not overwritten. You can overwrite existing translations (and remove the missing ones on local side) by using the <comment>--force</> flag: <info>php %command.full_name% --force provider</> Full example: <info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</> This command pulls all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale. Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case. Local translations for others domains and locales are ignored. EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $provider = $this->providerCollection->get($input->getArgument('provider')); $force = $input->getOption('force'); $intlIcu = $input->getOption('intl-icu'); $locales = $input->getOption('locales') ?: $this->enabledLocales; $domains = $input->getOption('domains'); $format = $input->getOption('format'); $xliffVersion = '1.2'; if ($intlIcu && !$force) { $io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.'); } switch ($format) { case 'xlf20': $xliffVersion = '2.0'; // no break case 'xlf12': $format = 'xlf'; } $writeOptions = [ 'path' => end($this->transPaths), 'xliff_version' => $xliffVersion, 'default_locale' => $this->defaultLocale, ]; if (!$domains) { $domains = $provider->getDomains(); } $providerTranslations = $provider->read($domains, $locales); if ($force) { foreach ($providerTranslations->getCatalogues() as $catalogue) { $operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue); if ($intlIcu) { $operation->moveMessagesToIntlDomainsIfPossible(); } $this->writer->write($operation->getResult(), $format, $writeOptions); } $io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); // Append pulled translations to local ones. $localTranslations->addBag($providerTranslations->diff($localTranslations)); foreach ($localTranslations->getCatalogues() as $catalogue) { $this->writer->write($catalogue, $format, $writeOptions); } $io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); return 0; } }
Simpan