File manager - Edit - /home/autoph/public_html/projects/Rating-AutoHub/public/css/psy.tar
Back
psysh/bin/psysh 0000644 00000011626 15024771425 0007570 0 ustar 00 #!/usr/bin/env php <?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. */ // Try to find an autoloader for a local psysh version. // We'll wrap this whole mess in a Closure so it doesn't leak any globals. call_user_func(function () { $cwd = null; // Find the cwd arg (if present) $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); foreach ($argv as $i => $arg) { if ($arg === '--cwd') { if ($i >= count($argv) - 1) { fwrite(STDERR, 'Missing --cwd argument.' . PHP_EOL); exit(1); } $cwd = $argv[$i + 1]; break; } if (preg_match('/^--cwd=/', $arg)) { $cwd = substr($arg, 6); break; } } // Or fall back to the actual cwd if (!isset($cwd)) { $cwd = getcwd(); } $cwd = str_replace('\\', '/', $cwd); $chunks = explode('/', $cwd); while (!empty($chunks)) { $path = implode('/', $chunks); $prettyPath = $path; if (isset($_SERVER['HOME']) && $_SERVER['HOME']) { $prettyPath = preg_replace('/^' . preg_quote($_SERVER['HOME'], '/') . '/', '~', $path); } // Find composer.json if (is_file($path . '/composer.json')) { if ($cfg = json_decode(file_get_contents($path . '/composer.json'), true)) { if (isset($cfg['name']) && $cfg['name'] === 'psy/psysh') { // We're inside the psysh project. Let's use the local Composer autoload. if (is_file($path . '/vendor/autoload.php')) { if (realpath($path) !== realpath(__DIR__ . '/..')) { fwrite(STDERR, 'Using local PsySH version at ' . $prettyPath . PHP_EOL); } require $path . '/vendor/autoload.php'; } return; } } } // Or a composer.lock if (is_file($path . '/composer.lock')) { if ($cfg = json_decode(file_get_contents($path . '/composer.lock'), true)) { foreach (array_merge($cfg['packages'], $cfg['packages-dev']) as $pkg) { if (isset($pkg['name']) && $pkg['name'] === 'psy/psysh') { // We're inside a project which requires psysh. We'll use the local Composer autoload. if (is_file($path . '/vendor/autoload.php')) { if (realpath($path . '/vendor') !== realpath(__DIR__ . '/../../..')) { fwrite(STDERR, 'Using local PsySH version at ' . $prettyPath . PHP_EOL); } require $path . '/vendor/autoload.php'; } return; } } } } array_pop($chunks); } }); // We didn't find an autoloader for a local version, so use the autoloader that // came with this script. if (!class_exists('Psy\Shell')) { /* <<< */ if (is_file(__DIR__ . '/../vendor/autoload.php')) { require __DIR__ . '/../vendor/autoload.php'; } elseif (is_file(__DIR__ . '/../../../autoload.php')) { require __DIR__ . '/../../../autoload.php'; } else { fwrite(STDERR, 'PsySH dependencies not found, be sure to run `composer install`.' . PHP_EOL); fwrite(STDERR, 'See https://getcomposer.org to get Composer.' . PHP_EOL); exit(1); } /* >>> */ } // If the psysh binary was included directly, assume they just wanted an // autoloader and bail early. // // Keep this PHP 5.3 and 5.4 code around for a while in case someone is using a // globally installed psysh as a bin launcher for older local versions. if (version_compare(PHP_VERSION, '5.3.6', '<')) { $trace = debug_backtrace(); } elseif (version_compare(PHP_VERSION, '5.4.0', '<')) { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } else { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); } if (Psy\Shell::isIncluded($trace)) { unset($trace); return; } // Clean up after ourselves. unset($trace); // If the local version is too old, we can't do this if (!function_exists('Psy\bin')) { $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); $first = array_shift($argv); if (preg_match('/php(\.exe)?$/', $first)) { array_shift($argv); } array_unshift($argv, 'vendor/bin/psysh'); fwrite(STDERR, 'A local PsySH dependency was found, but it cannot be loaded. Please update to' . PHP_EOL); fwrite(STDERR, 'the latest version, or run the local copy directly, e.g.:' . PHP_EOL); fwrite(STDERR, PHP_EOL); fwrite(STDERR, ' ' . implode(' ', $argv) . PHP_EOL); exit(1); } // And go! call_user_func(Psy\bin()); psysh/composer.json 0000644 00000003251 15024771425 0010444 0 ustar 00 { "name": "psy/psysh", "description": "An interactive shell for modern PHP.", "type": "library", "keywords": ["console", "interactive", "shell", "repl"], "homepage": "http://psysh.org", "license": "MIT", "authors": [ { "name": "Justin Hileman", "email": "justin@justinhileman.info", "homepage": "http://justinhileman.com" } ], "require": { "php": "^8.0 || ^7.0.8", "ext-json": "*", "ext-tokenizer": "*", "symfony/console": "^6.0 || ^5.0 || ^4.0 || ^3.4", "symfony/var-dumper": "^6.0 || ^5.0 || ^4.0 || ^3.4", "nikic/php-parser": "^4.0 || ^3.1" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.2" }, "suggest": { "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history.", "ext-pdo-sqlite": "The doc command requires SQLite to work." }, "autoload": { "files": ["src/functions.php"], "psr-4": { "Psy\\": "src/" } }, "autoload-dev": { "psr-4": { "Psy\\Test\\": "test/" } }, "bin": ["bin/psysh"], "config": { "allow-plugins": { "bamarni/composer-bin-plugin": true } }, "extra": { "branch-alias": { "dev-main": "0.11.x-dev" } }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" } } psysh/src/ParserFactory.php 0000644 00000003126 15024771425 0012007 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; use PhpParser\Parser; use PhpParser\ParserFactory as OriginalParserFactory; /** * Parser factory to abstract over PHP parser library versions. */ class ParserFactory { const ONLY_PHP5 = 'ONLY_PHP5'; const ONLY_PHP7 = 'ONLY_PHP7'; const PREFER_PHP5 = 'PREFER_PHP5'; const PREFER_PHP7 = 'PREFER_PHP7'; /** * Possible kinds of parsers for the factory, from PHP parser library. * * @return string[] */ public static function getPossibleKinds(): array { return ['ONLY_PHP5', 'ONLY_PHP7', 'PREFER_PHP5', 'PREFER_PHP7']; } /** * Default kind (if supported, based on current interpreter's version). * * @return string|null */ public function getDefaultKind() { return static::ONLY_PHP7; } /** * New parser instance with given kind. * * @param string|null $kind One of class constants (only for PHP parser 2.0 and above) */ public function createParser($kind = null): Parser { $originalFactory = new OriginalParserFactory(); $kind = $kind ?: $this->getDefaultKind(); if (!\in_array($kind, static::getPossibleKinds())) { throw new \InvalidArgumentException('Unknown parser kind'); } $parser = $originalFactory->create(\constant(OriginalParserFactory::class.'::'.$kind)); return $parser; } } psysh/src/Sudo.php 0000644 00000012441 15024771425 0010135 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; /** * Helpers for bypassing visibility restrictions, mostly used in code generated * by the `sudo` command. */ class Sudo { /** * Fetch a property of an object, bypassing visibility restrictions. * * @param object $object * @param string $property property name * * @return mixed Value of $object->property */ public static function fetchProperty($object, string $property) { $prop = self::getProperty(new \ReflectionObject($object), $property); return $prop->getValue($object); } /** * Assign the value of a property of an object, bypassing visibility restrictions. * * @param object $object * @param string $property property name * @param mixed $value * * @return mixed Value of $object->property */ public static function assignProperty($object, string $property, $value) { $prop = self::getProperty(new \ReflectionObject($object), $property); $prop->setValue($object, $value); return $value; } /** * Call a method on an object, bypassing visibility restrictions. * * @param object $object * @param string $method method name * @param mixed $args... * * @return mixed */ public static function callMethod($object, string $method, ...$args) { $refl = new \ReflectionObject($object); $reflMethod = $refl->getMethod($method); $reflMethod->setAccessible(true); return $reflMethod->invokeArgs($object, $args); } /** * Fetch a property of a class, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $property property name * * @return mixed Value of $class::$property */ public static function fetchStaticProperty($class, string $property) { $prop = self::getProperty(new \ReflectionClass($class), $property); $prop->setAccessible(true); return $prop->getValue(); } /** * Assign the value of a static property of a class, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $property property name * @param mixed $value * * @return mixed Value of $class::$property */ public static function assignStaticProperty($class, string $property, $value) { $prop = self::getProperty(new \ReflectionClass($class), $property); $prop->setValue($value); return $value; } /** * Call a static method on a class, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $method method name * @param mixed $args... * * @return mixed */ public static function callStatic($class, string $method, ...$args) { $refl = new \ReflectionClass($class); $reflMethod = $refl->getMethod($method); $reflMethod->setAccessible(true); return $reflMethod->invokeArgs(null, $args); } /** * Fetch a class constant, bypassing visibility restrictions. * * @param string|object $class class name or instance * @param string $const constant name * * @return mixed */ public static function fetchClassConst($class, string $const) { $refl = new \ReflectionClass($class); do { if ($refl->hasConstant($const)) { return $refl->getConstant($const); } $refl = $refl->getParentClass(); } while ($refl !== false); return false; } /** * Construct an instance of a class, bypassing private constructors. * * @param string $class class name * @param mixed $args... */ public static function newInstance(string $class, ...$args) { $refl = new \ReflectionClass($class); $instance = $refl->newInstanceWithoutConstructor(); $constructor = $refl->getConstructor(); $constructor->setAccessible(true); $constructor->invokeArgs($instance, $args); return $instance; } /** * Get a ReflectionProperty from an object (or its parent classes). * * @throws \ReflectionException if neither the object nor any of its parents has this property * * @param \ReflectionClass $refl * @param string $property property name * * @return \ReflectionProperty */ private static function getProperty(\ReflectionClass $refl, string $property): \ReflectionProperty { $firstException = null; do { try { $prop = $refl->getProperty($property); $prop->setAccessible(true); return $prop; } catch (\ReflectionException $e) { if ($firstException === null) { $firstException = $e; } $refl = $refl->getParentClass(); } } while ($refl !== false); throw $firstException; } } psysh/src/CodeCleaner.php 0000644 00000027563 15024771425 0011402 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; use PhpParser\NodeTraverser; use PhpParser\Parser; use PhpParser\PrettyPrinter\Standard as Printer; use Psy\CodeCleaner\AbstractClassPass; use Psy\CodeCleaner\AssignThisVariablePass; use Psy\CodeCleaner\CalledClassPass; use Psy\CodeCleaner\CallTimePassByReferencePass; use Psy\CodeCleaner\CodeCleanerPass; use Psy\CodeCleaner\EmptyArrayDimFetchPass; use Psy\CodeCleaner\ExitPass; use Psy\CodeCleaner\FinalClassPass; use Psy\CodeCleaner\FunctionContextPass; use Psy\CodeCleaner\FunctionReturnInWriteContextPass; use Psy\CodeCleaner\ImplicitReturnPass; use Psy\CodeCleaner\InstanceOfPass; use Psy\CodeCleaner\IssetPass; use Psy\CodeCleaner\LabelContextPass; use Psy\CodeCleaner\LeavePsyshAlonePass; use Psy\CodeCleaner\ListPass; use Psy\CodeCleaner\LoopContextPass; use Psy\CodeCleaner\MagicConstantsPass; use Psy\CodeCleaner\NamespacePass; use Psy\CodeCleaner\PassableByReferencePass; use Psy\CodeCleaner\RequirePass; use Psy\CodeCleaner\ReturnTypePass; use Psy\CodeCleaner\StrictTypesPass; use Psy\CodeCleaner\UseStatementPass; use Psy\CodeCleaner\ValidClassNamePass; use Psy\CodeCleaner\ValidConstructorPass; use Psy\CodeCleaner\ValidFunctionNamePass; use Psy\Exception\ParseErrorException; /** * A service to clean up user input, detect parse errors before they happen, * and generally work around issues with the PHP code evaluation experience. */ class CodeCleaner { private $yolo = false; private $parser; private $printer; private $traverser; private $namespace; /** * CodeCleaner constructor. * * @param Parser|null $parser A PhpParser Parser instance. One will be created if not explicitly supplied * @param Printer|null $printer A PhpParser Printer instance. One will be created if not explicitly supplied * @param NodeTraverser|null $traverser A PhpParser NodeTraverser instance. One will be created if not explicitly supplied * @param bool $yolo run without input validation */ public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null, bool $yolo = false) { $this->yolo = $yolo; if ($parser === null) { $parserFactory = new ParserFactory(); $parser = $parserFactory->createParser(); } $this->parser = $parser; $this->printer = $printer ?: new Printer(); $this->traverser = $traverser ?: new NodeTraverser(); foreach ($this->getDefaultPasses() as $pass) { $this->traverser->addVisitor($pass); } } /** * Check whether this CodeCleaner is in YOLO mode. */ public function yolo(): bool { return $this->yolo; } /** * Get default CodeCleaner passes. * * @return CodeCleanerPass[] */ private function getDefaultPasses(): array { if ($this->yolo) { return $this->getYoloPasses(); } $useStatementPass = new UseStatementPass(); $namespacePass = new NamespacePass($this); // Try to add implicit `use` statements and an implicit namespace, // based on the file in which the `debug` call was made. $this->addImplicitDebugContext([$useStatementPass, $namespacePass]); return [ // Validation passes new AbstractClassPass(), new AssignThisVariablePass(), new CalledClassPass(), new CallTimePassByReferencePass(), new FinalClassPass(), new FunctionContextPass(), new FunctionReturnInWriteContextPass(), new InstanceOfPass(), new IssetPass(), new LabelContextPass(), new LeavePsyshAlonePass(), new ListPass(), new LoopContextPass(), new PassableByReferencePass(), new ReturnTypePass(), new EmptyArrayDimFetchPass(), new ValidConstructorPass(), // Rewriting shenanigans $useStatementPass, // must run before the namespace pass new ExitPass(), new ImplicitReturnPass(), new MagicConstantsPass(), $namespacePass, // must run after the implicit return pass new RequirePass(), new StrictTypesPass(), // Namespace-aware validation (which depends on aforementioned shenanigans) new ValidClassNamePass(), new ValidFunctionNamePass(), ]; } /** * A set of code cleaner passes that don't try to do any validation, and * only do minimal rewriting to make things work inside the REPL. * * This list should stay in sync with the "rewriting shenanigans" in * getDefaultPasses above. * * @return CodeCleanerPass[] */ private function getYoloPasses(): array { $useStatementPass = new UseStatementPass(); $namespacePass = new NamespacePass($this); // Try to add implicit `use` statements and an implicit namespace, // based on the file in which the `debug` call was made. $this->addImplicitDebugContext([$useStatementPass, $namespacePass]); return [ new LeavePsyshAlonePass(), $useStatementPass, // must run before the namespace pass new ExitPass(), new ImplicitReturnPass(), new MagicConstantsPass(), $namespacePass, // must run after the implicit return pass new RequirePass(), new StrictTypesPass(), ]; } /** * "Warm up" code cleaner passes when we're coming from a debug call. * * This is useful, for example, for `UseStatementPass` and `NamespacePass` * which keep track of state between calls, to maintain the current * namespace and a map of use statements. * * @param array $passes */ private function addImplicitDebugContext(array $passes) { $file = $this->getDebugFile(); if ($file === null) { return; } try { $code = @\file_get_contents($file); if (!$code) { return; } $stmts = $this->parse($code, true); if ($stmts === false) { return; } // Set up a clean traverser for just these code cleaner passes $traverser = new NodeTraverser(); foreach ($passes as $pass) { $traverser->addVisitor($pass); } $traverser->traverse($stmts); } catch (\Throwable $e) { // Don't care. } } /** * Search the stack trace for a file in which the user called Psy\debug. * * @return string|null */ private static function getDebugFile() { $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); foreach (\array_reverse($trace) as $stackFrame) { if (!self::isDebugCall($stackFrame)) { continue; } if (\preg_match('/eval\(/', $stackFrame['file'])) { \preg_match_all('/([^\(]+)\((\d+)/', $stackFrame['file'], $matches); return $matches[1][0]; } return $stackFrame['file']; } } /** * Check whether a given backtrace frame is a call to Psy\debug. * * @param array $stackFrame */ 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 && $function === 'debug'); } /** * Clean the given array of code. * * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP * * @param array $codeLines * @param bool $requireSemicolons * * @return string|false Cleaned PHP code, False if the input is incomplete */ public function clean(array $codeLines, bool $requireSemicolons = false) { $stmts = $this->parse('<?php '.\implode(\PHP_EOL, $codeLines).\PHP_EOL, $requireSemicolons); if ($stmts === false) { return false; } // Catch fatal errors before they happen $stmts = $this->traverser->traverse($stmts); // Work around https://github.com/nikic/PHP-Parser/issues/399 $oldLocale = \setlocale(\LC_NUMERIC, 0); \setlocale(\LC_NUMERIC, 'C'); $code = $this->printer->prettyPrint($stmts); // Now put the locale back \setlocale(\LC_NUMERIC, $oldLocale); return $code; } /** * Set the current local namespace. * * @param array|null $namespace (default: null) */ public function setNamespace(array $namespace = null) { $this->namespace = $namespace; } /** * Get the current local namespace. * * @return array|null */ public function getNamespace() { return $this->namespace; } /** * Lex and parse a block of code. * * @see Parser::parse * * @throws ParseErrorException for parse errors that can't be resolved by * waiting a line to see what comes next * * @param string $code * @param bool $requireSemicolons * * @return array|false A set of statements, or false if incomplete */ protected function parse(string $code, bool $requireSemicolons = false) { try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if ($this->parseErrorIsUnclosedString($e, $code)) { return false; } if ($this->parseErrorIsUnterminatedComment($e, $code)) { return false; } if ($this->parseErrorIsTrailingComma($e, $code)) { return false; } if (!$this->parseErrorIsEOF($e)) { throw ParseErrorException::fromParseError($e); } if ($requireSemicolons) { return false; } try { // Unexpected EOF, try again with an implicit semicolon return $this->parser->parse($code.';'); } catch (\PhpParser\Error $e) { return false; } } } private function parseErrorIsEOF(\PhpParser\Error $e): bool { $msg = $e->getRawMessage(); return ($msg === 'Unexpected token EOF') || (\strpos($msg, 'Syntax error, unexpected EOF') !== false); } /** * A special test for unclosed single-quoted strings. * * Unlike (all?) other unclosed statements, single quoted strings have * their own special beautiful snowflake syntax error just for * themselves. * * @param \PhpParser\Error $e * @param string $code */ private function parseErrorIsUnclosedString(\PhpParser\Error $e, string $code): bool { if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') { return false; } try { $this->parser->parse($code."';"); } catch (\Throwable $e) { return false; } return true; } private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code): bool { return $e->getRawMessage() === 'Unterminated comment'; } private function parseErrorIsTrailingComma(\PhpParser\Error $e, $code): bool { return ($e->getRawMessage() === 'A trailing comma is not allowed here') && (\substr(\rtrim($code), -1) === ','); } } psysh/src/SuperglobalsEnv.php 0000644 00000001154 15024771425 0012335 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; /** * Environment variables implementation via $_SERVER superglobal. */ class SuperglobalsEnv implements EnvInterface { /** * Get an environment variable by name. * * @return string|null */ public function get(string $key) { if (isset($_SERVER[$key]) && $_SERVER[$key]) { return $_SERVER[$key]; } return null; } } psysh/src/ContextAware.php 0000644 00000001067 15024771425 0011631 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; /** * ContextAware interface. * * This interface is used to pass the Shell's context into commands and such * which require access to the current scope variables. */ interface ContextAware { /** * Set the Context reference. * * @param Context $context */ public function setContext(Context $context); } psysh/src/Reflection/ReflectionConstant.php 0000644 00000001352 15024771425 0015120 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\Reflection; /** * @deprecated ReflectionConstant is now ReflectionClassConstant. This class * name will be reclaimed in the next stable release, to be used for * ReflectionConstant_ :) */ class ReflectionConstant extends ReflectionClassConstant { /** * {inheritDoc}. */ public function __construct($class, $name) { @\trigger_error('ReflectionConstant is now ReflectionClassConstant', \E_USER_DEPRECATED); parent::__construct($class, $name); } } psysh/src/Reflection/ReflectionNamespace.php 0000644 00000002132 15024771425 0015220 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\Reflection; /** * A fake Reflector for namespaces. */ class ReflectionNamespace implements \Reflector { private $name; /** * Construct a ReflectionNamespace object. * * @param string $name */ public function __construct(string $name) { $this->name = $name; } /** * Gets the constant name. * * @return string */ public function getName(): string { return $this->name; } /** * This can't (and shouldn't) do anything :). * * @throws \RuntimeException */ public static function export($name) { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * To string. * * @return string */ public function __toString(): string { return $this->getName(); } } psysh/src/Reflection/ReflectionLanguageConstruct.php 0000644 00000006664 15024771425 0016772 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\Reflection; /** * A fake ReflectionFunction but for language constructs. */ class ReflectionLanguageConstruct extends \ReflectionFunctionAbstract { public $keyword; /** * Language construct parameter definitions. */ private static $languageConstructs = [ 'isset' => [ 'var' => [], '...' => [ 'isOptional' => true, 'defaultValue' => null, ], ], 'unset' => [ 'var' => [], '...' => [ 'isOptional' => true, 'defaultValue' => null, ], ], 'empty' => [ 'var' => [], ], 'echo' => [ 'arg1' => [], '...' => [ 'isOptional' => true, 'defaultValue' => null, ], ], 'print' => [ 'arg' => [], ], 'die' => [ 'status' => [ 'isOptional' => true, 'defaultValue' => 0, ], ], 'exit' => [ 'status' => [ 'isOptional' => true, 'defaultValue' => 0, ], ], ]; /** * Construct a ReflectionLanguageConstruct object. * * @param string $keyword */ public function __construct(string $keyword) { if (!self::isLanguageConstruct($keyword)) { throw new \InvalidArgumentException('Unknown language construct: '.$keyword); } $this->keyword = $keyword; } /** * This can't (and shouldn't) do anything :). * * @throws \RuntimeException */ public static function export($name) { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get language construct name. */ public function getName(): string { return $this->keyword; } /** * None of these return references. */ public function returnsReference(): bool { return false; } /** * Get language construct params. * * @return array */ public function getParameters(): array { $params = []; foreach (self::$languageConstructs[$this->keyword] as $parameter => $opts) { $params[] = new ReflectionLanguageConstructParameter($this->keyword, $parameter, $opts); } return $params; } /** * Gets the file name from a language construct. * * (Hint: it always returns false) * * @todo remove \ReturnTypeWillChange attribute after dropping support for PHP 7.x (when we can use union types) * * @return string|false (false) */ #[\ReturnTypeWillChange] public function getFileName() { return false; } /** * To string. */ public function __toString(): string { return $this->getName(); } /** * Check whether keyword is a (known) language construct. * * @param string $keyword */ public static function isLanguageConstruct(string $keyword): bool { return \array_key_exists($keyword, self::$languageConstructs); } } psysh/src/Reflection/ReflectionClassConstant.php 0000644 00000012044 15024771425 0016106 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\Reflection; /** * Somehow the standard reflection library didn't include class constants until 7.1. * * ReflectionClassConstant corrects that omission. */ class ReflectionClassConstant implements \Reflector { public $class; public $name; private $value; /** * Construct a ReflectionClassConstant object. * * @param string|object $class * @param string $name */ public function __construct($class, string $name) { if (!$class instanceof \ReflectionClass) { $class = new \ReflectionClass($class); } $this->class = $class; $this->name = $name; $constants = $class->getConstants(); if (!\array_key_exists($name, $constants)) { throw new \InvalidArgumentException('Unknown constant: '.$name); } $this->value = $constants[$name]; } /** * Exports a reflection. * * @param string|object $class * @param string $name * @param bool $return pass true to return the export, as opposed to emitting it * * @return string|null */ public static function export($class, string $name, bool $return = false) { $refl = new self($class, $name); $value = $refl->getValue(); $str = \sprintf('Constant [ public %s %s ] { %s }', \gettype($value), $refl->getName(), $value); if ($return) { return $str; } echo $str."\n"; } /** * Gets the declaring class. */ public function getDeclaringClass(): \ReflectionClass { $parent = $this->class; // Since we don't have real reflection constants, we can't see where // it's actually defined. Let's check for a constant that is also // available on the parent class which has exactly the same value. // // While this isn't _technically_ correct, it's prolly close enough. do { $class = $parent; $parent = $class->getParentClass(); } while ($parent && $parent->hasConstant($this->name) && $parent->getConstant($this->name) === $this->value); return $class; } /** * Get the constant's docblock. * * @return false */ public function getDocComment(): bool { return false; } /** * Gets the class constant modifiers. * * Since this is only used for PHP < 7.1, we can just return "public". All * the fancier modifiers are only available on PHP versions which have their * own ReflectionClassConstant class :) */ public function getModifiers(): int { return \ReflectionMethod::IS_PUBLIC; } /** * Gets the constant name. */ public function getName(): string { return $this->name; } /** * Gets the value of the constant. * * @return mixed */ public function getValue() { return $this->value; } /** * Checks if class constant is private. * * @return bool false */ public function isPrivate(): bool { return false; } /** * Checks if class constant is protected. * * @return bool false */ public function isProtected(): bool { return false; } /** * Checks if class constant is public. * * @return bool true */ public function isPublic(): bool { return true; } /** * To string. */ public function __toString(): string { return $this->getName(); } /** * Gets the constant's file name. * * Currently returns null, because if it returns a file name the signature * formatter will barf. */ public function getFileName() { return; // return $this->class->getFileName(); } /** * Get the code start line. * * @throws \RuntimeException */ public function getStartLine() { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get the code end line. * * @throws \RuntimeException */ public function getEndLine() { return $this->getStartLine(); } /** * Get a ReflectionClassConstant instance. * * In PHP >= 7.1, this will return a \ReflectionClassConstant from the * standard reflection library. For older PHP, it will return this polyfill. * * @param string|object $class * @param string $name * * @return ReflectionClassConstant|\ReflectionClassConstant */ public static function create($class, string $name) { if (\class_exists(\ReflectionClassConstant::class)) { return new \ReflectionClassConstant($class, $name); } return new self($class, $name); } } psysh/src/Reflection/ReflectionLanguageConstructParameter.php 0000644 00000004725 15024771425 0020627 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\Reflection; /** * A fake ReflectionParameter but for language construct parameters. * * It stubs out all the important bits and returns whatever was passed in $opts. */ class ReflectionLanguageConstructParameter extends \ReflectionParameter { private $function; private $parameter; private $opts; public function __construct($function, $parameter, array $opts) { $this->function = $function; $this->parameter = $parameter; $this->opts = $opts; } /** * No class here. * * @todo remove \ReturnTypeWillChange attribute after dropping support for PHP 7.0 (when we can use nullable types) */ #[\ReturnTypeWillChange] public function getClass() { return; } /** * Is the param an array? * * @return bool */ public function isArray(): bool { return \array_key_exists('isArray', $this->opts) && $this->opts['isArray']; } /** * Get param default value. * * @todo remove \ReturnTypeWillChange attribute after dropping support for PHP 7.x (when we can use mixed type) * * @return mixed */ #[\ReturnTypeWillChange] public function getDefaultValue() { if ($this->isDefaultValueAvailable()) { return $this->opts['defaultValue']; } return null; } /** * Get param name. * * @return string */ public function getName(): string { return $this->parameter; } /** * Is the param optional? * * @return bool */ public function isOptional(): bool { return \array_key_exists('isOptional', $this->opts) && $this->opts['isOptional']; } /** * Does the param have a default value? * * @return bool */ public function isDefaultValueAvailable(): bool { return \array_key_exists('defaultValue', $this->opts); } /** * Is the param passed by reference? * * (I don't think this is true for anything we need to fake a param for) * * @return bool */ public function isPassedByReference(): bool { return \array_key_exists('isPassedByReference', $this->opts) && $this->opts['isPassedByReference']; } } psysh/src/Reflection/ReflectionConstant_.php 0000644 00000007171 15024771425 0015264 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\Reflection; /** * Somehow the standard reflection library doesn't include constants. * * ReflectionConstant_ corrects that omission. * * Note: For backwards compatibility reasons, this class is named * ReflectionConstant_ rather than ReflectionConstant. It will be renamed in * v0.10.0. */ class ReflectionConstant_ implements \Reflector { public $name; private $value; private static $magicConstants = [ '__LINE__', '__FILE__', '__DIR__', '__FUNCTION__', '__CLASS__', '__TRAIT__', '__METHOD__', '__NAMESPACE__', '__COMPILER_HALT_OFFSET__', ]; /** * Construct a ReflectionConstant_ object. * * @param string $name */ public function __construct(string $name) { $this->name = $name; if (!\defined($name) && !self::isMagicConstant($name)) { throw new \InvalidArgumentException('Unknown constant: '.$name); } if (!self::isMagicConstant($name)) { $this->value = @\constant($name); } } /** * Exports a reflection. * * @param string $name * @param bool $return pass true to return the export, as opposed to emitting it * * @return string|null */ public static function export(string $name, bool $return = false) { $refl = new self($name); $value = $refl->getValue(); $str = \sprintf('Constant [ %s %s ] { %s }', \gettype($value), $refl->getName(), $value); if ($return) { return $str; } echo $str."\n"; } public static function isMagicConstant($name) { return \in_array($name, self::$magicConstants); } /** * Get the constant's docblock. * * @return false */ public function getDocComment(): bool { return false; } /** * Gets the constant name. */ public function getName(): string { return $this->name; } /** * Gets the namespace name. * * Returns '' when the constant is not namespaced. */ public function getNamespaceName(): string { if (!$this->inNamespace()) { return ''; } return \preg_replace('/\\\\[^\\\\]+$/', '', $this->name); } /** * Gets the value of the constant. * * @return mixed */ public function getValue() { return $this->value; } /** * Checks if this constant is defined in a namespace. */ public function inNamespace(): bool { return \strpos($this->name, '\\') !== false; } /** * To string. */ public function __toString(): string { return $this->getName(); } /** * Gets the constant's file name. * * Currently returns null, because if it returns a file name the signature * formatter will barf. */ public function getFileName() { return; // return $this->class->getFileName(); } /** * Get the code start line. * * @throws \RuntimeException */ public function getStartLine() { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get the code end line. * * @throws \RuntimeException */ public function getEndLine() { return $this->getStartLine(); } } psysh/src/Sudo/SudoVisitor.php 0000644 00000013214 15024771425 0012426 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\Sudo; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified as FullyQualifiedName; use PhpParser\Node\Scalar\String_; use PhpParser\NodeVisitorAbstract; use Psy\Sudo; /** * A PHP Parser node visitor which rewrites property and method access to use * the Psy\Sudo visibility bypass methods. * * @todo handle assigning by reference */ class SudoVisitor extends NodeVisitorAbstract { const PROPERTY_FETCH = 'fetchProperty'; const PROPERTY_ASSIGN = 'assignProperty'; const METHOD_CALL = 'callMethod'; const STATIC_PROPERTY_FETCH = 'fetchStaticProperty'; const STATIC_PROPERTY_ASSIGN = 'assignStaticProperty'; const STATIC_CALL = 'callStatic'; const CLASS_CONST_FETCH = 'fetchClassConst'; const NEW_INSTANCE = 'newInstance'; /** * {@inheritdoc} * * @return int|Node|null Replacement node (or special return value) */ public function enterNode(Node $node) { if ($node instanceof PropertyFetch) { $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; $args = [ $node->var, \is_string($name) ? new String_($name) : $name, ]; return $this->prepareCall(self::PROPERTY_FETCH, $args); } elseif ($node instanceof Assign && $node->var instanceof PropertyFetch) { $target = $node->var; $name = $target->name instanceof Identifier ? $target->name->toString() : $target->name; $args = [ $target->var, \is_string($name) ? new String_($name) : $name, $node->expr, ]; return $this->prepareCall(self::PROPERTY_ASSIGN, $args); } elseif ($node instanceof MethodCall) { $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; $args = $node->args; \array_unshift($args, new Arg(\is_string($name) ? new String_($name) : $name)); \array_unshift($args, new Arg($node->var)); // not using prepareCall because the $node->args we started with are already Arg instances return new StaticCall(new FullyQualifiedName(Sudo::class), self::METHOD_CALL, $args); } elseif ($node instanceof StaticPropertyFetch) { $class = $node->class instanceof Name ? $node->class->toString() : $node->class; $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; $args = [ \is_string($class) ? new String_($class) : $class, \is_string($name) ? new String_($name) : $name, ]; return $this->prepareCall(self::STATIC_PROPERTY_FETCH, $args); } elseif ($node instanceof Assign && $node->var instanceof StaticPropertyFetch) { $target = $node->var; $class = $target->class instanceof Name ? $target->class->toString() : $target->class; $name = $target->name instanceof Identifier ? $target->name->toString() : $target->name; $args = [ \is_string($class) ? new String_($class) : $class, \is_string($name) ? new String_($name) : $name, $node->expr, ]; return $this->prepareCall(self::STATIC_PROPERTY_ASSIGN, $args); } elseif ($node instanceof StaticCall) { $args = $node->args; $class = $node->class instanceof Name ? $node->class->toString() : $node->class; $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; \array_unshift($args, new Arg(\is_string($name) ? new String_($name) : $name)); \array_unshift($args, new Arg(\is_string($class) ? new String_($class) : $class)); // not using prepareCall because the $node->args we started with are already Arg instances return new StaticCall(new FullyQualifiedName(Sudo::class), self::STATIC_CALL, $args); } elseif ($node instanceof ClassConstFetch) { $class = $node->class instanceof Name ? $node->class->toString() : $node->class; $name = $node->name instanceof Identifier ? $node->name->toString() : $node->name; $args = [ \is_string($class) ? new String_($class) : $class, \is_string($name) ? new String_($name) : $name, ]; return $this->prepareCall(self::CLASS_CONST_FETCH, $args); } elseif ($node instanceof New_) { $args = $node->args; $class = $node->class instanceof Name ? $node->class->toString() : $node->class; \array_unshift($args, new Arg(\is_string($class) ? new String_($class) : $class)); // not using prepareCall because the $node->args we started with are already Arg instances return new StaticCall(new FullyQualifiedName(Sudo::class), self::NEW_INSTANCE, $args); } } private function prepareCall(string $method, array $args): StaticCall { return new StaticCall(new FullyQualifiedName(Sudo::class), $method, \array_map(function ($arg) { return new Arg($arg); }, $args)); } } psysh/src/ExecutionClosure.php 0000644 00000004373 15024771425 0012530 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; /** * The Psy Shell's execution scope. */ class ExecutionClosure { const NOOP_INPUT = 'return null;'; private $closure; /** * @param Shell $__psysh__ */ public function __construct(Shell $__psysh__) { $this->setClosure($__psysh__, function () use ($__psysh__) { try { // Restore execution scope variables \extract($__psysh__->getScopeVariables(false)); // Buffer stdout; we'll need it later \ob_start([$__psysh__, 'writeStdout'], 1); // Convert all errors to exceptions \set_error_handler([$__psysh__, 'handleError']); // Evaluate the current code buffer $_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: self::NOOP_INPUT)); } catch (\Throwable $_e) { // Clean up on our way out. if (\ob_get_level() > 0) { \ob_end_clean(); } throw $_e; } finally { // Won't be needing this anymore \restore_error_handler(); } // Flush stdout (write to shell output, plus save to magic variable) \ob_end_flush(); // Save execution scope variables for next time $__psysh__->setScopeVariables(\get_defined_vars()); return $_; }); } /** * Set the closure instance. * * @param Shell $shell * @param \Closure $closure */ protected function setClosure(Shell $shell, \Closure $closure) { $that = $shell->getBoundObject(); if (\is_object($that)) { $this->closure = $closure->bindTo($that, \get_class($that)); } else { $this->closure = $closure->bindTo(null, $shell->getBoundClass()); } } /** * Go go gadget closure. * * @return mixed */ public function execute() { $closure = $this->closure; return $closure(); } } psysh/src/Util/Docblock.php 0000644 00000015226 15024771425 0011664 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\Util; /** * A docblock representation. * * Based on PHP-DocBlock-Parser by Paul Scott: * * {@link http://www.github.com/icio/PHP-DocBlock-Parser} * * @author Paul Scott <paul@duedil.com> * @author Justin Hileman <justin@justinhileman.info> */ class Docblock { /** * Tags in the docblock that have a whitespace-delimited number of parameters * (such as `@param type var desc` and `@return type desc`) and the names of * those parameters. * * @var array */ public static $vectors = [ 'throws' => ['type', 'desc'], 'param' => ['type', 'var', 'desc'], 'return' => ['type', 'desc'], ]; protected $reflector; /** * The description of the symbol. * * @var string */ public $desc; /** * The tags defined in the docblock. * * The array has keys which are the tag names (excluding the @) and values * that are arrays, each of which is an entry for the tag. * * In the case where the tag name is defined in {@see DocBlock::$vectors} the * value within the tag-value array is an array in itself with keys as * described by {@see DocBlock::$vectors}. * * @var array */ public $tags; /** * The entire DocBlock comment that was parsed. * * @var string */ public $comment; /** * Docblock constructor. * * @param \Reflector $reflector */ public function __construct(\Reflector $reflector) { $this->reflector = $reflector; if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionClassConstant || $reflector instanceof \ReflectionFunctionAbstract || $reflector instanceof \ReflectionProperty) { $this->setComment($reflector->getDocComment()); } } /** * Set and parse the docblock comment. * * @param string $comment The docblock */ protected function setComment(string $comment) { $this->desc = ''; $this->tags = []; $this->comment = $comment; $this->parseComment($comment); } /** * Find the length of the docblock prefix. * * @param array $lines * * @return int Prefix length */ protected static function prefixLength(array $lines): int { // find only lines with interesting things $lines = \array_filter($lines, function ($line) { return \substr($line, \strspn($line, "* \t\n\r\0\x0B")); }); // if we sort the lines, we only have to compare two items \sort($lines); $first = \reset($lines); $last = \end($lines); // Special case for single-line comments if (\count($lines) === 1) { return \strspn($first, "* \t\n\r\0\x0B"); } // find the longest common substring $count = \min(\strlen($first), \strlen($last)); for ($i = 0; $i < $count; $i++) { if ($first[$i] !== $last[$i]) { return $i; } } return $count; } /** * Parse the comment into the component parts and set the state of the object. * * @param string $comment The docblock */ protected function parseComment(string $comment) { // Strip the opening and closing tags of the docblock $comment = \substr($comment, 3, -2); // Split into arrays of lines $comment = \array_filter(\preg_split('/\r?\n\r?/', $comment)); // Trim asterisks and whitespace from the beginning and whitespace from the end of lines $prefixLength = self::prefixLength($comment); $comment = \array_map(function ($line) use ($prefixLength) { return \rtrim(\substr($line, $prefixLength)); }, $comment); // Group the lines together by @tags $blocks = []; $b = -1; foreach ($comment as $line) { if (self::isTagged($line)) { $b++; $blocks[] = []; } elseif ($b === -1) { $b = 0; $blocks[] = []; } $blocks[$b][] = $line; } // Parse the blocks foreach ($blocks as $block => $body) { $body = \trim(\implode("\n", $body)); if ($block === 0 && !self::isTagged($body)) { // This is the description block $this->desc = $body; } else { // This block is tagged $tag = \substr(self::strTag($body), 1); $body = \ltrim(\substr($body, \strlen($tag) + 2)); if (isset(self::$vectors[$tag])) { // The tagged block is a vector $count = \count(self::$vectors[$tag]); if ($body) { $parts = \preg_split('/\s+/', $body, $count); } else { $parts = []; } // Default the trailing values $parts = \array_pad($parts, $count, null); // Store as a mapped array $this->tags[$tag][] = \array_combine(self::$vectors[$tag], $parts); } else { // The tagged block is only text $this->tags[$tag][] = $body; } } } } /** * Whether or not a docblock contains a given @tag. * * @param string $tag The name of the @tag to check for */ public function hasTag(string $tag): bool { return \is_array($this->tags) && \array_key_exists($tag, $this->tags); } /** * The value of a tag. * * @param string $tag * * @return array|null */ public function tag(string $tag) { // TODO: Add proper null-type return values once the lowest PHP version supported is 7.1 return $this->hasTag($tag) ? $this->tags[$tag] : null; } /** * Whether or not a string begins with a @tag. * * @param string $str */ public static function isTagged(string $str): bool { return isset($str[1]) && $str[0] === '@' && !\preg_match('/[^A-Za-z]/', $str[1]); } /** * The tag at the beginning of a string. * * @param string $str * * @return string|null */ public static function strTag(string $str) { if (\preg_match('/^@[a-z0-9_]+/', $str, $matches)) { return $matches[0]; } } } psysh/src/Util/Str.php 0000644 00000005614 15024771425 0010714 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\Util; /** * String utility methods. * * @author ju1ius */ class Str { const UNVIS_RX = <<<'EOS' / \\(?: ((?:040)|s) | (240) | (?: M-(.) ) | (?: M\^(.) ) | (?: \^(.) ) ) /xS EOS; /** * Decodes a string encoded by libsd's strvis. * * From `man 3 vis`: * * Use an ‘M’ to represent meta characters (characters with the 8th bit set), * and use a caret ‘^’ to represent control characters (see iscntrl(3)). * The following formats are used: * * \040 Represents ASCII space. * * \240 Represents Meta-space (  in HTML). * * \M-C Represents character ‘C’ with the 8th bit set. * Spans characters ‘\241’ through ‘\376’. * * \M^C Represents control character ‘C’ with the 8th bit set. * Spans characters ‘\200’ through ‘\237’, and ‘\377’ (as ‘\M^?’). * * \^C Represents the control character ‘C’. * Spans characters ‘\000’ through ‘\037’, and ‘\177’ (as ‘\^?’). * * The other formats are supported by PHP's stripcslashes, * except for the \s sequence (ASCII space). * * @param string $input The string to decode */ public static function unvis(string $input): string { $output = \preg_replace_callback(self::UNVIS_RX, [self::class, 'unvisReplace'], $input); // other escapes & octal are handled by stripcslashes return \stripcslashes($output); } /** * Callback for Str::unvis. * * @param array $match The matches passed by preg_replace_callback */ protected static function unvisReplace(array $match): string { // \040, \s if (!empty($match[1])) { return "\x20"; } // \240 if (!empty($match[2])) { return "\xa0"; } // \M-(.) if (isset($match[3]) && $match[3] !== '') { $chr = $match[3]; // unvis S_META1 $cp = 0200; $cp |= \ord($chr); return \chr($cp); } // \M^(.) if (isset($match[4]) && $match[4] !== '') { $chr = $match[4]; // unvis S_META | S_CTRL $cp = 0200; $cp |= ($chr === '?') ? 0177 : \ord($chr) & 037; return \chr($cp); } // \^(.) if (isset($match[5]) && $match[5] !== '') { $chr = $match[5]; // unvis S_CTRL $cp = 0; $cp |= ($chr === '?') ? 0177 : \ord($chr) & 037; return \chr($cp); } } } psysh/src/Util/Json.php 0000644 00000001161 15024771425 0011046 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\Util; /** * A static class to wrap JSON encoding/decoding with PsySH's default options. */ class Json { /** * Encode a value as JSON. * * @param mixed $val * @param int $opt */ public static function encode($val, int $opt = 0): string { $opt |= \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; return \json_encode($val, $opt); } } psysh/src/Util/Mirror.php 0000644 00000011700 15024771425 0011407 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\Util; use Psy\Exception\RuntimeException; use Psy\Reflection\ReflectionClassConstant; use Psy\Reflection\ReflectionConstant_; use Psy\Reflection\ReflectionNamespace; /** * A utility class for getting Reflectors. */ class Mirror { const CONSTANT = 1; const METHOD = 2; const STATIC_PROPERTY = 4; const PROPERTY = 8; /** * Get a Reflector for a function, class or instance, constant, method or property. * * Optionally, pass a $filter param to restrict the types of members checked. For example, to only Reflectors for * static properties and constants, pass: * * $filter = Mirror::CONSTANT | Mirror::STATIC_PROPERTY * * @throws \Psy\Exception\RuntimeException when a $member specified but not present on $value * @throws \InvalidArgumentException if $value is something other than an object or class/function name * * @param mixed $value Class or function name, or variable instance * @param string $member Optional: property, constant or method name (default: null) * @param int $filter (default: CONSTANT | METHOD | PROPERTY | STATIC_PROPERTY) * * @return \Reflector */ public static function get($value, string $member = null, int $filter = 15): \Reflector { if ($member === null && \is_string($value)) { if (\function_exists($value)) { return new \ReflectionFunction($value); } elseif (\defined($value) || ReflectionConstant_::isMagicConstant($value)) { return new ReflectionConstant_($value); } } $class = self::getClass($value); if ($member === null) { return $class; } elseif ($filter & self::CONSTANT && $class->hasConstant($member)) { return ReflectionClassConstant::create($value, $member); } elseif ($filter & self::METHOD && $class->hasMethod($member)) { return $class->getMethod($member); } elseif ($filter & self::PROPERTY && $class->hasProperty($member)) { return $class->getProperty($member); } elseif ($filter & self::STATIC_PROPERTY && $class->hasProperty($member) && $class->getProperty($member)->isStatic()) { return $class->getProperty($member); } else { throw new RuntimeException(\sprintf('Unknown member %s on class %s', $member, \is_object($value) ? \get_class($value) : $value)); } } /** * Get a ReflectionClass (or ReflectionObject, or ReflectionNamespace) if possible. * * @throws \InvalidArgumentException if $value is not a namespace or class name or instance * * @param mixed $value * * @return \ReflectionClass|ReflectionNamespace */ private static function getClass($value) { if (\is_object($value)) { return new \ReflectionObject($value); } if (!\is_string($value)) { throw new \InvalidArgumentException('Mirror expects an object or class'); } if (\class_exists($value) || \interface_exists($value) || \trait_exists($value)) { return new \ReflectionClass($value); } $namespace = \preg_replace('/(^\\\\|\\\\$)/', '', $value); if (self::namespaceExists($namespace)) { return new ReflectionNamespace($namespace); } throw new \InvalidArgumentException('Unknown namespace, class or function: '.$value); } /** * Check declared namespaces for a given namespace. */ private static function namespaceExists(string $value): bool { return \in_array(\strtolower($value), self::getDeclaredNamespaces()); } /** * Get an array of all currently declared namespaces. * * Note that this relies on at least one function, class, interface, trait * or constant to have been declared in that namespace. */ private static function getDeclaredNamespaces(): array { $functions = \get_defined_functions(); $allNames = \array_merge( $functions['internal'], $functions['user'], \get_declared_classes(), \get_declared_interfaces(), \get_declared_traits(), \array_keys(\get_defined_constants()) ); $namespaces = []; foreach ($allNames as $name) { $chunks = \explode('\\', \strtolower($name)); // the last one is the function or class or whatever... \array_pop($chunks); while (!empty($chunks)) { $namespaces[\implode('\\', $chunks)] = true; \array_pop($chunks); } } $namespaceNames = \array_keys($namespaces); \sort($namespaceNames); return $namespaceNames; } } psysh/src/Context.php 0000644 00000017334 15024771425 0010655 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; /** * The Shell execution context. * * This class encapsulates the current variables, most recent return value and * exception, and the current namespace. */ class Context { private static $specialNames = ['_', '_e', '__out', '__psysh__', 'this']; // Include a very limited number of command-scope magic variable names. // This might be a bad idea, but future me can sort it out. private static $commandScopeNames = [ '__function', '__method', '__class', '__namespace', '__file', '__line', '__dir', ]; private $scopeVariables = []; private $commandScopeVariables = []; private $returnValue; private $lastException; private $lastStdout; private $boundObject; private $boundClass; /** * Get a context variable. * * @throws \InvalidArgumentException If the variable is not found in the current context * * @param string $name * * @return mixed */ public function get(string $name) { switch ($name) { case '_': return $this->returnValue; case '_e': if (isset($this->lastException)) { return $this->lastException; } break; case '__out': if (isset($this->lastStdout)) { return $this->lastStdout; } break; case 'this': if (isset($this->boundObject)) { return $this->boundObject; } break; case '__function': case '__method': case '__class': case '__namespace': case '__file': case '__line': case '__dir': if (\array_key_exists($name, $this->commandScopeVariables)) { return $this->commandScopeVariables[$name]; } break; default: if (\array_key_exists($name, $this->scopeVariables)) { return $this->scopeVariables[$name]; } break; } throw new \InvalidArgumentException('Unknown variable: $'.$name); } /** * Get all defined variables. */ public function getAll(): array { return \array_merge($this->scopeVariables, $this->getSpecialVariables()); } /** * Get all defined magic variables: $_, $_e, $__out, $__class, $__file, etc. */ public function getSpecialVariables(): array { $vars = [ '_' => $this->returnValue, ]; if (isset($this->lastException)) { $vars['_e'] = $this->lastException; } if (isset($this->lastStdout)) { $vars['__out'] = $this->lastStdout; } if (isset($this->boundObject)) { $vars['this'] = $this->boundObject; } return \array_merge($vars, $this->commandScopeVariables); } /** * Set all scope variables. * * This method does *not* set any of the magic variables: $_, $_e, $__out, * $__class, $__file, etc. * * @param array $vars */ public function setAll(array $vars) { foreach (self::$specialNames as $key) { unset($vars[$key]); } foreach (self::$commandScopeNames as $key) { unset($vars[$key]); } $this->scopeVariables = $vars; } /** * Set the most recent return value. * * @param mixed $value */ public function setReturnValue($value) { $this->returnValue = $value; } /** * Get the most recent return value. * * @return mixed */ public function getReturnValue() { return $this->returnValue; } /** * Set the most recent Exception or Error. * * @param \Throwable $e */ public function setLastException(\Throwable $e) { $this->lastException = $e; } /** * Get the most recent Exception or Error. * * @throws \InvalidArgumentException If no Exception has been caught * * @return \Throwable|null */ public function getLastException() { if (!isset($this->lastException)) { throw new \InvalidArgumentException('No most-recent exception'); } return $this->lastException; } /** * Set the most recent output from evaluated code. * * @param string $lastStdout */ public function setLastStdout(string $lastStdout) { $this->lastStdout = $lastStdout; } /** * Get the most recent output from evaluated code. * * @throws \InvalidArgumentException If no output has happened yet * * @return string|null */ public function getLastStdout() { if (!isset($this->lastStdout)) { throw new \InvalidArgumentException('No most-recent output'); } return $this->lastStdout; } /** * Set the bound object ($this variable) for the interactive shell. * * Note that this unsets the bound class, if any exists. * * @param object|null $boundObject */ public function setBoundObject($boundObject) { $this->boundObject = \is_object($boundObject) ? $boundObject : null; $this->boundClass = null; } /** * Get the bound object ($this variable) for the interactive shell. * * @return object|null */ public function getBoundObject() { return $this->boundObject; } /** * Set the bound class (self) for the interactive shell. * * Note that this unsets the bound object, if any exists. * * @param string|null $boundClass */ public function setBoundClass($boundClass) { $this->boundClass = (\is_string($boundClass) && $boundClass !== '') ? $boundClass : null; $this->boundObject = null; } /** * Get the bound class (self) for the interactive shell. * * @return string|null */ public function getBoundClass() { return $this->boundClass; } /** * Set command-scope magic variables: $__class, $__file, etc. * * @param array $commandScopeVariables */ public function setCommandScopeVariables(array $commandScopeVariables) { $vars = []; foreach ($commandScopeVariables as $key => $value) { // kind of type check if (\is_scalar($value) && \in_array($key, self::$commandScopeNames)) { $vars[$key] = $value; } } $this->commandScopeVariables = $vars; } /** * Get command-scope magic variables: $__class, $__file, etc. */ public function getCommandScopeVariables(): array { return $this->commandScopeVariables; } /** * Get unused command-scope magic variables names: __class, __file, etc. * * This is used by the shell to unset old command-scope variables after a * new batch is set. * * @return array Array of unused variable names */ public function getUnusedCommandScopeVariableNames(): array { return \array_diff(self::$commandScopeNames, \array_keys($this->commandScopeVariables)); } /** * Check whether a variable name is a magic variable. * * @param string $name */ public static function isSpecialVariableName(string $name): bool { return \in_array($name, self::$specialNames) || \in_array($name, self::$commandScopeNames); } } psysh/src/Output/ShellOutput.php 0000644 00000013355 15024771425 0013020 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\Output; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Output\ConsoleOutput; /** * A ConsoleOutput subclass specifically for Psy Shell output. */ class ShellOutput extends ConsoleOutput { const NUMBER_LINES = 128; private $paging = 0; /** @var OutputPager */ private $pager; /** @var Theme */ private $theme; /** * Construct a ShellOutput instance. * * @param mixed $verbosity (default: self::VERBOSITY_NORMAL) * @param bool|null $decorated (default: null) * @param OutputFormatterInterface|null $formatter (default: null) * @param string|OutputPager|null $pager (default: null) */ public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null, $pager = null, $theme = null) { parent::__construct($verbosity, $decorated, $formatter); $this->theme = $theme ?? new Theme('modern'); $this->initFormatters(); if ($pager === null) { $this->pager = new PassthruPager($this); } elseif (\is_string($pager)) { $this->pager = new ProcOutputPager($this, $pager); } elseif ($pager instanceof OutputPager) { $this->pager = $pager; } else { throw new \InvalidArgumentException('Unexpected pager parameter: '.$pager); } } /** * Page multiple lines of output. * * The output pager is started * * If $messages is callable, it will be called, passing this output instance * for rendering. Otherwise, all passed $messages are paged to output. * * Upon completion, the output pager is flushed. * * @param string|array|\Closure $messages A string, array of strings or a callback * @param int $type (default: 0) */ public function page($messages, int $type = 0) { if (\is_string($messages)) { $messages = (array) $messages; } if (!\is_array($messages) && !\is_callable($messages)) { throw new \InvalidArgumentException('Paged output requires a string, array or callback'); } $this->startPaging(); if (\is_callable($messages)) { $messages($this); } else { $this->write($messages, true, $type); } $this->stopPaging(); } /** * Start sending output to the output pager. */ public function startPaging() { $this->paging++; } /** * Stop paging output and flush the output pager. */ public function stopPaging() { $this->paging--; $this->closePager(); } /** * Writes a message to the output. * * Optionally, pass `$type | self::NUMBER_LINES` as the $type parameter to * number the lines of output. * * @throws \InvalidArgumentException When unknown output type is given * * @param string|array $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $type The type of output */ public function write($messages, $newline = false, $type = 0) { if ($this->getVerbosity() === self::VERBOSITY_QUIET) { return; } $messages = (array) $messages; if ($type & self::NUMBER_LINES) { $pad = \strlen((string) \count($messages)); $template = $this->isDecorated() ? "<aside>%{$pad}s</aside>: %s" : "%{$pad}s: %s"; if ($type & self::OUTPUT_RAW) { $messages = \array_map([OutputFormatter::class, 'escape'], $messages); } foreach ($messages as $i => $line) { $messages[$i] = \sprintf($template, $i, $line); } // clean this up for super. $type = $type & ~self::NUMBER_LINES & ~self::OUTPUT_RAW; } parent::write($messages, $newline, $type); } /** * Writes a message to the output. * * Handles paged output, or writes directly to the output stream. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not */ public function doWrite($message, $newline) { if ($this->paging > 0) { $this->pager->doWrite($message, $newline); } else { parent::doWrite($message, $newline); } } /** * Set the output Theme. */ public function setTheme(Theme $theme) { $this->theme = $theme; $this->initFormatters(); } /** * Flush and close the output pager. */ private function closePager() { if ($this->paging <= 0) { $this->pager->close(); } } /** * Initialize output formatter styles. */ private function initFormatters() { $useGrayFallback = !$this->grayExists(); $this->theme->applyStyles($this->getFormatter(), $useGrayFallback); $this->theme->applyErrorStyles($this->getErrorOutput()->getFormatter(), $useGrayFallback); } /** * Checks if the "gray" color exists on the output. */ private function grayExists(): bool { try { $this->write('<fg=gray></>'); } catch (\InvalidArgumentException $e) { return false; } return true; } } psysh/src/Output/OutputPager.php 0000644 00000001066 15024771425 0013003 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\Output; use Symfony\Component\Console\Output\OutputInterface; /** * An output pager is much the same as a regular OutputInterface, but allows * the stream to be flushed to a pager periodically. */ interface OutputPager extends OutputInterface { /** * Close the current pager process. */ public function close(); } psysh/src/Output/Theme.php 0000644 00000016162 15024771425 0011571 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\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Formatter\OutputFormatterStyle; /** * An output Theme, which controls prompt strings, formatter styles, and compact output. */ class Theme { const MODERN_THEME = []; // Defaults :) const COMPACT_THEME = [ 'compact' => true, ]; const CLASSIC_THEME = [ 'compact' => true, 'prompt' => '>>> ', 'bufferPrompt' => '... ', 'replayPrompt' => '--> ', 'returnValue' => '=> ', ]; const DEFAULT_STYLES = [ 'info' => ['white', 'blue', ['bold']], 'warning' => ['black', 'yellow'], 'error' => ['white', 'red', ['bold']], 'whisper' => ['gray'], 'aside' => ['blue'], 'strong' => [null, null, ['bold']], 'return' => ['cyan'], 'urgent' => ['red'], 'hidden' => ['black'], // Visibility 'public' => [null, null, ['bold']], 'protected' => ['yellow'], 'private' => ['red'], 'global' => ['cyan', null, ['bold']], 'const' => ['cyan'], 'class' => ['blue', null, ['underscore']], 'function' => [null], 'default' => [null], // Types 'number' => ['magenta'], 'integer' => ['magenta'], 'float' => ['yellow'], 'string' => ['green'], 'bool' => ['cyan'], 'keyword' => ['yellow'], 'comment' => ['blue'], 'object' => ['blue'], 'resource' => ['yellow'], // Code-specific formatting 'inline_html' => ['cyan'], ]; const ERROR_STYLES = ['info', 'warning', 'error', 'whisper']; private $compact = false; private $prompt = '> '; private $bufferPrompt = '. '; private $replayPrompt = '- '; private $returnValue = '= '; private $grayFallback = 'blue'; private $styles = []; /** * @param string|array $config theme name or config options */ public function __construct($config = 'modern') { if (\is_string($config)) { switch ($config) { case 'modern': $config = static::MODERN_THEME; break; case 'compact': $config = static::COMPACT_THEME; break; case 'classic': $config = static::CLASSIC_THEME; break; default: \trigger_error(\sprintf('Unknown theme: %s', $config), \E_USER_NOTICE); $config = static::MODERN_THEME; break; } } if (!\is_array($config)) { throw new \InvalidArgumentException('Invalid theme config'); } foreach ($config as $name => $value) { switch ($name) { case 'compact': $this->setCompact($value); break; case 'prompt': $this->setPrompt($value); break; case 'bufferPrompt': $this->setBufferPrompt($value); break; case 'replayPrompt': $this->setReplayPrompt($value); break; case 'returnValue': $this->setReturnValue($value); break; case 'grayFallback': $this->setGrayFallback($value); break; } } $this->setStyles($config['styles'] ?? []); } /** * Enable or disable compact output. */ public function setCompact(bool $compact) { $this->compact = $compact; } /** * Get whether to use compact output. */ public function compact(): bool { return $this->compact; } /** * Set the prompt string. */ public function setPrompt(string $prompt) { $this->prompt = $prompt; } /** * Get the prompt string. */ public function prompt(): string { return $this->prompt; } /** * Set the buffer prompt string (used for multi-line input continuation). */ public function setBufferPrompt(string $bufferPrompt) { $this->bufferPrompt = $bufferPrompt; } /** * Get the buffer prompt string (used for multi-line input continuation). */ public function bufferPrompt(): string { return $this->bufferPrompt; } /** * Set the prompt string used when replaying history. */ public function setReplayPrompt(string $replayPrompt) { $this->replayPrompt = $replayPrompt; } /** * Get the prompt string used when replaying history. */ public function replayPrompt(): string { return $this->replayPrompt; } /** * Set the return value marker. */ public function setReturnValue(string $returnValue) { $this->returnValue = $returnValue; } /** * Get the return value marker. */ public function returnValue(): string { return $this->returnValue; } /** * Set the fallback color when "gray" is unavailable. */ public function setGrayFallback(string $grayFallback) { $this->grayFallback = $grayFallback; } /** * Set the shell output formatter styles. * * Accepts a map from style name to [fg, bg, options], for example: * * [ * 'error' => ['white', 'red', ['bold']], * 'warning' => ['black', 'yellow'], * ] * * Foreground, background or options can be null, or even omitted entirely. */ public function setStyles(array $styles) { foreach (\array_keys(static::DEFAULT_STYLES) as $name) { $this->styles[$name] = $styles[$name] ?? static::DEFAULT_STYLES[$name]; } } /** * Apply the current output formatter styles. */ public function applyStyles(OutputFormatterInterface $formatter, bool $useGrayFallback) { foreach (\array_keys(static::DEFAULT_STYLES) as $name) { $formatter->setStyle($name, new OutputFormatterStyle(...$this->getStyle($name, $useGrayFallback))); } } /** * Apply the current output formatter error styles. */ public function applyErrorStyles(OutputFormatterInterface $errorFormatter, bool $useGrayFallback) { foreach (static::ERROR_STYLES as $name) { $errorFormatter->setStyle($name, new OutputFormatterStyle(...$this->getStyle($name, $useGrayFallback))); } } private function getStyle(string $name, bool $useGrayFallback): array { return \array_map(function ($style) use ($useGrayFallback) { return ($useGrayFallback && $style === 'gray') ? $this->grayFallback : $style; }, $this->styles[$name]); } } psysh/src/Output/PassthruPager.php 0000644 00000001451 15024771425 0013312 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\Output; use Symfony\Component\Console\Output\StreamOutput; /** * A passthrough pager is a no-op. It simply wraps a StreamOutput's stream and * does nothing when the pager is closed. */ class PassthruPager extends StreamOutput implements OutputPager { /** * Constructor. * * @param StreamOutput $output */ public function __construct(StreamOutput $output) { parent::__construct($output->getStream()); } /** * Close the current pager process. */ public function close() { // nothing to do here } } psysh/src/Output/ProcOutputPager.php 0000644 00000005435 15024771425 0013633 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\Output; use Symfony\Component\Console\Output\StreamOutput; /** * ProcOutputPager class. * * A ProcOutputPager instance wraps a regular StreamOutput's stream. Rather * than writing directly to the stream, it shells out to a pager process and * gives that process the stream as stdout. This means regular *nix commands * like `less` and `more` can be used to page large amounts of output. */ class ProcOutputPager extends StreamOutput implements OutputPager { private $proc; private $pipe; private $stream; private $cmd; /** * Constructor. * * @param StreamOutput $output * @param string $cmd Pager process command (default: 'less -R -F -X') */ public function __construct(StreamOutput $output, string $cmd = 'less -R -F -X') { $this->stream = $output->getStream(); $this->cmd = $cmd; } /** * Writes a message to the output. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not * * @throws \RuntimeException When unable to write output (should never happen) */ public function doWrite($message, $newline) { $pipe = $this->getPipe(); if (false === @\fwrite($pipe, $message.($newline ? \PHP_EOL : ''))) { // @codeCoverageIgnoreStart // should never happen $this->close(); throw new \RuntimeException('Unable to write output'); // @codeCoverageIgnoreEnd } \fflush($pipe); } /** * Close the current pager process. */ public function close() { if (isset($this->pipe)) { \fclose($this->pipe); } if (isset($this->proc)) { $exit = \proc_close($this->proc); if ($exit !== 0) { throw new \RuntimeException('Error closing output stream'); } } $this->pipe = null; $this->proc = null; } /** * Get a pipe for paging output. * * If no active pager process exists, fork one and return its input pipe. */ private function getPipe() { if (!isset($this->pipe) || !isset($this->proc)) { $desc = [['pipe', 'r'], $this->stream, \fopen('php://stderr', 'w')]; $this->proc = \proc_open($this->cmd, $desc, $pipes); if (!\is_resource($this->proc)) { throw new \RuntimeException('Error opening output stream'); } $this->pipe = $pipes[0]; } return $this->pipe; } } psysh/src/Exception/BreakException.php 0000644 00000002106 15024771425 0014061 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\Exception; /** * A break exception, used for halting the Psy Shell. */ class BreakException extends \Exception implements Exception { private $rawMessage; /** * {@inheritdoc} */ public function __construct($message = '', $code = 0, \Throwable $previous = null) { $this->rawMessage = $message; parent::__construct(\sprintf('Exit: %s', $message), $code, $previous); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->rawMessage; } /** * Throws BreakException. * * Since `throw` can not be inserted into arbitrary expressions, it wraps with function call. * * @throws BreakException */ public static function exitShell() { throw new self('Goodbye'); } } psysh/src/Exception/ThrowUpException.php 0000644 00000002623 15024771425 0014451 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\Exception; /** * A throw-up exception, used for throwing an exception out of the Psy Shell. */ class ThrowUpException extends \Exception implements Exception { /** * {@inheritdoc} */ public function __construct(\Throwable $throwable) { $message = \sprintf("Throwing %s with message '%s'", \get_class($throwable), $throwable->getMessage()); parent::__construct($message, $throwable->getCode(), $throwable); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->getPrevious()->getMessage(); } /** * Create a ThrowUpException from a Throwable. * * @deprecated psySH no longer wraps Throwables * * @param \Throwable $throwable */ public static function fromThrowable($throwable): self { if ($throwable instanceof \Error) { $throwable = ErrorException::fromError($throwable); } if (!$throwable instanceof \Exception) { throw new \InvalidArgumentException('throw-up can only throw Exceptions and Errors'); } return new self($throwable); } } psysh/src/Exception/TypeErrorException.php 0000644 00000002627 15024771425 0015000 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\Exception; /** * A "type error" Exception for Psy. */ class TypeErrorException extends \Exception implements Exception { private $rawMessage; /** * Constructor! * * @deprecated psySH no longer wraps TypeErrors * * @param string $message (default: "") * @param int $code (default: 0) * @param \Throwable|null $previous (default: null) */ public function __construct(string $message = '', int $code = 0, \Throwable $previous = null) { $this->rawMessage = $message; $message = \preg_replace('/, called in .*?: eval\\(\\)\'d code/', '', $message); parent::__construct(\sprintf('TypeError: %s', $message), $code, $previous); } /** * Get the raw (unformatted) message for this error. */ public function getRawMessage(): string { return $this->rawMessage; } /** * Create a TypeErrorException from a TypeError. * * @deprecated psySH no longer wraps TypeErrors * * @param \TypeError $e */ public static function fromTypeError(\TypeError $e): self { return new self($e->getMessage(), $e->getCode(), $e); } } psysh/src/Exception/ErrorException.php 0000644 00000006014 15024771425 0014130 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\Exception; /** * A custom error Exception for Psy with a formatted $message. */ class ErrorException extends \ErrorException implements Exception { private $rawMessage; /** * Construct a Psy ErrorException. * * @param string $message (default: "") * @param int $code (default: 0) * @param int $severity (default: 1) * @param string|null $filename (default: null) * @param int|null $lineno (default: null) * @param \Throwable|null $previous (default: null) */ public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, \Throwable $previous = null) { $this->rawMessage = $message; if (!empty($filename) && \preg_match('{Psy[/\\\\]ExecutionLoop}', $filename)) { $filename = ''; } switch ($severity) { case \E_STRICT: $type = 'Strict error'; break; case \E_NOTICE: case \E_USER_NOTICE: $type = 'Notice'; break; case \E_WARNING: case \E_CORE_WARNING: case \E_COMPILE_WARNING: case \E_USER_WARNING: $type = 'Warning'; break; case \E_DEPRECATED: case \E_USER_DEPRECATED: $type = 'Deprecated'; break; case \E_RECOVERABLE_ERROR: $type = 'Recoverable fatal error'; break; default: $type = 'Error'; break; } $message = \sprintf('PHP %s: %s%s on line %d', $type, $message, $filename ? ' in '.$filename : '', $lineno); parent::__construct($message, $code, $severity, $filename, $lineno, $previous); } /** * Get the raw (unformatted) message for this error. */ public function getRawMessage(): string { return $this->rawMessage; } /** * Helper for throwing an ErrorException. * * This allows us to: * * set_error_handler([ErrorException::class, 'throwException']); * * @throws self * * @param int $errno Error type * @param string $errstr Message * @param string $errfile Filename * @param int $errline Line number */ public static function throwException($errno, $errstr, $errfile, $errline) { throw new self($errstr, 0, $errno, $errfile, $errline); } /** * Create an ErrorException from an Error. * * @deprecated psySH no longer wraps Errors * * @param \Error $e */ public static function fromError(\Error $e): self { return new self($e->getMessage(), $e->getCode(), 1, $e->getFile(), $e->getLine(), $e); } } psysh/src/Exception/FatalErrorException.php 0000644 00000002725 15024771425 0015105 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\Exception; /** * A "fatal error" Exception for Psy. */ class FatalErrorException extends \ErrorException implements Exception { private $rawMessage; /** * Create a fatal error. * * @param string $message (default: "") * @param int $code (default: 0) * @param int $severity (default: 1) * @param string|null $filename (default: null) * @param int|null $lineno (default: null) * @param \Throwable|null $previous (default: null) */ public function __construct($message = '', $code = 0, $severity = 1, $filename = null, $lineno = null, \Throwable $previous = null) { // Since these are basically always PHP Parser Node line numbers, treat -1 as null. if ($lineno === -1) { $lineno = null; } $this->rawMessage = $message; $message = \sprintf('PHP Fatal error: %s in %s on line %d', $message, $filename ?: "eval()'d code", $lineno); parent::__construct($message, $code, $severity, $filename, $lineno, $previous); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->rawMessage; } } psysh/src/Exception/UnexpectedTargetException.php 0000644 00000001536 15024771425 0016316 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\Exception; class UnexpectedTargetException extends RuntimeException { private $target; /** * @param mixed $target * @param string $message (default: "") * @param int $code (default: 0) * @param \Throwable|null $previous (default: null) */ public function __construct($target, string $message = '', int $code = 0, \Throwable $previous = null) { $this->target = $target; parent::__construct($message, $code, $previous); } /** * @return mixed */ public function getTarget() { return $this->target; } } psysh/src/Exception/DeprecatedException.php 0000644 00000000576 15024771425 0015106 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\Exception; /** * A DeprecatedException for Psy. */ class DeprecatedException extends RuntimeException { // This space intentionally left blank. } psysh/src/Exception/Exception.php 0000644 00000000761 15024771425 0013121 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\Exception; /** * An interface for Psy Exceptions. */ interface Exception { /** * This is the only thing, really... * * Return a raw (unformatted) version of the message. * * @return string */ public function getRawMessage(); } psysh/src/Exception/ParseErrorException.php 0000644 00000001665 15024771425 0015132 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\Exception; /** * A "parse error" Exception for Psy. */ class ParseErrorException extends \PhpParser\Error implements Exception { /** * Constructor! * * @param string $message (default: "") * @param int $line (default: -1) */ public function __construct(string $message = '', int $line = -1) { $message = \sprintf('PHP Parse error: %s', $message); parent::__construct($message, $line); } /** * Create a ParseErrorException from a PhpParser Error. * * @param \PhpParser\Error $e */ public static function fromParseError(\PhpParser\Error $e): self { return new self($e->getRawMessage(), $e->getStartLine()); } } psysh/src/Exception/RuntimeException.php 0000644 00000001710 15024771425 0014460 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\Exception; /** * A RuntimeException for Psy. */ class RuntimeException extends \RuntimeException implements Exception { private $rawMessage; /** * Make this bad boy. * * @param string $message (default: "") * @param int $code (default: 0) * @param \Throwable|null $previous (default: null) */ public function __construct(string $message = '', int $code = 0, \Throwable $previous = null) { $this->rawMessage = $message; parent::__construct($message, $code, $previous); } /** * Return a raw (unformatted) version of the error message. */ public function getRawMessage(): string { return $this->rawMessage; } } psysh/src/ExecutionLoop/ProcessForker.php 0000644 00000020473 15024771425 0014613 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\ExecutionLoop; use Psy\Context; use Psy\Exception\BreakException; use Psy\Shell; /** * An execution loop listener that forks the process before executing code. * * This is awesome, as the session won't die prematurely if user input includes * a fatal error, such as redeclaring a class or function. */ class ProcessForker extends AbstractListener { private $savegame; private $up; private static $pcntlFunctions = [ 'pcntl_fork', 'pcntl_signal_dispatch', 'pcntl_signal', 'pcntl_waitpid', 'pcntl_wexitstatus', ]; private static $posixFunctions = [ 'posix_getpid', 'posix_kill', ]; /** * Process forker is supported if pcntl and posix extensions are available. */ public static function isSupported(): bool { return self::isPcntlSupported() && !self::disabledPcntlFunctions() && self::isPosixSupported() && !self::disabledPosixFunctions(); } /** * Verify that all required pcntl functions are, in fact, available. */ public static function isPcntlSupported(): bool { foreach (self::$pcntlFunctions as $func) { if (!\function_exists($func)) { return false; } } return true; } /** * Check whether required pcntl functions are disabled. */ public static function disabledPcntlFunctions() { return self::checkDisabledFunctions(self::$pcntlFunctions); } /** * Verify that all required posix functions are, in fact, available. */ public static function isPosixSupported(): bool { foreach (self::$posixFunctions as $func) { if (!\function_exists($func)) { return false; } } return true; } /** * Check whether required posix functions are disabled. */ public static function disabledPosixFunctions() { return self::checkDisabledFunctions(self::$posixFunctions); } private static function checkDisabledFunctions(array $functions): array { return \array_values(\array_intersect($functions, \array_map('strtolower', \array_map('trim', \explode(',', \ini_get('disable_functions')))))); } /** * Forks into a main and a loop process. * * The loop process will handle the evaluation of all instructions, then * return its state via a socket upon completion. * * @param Shell $shell */ public function beforeRun(Shell $shell) { list($up, $down) = \stream_socket_pair(\STREAM_PF_UNIX, \STREAM_SOCK_STREAM, \STREAM_IPPROTO_IP); if (!$up) { throw new \RuntimeException('Unable to create socket pair'); } $pid = \pcntl_fork(); if ($pid < 0) { throw new \RuntimeException('Unable to start execution loop'); } elseif ($pid > 0) { // This is the main thread. We'll just wait for a while. // We won't be needing this one. \fclose($up); // Wait for a return value from the loop process. $read = [$down]; $write = null; $except = null; do { $n = @\stream_select($read, $write, $except, null); if ($n === 0) { throw new \RuntimeException('Process timed out waiting for execution loop'); } if ($n === false) { $err = \error_get_last(); if (!isset($err['message']) || \stripos($err['message'], 'interrupted system call') === false) { $msg = $err['message'] ? \sprintf('Error waiting for execution loop: %s', $err['message']) : 'Error waiting for execution loop'; throw new \RuntimeException($msg); } } } while ($n < 1); $content = \stream_get_contents($down); \fclose($down); if ($content) { $shell->setScopeVariables(@\unserialize($content)); } throw new BreakException('Exiting main thread'); } // This is the child process. It's going to do all the work. if (!@\cli_set_process_title('psysh (loop)')) { // Fall back to `setproctitle` if that wasn't succesful. if (\function_exists('setproctitle')) { @\setproctitle('psysh (loop)'); } } // We won't be needing this one. \fclose($down); // Save this; we'll need to close it in `afterRun` $this->up = $up; } /** * Create a savegame at the start of each loop iteration. * * @param Shell $shell */ public function beforeLoop(Shell $shell) { $this->createSavegame(); } /** * Clean up old savegames at the end of each loop iteration. * * @param Shell $shell */ public function afterLoop(Shell $shell) { // if there's an old savegame hanging around, let's kill it. if (isset($this->savegame)) { \posix_kill($this->savegame, \SIGKILL); \pcntl_signal_dispatch(); } } /** * After the REPL session ends, send the scope variables back up to the main * thread (if this is a child thread). * * @param Shell $shell */ public function afterRun(Shell $shell) { // We're a child thread. Send the scope variables back up to the main thread. if (isset($this->up)) { \fwrite($this->up, $this->serializeReturn($shell->getScopeVariables(false))); \fclose($this->up); \posix_kill(\posix_getpid(), \SIGKILL); } } /** * Create a savegame fork. * * The savegame contains the current execution state, and can be resumed in * the event that the worker dies unexpectedly (for example, by encountering * a PHP fatal error). */ private function createSavegame() { // the current process will become the savegame $this->savegame = \posix_getpid(); $pid = \pcntl_fork(); if ($pid < 0) { throw new \RuntimeException('Unable to create savegame fork'); } elseif ($pid > 0) { // we're the savegame now... let's wait and see what happens \pcntl_waitpid($pid, $status); // worker exited cleanly, let's bail if (!\pcntl_wexitstatus($status)) { \posix_kill(\posix_getpid(), \SIGKILL); } // worker didn't exit cleanly, we'll need to have another go $this->createSavegame(); } } /** * Serialize all serializable return values. * * A naïve serialization will run into issues if there is a Closure or * SimpleXMLElement (among other things) in scope when exiting the execution * loop. We'll just ignore these unserializable classes, and serialize what * we can. * * @param array $return */ private function serializeReturn(array $return): string { $serializable = []; foreach ($return as $key => $value) { // No need to return magic variables if (Context::isSpecialVariableName($key)) { continue; } // Resources and Closures don't error, but they don't serialize well either. if (\is_resource($value) || $value instanceof \Closure) { continue; } if (\version_compare(\PHP_VERSION, '8.1', '>=') && $value instanceof \UnitEnum) { // Enums defined in the REPL session can't be unserialized. $ref = new \ReflectionObject($value); if (\strpos($ref->getFileName(), ": eval()'d code") !== false) { continue; } } try { @\serialize($value); $serializable[$key] = $value; } catch (\Throwable $e) { // we'll just ignore this one... } } return @\serialize($serializable); } } psysh/src/ExecutionLoop/Listener.php 0000644 00000003452 15024771425 0013607 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\ExecutionLoop; use Psy\Shell; /** * Execution Loop Listener interface. */ interface Listener { /** * Determines whether this listener should be active. */ public static function isSupported(): bool; /** * Called once before the REPL session starts. * * @param Shell $shell */ public function beforeRun(Shell $shell); /** * Called at the start of each loop. * * @param Shell $shell */ public function beforeLoop(Shell $shell); /** * Called on user input. * * Return a new string to override or rewrite user input. * * @param Shell $shell * @param string $input * * @return string|null User input override */ public function onInput(Shell $shell, string $input); /** * Called before executing user code. * * Return a new string to override or rewrite user code. * * Note that this is run *after* the Code Cleaner, so if you return invalid * or unsafe PHP here, it'll be executed without any of the safety Code * Cleaner provides. This comes with the big kid warranty :) * * @param Shell $shell * @param string $code * * @return string|null User code override */ public function onExecute(Shell $shell, string $code); /** * Called at the end of each loop. * * @param Shell $shell */ public function afterLoop(Shell $shell); /** * Called once after the REPL session ends. * * @param Shell $shell */ public function afterRun(Shell $shell); } psysh/src/ExecutionLoop/AbstractListener.php 0000644 00000001676 15024771425 0015301 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\ExecutionLoop; use Psy\Shell; /** * Abstract Execution Loop Listener class. */ abstract class AbstractListener implements Listener { /** * {@inheritdoc} */ public function beforeRun(Shell $shell) { } /** * {@inheritdoc} */ public function beforeLoop(Shell $shell) { } /** * {@inheritdoc} */ public function onInput(Shell $shell, string $input) { } /** * {@inheritdoc} */ public function onExecute(Shell $shell, string $code) { } /** * {@inheritdoc} */ public function afterLoop(Shell $shell) { } /** * {@inheritdoc} */ public function afterRun(Shell $shell) { } } psysh/src/ExecutionLoop/RunkitReloader.php 0000644 00000007137 15024771425 0014760 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\ExecutionLoop; use Psy\Exception\ParseErrorException; use Psy\ParserFactory; use Psy\Shell; /** * A runkit-based code reloader, which is pretty much magic. */ class RunkitReloader extends AbstractListener { private $parser; private $timestamps = []; /** * Only enabled if Runkit is installed. */ public static function isSupported(): bool { // runkit_import was removed in runkit7-4.0.0a1 return \extension_loaded('runkit') || \extension_loaded('runkit7') && \function_exists('runkit_import'); } /** * Construct a Runkit Reloader. * * @todo Pass in Parser Factory instance for dependency injection? */ public function __construct() { $parserFactory = new ParserFactory(); $this->parser = $parserFactory->createParser(); } /** * Reload code on input. * * @param Shell $shell * @param string $input */ public function onInput(Shell $shell, string $input) { $this->reload($shell); } /** * Look through included files and update anything with a new timestamp. * * @param Shell $shell */ private function reload(Shell $shell) { \clearstatcache(); $modified = []; foreach (\get_included_files() as $file) { $timestamp = \filemtime($file); if (!isset($this->timestamps[$file])) { $this->timestamps[$file] = $timestamp; continue; } if ($this->timestamps[$file] === $timestamp) { continue; } if (!$this->lintFile($file)) { $msg = \sprintf('Modified file "%s" could not be reloaded', $file); $shell->writeException(new ParseErrorException($msg)); continue; } $modified[] = $file; $this->timestamps[$file] = $timestamp; } // switch (count($modified)) { // case 0: // return; // case 1: // printf("Reloading modified file: \"%s\"\n", str_replace(getcwd(), '.', $file)); // break; // default: // printf("Reloading %d modified files\n", count($modified)); // break; // } foreach ($modified as $file) { $flags = ( RUNKIT_IMPORT_FUNCTIONS | RUNKIT_IMPORT_CLASSES | RUNKIT_IMPORT_CLASS_METHODS | RUNKIT_IMPORT_CLASS_CONSTS | RUNKIT_IMPORT_CLASS_PROPS | RUNKIT_IMPORT_OVERRIDE ); // these two const cannot be used with RUNKIT_IMPORT_OVERRIDE in runkit7 if (\extension_loaded('runkit7')) { $flags &= ~RUNKIT_IMPORT_CLASS_PROPS & ~RUNKIT_IMPORT_CLASS_STATIC_PROPS; runkit7_import($file, $flags); } else { runkit_import($file, $flags); } } } /** * Should this file be re-imported? * * Use PHP-Parser to ensure that the file is valid PHP. * * @param string $file */ private function lintFile(string $file): bool { // first try to parse it try { $this->parser->parse(\file_get_contents($file)); } catch (\Throwable $e) { return false; } return true; } } psysh/src/Shell.php 0000644 00000136316 15024771425 0010302 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; use Psy\CodeCleaner\NoReturnValue; use Psy\Exception\BreakException; use Psy\Exception\ErrorException; use Psy\Exception\Exception as PsyException; use Psy\Exception\RuntimeException; use Psy\Exception\ThrowUpException; use Psy\ExecutionLoop\ProcessForker; use Psy\ExecutionLoop\RunkitReloader; use Psy\Formatter\TraceFormatter; use Psy\Input\ShellInput; use Psy\Input\SilentInput; use Psy\Output\ShellOutput; use Psy\TabCompletion\Matcher; use Psy\VarDumper\PresenterAware; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command as BaseCommand; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\ArrayInput; 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\Input\StringInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; /** * The Psy Shell application. * * Usage: * * $shell = new Shell; * $shell->run(); * * @author Justin Hileman <justin@justinhileman.info> */ class Shell extends Application { const VERSION = 'v0.11.13'; /** @deprecated */ const PROMPT = '>>> '; /** @deprecated */ const BUFF_PROMPT = '... '; /** @deprecated */ const REPLAY = '--> '; /** @deprecated */ const RETVAL = '=> '; private $config; private $cleaner; private $output; private $originalVerbosity; private $readline; private $inputBuffer; private $code; private $codeBuffer; private $codeBufferOpen; private $codeStack; private $stdoutBuffer; private $context; private $includes; private $outputWantsNewline = false; private $loopListeners; private $autoCompleter; private $matchers = []; private $commandsMatcher; private $lastExecSuccess = true; private $nonInteractive = false; private $errorReporting; /** * Create a new Psy Shell. * * @param Configuration|null $config (default: null) */ public function __construct(Configuration $config = null) { $this->config = $config ?: new Configuration(); $this->cleaner = $this->config->getCodeCleaner(); $this->context = new Context(); $this->includes = []; $this->readline = $this->config->getReadline(); $this->inputBuffer = []; $this->codeStack = []; $this->stdoutBuffer = ''; $this->loopListeners = $this->getDefaultLoopListeners(); parent::__construct('Psy Shell', self::VERSION); $this->config->setShell($this); // Register the current shell session's config with \Psy\info \Psy\info($this->config); } /** * Check whether the first thing in a backtrace is an include call. * * This is used by the psysh bin to decide whether to start a shell on boot, * or to simply autoload the library. */ public static function isIncluded(array $trace): bool { $isIncluded = isset($trace[0]['function']) && \in_array($trace[0]['function'], ['require', 'include', 'require_once', 'include_once']); // Detect Composer PHP bin proxies. if ($isIncluded && \array_key_exists('_composer_autoload_path', $GLOBALS) && \preg_match('{[\\\\/]psysh$}', $trace[0]['file'])) { // If we're in a bin proxy, we'll *always* see one include, but we // care if we see a second immediately after that. return isset($trace[1]['function']) && \in_array($trace[1]['function'], ['require', 'include', 'require_once', 'include_once']); } return $isIncluded; } /** * Check if the currently running PsySH bin is a phar archive. */ public static function isPhar(): bool { return \class_exists("\Phar") && \Phar::running() !== '' && \strpos(__FILE__, \Phar::running(true)) === 0; } /** * Invoke a Psy Shell from the current context. * * @see Psy\debug * @deprecated will be removed in 1.0. Use \Psy\debug instead * * @param array $vars Scope variables from the calling context (default: []) * @param object|string $bindTo Bound object ($this) or class (self) value for the shell * * @return array Scope variables from the debugger session */ public static function debug(array $vars = [], $bindTo = null): array { return \Psy\debug($vars, $bindTo); } /** * Adds a command object. * * {@inheritdoc} * * @param BaseCommand $command A Symfony Console Command object * * @return BaseCommand The registered command */ public function add(BaseCommand $command): BaseCommand { if ($ret = parent::add($command)) { if ($ret instanceof ContextAware) { $ret->setContext($this->context); } if ($ret instanceof PresenterAware) { $ret->setPresenter($this->config->getPresenter()); } if (isset($this->commandsMatcher)) { $this->commandsMatcher->setCommands($this->all()); } } return $ret; } /** * Gets the default input definition. * * @return InputDefinition An InputDefinition instance */ protected function getDefaultInputDefinition(): InputDefinition { return new InputDefinition([ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), ]); } /** * Gets the default commands that should always be available. * * @return array An array of default Command instances */ protected function getDefaultCommands(): array { $sudo = new Command\SudoCommand(); $sudo->setReadline($this->readline); $hist = new Command\HistoryCommand(); $hist->setReadline($this->readline); return [ new Command\HelpCommand(), new Command\ListCommand(), new Command\DumpCommand(), new Command\DocCommand(), new Command\ShowCommand(), new Command\WtfCommand(), new Command\WhereamiCommand(), new Command\ThrowUpCommand(), new Command\TimeitCommand(), new Command\TraceCommand(), new Command\BufferCommand(), new Command\ClearCommand(), new Command\EditCommand($this->config->getRuntimeDir()), // new Command\PsyVersionCommand(), $sudo, $hist, new Command\ExitCommand(), ]; } /** * @return Matcher\AbstractMatcher[] */ protected function getDefaultMatchers(): array { // Store the Commands Matcher for later. If more commands are added, // we'll update the Commands Matcher too. $this->commandsMatcher = new Matcher\CommandsMatcher($this->all()); return [ $this->commandsMatcher, new Matcher\KeywordsMatcher(), new Matcher\VariablesMatcher(), new Matcher\ConstantsMatcher(), new Matcher\FunctionsMatcher(), new Matcher\ClassNamesMatcher(), new Matcher\ClassMethodsMatcher(), new Matcher\ClassAttributesMatcher(), new Matcher\ObjectMethodsMatcher(), new Matcher\ObjectAttributesMatcher(), new Matcher\ClassMethodDefaultParametersMatcher(), new Matcher\ObjectMethodDefaultParametersMatcher(), new Matcher\FunctionDefaultParametersMatcher(), ]; } /** * @deprecated Nothing should use this anymore */ protected function getTabCompletionMatchers() { @\trigger_error('getTabCompletionMatchers is no longer used', \E_USER_DEPRECATED); } /** * Gets the default command loop listeners. * * @return array An array of Execution Loop Listener instances */ protected function getDefaultLoopListeners(): array { $listeners = []; if (ProcessForker::isSupported() && $this->config->usePcntl()) { $listeners[] = new ProcessForker(); } if (RunkitReloader::isSupported()) { $listeners[] = new RunkitReloader(); } return $listeners; } /** * Add tab completion matchers. * * @param array $matchers */ public function addMatchers(array $matchers) { $this->matchers = \array_merge($this->matchers, $matchers); if (isset($this->autoCompleter)) { $this->addMatchersToAutoCompleter($matchers); } } /** * @deprecated Call `addMatchers` instead * * @param array $matchers */ public function addTabCompletionMatchers(array $matchers) { $this->addMatchers($matchers); } /** * Set the Shell output. * * @param OutputInterface $output */ public function setOutput(OutputInterface $output) { $this->output = $output; $this->originalVerbosity = $output->getVerbosity(); } /** * Runs PsySH. * * @param InputInterface|null $input An Input instance * @param OutputInterface|null $output An Output instance * * @return int 0 if everything went fine, or an error code */ public function run(InputInterface $input = null, OutputInterface $output = null): int { // We'll just ignore the input passed in, and set up our own! $input = new ArrayInput([]); if ($output === null) { $output = $this->config->getOutput(); } $this->setAutoExit(false); $this->setCatchExceptions(false); try { return parent::run($input, $output); } catch (\Throwable $e) { $this->writeException($e); } return 1; } /** * Runs PsySH. * * @throws \Throwable if thrown via the `throw-up` command * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return int 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output): int { $this->setOutput($output); $this->resetCodeBuffer(); if ($input->isInteractive()) { // @todo should it be possible to have raw output in an interactive run? return $this->doInteractiveRun(); } else { return $this->doNonInteractiveRun($this->config->rawOutput()); } } /** * Run PsySH in interactive mode. * * Initializes tab completion and readline history, then spins up the * execution loop. * * @throws \Throwable if thrown via the `throw-up` command * * @return int 0 if everything went fine, or an error code */ private function doInteractiveRun(): int { $this->initializeTabCompletion(); $this->readline->readHistory(); $this->output->writeln($this->getHeader()); $this->writeVersionInfo(); $this->writeStartupMessage(); try { $this->beforeRun(); $this->loadIncludes(); $loop = new ExecutionLoopClosure($this); $loop->execute(); $this->afterRun(); } catch (ThrowUpException $e) { throw $e->getPrevious(); } catch (BreakException $e) { // The ProcessForker throws a BreakException to finish the main thread. } return 0; } /** * Run PsySH in non-interactive mode. * * Note that this isn't very useful unless you supply "include" arguments at * the command line, or code via stdin. * * @param bool $rawOutput * * @return int 0 if everything went fine, or an error code */ private function doNonInteractiveRun(bool $rawOutput): int { $this->nonInteractive = true; // If raw output is enabled (or output is piped) we don't want startup messages. if (!$rawOutput && !$this->config->outputIsPiped()) { $this->output->writeln($this->getHeader()); $this->writeVersionInfo(); $this->writeStartupMessage(); } $this->beforeRun(); $this->loadIncludes(); // For non-interactive execution, read only from the input buffer or from piped input. // Otherwise it'll try to readline and hang, waiting for user input with no indication of // what's holding things up. if (!empty($this->inputBuffer) || $this->config->inputIsPiped()) { $this->getInput(false); } if ($this->hasCode()) { $ret = $this->execute($this->flushCode()); $this->writeReturnValue($ret, $rawOutput); } $this->afterRun(); $this->nonInteractive = false; return 0; } /** * Configures the input and output instances based on the user arguments and options. */ protected function configureIO(InputInterface $input, OutputInterface $output) { // @todo overrides via environment variables (or should these happen in config? ... probably config) $input->setInteractive($this->config->getInputInteractive()); if ($this->config->getOutputDecorated() !== null) { $output->setDecorated($this->config->getOutputDecorated()); } $output->setVerbosity($this->config->getOutputVerbosity()); } /** * Load user-defined includes. */ private function loadIncludes() { // Load user-defined includes $load = function (self $__psysh__) { \set_error_handler([$__psysh__, 'handleError']); foreach ($__psysh__->getIncludes() as $__psysh_include__) { try { include_once $__psysh_include__; } catch (\Exception $_e) { $__psysh__->writeException($_e); } } \restore_error_handler(); unset($__psysh_include__); // Override any new local variables with pre-defined scope variables \extract($__psysh__->getScopeVariables(false)); // ... then add the whole mess of variables back. $__psysh__->setScopeVariables(\get_defined_vars()); }; $load($this); } /** * Read user input. * * This will continue fetching user input until the code buffer contains * valid code. * * @throws BreakException if user hits Ctrl+D * * @param bool $interactive */ public function getInput(bool $interactive = true) { $this->codeBufferOpen = false; do { // reset output verbosity (in case it was altered by a subcommand) $this->output->setVerbosity($this->originalVerbosity); $input = $this->readline(); /* * Handle Ctrl+D. It behaves differently in different cases: * * 1) In an expression, like a function or "if" block, clear the input buffer * 2) At top-level session, behave like the exit command * 3) When non-interactive, return, because that's the end of stdin */ if ($input === false) { if (!$interactive) { return; } $this->output->writeln(''); if ($this->hasCode()) { $this->resetCodeBuffer(); } else { throw new BreakException('Ctrl+D'); } } // handle empty input if (\trim($input) === '' && !$this->codeBufferOpen) { continue; } $input = $this->onInput($input); // If the input isn't in an open string or comment, check for commands to run. if ($this->hasCommand($input) && !$this->inputInOpenStringOrComment($input)) { $this->addHistory($input); $this->runCommand($input); continue; } $this->addCode($input); } while (!$interactive || !$this->hasValidCode()); } /** * Check whether the code buffer (plus current input) is in an open string or comment. * * @param string $input current line of input * * @return bool true if the input is in an open string or comment */ private function inputInOpenStringOrComment(string $input): bool { if (!$this->hasCode()) { return false; } $code = $this->codeBuffer; $code[] = $input; $tokens = @\token_get_all('<?php '.\implode("\n", $code)); $last = \array_pop($tokens); return $last === '"' || $last === '`' || (\is_array($last) && \in_array($last[0], [\T_ENCAPSED_AND_WHITESPACE, \T_START_HEREDOC, \T_COMMENT])); } /** * Run execution loop listeners before the shell session. */ protected function beforeRun() { foreach ($this->loopListeners as $listener) { $listener->beforeRun($this); } } /** * Run execution loop listeners at the start of each loop. */ public function beforeLoop() { foreach ($this->loopListeners as $listener) { $listener->beforeLoop($this); } } /** * Run execution loop listeners on user input. * * @param string $input */ public function onInput(string $input): string { foreach ($this->loopListeners as $listeners) { if (($return = $listeners->onInput($this, $input)) !== null) { $input = $return; } } return $input; } /** * Run execution loop listeners on code to be executed. * * @param string $code */ public function onExecute(string $code): string { $this->errorReporting = \error_reporting(); foreach ($this->loopListeners as $listener) { if (($return = $listener->onExecute($this, $code)) !== null) { $code = $return; } } $output = $this->output; if ($output instanceof ConsoleOutput) { $output = $output->getErrorOutput(); } $output->writeln(\sprintf('<aside>%s</aside>', OutputFormatter::escape($code)), ConsoleOutput::VERBOSITY_DEBUG); return $code; } /** * Run execution loop listeners after each loop. */ public function afterLoop() { foreach ($this->loopListeners as $listener) { $listener->afterLoop($this); } } /** * Run execution loop listers after the shell session. */ protected function afterRun() { foreach ($this->loopListeners as $listener) { $listener->afterRun($this); } } /** * Set the variables currently in scope. * * @param array $vars */ public function setScopeVariables(array $vars) { $this->context->setAll($vars); } /** * Return the set of variables currently in scope. * * @param bool $includeBoundObject Pass false to exclude 'this'. If you're * passing the scope variables to `extract` * in PHP 7.1+, you _must_ exclude 'this' * * @return array Associative array of scope variables */ public function getScopeVariables(bool $includeBoundObject = true): array { $vars = $this->context->getAll(); if (!$includeBoundObject) { unset($vars['this']); } return $vars; } /** * Return the set of magic variables currently in scope. * * @param bool $includeBoundObject Pass false to exclude 'this'. If you're * passing the scope variables to `extract` * in PHP 7.1+, you _must_ exclude 'this' * * @return array Associative array of magic scope variables */ public function getSpecialScopeVariables(bool $includeBoundObject = true): array { $vars = $this->context->getSpecialVariables(); if (!$includeBoundObject) { unset($vars['this']); } return $vars; } /** * Return the set of variables currently in scope which differ from the * values passed as $currentVars. * * This is used inside the Execution Loop Closure to pick up scope variable * changes made by commands while the loop is running. * * @param array $currentVars * * @return array Associative array of scope variables which differ from $currentVars */ public function getScopeVariablesDiff(array $currentVars): array { $newVars = []; foreach ($this->getScopeVariables(false) as $key => $value) { if (!\array_key_exists($key, $currentVars) || $currentVars[$key] !== $value) { $newVars[$key] = $value; } } return $newVars; } /** * Get the set of unused command-scope variable names. * * @return array Array of unused variable names */ public function getUnusedCommandScopeVariableNames(): array { return $this->context->getUnusedCommandScopeVariableNames(); } /** * Get the set of variable names currently in scope. * * @return array Array of variable names */ public function getScopeVariableNames(): array { return \array_keys($this->context->getAll()); } /** * Get a scope variable value by name. * * @param string $name * * @return mixed */ public function getScopeVariable(string $name) { return $this->context->get($name); } /** * Set the bound object ($this variable) for the interactive shell. * * @param object|null $boundObject */ public function setBoundObject($boundObject) { $this->context->setBoundObject($boundObject); } /** * Get the bound object ($this variable) for the interactive shell. * * @return object|null */ public function getBoundObject() { return $this->context->getBoundObject(); } /** * Set the bound class (self) for the interactive shell. * * @param string|null $boundClass */ public function setBoundClass($boundClass) { $this->context->setBoundClass($boundClass); } /** * Get the bound class (self) for the interactive shell. * * @return string|null */ public function getBoundClass() { return $this->context->getBoundClass(); } /** * Add includes, to be parsed and executed before running the interactive shell. * * @param array $includes */ public function setIncludes(array $includes = []) { $this->includes = $includes; } /** * Get PHP files to be parsed and executed before running the interactive shell. * * @return string[] */ public function getIncludes(): array { return \array_merge($this->config->getDefaultIncludes(), $this->includes); } /** * Check whether this shell's code buffer contains code. * * @return bool True if the code buffer contains code */ public function hasCode(): bool { return !empty($this->codeBuffer); } /** * Check whether the code in this shell's code buffer is valid. * * If the code is valid, the code buffer should be flushed and evaluated. * * @return bool True if the code buffer content is valid */ protected function hasValidCode(): bool { return !$this->codeBufferOpen && $this->code !== false; } /** * Add code to the code buffer. * * @param string $code * @param bool $silent */ public function addCode(string $code, bool $silent = false) { try { // Code lines ending in \ keep the buffer open if (\substr(\rtrim($code), -1) === '\\') { $this->codeBufferOpen = true; $code = \substr(\rtrim($code), 0, -1); } else { $this->codeBufferOpen = false; } $this->codeBuffer[] = $silent ? new SilentInput($code) : $code; $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons()); } catch (\Throwable $e) { // Add failed code blocks to the readline history. $this->addCodeBufferToHistory(); throw $e; } } /** * Set the code buffer. * * This is mostly used by `Shell::execute`. Any existing code in the input * buffer is pushed onto a stack and will come back after this new code is * executed. * * @throws \InvalidArgumentException if $code isn't a complete statement * * @param string $code * @param bool $silent */ private function setCode(string $code, bool $silent = false) { if ($this->hasCode()) { $this->codeStack[] = [$this->codeBuffer, $this->codeBufferOpen, $this->code]; } $this->resetCodeBuffer(); try { $this->addCode($code, $silent); } catch (\Throwable $e) { $this->popCodeStack(); throw $e; } if (!$this->hasValidCode()) { $this->popCodeStack(); throw new \InvalidArgumentException('Unexpected end of input'); } } /** * Get the current code buffer. * * This is useful for commands which manipulate the buffer. * * @return string[] */ public function getCodeBuffer(): array { return $this->codeBuffer; } /** * Run a Psy Shell command given the user input. * * @throws \InvalidArgumentException if the input is not a valid command * * @param string $input User input string * * @return mixed Who knows? */ protected function runCommand(string $input) { $command = $this->getCommand($input); if (empty($command)) { throw new \InvalidArgumentException('Command not found: '.$input); } $input = new ShellInput(\str_replace('\\', '\\\\', \rtrim($input, " \t\n\r\0\x0B;"))); if ($input->hasParameterOption(['--help', '-h'])) { $helpCommand = $this->get('help'); if (!$helpCommand instanceof Command\HelpCommand) { throw new RuntimeException('Invalid help command instance'); } $helpCommand->setCommand($command); return $helpCommand->run(new StringInput(''), $this->output); } return $command->run($input, $this->output); } /** * Reset the current code buffer. * * This should be run after evaluating user input, catching exceptions, or * on demand by commands such as BufferCommand. */ public function resetCodeBuffer() { $this->codeBuffer = []; $this->code = false; } /** * Inject input into the input buffer. * * This is useful for commands which want to replay history. * * @param string|array $input * @param bool $silent */ public function addInput($input, bool $silent = false) { foreach ((array) $input as $line) { $this->inputBuffer[] = $silent ? new SilentInput($line) : $line; } } /** * Flush the current (valid) code buffer. * * If the code buffer is valid, resets the code buffer and returns the * current code. * * @return string|null PHP code buffer contents */ public function flushCode() { if ($this->hasValidCode()) { $this->addCodeBufferToHistory(); $code = $this->code; $this->popCodeStack(); return $code; } } /** * Reset the code buffer and restore any code pushed during `execute` calls. */ private function popCodeStack() { $this->resetCodeBuffer(); if (empty($this->codeStack)) { return; } list($codeBuffer, $codeBufferOpen, $code) = \array_pop($this->codeStack); $this->codeBuffer = $codeBuffer; $this->codeBufferOpen = $codeBufferOpen; $this->code = $code; } /** * (Possibly) add a line to the readline history. * * Like Bash, if the line starts with a space character, it will be omitted * from history. Note that an entire block multi-line code input will be * omitted iff the first line begins with a space. * * Additionally, if a line is "silent", i.e. it was initially added with the * silent flag, it will also be omitted. * * @param string|SilentInput $line */ private function addHistory($line) { if ($line instanceof SilentInput) { return; } // Skip empty lines and lines starting with a space if (\trim($line) !== '' && \substr($line, 0, 1) !== ' ') { $this->readline->addHistory($line); } } /** * Filter silent input from code buffer, write the rest to readline history. */ private function addCodeBufferToHistory() { $codeBuffer = \array_filter($this->codeBuffer, function ($line) { return !$line instanceof SilentInput; }); $this->addHistory(\implode("\n", $codeBuffer)); } /** * Get the current evaluation scope namespace. * * @see CodeCleaner::getNamespace * * @return string|null Current code namespace */ public function getNamespace() { if ($namespace = $this->cleaner->getNamespace()) { return \implode('\\', $namespace); } } /** * Write a string to stdout. * * This is used by the shell loop for rendering output from evaluated code. * * @param string $out * @param int $phase Output buffering phase */ public function writeStdout(string $out, int $phase = \PHP_OUTPUT_HANDLER_END) { if ($phase & \PHP_OUTPUT_HANDLER_START) { if ($this->output instanceof ShellOutput) { $this->output->startPaging(); } } $isCleaning = $phase & \PHP_OUTPUT_HANDLER_CLEAN; // Incremental flush if ($out !== '' && !$isCleaning) { $this->output->write($out, false, OutputInterface::OUTPUT_RAW); $this->outputWantsNewline = (\substr($out, -1) !== "\n"); $this->stdoutBuffer .= $out; } // Output buffering is done! if ($phase & \PHP_OUTPUT_HANDLER_END) { // Write an extra newline if stdout didn't end with one if ($this->outputWantsNewline) { if (!$this->config->rawOutput() && !$this->config->outputIsPiped()) { $this->output->writeln(\sprintf('<whisper>%s</whisper>', $this->config->useUnicode() ? '⏎' : '\\n')); } else { $this->output->writeln(''); } $this->outputWantsNewline = false; } // Save the stdout buffer as $__out if ($this->stdoutBuffer !== '') { $this->context->setLastStdout($this->stdoutBuffer); $this->stdoutBuffer = ''; } if ($this->output instanceof ShellOutput) { $this->output->stopPaging(); } } } /** * Write a return value to stdout. * * The return value is formatted or pretty-printed, and rendered in a * visibly distinct manner (in this case, as cyan). * * @see self::presentValue * * @param mixed $ret * @param bool $rawOutput Write raw var_export-style values */ public function writeReturnValue($ret, bool $rawOutput = false) { $this->lastExecSuccess = true; if ($ret instanceof NoReturnValue) { return; } $this->context->setReturnValue($ret); if ($rawOutput) { $formatted = \var_export($ret, true); } else { $prompt = $this->config->theme()->returnValue(); $indent = \str_repeat(' ', \strlen($prompt)); $formatted = $this->presentValue($ret); $formattedRetValue = \sprintf('<whisper>%s</whisper>', $prompt); $formatted = $formattedRetValue.\str_replace(\PHP_EOL, \PHP_EOL.$indent, $formatted); } if ($this->output instanceof ShellOutput) { $this->output->page($formatted.\PHP_EOL); } else { $this->output->writeln($formatted); } } /** * Renders a caught Exception or Error. * * Exceptions are formatted according to severity. ErrorExceptions which were * warnings or Strict errors aren't rendered as harshly as real errors. * * Stores $e as the last Exception in the Shell Context. * * @param \Throwable $e An exception or error instance */ public function writeException(\Throwable $e) { // No need to write the break exception during a non-interactive run. if ($e instanceof BreakException && $this->nonInteractive) { $this->resetCodeBuffer(); return; } // Break exceptions don't count :) if (!$e instanceof BreakException) { $this->lastExecSuccess = false; $this->context->setLastException($e); } $output = $this->output; if ($output instanceof ConsoleOutput) { $output = $output->getErrorOutput(); } if (!$this->config->theme()->compact()) { $output->writeln(''); } $output->writeln($this->formatException($e)); if (!$this->config->theme()->compact()) { $output->writeln(''); } // Include an exception trace (as long as this isn't a BreakException). if (!$e instanceof BreakException && $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { $trace = TraceFormatter::formatTrace($e); if (\count($trace) !== 0) { $output->writeln('--'); $output->write($trace, true); $output->writeln(''); } } $this->resetCodeBuffer(); } /** * Check whether the last exec was successful. * * Returns true if a return value was logged rather than an exception. */ public function getLastExecSuccess(): bool { return $this->lastExecSuccess; } /** * Helper for formatting an exception or error for writeException(). * * @todo extract this to somewhere it makes more sense * * @param \Throwable $e */ public function formatException(\Throwable $e): string { $indent = $this->config->theme()->compact() ? '' : ' '; if ($e instanceof BreakException) { return \sprintf('%s<info> INFO </info> %s.', $indent, \rtrim($e->getRawMessage(), '.')); } elseif ($e instanceof PsyException) { $message = $e->getLine() > 1 ? \sprintf('%s in %s on line %d', $e->getRawMessage(), $e->getFile(), $e->getLine()) : \sprintf('%s in %s', $e->getRawMessage(), $e->getFile()); $messageLabel = \strtoupper($this->getMessageLabel($e)); } else { $message = $e->getMessage(); $messageLabel = $this->getMessageLabel($e); } $message = \preg_replace( "#(\\w:)?([\\\\/]\\w+)*[\\\\/]src[\\\\/]Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code#", "eval()'d code", $message ); $message = \str_replace(" in eval()'d code", '', $message); $message = \trim($message); // Ensures the given string ends with punctuation... if (!empty($message) && !\in_array(\substr($message, -1), ['.', '?', '!', ':'])) { $message = "$message."; } // Ensures the given message only contains relative paths... $message = \str_replace(\getcwd().\DIRECTORY_SEPARATOR, '', $message); $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error'; return \sprintf('%s<%s> %s </%s> %s', $indent, $severity, $messageLabel, $severity, OutputFormatter::escape($message)); } /** * Helper for getting an output style for the given ErrorException's level. * * @param \ErrorException $e */ protected function getSeverity(\ErrorException $e): string { $severity = $e->getSeverity(); if ($severity & \error_reporting()) { switch ($severity) { case \E_WARNING: case \E_NOTICE: case \E_CORE_WARNING: case \E_COMPILE_WARNING: case \E_USER_WARNING: case \E_USER_NOTICE: case \E_USER_DEPRECATED: case \E_DEPRECATED: case \E_STRICT: return 'warning'; default: return 'error'; } } else { // Since this is below the user's reporting threshold, it's always going to be a warning. return 'warning'; } } /** * Helper for getting an output style for the given ErrorException's level. * * @param \Throwable $e */ protected function getMessageLabel(\Throwable $e): string { if ($e instanceof \ErrorException) { $severity = $e->getSeverity(); if ($severity & \error_reporting()) { switch ($severity) { case \E_WARNING: return 'Warning'; case \E_NOTICE: return 'Notice'; case \E_CORE_WARNING: return 'Core Warning'; case \E_COMPILE_WARNING: return 'Compile Warning'; case \E_USER_WARNING: return 'User Warning'; case \E_USER_NOTICE: return 'User Notice'; case \E_USER_DEPRECATED: return 'User Deprecated'; case \E_DEPRECATED: return 'Deprecated'; case \E_STRICT: return 'Strict'; } } } if ($e instanceof PsyException) { $exceptionShortName = (new \ReflectionClass($e))->getShortName(); $typeParts = \preg_split('/(?=[A-Z])/', $exceptionShortName); \array_pop($typeParts); // Removes "Exception" return \trim(\strtoupper(\implode(' ', $typeParts))); } return \get_class($e); } /** * Execute code in the shell execution context. * * @param string $code * @param bool $throwExceptions * * @return mixed */ public function execute(string $code, bool $throwExceptions = false) { $this->setCode($code, true); $closure = new ExecutionClosure($this); if ($throwExceptions) { return $closure->execute(); } try { return $closure->execute(); } catch (\Throwable $_e) { $this->writeException($_e); } } /** * Helper for throwing an ErrorException. * * This allows us to: * * set_error_handler([$psysh, 'handleError']); * * Unlike ErrorException::throwException, this error handler respects error * levels; i.e. it logs warnings and notices, but doesn't throw exceptions. * This should probably only be used in the inner execution loop of the * shell, as most of the time a thrown exception is much more useful. * * If the error type matches the `errorLoggingLevel` config, it will be * logged as well, regardless of the `error_reporting` level. * * @see \Psy\Exception\ErrorException::throwException * @see \Psy\Shell::writeException * * @throws \Psy\Exception\ErrorException depending on the error level * * @param int $errno Error type * @param string $errstr Message * @param string $errfile Filename * @param int $errline Line number */ public function handleError($errno, $errstr, $errfile, $errline) { // This is an error worth throwing. // // n.b. Technically we can't handle all of these in userland code, but // we'll list 'em all for good measure if ($errno & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR)) { ErrorException::throwException($errno, $errstr, $errfile, $errline); } // When errors are suppressed, the error_reporting value will differ // from when we started executing. In that case, we won't log errors. $errorsSuppressed = $this->errorReporting !== null && $this->errorReporting !== \error_reporting(); // Otherwise log it and continue. if ($errno & \error_reporting() || (!$errorsSuppressed && ($errno & $this->config->errorLoggingLevel()))) { $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline)); } } /** * Format a value for display. * * @see Presenter::present * * @param mixed $val * * @return string Formatted value */ protected function presentValue($val): string { return $this->config->getPresenter()->present($val); } /** * Get a command (if one exists) for the current input string. * * @param string $input * * @return BaseCommand|null */ protected function getCommand(string $input) { $input = new StringInput($input); if ($name = $input->getFirstArgument()) { return $this->get($name); } } /** * Check whether a command is set for the current input string. * * @param string $input * * @return bool True if the shell has a command for the given input */ protected function hasCommand(string $input): bool { if (\preg_match('/([^\s]+?)(?:\s|$)/A', \ltrim($input), $match)) { return $this->has($match[1]); } return false; } /** * Get the current input prompt. * * @return string|null */ protected function getPrompt() { if ($this->output->isQuiet()) { return null; } $theme = $this->config->theme(); if ($this->hasCode()) { return $theme->bufferPrompt(); } return $theme->prompt(); } /** * Read a line of user input. * * This will return a line from the input buffer (if any exist). Otherwise, * it will ask the user for input. * * If readline is enabled, this delegates to readline. Otherwise, it's an * ugly `fgets` call. * * @param bool $interactive * * @return string|false One line of user input */ protected function readline(bool $interactive = true) { $prompt = $this->config->theme()->replayPrompt(); if (!empty($this->inputBuffer)) { $line = \array_shift($this->inputBuffer); if (!$line instanceof SilentInput) { $this->output->writeln(\sprintf('<whisper>%s</whisper><aside>%s</aside>', $prompt, OutputFormatter::escape($line))); } return $line; } $bracketedPaste = $interactive && $this->config->useBracketedPaste(); if ($bracketedPaste) { \printf("\e[?2004h"); // Enable bracketed paste } $line = $this->readline->readline($this->getPrompt()); if ($bracketedPaste) { \printf("\e[?2004l"); // ... and disable it again } return $line; } /** * Get the shell output header. */ protected function getHeader(): string { return \sprintf('<whisper>%s by Justin Hileman</whisper>', $this->getVersion()); } /** * Get the current version of Psy Shell. * * @deprecated call self::getVersionHeader instead */ public function getVersion(): string { return self::getVersionHeader($this->config->useUnicode()); } /** * Get a pretty header including the current version of Psy Shell. * * @param bool $useUnicode */ public static function getVersionHeader(bool $useUnicode = false): string { $separator = $useUnicode ? '—' : '-'; return \sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, \PHP_VERSION, $separator, \PHP_SAPI); } /** * Get a PHP manual database instance. * * @return \PDO|null */ public function getManualDb() { return $this->config->getManualDb(); } /** * @deprecated Tab completion is provided by the AutoCompleter service */ protected function autocomplete($text) { @\trigger_error('Tab completion is provided by the AutoCompleter service', \E_USER_DEPRECATED); } /** * Initialize tab completion matchers. * * If tab completion is enabled this adds tab completion matchers to the * auto completer and sets context if needed. */ protected function initializeTabCompletion() { if (!$this->config->useTabCompletion()) { return; } $this->autoCompleter = $this->config->getAutoCompleter(); // auto completer needs shell to be linked to configuration because of // the context aware matchers $this->addMatchersToAutoCompleter($this->getDefaultMatchers()); $this->addMatchersToAutoCompleter($this->matchers); $this->autoCompleter->activate(); } /** * Add matchers to the auto completer, setting context if needed. * * @param array $matchers */ private function addMatchersToAutoCompleter(array $matchers) { foreach ($matchers as $matcher) { if ($matcher instanceof ContextAware) { $matcher->setContext($this->context); } $this->autoCompleter->addMatcher($matcher); } } /** * @todo Implement prompt to start update * * @return void|string */ protected function writeVersionInfo() { if (\PHP_SAPI !== 'cli') { return; } try { $client = $this->config->getChecker(); if (!$client->isLatest()) { $this->output->writeln(\sprintf('<whisper>New version is available at psysh.org/psysh (current: %s, latest: %s)</whisper>', self::VERSION, $client->getLatest())); } } catch (\InvalidArgumentException $e) { $this->output->writeln($e->getMessage()); } } /** * Write a startup message if set. */ protected function writeStartupMessage() { $message = $this->config->getStartupMessage(); if ($message !== null && $message !== '') { $this->output->writeln($message); } } } psysh/src/TabCompletion/AutoCompleter.php 0000644 00000005526 15024771425 0014554 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\TabCompletion; use Psy\TabCompletion\Matcher\AbstractMatcher; /** * A readline tab completion service. * * @author Marc Garcia <markcial@gmail.com> */ class AutoCompleter { /** @var Matcher\AbstractMatcher[] */ protected $matchers; /** * Register a tab completion Matcher. * * @param AbstractMatcher $matcher */ public function addMatcher(AbstractMatcher $matcher) { $this->matchers[] = $matcher; } /** * Activate readline tab completion. */ public function activate() { \readline_completion_function([&$this, 'callback']); } /** * Handle readline completion. * * @param string $input Readline current word * @param int $index Current word index * @param array $info readline_info() data * * @return array */ public function processCallback(string $input, int $index, array $info = []): array { // Some (Windows?) systems provide incomplete `readline_info`, so let's // try to work around it. $line = $info['line_buffer']; if (isset($info['end'])) { $line = \substr($line, 0, $info['end']); } if ($line === '' && $input !== '') { $line = $input; } $tokens = \token_get_all('<?php '.$line); // remove whitespaces $tokens = \array_filter($tokens, function ($token) { return !AbstractMatcher::tokenIs($token, AbstractMatcher::T_WHITESPACE); }); // reset index from 0 to remove missing index number $tokens = \array_values($tokens); $matches = []; foreach ($this->matchers as $matcher) { if ($matcher->hasMatched($tokens)) { $matches = \array_merge($matcher->getMatches($tokens), $matches); } } $matches = \array_unique($matches); return !empty($matches) ? $matches : ['']; } /** * The readline_completion_function callback handler. * * @see processCallback * * @param string $input * @param int $index * * @return array */ public function callback(string $input, int $index): array { return $this->processCallback($input, $index, \readline_info()); } /** * Remove readline callback handler on destruct. */ public function __destruct() { // PHP didn't implement the whole readline API when they first switched // to libedit. And they still haven't. if (\function_exists('readline_callback_handler_remove')) { \readline_callback_handler_remove(); } } } psysh/src/TabCompletion/Matcher/AbstractDefaultParametersMatcher.php 0000644 00000004026 15024771425 0021746 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\TabCompletion\Matcher; abstract class AbstractDefaultParametersMatcher extends AbstractContextAwareMatcher { /** * @param \ReflectionParameter[] $reflectionParameters * * @return array */ public function getDefaultParameterCompletion(array $reflectionParameters): array { $parametersProcessed = []; foreach ($reflectionParameters as $parameter) { if (!$parameter->isDefaultValueAvailable()) { return []; } $defaultValue = $this->valueToShortString($parameter->getDefaultValue()); $parametersProcessed[] = \sprintf('$%s = %s', $parameter->getName(), $defaultValue); } if (empty($parametersProcessed)) { return []; } return [\implode(', ', $parametersProcessed).')']; } /** * Takes in the default value of a parameter and turns it into a * string representation that fits inline. * This is not 100% true to the original (newlines are inlined, for example). * * @param mixed $value */ private function valueToShortString($value): string { if (!\is_array($value)) { return \json_encode($value); } $chunks = []; $chunksSequential = []; $allSequential = true; foreach ($value as $key => $item) { $allSequential = $allSequential && \is_numeric($key) && $key === \count($chunksSequential); $keyString = $this->valueToShortString($key); $itemString = $this->valueToShortString($item); $chunks[] = "{$keyString} => {$itemString}"; $chunksSequential[] = $itemString; } $chunksToImplode = $allSequential ? $chunksSequential : $chunks; return '['.\implode(', ', $chunksToImplode).']'; } } psysh/src/TabCompletion/Matcher/ObjectAttributesMatcher.php 0000644 00000003622 15024771425 0020130 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\TabCompletion\Matcher; use InvalidArgumentException; /** * An object attribute tab completion Matcher. * * This matcher provides completion for properties of objects in the current * Context. * * @author Marc Garcia <markcial@gmail.com> */ class ObjectAttributesMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); if (!\is_array($objectToken)) { return []; } $objectName = \str_replace('$', '', $objectToken[1]); try { $object = $this->getVariable($objectName); } catch (InvalidArgumentException $e) { return []; } if (!\is_object($object)) { return []; } return \array_filter( \array_keys(\get_class_vars(\get_class($object))), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } psysh/src/TabCompletion/Matcher/MongoDatabaseMatcher.php 0000644 00000003224 15024771425 0017355 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\TabCompletion\Matcher; /** * A MongoDB tab completion Matcher. * * This matcher provides completion for Mongo collection names. * * @author Marc Garcia <markcial@gmail.com> */ class MongoDatabaseMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); $objectName = \str_replace('$', '', $objectToken[1]); $object = $this->getVariable($objectName); if (!$object instanceof \MongoDB) { return []; } return \array_filter( $object->getCollectionNames(), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } psysh/src/TabCompletion/Matcher/CommandsMatcher.php 0000644 00000004706 15024771425 0016420 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\TabCompletion\Matcher; use Psy\Command\Command; /** * A Psy Command tab completion Matcher. * * This matcher provides completion for all registered Psy Command names and * aliases. * * @author Marc Garcia <markcial@gmail.com> */ class CommandsMatcher extends AbstractMatcher { /** @var string[] */ protected $commands = []; /** * CommandsMatcher constructor. * * @param Command[] $commands */ public function __construct(array $commands) { $this->setCommands($commands); } /** * Set Commands for completion. * * @param Command[] $commands */ public function setCommands(array $commands) { $names = []; foreach ($commands as $command) { $names = \array_merge([$command->getName()], $names); $names = \array_merge($command->getAliases(), $names); } $this->commands = $names; } /** * Check whether a command $name is defined. * * @param string $name */ protected function isCommand(string $name): bool { return \in_array($name, $this->commands); } /** * Check whether input matches a defined command. * * @param string $name */ protected function matchCommand(string $name): bool { foreach ($this->commands as $cmd) { if ($this->startsWith($name, $cmd)) { return true; } } return false; } /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); return \array_filter($this->commands, function ($command) use ($input) { return AbstractMatcher::startsWith($input, $command); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { /* $openTag */ \array_shift($tokens); $command = \array_shift($tokens); switch (true) { case self::tokenIs($command, self::T_STRING) && !$this->isCommand($command[1]) && $this->matchCommand($command[1]) && empty($tokens): return true; } return false; } } psysh/src/TabCompletion/Matcher/ObjectMethodsMatcher.php 0000644 00000004040 15024771425 0017400 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\TabCompletion\Matcher; use InvalidArgumentException; /** * An object method tab completion Matcher. * * This matcher provides completion for methods of objects in the current * Context. * * @author Marc Garcia <markcial@gmail.com> */ class ObjectMethodsMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); if (!\is_array($objectToken)) { return []; } $objectName = \str_replace('$', '', $objectToken[1]); try { $object = $this->getVariable($objectName); } catch (InvalidArgumentException $e) { return []; } if (!\is_object($object)) { return []; } return \array_filter( \get_class_methods($object), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var) && // also check that we do not suggest invoking a super method(__construct, __wakeup, …) !AbstractMatcher::startsWith('__', $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } psysh/src/TabCompletion/Matcher/ClassAttributesMatcher.php 0000644 00000004321 15024771425 0017764 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\TabCompletion\Matcher; /** * A class attribute tab completion Matcher. * * Given a namespace and class, this matcher provides completion for constants * and static properties. * * @author Marc Garcia <markcial@gmail.com> */ class ClassAttributesMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the nekudotayim operator \array_pop($tokens); } $class = $this->getNamespaceAndClass($tokens); try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException $re) { return []; } $vars = \array_merge( \array_map( function ($var) { return '$'.$var; }, \array_keys($reflection->getStaticProperties()) ), \array_keys($reflection->getConstants()) ); return \array_map( function ($name) use ($class) { $chunks = \explode('\\', $class); $className = \array_pop($chunks); return $className.'::'.$name; }, \array_filter( $vars, function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } return false; } } psysh/src/TabCompletion/Matcher/ClassMethodDefaultParametersMatcher.php 0000644 00000003155 15024771425 0022413 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\TabCompletion\Matcher; class ClassMethodDefaultParametersMatcher extends AbstractDefaultParametersMatcher { public function getMatches(array $tokens, array $info = []): array { $openBracket = \array_pop($tokens); $functionName = \array_pop($tokens); $methodOperator = \array_pop($tokens); $class = $this->getNamespaceAndClass($tokens); try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException $e) { // In this case the class apparently does not exist, so we can do nothing return []; } $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC); foreach ($methods as $method) { if ($method->getName() === $functionName[1]) { return $this->getDefaultParameterCompletion($method->getParameters()); } } return []; } public function hasMatched(array $tokens): bool { $openBracket = \array_pop($tokens); if ($openBracket !== '(') { return false; } $functionName = \array_pop($tokens); if (!self::tokenIs($functionName, self::T_STRING)) { return false; } $operator = \array_pop($tokens); if (!self::tokenIs($operator, self::T_DOUBLE_COLON)) { return false; } return true; } } psysh/src/TabCompletion/Matcher/ConstantsMatcher.php 0000644 00000002515 15024771425 0016627 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\TabCompletion\Matcher; /** * A constant name tab completion Matcher. * * This matcher provides completion for all defined constants. * * @author Marc Garcia <markcial@gmail.com> */ class ConstantsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $const = $this->getInput($tokens); return \array_filter(\array_keys(\get_defined_constants()), function ($constant) use ($const) { return AbstractMatcher::startsWith($const, $constant); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_NEW): case self::tokenIs($prevToken, self::T_NS_SEPARATOR): return false; case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): case self::isOperator($token): return true; } return false; } } psysh/src/TabCompletion/Matcher/FunctionDefaultParametersMatcher.php 0000644 00000002314 15024771425 0021766 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\TabCompletion\Matcher; class FunctionDefaultParametersMatcher extends AbstractDefaultParametersMatcher { public function getMatches(array $tokens, array $info = []): array { \array_pop($tokens); // open bracket $functionName = \array_pop($tokens); try { $reflection = new \ReflectionFunction($functionName[1]); } catch (\ReflectionException $e) { return []; } $parameters = $reflection->getParameters(); return $this->getDefaultParameterCompletion($parameters); } public function hasMatched(array $tokens): bool { $openBracket = \array_pop($tokens); if ($openBracket !== '(') { return false; } $functionName = \array_pop($tokens); if (!self::tokenIs($functionName, self::T_STRING)) { return false; } if (!\function_exists($functionName[1])) { return false; } return true; } } psysh/src/TabCompletion/Matcher/ClassMethodsMatcher.php 0000644 00000004277 15024771425 0017253 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\TabCompletion\Matcher; /** * A class method tab completion Matcher. * * Given a namespace and class, this matcher provides completion for static * methods. * * @author Marc Garcia <markcial@gmail.com> */ class ClassMethodsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the nekudotayim operator \array_pop($tokens); } $class = $this->getNamespaceAndClass($tokens); try { $reflection = new \ReflectionClass($class); } catch (\ReflectionException $re) { return []; } if (self::needCompleteClass($tokens[1])) { $methods = $reflection->getMethods(); } else { $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC); } $methods = \array_map(function (\ReflectionMethod $method) { return $method->getName(); }, $methods); return \array_map( function ($name) use ($class) { $chunks = \explode('\\', $class); $className = \array_pop($chunks); return $className.'::'.$name; }, \array_filter($methods, function ($method) use ($input) { return AbstractMatcher::startsWith($input, $method); }) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } return false; } } psysh/src/TabCompletion/Matcher/FunctionsMatcher.php 0000644 00000002604 15024771425 0016622 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\TabCompletion\Matcher; /** * A function name tab completion Matcher. * * This matcher provides completion for all internal and user-defined functions. * * @author Marc Garcia <markcial@gmail.com> */ class FunctionsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $func = $this->getInput($tokens); $functions = \get_defined_functions(); $allFunctions = \array_merge($functions['user'], $functions['internal']); return \array_filter($allFunctions, function ($function) use ($func) { return AbstractMatcher::startsWith($func, $function); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_NEW): return false; case self::hasToken([self::T_OPEN_TAG, self::T_STRING], $token): case self::isOperator($token): return true; } return false; } } psysh/src/TabCompletion/Matcher/MongoClientMatcher.php 0000644 00000003417 15024771425 0017073 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\TabCompletion\Matcher; /** * A MongoDB Client tab completion Matcher. * * This matcher provides completion for MongoClient database names. * * @author Marc Garcia <markcial@gmail.com> */ class MongoClientMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator \array_pop($tokens); } $objectToken = \array_pop($tokens); $objectName = \str_replace('$', '', $objectToken[1]); $object = $this->getVariable($objectName); if (!$object instanceof \MongoClient) { return []; } $list = $object->listDBs(); return \array_filter( \array_map(function ($info) { return $info['name']; }, $list['databases']), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } psysh/src/TabCompletion/Matcher/AbstractContextAwareMatcher.php 0000644 00000002472 15024771425 0020745 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\TabCompletion\Matcher; use Psy\Context; use Psy\ContextAware; /** * An abstract tab completion Matcher which implements ContextAware. * * The AutoCompleter service will inject a Context instance into all * ContextAware Matchers. * * @author Marc Garcia <markcial@gmail.com> */ abstract class AbstractContextAwareMatcher extends AbstractMatcher implements ContextAware { /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * Get a Context variable by name. * * @param string $var Variable name * * @return mixed */ protected function getVariable(string $var) { return $this->context->get($var); } /** * Get all variables in the current Context. * * @return array */ protected function getVariables(): array { return $this->context->getAll(); } } psysh/src/TabCompletion/Matcher/VariablesMatcher.php 0000644 00000002355 15024771425 0016565 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\TabCompletion\Matcher; /** * A variable name tab completion Matcher. * * This matcher provides completion for variable names in the current Context. * * @author Marc Garcia <markcial@gmail.com> */ class VariablesMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $var = \str_replace('$', '', $this->getInput($tokens)); return \array_filter(\array_keys($this->getVariables()), function ($variable) use ($var) { return AbstractMatcher::startsWith($var, $variable); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); switch (true) { case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): case \is_string($token) && $token === '$': case self::isOperator($token): return true; } return false; } } psysh/src/TabCompletion/Matcher/KeywordsMatcher.php 0000644 00000004054 15024771425 0016462 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\TabCompletion\Matcher; /** * A PHP keyword tab completion Matcher. * * This matcher provides completion for all function-like PHP keywords. * * @author Marc Garcia <markcial@gmail.com> */ class KeywordsMatcher extends AbstractMatcher { protected $keywords = [ 'array', 'clone', 'declare', 'die', 'echo', 'empty', 'eval', 'exit', 'include', 'include_once', 'isset', 'list', 'print', 'require', 'require_once', 'unset', ]; protected $mandatoryStartKeywords = [ 'die', 'echo', 'print', 'unset', ]; /** * Get all (completable) PHP keywords. * * @return string[] */ public function getKeywords(): array { return $this->keywords; } /** * Check whether $keyword is a (completable) PHP keyword. * * @param string $keyword */ public function isKeyword(string $keyword): bool { return \in_array($keyword, $this->keywords); } /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); return \array_filter($this->keywords, function ($keyword) use ($input) { return AbstractMatcher::startsWith($input, $keyword); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); switch (true) { case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): // case is_string($token) && $token === '$': case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $prevToken) && self::tokenIs($token, self::T_STRING): case self::isOperator($token): return true; } return false; } } psysh/src/TabCompletion/Matcher/ClassNamesMatcher.php 0000644 00000004600 15024771425 0016701 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\TabCompletion\Matcher; /** * A class name tab completion Matcher. * * This matcher provides completion for all declared classes. * * @author Marc Garcia <markcial@gmail.com> */ class ClassNamesMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = []): array { $class = $this->getNamespaceAndClass($tokens); if ($class !== '' && $class[0] === '\\') { $class = \substr($class, 1, \strlen($class)); } $quotedClass = \preg_quote($class); return \array_map( function ($className) use ($class) { // get the number of namespace separators $nsPos = \substr_count($class, '\\'); $pieces = \explode('\\', $className); // $methods = Mirror::get($class); return \implode('\\', \array_slice($pieces, $nsPos, \count($pieces))); }, \array_filter( \array_merge(\get_declared_classes(), \get_declared_interfaces()), function ($className) use ($quotedClass) { return AbstractMatcher::startsWith($quotedClass, $className); } ) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens): bool { $token = \array_pop($tokens); $prevToken = \array_pop($tokens); $ignoredTokens = [ self::T_INCLUDE, self::T_INCLUDE_ONCE, self::T_REQUIRE, self::T_REQUIRE_ONCE, ]; switch (true) { case self::hasToken([$ignoredTokens], $token): case self::hasToken([$ignoredTokens], $prevToken): case \is_string($token) && $token === '$': return false; case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR, self::T_STRING], $prevToken): case self::hasToken([self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR], $token): case self::hasToken([self::T_OPEN_TAG, self::T_VARIABLE], $token): case self::isOperator($token): return true; } return false; } } psysh/src/TabCompletion/Matcher/AbstractMatcher.php 0000644 00000011506 15024771425 0016416 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\TabCompletion\Matcher; /** * Abstract tab completion Matcher. * * @author Marc Garcia <markcial@gmail.com> */ abstract class AbstractMatcher { /** Syntax types */ const CONSTANT_SYNTAX = '^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$'; const VAR_SYNTAX = '^\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$'; const MISC_OPERATORS = '+-*/^|&'; /** Token values */ const T_OPEN_TAG = 'T_OPEN_TAG'; const T_VARIABLE = 'T_VARIABLE'; const T_OBJECT_OPERATOR = 'T_OBJECT_OPERATOR'; const T_DOUBLE_COLON = 'T_DOUBLE_COLON'; const T_NEW = 'T_NEW'; const T_CLONE = 'T_CLONE'; const T_NS_SEPARATOR = 'T_NS_SEPARATOR'; const T_STRING = 'T_STRING'; const T_NAME_QUALIFIED = 'T_NAME_QUALIFIED'; const T_WHITESPACE = 'T_WHITESPACE'; const T_AND_EQUAL = 'T_AND_EQUAL'; const T_BOOLEAN_AND = 'T_BOOLEAN_AND'; const T_BOOLEAN_OR = 'T_BOOLEAN_OR'; const T_ENCAPSED_AND_WHITESPACE = 'T_ENCAPSED_AND_WHITESPACE'; const T_REQUIRE = 'T_REQUIRE'; const T_REQUIRE_ONCE = 'T_REQUIRE_ONCE'; const T_INCLUDE = 'T_INCLUDE'; const T_INCLUDE_ONCE = 'T_INCLUDE_ONCE'; /** * Check whether this matcher can provide completions for $tokens. * * @param array $tokens Tokenized readline input * * @return false */ public function hasMatched(array $tokens): bool { return false; } /** * Get current readline input word. * * @param array $tokens Tokenized readline input (see token_get_all) */ protected function getInput(array $tokens): string { $var = ''; $firstToken = \array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { $var = $firstToken[1]; } return $var; } /** * Get current namespace and class (if any) from readline input. * * @param array $tokens Tokenized readline input (see token_get_all) */ protected function getNamespaceAndClass(array $tokens): string { $class = ''; while (self::hasToken( [self::T_NS_SEPARATOR, self::T_STRING, self::T_NAME_QUALIFIED], $token = \array_pop($tokens) )) { if (self::needCompleteClass($token)) { continue; } $class = $token[1].$class; } return $class; } /** * Provide tab completion matches for readline input. * * @param array $tokens information substracted with get_token_all * @param array $info readline_info object * * @return array The matches resulting from the query */ abstract public function getMatches(array $tokens, array $info = []): array; /** * Check whether $word starts with $prefix. * * @param string $prefix * @param string $word */ public static function startsWith(string $prefix, string $word): bool { return \preg_match(\sprintf('#^%s#', $prefix), $word); } /** * Check whether $token matches a given syntax pattern. * * @param mixed $token A PHP token (see token_get_all) * @param string $syntax A syntax pattern (default: variable pattern) */ public static function hasSyntax($token, string $syntax = self::VAR_SYNTAX): bool { if (!\is_array($token)) { return false; } $regexp = \sprintf('#%s#', $syntax); return (bool) \preg_match($regexp, $token[1]); } /** * Check whether $token type is $which. * * @param mixed $token A PHP token (see token_get_all) * @param string $which A PHP token type */ public static function tokenIs($token, string $which): bool { if (!\is_array($token)) { return false; } return \token_name($token[0]) === $which; } /** * Check whether $token is an operator. * * @param mixed $token A PHP token (see token_get_all) */ public static function isOperator($token): bool { if (!\is_string($token)) { return false; } return \strpos(self::MISC_OPERATORS, $token) !== false; } public static function needCompleteClass($token): bool { return \in_array($token[1], ['doc', 'ls', 'show']); } /** * Check whether $token type is present in $coll. * * @param array $coll A list of token types * @param mixed $token A PHP token (see token_get_all) */ public static function hasToken(array $coll, $token): bool { if (!\is_array($token)) { return false; } return \in_array(\token_name($token[0]), $coll); } } psysh/src/TabCompletion/Matcher/ObjectMethodDefaultParametersMatcher.php 0000644 00000003375 15024771425 0022560 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\TabCompletion\Matcher; class ObjectMethodDefaultParametersMatcher extends AbstractDefaultParametersMatcher { public function getMatches(array $tokens, array $info = []): array { $openBracket = \array_pop($tokens); $functionName = \array_pop($tokens); $methodOperator = \array_pop($tokens); $objectToken = \array_pop($tokens); if (!\is_array($objectToken)) { return []; } $objectName = \str_replace('$', '', $objectToken[1]); try { $object = $this->getVariable($objectName); $reflection = new \ReflectionObject($object); } catch (\InvalidArgumentException $e) { return []; } catch (\ReflectionException $e) { return []; } $methods = $reflection->getMethods(); foreach ($methods as $method) { if ($method->getName() === $functionName[1]) { return $this->getDefaultParameterCompletion($method->getParameters()); } } return []; } public function hasMatched(array $tokens): bool { $openBracket = \array_pop($tokens); if ($openBracket !== '(') { return false; } $functionName = \array_pop($tokens); if (!self::tokenIs($functionName, self::T_STRING)) { return false; } $operator = \array_pop($tokens); if (!self::tokenIs($operator, self::T_OBJECT_OPERATOR)) { return false; } return true; } } psysh/src/ExecutionLoopClosure.php 0000644 00000005475 15024771425 0013366 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; use Psy\Exception\BreakException; use Psy\Exception\ThrowUpException; /** * The Psy Shell's execution loop scope. * * @todo Once we're on PHP 5.5, we can switch ExecutionClosure to a generator * and get rid of the duplicate closure implementations :) */ class ExecutionLoopClosure extends ExecutionClosure { /** * @param Shell $__psysh__ */ public function __construct(Shell $__psysh__) { $this->setClosure($__psysh__, function () use ($__psysh__) { // Restore execution scope variables \extract($__psysh__->getScopeVariables(false)); while (true) { $__psysh__->beforeLoop(); try { $__psysh__->getInput(); try { // Pull in any new execution scope variables if ($__psysh__->getLastExecSuccess()) { \extract($__psysh__->getScopeVariablesDiff(\get_defined_vars())); } // Buffer stdout; we'll need it later \ob_start([$__psysh__, 'writeStdout'], 1); // Convert all errors to exceptions \set_error_handler([$__psysh__, 'handleError']); // Evaluate the current code buffer $_ = eval($__psysh__->onExecute($__psysh__->flushCode() ?: ExecutionClosure::NOOP_INPUT)); } catch (\Throwable $_e) { // Clean up on our way out. if (\ob_get_level() > 0) { \ob_end_clean(); } throw $_e; } finally { // Won't be needing this anymore \restore_error_handler(); } // Flush stdout (write to shell output, plus save to magic variable) \ob_end_flush(); // Save execution scope variables for next time $__psysh__->setScopeVariables(\get_defined_vars()); $__psysh__->writeReturnValue($_); } catch (BreakException $_e) { $__psysh__->writeException($_e); return; } catch (ThrowUpException $_e) { $__psysh__->writeException($_e); throw $_e; } catch (\Throwable $_e) { $__psysh__->writeException($_e); } $__psysh__->afterLoop(); } }); } } psysh/src/Input/SilentInput.php 0000644 00000001567 15024771425 0012607 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\Input; /** * A simple class used internally by PsySH to represent silent input. * * Silent input is generally used for non-user-generated code, such as the * rewritten user code run by sudo command. Silent input isn't echoed before * evaluating, and it's not added to the readline history. */ class SilentInput { private $inputString; /** * Constructor. * * @param string $inputString */ public function __construct(string $inputString) { $this->inputString = $inputString; } /** * To. String. */ public function __toString(): string { return $this->inputString; } } psysh/src/Input/ShellInput.php 0000644 00000025622 15024771425 0012416 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\Input; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\StringInput; /** * A StringInput subclass specialized for code arguments. */ class ShellInput extends StringInput { private $hasCodeArgument = false; /** * Unlike the parent implementation's tokens, this contains an array of * token/rest pairs, so that code arguments can be handled while parsing. */ private $tokenPairs; private $parsed; /** * Constructor. * * @param string $input An array of parameters from the CLI (in the argv format) */ public function __construct(string $input) { parent::__construct($input); $this->tokenPairs = $this->tokenize($input); } /** * {@inheritdoc} * * @throws \InvalidArgumentException if $definition has CodeArgument before the final argument position */ public function bind(InputDefinition $definition) { $hasCodeArgument = false; if ($definition->getArgumentCount() > 0) { $args = $definition->getArguments(); $lastArg = \array_pop($args); foreach ($args as $arg) { if ($arg instanceof CodeArgument) { $msg = \sprintf('Unexpected CodeArgument before the final position: %s', $arg->getName()); throw new \InvalidArgumentException($msg); } } if ($lastArg instanceof CodeArgument) { $hasCodeArgument = true; } } $this->hasCodeArgument = $hasCodeArgument; return parent::bind($definition); } /** * Tokenizes a string. * * The version of this on StringInput is good, but doesn't handle code * arguments if they're at all complicated. This does :) * * @param string $input The input to tokenize * * @return array An array of token/rest pairs * * @throws \InvalidArgumentException When unable to parse input (should never happen) */ private function tokenize(string $input): array { $tokens = []; $length = \strlen($input); $cursor = 0; while ($cursor < $length) { if (\preg_match('/\s+/A', $input, $match, 0, $cursor)) { } elseif (\preg_match('/([^="\'\s]+?)(=?)('.StringInput::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) { $tokens[] = [ $match[1].$match[2].\stripcslashes(\str_replace(['"\'', '\'"', '\'\'', '""'], '', \substr($match[3], 1, \strlen($match[3]) - 2))), \stripcslashes(\substr($input, $cursor)), ]; } elseif (\preg_match('/'.StringInput::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) { $tokens[] = [ \stripcslashes(\substr($match[0], 1, \strlen($match[0]) - 2)), \stripcslashes(\substr($input, $cursor)), ]; } elseif (\preg_match('/'.StringInput::REGEX_STRING.'/A', $input, $match, 0, $cursor)) { $tokens[] = [ \stripcslashes($match[1]), \stripcslashes(\substr($input, $cursor)), ]; } else { // should never happen // @codeCoverageIgnoreStart throw new \InvalidArgumentException(\sprintf('Unable to parse input near "... %s ..."', \substr($input, $cursor, 10))); // @codeCoverageIgnoreEnd } $cursor += \strlen($match[0]); } return $tokens; } /** * Same as parent, but with some bonus handling for code arguments. */ protected function parse() { $parseOptions = true; $this->parsed = $this->tokenPairs; while (null !== $tokenPair = \array_shift($this->parsed)) { // token is what you'd expect. rest is the remainder of the input // string, including token, and will be used if this is a code arg. list($token, $rest) = $tokenPair; if ($parseOptions && '' === $token) { $this->parseShellArgument($token, $rest); } elseif ($parseOptions && '--' === $token) { $parseOptions = false; } elseif ($parseOptions && 0 === \strpos($token, '--')) { $this->parseLongOption($token); } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { $this->parseShortOption($token); } else { $this->parseShellArgument($token, $rest); } } } /** * Parses an argument, with bonus handling for code arguments. * * @param string $token The current token * @param string $rest The remaining unparsed input, including the current token * * @throws \RuntimeException When too many arguments are given */ private function parseShellArgument(string $token, string $rest) { $c = \count($this->arguments); // if input is expecting another argument, add it if ($this->definition->hasArgument($c)) { $arg = $this->definition->getArgument($c); if ($arg instanceof CodeArgument) { // When we find a code argument, we're done parsing. Add the // remaining input to the current argument and call it a day. $this->parsed = []; $this->arguments[$arg->getName()] = $rest; } else { $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; } return; } // (copypasta) // // @codeCoverageIgnoreStart // if last argument isArray(), append token to last argument if ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { $arg = $this->definition->getArgument($c - 1); $this->arguments[$arg->getName()][] = $token; return; } // unexpected argument $all = $this->definition->getArguments(); if (\count($all)) { throw new \RuntimeException(\sprintf('Too many arguments, expected arguments "%s".', \implode('" "', \array_keys($all)))); } throw new \RuntimeException(\sprintf('No arguments expected, got "%s".', $token)); // @codeCoverageIgnoreEnd } // Everything below this is copypasta from ArgvInput private methods // @codeCoverageIgnoreStart /** * Parses a short option. * * @param string $token The current token */ private function parseShortOption(string $token) { $name = \substr($token, 1); if (\strlen($name) > 1) { if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { // an option with a value (with no space) $this->addShortOption($name[0], \substr($name, 1)); } else { $this->parseShortOptionSet($name); } } else { $this->addShortOption($name, null); } } /** * Parses a short option set. * * @param string $name The current token * * @throws \RuntimeException When option given doesn't exist */ private function parseShortOptionSet(string $name) { $len = \strlen($name); for ($i = 0; $i < $len; $i++) { if (!$this->definition->hasShortcut($name[$i])) { throw new \RuntimeException(\sprintf('The "-%s" option does not exist.', $name[$i])); } $option = $this->definition->getOptionForShortcut($name[$i]); if ($option->acceptValue()) { $this->addLongOption($option->getName(), $i === $len - 1 ? null : \substr($name, $i + 1)); break; } else { $this->addLongOption($option->getName(), null); } } } /** * Parses a long option. * * @param string $token The current token */ private function parseLongOption(string $token) { $name = \substr($token, 2); if (false !== $pos = \strpos($name, '=')) { if (($value = \substr($name, $pos + 1)) === '') { \array_unshift($this->parsed, [$value, null]); } $this->addLongOption(\substr($name, 0, $pos), $value); } else { $this->addLongOption($name, null); } } /** * Adds a short option value. * * @param string $shortcut The short option key * @param mixed $value The value for the option * * @throws \RuntimeException When option given doesn't exist */ private function addShortOption(string $shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new \RuntimeException(\sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @param string $name The long option key * @param mixed $value The value for the option * * @throws \RuntimeException When option given doesn't exist */ private function addLongOption(string $name, $value) { if (!$this->definition->hasOption($name)) { throw new \RuntimeException(\sprintf('The "--%s" option does not exist.', $name)); } $option = $this->definition->getOption($name); if (null !== $value && !$option->acceptValue()) { throw new \RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name)); } if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) { // if option accepts an optional or mandatory argument // let's see if there is one provided $next = \array_shift($this->parsed); $nextToken = $next[0]; if ((isset($nextToken[0]) && '-' !== $nextToken[0]) || \in_array($nextToken, ['', null], true)) { $value = $nextToken; } else { \array_unshift($this->parsed, $next); } } if ($value === null) { if ($option->isValueRequired()) { throw new \RuntimeException(\sprintf('The "--%s" option requires a value.', $name)); } if (!$option->isArray() && !$option->isValueOptional()) { $value = true; } } if ($option->isArray()) { $this->options[$name][] = $value; } else { $this->options[$name] = $value; } } // @codeCoverageIgnoreEnd } psysh/src/Input/CodeArgument.php 0000644 00000003014 15024771425 0012673 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\Input; use Symfony\Component\Console\Input\InputArgument; /** * An input argument for code. * * A CodeArgument must be the final argument of the command. Once all options * and other arguments are used, any remaining input until the end of the string * is considered part of a single CodeArgument, regardless of spaces, quoting, * escaping, etc. * * This means commands can support crazy input like * * parse function() { return "wheee\n"; } * * ... without having to put the code in a quoted string and escape everything. */ class CodeArgument extends InputArgument { /** * Constructor. * * @param string $name The argument name * @param int $mode The argument mode: self::REQUIRED or self::OPTIONAL * @param string $description A description text * @param mixed $default The default value (for self::OPTIONAL mode only) * * @throws \InvalidArgumentException When argument mode is not valid */ public function __construct(string $name, int $mode = null, string $description = '', $default = null) { if ($mode & InputArgument::IS_ARRAY) { throw new \InvalidArgumentException('Argument mode IS_ARRAY is not valid'); } parent::__construct($name, $mode, $description, $default); } } psysh/src/Input/FilterOptions.php 0000644 00000007424 15024771425 0013130 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\Input; use Psy\Exception\ErrorException; use Psy\Exception\RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; /** * Parse, validate and match --grep, --insensitive and --invert command options. */ class FilterOptions { private $filter = false; private $pattern; private $insensitive; private $invert; /** * Get input option definitions for filtering. * * @return InputOption[] */ public static function getOptions(): array { return [ new InputOption('grep', 'G', InputOption::VALUE_REQUIRED, 'Limit to items matching the given pattern (string or regex).'), new InputOption('insensitive', 'i', InputOption::VALUE_NONE, 'Case-insensitive search (requires --grep).'), new InputOption('invert', 'v', InputOption::VALUE_NONE, 'Inverted search (requires --grep).'), ]; } /** * Bind input and prepare filter. * * @param InputInterface $input */ public function bind(InputInterface $input) { $this->validateInput($input); if (!$pattern = $input->getOption('grep')) { $this->filter = false; return; } if (!$this->stringIsRegex($pattern)) { $pattern = '/'.\preg_quote($pattern, '/').'/'; } if ($insensitive = $input->getOption('insensitive')) { $pattern .= 'i'; } $this->validateRegex($pattern); $this->filter = true; $this->pattern = $pattern; $this->insensitive = $insensitive; $this->invert = $input->getOption('invert'); } /** * Check whether the bound input has filter options. */ public function hasFilter(): bool { return $this->filter; } /** * Check whether a string matches the current filter options. * * @param string $string * @param array $matches */ public function match(string $string, array &$matches = null): bool { return $this->filter === false || (\preg_match($this->pattern, $string, $matches) xor $this->invert); } /** * Validate that grep, invert and insensitive input options are consistent. * * @throws RuntimeException if input is invalid * * @param InputInterface $input */ private function validateInput(InputInterface $input) { if (!$input->getOption('grep')) { foreach (['invert', 'insensitive'] as $option) { if ($input->getOption($option)) { throw new RuntimeException('--'.$option.' does not make sense without --grep'); } } } } /** * Check whether a string appears to be a regular expression. * * @param string $string */ private function stringIsRegex(string $string): bool { return \substr($string, 0, 1) === '/' && \substr($string, -1) === '/' && \strlen($string) >= 3; } /** * Validate that $pattern is a valid regular expression. * * @throws RuntimeException if pattern is invalid * * @param string $pattern */ private function validateRegex(string $pattern) { \set_error_handler([ErrorException::class, 'throwException']); try { \preg_match($pattern, ''); } catch (ErrorException $e) { throw new RuntimeException(\str_replace('preg_match(): ', 'Invalid regular expression: ', $e->getRawMessage())); } finally { \restore_error_handler(); } } } psysh/src/Command/WtfCommand.php 0000644 00000007414 15024771425 0012644 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; } } psysh/src/Command/HistoryCommand.php 0000644 00000017011 15024771425 0013537 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); } } psysh/src/Command/TraceCommand.php 0000644 00000005210 15024771425 0013132 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); } } psysh/src/Command/HelpCommand.php 0000644 00000005715 15024771425 0012776 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\Output\ShellOutput; use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Help command. * * Lists available commands, and gives command-specific help when asked nicely. */ class HelpCommand extends Command { private $command; /** * {@inheritdoc} */ protected function configure() { $this ->setName('help') ->setAliases(['?']) ->setDefinition([ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name.', null), ]) ->setDescription('Show a list of commands. Type `help [foo]` for information about [foo].') ->setHelp('My. How meta.'); } /** * Helper for setting a subcommand to retrieve help for. * * @param Command $command */ public function setCommand(Command $command) { $this->command = $command; } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { if ($this->command !== null) { // help for an individual command $output->page($this->command->asText()); $this->command = null; } elseif ($name = $input->getArgument('command_name')) { // help for an individual command $output->page($this->getApplication()->get($name)->asText()); } else { // list available commands $commands = $this->getApplication()->all(); $table = $this->getTable($output); foreach ($commands as $name => $command) { if ($name !== $command->getName()) { continue; } if ($command->getAliases()) { $aliases = \sprintf('<comment>Aliases:</comment> %s', \implode(', ', $command->getAliases())); } else { $aliases = ''; } $table->addRow([ \sprintf('<info>%s</info>', $name), $command->getDescription(), $aliases, ]); } if ($output instanceof ShellOutput) { $output->startPaging(); } if ($table instanceof TableHelper) { $table->render($output); } else { $table->render(); } if ($output instanceof ShellOutput) { $output->stopPaging(); } } return 0; } } psysh/src/Command/ExitCommand.php 0000644 00000002201 15024771425 0013002 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'); } } psysh/src/Command/ListCommand.php 0000644 00000023476 15024771425 0013025 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\Command\ListCommand\ClassConstantEnumerator; use Psy\Command\ListCommand\ClassEnumerator; use Psy\Command\ListCommand\ConstantEnumerator; use Psy\Command\ListCommand\FunctionEnumerator; use Psy\Command\ListCommand\GlobalVariableEnumerator; use Psy\Command\ListCommand\MethodEnumerator; use Psy\Command\ListCommand\PropertyEnumerator; use Psy\Command\ListCommand\VariableEnumerator; use Psy\Exception\RuntimeException; use Psy\Input\CodeArgument; use Psy\Input\FilterOptions; use Psy\Output\ShellOutput; use Psy\VarDumper\Presenter; use Psy\VarDumper\PresenterAware; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * List available local variables, object properties, etc. */ class ListCommand extends ReflectingCommand implements PresenterAware { protected $presenter; protected $enumerators; /** * PresenterAware interface. * * @param Presenter $presenter */ public function setPresenter(Presenter $presenter) { $this->presenter = $presenter; } /** * {@inheritdoc} */ protected function configure() { list($grep, $insensitive, $invert) = FilterOptions::getOptions(); $this ->setName('ls') ->setAliases(['dir']) ->setDefinition([ new CodeArgument('target', CodeArgument::OPTIONAL, 'A target class or object to list.'), new InputOption('vars', '', InputOption::VALUE_NONE, 'Display variables.'), new InputOption('constants', 'c', InputOption::VALUE_NONE, 'Display defined constants.'), new InputOption('functions', 'f', InputOption::VALUE_NONE, 'Display defined functions.'), new InputOption('classes', 'k', InputOption::VALUE_NONE, 'Display declared classes.'), new InputOption('interfaces', 'I', InputOption::VALUE_NONE, 'Display declared interfaces.'), new InputOption('traits', 't', InputOption::VALUE_NONE, 'Display declared traits.'), new InputOption('no-inherit', '', InputOption::VALUE_NONE, 'Exclude inherited methods, properties and constants.'), new InputOption('properties', 'p', InputOption::VALUE_NONE, 'Display class or object properties (public properties by default).'), new InputOption('methods', 'm', InputOption::VALUE_NONE, 'Display class or object methods (public methods by default).'), $grep, $insensitive, $invert, new InputOption('globals', 'g', InputOption::VALUE_NONE, 'Include global variables.'), new InputOption('internal', 'n', InputOption::VALUE_NONE, 'Limit to internal functions and classes.'), new InputOption('user', 'u', InputOption::VALUE_NONE, 'Limit to user-defined constants, functions and classes.'), new InputOption('category', 'C', InputOption::VALUE_REQUIRED, 'Limit to constants in a specific category (e.g. "date").'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'), new InputOption('long', 'l', InputOption::VALUE_NONE, 'List in long format: includes class names and method signatures.'), ]) ->setDescription('List local, instance or class variables, methods and constants.') ->setHelp( <<<'HELP' List variables, constants, classes, interfaces, traits, functions, methods, and properties. Called without options, this will return a list of variables currently in scope. If a target object is provided, list properties, constants and methods of that target. If a class, interface or trait name is passed instead, list constants and methods on that class. e.g. <return>>>> ls</return> <return>>>> ls $foo</return> <return>>>> ls -k --grep mongo -i</return> <return>>>> ls -al ReflectionClass</return> <return>>>> ls --constants --category date</return> <return>>>> ls -l --functions --grep /^array_.*/</return> <return>>>> ls -l --properties new DateTime()</return> HELP ); } /** * {@inheritdoc} * * @return int 0 if everything went fine, or an exit code */ protected function execute(InputInterface $input, OutputInterface $output) { $this->validateInput($input); $this->initEnumerators(); $method = $input->getOption('long') ? 'writeLong' : 'write'; if ($target = $input->getArgument('target')) { list($target, $reflector) = $this->getTargetAndReflector($target); } else { $reflector = null; } // @todo something cleaner than this :-/ if ($output instanceof ShellOutput && $input->getOption('long')) { $output->startPaging(); } foreach ($this->enumerators as $enumerator) { $this->$method($output, $enumerator->enumerate($input, $reflector, $target)); } if ($output instanceof ShellOutput && $input->getOption('long')) { $output->stopPaging(); } // Set some magic local variables if ($reflector !== null) { $this->setCommandScopeVariables($reflector); } return 0; } /** * Initialize Enumerators. */ protected function initEnumerators() { if (!isset($this->enumerators)) { $mgr = $this->presenter; $this->enumerators = [ new ClassConstantEnumerator($mgr), new ClassEnumerator($mgr), new ConstantEnumerator($mgr), new FunctionEnumerator($mgr), new GlobalVariableEnumerator($mgr), new PropertyEnumerator($mgr), new MethodEnumerator($mgr), new VariableEnumerator($mgr, $this->context), ]; } } /** * Write the list items to $output. * * @param OutputInterface $output * @param array $result List of enumerated items */ protected function write(OutputInterface $output, array $result) { if (\count($result) === 0) { return; } foreach ($result as $label => $items) { $names = \array_map([$this, 'formatItemName'], $items); $output->writeln(\sprintf('<strong>%s</strong>: %s', $label, \implode(', ', $names))); } } /** * Write the list items to $output. * * Items are listed one per line, and include the item signature. * * @param OutputInterface $output * @param array $result List of enumerated items */ protected function writeLong(OutputInterface $output, array $result) { if (\count($result) === 0) { return; } $table = $this->getTable($output); foreach ($result as $label => $items) { $output->writeln(''); $output->writeln(\sprintf('<strong>%s:</strong>', $label)); $table->setRows([]); foreach ($items as $item) { $table->addRow([$this->formatItemName($item), $item['value']]); } if ($table instanceof TableHelper) { $table->render($output); } else { $table->render(); } } } /** * Format an item name given its visibility. * * @param array $item */ private function formatItemName(array $item): string { return \sprintf('<%s>%s</%s>', $item['style'], OutputFormatter::escape($item['name']), $item['style']); } /** * Validate that input options make sense, provide defaults when called without options. * * @throws RuntimeException if options are inconsistent * * @param InputInterface $input */ private function validateInput(InputInterface $input) { if (!$input->getArgument('target')) { // if no target is passed, there can be no properties or methods foreach (['properties', 'methods', 'no-inherit'] as $option) { if ($input->getOption($option)) { throw new RuntimeException('--'.$option.' does not make sense without a specified target'); } } foreach (['globals', 'vars', 'constants', 'functions', 'classes', 'interfaces', 'traits'] as $option) { if ($input->getOption($option)) { return; } } // default to --vars if no other options are passed $input->setOption('vars', true); } else { // if a target is passed, classes, functions, etc don't make sense foreach (['vars', 'globals'] as $option) { if ($input->getOption($option)) { throw new RuntimeException('--'.$option.' does not make sense with a specified target'); } } // @todo ensure that 'functions', 'classes', 'interfaces', 'traits' only accept namespace target? foreach (['constants', 'properties', 'methods', 'functions', 'classes', 'interfaces', 'traits'] as $option) { if ($input->getOption($option)) { return; } } // default to --constants --properties --methods if no other options are passed $input->setOption('constants', true); $input->setOption('properties', true); $input->setOption('methods', true); } } } psysh/src/Command/ClearCommand.php 0000644 00000002150 15024771425 0013122 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; } } psysh/src/Command/WhereamiCommand.php 0000644 00000010416 15024771425 0013641 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); } } psysh/src/Command/ParseCommand.php 0000644 00000011263 15024771425 0013153 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'); if (\strpos($code, '<?') === false) { $code = '<?php '.$code; } $parserKind = $input->getOption('kind'); $depth = $input->getOption('depth'); $nodes = $this->parse($this->getParser($parserKind), $code); $output->page($this->presenter->present($nodes, $depth)); $this->context->setReturnValue($nodes); return 0; } /** * Lex and parse a string of code into statements. * * @param Parser $parser * @param string $code * * @return array Statements */ private function parse(Parser $parser, string $code): array { try { return $parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $parser->parse($code.';'); } } /** * 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): Parser { if (!\array_key_exists($kind, $this->parsers)) { $this->parsers[$kind] = $this->parserFactory->createParser($kind); } return $this->parsers[$kind]; } } psysh/src/Command/SudoCommand.php 0000644 00000007413 15024771425 0013015 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\ParserFactory; 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) { $parserFactory = new ParserFactory(); $this->parser = $parserFactory->createParser(); $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]; } if (\strpos($code, '<?') === false) { $code = '<?php '.$code; } $nodes = $this->traverser->traverse($this->parse($code)); $sudoCode = $this->printer->prettyPrint($nodes); $shell = $this->getApplication(); $shell->addCode($sudoCode, !$shell->hasCode()); return 0; } /** * Lex and parse a string of code into statements. * * @param string $code * * @return array Statements */ private function parse(string $code): array { try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $this->parser->parse($code.';'); } } } psysh/src/Command/BufferCommand.php 0000644 00000004665 15024771425 0013322 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); } } psysh/src/Command/Command.php 0000644 00000017263 15024771425 0012166 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\Shell; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command as BaseCommand; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Helper\TableStyle; use Symfony\Component\Console\Output\OutputInterface; /** * The Psy Shell base command. */ abstract class Command extends BaseCommand { /** * Sets the application instance for this command. * * @param Application|null $application An Application instance * * @api */ public function setApplication(Application $application = null) { if ($application !== null && !$application instanceof Shell) { throw new \InvalidArgumentException('PsySH Commands require an instance of Psy\Shell'); } return parent::setApplication($application); } /** * {@inheritdoc} */ public function asText(): string { $messages = [ '<comment>Usage:</comment>', ' '.$this->getSynopsis(), '', ]; if ($this->getAliases()) { $messages[] = $this->aliasesAsText(); } if ($this->getArguments()) { $messages[] = $this->argumentsAsText(); } if ($this->getOptions()) { $messages[] = $this->optionsAsText(); } if ($help = $this->getProcessedHelp()) { $messages[] = '<comment>Help:</comment>'; $messages[] = ' '.\str_replace("\n", "\n ", $help)."\n"; } return \implode("\n", $messages); } /** * {@inheritdoc} */ private function getArguments(): array { $hidden = $this->getHiddenArguments(); return \array_filter($this->getNativeDefinition()->getArguments(), function ($argument) use ($hidden) { return !\in_array($argument->getName(), $hidden); }); } /** * These arguments will be excluded from help output. * * @return string[] */ protected function getHiddenArguments(): array { return ['command']; } /** * {@inheritdoc} */ private function getOptions(): array { $hidden = $this->getHiddenOptions(); return \array_filter($this->getNativeDefinition()->getOptions(), function ($option) use ($hidden) { return !\in_array($option->getName(), $hidden); }); } /** * These options will be excluded from help output. * * @return string[] */ protected function getHiddenOptions(): array { return ['verbose']; } /** * Format command aliases as text.. */ private function aliasesAsText(): string { return '<comment>Aliases:</comment> <info>'.\implode(', ', $this->getAliases()).'</info>'.\PHP_EOL; } /** * Format command arguments as text. */ private function argumentsAsText(): string { $max = $this->getMaxWidth(); $messages = []; $arguments = $this->getArguments(); if (!empty($arguments)) { $messages[] = '<comment>Arguments:</comment>'; foreach ($arguments as $argument) { if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { $default = \sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($argument->getDefault())); } else { $default = ''; } $description = \str_replace("\n", "\n".\str_pad('', $max + 2, ' '), $argument->getDescription()); $messages[] = \sprintf(" <info>%-{$max}s</info> %s%s", $argument->getName(), $description, $default); } $messages[] = ''; } return \implode(\PHP_EOL, $messages); } /** * Format options as text. */ private function optionsAsText(): string { $max = $this->getMaxWidth(); $messages = []; $options = $this->getOptions(); if ($options) { $messages[] = '<comment>Options:</comment>'; foreach ($options as $option) { if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { $default = \sprintf('<comment> (default: %s)</comment>', $this->formatDefaultValue($option->getDefault())); } else { $default = ''; } $multiple = $option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''; $description = \str_replace("\n", "\n".\str_pad('', $max + 2, ' '), $option->getDescription()); $optionMax = $max - \strlen($option->getName()) - 2; $messages[] = \sprintf( " <info>%s</info> %-{$optionMax}s%s%s%s", '--'.$option->getName(), $option->getShortcut() ? \sprintf('(-%s) ', $option->getShortcut()) : '', $description, $default, $multiple ); } $messages[] = ''; } return \implode(\PHP_EOL, $messages); } /** * Calculate the maximum padding width for a set of lines. */ private function getMaxWidth(): int { $max = 0; foreach ($this->getOptions() as $option) { $nameLength = \strlen($option->getName()) + 2; if ($option->getShortcut()) { $nameLength += \strlen($option->getShortcut()) + 3; } $max = \max($max, $nameLength); } foreach ($this->getArguments() as $argument) { $max = \max($max, \strlen($argument->getName())); } return ++$max; } /** * Format an option default as text. * * @param mixed $default */ private function formatDefaultValue($default): string { if (\is_array($default) && $default === \array_values($default)) { return \sprintf("['%s']", \implode("', '", $default)); } return \str_replace("\n", '', \var_export($default, true)); } /** * Get a Table instance. * * Falls back to legacy TableHelper. * * @return Table|TableHelper */ protected function getTable(OutputInterface $output) { if (!\class_exists(Table::class)) { return $this->getTableHelper(); } $style = new TableStyle(); // Symfony 4.1 deprecated single-argument style setters. if (\method_exists($style, 'setVerticalBorderChars')) { $style->setVerticalBorderChars(' '); $style->setHorizontalBorderChars(''); $style->setCrossingChars('', '', '', '', '', '', '', '', ''); } else { $style->setVerticalBorderChar(' '); $style->setHorizontalBorderChar(''); $style->setCrossingChar(''); } $table = new Table($output); return $table ->setRows([]) ->setStyle($style); } /** * Legacy fallback for getTable. */ protected function getTableHelper(): TableHelper { $table = $this->getApplication()->getHelperSet()->get('table'); return $table ->setRows([]) ->setLayout(TableHelper::LAYOUT_BORDERLESS) ->setHorizontalBorderChar('') ->setCrossingChar(''); } } psysh/src/Command/DocCommand.php 0000644 00000022041 15024771425 0012602 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); } } } } psysh/src/Command/DumpCommand.php 0000644 00000005070 15024771425 0013005 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); } } psysh/src/Command/ListCommand/PropertyEnumerator.php 0000644 00000011536 15024771425 0016705 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); } } psysh/src/Command/ListCommand/MethodEnumerator.php 0000644 00000007616 15024771425 0016305 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; } } } psysh/src/Command/ListCommand/ClassConstantEnumerator.php 0000644 00000006256 15024771425 0017643 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'; } } } psysh/src/Command/ListCommand/Enumerator.php 0000644 00000005205 15024771425 0015134 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); } } psysh/src/Command/ListCommand/FunctionEnumerator.php 0000644 00000006004 15024771425 0016640 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; } } psysh/src/Command/ListCommand/ClassEnumerator.php 0000644 00000007426 15024771425 0016131 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; } } psysh/src/Command/ListCommand/ConstantEnumerator.php 0000644 00000011211 15024771425 0016640 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; } } psysh/src/Command/ListCommand/VariableEnumerator.php 0000644 00000006707 15024771425 0016612 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; } } psysh/src/Command/ListCommand/GlobalVariableEnumerator.php 0000644 00000003771 15024771425 0017731 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; } } psysh/src/Command/PsyVersionCommand.php 0000644 00000001671 15024771425 0014224 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; } } psysh/src/Command/ThrowUpCommand.php 0000644 00000010565 15024771425 0013515 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 Psy\ParserFactory; 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) { $parserFactory = new ParserFactory(); $this->parser = $parserFactory->createParser(); $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'))]; } if (\strpos($code, '<?') === false) { $code = '<?php '.$code; } $nodes = $this->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; } /** * Lex and parse a string of code into statements. * * @param string $code * * @return array Statements */ private function parse(string $code): array { try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if (\strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $this->parser->parse($code.';'); } } } psysh/src/Command/EditCommand.php 0000644 00000013343 15024771425 0012767 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; } } psysh/src/Command/TimeitCommand.php 0000644 00000012446 15024771425 0013340 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 Psy\ParserFactory; 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>'; private static $start = null; private static $times = []; private $parser; private $traverser; private $printer; /** * {@inheritdoc} */ public function __construct($name = null) { $parserFactory = new ParserFactory(); $this->parser = $parserFactory->createParser(); $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 = $input->getOption('num') ?: 1; $shell = $this->getApplication(); $instrumentedCode = $this->instrumentCode($code); self::$times = []; for ($i = 0; $i < $num; $i++) { $_ = $shell->execute($instrumentedCode); $this->ensureEndMarked(); } $shell->writeReturnValue($_); $times = self::$times; self::$times = []; if ($num === 1) { $output->writeln(\sprintf(self::RESULT_MSG, $times[0])); } else { $total = \array_sum($times); \rsort($times); $median = $times[\round($num / 2)]; $output->writeln(\sprintf(self::AVG_RESULT_MSG, $total / $num, $median, $total)); } 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 = \microtime(true); } /** * 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[] = \microtime(true) - 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. * * @param string $code */ private function instrumentCode(string $code): string { return $this->printer->prettyPrint($this->traverser->traverse($this->parse($code))); } /** * Lex and parse a string of code into statements. * * @param string $code * * @return array Statements */ private 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 $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $this->parser->parse($code.';'); } } } psysh/src/Command/ShowCommand.php 0000644 00000023221 15024771425 0013016 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]]; } } } psysh/src/Command/TimeitCommand/TimeitVisitor.php 0000644 00000010120 15024771425 0016136 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; } } psysh/src/Command/ReflectingCommand.php 0000644 00000024435 15024771425 0014170 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\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\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; /** * 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 { $value = $this->getApplication()->execute($code, 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); } } psysh/src/Formatter/DocblockFormatter.php 0000644 00000011060 15024771425 0014566 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\Formatter; use Psy\Util\Docblock; use Symfony\Component\Console\Formatter\OutputFormatter; /** * A pretty-printer for docblocks. */ class DocblockFormatter implements ReflectorFormatter { private static $vectorParamTemplates = [ 'type' => 'info', 'var' => 'strong', ]; /** * Format a docblock. * * @param \Reflector $reflector * * @return string Formatted docblock */ public static function format(\Reflector $reflector): string { $docblock = new Docblock($reflector); $chunks = []; if (!empty($docblock->desc)) { $chunks[] = '<comment>Description:</comment>'; $chunks[] = self::indent(OutputFormatter::escape($docblock->desc), ' '); $chunks[] = ''; } if (!empty($docblock->tags)) { foreach ($docblock::$vectors as $name => $vector) { if (isset($docblock->tags[$name])) { $chunks[] = \sprintf('<comment>%s:</comment>', self::inflect($name)); $chunks[] = self::formatVector($vector, $docblock->tags[$name]); $chunks[] = ''; } } $tags = self::formatTags(\array_keys($docblock::$vectors), $docblock->tags); if (!empty($tags)) { $chunks[] = $tags; $chunks[] = ''; } } return \rtrim(\implode("\n", $chunks)); } /** * Format a docblock vector, for example, `@throws`, `@param`, or `@return`. * * @see DocBlock::$vectors * * @param array $vector * @param array $lines */ private static function formatVector(array $vector, array $lines): string { $template = [' ']; foreach ($vector as $type) { $max = 0; foreach ($lines as $line) { $chunk = $line[$type]; $cur = empty($chunk) ? 0 : \strlen($chunk) + 1; if ($cur > $max) { $max = $cur; } } $template[] = self::getVectorParamTemplate($type, $max); } $template = \implode(' ', $template); return \implode("\n", \array_map(function ($line) use ($template) { $escaped = \array_map(function ($l) { if ($l === null) { return ''; } return OutputFormatter::escape($l); }, $line); return \rtrim(\vsprintf($template, $escaped)); }, $lines)); } /** * Format docblock tags. * * @param array $skip Tags to exclude * @param array $tags Tags to format * * @return string formatted tags */ private static function formatTags(array $skip, array $tags): string { $chunks = []; foreach ($tags as $name => $values) { if (\in_array($name, $skip)) { continue; } foreach ($values as $value) { $chunks[] = \sprintf('<comment>%s%s</comment> %s', self::inflect($name), empty($value) ? '' : ':', OutputFormatter::escape($value)); } $chunks[] = ''; } return \implode("\n", $chunks); } /** * Get a docblock vector template. * * @param string $type Vector type * @param int $max Pad width */ private static function getVectorParamTemplate(string $type, int $max): string { if (!isset(self::$vectorParamTemplates[$type])) { return \sprintf('%%-%ds', $max); } return \sprintf('<%s>%%-%ds</%s>', self::$vectorParamTemplates[$type], $max, self::$vectorParamTemplates[$type]); } /** * Indent a string. * * @param string $text String to indent * @param string $indent (default: ' ') */ private static function indent(string $text, string $indent = ' '): string { return $indent.\str_replace("\n", "\n".$indent, $text); } /** * Convert underscored or whitespace separated words into sentence case. * * @param string $text */ private static function inflect(string $text): string { $words = \trim(\preg_replace('/[\s_-]+/', ' ', \preg_replace('/([a-z])([A-Z])/', '$1 $2', $text))); return \implode(' ', \array_map('ucfirst', \explode(' ', $words))); } } psysh/src/Formatter/CodeFormatter.php 0000644 00000023532 15024771425 0013727 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\Formatter; use Psy\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; /** * A pretty-printer for code. */ class CodeFormatter implements ReflectorFormatter { const LINE_MARKER = ' <urgent>></urgent> '; const NO_LINE_MARKER = ' '; const HIGHLIGHT_DEFAULT = 'default'; const HIGHLIGHT_KEYWORD = 'keyword'; const HIGHLIGHT_PUBLIC = 'public'; const HIGHLIGHT_PROTECTED = 'protected'; const HIGHLIGHT_PRIVATE = 'private'; const HIGHLIGHT_CONST = 'const'; const HIGHLIGHT_NUMBER = 'number'; const HIGHLIGHT_STRING = 'string'; const HIGHLIGHT_COMMENT = 'comment'; const HIGHLIGHT_INLINE_HTML = 'inline_html'; private static $tokenMap = [ // Not highlighted \T_OPEN_TAG => self::HIGHLIGHT_DEFAULT, \T_OPEN_TAG_WITH_ECHO => self::HIGHLIGHT_DEFAULT, \T_CLOSE_TAG => self::HIGHLIGHT_DEFAULT, \T_STRING => self::HIGHLIGHT_DEFAULT, \T_VARIABLE => self::HIGHLIGHT_DEFAULT, \T_NS_SEPARATOR => self::HIGHLIGHT_DEFAULT, // Visibility \T_PUBLIC => self::HIGHLIGHT_PUBLIC, \T_PROTECTED => self::HIGHLIGHT_PROTECTED, \T_PRIVATE => self::HIGHLIGHT_PRIVATE, // Constants \T_DIR => self::HIGHLIGHT_CONST, \T_FILE => self::HIGHLIGHT_CONST, \T_METHOD_C => self::HIGHLIGHT_CONST, \T_NS_C => self::HIGHLIGHT_CONST, \T_LINE => self::HIGHLIGHT_CONST, \T_CLASS_C => self::HIGHLIGHT_CONST, \T_FUNC_C => self::HIGHLIGHT_CONST, \T_TRAIT_C => self::HIGHLIGHT_CONST, // Types \T_DNUMBER => self::HIGHLIGHT_NUMBER, \T_LNUMBER => self::HIGHLIGHT_NUMBER, \T_ENCAPSED_AND_WHITESPACE => self::HIGHLIGHT_STRING, \T_CONSTANT_ENCAPSED_STRING => self::HIGHLIGHT_STRING, // Comments \T_COMMENT => self::HIGHLIGHT_COMMENT, \T_DOC_COMMENT => self::HIGHLIGHT_COMMENT, // @todo something better here? \T_INLINE_HTML => self::HIGHLIGHT_INLINE_HTML, ]; /** * Format the code represented by $reflector for shell output. * * @param \Reflector $reflector * @param string|null $colorMode (deprecated and ignored) * * @return string formatted code */ public static function format(\Reflector $reflector, string $colorMode = null): string { if (self::isReflectable($reflector)) { if ($code = @\file_get_contents($reflector->getFileName())) { return self::formatCode($code, self::getStartLine($reflector), $reflector->getEndLine()); } } throw new RuntimeException('Source code unavailable'); } /** * Format code for shell output. * * Optionally, restrict by $startLine and $endLine line numbers, or pass $markLine to add a line marker. * * @param string $code * @param int $startLine * @param int|null $endLine * @param int|null $markLine * * @return string formatted code */ public static function formatCode(string $code, int $startLine = 1, int $endLine = null, int $markLine = null): string { $spans = self::tokenizeSpans($code); $lines = self::splitLines($spans, $startLine, $endLine); $lines = self::formatLines($lines); $lines = self::numberLines($lines, $markLine); return \implode('', \iterator_to_array($lines)); } /** * Get the start line for a given Reflector. * * Tries to incorporate doc comments if possible. * * This is typehinted as \Reflector but we've narrowed the input via self::isReflectable already. * * @param \ReflectionClass|\ReflectionFunctionAbstract $reflector */ private static function getStartLine(\Reflector $reflector): int { $startLine = $reflector->getStartLine(); if ($docComment = $reflector->getDocComment()) { $startLine -= \preg_match_all('/(\r\n?|\n)/', $docComment) + 1; } return \max($startLine, 1); } /** * Split code into highlight spans. * * Tokenize via \token_get_all, then map these tokens to internal highlight types, combining * adjacent spans of the same highlight type. * * @todo consider switching \token_get_all() out for PHP-Parser-based formatting at some point. * * @param string $code * * @return \Generator [$spanType, $spanText] highlight spans */ private static function tokenizeSpans(string $code): \Generator { $spanType = null; $buffer = ''; foreach (\token_get_all($code) as $token) { $nextType = self::nextHighlightType($token, $spanType); $spanType = $spanType ?: $nextType; if ($spanType !== $nextType) { yield [$spanType, $buffer]; $spanType = $nextType; $buffer = ''; } $buffer .= \is_array($token) ? $token[1] : $token; } if ($spanType !== null && $buffer !== '') { yield [$spanType, $buffer]; } } /** * Given a token and the current highlight span type, compute the next type. * * @param array|string $token \token_get_all token * @param string|null $currentType * * @return string|null */ private static function nextHighlightType($token, $currentType) { if ($token === '"') { return self::HIGHLIGHT_STRING; } if (\is_array($token)) { if ($token[0] === \T_WHITESPACE) { return $currentType; } if (\array_key_exists($token[0], self::$tokenMap)) { return self::$tokenMap[$token[0]]; } } return self::HIGHLIGHT_KEYWORD; } /** * Group highlight spans into an array of lines. * * Optionally, restrict by start and end line numbers. * * @param \Generator $spans as [$spanType, $spanText] pairs * @param int $startLine * @param int|null $endLine * * @return \Generator lines, each an array of [$spanType, $spanText] pairs */ private static function splitLines(\Generator $spans, int $startLine = 1, int $endLine = null): \Generator { $lineNum = 1; $buffer = []; foreach ($spans as list($spanType, $spanText)) { foreach (\preg_split('/(\r\n?|\n)/', $spanText) as $index => $spanLine) { if ($index > 0) { if ($lineNum >= $startLine) { yield $lineNum => $buffer; } $lineNum++; $buffer = []; if ($endLine !== null && $lineNum > $endLine) { return; } } if ($spanLine !== '') { $buffer[] = [$spanType, $spanLine]; } } } if (!empty($buffer)) { yield $lineNum => $buffer; } } /** * Format lines of highlight spans for shell output. * * @param \Generator $spanLines lines, each an array of [$spanType, $spanText] pairs * * @return \Generator Formatted lines */ private static function formatLines(\Generator $spanLines): \Generator { foreach ($spanLines as $lineNum => $spanLine) { $line = ''; foreach ($spanLine as list($spanType, $spanText)) { if ($spanType === self::HIGHLIGHT_DEFAULT) { $line .= OutputFormatter::escape($spanText); } else { $line .= \sprintf('<%s>%s</%s>', $spanType, OutputFormatter::escape($spanText), $spanType); } } yield $lineNum => $line.\PHP_EOL; } } /** * Prepend line numbers to formatted lines. * * Lines must be in an associative array with the correct keys in order to be numbered properly. * * Optionally, pass $markLine to add a line marker. * * @param \Generator $lines Formatted lines * @param int|null $markLine * * @return \Generator Numbered, formatted lines */ private static function numberLines(\Generator $lines, int $markLine = null): \Generator { $lines = \iterator_to_array($lines); // Figure out how much space to reserve for line numbers. \end($lines); $pad = \strlen(\key($lines)); // If $markLine is before or after our line range, don't bother reserving space for the marker. if ($markLine !== null) { if ($markLine > \key($lines)) { $markLine = null; } \reset($lines); if ($markLine < \key($lines)) { $markLine = null; } } foreach ($lines as $lineNum => $line) { $mark = ''; if ($markLine !== null) { $mark = ($markLine === $lineNum) ? self::LINE_MARKER : self::NO_LINE_MARKER; } yield \sprintf("%s<aside>%{$pad}s</aside>: %s", $mark, $lineNum, $line); } } /** * Check whether a Reflector instance is reflectable by this formatter. * * @phpstan-assert-if-true \ReflectionClass|\ReflectionFunctionAbstract $reflector * * @param \Reflector $reflector */ private static function isReflectable(\Reflector $reflector): bool { return ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) && \is_file($reflector->getFileName()); } } psysh/src/Formatter/ReflectorFormatter.php 0000644 00000000662 15024771425 0015001 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\Formatter; /** * Reflector formatter interface. */ interface ReflectorFormatter { /** * @param \Reflector $reflector */ public static function format(\Reflector $reflector): string; } psysh/src/Formatter/SignatureFormatter.php 0000644 00000025407 15024771425 0015021 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\Formatter; use Psy\Reflection\ReflectionClassConstant; use Psy\Reflection\ReflectionConstant_; use Psy\Reflection\ReflectionLanguageConstruct; use Psy\Util\Json; use Symfony\Component\Console\Formatter\OutputFormatter; /** * An abstract representation of a function, class or property signature. */ class SignatureFormatter implements ReflectorFormatter { /** * Format a signature for the given reflector. * * Defers to subclasses to do the actual formatting. * * @param \Reflector $reflector * * @return string Formatted signature */ public static function format(\Reflector $reflector): string { switch (true) { case $reflector instanceof \ReflectionFunction: case $reflector instanceof ReflectionLanguageConstruct: return self::formatFunction($reflector); case $reflector instanceof \ReflectionClass: // this case also covers \ReflectionObject return self::formatClass($reflector); case $reflector instanceof ReflectionClassConstant: case $reflector instanceof \ReflectionClassConstant: return self::formatClassConstant($reflector); case $reflector instanceof \ReflectionMethod: return self::formatMethod($reflector); case $reflector instanceof \ReflectionProperty: return self::formatProperty($reflector); case $reflector instanceof ReflectionConstant_: return self::formatConstant($reflector); default: throw new \InvalidArgumentException('Unexpected Reflector class: '.\get_class($reflector)); } } /** * Print the signature name. * * @param \ReflectionClass|ReflectionClassConstant|\ReflectionClassConstant|\ReflectionFunctionAbstract $reflector * * @return string Formatted name */ public static function formatName(\Reflector $reflector): string { return $reflector->getName(); } /** * Print the method, property or class modifiers. * * @param \ReflectionMethod|\ReflectionProperty|\ReflectionClass $reflector * * @return string Formatted modifiers */ private static function formatModifiers(\Reflector $reflector): string { return \implode(' ', \array_map(function ($modifier) { return \sprintf('<keyword>%s</keyword>', $modifier); }, \Reflection::getModifierNames($reflector->getModifiers()))); } /** * Format a class signature. * * @param \ReflectionClass $reflector * * @return string Formatted signature */ private static function formatClass(\ReflectionClass $reflector): string { $chunks = []; if ($modifiers = self::formatModifiers($reflector)) { $chunks[] = $modifiers; } if ($reflector->isTrait()) { $chunks[] = 'trait'; } else { $chunks[] = $reflector->isInterface() ? 'interface' : 'class'; } $chunks[] = \sprintf('<class>%s</class>', self::formatName($reflector)); if ($parent = $reflector->getParentClass()) { $chunks[] = 'extends'; $chunks[] = \sprintf('<class>%s</class>', $parent->getName()); } $interfaces = $reflector->getInterfaceNames(); if (!empty($interfaces)) { \sort($interfaces); $chunks[] = $reflector->isInterface() ? 'extends' : 'implements'; $chunks[] = \implode(', ', \array_map(function ($name) { return \sprintf('<class>%s</class>', $name); }, $interfaces)); } return \implode(' ', $chunks); } /** * Format a constant signature. * * @param ReflectionClassConstant|\ReflectionClassConstant $reflector * * @return string Formatted signature */ private static function formatClassConstant($reflector): string { $value = $reflector->getValue(); $style = self::getTypeStyle($value); return \sprintf( '<keyword>const</keyword> <const>%s</const> = <%s>%s</%s>', self::formatName($reflector), $style, OutputFormatter::escape(Json::encode($value)), $style ); } /** * Format a constant signature. * * @param ReflectionConstant_ $reflector * * @return string Formatted signature */ private static function formatConstant(ReflectionConstant_ $reflector): string { $value = $reflector->getValue(); $style = self::getTypeStyle($value); return \sprintf( '<keyword>define</keyword>(<string>%s</string>, <%s>%s</%s>)', OutputFormatter::escape(Json::encode($reflector->getName())), $style, OutputFormatter::escape(Json::encode($value)), $style ); } /** * Helper for getting output style for a given value's type. * * @param mixed $value */ private static function getTypeStyle($value): string { if (\is_int($value) || \is_float($value)) { return 'number'; } elseif (\is_string($value)) { return 'string'; } elseif (\is_bool($value) || $value === null) { return 'bool'; } else { return 'strong'; // @codeCoverageIgnore } } /** * Format a property signature. * * @param \ReflectionProperty $reflector * * @return string Formatted signature */ private static function formatProperty(\ReflectionProperty $reflector): string { return \sprintf( '%s <strong>$%s</strong>', self::formatModifiers($reflector), $reflector->getName() ); } /** * Format a function signature. * * @param \ReflectionFunction $reflector * * @return string Formatted signature */ private static function formatFunction(\ReflectionFunctionAbstract $reflector): string { return \sprintf( '<keyword>function</keyword> %s<function>%s</function>(%s)%s', $reflector->returnsReference() ? '&' : '', self::formatName($reflector), \implode(', ', self::formatFunctionParams($reflector)), self::formatFunctionReturnType($reflector) ); } /** * Format a function signature's return type (if available). * * @param \ReflectionFunctionAbstract $reflector * * @return string Formatted return type */ private static function formatFunctionReturnType(\ReflectionFunctionAbstract $reflector): string { if (!\method_exists($reflector, 'hasReturnType') || !$reflector->hasReturnType()) { return ''; } return \sprintf(': %s', self::formatReflectionType($reflector->getReturnType())); } /** * Format a method signature. * * @param \ReflectionMethod $reflector * * @return string Formatted signature */ private static function formatMethod(\ReflectionMethod $reflector): string { return \sprintf( '%s %s', self::formatModifiers($reflector), self::formatFunction($reflector) ); } /** * Print the function params. * * @param \ReflectionFunctionAbstract $reflector * * @return array */ private static function formatFunctionParams(\ReflectionFunctionAbstract $reflector): array { $params = []; foreach ($reflector->getParameters() as $param) { $hint = ''; try { if (\method_exists($param, 'getType')) { $hint = self::formatReflectionType($param->getType()); } else { if ($param->isArray()) { $hint = '<keyword>array</keyword>'; } elseif ($class = $param->getClass()) { $hint = \sprintf('<class>%s</class>', $class->getName()); } } } catch (\Throwable $e) { // sometimes we just don't know... // bad class names, or autoloaded classes that haven't been loaded yet, or whathaveyou. // come to think of it, the only time I've seen this is with the intl extension. // Hax: we'll try to extract it :P // @codeCoverageIgnoreStart $chunks = \explode('$'.$param->getName(), (string) $param); $chunks = \explode(' ', \trim($chunks[0])); $guess = \end($chunks); $hint = \sprintf('<urgent>%s</urgent>', OutputFormatter::escape($guess)); // @codeCoverageIgnoreEnd } if ($param->isOptional()) { if (!$param->isDefaultValueAvailable()) { $value = 'unknown'; $typeStyle = 'urgent'; } else { $value = $param->getDefaultValue(); $typeStyle = self::getTypeStyle($value); $value = \is_array($value) ? '[]' : ($value === null ? 'null' : \var_export($value, true)); } $default = \sprintf(' = <%s>%s</%s>', $typeStyle, OutputFormatter::escape($value), $typeStyle); } else { $default = ''; } $params[] = \sprintf( '%s%s%s<strong>$%s</strong>%s', $param->isPassedByReference() ? '&' : '', $hint, $hint !== '' ? ' ' : '', $param->getName(), $default ); } return $params; } /** * Print function param or return type(s). * * @param \ReflectionType $type */ private static function formatReflectionType(\ReflectionType $type = null): string { if ($type === null) { return ''; } $types = $type instanceof \ReflectionUnionType ? $type->getTypes() : [$type]; $formattedTypes = []; foreach ($types as $type) { $typeStyle = $type->isBuiltin() ? 'keyword' : 'class'; // PHP 7.0 didn't have `getName` on reflection types, so wheee! $typeName = \method_exists($type, 'getName') ? $type->getName() : (string) $type; // @todo Do we want to include the ? for nullable types? Maybe only sometimes? $formattedTypes[] = \sprintf('<%s>%s</%s>', $typeStyle, OutputFormatter::escape($typeName), $typeStyle); } return \implode('|', $formattedTypes); } } psysh/src/Formatter/Formatter.php 0000644 00000000646 15024771425 0013135 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\Formatter; /** * Formatter interface. * * @deprecated this interface only exists for backwards compatibility. Use ReflectorFormatter. */ interface Formatter extends ReflectorFormatter { } psysh/src/Formatter/TraceFormatter.php 0000644 00000006431 15024771425 0014112 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\Formatter; use Psy\Input\FilterOptions; use Symfony\Component\Console\Formatter\OutputFormatter; /** * Output formatter for exception traces. */ class TraceFormatter { /** * Format the trace of the given exception. * * @param \Throwable $throwable The error or exception with a backtrace * @param FilterOptions $filter (default: null) * @param int $count (default: PHP_INT_MAX) * @param bool $includePsy (default: true) * * @return string[] Formatted stacktrace lines */ public static function formatTrace(\Throwable $throwable, FilterOptions $filter = null, int $count = null, bool $includePsy = true): array { if ($cwd = \getcwd()) { $cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; } if ($count === null) { $count = \PHP_INT_MAX; } $lines = []; $trace = $throwable->getTrace(); \array_unshift($trace, [ 'function' => '', 'file' => $throwable->getFile() !== null ? $throwable->getFile() : 'n/a', 'line' => $throwable->getLine() !== null ? $throwable->getLine() : 'n/a', 'args' => [], ]); if (!$includePsy) { for ($i = \count($trace) - 1; $i >= 0; $i--) { $thing = isset($trace[$i]['class']) ? $trace[$i]['class'] : $trace[$i]['function']; if (\preg_match('/\\\\?Psy\\\\/', $thing)) { $trace = \array_slice($trace, $i + 1); break; } } } for ($i = 0, $count = \min($count, \count($trace)); $i < $count; $i++) { $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; $function = $trace[$i]['function']; $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; // Make file paths relative to cwd if ($cwd !== false) { $file = \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file); } // Leave execution loop out of the `eval()'d code` lines if (\preg_match("#/src/Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code$#", \str_replace('\\', '/', $file))) { $file = "eval()'d code"; } // Skip any lines that don't match our filter options if ($filter !== null && !$filter->match(\sprintf('%s%s%s() at %s:%s', $class, $type, $function, $file, $line))) { continue; } $lines[] = \sprintf( ' <class>%s</class>%s%s() at <info>%s:%s</info>', OutputFormatter::escape($class), OutputFormatter::escape($type), OutputFormatter::escape($function), OutputFormatter::escape($file), OutputFormatter::escape($line) ); } return $lines; } } psysh/src/Configuration.php 0000644 00000154542 15024771425 0012043 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; use Psy\Exception\DeprecatedException; use Psy\Exception\RuntimeException; use Psy\ExecutionLoop\ProcessForker; use Psy\Output\OutputPager; use Psy\Output\ShellOutput; use Psy\Output\Theme; use Psy\TabCompletion\AutoCompleter; use Psy\VarDumper\Presenter; use Psy\VersionUpdater\Checker; use Psy\VersionUpdater\GitHubChecker; use Psy\VersionUpdater\IntervalChecker; use Psy\VersionUpdater\NoopChecker; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * The Psy Shell configuration. */ class Configuration { const COLOR_MODE_AUTO = 'auto'; const COLOR_MODE_FORCED = 'forced'; const COLOR_MODE_DISABLED = 'disabled'; const INTERACTIVE_MODE_AUTO = 'auto'; const INTERACTIVE_MODE_FORCED = 'forced'; const INTERACTIVE_MODE_DISABLED = 'disabled'; const VERBOSITY_QUIET = 'quiet'; const VERBOSITY_NORMAL = 'normal'; const VERBOSITY_VERBOSE = 'verbose'; const VERBOSITY_VERY_VERBOSE = 'very_verbose'; const VERBOSITY_DEBUG = 'debug'; private static $AVAILABLE_OPTIONS = [ 'codeCleaner', 'colorMode', 'configDir', 'dataDir', 'defaultIncludes', 'eraseDuplicates', 'errorLoggingLevel', 'forceArrayIndexes', 'formatterStyles', 'historyFile', 'historySize', 'interactiveMode', 'manualDbFile', 'pager', 'prompt', 'rawOutput', 'requireSemicolons', 'runtimeDir', 'startupMessage', 'theme', 'updateCheck', 'useBracketedPaste', 'usePcntl', 'useReadline', 'useTabCompletion', 'useUnicode', 'verbosity', 'warnOnMultipleConfigs', 'yolo', ]; private $defaultIncludes; private $configDir; private $dataDir; private $runtimeDir; private $configFile; /** @var string|false */ private $historyFile; private $historySize; private $eraseDuplicates; private $manualDbFile; private $hasReadline; private $useReadline; private $useBracketedPaste; private $hasPcntl; private $usePcntl; private $newCommands = []; private $pipedInput; private $pipedOutput; private $rawOutput = false; private $requireSemicolons = false; private $useUnicode; private $useTabCompletion; private $newMatchers = []; private $errorLoggingLevel = \E_ALL; private $warnOnMultipleConfigs = false; private $colorMode = self::COLOR_MODE_AUTO; private $interactiveMode = self::INTERACTIVE_MODE_AUTO; private $updateCheck; private $startupMessage; private $forceArrayIndexes = false; /** @deprecated */ private $formatterStyles = []; private $verbosity = self::VERBOSITY_NORMAL; private $yolo = false; /** @var Theme */ private $theme; // services private $readline; /** @var ShellOutput */ private $output; private $shell; private $cleaner; private $pager; private $manualDb; private $presenter; private $autoCompleter; private $checker; /** @deprecated */ private $prompt; private $configPaths; /** * Construct a Configuration instance. * * Optionally, supply an array of configuration values to load. * * @param array $config Optional array of configuration values */ public function __construct(array $config = []) { $this->configPaths = new ConfigPaths(); // explicit configFile option if (isset($config['configFile'])) { $this->configFile = $config['configFile']; } elseif (isset($_SERVER['PSYSH_CONFIG']) && $_SERVER['PSYSH_CONFIG']) { $this->configFile = $_SERVER['PSYSH_CONFIG']; } // legacy baseDir option if (isset($config['baseDir'])) { $msg = "The 'baseDir' configuration option is deprecated; ". "please specify 'configDir' and 'dataDir' options instead"; throw new DeprecatedException($msg); } unset($config['configFile'], $config['baseDir']); // go go gadget, config! $this->loadConfig($config); $this->init(); } /** * Construct a Configuration object from Symfony Console input. * * This is great for adding psysh-compatible command line options to framework- or app-specific * wrappers. * * $input should already be bound to an appropriate InputDefinition (see self::getInputOptions * if you want to build your own) before calling this method. It's not required, but things work * a lot better if we do. * * @see self::getInputOptions * * @throws \InvalidArgumentException * * @param InputInterface $input */ public static function fromInput(InputInterface $input): self { $config = new self(['configFile' => self::getConfigFileFromInput($input)]); // Handle --color and --no-color (and --ansi and --no-ansi aliases) if (self::getOptionFromInput($input, ['color', 'ansi'])) { $config->setColorMode(self::COLOR_MODE_FORCED); } elseif (self::getOptionFromInput($input, ['no-color', 'no-ansi'])) { $config->setColorMode(self::COLOR_MODE_DISABLED); } // Handle verbosity options if ($verbosity = self::getVerbosityFromInput($input)) { $config->setVerbosity($verbosity); } // Handle interactive mode if (self::getOptionFromInput($input, ['interactive', 'interaction'], ['-a', '-i'])) { $config->setInteractiveMode(self::INTERACTIVE_MODE_FORCED); } elseif (self::getOptionFromInput($input, ['no-interactive', 'no-interaction'], ['-n'])) { $config->setInteractiveMode(self::INTERACTIVE_MODE_DISABLED); } // Handle --compact if (self::getOptionFromInput($input, ['compact'])) { $config->setTheme('compact'); } // Handle --raw-output // @todo support raw output with interactive input? if (!$config->getInputInteractive()) { if (self::getOptionFromInput($input, ['raw-output'], ['-r'])) { $config->setRawOutput(true); } } // Handle --yolo if (self::getOptionFromInput($input, ['yolo'])) { $config->setYolo(true); } return $config; } /** * Get the desired config file from the given input. * * @return string|null config file path, or null if none is specified */ private static function getConfigFileFromInput(InputInterface $input) { // Best case, input is properly bound and validated. if ($input->hasOption('config')) { return $input->getOption('config'); } return $input->getParameterOption('--config', null, true) ?: $input->getParameterOption('-c', null, true); } /** * Get a boolean option from the given input. * * This helper allows fallback for unbound and unvalidated input. It's not perfect--for example, * it can't deal with several short options squished together--but it's better than falling over * any time someone gives us unbound input. * * @return bool true if the option (or an alias) is present */ private static function getOptionFromInput(InputInterface $input, array $names, array $otherParams = []): bool { // Best case, input is properly bound and validated. foreach ($names as $name) { if ($input->hasOption($name) && $input->getOption($name)) { return true; } } foreach ($names as $name) { $otherParams[] = '--'.$name; } foreach ($otherParams as $name) { if ($input->hasParameterOption($name, true)) { return true; } } return false; } /** * Get the desired verbosity from the given input. * * This is a bit more complext than the other options parsers. It handles `--quiet` and * `--verbose`, along with their short aliases, and fancy things like `-vvv`. * * @return string|null configuration constant, or null if no verbosity option is specified */ private static function getVerbosityFromInput(InputInterface $input) { // --quiet wins! if (self::getOptionFromInput($input, ['quiet'], ['-q'])) { return self::VERBOSITY_QUIET; } // Best case, input is properly bound and validated. // // Note that if the `--verbose` option is incorrectly defined as `VALUE_NONE` rather than // `VALUE_OPTIONAL` (as it is in Symfony Console by default) it doesn't actually work with // multiple verbosity levels as it claims. // // We can detect this by checking whether the the value === true, and fall back to unbound // parsing for this option. if ($input->hasOption('verbose') && $input->getOption('verbose') !== true) { switch ($input->getOption('verbose')) { case '-1': return self::VERBOSITY_QUIET; case '0': // explicitly normal, overrides config file default return self::VERBOSITY_NORMAL; case '1': case null: // `--verbose` and `-v` return self::VERBOSITY_VERBOSE; case '2': case 'v': // `-vv` return self::VERBOSITY_VERY_VERBOSE; case '3': case 'vv': // `-vvv` return self::VERBOSITY_DEBUG; default: // implicitly normal, config file default wins return; } } // quiet and normal have to come before verbose, because it eats everything else. if ($input->hasParameterOption('--verbose=-1', true) || $input->getParameterOption('--verbose', false, true) === '-1') { return self::VERBOSITY_QUIET; } if ($input->hasParameterOption('--verbose=0', true) || $input->getParameterOption('--verbose', false, true) === '0') { return self::VERBOSITY_NORMAL; } // `-vvv`, `-vv` and `-v` have to come in descending length order, because `hasParameterOption` matches prefixes. if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || $input->getParameterOption('--verbose', false, true) === '3') { return self::VERBOSITY_DEBUG; } if ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || $input->getParameterOption('--verbose', false, true) === '2') { return self::VERBOSITY_VERY_VERBOSE; } if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true)) { return self::VERBOSITY_VERBOSE; } } /** * Get a list of input options expected when initializing Configuration via input. * * @see self::fromInput * * @return InputOption[] */ public static function getInputOptions(): array { return [ new InputOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use an alternate PsySH config file location.'), new InputOption('cwd', null, InputOption::VALUE_REQUIRED, 'Use an alternate working directory.'), new InputOption('color', null, InputOption::VALUE_NONE, 'Force colors in output.'), new InputOption('no-color', null, InputOption::VALUE_NONE, 'Disable colors in output.'), // --ansi and --no-ansi aliases to match Symfony, Composer, etc. new InputOption('ansi', null, InputOption::VALUE_NONE, 'Force colors in output.'), new InputOption('no-ansi', null, InputOption::VALUE_NONE, 'Disable colors in output.'), new InputOption('quiet', 'q', InputOption::VALUE_NONE, 'Shhhhhh.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_OPTIONAL, 'Increase the verbosity of messages.', '0'), new InputOption('compact', null, InputOption::VALUE_NONE, 'Run PsySH with compact output.'), new InputOption('interactive', 'i|a', InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'), new InputOption('no-interactive', 'n', InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'), // --interaction and --no-interaction aliases for compatibility with Symfony, Composer, etc new InputOption('interaction', null, InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'), new InputOption('no-interaction', null, InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'), new InputOption('raw-output', 'r', InputOption::VALUE_NONE, 'Print var_export-style return values (for non-interactive input)'), new InputOption('self-update', 'u', InputOption::VALUE_NONE, 'Update to the latest version'), new InputOption('yolo', null, InputOption::VALUE_NONE, 'Run PsySH with minimal input validation. You probably don\'t want this.'), ]; } /** * Initialize the configuration. * * This checks for the presence of Readline and Pcntl extensions. * * If a config file is available, it will be loaded and merged with the current config. * * If no custom config file was specified and a local project config file * is available, it will be loaded and merged with the current config. */ public function init() { // feature detection $this->hasReadline = \function_exists('readline'); $this->hasPcntl = ProcessForker::isSupported(); if ($configFile = $this->getConfigFile()) { $this->loadConfigFile($configFile); } if (!$this->configFile && $localConfig = $this->getLocalConfigFile()) { $this->loadConfigFile($localConfig); } $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the current PsySH config file. * * If a `configFile` option was passed to the Configuration constructor, * this file will be returned. If not, all possible config directories will * be searched, and the first `config.php` or `rc.php` file which exists * will be returned. * * If you're trying to decide where to put your config file, pick * * ~/.config/psysh/config.php * * @return string|null */ public function getConfigFile() { if (isset($this->configFile)) { return $this->configFile; } $files = $this->configPaths->configFiles(['config.php', 'rc.php']); if (!empty($files)) { if ($this->warnOnMultipleConfigs && \count($files) > 1) { $msg = \sprintf('Multiple configuration files found: %s. Using %s', \implode(', ', $files), $files[0]); \trigger_error($msg, \E_USER_NOTICE); } return $files[0]; } } /** * Get the local PsySH config file. * * Searches for a project specific config file `.psysh.php` in the current * working directory. * * @return string|null */ public function getLocalConfigFile() { $localConfig = \getcwd().'/.psysh.php'; if (@\is_file($localConfig)) { return $localConfig; } } /** * Load configuration values from an array of options. * * @param array $options */ public function loadConfig(array $options) { foreach (self::$AVAILABLE_OPTIONS as $option) { if (isset($options[$option])) { $method = 'set'.\ucfirst($option); $this->$method($options[$option]); } } // legacy `tabCompletion` option if (isset($options['tabCompletion'])) { $msg = '`tabCompletion` is deprecated; use `useTabCompletion` instead.'; @\trigger_error($msg, \E_USER_DEPRECATED); $this->setUseTabCompletion($options['tabCompletion']); } foreach (['commands', 'matchers', 'casters'] as $option) { if (isset($options[$option])) { $method = 'add'.\ucfirst($option); $this->$method($options[$option]); } } // legacy `tabCompletionMatchers` option if (isset($options['tabCompletionMatchers'])) { $msg = '`tabCompletionMatchers` is deprecated; use `matchers` instead.'; @\trigger_error($msg, \E_USER_DEPRECATED); $this->addMatchers($options['tabCompletionMatchers']); } } /** * Load a configuration file (default: `$HOME/.config/psysh/config.php`). * * This configuration instance will be available to the config file as $config. * The config file may directly manipulate the configuration, or may return * an array of options which will be merged with the current configuration. * * @throws \InvalidArgumentException if the config file does not exist or returns a non-array result * * @param string $file */ public function loadConfigFile(string $file) { if (!\is_file($file)) { throw new \InvalidArgumentException(\sprintf('Invalid configuration file specified, %s does not exist', $file)); } $__psysh_config_file__ = $file; $load = function ($config) use ($__psysh_config_file__) { $result = require $__psysh_config_file__; if ($result !== 1) { return $result; } }; $result = $load($this); if (!empty($result)) { if (\is_array($result)) { $this->loadConfig($result); } else { throw new \InvalidArgumentException('Psy Shell configuration must return an array of options'); } } } /** * Set files to be included by default at the start of each shell session. * * @param array $includes */ public function setDefaultIncludes(array $includes = []) { $this->defaultIncludes = $includes; } /** * Get files to be included by default at the start of each shell session. * * @return string[] */ public function getDefaultIncludes(): array { return $this->defaultIncludes ?: []; } /** * Set the shell's config directory location. * * @param string $dir */ public function setConfigDir(string $dir) { $this->configDir = (string) $dir; $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the current configuration directory, if any is explicitly set. * * @return string|null */ public function getConfigDir() { return $this->configDir; } /** * Set the shell's data directory location. * * @param string $dir */ public function setDataDir(string $dir) { $this->dataDir = (string) $dir; $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the current data directory, if any is explicitly set. * * @return string|null */ public function getDataDir() { return $this->dataDir; } /** * Set the shell's temporary directory location. * * @param string $dir */ public function setRuntimeDir(string $dir) { $this->runtimeDir = (string) $dir; $this->configPaths->overrideDirs([ 'configDir' => $this->configDir, 'dataDir' => $this->dataDir, 'runtimeDir' => $this->runtimeDir, ]); } /** * Get the shell's temporary directory location. * * Defaults to `/psysh` inside the system's temp dir unless explicitly * overridden. * * @throws RuntimeException if no temporary directory is set and it is not possible to create one */ public function getRuntimeDir(): string { $runtimeDir = $this->configPaths->runtimeDir(); if (!\is_dir($runtimeDir)) { if (!@\mkdir($runtimeDir, 0700, true)) { throw new RuntimeException(\sprintf('Unable to create PsySH runtime directory. Make sure PHP is able to write to %s in order to continue.', \dirname($runtimeDir))); } } return $runtimeDir; } /** * Set the readline history file path. * * @param string $file */ public function setHistoryFile(string $file) { $this->historyFile = ConfigPaths::touchFileWithMkdir($file); } /** * Get the readline history file path. * * Defaults to `/history` inside the shell's base config dir unless * explicitly overridden. */ public function getHistoryFile(): string { if (isset($this->historyFile)) { return $this->historyFile; } $files = $this->configPaths->configFiles(['psysh_history', 'history']); if (!empty($files)) { if ($this->warnOnMultipleConfigs && \count($files) > 1) { $msg = \sprintf('Multiple history files found: %s. Using %s', \implode(', ', $files), $files[0]); \trigger_error($msg, \E_USER_NOTICE); } $this->setHistoryFile($files[0]); } else { // fallback: create our own history file $this->setHistoryFile($this->configPaths->currentConfigDir().'/psysh_history'); } return $this->historyFile; } /** * Set the readline max history size. * * @param int $value */ public function setHistorySize(int $value) { $this->historySize = (int) $value; } /** * Get the readline max history size. * * @return int */ public function getHistorySize() { return $this->historySize; } /** * Sets whether readline erases old duplicate history entries. * * @param bool $value */ public function setEraseDuplicates(bool $value) { $this->eraseDuplicates = (bool) $value; } /** * Get whether readline erases old duplicate history entries. * * @return bool|null */ public function getEraseDuplicates() { return $this->eraseDuplicates; } /** * Get a temporary file of type $type for process $pid. * * The file will be created inside the current temporary directory. * * @see self::getRuntimeDir * * @param string $type * @param int $pid * * @return string Temporary file name */ public function getTempFile(string $type, int $pid): string { return \tempnam($this->getRuntimeDir(), $type.'_'.$pid.'_'); } /** * Get a filename suitable for a FIFO pipe of $type for process $pid. * * The pipe will be created inside the current temporary directory. * * @param string $type * @param int $pid * * @return string Pipe name */ public function getPipe(string $type, int $pid): string { return \sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid); } /** * Check whether this PHP instance has Readline available. * * @return bool True if Readline is available */ public function hasReadline(): bool { return $this->hasReadline; } /** * Enable or disable Readline usage. * * @param bool $useReadline */ public function setUseReadline(bool $useReadline) { $this->useReadline = (bool) $useReadline; } /** * Check whether to use Readline. * * If `setUseReadline` as been set to true, but Readline is not actually * available, this will return false. * * @return bool True if the current Shell should use Readline */ public function useReadline(): bool { return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline; } /** * Set the Psy Shell readline service. * * @param Readline\Readline $readline */ public function setReadline(Readline\Readline $readline) { $this->readline = $readline; } /** * Get the Psy Shell readline service. * * By default, this service uses (in order of preference): * * * GNU Readline * * Libedit * * A transient array-based readline emulation. * * @return Readline\Readline */ public function getReadline(): Readline\Readline { if (!isset($this->readline)) { $className = $this->getReadlineClass(); $this->readline = new $className( $this->getHistoryFile(), $this->getHistorySize(), $this->getEraseDuplicates() ); } return $this->readline; } /** * Get the appropriate Readline implementation class name. * * @see self::getReadline */ private function getReadlineClass(): string { if ($this->useReadline()) { if (Readline\GNUReadline::isSupported()) { return Readline\GNUReadline::class; } elseif (Readline\Libedit::isSupported()) { return Readline\Libedit::class; } } if (Readline\Userland::isSupported()) { return Readline\Userland::class; } return Readline\Transient::class; } /** * Enable or disable bracketed paste. * * Note that this only works with readline (not libedit) integration for now. * * @param bool $useBracketedPaste */ public function setUseBracketedPaste(bool $useBracketedPaste) { $this->useBracketedPaste = (bool) $useBracketedPaste; } /** * Check whether to use bracketed paste with readline. * * When this works, it's magical. Tabs in pastes don't try to autcomplete. * Newlines in paste don't execute code until you get to the end. It makes * readline act like you'd expect when pasting. * * But it often (usually?) does not work. And when it doesn't, it just spews * escape codes all over the place and generally makes things ugly :( * * If `useBracketedPaste` has been set to true, but the current readline * implementation is anything besides GNU readline, this will return false. * * @return bool True if the shell should use bracketed paste */ public function useBracketedPaste(): bool { $readlineClass = $this->getReadlineClass(); return $this->useBracketedPaste && $readlineClass::supportsBracketedPaste(); // @todo mebbe turn this on by default some day? // return $readlineClass::supportsBracketedPaste() && $this->useBracketedPaste !== false; } /** * Check whether this PHP instance has Pcntl available. * * @return bool True if Pcntl is available */ public function hasPcntl(): bool { return $this->hasPcntl; } /** * Enable or disable Pcntl usage. * * @param bool $usePcntl */ public function setUsePcntl(bool $usePcntl) { $this->usePcntl = (bool) $usePcntl; } /** * Check whether to use Pcntl. * * If `setUsePcntl` has been set to true, but Pcntl is not actually * available, this will return false. * * @return bool True if the current Shell should use Pcntl */ public function usePcntl(): bool { if (!isset($this->usePcntl)) { // Unless pcntl is explicitly *enabled*, don't use it while XDebug is debugging. // See https://github.com/bobthecow/psysh/issues/742 if (\function_exists('xdebug_is_debugger_active') && \xdebug_is_debugger_active()) { return false; } return $this->hasPcntl; } return $this->hasPcntl && $this->usePcntl; } /** * Check whether to use raw output. * * This is set by the --raw-output (-r) flag, and really only makes sense * when non-interactive, e.g. executing stdin. * * @return bool true if raw output is enabled */ public function rawOutput(): bool { return $this->rawOutput; } /** * Enable or disable raw output. * * @param bool $rawOutput */ public function setRawOutput(bool $rawOutput) { $this->rawOutput = (bool) $rawOutput; } /** * Enable or disable strict requirement of semicolons. * * @see self::requireSemicolons() * * @param bool $requireSemicolons */ public function setRequireSemicolons(bool $requireSemicolons) { $this->requireSemicolons = (bool) $requireSemicolons; } /** * Check whether to require semicolons on all statements. * * By default, PsySH will automatically insert semicolons at the end of * statements if they're missing. To strictly require semicolons, set * `requireSemicolons` to true. */ public function requireSemicolons(): bool { return $this->requireSemicolons; } /** * Enable or disable Unicode in PsySH specific output. * * Note that this does not disable Unicode output in general, it just makes * it so PsySH won't output any itself. * * @param bool $useUnicode */ public function setUseUnicode(bool $useUnicode) { $this->useUnicode = (bool) $useUnicode; } /** * Check whether to use Unicode in PsySH specific output. * * Note that this does not disable Unicode output in general, it just makes * it so PsySH won't output any itself. */ public function useUnicode(): bool { if (isset($this->useUnicode)) { return $this->useUnicode; } // @todo detect `chsh` != 65001 on Windows and return false return true; } /** * Set the error logging level. * * @see self::errorLoggingLevel * * @param int $errorLoggingLevel */ public function setErrorLoggingLevel($errorLoggingLevel) { $this->errorLoggingLevel = (\E_ALL | \E_STRICT) & $errorLoggingLevel; } /** * Get the current error logging level. * * By default, PsySH will automatically log all errors, regardless of the * current `error_reporting` level. * * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it * to any valid error_reporting value to log only errors which match that * level. * * http://php.net/manual/en/function.error-reporting.php */ public function errorLoggingLevel(): int { return $this->errorLoggingLevel; } /** * Set a CodeCleaner service instance. * * @param CodeCleaner $cleaner */ public function setCodeCleaner(CodeCleaner $cleaner) { $this->cleaner = $cleaner; } /** * Get a CodeCleaner service instance. * * If none has been explicitly defined, this will create a new instance. */ public function getCodeCleaner(): CodeCleaner { if (!isset($this->cleaner)) { $this->cleaner = new CodeCleaner(null, null, null, $this->yolo()); } return $this->cleaner; } /** * Enable or disable running PsySH without input validation. * * You don't want this. */ public function setYolo($yolo) { $this->yolo = (bool) $yolo; } /** * Check whether to disable input validation. */ public function yolo(): bool { return $this->yolo; } /** * Enable or disable tab completion. * * @param bool $useTabCompletion */ public function setUseTabCompletion(bool $useTabCompletion) { $this->useTabCompletion = (bool) $useTabCompletion; } /** * @deprecated Call `setUseTabCompletion` instead * * @param bool $useTabCompletion */ public function setTabCompletion(bool $useTabCompletion) { $this->setUseTabCompletion($useTabCompletion); } /** * Check whether to use tab completion. * * If `setUseTabCompletion` has been set to true, but readline is not * actually available, this will return false. * * @return bool True if the current Shell should use tab completion */ public function useTabCompletion(): bool { return isset($this->useTabCompletion) ? ($this->hasReadline && $this->useTabCompletion) : $this->hasReadline; } /** * @deprecated Call `useTabCompletion` instead */ public function getTabCompletion(): bool { return $this->useTabCompletion(); } /** * Set the Shell Output service. * * @param ShellOutput $output */ public function setOutput(ShellOutput $output) { $this->output = $output; $this->pipedOutput = null; // Reset cached pipe info if (isset($this->theme)) { $output->setTheme($this->theme); } $this->applyFormatterStyles(); } /** * Get a Shell Output service instance. * * If none has been explicitly provided, this will create a new instance * with the configured verbosity and output pager supplied by self::getPager * * @see self::verbosity * @see self::getPager */ public function getOutput(): ShellOutput { if (!isset($this->output)) { $this->setOutput(new ShellOutput( $this->getOutputVerbosity(), null, null, $this->getPager() ?: null, $this->theme() )); // This is racy because `getOutputDecorated` needs access to the // output stream to figure out if it's piped or not, so create it // first, then update after we have a stream. $decorated = $this->getOutputDecorated(); if ($decorated !== null) { $this->output->setDecorated($decorated); } } return $this->output; } /** * Get the decoration (i.e. color) setting for the Shell Output service. * * @return bool|null 3-state boolean corresponding to the current color mode */ public function getOutputDecorated() { switch ($this->colorMode()) { case self::COLOR_MODE_FORCED: return true; case self::COLOR_MODE_DISABLED: return false; case self::COLOR_MODE_AUTO: default: return $this->outputIsPiped() ? false : null; } } /** * Get the interactive setting for shell input. */ public function getInputInteractive(): bool { switch ($this->interactiveMode()) { case self::INTERACTIVE_MODE_FORCED: return true; case self::INTERACTIVE_MODE_DISABLED: return false; case self::INTERACTIVE_MODE_AUTO: default: return !$this->inputIsPiped(); } } /** * Set the OutputPager service. * * If a string is supplied, a ProcOutputPager will be used which shells out * to the specified command. * * `cat` is special-cased to use the PassthruPager directly. * * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance * * @param string|OutputPager|false $pager */ public function setPager($pager) { if ($pager === null || $pager === false || $pager === 'cat') { $pager = false; } if ($pager !== false && !\is_string($pager) && !$pager instanceof OutputPager) { throw new \InvalidArgumentException('Unexpected pager instance'); } $this->pager = $pager; } /** * Get an OutputPager instance or a command for an external Proc pager. * * If no Pager has been explicitly provided, and Pcntl is available, this * will default to `cli.pager` ini value, falling back to `which less`. * * @return string|OutputPager|false */ public function getPager() { if (!isset($this->pager) && $this->usePcntl()) { if (\getenv('TERM') === 'dumb') { return false; } if ($pager = \ini_get('cli.pager')) { // use the default pager $this->pager = $pager; } elseif ($less = $this->configPaths->which('less')) { // check for the presence of less... $this->pager = $less.' -R -F -X'; } } return $this->pager; } /** * Set the Shell AutoCompleter service. * * @param AutoCompleter $autoCompleter */ public function setAutoCompleter(AutoCompleter $autoCompleter) { $this->autoCompleter = $autoCompleter; } /** * Get an AutoCompleter service instance. */ public function getAutoCompleter(): AutoCompleter { if (!isset($this->autoCompleter)) { $this->autoCompleter = new AutoCompleter(); } return $this->autoCompleter; } /** * @deprecated Nothing should be using this anymore */ public function getTabCompletionMatchers(): array { return []; } /** * Add tab completion matchers to the AutoCompleter. * * This will buffer new matchers in the event that the Shell has not yet * been instantiated. This allows the user to specify matchers in their * config rc file, despite the fact that their file is needed in the Shell * constructor. * * @param array $matchers */ public function addMatchers(array $matchers) { $this->newMatchers = \array_merge($this->newMatchers, $matchers); if (isset($this->shell)) { $this->doAddMatchers(); } } /** * Internal method for adding tab completion matchers. This will set any new * matchers once a Shell is available. */ private function doAddMatchers() { if (!empty($this->newMatchers)) { $this->shell->addMatchers($this->newMatchers); $this->newMatchers = []; } } /** * @deprecated Use `addMatchers` instead * * @param array $matchers */ public function addTabCompletionMatchers(array $matchers) { $this->addMatchers($matchers); } /** * Add commands to the Shell. * * This will buffer new commands in the event that the Shell has not yet * been instantiated. This allows the user to specify commands in their * config rc file, despite the fact that their file is needed in the Shell * constructor. * * @param array $commands */ public function addCommands(array $commands) { $this->newCommands = \array_merge($this->newCommands, $commands); if (isset($this->shell)) { $this->doAddCommands(); } } /** * Internal method for adding commands. This will set any new commands once * a Shell is available. */ private function doAddCommands() { if (!empty($this->newCommands)) { $this->shell->addCommands($this->newCommands); $this->newCommands = []; } } /** * Set the Shell backreference and add any new commands to the Shell. * * @param Shell $shell */ public function setShell(Shell $shell) { $this->shell = $shell; $this->doAddCommands(); $this->doAddMatchers(); } /** * Set the PHP manual database file. * * This file should be an SQLite database generated from the phpdoc source * with the `bin/build_manual` script. * * @param string $filename */ public function setManualDbFile(string $filename) { $this->manualDbFile = (string) $filename; } /** * Get the current PHP manual database file. * * @return string|null Default: '~/.local/share/psysh/php_manual.sqlite' */ public function getManualDbFile() { if (isset($this->manualDbFile)) { return $this->manualDbFile; } $files = $this->configPaths->dataFiles(['php_manual.sqlite']); if (!empty($files)) { if ($this->warnOnMultipleConfigs && \count($files) > 1) { $msg = \sprintf('Multiple manual database files found: %s. Using %s', \implode(', ', $files), $files[0]); \trigger_error($msg, \E_USER_NOTICE); } return $this->manualDbFile = $files[0]; } } /** * Get a PHP manual database connection. * * @return \PDO|null */ public function getManualDb() { if (!isset($this->manualDb)) { $dbFile = $this->getManualDbFile(); if ($dbFile !== null && \is_file($dbFile)) { try { $this->manualDb = new \PDO('sqlite:'.$dbFile); } catch (\PDOException $e) { if ($e->getMessage() === 'could not find driver') { throw new RuntimeException('SQLite PDO driver not found', 0, $e); } else { throw $e; } } } } return $this->manualDb; } /** * Add an array of casters definitions. * * @param array $casters */ public function addCasters(array $casters) { $this->getPresenter()->addCasters($casters); } /** * Get the Presenter service. */ public function getPresenter(): Presenter { if (!isset($this->presenter)) { $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes()); } return $this->presenter; } /** * Enable or disable warnings on multiple configuration or data files. * * @see self::warnOnMultipleConfigs() * * @param bool $warnOnMultipleConfigs */ public function setWarnOnMultipleConfigs(bool $warnOnMultipleConfigs) { $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs; } /** * Check whether to warn on multiple configuration or data files. * * By default, PsySH will use the file with highest precedence, and will * silently ignore all others. With this enabled, a warning will be emitted * (but not an exception thrown) if multiple configuration or data files * are found. * * This will default to true in a future release, but is false for now. */ public function warnOnMultipleConfigs(): bool { return $this->warnOnMultipleConfigs; } /** * Set the current color mode. * * @throws \InvalidArgumentException if the color mode isn't auto, forced or disabled * * @param string $colorMode */ public function setColorMode(string $colorMode) { $validColorModes = [ self::COLOR_MODE_AUTO, self::COLOR_MODE_FORCED, self::COLOR_MODE_DISABLED, ]; if (!\in_array($colorMode, $validColorModes)) { throw new \InvalidArgumentException('Invalid color mode: '.$colorMode); } $this->colorMode = $colorMode; } /** * Get the current color mode. */ public function colorMode(): string { return $this->colorMode; } /** * Set the shell's interactive mode. * * @throws \InvalidArgumentException if interactive mode isn't disabled, forced, or auto * * @param string $interactiveMode */ public function setInteractiveMode(string $interactiveMode) { $validInteractiveModes = [ self::INTERACTIVE_MODE_AUTO, self::INTERACTIVE_MODE_FORCED, self::INTERACTIVE_MODE_DISABLED, ]; if (!\in_array($interactiveMode, $validInteractiveModes)) { throw new \InvalidArgumentException('Invalid interactive mode: '.$interactiveMode); } $this->interactiveMode = $interactiveMode; } /** * Get the current interactive mode. */ public function interactiveMode(): string { return $this->interactiveMode; } /** * Set an update checker service instance. * * @param Checker $checker */ public function setChecker(Checker $checker) { $this->checker = $checker; } /** * Get an update checker service instance. * * If none has been explicitly defined, this will create a new instance. */ public function getChecker(): Checker { if (!isset($this->checker)) { $interval = $this->getUpdateCheck(); switch ($interval) { case Checker::ALWAYS: $this->checker = new GitHubChecker(); break; case Checker::DAILY: case Checker::WEEKLY: case Checker::MONTHLY: $checkFile = $this->getUpdateCheckCacheFile(); if ($checkFile === false) { $this->checker = new NoopChecker(); } else { $this->checker = new IntervalChecker($checkFile, $interval); } break; case Checker::NEVER: $this->checker = new NoopChecker(); break; } } return $this->checker; } /** * Get the current update check interval. * * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is * explicitly set, default to 'weekly'. */ public function getUpdateCheck(): string { return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY; } /** * Set the update check interval. * * @throws \InvalidArgumentException if the update check interval is unknown * * @param string $interval */ public function setUpdateCheck(string $interval) { $validIntervals = [ Checker::ALWAYS, Checker::DAILY, Checker::WEEKLY, Checker::MONTHLY, Checker::NEVER, ]; if (!\in_array($interval, $validIntervals)) { throw new \InvalidArgumentException('Invalid update check interval: '.$interval); } $this->updateCheck = $interval; } /** * Get a cache file path for the update checker. * * @return string|false Return false if config file/directory is not writable */ public function getUpdateCheckCacheFile() { return ConfigPaths::touchFileWithMkdir($this->configPaths->currentConfigDir().'/update_check.json'); } /** * Set the startup message. * * @param string $message */ public function setStartupMessage(string $message) { $this->startupMessage = $message; } /** * Get the startup message. * * @return string|null */ public function getStartupMessage() { return $this->startupMessage; } /** * Set the prompt. * * @deprecated The `prompt` configuration has been replaced by Themes and support will * eventually be removed. In the meantime, prompt is applied first by the Theme, then overridden * by any explicitly defined prompt. * * Note that providing a prompt but not a theme config will implicitly use the `classic` theme. */ public function setPrompt(string $prompt) { $this->prompt = $prompt; if (isset($this->theme)) { $this->theme->setPrompt($prompt); } } /** * Get the prompt. * * @return string|null */ public function getPrompt() { return $this->prompt; } /** * Get the force array indexes. */ public function forceArrayIndexes(): bool { return $this->forceArrayIndexes; } /** * Set the force array indexes. * * @param bool $forceArrayIndexes */ public function setForceArrayIndexes(bool $forceArrayIndexes) { $this->forceArrayIndexes = $forceArrayIndexes; } /** * Set the current output Theme. * * @param Theme|string|array $theme Theme (or Theme config) */ public function setTheme($theme) { if (!$theme instanceof Theme) { $theme = new Theme($theme); } $this->theme = $theme; if (isset($this->prompt)) { $this->theme->setPrompt($this->prompt); } if (isset($this->output)) { $this->output->setTheme($theme); $this->applyFormatterStyles(); } } /** * Get the current output Theme. */ public function theme(): Theme { if (!isset($this->theme)) { // If a prompt is explicitly set, and a theme is not, base it on the `classic` theme. $this->theme = $this->prompt ? new Theme('classic') : new Theme(); } if (isset($this->prompt)) { $this->theme->setPrompt($this->prompt); } return $this->theme; } /** * Set the shell output formatter styles. * * Accepts a map from style name to [fg, bg, options], for example: * * [ * 'error' => ['white', 'red', ['bold']], * 'warning' => ['black', 'yellow'], * ] * * Foreground, background or options can be null, or even omitted entirely. * * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will * eventually be removed. In the meantime, styles are applied first by the Theme, then * overridden by any explicitly defined formatter styles. */ public function setFormatterStyles(array $formatterStyles) { foreach ($formatterStyles as $name => $style) { $this->formatterStyles[$name] = new OutputFormatterStyle(...$style); } if (isset($this->output)) { $this->applyFormatterStyles(); } } /** * Internal method for applying output formatter style customization. * * This is called on initialization of the shell output, and again if the * formatter styles config is updated. * * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will * eventually be removed. In the meantime, styles are applied first by the Theme, then * overridden by any explicitly defined formatter styles. */ private function applyFormatterStyles() { $formatter = $this->output->getFormatter(); foreach ($this->formatterStyles as $name => $style) { $formatter->setStyle($name, $style); } $errorFormatter = $this->output->getErrorOutput()->getFormatter(); foreach (Theme::ERROR_STYLES as $name) { if (isset($this->formatterStyles[$name])) { $errorFormatter->setStyle($name, $this->formatterStyles[$name]); } } } /** * Get the configured output verbosity. */ public function verbosity(): string { return $this->verbosity; } /** * Set the shell output verbosity. * * Accepts OutputInterface verbosity constants. * * @throws \InvalidArgumentException if verbosity level is invalid * * @param string $verbosity */ public function setVerbosity(string $verbosity) { $validVerbosityLevels = [ self::VERBOSITY_QUIET, self::VERBOSITY_NORMAL, self::VERBOSITY_VERBOSE, self::VERBOSITY_VERY_VERBOSE, self::VERBOSITY_DEBUG, ]; if (!\in_array($verbosity, $validVerbosityLevels)) { throw new \InvalidArgumentException('Invalid verbosity level: '.$verbosity); } $this->verbosity = $verbosity; if (isset($this->output)) { $this->output->setVerbosity($this->getOutputVerbosity()); } } /** * Map the verbosity configuration to OutputInterface verbosity constants. * * @return int OutputInterface verbosity level */ public function getOutputVerbosity(): int { switch ($this->verbosity()) { case self::VERBOSITY_QUIET: return OutputInterface::VERBOSITY_QUIET; case self::VERBOSITY_VERBOSE: return OutputInterface::VERBOSITY_VERBOSE; case self::VERBOSITY_VERY_VERBOSE: return OutputInterface::VERBOSITY_VERY_VERBOSE; case self::VERBOSITY_DEBUG: return OutputInterface::VERBOSITY_DEBUG; case self::VERBOSITY_NORMAL: default: return OutputInterface::VERBOSITY_NORMAL; } } /** * Guess whether stdin is piped. * * This is mostly useful for deciding whether to use non-interactive mode. */ public function inputIsPiped(): bool { if ($this->pipedInput === null) { $this->pipedInput = \defined('STDIN') && self::looksLikeAPipe(\STDIN); } return $this->pipedInput; } /** * Guess whether shell output is piped. * * This is mostly useful for deciding whether to use non-decorated output. */ public function outputIsPiped(): bool { if ($this->pipedOutput === null) { $this->pipedOutput = self::looksLikeAPipe($this->getOutput()->getStream()); } return $this->pipedOutput; } /** * Guess whether an input or output stream is piped. * * @param resource|int $stream */ private static function looksLikeAPipe($stream): bool { if (\function_exists('posix_isatty')) { return !\posix_isatty($stream); } $stat = \fstat($stream); $mode = $stat['mode'] & 0170000; return $mode === 0010000 || $mode === 0040000 || $mode === 0100000 || $mode === 0120000; } } psysh/src/VarDumper/PresenterAware.php 0000644 00000001017 15024771425 0014054 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\VarDumper; /** * Presenter injects itself as a dependency to all objects which * implement PresenterAware. */ interface PresenterAware { /** * Set a reference to the Presenter. * * @param Presenter $presenter */ public function setPresenter(Presenter $presenter); } psysh/src/VarDumper/Presenter.php 0000644 00000007415 15024771425 0013104 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\VarDumper; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * A Presenter service. */ class Presenter { const VERBOSE = 1; private $cloner; private $dumper; private $exceptionsImportants = [ "\0*\0message", "\0*\0code", "\0*\0file", "\0*\0line", "\0Exception\0previous", ]; private $styles = [ 'num' => 'number', 'integer' => 'integer', 'float' => 'float', 'const' => 'const', 'str' => 'string', 'cchr' => 'default', 'note' => 'class', 'ref' => 'default', 'public' => 'public', 'protected' => 'protected', 'private' => 'private', 'meta' => 'comment', 'key' => 'comment', 'index' => 'number', ]; public function __construct(OutputFormatter $formatter, $forceArrayIndexes = false) { // Work around https://github.com/symfony/symfony/issues/23572 $oldLocale = \setlocale(\LC_NUMERIC, 0); \setlocale(\LC_NUMERIC, 'C'); $this->dumper = new Dumper($formatter, $forceArrayIndexes); $this->dumper->setStyles($this->styles); // Now put the locale back \setlocale(\LC_NUMERIC, $oldLocale); $this->cloner = new Cloner(); $this->cloner->addCasters(['*' => function ($obj, array $a, Stub $stub, $isNested, $filter = 0) { if ($filter || $isNested) { if ($obj instanceof \Throwable) { $a = Caster::filter($a, Caster::EXCLUDE_NOT_IMPORTANT | Caster::EXCLUDE_EMPTY, $this->exceptionsImportants); } else { $a = Caster::filter($a, Caster::EXCLUDE_PROTECTED | Caster::EXCLUDE_PRIVATE); } } return $a; }]); } /** * Register casters. * * @see http://symfony.com/doc/current/components/var_dumper/advanced.html#casters * * @param callable[] $casters A map of casters */ public function addCasters(array $casters) { $this->cloner->addCasters($casters); } /** * Present a reference to the value. * * @param mixed $value */ public function presentRef($value): string { return $this->present($value, 0); } /** * Present a full representation of the value. * * If $depth is 0, the value will be presented as a ref instead. * * @param mixed $value * @param int $depth (default: null) * @param int $options One of Presenter constants */ public function present($value, int $depth = null, int $options = 0): string { $data = $this->cloner->cloneVar($value, !($options & self::VERBOSE) ? Caster::EXCLUDE_VERBOSE : 0); if (null !== $depth) { $data = $data->withMaxDepth($depth); } // Work around https://github.com/symfony/symfony/issues/23572 $oldLocale = \setlocale(\LC_NUMERIC, 0); \setlocale(\LC_NUMERIC, 'C'); $output = ''; $this->dumper->dump($data, function ($line, $depth) use (&$output) { if ($depth >= 0) { if ('' !== $output) { $output .= \PHP_EOL; } $output .= \str_repeat(' ', $depth).$line; } }); // Now put the locale back \setlocale(\LC_NUMERIC, $oldLocale); return OutputFormatter::escape($output); } } psysh/src/VarDumper/Dumper.php 0000644 00000005673 15024771425 0012375 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\VarDumper; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\VarDumper\Cloner\Cursor; use Symfony\Component\VarDumper\Dumper\CliDumper; /** * A PsySH-specialized CliDumper. */ class Dumper extends CliDumper { private $formatter; private $forceArrayIndexes; protected static $onlyControlCharsRx = '/^[\x00-\x1F\x7F]+$/'; protected static $controlCharsRx = '/([\x00-\x1F\x7F]+)/'; protected static $controlCharsMap = [ "\0" => '\0', "\t" => '\t', "\n" => '\n', "\v" => '\v', "\f" => '\f', "\r" => '\r', "\033" => '\e', ]; public function __construct(OutputFormatter $formatter, $forceArrayIndexes = false) { $this->formatter = $formatter; $this->forceArrayIndexes = $forceArrayIndexes; parent::__construct(); $this->setColors(false); } /** * {@inheritdoc} */ public function enterHash(Cursor $cursor, $type, $class, $hasChild) { if (Cursor::HASH_INDEXED === $type || Cursor::HASH_ASSOC === $type) { $class = 0; } parent::enterHash($cursor, $type, $class, $hasChild); } /** * {@inheritdoc} */ protected function dumpKey(Cursor $cursor) { if ($this->forceArrayIndexes || Cursor::HASH_INDEXED !== $cursor->hashType) { parent::dumpKey($cursor); } } protected function style($style, $value, $attr = []): string { if ('ref' === $style) { $value = \strtr($value, '@', '#'); } $styled = ''; $map = self::$controlCharsMap; $cchr = $this->styles['cchr']; $chunks = \preg_split(self::$controlCharsRx, $value, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); foreach ($chunks as $chunk) { if (\preg_match(self::$onlyControlCharsRx, $chunk)) { $chars = ''; $i = 0; do { $chars .= isset($map[$chunk[$i]]) ? $map[$chunk[$i]] : \sprintf('\x%02X', \ord($chunk[$i])); } while (isset($chunk[++$i])); $chars = $this->formatter->escape($chars); $styled .= "<{$cchr}>{$chars}</{$cchr}>"; } else { $styled .= $this->formatter->escape($chunk); } } $style = $this->styles[$style]; return "<{$style}>{$styled}</{$style}>"; } /** * {@inheritdoc} */ protected function dumpLine($depth, $endOfValue = false) { if ($endOfValue && 0 < $depth) { $this->line .= ','; } $this->line = $this->formatter->format($this->line); parent::dumpLine($depth, $endOfValue); } } psysh/src/VarDumper/Cloner.php 0000644 00000001647 15024771425 0012360 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\VarDumper; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Cloner\VarCloner; /** * A PsySH-specialized VarCloner. */ class Cloner extends VarCloner { private $filter = 0; /** * {@inheritdoc} */ public function cloneVar($var, $filter = 0): Data { $this->filter = $filter; return parent::cloneVar($var, $filter); } /** * {@inheritdoc} */ protected function castResource(Stub $stub, $isNested): array { return Caster::EXCLUDE_VERBOSE & $this->filter ? [] : parent::castResource($stub, $isNested); } } psysh/src/functions.php 0000644 00000041010 15024771425 0011225 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; use Psy\ExecutionLoop\ProcessForker; use Psy\VersionUpdater\GitHubChecker; use Psy\VersionUpdater\Installer; use Psy\VersionUpdater\SelfUpdate; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; if (!\function_exists('Psy\\sh')) { /** * Command to return the eval-able code to startup PsySH. * * eval(\Psy\sh()); */ function sh(): string { if (\version_compare(\PHP_VERSION, '8.0', '<')) { return '\extract(\Psy\debug(\get_defined_vars(), isset($this) ? $this : @\get_called_class()));'; } return <<<'EOS' if (isset($this)) { \extract(\Psy\debug(\get_defined_vars(), $this)); } else { try { static::class; \extract(\Psy\debug(\get_defined_vars(), static::class)); } catch (\Error $e) { \extract(\Psy\debug(\get_defined_vars())); } } EOS; } } if (!\function_exists('Psy\\debug')) { /** * Invoke a Psy Shell from the current context. * * For example: * * foreach ($items as $item) { * \Psy\debug(get_defined_vars()); * } * * If you would like your shell interaction to affect the state of the * current context, you can extract() the values returned from this call: * * foreach ($items as $item) { * extract(\Psy\debug(get_defined_vars())); * var_dump($item); // will be whatever you set $item to in Psy Shell * } * * Optionally, supply an object as the `$bindTo` parameter. This determines * the value `$this` will have in the shell, and sets up class scope so that * private and protected members are accessible: * * class Foo { * function bar() { * \Psy\debug(get_defined_vars(), $this); * } * } * * For the static equivalent, pass a class name as the `$bindTo` parameter. * This makes `self` work in the shell, and sets up static scope so that * private and protected static members are accessible: * * class Foo { * static function bar() { * \Psy\debug(get_defined_vars(), get_called_class()); * } * } * * @param array $vars Scope variables from the calling context (default: []) * @param object|string $bindTo Bound object ($this) or class (self) value for the shell * * @return array Scope variables from the debugger session */ function debug(array $vars = [], $bindTo = null): array { echo \PHP_EOL; $sh = new Shell(); $sh->setScopeVariables($vars); // Show a couple of lines of call context for the debug session. // // @todo come up with a better way of doing this which doesn't involve injecting input :-P if ($sh->has('whereami')) { $sh->addInput('whereami -n2', true); } if (\is_string($bindTo)) { $sh->setBoundClass($bindTo); } elseif ($bindTo !== null) { $sh->setBoundObject($bindTo); } $sh->run(); return $sh->getScopeVariables(false); } } if (!\function_exists('Psy\\info')) { /** * Get a bunch of debugging info about the current PsySH environment and * configuration. * * If a Configuration param is passed, that configuration is stored and * used for the current shell session, and no debugging info is returned. * * @param Configuration|null $config * * @return array|null */ function info(Configuration $config = null) { static $lastConfig; if ($config !== null) { $lastConfig = $config; return; } $prettyPath = function ($path) { return $path; }; $homeDir = (new ConfigPaths())->homeDir(); if ($homeDir && $homeDir = \rtrim($homeDir, '/')) { $homePattern = '#^'.\preg_quote($homeDir, '#').'/#'; $prettyPath = function ($path) use ($homePattern) { if (\is_string($path)) { return \preg_replace($homePattern, '~/', $path); } else { return $path; } }; } $config = $lastConfig ?: new Configuration(); $configEnv = (isset($_SERVER['PSYSH_CONFIG']) && $_SERVER['PSYSH_CONFIG']) ? $_SERVER['PSYSH_CONFIG'] : false; $shellInfo = [ 'PsySH version' => Shell::VERSION, ]; $core = [ 'PHP version' => \PHP_VERSION, 'OS' => \PHP_OS, 'default includes' => $config->getDefaultIncludes(), 'require semicolons' => $config->requireSemicolons(), 'error logging level' => $config->errorLoggingLevel(), 'config file' => [ 'default config file' => $prettyPath($config->getConfigFile()), 'local config file' => $prettyPath($config->getLocalConfigFile()), 'PSYSH_CONFIG env' => $prettyPath($configEnv), ], // 'config dir' => $config->getConfigDir(), // 'data dir' => $config->getDataDir(), // 'runtime dir' => $config->getRuntimeDir(), ]; // Use an explicit, fresh update check here, rather than relying on whatever is in $config. $checker = new GitHubChecker(); $updateAvailable = null; $latest = null; try { $updateAvailable = !$checker->isLatest(); $latest = $checker->getLatest(); } catch (\Throwable $e) { } $updates = [ 'update available' => $updateAvailable, 'latest release version' => $latest, 'update check interval' => $config->getUpdateCheck(), 'update cache file' => $prettyPath($config->getUpdateCheckCacheFile()), ]; $input = [ 'interactive mode' => $config->interactiveMode(), 'input interactive' => $config->getInputInteractive(), 'yolo' => $config->yolo(), ]; if ($config->hasReadline()) { $info = \readline_info(); $readline = [ 'readline available' => true, 'readline enabled' => $config->useReadline(), 'readline service' => \get_class($config->getReadline()), ]; if (isset($info['library_version'])) { $readline['readline library'] = $info['library_version']; } if (isset($info['readline_name']) && $info['readline_name'] !== '') { $readline['readline name'] = $info['readline_name']; } } else { $readline = [ 'readline available' => false, ]; } $output = [ 'color mode' => $config->colorMode(), 'output decorated' => $config->getOutputDecorated(), 'output verbosity' => $config->verbosity(), 'output pager' => $config->getPager(), ]; $theme = $config->theme(); // TODO: show styles (but only if they're different than default?) $output['theme'] = [ 'compact' => $theme->compact(), 'prompt' => $theme->prompt(), 'bufferPrompt' => $theme->bufferPrompt(), 'replayPrompt' => $theme->replayPrompt(), 'returnValue' => $theme->returnValue(), ]; $pcntl = [ 'pcntl available' => ProcessForker::isPcntlSupported(), 'posix available' => ProcessForker::isPosixSupported(), ]; if ($disabledPcntl = ProcessForker::disabledPcntlFunctions()) { $pcntl['disabled pcntl functions'] = $disabledPcntl; } if ($disabledPosix = ProcessForker::disabledPosixFunctions()) { $pcntl['disabled posix functions'] = $disabledPosix; } $pcntl['use pcntl'] = $config->usePcntl(); $history = [ 'history file' => $prettyPath($config->getHistoryFile()), 'history size' => $config->getHistorySize(), 'erase duplicates' => $config->getEraseDuplicates(), ]; $docs = [ 'manual db file' => $prettyPath($config->getManualDbFile()), 'sqlite available' => true, ]; try { if ($db = $config->getManualDb()) { if ($q = $db->query('SELECT * FROM meta;')) { $q->setFetchMode(\PDO::FETCH_KEY_PAIR); $meta = $q->fetchAll(); foreach ($meta as $key => $val) { switch ($key) { case 'built_at': $d = new \DateTime('@'.$val); $val = $d->format(\DateTime::RFC2822); break; } $key = 'db '.\str_replace('_', ' ', $key); $docs[$key] = $val; } } else { $docs['db schema'] = '0.1.0'; } } } catch (Exception\RuntimeException $e) { if ($e->getMessage() === 'SQLite PDO driver not found') { $docs['sqlite available'] = false; } else { throw $e; } } $autocomplete = [ 'tab completion enabled' => $config->useTabCompletion(), 'bracketed paste' => $config->useBracketedPaste(), ]; // Shenanigans, but totally justified. try { if ($shell = Sudo::fetchProperty($config, 'shell')) { $shellClass = \get_class($shell); if ($shellClass !== 'Psy\\Shell') { $shellInfo = [ 'PsySH version' => $shell::VERSION, 'Shell class' => $shellClass, ]; } try { $core['loop listeners'] = \array_map('get_class', Sudo::fetchProperty($shell, 'loopListeners')); } catch (\ReflectionException $e) { // shrug } $core['commands'] = \array_map('get_class', $shell->all()); try { $autocomplete['custom matchers'] = \array_map('get_class', Sudo::fetchProperty($shell, 'matchers')); } catch (\ReflectionException $e) { // shrug } } } catch (\ReflectionException $e) { // shrug } // @todo Show Presenter / custom casters. return \array_merge($shellInfo, $core, \compact('updates', 'pcntl', 'input', 'readline', 'output', 'history', 'docs', 'autocomplete')); } } if (!\function_exists('Psy\\bin')) { /** * `psysh` command line executable. * * @return \Closure */ function bin(): \Closure { return function () { if (!isset($_SERVER['PSYSH_IGNORE_ENV']) || !$_SERVER['PSYSH_IGNORE_ENV']) { if (\defined('HHVM_VERSION_ID')) { \fwrite(\STDERR, 'PsySH v0.11 and higher does not support HHVM. Install an older version, or set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL); exit(1); } if (\PHP_VERSION_ID < 70000) { \fwrite(\STDERR, 'PHP 7.0.0 or higher is required. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL); exit(1); } if (\PHP_VERSION_ID > 89999) { \fwrite(\STDERR, 'PHP 9 or higher is not supported. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL); exit(1); } if (!\function_exists('json_encode')) { \fwrite(\STDERR, 'The JSON extension is required. Please install it. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL); exit(1); } if (!\function_exists('token_get_all')) { \fwrite(\STDERR, 'The Tokenizer extension is required. Please install it. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL); exit(1); } } $usageException = null; $shellIsPhar = Shell::isPhar(); $input = new ArgvInput(); try { $input->bind(new InputDefinition(\array_merge(Configuration::getInputOptions(), [ new InputOption('help', 'h', InputOption::VALUE_NONE), new InputOption('version', 'V', InputOption::VALUE_NONE), new InputOption('self-update', 'u', InputOption::VALUE_NONE), new InputArgument('include', InputArgument::IS_ARRAY), ]))); } catch (\RuntimeException $e) { $usageException = $e; } try { $config = Configuration::fromInput($input); } catch (\InvalidArgumentException $e) { $usageException = $e; } // Handle --help if ($usageException !== null || $input->getOption('help')) { if ($usageException !== null) { echo $usageException->getMessage().\PHP_EOL.\PHP_EOL; } $version = Shell::getVersionHeader(false); $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : []; $name = $argv ? \basename(\reset($argv)) : 'psysh'; echo <<<EOL $version Usage: $name [--version] [--help] [files...] Options: -h, --help Display this help message. -c, --config FILE Use an alternate PsySH config file location. --cwd PATH Use an alternate working directory. -V, --version Display the PsySH version. EOL; if ($shellIsPhar) { echo <<<EOL -u, --self-update Install a newer version if available. EOL; } echo <<<EOL --color Force colors in output. --no-color Disable colors in output. -i, --interactive Force PsySH to run in interactive mode. -n, --no-interactive Run PsySH without interactive input. Requires input from stdin. -r, --raw-output Print var_export-style return values (for non-interactive input) --compact Run PsySH with compact output. -q, --quiet Shhhhhh. -v|vv|vvv, --verbose Increase the verbosity of messages. --yolo Run PsySH without input validation. You don't want this. EOL; exit($usageException === null ? 0 : 1); } // Handle --version if ($input->getOption('version')) { echo Shell::getVersionHeader($config->useUnicode()).\PHP_EOL; exit(0); } // Handle --self-update if ($input->getOption('self-update')) { if (!$shellIsPhar) { \fwrite(\STDERR, 'The --self-update option can only be used with with a phar based install.'.\PHP_EOL); exit(1); } $selfUpdate = new SelfUpdate(new GitHubChecker(), new Installer()); $result = $selfUpdate->run($input, $config->getOutput()); exit($result); } $shell = new Shell($config); // Pass additional arguments to Shell as 'includes' $shell->setIncludes($input->getArgument('include')); try { // And go! $shell->run(); } catch (\Throwable $e) { \fwrite(\STDERR, $e->getMessage().\PHP_EOL); // @todo this triggers the "exited unexpectedly" logic in the // ForkingLoop, so we can't exit(1) after starting the shell... // fix this :) // exit(1); } }; } } psysh/src/ConfigPaths.php 0000644 00000026621 15024771425 0011435 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; /** * A Psy Shell configuration path helper. */ class ConfigPaths { private $configDir; private $dataDir; private $runtimeDir; private $env; /** * ConfigPaths constructor. * * Optionally provide `configDir`, `dataDir` and `runtimeDir` overrides. * * @see self::overrideDirs * * @param string[] $overrides Directory overrides * @param EnvInterface $env */ public function __construct(array $overrides = [], EnvInterface $env = null) { $this->overrideDirs($overrides); $this->env = $env ?: new SuperglobalsEnv(); } /** * Provide `configDir`, `dataDir` and `runtimeDir` overrides. * * If a key is set but empty, the override will be removed. If it is not set * at all, any existing override will persist. * * @param string[] $overrides Directory overrides */ public function overrideDirs(array $overrides) { if (\array_key_exists('configDir', $overrides)) { $this->configDir = $overrides['configDir'] ?: null; } if (\array_key_exists('dataDir', $overrides)) { $this->dataDir = $overrides['dataDir'] ?: null; } if (\array_key_exists('runtimeDir', $overrides)) { $this->runtimeDir = $overrides['runtimeDir'] ?: null; } } /** * Get the current home directory. * * @return string|null */ public function homeDir() { if ($homeDir = $this->getEnv('HOME') ?: $this->windowsHomeDir()) { return \strtr($homeDir, '\\', '/'); } return null; } private function windowsHomeDir() { if (\defined('PHP_WINDOWS_VERSION_MAJOR')) { $homeDrive = $this->getEnv('HOMEDRIVE'); $homePath = $this->getEnv('HOMEPATH'); if ($homeDrive && $homePath) { return $homeDrive.'/'.$homePath; } } return null; } private function homeConfigDir() { if ($homeConfigDir = $this->getEnv('XDG_CONFIG_HOME')) { return $homeConfigDir; } $homeDir = $this->homeDir(); return $homeDir === '/' ? $homeDir.'.config' : $homeDir.'/.config'; } /** * Get potential config directory paths. * * Returns `~/.psysh`, `%APPDATA%/PsySH` (when on Windows), and all * XDG Base Directory config directories: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @return string[] */ public function configDirs(): array { if ($this->configDir !== null) { return [$this->configDir]; } $configDirs = $this->getEnvArray('XDG_CONFIG_DIRS') ?: ['/etc/xdg']; return $this->allDirNames(\array_merge([$this->homeConfigDir()], $configDirs)); } /** * @deprecated */ public static function getConfigDirs(): array { return (new self())->configDirs(); } /** * Get potential home config directory paths. * * Returns `~/.psysh`, `%APPDATA%/PsySH` (when on Windows), and the * XDG Base Directory home config directory: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @deprecated * * @return string[] */ public static function getHomeConfigDirs(): array { // Not quite the same, but this is deprecated anyway /shrug return self::getConfigDirs(); } /** * Get the current home config directory. * * Returns the highest precedence home config directory which actually * exists. If none of them exists, returns the highest precedence home * config directory (`%APPDATA%/PsySH` on Windows, `~/.config/psysh` * everywhere else). * * @see self::homeConfigDir */ public function currentConfigDir(): string { if ($this->configDir !== null) { return $this->configDir; } $configDirs = $this->allDirNames([$this->homeConfigDir()]); foreach ($configDirs as $configDir) { if (@\is_dir($configDir)) { return $configDir; } } return $configDirs[0]; } /** * @deprecated */ public static function getCurrentConfigDir(): string { return (new self())->currentConfigDir(); } /** * Find real config files in config directories. * * @param string[] $names Config file names * * @return string[] */ public function configFiles(array $names): array { return $this->allRealFiles($this->configDirs(), $names); } /** * @deprecated */ public static function getConfigFiles(array $names, $configDir = null): array { return (new self(['configDir' => $configDir]))->configFiles($names); } /** * Get potential data directory paths. * * If a `dataDir` option was explicitly set, returns an array containing * just that directory. * * Otherwise, it returns `~/.psysh` and all XDG Base Directory data directories: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @return string[] */ public function dataDirs(): array { if ($this->dataDir !== null) { return [$this->dataDir]; } $homeDataDir = $this->getEnv('XDG_DATA_HOME') ?: $this->homeDir().'/.local/share'; $dataDirs = $this->getEnvArray('XDG_DATA_DIRS') ?: ['/usr/local/share', '/usr/share']; return $this->allDirNames(\array_merge([$homeDataDir], $dataDirs)); } /** * @deprecated */ public static function getDataDirs(): array { return (new self())->dataDirs(); } /** * Find real data files in config directories. * * @param string[] $names Config file names * * @return string[] */ public function dataFiles(array $names): array { return $this->allRealFiles($this->dataDirs(), $names); } /** * @deprecated */ public static function getDataFiles(array $names, $dataDir = null): array { return (new self(['dataDir' => $dataDir]))->dataFiles($names); } /** * Get a runtime directory. * * Defaults to `/psysh` inside the system's temp dir. */ public function runtimeDir(): string { if ($this->runtimeDir !== null) { return $this->runtimeDir; } // Fallback to a boring old folder in the system temp dir. $runtimeDir = $this->getEnv('XDG_RUNTIME_DIR') ?: \sys_get_temp_dir(); return \strtr($runtimeDir, '\\', '/').'/psysh'; } /** * @deprecated */ public static function getRuntimeDir(): string { return (new self())->runtimeDir(); } /** * Get a list of directories in PATH. * * If $PATH is unset/empty it defaults to '/usr/sbin:/usr/bin:/sbin:/bin'. * * @return string[] */ public function pathDirs(): array { return $this->getEnvArray('PATH') ?: ['/usr/sbin', '/usr/bin', '/sbin', '/bin']; } /** * Locate a command (an executable) in $PATH. * * Behaves like 'command -v COMMAND' or 'which COMMAND'. * If $PATH is unset/empty it defaults to '/usr/sbin:/usr/bin:/sbin:/bin'. * * @param string $command the executable to locate * * @return string */ public function which($command) { foreach ($this->pathDirs() as $path) { $fullpath = $path.\DIRECTORY_SEPARATOR.$command; if (@\is_file($fullpath) && @\is_executable($fullpath)) { return $fullpath; } } return null; } /** * Get all PsySH directory name candidates given a list of base directories. * * This expects that XDG-compatible directory paths will be passed in. * `psysh` will be added to each of $baseDirs, and we'll throw in `~/.psysh` * and a couple of Windows-friendly paths as well. * * @param string[] $baseDirs base directory paths * * @return string[] */ private function allDirNames(array $baseDirs): array { $dirs = \array_map(function ($dir) { return \strtr($dir, '\\', '/').'/psysh'; }, $baseDirs); // Add ~/.psysh if ($home = $this->getEnv('HOME')) { $dirs[] = \strtr($home, '\\', '/').'/.psysh'; } // Add some Windows specific ones :) if (\defined('PHP_WINDOWS_VERSION_MAJOR')) { if ($appData = $this->getEnv('APPDATA')) { // AppData gets preference \array_unshift($dirs, \strtr($appData, '\\', '/').'/PsySH'); } if ($windowsHomeDir = $this->windowsHomeDir()) { $dir = \strtr($windowsHomeDir, '\\', '/').'/.psysh'; if (!\in_array($dir, $dirs)) { $dirs[] = $dir; } } } return $dirs; } /** * Given a list of directories, and a list of filenames, find the ones that * are real files. * * @return string[] */ private function allRealFiles(array $dirNames, array $fileNames): array { $files = []; foreach ($dirNames as $dir) { foreach ($fileNames as $name) { $file = $dir.'/'.$name; if (@\is_file($file)) { $files[] = $file; } } } return $files; } /** * Ensure that $dir exists and is writable. * * Generates E_USER_NOTICE error if the directory is not writable or creatable. * * @param string $dir * * @return bool False if directory exists but is not writeable, or cannot be created */ public static function ensureDir(string $dir): bool { if (!\is_dir($dir)) { // Just try making it and see if it works @\mkdir($dir, 0700, true); } if (!\is_dir($dir) || !\is_writable($dir)) { \trigger_error(\sprintf('Writing to directory %s is not allowed.', $dir), \E_USER_NOTICE); return false; } return true; } /** * Ensure that $file exists and is writable, make the parent directory if necessary. * * Generates E_USER_NOTICE error if either $file or its directory is not writable. * * @param string $file * * @return string|false Full path to $file, or false if file is not writable */ public static function touchFileWithMkdir(string $file) { if (\file_exists($file)) { if (\is_writable($file)) { return $file; } \trigger_error(\sprintf('Writing to %s is not allowed.', $file), \E_USER_NOTICE); return false; } if (!self::ensureDir(\dirname($file))) { return false; } \touch($file); return $file; } private function getEnv($key) { return $this->env->get($key); } private function getEnvArray($key) { if ($value = $this->getEnv($key)) { return \explode(\PATH_SEPARATOR, $value); } return null; } } psysh/src/Readline/Userland.php 0000644 00000007203 15024771425 0012523 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\Readline; use Psy\Exception\BreakException; use Psy\Readline\Hoa\Console as HoaConsole; use Psy\Readline\Hoa\ConsoleCursor as HoaConsoleCursor; use Psy\Readline\Hoa\ConsoleInput as HoaConsoleInput; use Psy\Readline\Hoa\ConsoleOutput as HoaConsoleOutput; use Psy\Readline\Hoa\ConsoleTput as HoaConsoleTput; use Psy\Readline\Hoa\Readline as HoaReadline; use Psy\Readline\Hoa\Ustring as HoaUstring; /** * Userland Readline implementation. */ class Userland implements Readline { /** @var HoaReadline */ private $hoaReadline; /** @var string|null */ private $lastPrompt; private $tput; private $input; private $output; public static function isSupported(): bool { static::bootstrapHoa(); return HoaUstring::checkMbString() && HoaConsoleTput::isSupported(); } /** * {@inheritdoc} */ public static function supportsBracketedPaste(): bool { return false; } /** * Doesn't (currently) support history file, size or erase dupes configs. */ public function __construct($historyFile = null, $historySize = 0, $eraseDups = false) { static::bootstrapHoa(true); $this->hoaReadline = new HoaReadline(); $this->hoaReadline->addMapping('\C-l', function () { $this->redisplay(); return HoaReadline::STATE_NO_ECHO; }); $this->tput = new HoaConsoleTput(); HoaConsole::setTput($this->tput); $this->input = new HoaConsoleInput(); HoaConsole::setInput($this->input); $this->output = new HoaConsoleOutput(); HoaConsole::setOutput($this->output); } /** * Bootstrap some things that Hoa used to do itself. */ public static function bootstrapHoa(bool $withTerminalResize = false) { // A side effect registers hoa:// stream wrapper \class_exists('Psy\Readline\Hoa\ProtocolWrapper'); // A side effect registers hoa://Library/Stream \class_exists('Psy\Readline\Hoa\Stream'); // A side effect binds terminal resize $withTerminalResize && \class_exists('Psy\Readline\Hoa\ConsoleWindow'); } /** * {@inheritdoc} */ public function addHistory(string $line): bool { $this->hoaReadline->addHistory($line); return true; } /** * {@inheritdoc} */ public function clearHistory(): bool { $this->hoaReadline->clearHistory(); return true; } /** * {@inheritdoc} */ public function listHistory(): array { $i = 0; $list = []; while (($item = $this->hoaReadline->getHistory($i++)) !== null) { $list[] = $item; } return $list; } /** * {@inheritdoc} */ public function readHistory(): bool { return true; } /** * {@inheritdoc} * * @throws BreakException if user hits Ctrl+D * * @return string */ public function readline(string $prompt = null) { $this->lastPrompt = $prompt; return $this->hoaReadline->readLine($prompt); } /** * {@inheritdoc} */ public function redisplay() { $currentLine = $this->hoaReadline->getLine(); HoaConsoleCursor::clear('all'); echo $this->lastPrompt, $currentLine; } /** * {@inheritdoc} */ public function writeHistory(): bool { return true; } } psysh/src/Readline/GNUReadline.php 0000644 00000010044 15024771425 0013040 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\Readline; /** * A Readline interface implementation for GNU Readline. * * This is by far the coolest way to do it, if you can. * * Oh well. */ class GNUReadline implements Readline { /** @var string|false */ protected $historyFile; /** @var int */ protected $historySize; /** @var bool */ protected $eraseDups; /** * GNU Readline is supported iff `readline_list_history` is defined. PHP * decided it would be awesome to swap out GNU Readline for Libedit, but * they ended up shipping an incomplete implementation. So we've got this. * * NOTE: As of PHP 7.4, PHP sometimes has history support in the Libedit * wrapper, so that will use the GNUReadline implementation as well! */ public static function isSupported(): bool { return \function_exists('readline') && \function_exists('readline_list_history'); } /** * Check whether this readline implementation supports bracketed paste. * * Currently, the GNU readline implementation does, but the libedit wrapper does not. */ public static function supportsBracketedPaste(): bool { return self::isSupported() && \stripos(\readline_info('library_version') ?: '', 'editline') === false; } public function __construct($historyFile = null, $historySize = 0, $eraseDups = false) { $this->historyFile = ($historyFile !== null) ? $historyFile : false; $this->historySize = $historySize; $this->eraseDups = $eraseDups; \readline_info('readline_name', 'psysh'); } /** * {@inheritdoc} */ public function addHistory(string $line): bool { if ($res = \readline_add_history($line)) { $this->writeHistory(); } return $res; } /** * {@inheritdoc} */ public function clearHistory(): bool { if ($res = \readline_clear_history()) { $this->writeHistory(); } return $res; } /** * {@inheritdoc} */ public function listHistory(): array { return \readline_list_history(); } /** * {@inheritdoc} */ public function readHistory(): bool { \readline_read_history(); \readline_clear_history(); return \readline_read_history($this->historyFile); } /** * {@inheritdoc} */ public function readline(string $prompt = null) { return \readline($prompt); } /** * {@inheritdoc} */ public function redisplay() { \readline_redisplay(); } /** * {@inheritdoc} */ public function writeHistory(): bool { // We have to write history first, since it is used // by Libedit to list history if ($this->historyFile !== false) { $res = \readline_write_history($this->historyFile); } else { $res = true; } if (!$res || !$this->eraseDups && !$this->historySize > 0) { return $res; } $hist = $this->listHistory(); if (!$hist) { return true; } if ($this->eraseDups) { // flip-flip technique: removes duplicates, latest entries win. $hist = \array_flip(\array_flip($hist)); // sort on keys to get the order back \ksort($hist); } if ($this->historySize > 0) { $histsize = \count($hist); if ($histsize > $this->historySize) { $hist = \array_slice($hist, $histsize - $this->historySize); } } \readline_clear_history(); foreach ($hist as $line) { \readline_add_history($line); } if ($this->historyFile !== false) { return \readline_write_history($this->historyFile); } return true; } } psysh/src/Readline/Transient.php 0000644 00000005677 15024771425 0012732 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\Readline; use Psy\Exception\BreakException; /** * An array-based Readline emulation implementation. */ class Transient implements Readline { private $history; private $historySize; private $eraseDups; private $stdin; /** * Transient Readline is always supported. * * {@inheritdoc} */ public static function isSupported(): bool { return true; } /** * {@inheritdoc} */ public static function supportsBracketedPaste(): bool { return false; } /** * Transient Readline constructor. */ public function __construct($historyFile = null, $historySize = 0, $eraseDups = false) { // don't do anything with the history file... $this->history = []; $this->historySize = $historySize; $this->eraseDups = $eraseDups; } /** * {@inheritdoc} */ public function addHistory(string $line): bool { if ($this->eraseDups) { if (($key = \array_search($line, $this->history)) !== false) { unset($this->history[$key]); } } $this->history[] = $line; if ($this->historySize > 0) { $histsize = \count($this->history); if ($histsize > $this->historySize) { $this->history = \array_slice($this->history, $histsize - $this->historySize); } } $this->history = \array_values($this->history); return true; } /** * {@inheritdoc} */ public function clearHistory(): bool { $this->history = []; return true; } /** * {@inheritdoc} */ public function listHistory(): array { return $this->history; } /** * {@inheritdoc} */ public function readHistory(): bool { return true; } /** * {@inheritdoc} * * @throws BreakException if user hits Ctrl+D * * @return false|string */ public function readline(string $prompt = null) { echo $prompt; return \rtrim(\fgets($this->getStdin()), "\n\r"); } /** * {@inheritdoc} */ public function redisplay() { // noop } /** * {@inheritdoc} */ public function writeHistory(): bool { return true; } /** * Get a STDIN file handle. * * @throws BreakException if user hits Ctrl+D * * @return resource */ private function getStdin() { if (!isset($this->stdin)) { $this->stdin = \fopen('php://stdin', 'r'); } if (\feof($this->stdin)) { throw new BreakException('Ctrl+D'); } return $this->stdin; } } psysh/src/Readline/Readline.php 0000644 00000003447 15024771425 0012477 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\Readline; /** * An interface abstracting the various readline_* functions. */ interface Readline { /** * @param string|false $historyFile * @param int|null $historySize * @param bool|null $eraseDups */ public function __construct($historyFile = null, $historySize = 0, $eraseDups = false); /** * Check whether this Readline class is supported by the current system. */ public static function isSupported(): bool; /** * Check whether this Readline class supports bracketed paste. */ public static function supportsBracketedPaste(): bool; /** * Add a line to the command history. * * @param string $line * * @return bool Success */ public function addHistory(string $line): bool; /** * Clear the command history. * * @return bool Success */ public function clearHistory(): bool; /** * List the command history. * * @return string[] */ public function listHistory(): array; /** * Read the command history. * * @return bool Success */ public function readHistory(): bool; /** * Read a single line of input from the user. * * @param string|null $prompt * * @return false|string */ public function readline(string $prompt = null); /** * Redraw readline to redraw the display. */ public function redisplay(); /** * Write the command history to a file. * * @return bool Success */ public function writeHistory(): bool; } psysh/src/Readline/HoaConsole.php 0000644 00000000557 15024771425 0013005 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\Readline; /** * Hoa\Console Readline implementation. * * @deprecated, use Userland readline */ class HoaConsole extends Userland { } psysh/src/Readline/Hoa/ProtocolNodeLibrary.php 0000644 00000006064 15024771425 0015415 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * The `hoa://Library/` node. */ class ProtocolNodeLibrary extends ProtocolNode { /** * Queue of the component. */ public function reach(string $queue = null) { $withComposer = \class_exists('Composer\Autoload\ClassLoader', false) || ('cli' === \PHP_SAPI && \file_exists(__DIR__.DS.'..'.DS.'..'.DS.'..'.DS.'..'.DS.'autoload.php')); if ($withComposer) { return parent::reach($queue); } if (!empty($queue)) { $head = $queue; if (false !== $pos = \strpos($queue, '/')) { $head = \substr($head, 0, $pos); $queue = \DIRECTORY_SEPARATOR.\substr($queue, $pos + 1); } else { $queue = null; } $out = []; foreach (\explode(';', $this->_reach) as $part) { $out[] = "\r".$part.\strtolower($head).$queue; } $out[] = "\r".\dirname(__DIR__, 5).$queue; return \implode(';', $out); } $out = []; foreach (\explode(';', $this->_reach) as $part) { $pos = \strrpos(\rtrim($part, \DIRECTORY_SEPARATOR), \DIRECTORY_SEPARATOR) + 1; $head = \substr($part, 0, $pos); $tail = \substr($part, $pos); $out[] = $head.\strtolower($tail); } $this->_reach = \implode(';', $out); return parent::reach($queue); } } psysh/src/Readline/Hoa/IteratorFileSystem.php 0000644 00000005372 15024771425 0015260 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Iterator\FileSystem. * * Extending the SPL FileSystemIterator class. */ class IteratorFileSystem extends \FilesystemIterator { /** * SplFileInfo classname. */ protected $_splFileInfoClass = null; /** * Constructor. * Please, see \FileSystemIterator::__construct() method. * We add the $splFileInfoClass parameter. */ public function __construct(string $path, int $flags = null, string $splFileInfoClass = null) { $this->_splFileInfoClass = $splFileInfoClass; if (null === $flags) { parent::__construct($path); } else { parent::__construct($path, $flags); } return; } /** * Current. * Please, see \FileSystemIterator::current() method. */ #[\ReturnTypeWillChange] public function current() { $out = parent::current(); if (null !== $this->_splFileInfoClass && $out instanceof \SplFileInfo) { $out->setInfoClass($this->_splFileInfoClass); $out = $out->getFileInfo(); } return $out; } } psysh/src/Readline/Hoa/Console.php 0000644 00000017461 15024771425 0013066 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console. * * A set of utils and helpers about the console. */ class Console { /** * Pipe mode: FIFO. */ const IS_FIFO = 0; /** * Pipe mode: character. */ const IS_CHARACTER = 1; /** * Pipe mode: directory. */ const IS_DIRECTORY = 2; /** * Pipe mode: block. */ const IS_BLOCK = 3; /** * Pipe mode: regular. */ const IS_REGULAR = 4; /** * Pipe mode: link. */ const IS_LINK = 5; /** * Pipe mode: socket. */ const IS_SOCKET = 6; /** * Pipe mode: whiteout. */ const IS_WHITEOUT = 7; /** * Advanced interaction is on. */ private static $_advanced = null; /** * Previous STTY configuration. */ private static $_old = null; /** * Mode. */ protected static $_mode = []; /** * Input. */ protected static $_input = null; /** * Output. */ protected static $_output = null; /** * Tput. */ protected static $_tput = null; /** * Prepare the environment for advanced interactions. */ public static function advancedInteraction(bool $force = false): bool { if (null !== self::$_advanced) { return self::$_advanced; } if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return self::$_advanced = false; } if (false === $force && true === \defined('STDIN') && false === self::isDirect(\STDIN)) { return self::$_advanced = false; } self::$_old = ConsoleProcessus::execute('stty -g < /dev/tty', false); ConsoleProcessus::execute('stty -echo -icanon min 1 time 0 < /dev/tty', false); return self::$_advanced = true; } /** * Restore previous interaction options. */ public static function restoreInteraction() { if (null === self::$_old) { return; } ConsoleProcessus::execute('stty '.self::$_old.' < /dev/tty', false); return; } /** * Get mode of a certain pipe. * Inspired by sys/stat.h. */ public static function getMode($pipe = \STDIN): int { $_pipe = (int) $pipe; if (isset(self::$_mode[$_pipe])) { return self::$_mode[$_pipe]; } $stat = \fstat($pipe); switch ($stat['mode'] & 0170000) { // named pipe (fifo). case 0010000: $mode = self::IS_FIFO; break; // character special. case 0020000: $mode = self::IS_CHARACTER; break; // directory. case 0040000: $mode = self::IS_DIRECTORY; break; // block special. case 0060000: $mode = self::IS_BLOCK; break; // regular. case 0100000: $mode = self::IS_REGULAR; break; // symbolic link. case 0120000: $mode = self::IS_LINK; break; // socket. case 0140000: $mode = self::IS_SOCKET; break; // whiteout. case 0160000: $mode = self::IS_WHITEOUT; break; default: $mode = -1; } return self::$_mode[$_pipe] = $mode; } /** * Check whether a certain pipe is a character device (keyboard, screen * etc.). * For example: * $ php Mode.php * In this case, self::isDirect(STDOUT) will return true. */ public static function isDirect($pipe): bool { return self::IS_CHARACTER === self::getMode($pipe); } /** * Check whether a certain pipe is a pipe. * For example: * $ php Mode.php | foobar * In this case, self::isPipe(STDOUT) will return true. */ public static function isPipe($pipe): bool { return self::IS_FIFO === self::getMode($pipe); } /** * Check whether a certain pipe is a redirection. * For example: * $ php Mode.php < foobar * In this case, self::isRedirection(STDIN) will return true. */ public static function isRedirection($pipe): bool { $mode = self::getMode($pipe); return self::IS_REGULAR === $mode || self::IS_DIRECTORY === $mode || self::IS_LINK === $mode || self::IS_SOCKET === $mode || self::IS_BLOCK === $mode; } /** * Set input layer. */ public static function setInput(ConsoleInput $input) { $old = static::$_input; static::$_input = $input; return $old; } /** * Get input layer. */ public static function getInput(): ConsoleInput { if (null === static::$_input) { static::$_input = new ConsoleInput(); } return static::$_input; } /** * Set output layer. */ public static function setOutput(ConsoleOutput $output) { $old = static::$_output; static::$_output = $output; return $old; } /** * Get output layer. */ public static function getOutput(): ConsoleOutput { if (null === static::$_output) { static::$_output = new ConsoleOutput(); } return static::$_output; } /** * Set tput. */ public static function setTput(ConsoleTput $tput) { $old = static::$_tput; static::$_tput = $tput; return $old; } /** * Get the current tput instance of the current process. */ public static function getTput(): ConsoleTput { if (null === static::$_tput) { static::$_tput = new ConsoleTput(); } return static::$_tput; } /** * Check whether we are running behind TMUX(1). */ public static function isTmuxRunning(): bool { return isset($_SERVER['TMUX']); } } /* * Restore interaction. */ \register_shutdown_function([Console::class, 'restoreInteraction']); psysh/src/Readline/Hoa/AutocompleterAggregate.php 0000644 00000006414 15024771425 0016112 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Readline\Autocompleter\Aggregate. * * Aggregate several autocompleters. */ class AutocompleterAggregate implements Autocompleter { /** * List of autocompleters. */ protected $_autocompleters = null; /** * Constructor. */ public function __construct(array $autocompleters) { $this->setAutocompleters($autocompleters); return; } /** * Complete a word. * Returns null for no word, a full-word or an array of full-words. */ public function complete(&$prefix) { foreach ($this->getAutocompleters() as $autocompleter) { $preg = \preg_match( '#('.$autocompleter->getWordDefinition().')$#u', $prefix, $match ); if (0 === $preg) { continue; } $_prefix = $match[0]; if (null === $out = $autocompleter->complete($_prefix)) { continue; } $prefix = $_prefix; return $out; } return null; } /** * Set/initialize list of autocompleters. */ protected function setAutocompleters(array $autocompleters) { $old = $this->_autocompleters; $this->_autocompleters = new \ArrayObject($autocompleters); return $old; } /** * Get list of autocompleters. */ public function getAutocompleters() { return $this->_autocompleters; } /** * Get definition of a word. */ public function getWordDefinition(): string { return '.*'; } } psysh/src/Readline/Hoa/FileGeneric.php 0000644 00000026261 15024771425 0013636 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Generic. * * Describe a super-file. */ abstract class FileGeneric extends Stream implements StreamPathable, StreamStatable, StreamTouchable { /** * Mode. */ protected $_mode = null; /** * Get filename component of path. */ public function getBasename(): string { return \basename($this->getStreamName()); } /** * Get directory name component of path. */ public function getDirname(): string { return \dirname($this->getStreamName()); } /** * Get size. */ public function getSize(): int { if (false === $this->getStatistic()) { return false; } return \filesize($this->getStreamName()); } /** * Get informations about a file. */ public function getStatistic(): array { return \fstat($this->getStream()); } /** * Get last access time of file. */ public function getATime(): int { return \fileatime($this->getStreamName()); } /** * Get inode change time of file. */ public function getCTime(): int { return \filectime($this->getStreamName()); } /** * Get file modification time. */ public function getMTime(): int { return \filemtime($this->getStreamName()); } /** * Get file group. */ public function getGroup(): int { return \filegroup($this->getStreamName()); } /** * Get file owner. */ public function getOwner(): int { return \fileowner($this->getStreamName()); } /** * Get file permissions. */ public function getPermissions(): int { return \fileperms($this->getStreamName()); } /** * Get file permissions as a string. * Result sould be interpreted like this: * * s: socket; * * l: symbolic link; * * -: regular; * * b: block special; * * d: directory; * * c: character special; * * p: FIFO pipe; * * u: unknown. */ public function getReadablePermissions(): string { $p = $this->getPermissions(); if (($p & 0xC000) === 0xC000) { $out = 's'; } elseif (($p & 0xA000) === 0xA000) { $out = 'l'; } elseif (($p & 0x8000) === 0x8000) { $out = '-'; } elseif (($p & 0x6000) === 0x6000) { $out = 'b'; } elseif (($p & 0x4000) === 0x4000) { $out = 'd'; } elseif (($p & 0x2000) === 0x2000) { $out = 'c'; } elseif (($p & 0x1000) === 0x1000) { $out = 'p'; } else { $out = 'u'; } $out .= (($p & 0x0100) ? 'r' : '-'). (($p & 0x0080) ? 'w' : '-'). (($p & 0x0040) ? (($p & 0x0800) ? 's' : 'x') : (($p & 0x0800) ? 'S' : '-')). (($p & 0x0020) ? 'r' : '-'). (($p & 0x0010) ? 'w' : '-'). (($p & 0x0008) ? (($p & 0x0400) ? 's' : 'x') : (($p & 0x0400) ? 'S' : '-')). (($p & 0x0004) ? 'r' : '-'). (($p & 0x0002) ? 'w' : '-'). (($p & 0x0001) ? (($p & 0x0200) ? 't' : 'x') : (($p & 0x0200) ? 'T' : '-')); return $out; } /** * Check if the file is readable. */ public function isReadable(): bool { return \is_readable($this->getStreamName()); } /** * Check if the file is writable. */ public function isWritable(): bool { return \is_writable($this->getStreamName()); } /** * Check if the file is executable. */ public function isExecutable(): bool { return \is_executable($this->getStreamName()); } /** * Clear file status cache. */ public function clearStatisticCache() { \clearstatcache(true, $this->getStreamName()); } /** * Clear all files status cache. */ public static function clearAllStatisticCaches() { \clearstatcache(); } /** * Set access and modification time of file. */ public function touch(int $time = null, int $atime = null): bool { if (null === $time) { $time = \time(); } if (null === $atime) { $atime = $time; } return \touch($this->getStreamName(), $time, $atime); } /** * Copy file. * Return the destination file path if succeed, false otherwise. */ public function copy(string $to, bool $force = StreamTouchable::DO_NOT_OVERWRITE): bool { $from = $this->getStreamName(); if ($force === StreamTouchable::DO_NOT_OVERWRITE && true === \file_exists($to)) { return true; } if (null === $this->getStreamContext()) { return @\copy($from, $to); } return @\copy($from, $to, $this->getStreamContext()->getContext()); } /** * Move a file. */ public function move( string $name, bool $force = StreamTouchable::DO_NOT_OVERWRITE, bool $mkdir = StreamTouchable::DO_NOT_MAKE_DIRECTORY ): bool { $from = $this->getStreamName(); if ($force === StreamTouchable::DO_NOT_OVERWRITE && true === \file_exists($name)) { return false; } if (StreamTouchable::MAKE_DIRECTORY === $mkdir) { FileDirectory::create( \dirname($name), FileDirectory::MODE_CREATE_RECURSIVE ); } if (null === $this->getStreamContext()) { return @\rename($from, $name); } return @\rename($from, $name, $this->getStreamContext()->getContext()); } /** * Delete a file. */ public function delete(): bool { if (null === $this->getStreamContext()) { return @\unlink($this->getStreamName()); } return @\unlink( $this->getStreamName(), $this->getStreamContext()->getContext() ); } /** * Change file group. */ public function changeGroup($group): bool { return \chgrp($this->getStreamName(), $group); } /** * Change file mode. */ public function changeMode(int $mode): bool { return \chmod($this->getStreamName(), $mode); } /** * Change file owner. */ public function changeOwner($user): bool { return \chown($this->getStreamName(), $user); } /** * Change the current umask. */ public static function umask(int $umask = null): int { if (null === $umask) { return \umask(); } return \umask($umask); } /** * Check if it is a file. */ public function isFile(): bool { return \is_file($this->getStreamName()); } /** * Check if it is a link. */ public function isLink(): bool { return \is_link($this->getStreamName()); } /** * Check if it is a directory. */ public function isDirectory(): bool { return \is_dir($this->getStreamName()); } /** * Check if it is a socket. */ public function isSocket(): bool { return \filetype($this->getStreamName()) === 'socket'; } /** * Check if it is a FIFO pipe. */ public function isFIFOPipe(): bool { return \filetype($this->getStreamName()) === 'fifo'; } /** * Check if it is character special file. */ public function isCharacterSpecial(): bool { return \filetype($this->getStreamName()) === 'char'; } /** * Check if it is block special. */ public function isBlockSpecial(): bool { return \filetype($this->getStreamName()) === 'block'; } /** * Check if it is an unknown type. */ public function isUnknown(): bool { return \filetype($this->getStreamName()) === 'unknown'; } /** * Set the open mode. */ protected function setMode(string $mode) { $old = $this->_mode; $this->_mode = $mode; return $old; } /** * Get the open mode. */ public function getMode() { return $this->_mode; } /** * Get inode. */ public function getINode(): int { return \fileinode($this->getStreamName()); } /** * Check if the system is case sensitive or not. */ public static function isCaseSensitive(): bool { return !( \file_exists(\mb_strtolower(__FILE__)) && \file_exists(\mb_strtoupper(__FILE__)) ); } /** * Get a canonicalized absolute pathname. */ public function getRealPath(): string { if (false === $out = \realpath($this->getStreamName())) { return $this->getStreamName(); } return $out; } /** * Get file extension (if exists). */ public function getExtension(): string { return \pathinfo( $this->getStreamName(), \PATHINFO_EXTENSION ); } /** * Get filename without extension. */ public function getFilename(): string { $file = \basename($this->getStreamName()); if (\defined('PATHINFO_FILENAME')) { return \pathinfo($file, \PATHINFO_FILENAME); } if (\strstr($file, '.')) { return \substr($file, 0, \strrpos($file, '.')); } return $file; } } psysh/src/Readline/Hoa/StreamBufferable.php 0000644 00000004674 15024771425 0014677 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Bufferable. * * Interface for bufferable streams. It's complementary to native buffer support * of Hoa\Stream (please, see *StreamBuffer*() methods). Classes implementing * this interface are able to create nested buffers, flush them etc. */ interface StreamBufferable extends IStream { /** * Start a new buffer. * The callable acts like a light filter. */ public function newBuffer($callable = null, int $size = null): int; /** * Flush the buffer. */ public function flush(); /** * Delete buffer. */ public function deleteBuffer(): bool; /** * Get bufffer level. */ public function getBufferLevel(): int; /** * Get buffer size. */ public function getBufferSize(): int; } psysh/src/Readline/Hoa/EventException.php 0000644 00000003364 15024771425 0014421 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Extending the `Hoa\Exception\Exception` class. */ class EventException extends Exception { } psysh/src/Readline/Hoa/Stream.php 0000644 00000035007 15024771425 0012713 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Stream. * * Static register for all streams (files, sockets etc.). */ abstract class Stream implements IStream, EventListenable { use EventListens; /** * Name index in the stream bucket. */ const NAME = 0; /** * Handler index in the stream bucket. */ const HANDLER = 1; /** * Resource index in the stream bucket. */ const RESOURCE = 2; /** * Context index in the stream bucket. */ const CONTEXT = 3; /** * Default buffer size. */ const DEFAULT_BUFFER_SIZE = 8192; /** * Current stream bucket. */ protected $_bucket = []; /** * Static stream register. */ private static $_register = []; /** * Buffer size (default is 8Ko). */ protected $_bufferSize = self::DEFAULT_BUFFER_SIZE; /** * Original stream name, given to the stream constructor. */ protected $_streamName = null; /** * Context name. */ protected $_context = null; /** * Whether the opening has been deferred. */ protected $_hasBeenDeferred = false; /** * Whether this stream is already opened by another handler. */ protected $_borrowing = false; /** * Set the current stream. * If not exists in the register, try to call the * `$this->_open()` method. Please, see the `self::_getStream()` method. */ public function __construct(string $streamName, string $context = null, bool $wait = false) { $this->_streamName = $streamName; $this->_context = $context; $this->_hasBeenDeferred = $wait; $this->setListener( new EventListener( $this, [ 'authrequire', 'authresult', 'complete', 'connect', 'failure', 'mimetype', 'progress', 'redirect', 'resolve', 'size', ] ) ); if (true === $wait) { return; } $this->open(); return; } /** * Get a stream in the register. * If the stream does not exist, try to open it by calling the * $handler->_open() method. */ private static function &_getStream( string $streamName, self $handler, string $context = null ): array { $name = \md5($streamName); if (null !== $context) { if (false === StreamContext::contextExists($context)) { throw new StreamException('Context %s was not previously declared, cannot retrieve '.'this context.', 0, $context); } $context = StreamContext::getInstance($context); } if (!isset(self::$_register[$name])) { self::$_register[$name] = [ self::NAME => $streamName, self::HANDLER => $handler, self::RESOURCE => $handler->_open($streamName, $context), self::CONTEXT => $context, ]; Event::register( 'hoa://Event/Stream/'.$streamName, $handler ); // Add :open-ready? Event::register( 'hoa://Event/Stream/'.$streamName.':close-before', $handler ); } else { $handler->_borrowing = true; } if (null === self::$_register[$name][self::RESOURCE]) { self::$_register[$name][self::RESOURCE] = $handler->_open($streamName, $context); } return self::$_register[$name]; } /** * Open the stream and return the associated resource. * Note: This method is protected, but do not forget that it could be * overloaded into a public context. */ abstract protected function &_open(string $streamName, StreamContext $context = null); /** * Close the current stream. * Note: this method is protected, but do not forget that it could be * overloaded into a public context. */ abstract protected function _close(): bool; /** * Open the stream. */ final public function open(): self { $context = $this->_context; if (true === $this->hasBeenDeferred()) { if (null === $context) { $handle = StreamContext::getInstance(\uniqid()); $handle->setParameters([ 'notification' => [$this, '_notify'], ]); $context = $handle->getId(); } elseif (true === StreamContext::contextExists($context)) { $handle = StreamContext::getInstance($context); $parameters = $handle->getParameters(); if (!isset($parameters['notification'])) { $handle->setParameters([ 'notification' => [$this, '_notify'], ]); } } } $this->_bufferSize = self::DEFAULT_BUFFER_SIZE; $this->_bucket = self::_getStream( $this->_streamName, $this, $context ); return $this; } /** * Close the current stream. */ final public function close() { $streamName = $this->getStreamName(); if (null === $streamName) { return; } $name = \md5($streamName); if (!isset(self::$_register[$name])) { return; } Event::notify( 'hoa://Event/Stream/'.$streamName.':close-before', $this, new EventBucket() ); if (false === $this->_close()) { return; } unset(self::$_register[$name]); $this->_bucket[self::HANDLER] = null; Event::unregister( 'hoa://Event/Stream/'.$streamName ); Event::unregister( 'hoa://Event/Stream/'.$streamName.':close-before' ); return; } /** * Get the current stream name. */ public function getStreamName() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::NAME]; } /** * Get the current stream. */ public function getStream() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::RESOURCE]; } /** * Get the current stream context. */ public function getStreamContext() { if (empty($this->_bucket)) { return null; } return $this->_bucket[self::CONTEXT]; } /** * Get stream handler according to its name. */ public static function getStreamHandler(string $streamName) { $name = \md5($streamName); if (!isset(self::$_register[$name])) { return null; } return self::$_register[$name][self::HANDLER]; } /** * Set the current stream. Useful to manage a stack of streams (e.g. socket * and select). Notice that it could be unsafe to use this method without * taking time to think about it two minutes. Resource of type “Unknown” is * considered as valid. */ public function _setStream($stream) { if (false === \is_resource($stream) && ('resource' !== \gettype($stream) || 'Unknown' !== \get_resource_type($stream))) { throw new StreamException('Try to change the stream resource with an invalid one; '.'given %s.', 1, \gettype($stream)); } $old = $this->_bucket[self::RESOURCE]; $this->_bucket[self::RESOURCE] = $stream; return $old; } /** * Check if the stream is opened. */ public function isOpened(): bool { return \is_resource($this->getStream()); } /** * Set the timeout period. */ public function setStreamTimeout(int $seconds, int $microseconds = 0): bool { return \stream_set_timeout($this->getStream(), $seconds, $microseconds); } /** * Whether the opening of the stream has been deferred. */ protected function hasBeenDeferred() { return $this->_hasBeenDeferred; } /** * Check whether the connection has timed out or not. * This is basically a shortcut of `getStreamMetaData` + the `timed_out` * index, but the resulting code is more readable. */ public function hasTimedOut(): bool { $metaData = $this->getStreamMetaData(); return true === $metaData['timed_out']; } /** * Set blocking/non-blocking mode. */ public function setStreamBlocking(bool $mode): bool { return \stream_set_blocking($this->getStream(), $mode); } /** * Set stream buffer. * Output using fwrite() (or similar function) is normally buffered at 8 Ko. * This means that if there are two processes wanting to write to the same * output stream, each is paused after 8 Ko of data to allow the other to * write. */ public function setStreamBuffer(int $buffer): bool { // Zero means success. $out = 0 === \stream_set_write_buffer($this->getStream(), $buffer); if (true === $out) { $this->_bufferSize = $buffer; } return $out; } /** * Disable stream buffering. * Alias of $this->setBuffer(0). */ public function disableStreamBuffer(): bool { return $this->setStreamBuffer(0); } /** * Get stream buffer size. */ public function getStreamBufferSize(): int { return $this->_bufferSize; } /** * Get stream wrapper name. */ public function getStreamWrapperName(): string { if (false === $pos = \strpos($this->getStreamName(), '://')) { return 'file'; } return \substr($this->getStreamName(), 0, $pos); } /** * Get stream meta data. */ public function getStreamMetaData(): array { return \stream_get_meta_data($this->getStream()); } /** * Whether this stream is already opened by another handler. */ public function isBorrowing(): bool { return $this->_borrowing; } /** * Notification callback. */ public function _notify( int $ncode, int $severity, $message, $code, $transferred, $max ) { static $_map = [ \STREAM_NOTIFY_AUTH_REQUIRED => 'authrequire', \STREAM_NOTIFY_AUTH_RESULT => 'authresult', \STREAM_NOTIFY_COMPLETED => 'complete', \STREAM_NOTIFY_CONNECT => 'connect', \STREAM_NOTIFY_FAILURE => 'failure', \STREAM_NOTIFY_MIME_TYPE_IS => 'mimetype', \STREAM_NOTIFY_PROGRESS => 'progress', \STREAM_NOTIFY_REDIRECTED => 'redirect', \STREAM_NOTIFY_RESOLVE => 'resolve', \STREAM_NOTIFY_FILE_SIZE_IS => 'size', ]; $this->getListener()->fire($_map[$ncode], new EventBucket([ 'code' => $code, 'severity' => $severity, 'message' => $message, 'transferred' => $transferred, 'max' => $max, ])); } /** * Call the $handler->close() method on each stream in the static stream * register. * This method does not check the return value of $handler->close(). Thus, * if a stream is persistent, the $handler->close() should do anything. It * is a very generic method. */ final public static function _Hoa_Stream() { foreach (self::$_register as $entry) { $entry[self::HANDLER]->close(); } return; } /** * Transform object to string. */ public function __toString(): string { return $this->getStreamName(); } /** * Close the stream when destructing. */ public function __destruct() { if (false === $this->isOpened()) { return; } $this->close(); return; } } /** * Class \Hoa\Stream\_Protocol. * * The `hoa://Library/Stream` node. * * @license New BSD License */ class _Protocol extends ProtocolNode { /** * Component's name. * * @var string */ protected $_name = 'Stream'; /** * ID of the component. * * @param string $id ID of the component * * @return mixed */ public function reachId(string $id) { return Stream::getStreamHandler($id); } } /* * Shutdown method. */ \register_shutdown_function([Stream::class, '_Hoa_Stream']); /** * Add the `hoa://Library/Stream` node. Should be use to reach/get an entry * in the stream register. */ $protocol = Protocol::getInstance(); $protocol['Library'][] = new _Protocol(); psysh/src/Readline/Hoa/StreamIn.php 0000644 00000005471 15024771425 0013204 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\In. * * Interface for input. */ interface StreamIn extends IStream { /** * Test for end-of-stream. */ public function eof(): bool; /** * Read n characters. */ public function read(int $length); /** * Alias of $this->read(). */ public function readString(int $length); /** * Read a character. * It could be equivalent to $this->read(1). */ public function readCharacter(); /** * Read a boolean. */ public function readBoolean(); /** * Read an integer. */ public function readInteger(int $length = 1); /** * Read a float. */ public function readFloat(int $length = 1); /** * Read an array. * In most cases, it could be an alias to the $this->scanf() method. */ public function readArray(); /** * Read a line. */ public function readLine(); /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0); /** * Parse input from a stream according to a format. */ public function scanf(string $format): array; } psysh/src/Readline/Hoa/EventSource.php 0000644 00000003363 15024771425 0013722 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Each object which is listenable must implement this interface. */ interface EventSource { } psysh/src/Readline/Hoa/FileFinder.php 0000644 00000037426 15024771425 0013476 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Finder. * * This class allows to find files easily by using filters and flags. */ class FileFinder implements \IteratorAggregate { /** * SplFileInfo classname. */ protected $_splFileInfo = \SplFileInfo::class; /** * Paths where to look for. */ protected $_paths = []; /** * Max depth in recursion. */ protected $_maxDepth = -1; /** * Filters. */ protected $_filters = []; /** * Flags. */ protected $_flags = -1; /** * Types of files to handle. */ protected $_types = []; /** * What comes first: parent or child? */ protected $_first = -1; /** * Sorts. */ protected $_sorts = []; /** * Initialize. */ public function __construct() { $this->_flags = IteratorFileSystem::KEY_AS_PATHNAME | IteratorFileSystem::CURRENT_AS_FILEINFO | IteratorFileSystem::SKIP_DOTS; $this->_first = \RecursiveIteratorIterator::SELF_FIRST; return; } /** * Select a directory to scan. */ public function in($paths): self { if (!\is_array($paths)) { $paths = [$paths]; } foreach ($paths as $path) { if (1 === \preg_match('/[\*\?\[\]]/', $path)) { $iterator = new \CallbackFilterIterator( new \GlobIterator(\rtrim($path, \DIRECTORY_SEPARATOR)), function ($current) { return $current->isDir(); } ); foreach ($iterator as $fileInfo) { $this->_paths[] = $fileInfo->getPathname(); } } else { $this->_paths[] = $path; } } return $this; } /** * Set max depth for recursion. */ public function maxDepth(int $depth): self { $this->_maxDepth = $depth; return $this; } /** * Include files in the result. */ public function files(): self { $this->_types[] = 'file'; return $this; } /** * Include directories in the result. */ public function directories(): self { $this->_types[] = 'dir'; return $this; } /** * Include links in the result. */ public function links(): self { $this->_types[] = 'link'; return $this; } /** * Follow symbolink links. */ public function followSymlinks(bool $flag = true): self { if (true === $flag) { $this->_flags ^= IteratorFileSystem::FOLLOW_SYMLINKS; } else { $this->_flags |= IteratorFileSystem::FOLLOW_SYMLINKS; } return $this; } /** * Include files that match a regex. * Example: * $this->name('#\.php$#');. */ public function name(string $regex): self { $this->_filters[] = function (\SplFileInfo $current) use ($regex) { return 0 !== \preg_match($regex, $current->getBasename()); }; return $this; } /** * Exclude directories that match a regex. * Example: * $this->notIn('#^\.(git|hg)$#');. */ public function notIn(string $regex): self { $this->_filters[] = function (\SplFileInfo $current) use ($regex) { foreach (\explode(\DIRECTORY_SEPARATOR, $current->getPathname()) as $part) { if (0 !== \preg_match($regex, $part)) { return false; } } return true; }; return $this; } /** * Include files that respect a certain size. * The size is a string of the form: * operator number unit * where * • operator could be: <, <=, >, >= or =; * • number is a positive integer; * • unit could be: b (default), Kb, Mb, Gb, Tb, Pb, Eb, Zb, Yb. * Example: * $this->size('>= 12Kb');. */ public function size(string $size): self { if (0 === \preg_match('#^(<|<=|>|>=|=)\s*(\d+)\s*((?:[KMGTPEZY])b)?$#', $size, $matches)) { return $this; } $number = (float) ($matches[2]); $unit = $matches[3] ?? 'b'; $operator = $matches[1]; switch ($unit) { case 'b': break; // kilo case 'Kb': $number <<= 10; break; // mega. case 'Mb': $number <<= 20; break; // giga. case 'Gb': $number <<= 30; break; // tera. case 'Tb': $number *= 1099511627776; break; // peta. case 'Pb': $number *= 1024 ** 5; break; // exa. case 'Eb': $number *= 1024 ** 6; break; // zetta. case 'Zb': $number *= 1024 ** 7; break; // yota. case 'Yb': $number *= 1024 ** 8; break; } $filter = null; switch ($operator) { case '<': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() < $number; }; break; case '<=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() <= $number; }; break; case '>': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() > $number; }; break; case '>=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() >= $number; }; break; case '=': $filter = function (\SplFileInfo $current) use ($number) { return $current->getSize() === $number; }; break; } $this->_filters[] = $filter; return $this; } /** * Whether we should include dots or not (respectively . and ..). */ public function dots(bool $flag = true): self { if (true === $flag) { $this->_flags ^= IteratorFileSystem::SKIP_DOTS; } else { $this->_flags |= IteratorFileSystem::SKIP_DOTS; } return $this; } /** * Include files that are owned by a certain owner. */ public function owner(int $owner): self { $this->_filters[] = function (\SplFileInfo $current) use ($owner) { return $current->getOwner() === $owner; }; return $this; } /** * Format date. * Date can have the following syntax: * date * since date * until date * If the date does not have the “ago” keyword, it will be added. * Example: “42 hours” is equivalent to “since 42 hours” which is equivalent * to “since 42 hours ago”. */ protected function formatDate(string $date, &$operator): int { $operator = -1; if (0 === \preg_match('#\bago\b#', $date)) { $date .= ' ago'; } if (0 !== \preg_match('#^(since|until)\b(.+)$#', $date, $matches)) { $time = \strtotime($matches[2]); if ('until' === $matches[1]) { $operator = 1; } } else { $time = \strtotime($date); } return $time; } /** * Include files that have been changed from a certain date. * Example: * $this->changed('since 13 days');. */ public function changed(string $date): self { $time = $this->formatDate($date, $operator); if (-1 === $operator) { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getCTime() >= $time; }; } else { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getCTime() < $time; }; } return $this; } /** * Include files that have been modified from a certain date. * Example: * $this->modified('since 13 days');. */ public function modified(string $date): self { $time = $this->formatDate($date, $operator); if (-1 === $operator) { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getMTime() >= $time; }; } else { $this->_filters[] = function (\SplFileInfo $current) use ($time) { return $current->getMTime() < $time; }; } return $this; } /** * Add your own filter. * The callback will receive 3 arguments: $current, $key and $iterator. It * must return a boolean: true to include the file, false to exclude it. * Example: * // Include files that are readable * $this->filter(function ($current) { * return $current->isReadable(); * });. */ public function filter($callback): self { $this->_filters[] = $callback; return $this; } /** * Sort result by name. * If \Collator exists (from ext/intl), the $locale argument will be used * for its constructor. Else, strcmp() will be used. * Example: * $this->sortByName('fr_FR');. */ public function sortByName(string $locale = 'root'): self { if (true === \class_exists('Collator', false)) { $collator = new \Collator($locale); $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) use ($collator) { return $collator->compare($a->getPathname(), $b->getPathname()); }; } else { $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { return \strcmp($a->getPathname(), $b->getPathname()); }; } return $this; } /** * Sort result by size. * Example: * $this->sortBySize();. */ public function sortBySize(): self { $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { return $a->getSize() < $b->getSize(); }; return $this; } /** * Add your own sort. * The callback will receive 2 arguments: $a and $b. Please see the uasort() * function. * Example: * // Sort files by their modified time. * $this->sort(function ($a, $b) { * return $a->getMTime() < $b->getMTime(); * });. */ public function sort($callable): self { $this->_sorts[] = $callable; return $this; } /** * Child comes first when iterating. */ public function childFirst(): self { $this->_first = \RecursiveIteratorIterator::CHILD_FIRST; return $this; } /** * Get the iterator. */ public function getIterator() { $_iterator = new \AppendIterator(); $types = $this->getTypes(); if (!empty($types)) { $this->_filters[] = function (\SplFileInfo $current) use ($types) { return \in_array($current->getType(), $types); }; } $maxDepth = $this->getMaxDepth(); $splFileInfo = $this->getSplFileInfo(); foreach ($this->getPaths() as $path) { if (1 === $maxDepth) { $iterator = new \IteratorIterator( new IteratorRecursiveDirectory( $path, $this->getFlags(), $splFileInfo ), $this->getFirst() ); } else { $iterator = new \RecursiveIteratorIterator( new IteratorRecursiveDirectory( $path, $this->getFlags(), $splFileInfo ), $this->getFirst() ); if (1 < $maxDepth) { $iterator->setMaxDepth($maxDepth - 1); } } $_iterator->append($iterator); } foreach ($this->getFilters() as $filter) { $_iterator = new \CallbackFilterIterator( $_iterator, $filter ); } $sorts = $this->getSorts(); if (empty($sorts)) { return $_iterator; } $array = \iterator_to_array($_iterator); foreach ($sorts as $sort) { \uasort($array, $sort); } return new \ArrayIterator($array); } /** * Set SplFileInfo classname. */ public function setSplFileInfo(string $splFileInfo): string { $old = $this->_splFileInfo; $this->_splFileInfo = $splFileInfo; return $old; } /** * Get SplFileInfo classname. */ public function getSplFileInfo(): string { return $this->_splFileInfo; } /** * Get all paths. */ protected function getPaths(): array { return $this->_paths; } /** * Get max depth. */ public function getMaxDepth(): int { return $this->_maxDepth; } /** * Get types. */ public function getTypes(): array { return $this->_types; } /** * Get filters. */ protected function getFilters(): array { return $this->_filters; } /** * Get sorts. */ protected function getSorts(): array { return $this->_sorts; } /** * Get flags. */ public function getFlags(): int { return $this->_flags; } /** * Get first. */ public function getFirst(): int { return $this->_first; } } psysh/src/Readline/Hoa/ProtocolWrapper.php 0000644 00000032602 15024771425 0014620 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Stream wrapper for the `hoa://` protocol. */ class ProtocolWrapper { /** * Opened stream as a resource. */ private $_stream = null; /** * Stream name (filename). */ private $_streamName = null; /** * Stream context (given by the streamWrapper class) as a resource. */ public $context = null; /** * Get the real path of the given URL. * Could return false if the path cannot be reached. */ public static function realPath(string $path, bool $exists = true) { return ProtocolNode::getRoot()->resolve($path, $exists); } /** * Retrieve the underlying resource. * * `$castAs` can be `STREAM_CAST_FOR_SELECT` when `stream_select` is * calling `stream_cast` or `STREAM_CAST_AS_STREAM` when `stream_cast` is * called for other uses. */ public function stream_cast(int $castAs) { return null; } /** * Closes a resource. * This method is called in response to `fclose`. * All resources that were locked, or allocated, by the wrapper should be * released. */ public function stream_close() { if (true === @\fclose($this->getStream())) { $this->_stream = null; $this->_streamName = null; } } /** * Tests for end-of-file on a file pointer. * This method is called in response to feof(). */ public function stream_eof(): bool { return \feof($this->getStream()); } /** * Flush the output. * This method is called in respond to fflush(). * If we have cached data in our stream but not yet stored it into the * underlying storage, we should do so now. */ public function stream_flush(): bool { return \fflush($this->getStream()); } /** * Advisory file locking. * This method is called in response to flock(), when file_put_contents() * (when flags contains LOCK_EX), stream_set_blocking() and when closing the * stream (LOCK_UN). * * Operation is one the following: * * LOCK_SH to acquire a shared lock (reader) ; * * LOCK_EX to acquire an exclusive lock (writer) ; * * LOCK_UN to release a lock (shared or exclusive) ; * * LOCK_NB if we don't want flock() to * block while locking (not supported on * Windows). */ public function stream_lock(int $operation): bool { return \flock($this->getStream(), $operation); } /** * Change stream options. * This method is called to set metadata on the stream. It is called when * one of the following functions is called on a stream URL: touch, chmod, * chown or chgrp. * * Option must be one of the following constant: * * STREAM_META_TOUCH, * * STREAM_META_OWNER_NAME, * * STREAM_META_OWNER, * * STREAM_META_GROUP_NAME, * * STREAM_META_GROUP, * * STREAM_META_ACCESS. * * Values are arguments of `touch`, `chmod`, `chown`, and `chgrp`. */ public function stream_metadata(string $path, int $option, $values): bool { $path = static::realPath($path, false); switch ($option) { case \STREAM_META_TOUCH: $arity = \count($values); if (0 === $arity) { $out = \touch($path); } elseif (1 === $arity) { $out = \touch($path, $values[0]); } else { $out = \touch($path, $values[0], $values[1]); } break; case \STREAM_META_OWNER_NAME: case \STREAM_META_OWNER: $out = \chown($path, $values); break; case \STREAM_META_GROUP_NAME: case \STREAM_META_GROUP: $out = \chgrp($path, $values); break; case \STREAM_META_ACCESS: $out = \chmod($path, $values); break; default: $out = false; } return $out; } /** * Open file or URL. * This method is called immediately after the wrapper is initialized (f.e. * by fopen() and file_get_contents()). */ public function stream_open(string $path, string $mode, int $options, &$openedPath): bool { $path = static::realPath($path, 'r' === $mode[0]); if (Protocol::NO_RESOLUTION === $path) { return false; } if (null === $this->context) { $openedPath = \fopen($path, $mode, $options & \STREAM_USE_PATH); } else { $openedPath = \fopen( $path, $mode, (bool) ($options & \STREAM_USE_PATH), $this->context ); } if (false === \is_resource($openedPath)) { return false; } $this->_stream = $openedPath; $this->_streamName = $path; return true; } /** * Read from stream. * This method is called in response to fread() and fgets(). */ public function stream_read(int $size): string { return \fread($this->getStream(), $size); } /** * Seek to specific location in a stream. * This method is called in response to fseek(). * The read/write position of the stream should be updated according to the * $offset and $whence. * * The possible values for `$whence` are: * * SEEK_SET to set position equal to $offset bytes, * * SEEK_CUR to set position to current location plus `$offset`, * * SEEK_END to set position to end-of-file plus `$offset`. */ public function stream_seek(int $offset, int $whence = \SEEK_SET): bool { return 0 === \fseek($this->getStream(), $offset, $whence); } /** * Retrieve information about a file resource. * This method is called in response to fstat(). */ public function stream_stat(): array { return \fstat($this->getStream()); } /** * Retrieve the current position of a stream. * This method is called in response to ftell(). */ public function stream_tell(): int { return \ftell($this->getStream()); } /** * Truncate a stream to a given length. */ public function stream_truncate(int $size): bool { return \ftruncate($this->getStream(), $size); } /** * Write to stream. * This method is called in response to fwrite(). */ public function stream_write(string $data): int { return \fwrite($this->getStream(), $data); } /** * Close directory handle. * This method is called in to closedir(). * Any resources which were locked, or allocated, during opening and use of * the directory stream should be released. */ public function dir_closedir() { \closedir($this->getStream()); $this->_stream = null; $this->_streamName = null; } /** * Open directory handle. * This method is called in response to opendir(). * * The `$options` input represents whether or not to enforce safe_mode * (0x04). It is not used here. */ public function dir_opendir(string $path, int $options): bool { $path = static::realPath($path); $handle = null; if (null === $this->context) { $handle = @\opendir($path); } else { $handle = @\opendir($path, $this->context); } if (false === $handle) { return false; } $this->_stream = $handle; $this->_streamName = $path; return true; } /** * Read entry from directory handle. * This method is called in response to readdir(). * * @return mixed */ public function dir_readdir() { return \readdir($this->getStream()); } /** * Rewind directory handle. * This method is called in response to rewinddir(). * Should reset the output generated by self::dir_readdir, i.e. the next * call to self::dir_readdir should return the first entry in the location * returned by self::dir_opendir. */ public function dir_rewinddir() { \rewinddir($this->getStream()); } /** * Create a directory. * This method is called in response to mkdir(). */ public function mkdir(string $path, int $mode, int $options): bool { if (null === $this->context) { return \mkdir( static::realPath($path, false), $mode, $options | \STREAM_MKDIR_RECURSIVE ); } return \mkdir( static::realPath($path, false), $mode, (bool) ($options | \STREAM_MKDIR_RECURSIVE), $this->context ); } /** * Rename a file or directory. * This method is called in response to rename(). * Should attempt to rename $from to $to. */ public function rename(string $from, string $to): bool { if (null === $this->context) { return \rename(static::realPath($from), static::realPath($to, false)); } return \rename( static::realPath($from), static::realPath($to, false), $this->context ); } /** * Remove a directory. * This method is called in response to rmdir(). * The `$options` input is a bitwise mask of values. It is not used here. */ public function rmdir(string $path, int $options): bool { if (null === $this->context) { return \rmdir(static::realPath($path)); } return \rmdir(static::realPath($path), $this->context); } /** * Delete a file. * This method is called in response to unlink(). */ public function unlink(string $path): bool { if (null === $this->context) { return \unlink(static::realPath($path)); } return \unlink(static::realPath($path), $this->context); } /** * Retrieve information about a file. * This method is called in response to all stat() related functions. * The `$flags` input holds additional flags set by the streams API. It * can hold one or more of the following values OR'd together. * STREAM_URL_STAT_LINK: for resource with the ability to link to other * resource (such as an HTTP location: forward, or a filesystem * symlink). This flag specified that only information about the link * itself should be returned, not the resource pointed to by the * link. This flag is set in response to calls to lstat(), is_link(), or * filetype(). STREAM_URL_STAT_QUIET: if this flag is set, our wrapper * should not raise any errors. If this flag is not set, we are * responsible for reporting errors using the trigger_error() function * during stating of the path. */ public function url_stat(string $path, int $flags) { $path = static::realPath($path); if (Protocol::NO_RESOLUTION === $path) { if ($flags & \STREAM_URL_STAT_QUIET) { return 0; } else { return \trigger_error( 'Path '.$path.' cannot be resolved.', \E_WARNING ); } } if ($flags & \STREAM_URL_STAT_LINK) { return @\lstat($path); } return @\stat($path); } /** * Get stream resource. */ public function getStream() { return $this->_stream; } /** * Get stream name. */ public function getStreamName() { return $this->_streamName; } } /* * Register the `hoa://` protocol. */ \stream_wrapper_register('hoa', ProtocolWrapper::class); psysh/src/Readline/Hoa/ConsoleWindow.php 0000644 00000030231 15024771425 0014244 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Allow to manipulate the window. * * We can listen the event channel hoa://Event/Console/Window:resize to detect * if the window has been resized. Please, see the constructor documentation to * get more informations. */ class ConsoleWindow implements EventSource { /** * Singleton (only for events). */ private static $_instance = null; /** * Set the event channel. * We need to declare(ticks = 1) in the main script to ensure that the event * is fired. Also, we need the pcntl_signal() function enabled. */ private function __construct() { Event::register( 'hoa://Event/Console/Window:resize', $this ); return; } /** * Singleton. */ public static function getInstance(): self { if (null === static::$_instance) { static::$_instance = new self(); } return static::$_instance; } /** * Set size to X lines and Y columns. */ public static function setSize(int $x, int $y) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } Console::getOutput()->writeAll("\033[8;".$y.';'.$x.'t'); return; } /** * Get current size (x and y) of the window. */ public static function getSize(): array { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { $modecon = \explode("\n", \ltrim(ConsoleProcessus::execute('mode con'))); $_y = \trim($modecon[2]); \preg_match('#[^:]+:\s*([0-9]+)#', $_y, $matches); $y = (int) $matches[1]; $_x = \trim($modecon[3]); \preg_match('#[^:]+:\s*([0-9]+)#', $_x, $matches); $x = (int) $matches[1]; return [ 'x' => $x, 'y' => $y, ]; } $term = ''; if (isset($_SERVER['TERM'])) { $term = 'TERM="'.$_SERVER['TERM'].'" '; } $command = $term.'tput cols && '.$term.'tput lines'; $tput = ConsoleProcessus::execute($command, false); if (!empty($tput)) { list($x, $y) = \explode("\n", $tput); return [ 'x' => (int) $x, 'y' => (int) $y, ]; } // DECSLPP. Console::getOutput()->writeAll("\033[18t"); $input = Console::getInput(); // Read \033[8;y;xt. $input->read(4); // skip \033, [, 8 and ;. $x = null; $y = null; $handle = &$y; while (true) { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$x; break; case 't': break 2; default: if (false === \ctype_digit($char)) { break 2; } $handle .= $char; } } if (null === $x || null === $y) { return [ 'x' => 0, 'y' => 0, ]; } return [ 'x' => (int) $x, 'y' => (int) $y, ]; } /** * Move to X and Y (in pixels). */ public static function moveTo(int $x, int $y) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033[3;".$x.';'.$y.'t'); return; } /** * Get current position (x and y) of the window (in pixels). */ public static function getPosition(): array { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return ['x' => 0, 'y' => 0]; } // DECSLPP. Console::getOutput()->writeAll("\033[13t"); $input = Console::getInput(); // Read \033[3;x;yt. $input->read(4); // skip \033, [, 3 and ;. $x = null; $y = null; $handle = &$x; while (true) { $char = $input->readCharacter(); switch ($char) { case ';': $handle = &$y; break; case 't': break 2; default: $handle .= $char; } } return [ 'x' => (int) $x, 'y' => (int) $y, ]; } /** * Scroll whole page. * Directions can be: * • u, up, ↑ : scroll whole page up; * • d, down, ↓ : scroll whole page down. * Directions can be concatenated by a single space. */ public static function scroll(string $directions, int $repeat = 1) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } if (1 > $repeat) { return; } elseif (1 === $repeat) { $handle = \explode(' ', $directions); } else { $handle = \explode(' ', $directions, 1); } $tput = Console::getTput(); $count = ['up' => 0, 'down' => 0]; foreach ($handle as $direction) { switch ($direction) { case 'u': case 'up': case '↑': ++$count['up']; break; case 'd': case 'down': case '↓': ++$count['down']; break; } } $output = Console::getOutput(); if (0 < $count['up']) { $output->writeAll( \str_replace( '%p1%d', $count['up'] * $repeat, $tput->get('parm_index') ) ); } if (0 < $count['down']) { $output->writeAll( \str_replace( '%p1%d', $count['down'] * $repeat, $tput->get('parm_rindex') ) ); } return; } /** * Minimize the window. */ public static function minimize() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033[2t"); return; } /** * Restore the window (de-minimize). */ public static function restore() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } Console::getOutput()->writeAll("\033[1t"); return; } /** * Raise the window to the front of the stacking order. */ public static function raise() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } Console::getOutput()->writeAll("\033[5t"); return; } /** * Lower the window to the bottom of the stacking order. */ public static function lower() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } Console::getOutput()->writeAll("\033[6t"); return; } /** * Set title. */ public static function setTitle(string $title) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033]0;".$title."\033\\"); return; } /** * Get title. */ public static function getTitle() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return null; } // DECSLPP. Console::getOutput()->writeAll("\033[21t"); $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = []; $except = []; $out = null; if (0 === \stream_select($read, $write, $except, 0, 50000)) { return $out; } // Read \033]l<title>\033\ $input->read(3); // skip \033, ] and l. while (true) { $char = $input->readCharacter(); if ("\033" === $char) { $chaar = $input->readCharacter(); if ('\\' === $chaar) { break; } $char .= $chaar; } $out .= $char; } return $out; } /** * Get label. */ public static function getLabel() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return null; } // DECSLPP. Console::getOutput()->writeAll("\033[20t"); $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = []; $except = []; $out = null; if (0 === \stream_select($read, $write, $except, 0, 50000)) { return $out; } // Read \033]L<label>\033\ $input->read(3); // skip \033, ] and L. while (true) { $char = $input->readCharacter(); if ("\033" === $char) { $chaar = $input->readCharacter(); if ('\\' === $chaar) { break; } $char .= $chaar; } $out .= $char; } return $out; } /** * Refresh the window. */ public static function refresh() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } // DECSLPP. Console::getOutput()->writeAll("\033[7t"); return; } /** * Set clipboard value. */ public static function copy(string $data) { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } $out = "\033]52;;".\base64_encode($data)."\033\\"; $output = Console::getOutput(); $considerMultiplexer = $output->considerMultiplexer(true); $output->writeAll($out); $output->considerMultiplexer($considerMultiplexer); return; } } /* * Advanced interaction. */ Console::advancedInteraction(); /* * Event. */ if (\function_exists('pcntl_signal')) { ConsoleWindow::getInstance(); \pcntl_signal( \SIGWINCH, function () { static $_window = null; if (null === $_window) { $_window = ConsoleWindow::getInstance(); } Event::notify( 'hoa://Event/Console/Window:resize', $_window, new EventBucket([ 'size' => ConsoleWindow::getSize(), ]) ); } ); } psysh/src/Readline/Hoa/FileException.php 0000644 00000003465 15024771425 0014221 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Exception. * * Extending the \Hoa\Exception\Exception class. * * @license New BSD License */ class FileException extends Exception { } psysh/src/Readline/Hoa/FileLinkReadWrite.php 0000644 00000015476 15024771425 0014774 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Link\ReadWrite. * * File handler. */ class FileLinkReadWrite extends FileLink implements StreamIn, StreamOut { /** * Open a file. */ public function __construct( string $streamName, string $mode = parent::MODE_APPEND_READ_WRITE, string $context = null, bool $wait = false ) { parent::__construct($streamName, $mode, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { static $createModes = [ parent::MODE_READ_WRITE, parent::MODE_TRUNCATE_READ_WRITE, parent::MODE_APPEND_READ_WRITE, parent::MODE_CREATE_READ_WRITE, ]; if (!\in_array($this->getMode(), $createModes)) { throw new FileException('Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), \implode(', ', $createModes)]); } \preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] === 'file') || !isset($match[1])) && !\file_exists($streamName) && parent::MODE_READ_WRITE === $this->getMode()) { throw new FileDoesNotExistException('File %s does not exist.', 1, $streamName); } $out = parent::_open($streamName, $context); return $out; } /** * Test for end-of-file. */ public function eof(): bool { return \feof($this->getStream()); } /** * Read n characters. */ public function read(int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 2, $length); } return \fread($this->getStream(), $length); } /** * Alias of $this->read(). */ public function readString(int $length) { return $this->read($length); } /** * Read a character. */ public function readCharacter() { return \fgetc($this->getStream()); } /** * Read a boolean. */ public function readBoolean() { return (bool) $this->read(1); } /** * Read an integer. */ public function readInteger(int $length = 1) { return (int) $this->read($length); } /** * Read a float. */ public function readFloat(int $length = 1) { return (float) $this->read($length); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray(string $format = null) { return $this->scanf($format); } /** * Read a line. */ public function readLine() { return \fgets($this->getStream()); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0) { return \stream_get_contents($this->getStream(), -1, $offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format): array { return \fscanf($this->getStream(), $format); } /** * Write n characters. */ public function write(string $string, int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 3, $length); } return \fwrite($this->getStream(), $string, $length); } /** * Write a string. */ public function writeString(string $string) { $string = (string) $string; return $this->write($string, \strlen($string)); } /** * Write a character. */ public function writeCharacter(string $char) { return $this->write((string) $char[0], 1); } /** * Write a boolean. */ public function writeBoolean(bool $boolean) { return $this->write((string) (bool) $boolean, 1); } /** * Write an integer. */ public function writeInteger(int $integer) { $integer = (string) (int) $integer; return $this->write($integer, \strlen($integer)); } /** * Write a float. */ public function writeFloat(float $float) { $float = (string) (float) $float; return $this->write($float, \strlen($float)); } /** * Write an array. */ public function writeArray(array $array) { $array = \var_export($array, true); return $this->write($array, \strlen($array)); } /** * Write a line. */ public function writeLine(string $line) { if (false === $n = \strpos($line, "\n")) { return $this->write($line."\n", \strlen($line) + 1); } ++$n; return $this->write(\substr($line, 0, $n), $n); } /** * Write all, i.e. as much as possible. */ public function writeAll(string $string) { return $this->write($string, \strlen($string)); } /** * Truncate a file to a given length. */ public function truncate(int $size): bool { return \ftruncate($this->getStream(), $size); } } psysh/src/Readline/Hoa/StreamLockable.php 0000644 00000005060 15024771425 0014344 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Lockable. * * Interface for lockable input/output. * * @license New BSD License */ interface StreamLockable extends IStream { /** * Acquire a shared lock (reader). * * @const int */ const LOCK_SHARED = \LOCK_SH; /** * Acquire an exclusive lock (writer). * * @const int */ const LOCK_EXCLUSIVE = \LOCK_EX; /** * Release a lock (shared or exclusive). * * @const int */ const LOCK_RELEASE = \LOCK_UN; /** * If we do not want $this->lock() to block while locking. * * @const int */ const LOCK_NO_BLOCK = \LOCK_NB; /** * Portable advisory locking. * Should take a look at stream_supports_lock(). * * @param int $operation operation, use the self::LOCK_* constants * * @return bool */ public function lock(int $operation): bool; } psysh/src/Readline/Hoa/StreamContext.php 0000644 00000007055 15024771425 0014262 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Stream\Context. * * Make a multiton of stream contexts. */ class StreamContext { /** * Context ID. */ protected $_id = null; /** * @var resource */ protected $_context; /** * Multiton. */ protected static $_instances = []; /** * Construct a context. */ protected function __construct($id) { $this->_id = $id; $this->_context = \stream_context_create(); return; } /** * Multiton. */ public static function getInstance(string $id): self { if (false === static::contextExists($id)) { static::$_instances[$id] = new self($id); } return static::$_instances[$id]; } /** * Get context ID. */ public function getId(): string { return $this->_id; } /** * Check if a context exists. */ public static function contextExists(string $id): bool { return \array_key_exists($id, static::$_instances); } /** * Set options. * Please, see http://php.net/context. */ public function setOptions(array $options): bool { return \stream_context_set_option($this->getContext(), $options); } /** * Set parameters. * Please, see http://php.net/context.params. */ public function setParameters(array $parameters): bool { return \stream_context_set_params($this->getContext(), $parameters); } /** * Get options. */ public function getOptions(): array { return \stream_context_get_options($this->getContext()); } /** * Get parameters. */ public function getParameters(): array { return \stream_context_get_params($this->getContext()); } /** * Get context as a resource. */ public function getContext() { return $this->_context; } } psysh/src/Readline/Hoa/ProtocolNode.php 0000644 00000017657 15024771425 0014102 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Abstract class for all `hoa://`'s nodes. */ class ProtocolNode implements \ArrayAccess, \IteratorAggregate { /** * Node's name. */ protected $_name = null; /** * Path for the `reach` method. */ protected $_reach = null; /** * Children of the node. */ private $_children = []; /** * Construct a protocol's node. * If it is not a data object (i.e. if it does not extend this class to * overload the `$_name` attribute), we can set the `$_name` attribute * dynamically. This is useful to create a node on-the-fly. */ public function __construct(string $name = null, string $reach = null, array $children = []) { if (null !== $name) { $this->_name = $name; } if (null !== $reach) { $this->_reach = $reach; } foreach ($children as $child) { $this[] = $child; } return; } /** * Add a node. */ #[\ReturnTypeWillChange] public function offsetSet($name, $node) { if (!($node instanceof self)) { throw new ProtocolException('Protocol node must extend %s.', 0, __CLASS__); } if (empty($name)) { $name = $node->getName(); } if (empty($name)) { throw new ProtocolException('Cannot add a node to the `hoa://` protocol without a name.', 1); } $this->_children[$name] = $node; } /** * Get a specific node. */ public function offsetGet($name): self { if (!isset($this[$name])) { throw new ProtocolException('Node %s does not exist.', 2, $name); } return $this->_children[$name]; } /** * Check if a node exists. */ public function offsetExists($name): bool { return true === \array_key_exists($name, $this->_children); } /** * Remove a node. */ #[\ReturnTypeWillChange] public function offsetUnset($name) { unset($this->_children[$name]); } /** * Resolve a path, i.e. iterate the nodes tree and reach the queue of * the path. */ protected function _resolve(string $path, &$accumulator, string $id = null) { if (\substr($path, 0, 6) === 'hoa://') { $path = \substr($path, 6); } if (empty($path)) { return null; } if (null === $accumulator) { $accumulator = []; $posId = \strpos($path, '#'); if (false !== $posId) { $id = \substr($path, $posId + 1); $path = \substr($path, 0, $posId); } else { $id = null; } } $path = \trim($path, '/'); $pos = \strpos($path, '/'); if (false !== $pos) { $next = \substr($path, 0, $pos); } else { $next = $path; } if (isset($this[$next])) { if (false === $pos) { if (null === $id) { $this->_resolveChoice($this[$next]->reach(), $accumulator); return true; } $accumulator = null; return $this[$next]->reachId($id); } $tnext = $this[$next]; $this->_resolveChoice($tnext->reach(), $accumulator); return $tnext->_resolve(\substr($path, $pos + 1), $accumulator, $id); } $this->_resolveChoice($this->reach($path), $accumulator); return true; } /** * Resolve choices, i.e. a reach value has a “;”. */ protected function _resolveChoice($reach, &$accumulator) { if (null === $reach) { $reach = ''; } if (empty($accumulator)) { $accumulator = \explode(';', $reach); return; } if (false === \strpos($reach, ';')) { if (false !== $pos = \strrpos($reach, "\r")) { $reach = \substr($reach, $pos + 1); foreach ($accumulator as &$entry) { $entry = null; } } foreach ($accumulator as &$entry) { $entry .= $reach; } return; } $choices = \explode(';', $reach); $ref = $accumulator; $accumulator = []; foreach ($choices as $choice) { if (false !== $pos = \strrpos($choice, "\r")) { $choice = \substr($choice, $pos + 1); foreach ($ref as $entry) { $accumulator[] = $choice; } } else { foreach ($ref as $entry) { $accumulator[] = $entry.$choice; } } } unset($ref); return; } /** * Queue of the node. * Generic one. Must be overrided in children classes. */ public function reach(string $queue = null) { return empty($queue) ? $this->_reach : $queue; } /** * ID of the component. * Generic one. Should be overrided in children classes. */ public function reachId(string $id) { throw new ProtocolException('The node %s has no ID support (tried to reach #%s).', 4, [$this->getName(), $id]); } /** * Set a new reach value. */ public function setReach(string $reach) { $old = $this->_reach; $this->_reach = $reach; return $old; } /** * Get node's name. */ public function getName() { return $this->_name; } /** * Get reach's root. */ protected function getReach() { return $this->_reach; } /** * Get an iterator. */ public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->_children); } /** * Get root the protocol. */ public static function getRoot(): Protocol { return Protocol::getInstance(); } /** * Print a tree of component. */ public function __toString(): string { static $i = 0; $out = \str_repeat(' ', $i).$this->getName()."\n"; foreach ($this as $node) { ++$i; $out .= $node; --$i; } return $out; } } psysh/src/Readline/Hoa/StreamOut.php 0000644 00000005250 15024771425 0013400 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Out. * * Interface for output. */ interface StreamOut extends IStream { /** * Write n characters. */ public function write(string $string, int $length); /** * Write a string. */ public function writeString(string $string); /** * Write a character. */ public function writeCharacter(string $character); /** * Write a boolean. */ public function writeBoolean(bool $boolean); /** * Write an integer. */ public function writeInteger(int $integer); /** * Write a float. */ public function writeFloat(float $float); /** * Write an array. */ public function writeArray(array $array); /** * Write a line. */ public function writeLine(string $line); /** * Write all, i.e. as much as possible. */ public function writeAll(string $string); /** * Truncate a stream to a given length. */ public function truncate(int $size): bool; } psysh/src/Readline/Hoa/StreamException.php 0000644 00000003427 15024771425 0014573 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Stream\Exception. * * Extending the \Hoa\Exception\Exception class. */ class StreamException extends Exception { } psysh/src/Readline/Hoa/FileRead.php 0000644 00000011116 15024771425 0013126 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Read. * * File handler. */ class FileRead extends File implements StreamIn { /** * Open a file. */ public function __construct( string $streamName, string $mode = parent::MODE_READ, string $context = null, bool $wait = false ) { parent::__construct($streamName, $mode, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { static $createModes = [ parent::MODE_READ, ]; if (!\in_array($this->getMode(), $createModes)) { throw new FileException('Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), \implode(', ', $createModes)]); } \preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] === 'file') || !isset($match[1])) && !\file_exists($streamName)) { throw new FileDoesNotExistException('File %s does not exist.', 1, $streamName); } $out = parent::_open($streamName, $context); return $out; } /** * Test for end-of-file. */ public function eof(): bool { return \feof($this->getStream()); } /** * Read n characters. */ public function read(int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 2, $length); } return \fread($this->getStream(), $length); } /** * Alias of $this->read(). */ public function readString(int $length) { return $this->read($length); } /** * Read a character. */ public function readCharacter() { return \fgetc($this->getStream()); } /** * Read a boolean. */ public function readBoolean() { return (bool) $this->read(1); } /** * Read an integer. */ public function readInteger(int $length = 1) { return (int) $this->read($length); } /** * Read a float. */ public function readFloat(int $length = 1) { return (float) $this->read($length); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray(string $format = null) { return $this->scanf($format); } /** * Read a line. */ public function readLine() { return \fgets($this->getStream()); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0) { return \stream_get_contents($this->getStream(), -1, $offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format): array { return \fscanf($this->getStream(), $format); } } psysh/src/Readline/Hoa/ProtocolException.php 0000644 00000003365 15024771425 0015142 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Extends the `Hoa\Exception\Exception` class. */ class ProtocolException extends Exception { } psysh/src/Readline/Hoa/StreamPointable.php 0000644 00000004620 15024771425 0014546 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Pointable. * * Interface for pointable input/output. */ interface StreamPointable extends IStream { /** * Set position equal to $offset bytes. */ const SEEK_SET = \SEEK_SET; /** * Set position to current location plus $offset. */ const SEEK_CURRENT = \SEEK_CUR; /** * Set position to end-of-file plus $offset. */ const SEEK_END = \SEEK_END; /** * Rewind the position of a stream pointer. */ public function rewind(): bool; /** * Seek on a stream pointer. */ public function seek(int $offset, int $whence = self::SEEK_SET): int; /** * Get the current position of the stream pointer. */ public function tell(): int; } psysh/src/Readline/Hoa/FileLinkRead.php 0000644 00000013541 15024771425 0013750 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Link\Read. * * File handler. * * @license New BSD License */ class FileLinkRead extends FileLink implements StreamIn { /** * Open a file. * * @param string $streamName stream name * @param string $mode open mode, see the parent::MODE_* constants * @param string $context context ID (please, see the * \Hoa\Stream\Context class) * @param bool $wait differ opening or not */ public function __construct( string $streamName, string $mode = parent::MODE_READ, string $context = null, bool $wait = false ) { parent::__construct($streamName, $mode, $context, $wait); return; } /** * Open the stream and return the associated resource. * * @param string $streamName Stream name (e.g. path or URL). * @param \Hoa\Stream\Context $context context * * @return resource * * @throws \Hoa\File\Exception\FileDoesNotExist * @throws \Hoa\File\Exception */ protected function &_open(string $streamName, StreamContext $context = null) { static $createModes = [ parent::MODE_READ, ]; if (!\in_array($this->getMode(), $createModes)) { throw new FileException('Open mode are not supported; given %d. Only %s are supported.', 0, [$this->getMode(), \implode(', ', $createModes)]); } \preg_match('#^(\w+)://#', $streamName, $match); if (((isset($match[1]) && $match[1] === 'file') || !isset($match[1])) && !\file_exists($streamName)) { throw new FileDoesNotExistException('File %s does not exist.', 1, $streamName); } $out = parent::_open($streamName, $context); return $out; } /** * Test for end-of-file. * * @return bool */ public function eof(): bool { return \feof($this->getStream()); } /** * Read n characters. * * @param int $length length * * @return string * * @throws \Hoa\File\Exception */ public function read(int $length) { if (0 > $length) { throw new FileException('Length must be greater than 0, given %d.', 2, $length); } return \fread($this->getStream(), $length); } /** * Alias of $this->read(). * * @param int $length length * * @return string */ public function readString(int $length) { return $this->read($length); } /** * Read a character. * * @return string */ public function readCharacter() { return \fgetc($this->getStream()); } /** * Read a boolean. * * @return bool */ public function readBoolean() { return (bool) $this->read(1); } /** * Read an integer. * * @param int $length length * * @return int */ public function readInteger(int $length = 1) { return (int) $this->read($length); } /** * Read a float. * * @param int $length length * * @return float */ public function readFloat(int $length = 1) { return (float) $this->read($length); } /** * Read an array. * Alias of the $this->scanf() method. * * @param string $format format (see printf's formats) * * @return array */ public function readArray(string $format = null) { return $this->scanf($format); } /** * Read a line. * * @return string */ public function readLine() { return \fgets($this->getStream()); } /** * Read all, i.e. read as much as possible. * * @param int $offset offset * * @return string */ public function readAll(int $offset = 0) { return \stream_get_contents($this->getStream(), -1, $offset); } /** * Parse input from a stream according to a format. * * @param string $format format (see printf's formats) * * @return array */ public function scanf(string $format): array { return \fscanf($this->getStream(), $format); } } psysh/src/Readline/Hoa/Readline.php 0000644 00000064567 15024771425 0013220 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\Console\Readline. * * Read, edit, bind… a line from the input. */ class Readline { /** * State: continue to read. */ const STATE_CONTINUE = 1; /** * State: stop to read. */ const STATE_BREAK = 2; /** * State: no output the current buffer. */ const STATE_NO_ECHO = 4; /** * Current editing line. */ protected $_line = null; /** * Current editing line seek. */ protected $_lineCurrent = 0; /** * Current editing line length. */ protected $_lineLength = 0; /** * Current buffer (most of the time, a char). */ protected $_buffer = null; /** * Mapping. */ protected $_mapping = []; /** * History. */ protected $_history = []; /** * History current position. */ protected $_historyCurrent = 0; /** * History size. */ protected $_historySize = 0; /** * Prefix. */ protected $_prefix = null; /** * Autocompleter. */ protected $_autocompleter = null; /** * Initialize the readline editor. */ public function __construct() { if (\defined('PHP_WINDOWS_VERSION_PLATFORM')) { return; } $this->_mapping["\033[A"] = [$this, '_bindArrowUp']; $this->_mapping["\033[B"] = [$this, '_bindArrowDown']; $this->_mapping["\033[C"] = [$this, '_bindArrowRight']; $this->_mapping["\033[D"] = [$this, '_bindArrowLeft']; $this->_mapping["\001"] = [$this, '_bindControlA']; $this->_mapping["\002"] = [$this, '_bindControlB']; $this->_mapping["\005"] = [$this, '_bindControlE']; $this->_mapping["\006"] = [$this, '_bindControlF']; $this->_mapping["\010"] = $this->_mapping["\177"] = [$this, '_bindBackspace']; $this->_mapping["\027"] = [$this, '_bindControlW']; $this->_mapping["\n"] = [$this, '_bindNewline']; $this->_mapping["\t"] = [$this, '_bindTab']; return; } /** * Read a line from the input. */ public function readLine(string $prefix = null) { $input = Console::getInput(); if (true === $input->eof()) { return false; } $direct = Console::isDirect($input->getStream()->getStream()); $output = Console::getOutput(); if (false === $direct || \defined('PHP_WINDOWS_VERSION_PLATFORM')) { $out = $input->readLine(); if (false === $out) { return false; } $out = \substr($out, 0, -1); if (true === $direct) { $output->writeAll($prefix); } else { $output->writeAll($prefix.$out."\n"); } return $out; } $this->resetLine(); $this->setPrefix($prefix); $read = [$input->getStream()->getStream()]; $write = $except = []; $output->writeAll($prefix); while (true) { @\stream_select($read, $write, $except, 30, 0); if (empty($read)) { $read = [$input->getStream()->getStream()]; continue; } $char = $this->_read(); $this->_buffer = $char; $return = $this->_readLine($char); if (0 === ($return & self::STATE_NO_ECHO)) { $output->writeAll($this->_buffer); } if (0 !== ($return & self::STATE_BREAK)) { break; } } return $this->getLine(); } /** * Readline core. */ public function _readLine(string $char) { if (isset($this->_mapping[$char]) && \is_callable($this->_mapping[$char])) { $mapping = $this->_mapping[$char]; return $mapping($this); } if (isset($this->_mapping[$char])) { $this->_buffer = $this->_mapping[$char]; } elseif (false === Ustring::isCharPrintable($char)) { ConsoleCursor::bip(); return static::STATE_CONTINUE | static::STATE_NO_ECHO; } if ($this->getLineLength() === $this->getLineCurrent()) { $this->appendLine($this->_buffer); return static::STATE_CONTINUE; } $this->insertLine($this->_buffer); $tail = \mb_substr( $this->getLine(), $this->getLineCurrent() - 1 ); $this->_buffer = "\033[K".$tail.\str_repeat( "\033[D", \mb_strlen($tail) - 1 ); return static::STATE_CONTINUE; } /** * Add mappings. */ public function addMappings(array $mappings) { foreach ($mappings as $key => $mapping) { $this->addMapping($key, $mapping); } } /** * Add a mapping. * Supported key: * • \e[… for \033[…; * • \C-… for Ctrl-…; * • abc for a simple mapping. * A mapping is a callable that has only one parameter of type * Hoa\Console\Readline and that returns a self::STATE_* constant. */ public function addMapping(string $key, $mapping) { if ('\e[' === \substr($key, 0, 3)) { $this->_mapping["\033[".\substr($key, 3)] = $mapping; } elseif ('\C-' === \substr($key, 0, 3)) { $_key = \ord(\strtolower(\substr($key, 3))) - 96; $this->_mapping[\chr($_key)] = $mapping; } else { $this->_mapping[$key] = $mapping; } } /** * Add an entry in the history. */ public function addHistory(string $line = null) { if (empty($line)) { return; } $this->_history[] = $line; $this->_historyCurrent = $this->_historySize++; } /** * Clear history. */ public function clearHistory() { unset($this->_history); $this->_history = []; $this->_historyCurrent = 0; $this->_historySize = 1; } /** * Get an entry in the history. */ public function getHistory(int $i = null) { if (null === $i) { $i = $this->_historyCurrent; } if (!isset($this->_history[$i])) { return null; } return $this->_history[$i]; } /** * Go backward in the history. */ public function previousHistory() { if (0 >= $this->_historyCurrent) { return $this->getHistory(0); } return $this->getHistory($this->_historyCurrent--); } /** * Go forward in the history. */ public function nextHistory() { if ($this->_historyCurrent + 1 >= $this->_historySize) { return $this->getLine(); } return $this->getHistory(++$this->_historyCurrent); } /** * Get current line. */ public function getLine() { return $this->_line; } /** * Append to current line. */ public function appendLine(string $append) { $this->_line .= $append; $this->_lineLength = \mb_strlen($this->_line); $this->_lineCurrent = $this->_lineLength; } /** * Insert into current line at the current seek. */ public function insertLine(string $insert) { if ($this->_lineLength === $this->_lineCurrent) { return $this->appendLine($insert); } $this->_line = \mb_substr($this->_line, 0, $this->_lineCurrent). $insert. \mb_substr($this->_line, $this->_lineCurrent); $this->_lineLength = \mb_strlen($this->_line); $this->_lineCurrent += \mb_strlen($insert); return; } /** * Reset current line. */ protected function resetLine() { $this->_line = null; $this->_lineCurrent = 0; $this->_lineLength = 0; } /** * Get current line seek. */ public function getLineCurrent(): int { return $this->_lineCurrent; } /** * Get current line length. * * @return int */ public function getLineLength(): int { return $this->_lineLength; } /** * Set prefix. */ public function setPrefix(string $prefix) { $this->_prefix = $prefix; } /** * Get prefix. */ public function getPrefix() { return $this->_prefix; } /** * Get buffer. Not for user. */ public function getBuffer() { return $this->_buffer; } /** * Set an autocompleter. */ public function setAutocompleter(Autocompleter $autocompleter) { $old = $this->_autocompleter; $this->_autocompleter = $autocompleter; return $old; } /** * Get the autocompleter. * * @return ?Autocompleter */ public function getAutocompleter() { return $this->_autocompleter; } /** * Read on input. Not for user. */ public function _read(int $length = 512): string { return Console::getInput()->read($length); } /** * Set current line. Not for user. */ public function setLine(string $line) { $this->_line = $line; $this->_lineLength = \mb_strlen($this->_line ?: ''); $this->_lineCurrent = $this->_lineLength; } /** * Set current line seek. Not for user. */ public function setLineCurrent(int $current) { $this->_lineCurrent = $current; } /** * Set line length. Not for user. */ public function setLineLength(int $length) { $this->_lineLength = $length; } /** * Set buffer. Not for user. */ public function setBuffer(string $buffer) { $this->_buffer = $buffer; } /** * Up arrow binding. * Go backward in the history. */ public function _bindArrowUp(self $self): int { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::clear('↔'); Console::getOutput()->writeAll($self->getPrefix()); } $buffer = $self->previousHistory() ?? ''; $self->setBuffer($buffer); $self->setLine($buffer); return static::STATE_CONTINUE; } /** * Down arrow binding. * Go forward in the history. */ public function _bindArrowDown(self $self): int { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::clear('↔'); Console::getOutput()->writeAll($self->getPrefix()); } $self->setBuffer($buffer = $self->nextHistory()); $self->setLine($buffer); return static::STATE_CONTINUE; } /** * Right arrow binding. * Move cursor to the right. */ public function _bindArrowRight(self $self): int { if ($self->getLineLength() > $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::move('→'); } $self->setLineCurrent($self->getLineCurrent() + 1); } $self->setBuffer(''); return static::STATE_CONTINUE; } /** * Left arrow binding. * Move cursor to the left. */ public function _bindArrowLeft(self $self): int { if (0 < $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::move('←'); } $self->setLineCurrent($self->getLineCurrent() - 1); } $self->setBuffer(''); return static::STATE_CONTINUE; } /** * Backspace and Control-H binding. * Delete the first character at the right of the cursor. */ public function _bindBackspace(self $self): int { $buffer = ''; if (0 < $self->getLineCurrent()) { if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) { ConsoleCursor::move('←'); ConsoleCursor::clear('→'); } if ($self->getLineLength() === $current = $self->getLineCurrent()) { $self->setLine(\mb_substr($self->getLine(), 0, -1)); } else { $line = $self->getLine(); $current = $self->getLineCurrent(); $tail = \mb_substr($line, $current); $buffer = $tail.\str_repeat("\033[D", \mb_strlen($tail)); $self->setLine(\mb_substr($line, 0, $current - 1).$tail); $self->setLineCurrent($current - 1); } } $self->setBuffer($buffer); return static::STATE_CONTINUE; } /** * Control-A binding. * Move cursor to beginning of line. */ public function _bindControlA(self $self): int { for ($i = $self->getLineCurrent() - 1; 0 <= $i; --$i) { $self->_bindArrowLeft($self); } return static::STATE_CONTINUE; } /** * Control-B binding. * Move cursor backward one word. */ public function _bindControlB(self $self): int { $current = $self->getLineCurrent(); if (0 === $current) { return static::STATE_CONTINUE; } $words = \preg_split( '#\b#u', $self->getLine(), -1, \PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = \count($words) - 1; $i < $max && $words[$i + 1][1] < $current; ++$i ) { } for ($j = $words[$i][1] + 1; $current >= $j; ++$j) { $self->_bindArrowLeft($self); } return static::STATE_CONTINUE; } /** * Control-E binding. * Move cursor to end of line. */ public function _bindControlE(self $self): int { for ( $i = $self->getLineCurrent(), $max = $self->getLineLength(); $i < $max; ++$i ) { $self->_bindArrowRight($self); } return static::STATE_CONTINUE; } /** * Control-F binding. * Move cursor forward one word. */ public function _bindControlF(self $self): int { $current = $self->getLineCurrent(); if ($self->getLineLength() === $current) { return static::STATE_CONTINUE; } $words = \preg_split( '#\b#u', $self->getLine(), -1, \PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = \count($words) - 1; $i < $max && $words[$i][1] < $current; ++$i ) { } if (!isset($words[$i + 1])) { $words[$i + 1] = [1 => $self->getLineLength()]; } for ($j = $words[$i + 1][1]; $j > $current; --$j) { $self->_bindArrowRight($self); } return static::STATE_CONTINUE; } /** * Control-W binding. * Delete first backward word. */ public function _bindControlW(self $self): int { $current = $self->getLineCurrent(); if (0 === $current) { return static::STATE_CONTINUE; } $words = \preg_split( '#\b#u', $self->getLine(), -1, \PREG_SPLIT_OFFSET_CAPTURE | \PREG_SPLIT_NO_EMPTY ); for ( $i = 0, $max = \count($words) - 1; $i < $max && $words[$i + 1][1] < $current; ++$i ) { } for ($j = $words[$i][1] + 1; $current >= $j; ++$j) { $self->_bindBackspace($self); } return static::STATE_CONTINUE; } /** * Newline binding. */ public function _bindNewline(self $self): int { $self->addHistory($self->getLine()); return static::STATE_BREAK; } /** * Tab binding. */ public function _bindTab(self $self): int { $output = Console::getOutput(); $autocompleter = $self->getAutocompleter(); $state = static::STATE_CONTINUE | static::STATE_NO_ECHO; if (null === $autocompleter) { return $state; } $current = $self->getLineCurrent(); $line = $self->getLine(); if (0 === $current) { return $state; } $matches = \preg_match_all( '#'.$autocompleter->getWordDefinition().'$#u', \mb_substr($line, 0, $current), $words ); if (0 === $matches) { return $state; } $word = $words[0][0]; if ('' === \trim($word)) { return $state; } $solution = $autocompleter->complete($word); $length = \mb_strlen($word); if (null === $solution) { return $state; } if (\is_array($solution)) { $_solution = $solution; $count = \count($_solution) - 1; $cWidth = 0; $window = ConsoleWindow::getSize(); $wWidth = $window['x']; $cursor = ConsoleCursor::getPosition(); \array_walk($_solution, function (&$value) use (&$cWidth) { $handle = \mb_strlen($value); if ($handle > $cWidth) { $cWidth = $handle; } return; }); \array_walk($_solution, function (&$value) use (&$cWidth) { $handle = \mb_strlen($value); if ($handle >= $cWidth) { return; } $value .= \str_repeat(' ', $cWidth - $handle); return; }); $mColumns = (int) \floor($wWidth / ($cWidth + 2)); $mLines = (int) \ceil(($count + 1) / $mColumns); --$mColumns; $i = 0; if (0 > $window['y'] - $cursor['y'] - $mLines) { ConsoleWindow::scroll('↑', $mLines); ConsoleCursor::move('↑', $mLines); } ConsoleCursor::save(); ConsoleCursor::hide(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::clear('↓'); foreach ($_solution as $j => $s) { $output->writeAll("\033[0m".$s."\033[0m"); if ($i++ < $mColumns) { $output->writeAll(' '); } else { $i = 0; if (isset($_solution[$j + 1])) { $output->writeAll("\n"); } } } ConsoleCursor::restore(); ConsoleCursor::show(); ++$mColumns; $input = Console::getInput(); $read = [$input->getStream()->getStream()]; $write = $except = []; $mColumn = -1; $mLine = -1; $coord = -1; $unselect = function () use ( &$mColumn, &$mLine, &$coord, &$_solution, &$cWidth, $output ) { ConsoleCursor::save(); ConsoleCursor::hide(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::move('→', $mColumn * ($cWidth + 2)); ConsoleCursor::move('↓', $mLine); $output->writeAll("\033[0m".$_solution[$coord]."\033[0m"); ConsoleCursor::restore(); ConsoleCursor::show(); return; }; $select = function () use ( &$mColumn, &$mLine, &$coord, &$_solution, &$cWidth, $output ) { ConsoleCursor::save(); ConsoleCursor::hide(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::move('→', $mColumn * ($cWidth + 2)); ConsoleCursor::move('↓', $mLine); $output->writeAll("\033[7m".$_solution[$coord]."\033[0m"); ConsoleCursor::restore(); ConsoleCursor::show(); return; }; $init = function () use ( &$mColumn, &$mLine, &$coord, &$select ) { $mColumn = 0; $mLine = 0; $coord = 0; $select(); return; }; while (true) { @\stream_select($read, $write, $except, 30, 0); if (empty($read)) { $read = [$input->getStream()->getStream()]; continue; } switch ($char = $self->_read()) { case "\033[A": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \max(0, $coord - $mColumns); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\033[B": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \min($count, $coord + $mColumns); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\t": case "\033[C": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \min($count, $coord + 1); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\033[D": if (-1 === $mColumn && -1 === $mLine) { $init(); break; } $unselect(); $coord = \max(0, $coord - 1); $mLine = (int) \floor($coord / $mColumns); $mColumn = $coord % $mColumns; $select(); break; case "\n": if (-1 !== $mColumn && -1 !== $mLine) { $tail = \mb_substr($line, $current); $current -= $length; $self->setLine( \mb_substr($line, 0, $current). $solution[$coord]. $tail ); $self->setLineCurrent( $current + \mb_strlen($solution[$coord]) ); ConsoleCursor::move('←', $length); $output->writeAll($solution[$coord]); ConsoleCursor::clear('→'); $output->writeAll($tail); ConsoleCursor::move('←', \mb_strlen($tail)); } // no break default: $mColumn = -1; $mLine = -1; $coord = -1; ConsoleCursor::save(); ConsoleCursor::move('↓ LEFT'); ConsoleCursor::clear('↓'); ConsoleCursor::restore(); if ("\033" !== $char && "\n" !== $char) { $self->setBuffer($char); return $self->_readLine($char); } break 2; } } return $state; } $tail = \mb_substr($line, $current); $current -= $length; $self->setLine( \mb_substr($line, 0, $current). $solution. $tail ); $self->setLineCurrent( $current + \mb_strlen($solution) ); ConsoleCursor::move('←', $length); $output->writeAll($solution); ConsoleCursor::clear('→'); $output->writeAll($tail); ConsoleCursor::move('←', \mb_strlen($tail)); return $state; } } /* * Advanced interaction. */ Console::advancedInteraction(); psysh/src/Readline/Hoa/FileDoesNotExistException.php 0000644 00000003521 15024771425 0016523 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Class \Hoa\File\Exception\FileDoesNotExist. * * Extending the \Hoa\File\Exception class. * * @license New BSD License */ class FileDoesNotExistException extends FileException { } psysh/src/Readline/Hoa/IStream.php 0000644 00000003507 15024771425 0013024 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Stream\IStream\Stream. * * Interface for all streams. */ interface IStream { /** * Get the current stream. */ public function getStream(); } psysh/src/Readline/Hoa/ConsoleInput.php 0000644 00000010234 15024771425 0014075 0 ustar 00 <?php /** * Hoa * * * @license * * New BSD License * * Copyright © 2007-2017, Hoa community. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Hoa nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ namespace Psy\Readline\Hoa; /** * Interface \Hoa\Console\Input. * * This interface represents the input of a program. Most of the time, this is * going to be `php://stdin` but it can be `/dev/tty` if the former has been * closed. */ class ConsoleInput implements StreamIn { /** * Real input stream. */ protected $_input = null; /** * Wraps an `Hoa\Stream\IStream\In` stream. */ public function __construct(StreamIn $input = null) { if (null === $input) { if (\defined('STDIN') && false !== @\stream_get_meta_data(\STDIN)) { $input = new FileRead('php://stdin'); } else { $input = new FileRead('/dev/tty'); } } $this->_input = $input; return; } /** * Get underlying stream. */ public function getStream(): StreamIn { return $this->_input; } /** * Test for end-of-file. */ public function eof(): bool { return $this->_input->eof(); } /** * Read n characters. */ public function read(int $length) { return $this->_input->read($length); } /** * Alias of $this->read(). */ public function readString(int $length) { return $this->_input->readString($length); } /** * Read a character. */ public function readCharacter() { return $this->_input->readCharacter(); } /** * Read a boolean. */ public function readBoolean() { return $this->_input->readBoolean(); } /** * Read an integer. */ public function readInteger(int $length = 1) { return $this->_input->readInteger($length); } /** * Read a float. */ public function readFloat(int $length = 1) { return $this->_input->readFloat($length); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray($argument = null) { return $this->_input->readArray($argument); } /** * Read a line. */ public function readLine() { return $this->_input->readLine(); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = 0) { return $this->_input->readAll($offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format): array { return $this->_input->scanf($format); } } psysh/src/Readline/Hoa/Terminfo/78/xterm-256color 0000644 00000006372 15024771425 0015426 0 ustar 00 % &