Файловый менеджер - Редактировать - /var/www/vhosts/aviointeriors.dev1.mndrn.cloud/routes/update/psy.tar
Назад
psysh/bin/psysh 0000755 00000011626 15233636330 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 00000003245 15233636330 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.4", "ext-json": "*", "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "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-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.12.x-dev" }, "bamarni-bin": { "bin-links": false, "forward-command": false } }, "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/README.md 0000644 00000004504 15233636330 0007200 0 ustar 00 # PsySH PsySH is a runtime developer console, interactive debugger and [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) for PHP. Learn more at [psysh.org](http://psysh.org/) and [in the manual](https://github.com/bobthecow/psysh/wiki/Home). [](https://packagist.org/packages/psy/psysh) [](https://packagist.org/packages/psy/psysh) [](http://psysh.org) [](https://github.com/bobthecow/psysh/actions?query=branch:main) [](https://styleci.io/repos/4549925) <a id="downloading-the-manual"></a> ## [PsySH manual](https://github.com/bobthecow/psysh/wiki/Home) ### [💾 Installation](https://github.com/bobthecow/psysh/wiki/Installation) * [📕 PHP manual installation](https://github.com/bobthecow/psysh/wiki/PHP-manual) * <a class="internal present" href="https://github.com/bobthecow/psysh/wiki/Windows"><img src="https://user-images.githubusercontent.com/53660/40878809-407e8368-664b-11e8-8455-f11602c41dfe.png" width="18"> Windows</a> ### [🖥 Usage](https://github.com/bobthecow/psysh/wiki/Usage) * [✨ Magic variables](https://github.com/bobthecow/psysh/wiki/Magic-variables) * [⏳ Managing history](https://github.com/bobthecow/psysh/wiki/History) * [💲 System shell integration](https://github.com/bobthecow/psysh/wiki/Shell-integration) * [🎥 Tutorials & guides](https://github.com/bobthecow/psysh/wiki/Tutorials) * [🐛 Troubleshooting](https://github.com/bobthecow/psysh/wiki/Troubleshooting) ### [📢 Commands](https://github.com/bobthecow/psysh/wiki/Commands) ### [🛠 Configuration](https://github.com/bobthecow/psysh/wiki/Configuration) * [🎛 Config options](https://github.com/bobthecow/psysh/wiki/Config-options) * [🎨 Themes](https://github.com/bobthecow/psysh/wiki/Themes) * [📄 Sample config file](https://github.com/bobthecow/psysh/wiki/Sample-config) ### [🔌 Integrations](https://github.com/bobthecow/psysh/wiki/Integrations) psysh/LICENSE 0000644 00000002076 15233636330 0006730 0 ustar 00 The MIT License (MIT) Copyright (c) 2012-2023 Justin Hileman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. psysh/src/Shell.php 0000644 00000136523 15233636330 0010277 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\Exception\ExceptionInterface as SymfonyConsoleException; 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.12.0'; 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 { @\trigger_error('`Psy\\Shell::debug` is deprecated; call `Psy\\debug` instead.', \E_USER_DEPRECATED); 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(), ]; } /** * 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) { @\trigger_error('`addTabCompletionMatchers` is deprecated; call `addMatchers` instead.', \E_USER_DEPRECATED); $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): void { // @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('<whisper>%s</whisper>', 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` * 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` * 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 || $e instanceof SymfonyConsoleException) { $exceptionShortName = (new \ReflectionClass($e))->getShortName(); $typeParts = \preg_split('/(?=[A-Z])/', $exceptionShortName); switch ($exceptionShortName) { case 'RuntimeException': case 'LogicException': // These ones look weird without 'Exception' break; default: if (\end($typeParts) === 'Exception') { \array_pop($typeParts); } break; } 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 { @\trigger_error('`getVersion` is deprecated; call `self::getVersionHeader` instead.', \E_USER_DEPRECATED); 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(); } /** * 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/ContextAware.php 0000644 00000001067 15233636330 0011626 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/CodeCleaner.php 0000644 00000027646 15233636330 0011401 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\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 $strictTypes = 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 * @param bool $strictTypes enforce strict types by default */ public function __construct(Parser $parser = null, Printer $printer = null, NodeTraverser $traverser = null, bool $yolo = false, bool $strictTypes = false) { $this->yolo = $yolo; $this->strictTypes = $strictTypes; $this->parser = $parser ?? (new ParserFactory())->createParser(); $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 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($this->strictTypes), // 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($this->strictTypes), ]; } /** * "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/VarDumper/Dumper.php 0000644 00000005676 15233636330 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; private const ONLY_CONTROL_CHARS = '/^[\x00-\x1F\x7F]+$/'; private const CONTROL_CHARS = '/([\x00-\x1F\x7F]+)/'; private const CONTROL_CHARS_MAP = [ "\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): void { if (Cursor::HASH_INDEXED === $type || Cursor::HASH_ASSOC === $type) { $class = 0; } parent::enterHash($cursor, $type, $class, $hasChild); } /** * {@inheritdoc} */ protected function dumpKey(Cursor $cursor): void { 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 = ''; $cchr = $this->styles['cchr']; $chunks = \preg_split(self::CONTROL_CHARS, $value, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); foreach ($chunks as $chunk) { if (\preg_match(self::ONLY_CONTROL_CHARS, $chunk)) { $chars = ''; $i = 0; do { $chars .= isset(self::CONTROL_CHARS_MAP[$chunk[$i]]) ? self::CONTROL_CHARS_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): void { if ($endOfValue && 0 < $depth) { $this->line .= ','; } $this->line = $this->formatter->format($this->line); parent::dumpLine($depth, $endOfValue); } } psysh/src/VarDumper/PresenterAware.php 0000644 00000001017 15233636330 0014051 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 15233636330 0013101 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/Cloner.php 0000644 00000001647 15233636330 0012355 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/Output/OutputPager.php 0000644 00000001066 15233636330 0013000 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/ProcOutputPager.php 0000644 00000005443 15233636330 0013627 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): void { $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/Output/PassthruPager.php 0000644 00000001451 15233636330 0013307 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/Theme.php 0000644 00000016303 15233636330 0011563 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'], 'code_comment' => ['gray'], 'object' => ['blue'], 'resource' => ['yellow'], // Code-specific formatting 'inline_html' => ['cyan'], ]; const ERROR_STYLES = ['info', 'warning', 'error', 'whisper', 'class']; 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/ShellOutput.php 0000644 00000013371 15233636330 0013013 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): void { 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): void { 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/Util/Str.php 0000644 00000005614 15233636330 0010711 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/Docblock.php 0000644 00000015074 15233636330 0011662 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): ?array { 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/Mirror.php 0000644 00000011616 15233636330 0011412 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\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 new \ReflectionClassConstant($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/Util/Json.php 0000644 00000001161 15233636330 0011043 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/SuperglobalsEnv.php 0000644 00000001154 15233636330 0012332 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/VersionUpdater/Downloader/FileDownloader.php 0000644 00000002405 15233636330 0017165 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\VersionUpdater\Downloader; use Psy\VersionUpdater\Downloader; class FileDownloader implements Downloader { private $tempDir = null; private $outputFile = null; /** {@inheritDoc} */ public function setTempDir(string $tempDir) { $this->tempDir = $tempDir; } /** {@inheritDoc} */ public function download(string $url): bool { $tempDir = $this->tempDir ?: \sys_get_temp_dir(); $this->outputFile = \tempnam($tempDir, 'psysh-archive-'); $targetName = $this->outputFile.'.tar.gz'; if (!\rename($this->outputFile, $targetName)) { return false; } $this->outputFile = $targetName; return (bool) \file_put_contents($this->outputFile, \file_get_contents($url)); } /** {@inheritDoc} */ public function getFilename(): string { return $this->outputFile; } /** {@inheritDoc} */ public function cleanup() { if (\file_exists($this->outputFile)) { \unlink($this->outputFile); } } } psysh/src/VersionUpdater/Downloader/CurlDownloader.php 0000644 00000004065 15233636330 0017217 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\VersionUpdater\Downloader; use Psy\Exception\ErrorException; use Psy\Shell; use Psy\VersionUpdater\Downloader; class CurlDownloader implements Downloader { private $tempDir = null; private $outputFile = null; /** {@inheritDoc} */ public function setTempDir(string $tempDir) { $this->tempDir = $tempDir; } /** {@inheritDoc} */ public function download(string $url): bool { $tempDir = $this->tempDir ?: \sys_get_temp_dir(); $this->outputFile = \tempnam($tempDir, 'psysh-archive-'); $targetName = $this->outputFile.'.tar.gz'; if (!\rename($this->outputFile, $targetName)) { return false; } $this->outputFile = $targetName; $outputHandle = \fopen($this->outputFile, 'w'); if (!$outputHandle) { return false; } $curl = \curl_init(); \curl_setopt_array($curl, [ \CURLOPT_FAILONERROR => true, \CURLOPT_HEADER => 0, \CURLOPT_FOLLOWLOCATION => true, \CURLOPT_TIMEOUT => 10, \CURLOPT_FILE => $outputHandle, \CURLOPT_HTTPHEADER => [ 'User-Agent' => 'PsySH/'.Shell::VERSION, ], ]); \curl_setopt($curl, \CURLOPT_URL, $url); $result = \curl_exec($curl); $error = \curl_error($curl); \curl_close($curl); \fclose($outputHandle); if (!$result) { throw new ErrorException('cURL Error: '.$error); } return (bool) $result; } /** {@inheritDoc} */ public function getFilename(): string { return $this->outputFile; } /** {@inheritDoc} */ public function cleanup() { if (\file_exists($this->outputFile)) { \unlink($this->outputFile); } } } psysh/src/VersionUpdater/Downloader/Factory.php 0000644 00000001334 15233636331 0015677 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\VersionUpdater\Downloader; use Psy\Exception\ErrorException; use Psy\VersionUpdater\Downloader; class Factory { /** * @throws ErrorException If no downloaders can be used */ public static function getDownloader(): Downloader { if (\extension_loaded('curl')) { return new CurlDownloader(); } elseif (\ini_get('allow_url_fopen')) { return new FileDownloader(); } throw new ErrorException('No downloader available.'); } } psysh/src/VersionUpdater/Checker.php 0000644 00000000754 15233636331 0013543 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\VersionUpdater; interface Checker { const ALWAYS = 'always'; const DAILY = 'daily'; const WEEKLY = 'weekly'; const MONTHLY = 'monthly'; const NEVER = 'never'; public function isLatest(): bool; public function getLatest(): string; } psysh/src/VersionUpdater/Installer.php 0000644 00000007442 15233636331 0014135 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\VersionUpdater; use Psy\Exception\ErrorException; class Installer { /** * @var string */ protected $installLocation; /** * @var string */ protected $tempDirectory; public function __construct(string $tempDirectory = null) { $this->tempDirectory = $tempDirectory ?: \sys_get_temp_dir(); $this->installLocation = \Phar::running(false); } /** * Public to allow the Downloader to use the temporary directory if it's been set. */ public function getTempDirectory(): string { return $this->tempDirectory; } /** * Verify the currently installed PsySH phar is writable so it can be replaced. */ public function isInstallLocationWritable(): bool { return \is_writable($this->installLocation); } /** * Verify the temporary directory is writable so downloads and backups can be saved there. */ public function isTempDirectoryWritable(): bool { return \is_writable($this->tempDirectory); } /** * Verifies the downloaded archive can be extracted with \PharData. * * @param string $sourceArchive */ public function isValidSource(string $sourceArchive): bool { if (!\class_exists('\PharData')) { return false; } $pharArchive = new \PharData($sourceArchive); return $pharArchive->valid(); } /** * Extract the "psysh" phar from the archive and move it, replacing the currently installed phar. * * @param string $sourceArchive */ public function install(string $sourceArchive): bool { $pharArchive = new \PharData($sourceArchive); $outputDirectory = \tempnam($this->tempDirectory, 'psysh-'); // remove the temp file, and replace it with a sub-directory if (!\unlink($outputDirectory) || !\mkdir($outputDirectory, 0700)) { return false; } $pharArchive->extractTo($outputDirectory, ['psysh'], true); $renamed = \rename($outputDirectory.'/psysh', $this->installLocation); // Remove the sub-directory created to extract the psysh binary/phar \rmdir($outputDirectory); return $renamed; } /** * Create a backup of the currently installed PsySH phar in the temporary directory with a version number postfix. * * @param string $version */ public function createBackup(string $version): bool { $backupFilename = $this->getBackupFilename($version); if (\file_exists($backupFilename) && !\is_writable($backupFilename)) { return false; } return \rename($this->installLocation, $backupFilename); } /** * Restore the backup file to the original PsySH install location. * * @param string $version * * @throws ErrorException If the backup file could not be found */ public function restoreFromBackup(string $version): bool { $backupFilename = $this->getBackupFilename($version); if (!\file_exists($backupFilename)) { throw new ErrorException("Cannot restore from backup. File not found! [{$backupFilename}]"); } return \rename($backupFilename, $this->installLocation); } /** * Get the full path for the backup target file location. * * @param string $version */ public function getBackupFilename(string $version): string { $installFilename = \basename($this->installLocation); return \sprintf('%s/%s.%s', $this->tempDirectory, $installFilename, $version); } } psysh/src/VersionUpdater/Downloader.php 0000644 00000001533 15233636331 0014271 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\VersionUpdater; use Psy\Exception\ErrorException; interface Downloader { /** * Set the directory where the download will be written to. * * @param string $tempDir */ public function setTempDir(string $tempDir); /** * @param string $url * * @throws ErrorException on failure */ public function download(string $url): bool; /** * Get the temporary file name the download was written to. */ public function getFilename(): string; /** * Delete the downloaded file if it exists. * * @return void */ public function cleanup(); } psysh/src/VersionUpdater/GitHubChecker.php 0000644 00000004033 15233636331 0014640 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\VersionUpdater; use Psy\Shell; class GitHubChecker implements Checker { const URL = 'https://api.github.com/repos/bobthecow/psysh/releases/latest'; private $latest; public function isLatest(): bool { // version_compare doesn't handle semver completely; // strip pre-release and build metadata before comparing $version = \preg_replace('/[+-]\w+/', '', Shell::VERSION); return \version_compare($version, $this->getLatest(), '>='); } public function getLatest(): string { if (!isset($this->latest)) { $this->setLatest($this->getVersionFromTag()); } return $this->latest; } /** * @param string $version */ public function setLatest(string $version) { $this->latest = $version; } /** * @return string|null */ private function getVersionFromTag() { $contents = $this->fetchLatestRelease(); if (!$contents || !isset($contents->tag_name)) { throw new \InvalidArgumentException('Unable to check for updates'); } $this->setLatest($contents->tag_name); return $this->getLatest(); } /** * Set to public to make testing easier. * * @return mixed */ public function fetchLatestRelease() { $context = \stream_context_create([ 'http' => [ 'user_agent' => 'PsySH/'.Shell::VERSION, 'timeout' => 1.0, ], ]); \set_error_handler(function () { // Just ignore all errors with this. The checker will throw an exception // if it doesn't work :) }); $result = @\file_get_contents(self::URL, false, $context); \restore_error_handler(); return \json_decode($result); } } psysh/src/VersionUpdater/NoopChecker.php 0000644 00000001042 15233636331 0014366 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\VersionUpdater; use Psy\Shell; /** * A version checker stub which always thinks the current version is up to date. */ class NoopChecker implements Checker { public function isLatest(): bool { return true; } public function getLatest(): string { return Shell::VERSION; } } psysh/src/VersionUpdater/IntervalChecker.php 0000644 00000003642 15233636331 0015247 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\VersionUpdater; class IntervalChecker extends GitHubChecker { private $cacheFile; private $interval; public function __construct($cacheFile, $interval) { $this->cacheFile = $cacheFile; $this->interval = $interval; } public function fetchLatestRelease() { // Read the cached file $cached = \json_decode(@\file_get_contents($this->cacheFile, false)); if ($cached && isset($cached->last_check) && isset($cached->release)) { $now = new \DateTime(); $lastCheck = new \DateTime($cached->last_check); if ($lastCheck >= $now->sub($this->getDateInterval())) { return $cached->release; } } // Fall back to fetching from GitHub $release = parent::fetchLatestRelease(); if ($release && isset($release->tag_name)) { $this->updateCache($release); } return $release; } /** * @throws \RuntimeException if interval passed to constructor is not supported */ private function getDateInterval(): \DateInterval { switch ($this->interval) { case Checker::DAILY: return new \DateInterval('P1D'); case Checker::WEEKLY: return new \DateInterval('P1W'); case Checker::MONTHLY: return new \DateInterval('P1M'); } throw new \RuntimeException('Invalid interval configured'); } private function updateCache($release) { $data = [ 'last_check' => \date(\DATE_ATOM), 'release' => $release, ]; \file_put_contents($this->cacheFile, \json_encode($data)); } } psysh/src/VersionUpdater/SelfUpdate.php 0000644 00000013276 15233636331 0014236 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\VersionUpdater; use Psy\Exception\ErrorException; use Psy\Shell; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Self update command. * * If a new version is available, this command will download it and replace the currently installed version */ class SelfUpdate { const URL_PREFIX = 'https://github.com/bobthecow/psysh/releases/download'; const SUCCESS = 0; const FAILURE = 1; /** @var Checker */ private $checker; /** @var Installer */ private $installer; /** @var Downloader */ private $downloader; public function __construct(Checker $checker, Installer $installer) { $this->checker = $checker; $this->installer = $installer; } /** * Allow the downloader to be injected for testing. * * @param Downloader $downloader * * @return void */ public function setDownloader(Downloader $downloader) { $this->downloader = $downloader; } /** * Get the currently set Downloader or create one based on the capabilities of the php environment. * * @throws ErrorException if a downloader cannot be created for the php environment */ private function getDownloader(): Downloader { if (!isset($this->downloader)) { return Downloader\Factory::getDownloader(); } return $this->downloader; } /** * Build the download URL for the latest release. * * The file name used in the URL will include the flavour postfix extracted from the current version * if it's present * * @param string $latestVersion */ private function getAssetUrl(string $latestVersion): string { $versionPostfix = ''; if (\strpos(Shell::VERSION, '+')) { $versionPostfix = '-'.\substr(Shell::VERSION, \strpos(Shell::VERSION, '+') + 1); } $downloadFilename = \sprintf('psysh-%s%s.tar.gz', $latestVersion, $versionPostfix); // check if latest release data contains an asset matching the filename? return \sprintf('%s/%s/%s', self::URL_PREFIX, $latestVersion, $downloadFilename); } /** * Execute the self-update process. * * @param InputInterface $input * @param OutputInterface $output * * @throws ErrorException if the current version is not restored when installation fails */ public function run(InputInterface $input, OutputInterface $output): int { $currentVersion = Shell::VERSION; // already have the latest version? if ($this->checker->isLatest()) { // current version is latest version... $output->writeln('<info>Current version is up-to-date.</info>'); return self::SUCCESS; } // can overwrite current version? if (!$this->installer->isInstallLocationWritable()) { $output->writeln('<error>Installed version is not writable.</error>'); return self::FAILURE; } // can download to, and create a backup in the temp directory? if (!$this->installer->isTempDirectoryWritable()) { $output->writeln('<error>Temporary directory is not writable.</error>'); return self::FAILURE; } $latestVersion = $this->checker->getLatest(); $downloadUrl = $this->getAssetUrl($latestVersion); $output->write("Downloading PsySH $latestVersion ..."); try { $downloader = $this->getDownloader(); $downloader->setTempDir($this->installer->getTempDirectory()); $downloaded = $downloader->download($downloadUrl); } catch (ErrorException $e) { $output->write(' <error>Failed.</error>'); $output->writeln(\sprintf('<error>%s</error>', $e->getMessage())); return self::FAILURE; } if (!$downloaded) { $output->writeln('<error>Download failed.</error>'); $downloader->cleanup(); return self::FAILURE; } else { $output->write(' <info>OK</info>'.\PHP_EOL); } $downloadedFile = $downloader->getFilename(); if (!$this->installer->isValidSource($downloadedFile)) { $downloader->cleanup(); $output->writeln('<error>Downloaded file is not a valid archive.</error>'); return self::FAILURE; } // create backup as bin.old-version in the temporary directory $backupCreated = $this->installer->createBackup($currentVersion); if (!$backupCreated) { $downloader->cleanup(); $output->writeln('<error>Failed to create a backup of the current version.</error>'); return self::FAILURE; } elseif ($input->getOption('verbose')) { $backupFilename = $this->installer->getBackupFilename($currentVersion); $output->writeln('Created backup of current version: '.$backupFilename); } if (!$this->installer->install($downloadedFile)) { $this->installer->restoreFromBackup($currentVersion); $downloader->cleanup(); $output->writeln("<error>Failed to install new PsySH version $latestVersion.</error>"); return self::FAILURE; } // Remove the downloaded archive file from the temporary directory $downloader->cleanup(); $output->writeln("Updated PsySH from $currentVersion to <info>$latestVersion</info>"); return self::SUCCESS; } } psysh/src/Readline/Transient.php 0000644 00000005677 15233636331 0012730 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/Userland.php 0000644 00000007203 15233636331 0012521 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 15233636331 0013036 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/Readline.php 0000644 00000003447 15233636331 0012475 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/Libedit.php 0000644 00000006470 15233636331 0012325 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\Util\Str; /** * A Libedit-based Readline implementation. * * This is largely the same as the Readline implementation, but it emulates * support for `readline_list_history` since PHP decided it was a good idea to * ship a fake Readline implementation that is missing history support. * * NOTE: As of PHP 7.4, PHP sometimes has history support in the Libedit * wrapper, so it will use the GNUReadline implementation rather than this one. */ class Libedit extends GNUReadline { private $hasWarnedOwnership = false; /** * Let's emulate GNU Readline by manually reading and parsing the history file! */ public static function isSupported(): bool { return \function_exists('readline') && !\function_exists('readline_list_history'); } /** * {@inheritdoc} */ public static function supportsBracketedPaste(): bool { return false; } /** * {@inheritdoc} */ public function listHistory(): array { $history = \file_get_contents($this->historyFile); if (!$history) { return []; } // libedit doesn't seem to support non-unix line separators. $history = \explode("\n", $history); // remove history signature if it exists if ($history[0] === '_HiStOrY_V2_') { \array_shift($history); } // decode the line $history = \array_map([$this, 'parseHistoryLine'], $history); // filter empty lines & comments return \array_values(\array_filter($history)); } /** * {@inheritdoc} */ public function writeHistory(): bool { $res = parent::writeHistory(); // Libedit apparently refuses to save history if the history file is not // owned by the user, even if it is writable. Warn when this happens. // // See https://github.com/bobthecow/psysh/issues/552 if ($res === false && !$this->hasWarnedOwnership) { if (\is_file($this->historyFile) && \is_writable($this->historyFile)) { $this->hasWarnedOwnership = true; $msg = \sprintf('Error writing history file, check file ownership: %s', $this->historyFile); \trigger_error($msg, \E_USER_NOTICE); } } return $res; } /** * From GNUReadline (readline/histfile.c & readline/histexpand.c): * lines starting with "\0" are comments or timestamps; * if "\0" is found in an entry, * everything from it until the next line is a comment. * * @param string $line The history line to parse * * @return string|null */ protected function parseHistoryLine(string $line) { // empty line, comment or timestamp if (!$line || $line[0] === "\0") { return; } // if "\0" is found in an entry, then // everything from it until the end of line is a comment. if (($pos = \strpos($line, "\0")) !== false) { $line = \substr($line, 0, $pos); } return ($line !== '') ? Str::unvis($line) : null; } } psysh/src/Readline/Hoa/Exception.php 0000644 00000005237 15233636331 0013416 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 exception must extend `Hoa\Exception\Exception`. */ class Exception extends ExceptionIdle implements EventSource { /** * Allocates a new exception. * * An exception is built with a formatted message, a code (an ID), and an * array that contains the list of formatted string for the message. If * chaining, a previous exception can be added. */ public function __construct( string $message, int $code = 0, $arguments = [], \Throwable $previous = null ) { parent::__construct($message, $code, $arguments, $previous); if (false === Event::eventExists('hoa://Event/Exception')) { Event::register('hoa://Event/Exception', __CLASS__); } $this->send(); return; } /** * Sends the exception on `hoa://Event/Exception`. */ public function send() { Event::notify( 'hoa://Event/Exception', $this, new EventBucket($this) ); } } psysh/src/Readline/Hoa/EventException.php 0000644 00000003364 15233636331 0014417 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/StreamLockable.php 0000644 00000005060 15233636331 0014342 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/FileGeneric.php 0000644 00000026261 15233636331 0013634 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/FileLinkRead.php 0000644 00000013541 15233636331 0013746 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/StreamPointable.php 0000644 00000004620 15233636331 0014544 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/StreamTouchable.php 0000644 00000006141 15233636331 0014535 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\Touchable. * * Interface for touchable input/output. */ interface StreamTouchable extends IStream { /** * Overwrite file if already exists. */ const OVERWRITE = true; /** * Do not overwrite file if already exists. */ const DO_NOT_OVERWRITE = false; /** * Make directory if does not exist. */ const MAKE_DIRECTORY = true; /** * Do not make directory if does not exist. */ const DO_NOT_MAKE_DIRECTORY = false; /** * Set access and modification time of file. */ public function touch(int $time = -1, int $atime = -1): bool; /** * Copy file. * Return the destination file path if succeed, false otherwise. */ public function copy(string $to, bool $force = self::DO_NOT_OVERWRITE): bool; /** * Move a file. */ public function move( string $name, bool $force = self::DO_NOT_OVERWRITE, bool $mkdir = self::DO_NOT_MAKE_DIRECTORY ): bool; /** * Delete a file. */ public function delete(): bool; /** * Change file group. */ public function changeGroup($group): bool; /** * Change file mode. */ public function changeMode(int $mode): bool; /** * Change file owner. */ public function changeOwner($user): bool; /** * Change the current umask. */ public static function umask(int $umask = null): int; } psysh/src/Readline/Hoa/FileDirectory.php 0000644 00000014525 15233636331 0014224 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\Directory. * * Directory handler. */ class FileDirectory extends FileGeneric { /** * Open for reading. */ const MODE_READ = 'rb'; /** * Open for reading and writing. If the directory does not exist, attempt to * create it. */ const MODE_CREATE = 'xb'; /** * Open for reading and writing. If the directory does not exist, attempt to * create it recursively. */ const MODE_CREATE_RECURSIVE = 'xrb'; /** * Open a directory. */ public function __construct( string $streamName, string $mode = self::MODE_READ, string $context = null, bool $wait = false ) { $this->setMode($mode); parent::__construct($streamName, $context, $wait); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { if (false === \is_dir($streamName)) { if ($this->getMode() === self::MODE_READ) { throw new FileDoesNotExistException('Directory %s does not exist.', 0, $streamName); } else { self::create( $streamName, $this->getMode(), null !== $context ? $context->getContext() : null ); } } $out = null; return $out; } /** * Close the current stream. */ protected function _close(): bool { return true; } /** * Recursive copy of a directory. */ public function copy(string $to, bool $force = StreamTouchable::DO_NOT_OVERWRITE): bool { if (empty($to)) { throw new FileException('The destination path (to copy) is empty.', 1); } $from = $this->getStreamName(); $fromLength = \strlen($from) + 1; $finder = new FileFinder(); $finder->in($from); self::create($to, self::MODE_CREATE_RECURSIVE); foreach ($finder as $file) { $relative = \substr($file->getPathname(), $fromLength); $_to = $to.\DIRECTORY_SEPARATOR.$relative; if (true === $file->isDir()) { self::create($_to, self::MODE_CREATE); continue; } // This is not possible to do `$file->open()->copy(); // $file->close();` because the file will be opened in read and // write mode. In a PHAR for instance, this operation is // forbidden. So a special care must be taken to open file in read // only mode. $handle = null; if (true === $file->isFile()) { $handle = new FileRead($file->getPathname()); } elseif (true === $file->isDir()) { $handle = new self($file->getPathName()); } elseif (true === $file->isLink()) { $handle = new FileLinkRead($file->getPathName()); } if (null !== $handle) { $handle->copy($_to, $force); $handle->close(); } } return true; } /** * Delete a directory. */ public function delete(): bool { $from = $this->getStreamName(); $finder = new FileFinder(); $finder->in($from) ->childFirst(); foreach ($finder as $file) { $file->open()->delete(); $file->close(); } if (null === $this->getStreamContext()) { return @\rmdir($from); } return @\rmdir($from, $this->getStreamContext()->getContext()); } /** * Create a directory. */ public static function create( string $name, string $mode = self::MODE_CREATE_RECURSIVE, string $context = null ): bool { if (true === \is_dir($name)) { return true; } if (empty($name)) { return false; } if (null !== $context) { if (false === StreamContext::contextExists($context)) { throw new FileException('Context %s was not previously declared, cannot retrieve '.'this context.', 2, $context); } else { $context = StreamContext::getInstance($context); } } if (null === $context) { return @\mkdir( $name, 0755, self::MODE_CREATE_RECURSIVE === $mode ); } return @\mkdir( $name, 0755, self::MODE_CREATE_RECURSIVE === $mode, $context->getContext() ); } } psysh/src/Readline/Hoa/ConsoleException.php 0000644 00000003431 15233636331 0014733 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\Exception. * * Extending the \Hoa\Exception\Exception class. */ class ConsoleException extends Exception { } psysh/src/Readline/Hoa/ConsoleWindow.php 0000644 00000030231 15233636331 0014242 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/AutocompleterAggregate.php 0000644 00000006414 15233636331 0016110 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/ProtocolException.php 0000644 00000003365 15233636331 0015140 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/StreamStatable.php 0000644 00000005767 15233636331 0014403 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\Statable. * * Interface for statable input/output. */ interface StreamStatable extends IStream { /** * Size is undefined. */ const SIZE_UNDEFINED = -1; /** * Get size. */ public function getSize(): int; /** * Get informations about a file. */ public function getStatistic(): array; /** * Get last access time of file. */ public function getATime(): int; /** * Get inode change time of file. */ public function getCTime(): int; /** * Get file modification time. */ public function getMTime(): int; /** * Get file group. */ public function getGroup(): int; /** * Get file owner. */ public function getOwner(): int; /** * Get file permissions. */ public function getPermissions(): int; /** * Check if the file is readable. */ public function isReadable(): bool; /** * Check if the file is writable. */ public function isWritable(): bool; /** * Check if the file is executable. */ public function isExecutable(): bool; /** * Clear file status cache. */ public function clearStatisticCache(); /** * Clear all files status cache. */ public static function clearAllStatisticCaches(); } psysh/src/Readline/Hoa/ConsoleProcessus.php 0000644 00000050674 15233636331 0014776 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; /** * Manipulate a processus as a stream. */ class ConsoleProcessus extends Stream implements StreamIn, StreamOut, StreamPathable { /** * Signal: terminal line hangup (terminate process). */ const SIGHUP = 1; /** * Signal: interrupt program (terminate process). */ const SIGINT = 2; /** * Signal: quit program (create core image). */ const SIGQUIT = 3; /** * Signal: illegal instruction (create core image). */ const SIGILL = 4; /** * Signal: trace trap (create core image). */ const SIGTRAP = 5; /** * Signal: abort program, formerly SIGIOT (create core image). */ const SIGABRT = 6; /** * Signal: emulate instruction executed (create core image). */ const SIGEMT = 7; /** * Signal: floating-point exception (create core image). */ const SIGFPE = 8; /** * Signal: kill program (terminate process). */ const SIGKILL = 9; /** * Signal: bus error. */ const SIGBUS = 10; /** * Signal: segmentation violation (create core image). */ const SIGSEGV = 11; /** * Signal: non-existent system call invoked (create core image). */ const SIGSYS = 12; /** * Signal: write on a pipe with no reader (terminate process). */ const SIGPIPE = 13; /** * Signal: real-time timer expired (terminate process). */ const SIGALRM = 14; /** * Signal: software termination signal (terminate process). */ const SIGTERM = 15; /** * Signal: urgent condition present on socket (discard signal). */ const SIGURG = 16; /** * Signal: stop, cannot be caught or ignored (stop proces). */ const SIGSTOP = 17; /** * Signal: stop signal generated from keyboard (stop process). */ const SIGTSTP = 18; /** * Signal: continue after stop (discard signal). */ const SIGCONT = 19; /** * Signal: child status has changed (discard signal). */ const SIGCHLD = 20; /** * Signal: background read attempted from control terminal (stop process). */ const SIGTTIN = 21; /** * Signal: background write attempted to control terminal (stop process). */ const SIGTTOU = 22; /** * Signal: I/O is possible on a descriptor, see fcntl(2) (discard signal). */ const SIGIO = 23; /** * Signal: cpu time limit exceeded, see setrlimit(2) (terminate process). */ const SIGXCPU = 24; /** * Signal: file size limit exceeded, see setrlimit(2) (terminate process). */ const SIGXFSZ = 25; /** * Signal: virtual time alarm, see setitimer(2) (terminate process). */ const SIGVTALRM = 26; /** * Signal: profiling timer alarm, see setitimer(2) (terminate process). */ const SIGPROF = 27; /** * Signal: Window size change (discard signal). */ const SIGWINCH = 28; /** * Signal: status request from keyboard (discard signal). */ const SIGINFO = 29; /** * Signal: User defined signal 1 (terminate process). */ const SIGUSR1 = 30; /** * Signal: User defined signal 2 (terminate process). */ const SIGUSR2 = 31; /** * Command name. */ protected $_command = null; /** * Command options (options => value, or input). */ protected $_options = []; /** * Current working directory. */ protected $_cwd = null; /** * Environment. */ protected $_environment = null; /** * Timeout. */ protected $_timeout = 30; /** * Descriptor. */ protected $_descriptors = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; /** * Pipe descriptors of the processus. */ protected $_pipes = null; /** * Seekability of pipes. */ protected $_seekable = []; /** * Start a processus. */ public function __construct( string $command, array $options = null, array $descriptors = null, string $cwd = null, array $environment = null, int $timeout = 30 ) { $this->setCommand($command); if (null !== $options) { $this->setOptions($options); } if (null !== $descriptors) { $this->_descriptors = []; foreach ($descriptors as $descriptor => $nature) { if (isset($this->_descriptors[$descriptor])) { throw new ConsoleException('Pipe descriptor %d already exists, cannot '.'redefine it.', 0, $descriptor); } $this->_descriptors[$descriptor] = $nature; } } $this->setCwd($cwd ?: \getcwd()); if (null !== $environment) { $this->setEnvironment($environment); } $this->setTimeout($timeout); parent::__construct($this->getCommandLine(), null, true); $this->getListener()->addIds(['input', 'output', 'timeout', 'start', 'stop']); return; } /** * Open the stream and return the associated resource. */ protected function &_open(string $streamName, StreamContext $context = null) { $out = @\proc_open( $streamName, $this->_descriptors, $this->_pipes, $this->getCwd(), $this->getEnvironment() ); if (false === $out) { throw new ConsoleException('Something wrong happen when running %s.', 1, $streamName); } return $out; } /** * Close the current stream. */ protected function _close(): bool { foreach ($this->_pipes as $pipe) { @\fclose($pipe); } return (bool) @\proc_close($this->getStream()); } /** * Run the process and fire events (amongst start, stop, input, output and * timeout). * If an event returns false, it will close the current pipe. * For a simple run without firing events, use the $this->open() method. */ public function run() { if (false === $this->isOpened()) { $this->open(); } else { $this->_close(); $this->_setStream($this->_open( $this->getStreamName(), $this->getStreamContext() )); } $this->getListener()->fire('start', new EventBucket()); $_read = []; $_write = []; $_except = []; foreach ($this->_pipes as $p => $pipe) { switch ($this->_descriptors[$p][1]) { case 'r': \stream_set_blocking($pipe, false); $_write[] = $pipe; break; case 'w': case 'a': \stream_set_blocking($pipe, true); $_read[] = $pipe; break; } } while (true) { foreach ($_read as $i => $r) { if (false === \is_resource($r)) { unset($_read[$i]); } } foreach ($_write as $i => $w) { if (false === \is_resource($w)) { unset($_write[$i]); } } foreach ($_except as $i => $e) { if (false === \is_resource($e)) { unset($_except[$i]); } } if (empty($_read) && empty($_write) && empty($_except)) { break; } $read = $_read; $write = $_write; $except = $_except; $select = \stream_select($read, $write, $except, $this->getTimeout()); if (0 === $select) { $this->getListener()->fire('timeout', new EventBucket()); break; } foreach ($read as $i => $_r) { $pipe = \array_search($_r, $this->_pipes); $line = $this->readLine($pipe); if (false === $line) { $result = [false]; } else { $result = $this->getListener()->fire( 'output', new EventBucket([ 'pipe' => $pipe, 'line' => $line, ]) ); } if (true === \feof($_r) || \in_array(false, $result, true)) { \fclose($_r); unset($_read[$i]); break; } } foreach ($write as $j => $_w) { $result = $this->getListener()->fire( 'input', new EventBucket([ 'pipe' => \array_search($_w, $this->_pipes), ]) ); if (true === \feof($_w) || \in_array(false, $result, true)) { \fclose($_w); unset($_write[$j]); } } if (empty($_read)) { break; } } $this->getListener()->fire('stop', new EventBucket()); return; } /** * Get pipe resource. */ protected function getPipe(int $pipe) { if (!isset($this->_pipes[$pipe])) { throw new ConsoleException('Pipe descriptor %d does not exist, cannot read from it.', 2, $pipe); } return $this->_pipes[$pipe]; } /** * Check if a pipe is seekable or not. */ protected function isPipeSeekable(int $pipe): bool { if (!isset($this->_seekable[$pipe])) { $_pipe = $this->getPipe($pipe); $data = \stream_get_meta_data($_pipe); $this->_seekable[$pipe] = $data['seekable']; } return $this->_seekable[$pipe]; } /** * Test for end-of-file. */ public function eof(int $pipe = 1): bool { return \feof($this->getPipe($pipe)); } /** * Read n characters. */ public function read(int $length, int $pipe = 1) { if (0 > $length) { throw new ConsoleException('Length must be greater than 0, given %d.', 3, $length); } return \fread($this->getPipe($pipe), $length); } /** * Alias of $this->read(). */ public function readString(int $length, int $pipe = 1) { return $this->read($length, $pipe); } /** * Read a character. */ public function readCharacter(int $pipe = 1) { return \fgetc($this->getPipe($pipe)); } /** * Read a boolean. */ public function readBoolean(int $pipe = 1) { return (bool) $this->read(1, $pipe); } /** * Read an integer. */ public function readInteger(int $length = 1, int $pipe = 1) { return (int) $this->read($length, $pipe); } /** * Read a float. */ public function readFloat(int $length = 1, int $pipe = 1) { return (float) $this->read($length, $pipe); } /** * Read an array. * Alias of the $this->scanf() method. */ public function readArray(string $format = null, int $pipe = 1) { return $this->scanf($format, $pipe); } /** * Read a line. */ public function readLine(int $pipe = 1) { return \stream_get_line($this->getPipe($pipe), 1 << 15, "\n"); } /** * Read all, i.e. read as much as possible. */ public function readAll(int $offset = -1, int $pipe = 1) { $_pipe = $this->getPipe($pipe); if (true === $this->isPipeSeekable($pipe)) { $offset += \ftell($_pipe); } else { $offset = -1; } return \stream_get_contents($_pipe, -1, $offset); } /** * Parse input from a stream according to a format. */ public function scanf(string $format, int $pipe = 1): array { return \fscanf($this->getPipe($pipe), $format); } /** * Write n characters. */ public function write(string $string, int $length, int $pipe = 0) { if (0 > $length) { throw new ConsoleException('Length must be greater than 0, given %d.', 4, $length); } return \fwrite($this->getPipe($pipe), $string, $length); } /** * Write a string. */ public function writeString(string $string, int $pipe = 0) { $string = (string) $string; return $this->write($string, \strlen($string), $pipe); } /** * Write a character. */ public function writeCharacter(string $char, int $pipe = 0) { return $this->write((string) $char[0], 1, $pipe); } /** * Write a boolean. */ public function writeBoolean(bool $boolean, int $pipe = 0) { return $this->write((string) (bool) $boolean, 1, $pipe); } /** * Write an integer. */ public function writeInteger(int $integer, int $pipe = 0) { $integer = (string) (int) $integer; return $this->write($integer, \strlen($integer), $pipe); } /** * Write a float. */ public function writeFloat(float $float, int $pipe = 0) { $float = (string) (float) $float; return $this->write($float, \strlen($float), $pipe); } /** * Write an array. */ public function writeArray(array $array, int $pipe = 0) { $array = \var_export($array, true); return $this->write($array, \strlen($array), $pipe); } /** * Write a line. */ public function writeLine(string $line, int $pipe = 0) { if (false === $n = \strpos($line, "\n")) { return $this->write($line."\n", \strlen($line) + 1, $pipe); } ++$n; return $this->write(\substr($line, 0, $n), $n, $pipe); } /** * Write all, i.e. as much as possible. */ public function writeAll(string $string, int $pipe = 0) { return $this->write($string, \strlen($string), $pipe); } /** * Truncate a file to a given length. */ public function truncate(int $size, int $pipe = 0): bool { return \ftruncate($this->getPipe($pipe), $size); } /** * Get filename component of path. */ public function getBasename(): string { return \basename($this->getCommand()); } /** * Get directory name component of path. */ public function getDirname(): string { return \dirname($this->getCommand()); } /** * Get status. */ public function getStatus(): array { return \proc_get_status($this->getStream()); } /** * Get exit code (alias of $this->getStatus()['exitcode']);. */ public function getExitCode(): int { $handle = $this->getStatus(); return $handle['exitcode']; } /** * Whether the processus have ended successfully. * * @return bool */ public function isSuccessful(): bool { return 0 === $this->getExitCode(); } /** * Terminate the process. * * Valid signals are self::SIGHUP, SIGINT, SIGQUIT, SIGABRT, SIGKILL, * SIGALRM and SIGTERM. */ public function terminate(int $signal = self::SIGTERM): bool { return \proc_terminate($this->getStream(), $signal); } /** * Set command name. */ protected function setCommand(string $command) { $old = $this->_command; $this->_command = \escapeshellcmd($command); return $old; } /** * Get command name. */ public function getCommand() { return $this->_command; } /** * Set command options. */ protected function setOptions(array $options): array { foreach ($options as &$option) { $option = \escapeshellarg($option); } $old = $this->_options; $this->_options = $options; return $old; } /** * Get options. */ public function getOptions(): array { return $this->_options; } /** * Get command-line. */ public function getCommandLine(): string { $out = $this->getCommand(); foreach ($this->getOptions() as $key => $value) { if (!\is_int($key)) { $out .= ' '.$key.'='.$value; } else { $out .= ' '.$value; } } return $out; } /** * Set current working directory of the process. */ protected function setCwd(string $cwd) { $old = $this->_cwd; $this->_cwd = $cwd; return $old; } /** * Get current working directory of the process. */ public function getCwd(): string { return $this->_cwd; } /** * Set environment of the process. */ protected function setEnvironment(array $environment) { $old = $this->_environment; $this->_environment = $environment; return $old; } /** * Get environment of the process. */ public function getEnvironment() { return $this->_environment; } /** * Set timeout of the process. */ public function setTimeout(int $timeout) { $old = $this->_timeout; $this->_timeout = $timeout; return $old; } /** * Get timeout of the process. */ public function getTimeout(): int { return $this->_timeout; } /** * Set process title. */ public static function setTitle(string $title) { \cli_set_process_title($title); } /** * Get process title. */ public static function getTitle() { return \cli_get_process_title(); } /** * Found the place of a binary. */ public static function locate(string $binary) { if (isset($_ENV['PATH'])) { $separator = ':'; $path = &$_ENV['PATH']; } elseif (isset($_SERVER['PATH'])) { $separator = ':'; $path = &$_SERVER['PATH']; } elseif (isset($_SERVER['Path'])) { $separator = ';'; $path = &$_SERVER['Path']; } else { return null; } foreach (\explode($separator, $path) as $directory) { if (true === \file_exists($out = $directory.\DIRECTORY_SEPARATOR.$binary)) { return $out; } } return null; } /** * Quick process execution. * Returns only the STDOUT. */ public static function execute(string $commandLine, bool $escape = true): string { if (true === $escape) { $commandLine = \escapeshellcmd($commandLine); } return \rtrim(\shell_exec($commandLine) ?? ''); } } psysh/src/Readline/Hoa/StreamBufferable.php 0000644 00000004674 15233636331 0014675 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/Terminfo/77/windows-ansi 0000644 00000002711 15233636331 0015326 0 ustar 00 ( &