Chris@13: run(); Chris@13: * Chris@13: * @author Justin Hileman Chris@13: */ Chris@13: class Shell extends Application Chris@13: { Chris@17: const VERSION = 'v0.9.9'; Chris@13: Chris@13: const PROMPT = '>>> '; Chris@13: const BUFF_PROMPT = '... '; Chris@13: const REPLAY = '--> '; Chris@13: const RETVAL = '=> '; Chris@13: Chris@13: private $config; Chris@13: private $cleaner; Chris@13: private $output; Chris@13: private $readline; Chris@13: private $inputBuffer; Chris@13: private $code; Chris@13: private $codeBuffer; Chris@13: private $codeBufferOpen; Chris@13: private $codeStack; Chris@13: private $stdoutBuffer; Chris@13: private $context; Chris@13: private $includes; Chris@13: private $loop; Chris@13: private $outputWantsNewline = false; Chris@13: private $prompt; Chris@13: private $loopListeners; Chris@13: private $autoCompleter; Chris@13: private $matchers = []; Chris@13: private $commandsMatcher; Chris@17: private $lastExecSuccess = true; Chris@13: Chris@13: /** Chris@13: * Create a new Psy Shell. Chris@13: * Chris@13: * @param Configuration $config (default: null) Chris@13: */ Chris@13: public function __construct(Configuration $config = null) Chris@13: { Chris@13: $this->config = $config ?: new Configuration(); Chris@13: $this->cleaner = $this->config->getCodeCleaner(); Chris@13: $this->loop = new ExecutionLoop(); Chris@13: $this->context = new Context(); Chris@13: $this->includes = []; Chris@13: $this->readline = $this->config->getReadline(); Chris@13: $this->inputBuffer = []; Chris@13: $this->codeStack = []; Chris@13: $this->stdoutBuffer = ''; Chris@13: $this->loopListeners = $this->getDefaultLoopListeners(); Chris@13: Chris@13: parent::__construct('Psy Shell', self::VERSION); Chris@13: Chris@13: $this->config->setShell($this); Chris@13: Chris@13: // Register the current shell session's config with \Psy\info Chris@13: \Psy\info($this->config); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether the first thing in a backtrace is an include call. Chris@13: * Chris@13: * This is used by the psysh bin to decide whether to start a shell on boot, Chris@13: * or to simply autoload the library. Chris@13: */ Chris@13: public static function isIncluded(array $trace) Chris@13: { Chris@13: return isset($trace[0]['function']) && Chris@17: \in_array($trace[0]['function'], ['require', 'include', 'require_once', 'include_once']); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Invoke a Psy Shell from the current context. Chris@13: * Chris@13: * @see Psy\debug Chris@13: * @deprecated will be removed in 1.0. Use \Psy\debug instead Chris@13: * Chris@16: * @param array $vars Scope variables from the calling context (default: array()) Chris@16: * @param object|string $bindTo Bound object ($this) or class (self) value for the shell Chris@13: * Chris@13: * @return array Scope variables from the debugger session Chris@13: */ Chris@16: public static function debug(array $vars = [], $bindTo = null) Chris@13: { Chris@16: return \Psy\debug($vars, $bindTo); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Adds a command object. Chris@13: * Chris@13: * {@inheritdoc} Chris@13: * Chris@13: * @param BaseCommand $command A Symfony Console Command object Chris@13: * Chris@13: * @return BaseCommand The registered command Chris@13: */ Chris@13: public function add(BaseCommand $command) Chris@13: { Chris@13: if ($ret = parent::add($command)) { Chris@13: if ($ret instanceof ContextAware) { Chris@13: $ret->setContext($this->context); Chris@13: } Chris@13: Chris@13: if ($ret instanceof PresenterAware) { Chris@13: $ret->setPresenter($this->config->getPresenter()); Chris@13: } Chris@13: Chris@13: if (isset($this->commandsMatcher)) { Chris@13: $this->commandsMatcher->setCommands($this->all()); Chris@13: } Chris@13: } Chris@13: Chris@13: return $ret; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Gets the default input definition. Chris@13: * Chris@13: * @return InputDefinition An InputDefinition instance Chris@13: */ Chris@13: protected function getDefaultInputDefinition() Chris@13: { Chris@13: return new InputDefinition([ Chris@13: new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), Chris@13: new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), Chris@13: ]); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Gets the default commands that should always be available. Chris@13: * Chris@13: * @return array An array of default Command instances Chris@13: */ Chris@13: protected function getDefaultCommands() Chris@13: { Chris@13: $sudo = new Command\SudoCommand(); Chris@13: $sudo->setReadline($this->readline); Chris@13: Chris@13: $hist = new Command\HistoryCommand(); Chris@13: $hist->setReadline($this->readline); Chris@13: Chris@13: return [ Chris@13: new Command\HelpCommand(), Chris@13: new Command\ListCommand(), Chris@13: new Command\DumpCommand(), Chris@13: new Command\DocCommand(), Chris@13: new Command\ShowCommand($this->config->colorMode()), Chris@13: new Command\WtfCommand($this->config->colorMode()), Chris@13: new Command\WhereamiCommand($this->config->colorMode()), Chris@13: new Command\ThrowUpCommand(), Chris@13: new Command\TimeitCommand(), Chris@13: new Command\TraceCommand(), Chris@13: new Command\BufferCommand(), Chris@13: new Command\ClearCommand(), Chris@13: new Command\EditCommand($this->config->getRuntimeDir()), Chris@13: // new Command\PsyVersionCommand(), Chris@13: $sudo, Chris@13: $hist, Chris@13: new Command\ExitCommand(), Chris@13: ]; Chris@13: } Chris@13: Chris@13: /** Chris@13: * @return array Chris@13: */ Chris@13: protected function getDefaultMatchers() Chris@13: { Chris@13: // Store the Commands Matcher for later. If more commands are added, Chris@13: // we'll update the Commands Matcher too. Chris@13: $this->commandsMatcher = new Matcher\CommandsMatcher($this->all()); Chris@13: Chris@13: return [ Chris@13: $this->commandsMatcher, Chris@13: new Matcher\KeywordsMatcher(), Chris@13: new Matcher\VariablesMatcher(), Chris@13: new Matcher\ConstantsMatcher(), Chris@13: new Matcher\FunctionsMatcher(), Chris@13: new Matcher\ClassNamesMatcher(), Chris@13: new Matcher\ClassMethodsMatcher(), Chris@13: new Matcher\ClassAttributesMatcher(), Chris@13: new Matcher\ObjectMethodsMatcher(), Chris@13: new Matcher\ObjectAttributesMatcher(), Chris@13: new Matcher\ClassMethodDefaultParametersMatcher(), Chris@13: new Matcher\ObjectMethodDefaultParametersMatcher(), Chris@13: new Matcher\FunctionDefaultParametersMatcher(), Chris@13: ]; Chris@13: } Chris@13: Chris@13: /** Chris@13: * @deprecated Nothing should use this anymore Chris@13: */ Chris@13: protected function getTabCompletionMatchers() Chris@13: { Chris@17: @\trigger_error('getTabCompletionMatchers is no longer used', E_USER_DEPRECATED); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Gets the default command loop listeners. Chris@13: * Chris@13: * @return array An array of Execution Loop Listener instances Chris@13: */ Chris@13: protected function getDefaultLoopListeners() Chris@13: { Chris@13: $listeners = []; Chris@13: Chris@13: if (ProcessForker::isSupported() && $this->config->usePcntl()) { Chris@13: $listeners[] = new ProcessForker(); Chris@13: } Chris@13: Chris@13: if (RunkitReloader::isSupported()) { Chris@13: $listeners[] = new RunkitReloader(); Chris@13: } Chris@13: Chris@13: return $listeners; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Add tab completion matchers. Chris@13: * Chris@13: * @param array $matchers Chris@13: */ Chris@13: public function addMatchers(array $matchers) Chris@13: { Chris@17: $this->matchers = \array_merge($this->matchers, $matchers); Chris@13: Chris@13: if (isset($this->autoCompleter)) { Chris@13: $this->addMatchersToAutoCompleter($matchers); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * @deprecated Call `addMatchers` instead Chris@13: * Chris@13: * @param array $matchers Chris@13: */ Chris@13: public function addTabCompletionMatchers(array $matchers) Chris@13: { Chris@13: $this->addMatchers($matchers); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the Shell output. Chris@13: * Chris@13: * @param OutputInterface $output Chris@13: */ Chris@13: public function setOutput(OutputInterface $output) Chris@13: { Chris@13: $this->output = $output; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Runs the current application. Chris@13: * Chris@13: * @param InputInterface $input An Input instance Chris@13: * @param OutputInterface $output An Output instance Chris@13: * Chris@13: * @return int 0 if everything went fine, or an error code Chris@13: */ Chris@13: public function run(InputInterface $input = null, OutputInterface $output = null) Chris@13: { Chris@13: $this->initializeTabCompletion(); Chris@13: Chris@13: if ($input === null && !isset($_SERVER['argv'])) { Chris@13: $input = new ArgvInput([]); Chris@13: } Chris@13: Chris@13: if ($output === null) { Chris@13: $output = $this->config->getOutput(); Chris@13: } Chris@13: Chris@13: try { Chris@13: return parent::run($input, $output); Chris@13: } catch (\Exception $e) { Chris@13: $this->writeException($e); Chris@13: } Chris@13: Chris@13: return 1; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Runs the current application. Chris@13: * Chris@13: * @throws Exception if thrown via the `throw-up` command Chris@13: * Chris@13: * @param InputInterface $input An Input instance Chris@13: * @param OutputInterface $output An Output instance Chris@13: * Chris@13: * @return int 0 if everything went fine, or an error code Chris@13: */ Chris@13: public function doRun(InputInterface $input, OutputInterface $output) Chris@13: { Chris@13: $this->setOutput($output); Chris@13: Chris@13: $this->resetCodeBuffer(); Chris@13: Chris@13: $this->setAutoExit(false); Chris@13: $this->setCatchExceptions(false); Chris@13: Chris@13: $this->readline->readHistory(); Chris@13: Chris@13: $this->output->writeln($this->getHeader()); Chris@13: $this->writeVersionInfo(); Chris@13: $this->writeStartupMessage(); Chris@13: Chris@13: try { Chris@13: $this->beforeRun(); Chris@13: $this->loop->run($this); Chris@13: $this->afterRun(); Chris@13: } catch (ThrowUpException $e) { Chris@13: throw $e->getPrevious(); Chris@13: } catch (BreakException $e) { Chris@13: // The ProcessForker throws a BreakException to finish the main thread. Chris@13: return; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Read user input. Chris@13: * Chris@13: * This will continue fetching user input until the code buffer contains Chris@13: * valid code. Chris@13: * Chris@13: * @throws BreakException if user hits Ctrl+D Chris@13: */ Chris@13: public function getInput() Chris@13: { Chris@13: $this->codeBufferOpen = false; Chris@13: Chris@13: do { Chris@13: // reset output verbosity (in case it was altered by a subcommand) Chris@13: $this->output->setVerbosity(ShellOutput::VERBOSITY_VERBOSE); Chris@13: Chris@13: $input = $this->readline(); Chris@13: Chris@13: /* Chris@13: * Handle Ctrl+D. It behaves differently in different cases: Chris@13: * Chris@13: * 1) In an expression, like a function or "if" block, clear the input buffer Chris@13: * 2) At top-level session, behave like the exit command Chris@13: */ Chris@13: if ($input === false) { Chris@13: $this->output->writeln(''); Chris@13: Chris@13: if ($this->hasCode()) { Chris@13: $this->resetCodeBuffer(); Chris@13: } else { Chris@13: throw new BreakException('Ctrl+D'); Chris@13: } Chris@13: } Chris@13: Chris@13: // handle empty input Chris@17: if (\trim($input) === '' && !$this->codeBufferOpen) { Chris@13: continue; Chris@13: } Chris@13: Chris@13: $input = $this->onInput($input); Chris@13: Chris@16: // If the input isn't in an open string or comment, check for commands to run. Chris@16: if ($this->hasCommand($input) && !$this->inputInOpenStringOrComment($input)) { Chris@13: $this->addHistory($input); Chris@13: $this->runCommand($input); Chris@13: Chris@13: continue; Chris@13: } Chris@13: Chris@13: $this->addCode($input); Chris@13: } while (!$this->hasValidCode()); Chris@13: } Chris@13: Chris@13: /** Chris@16: * Check whether the code buffer (plus current input) is in an open string or comment. Chris@16: * Chris@16: * @param string $input current line of input Chris@16: * Chris@16: * @return bool true if the input is in an open string or comment Chris@16: */ Chris@16: private function inputInOpenStringOrComment($input) Chris@16: { Chris@16: if (!$this->hasCode()) { Chris@16: return; Chris@16: } Chris@16: Chris@16: $code = $this->codeBuffer; Chris@17: \array_push($code, $input); Chris@17: $tokens = @\token_get_all('loopListeners as $listener) { Chris@13: $listener->beforeRun($this); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Run execution loop listeners at the start of each loop. Chris@13: */ Chris@13: public function beforeLoop() Chris@13: { Chris@13: foreach ($this->loopListeners as $listener) { Chris@13: $listener->beforeLoop($this); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Run execution loop listeners on user input. Chris@13: * Chris@13: * @param string $input Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function onInput($input) Chris@13: { Chris@13: foreach ($this->loopListeners as $listeners) { Chris@13: if (($return = $listeners->onInput($this, $input)) !== null) { Chris@13: $input = $return; Chris@13: } Chris@13: } Chris@13: Chris@13: return $input; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Run execution loop listeners on code to be executed. Chris@13: * Chris@13: * @param string $code Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function onExecute($code) Chris@13: { Chris@13: foreach ($this->loopListeners as $listener) { Chris@13: if (($return = $listener->onExecute($this, $code)) !== null) { Chris@13: $code = $return; Chris@13: } Chris@13: } Chris@13: Chris@13: return $code; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Run execution loop listeners after each loop. Chris@13: */ Chris@13: public function afterLoop() Chris@13: { Chris@13: foreach ($this->loopListeners as $listener) { Chris@13: $listener->afterLoop($this); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Run execution loop listers after the shell session. Chris@13: */ Chris@13: protected function afterRun() Chris@13: { Chris@13: foreach ($this->loopListeners as $listener) { Chris@13: $listener->afterRun($this); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the variables currently in scope. Chris@13: * Chris@13: * @param array $vars Chris@13: */ Chris@13: public function setScopeVariables(array $vars) Chris@13: { Chris@13: $this->context->setAll($vars); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Return the set of variables currently in scope. Chris@13: * Chris@13: * @param bool $includeBoundObject Pass false to exclude 'this'. If you're Chris@13: * passing the scope variables to `extract` Chris@13: * in PHP 7.1+, you _must_ exclude 'this' Chris@13: * Chris@13: * @return array Associative array of scope variables Chris@13: */ Chris@13: public function getScopeVariables($includeBoundObject = true) Chris@13: { Chris@13: $vars = $this->context->getAll(); Chris@13: Chris@13: if (!$includeBoundObject) { Chris@13: unset($vars['this']); Chris@13: } Chris@13: Chris@13: return $vars; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Return the set of magic variables currently in scope. Chris@13: * Chris@13: * @param bool $includeBoundObject Pass false to exclude 'this'. If you're Chris@13: * passing the scope variables to `extract` Chris@13: * in PHP 7.1+, you _must_ exclude 'this' Chris@13: * Chris@13: * @return array Associative array of magic scope variables Chris@13: */ Chris@13: public function getSpecialScopeVariables($includeBoundObject = true) Chris@13: { Chris@13: $vars = $this->context->getSpecialVariables(); Chris@13: Chris@13: if (!$includeBoundObject) { Chris@13: unset($vars['this']); Chris@13: } Chris@13: Chris@13: return $vars; Chris@13: } Chris@13: Chris@13: /** Chris@17: * Return the set of variables currently in scope which differ from the Chris@17: * values passed as $currentVars. Chris@17: * Chris@17: * This is used inside the Execution Loop Closure to pick up scope variable Chris@17: * changes made by commands while the loop is running. Chris@17: * Chris@17: * @param array $currentVars Chris@17: * Chris@17: * @return array Associative array of scope variables which differ from $currentVars Chris@17: */ Chris@17: public function getScopeVariablesDiff(array $currentVars) Chris@17: { Chris@17: $newVars = []; Chris@17: Chris@17: foreach ($this->getScopeVariables(false) as $key => $value) { Chris@17: if (!array_key_exists($key, $currentVars) || $currentVars[$key] !== $value) { Chris@17: $newVars[$key] = $value; Chris@17: } Chris@17: } Chris@17: Chris@17: return $newVars; Chris@17: } Chris@17: Chris@17: /** Chris@13: * Get the set of unused command-scope variable names. Chris@13: * Chris@13: * @return array Array of unused variable names Chris@13: */ Chris@13: public function getUnusedCommandScopeVariableNames() Chris@13: { Chris@13: return $this->context->getUnusedCommandScopeVariableNames(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the set of variable names currently in scope. Chris@13: * Chris@13: * @return array Array of variable names Chris@13: */ Chris@13: public function getScopeVariableNames() Chris@13: { Chris@17: return \array_keys($this->context->getAll()); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a scope variable value by name. Chris@13: * Chris@13: * @param string $name Chris@13: * Chris@13: * @return mixed Chris@13: */ Chris@13: public function getScopeVariable($name) Chris@13: { Chris@13: return $this->context->get($name); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the bound object ($this variable) for the interactive shell. Chris@13: * Chris@13: * @param object|null $boundObject Chris@13: */ Chris@13: public function setBoundObject($boundObject) Chris@13: { Chris@13: $this->context->setBoundObject($boundObject); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the bound object ($this variable) for the interactive shell. Chris@13: * Chris@13: * @return object|null Chris@13: */ Chris@13: public function getBoundObject() Chris@13: { Chris@13: return $this->context->getBoundObject(); Chris@13: } Chris@13: Chris@13: /** Chris@16: * Set the bound class (self) for the interactive shell. Chris@16: * Chris@16: * @param string|null $boundClass Chris@16: */ Chris@16: public function setBoundClass($boundClass) Chris@16: { Chris@16: $this->context->setBoundClass($boundClass); Chris@16: } Chris@16: Chris@16: /** Chris@16: * Get the bound class (self) for the interactive shell. Chris@16: * Chris@16: * @return string|null Chris@16: */ Chris@16: public function getBoundClass() Chris@16: { Chris@16: return $this->context->getBoundClass(); Chris@16: } Chris@16: Chris@16: /** Chris@13: * Add includes, to be parsed and executed before running the interactive shell. Chris@13: * Chris@13: * @param array $includes Chris@13: */ Chris@13: public function setIncludes(array $includes = []) Chris@13: { Chris@13: $this->includes = $includes; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get PHP files to be parsed and executed before running the interactive shell. Chris@13: * Chris@13: * @return array Chris@13: */ Chris@13: public function getIncludes() Chris@13: { Chris@17: return \array_merge($this->config->getDefaultIncludes(), $this->includes); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether this shell's code buffer contains code. Chris@13: * Chris@13: * @return bool True if the code buffer contains code Chris@13: */ Chris@13: public function hasCode() Chris@13: { Chris@13: return !empty($this->codeBuffer); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether the code in this shell's code buffer is valid. Chris@13: * Chris@13: * If the code is valid, the code buffer should be flushed and evaluated. Chris@13: * Chris@13: * @return bool True if the code buffer content is valid Chris@13: */ Chris@13: protected function hasValidCode() Chris@13: { Chris@13: return !$this->codeBufferOpen && $this->code !== false; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Add code to the code buffer. Chris@13: * Chris@13: * @param string $code Chris@13: * @param bool $silent Chris@13: */ Chris@13: public function addCode($code, $silent = false) Chris@13: { Chris@13: try { Chris@13: // Code lines ending in \ keep the buffer open Chris@17: if (\substr(\rtrim($code), -1) === '\\') { Chris@13: $this->codeBufferOpen = true; Chris@17: $code = \substr(\rtrim($code), 0, -1); Chris@13: } else { Chris@13: $this->codeBufferOpen = false; Chris@13: } Chris@13: Chris@13: $this->codeBuffer[] = $silent ? new SilentInput($code) : $code; Chris@13: $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons()); Chris@13: } catch (\Exception $e) { Chris@13: // Add failed code blocks to the readline history. Chris@13: $this->addCodeBufferToHistory(); Chris@13: Chris@13: throw $e; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the code buffer. Chris@13: * Chris@13: * This is mostly used by `Shell::execute`. Any existing code in the input Chris@13: * buffer is pushed onto a stack and will come back after this new code is Chris@13: * executed. Chris@13: * Chris@13: * @throws \InvalidArgumentException if $code isn't a complete statement Chris@13: * Chris@13: * @param string $code Chris@13: * @param bool $silent Chris@13: */ Chris@13: private function setCode($code, $silent = false) Chris@13: { Chris@13: if ($this->hasCode()) { Chris@13: $this->codeStack[] = [$this->codeBuffer, $this->codeBufferOpen, $this->code]; Chris@13: } Chris@13: Chris@13: $this->resetCodeBuffer(); Chris@13: try { Chris@13: $this->addCode($code, $silent); Chris@13: } catch (\Throwable $e) { Chris@13: $this->popCodeStack(); Chris@13: Chris@13: throw $e; Chris@13: } catch (\Exception $e) { Chris@13: $this->popCodeStack(); Chris@13: Chris@13: throw $e; Chris@13: } Chris@13: Chris@13: if (!$this->hasValidCode()) { Chris@13: $this->popCodeStack(); Chris@13: Chris@13: throw new \InvalidArgumentException('Unexpected end of input'); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current code buffer. Chris@13: * Chris@13: * This is useful for commands which manipulate the buffer. Chris@13: * Chris@13: * @return array Chris@13: */ Chris@13: public function getCodeBuffer() Chris@13: { Chris@13: return $this->codeBuffer; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Run a Psy Shell command given the user input. Chris@13: * Chris@13: * @throws InvalidArgumentException if the input is not a valid command Chris@13: * Chris@13: * @param string $input User input string Chris@13: * Chris@13: * @return mixed Who knows? Chris@13: */ Chris@13: protected function runCommand($input) Chris@13: { Chris@13: $command = $this->getCommand($input); Chris@13: Chris@13: if (empty($command)) { Chris@13: throw new \InvalidArgumentException('Command not found: ' . $input); Chris@13: } Chris@13: Chris@17: $input = new ShellInput(\str_replace('\\', '\\\\', \rtrim($input, " \t\n\r\0\x0B;"))); Chris@13: Chris@13: if ($input->hasParameterOption(['--help', '-h'])) { Chris@13: $helpCommand = $this->get('help'); Chris@13: $helpCommand->setCommand($command); Chris@13: Chris@13: return $helpCommand->run($input, $this->output); Chris@13: } Chris@13: Chris@13: return $command->run($input, $this->output); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Reset the current code buffer. Chris@13: * Chris@13: * This should be run after evaluating user input, catching exceptions, or Chris@13: * on demand by commands such as BufferCommand. Chris@13: */ Chris@13: public function resetCodeBuffer() Chris@13: { Chris@13: $this->codeBuffer = []; Chris@13: $this->code = false; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Inject input into the input buffer. Chris@13: * Chris@13: * This is useful for commands which want to replay history. Chris@13: * Chris@13: * @param string|array $input Chris@13: * @param bool $silent Chris@13: */ Chris@13: public function addInput($input, $silent = false) Chris@13: { Chris@13: foreach ((array) $input as $line) { Chris@13: $this->inputBuffer[] = $silent ? new SilentInput($line) : $line; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Flush the current (valid) code buffer. Chris@13: * Chris@13: * If the code buffer is valid, resets the code buffer and returns the Chris@13: * current code. Chris@13: * Chris@13: * @return string PHP code buffer contents Chris@13: */ Chris@13: public function flushCode() Chris@13: { Chris@13: if ($this->hasValidCode()) { Chris@13: $this->addCodeBufferToHistory(); Chris@13: $code = $this->code; Chris@13: $this->popCodeStack(); Chris@13: Chris@13: return $code; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Reset the code buffer and restore any code pushed during `execute` calls. Chris@13: */ Chris@13: private function popCodeStack() Chris@13: { Chris@13: $this->resetCodeBuffer(); Chris@13: Chris@13: if (empty($this->codeStack)) { Chris@13: return; Chris@13: } Chris@13: Chris@17: list($codeBuffer, $codeBufferOpen, $code) = \array_pop($this->codeStack); Chris@13: Chris@13: $this->codeBuffer = $codeBuffer; Chris@13: $this->codeBufferOpen = $codeBufferOpen; Chris@13: $this->code = $code; Chris@13: } Chris@13: Chris@13: /** Chris@13: * (Possibly) add a line to the readline history. Chris@13: * Chris@13: * Like Bash, if the line starts with a space character, it will be omitted Chris@13: * from history. Note that an entire block multi-line code input will be Chris@13: * omitted iff the first line begins with a space. Chris@13: * Chris@13: * Additionally, if a line is "silent", i.e. it was initially added with the Chris@13: * silent flag, it will also be omitted. Chris@13: * Chris@13: * @param string|SilentInput $line Chris@13: */ Chris@13: private function addHistory($line) Chris@13: { Chris@13: if ($line instanceof SilentInput) { Chris@13: return; Chris@13: } Chris@13: Chris@13: // Skip empty lines and lines starting with a space Chris@17: if (\trim($line) !== '' && \substr($line, 0, 1) !== ' ') { Chris@13: $this->readline->addHistory($line); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Filter silent input from code buffer, write the rest to readline history. Chris@13: */ Chris@13: private function addCodeBufferToHistory() Chris@13: { Chris@17: $codeBuffer = \array_filter($this->codeBuffer, function ($line) { Chris@13: return !$line instanceof SilentInput; Chris@13: }); Chris@13: Chris@17: $this->addHistory(\implode("\n", $codeBuffer)); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current evaluation scope namespace. Chris@13: * Chris@13: * @see CodeCleaner::getNamespace Chris@13: * Chris@13: * @return string Current code namespace Chris@13: */ Chris@13: public function getNamespace() Chris@13: { Chris@13: if ($namespace = $this->cleaner->getNamespace()) { Chris@17: return \implode('\\', $namespace); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Write a string to stdout. Chris@13: * Chris@13: * This is used by the shell loop for rendering output from evaluated code. Chris@13: * Chris@13: * @param string $out Chris@13: * @param int $phase Output buffering phase Chris@13: */ Chris@13: public function writeStdout($out, $phase = PHP_OUTPUT_HANDLER_END) Chris@13: { Chris@13: $isCleaning = $phase & PHP_OUTPUT_HANDLER_CLEAN; Chris@13: Chris@13: // Incremental flush Chris@13: if ($out !== '' && !$isCleaning) { Chris@13: $this->output->write($out, false, ShellOutput::OUTPUT_RAW); Chris@17: $this->outputWantsNewline = (\substr($out, -1) !== "\n"); Chris@13: $this->stdoutBuffer .= $out; Chris@13: } Chris@13: Chris@13: // Output buffering is done! Chris@13: if ($phase & PHP_OUTPUT_HANDLER_END) { Chris@13: // Write an extra newline if stdout didn't end with one Chris@13: if ($this->outputWantsNewline) { Chris@17: $this->output->writeln(\sprintf('', $this->config->useUnicode() ? '⏎' : '\\n')); Chris@13: $this->outputWantsNewline = false; Chris@13: } Chris@13: Chris@13: // Save the stdout buffer as $__out Chris@13: if ($this->stdoutBuffer !== '') { Chris@13: $this->context->setLastStdout($this->stdoutBuffer); Chris@13: $this->stdoutBuffer = ''; Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Write a return value to stdout. Chris@13: * Chris@13: * The return value is formatted or pretty-printed, and rendered in a Chris@13: * visibly distinct manner (in this case, as cyan). Chris@13: * Chris@13: * @see self::presentValue Chris@13: * Chris@13: * @param mixed $ret Chris@13: */ Chris@13: public function writeReturnValue($ret) Chris@13: { Chris@17: $this->lastExecSuccess = true; Chris@17: Chris@13: if ($ret instanceof NoReturnValue) { Chris@13: return; Chris@13: } Chris@13: Chris@13: $this->context->setReturnValue($ret); Chris@13: $ret = $this->presentValue($ret); Chris@17: $indent = \str_repeat(' ', \strlen(static::RETVAL)); Chris@13: Chris@17: $this->output->writeln(static::RETVAL . \str_replace(PHP_EOL, PHP_EOL . $indent, $ret)); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Renders a caught Exception. Chris@13: * Chris@13: * Exceptions are formatted according to severity. ErrorExceptions which were Chris@13: * warnings or Strict errors aren't rendered as harshly as real errors. Chris@13: * Chris@13: * Stores $e as the last Exception in the Shell Context. Chris@13: * Chris@13: * @param \Exception $e An exception instance Chris@13: */ Chris@13: public function writeException(\Exception $e) Chris@13: { Chris@17: $this->lastExecSuccess = false; Chris@13: $this->context->setLastException($e); Chris@13: $this->output->writeln($this->formatException($e)); Chris@13: $this->resetCodeBuffer(); Chris@13: } Chris@13: Chris@13: /** Chris@17: * Check whether the last exec was successful. Chris@17: * Chris@17: * Returns true if a return value was logged rather than an exception. Chris@17: * Chris@17: * @return bool Chris@17: */ Chris@17: public function getLastExecSuccess() Chris@17: { Chris@17: return $this->lastExecSuccess; Chris@17: } Chris@17: Chris@17: /** Chris@13: * Helper for formatting an exception for writeException(). Chris@13: * Chris@13: * @todo extract this to somewhere it makes more sense Chris@13: * Chris@13: * @param \Exception $e Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function formatException(\Exception $e) Chris@13: { Chris@13: $message = $e->getMessage(); Chris@13: if (!$e instanceof PsyException) { Chris@13: if ($message === '') { Chris@17: $message = \get_class($e); Chris@13: } else { Chris@17: $message = \sprintf('%s with message \'%s\'', \get_class($e), $message); Chris@13: } Chris@13: } Chris@13: Chris@17: $message = \preg_replace( Chris@16: "#(\\w:)?(/\\w+)*/src/Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code#", Chris@13: "eval()'d code", Chris@17: \str_replace('\\', '/', $message) Chris@13: ); Chris@13: Chris@17: $message = \str_replace(" in eval()'d code", ' in Psy Shell code', $message); Chris@13: Chris@13: $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error'; Chris@13: Chris@17: return \sprintf('<%s>%s', $severity, OutputFormatter::escape($message), $severity); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Helper for getting an output style for the given ErrorException's level. Chris@13: * Chris@13: * @param \ErrorException $e Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: protected function getSeverity(\ErrorException $e) Chris@13: { Chris@13: $severity = $e->getSeverity(); Chris@17: if ($severity & \error_reporting()) { Chris@13: switch ($severity) { Chris@13: case E_WARNING: Chris@13: case E_NOTICE: Chris@13: case E_CORE_WARNING: Chris@13: case E_COMPILE_WARNING: Chris@13: case E_USER_WARNING: Chris@13: case E_USER_NOTICE: Chris@13: case E_STRICT: Chris@13: return 'warning'; Chris@13: Chris@13: default: Chris@13: return 'error'; Chris@13: } Chris@13: } else { Chris@13: // Since this is below the user's reporting threshold, it's always going to be a warning. Chris@13: return 'warning'; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Execute code in the shell execution context. Chris@13: * Chris@13: * @param string $code Chris@13: * @param bool $throwExceptions Chris@13: * Chris@13: * @return mixed Chris@13: */ Chris@13: public function execute($code, $throwExceptions = false) Chris@13: { Chris@13: $this->setCode($code, true); Chris@13: $closure = new ExecutionClosure($this); Chris@13: Chris@13: if ($throwExceptions) { Chris@13: return $closure->execute(); Chris@13: } Chris@13: Chris@13: try { Chris@13: return $closure->execute(); Chris@13: } catch (\TypeError $_e) { Chris@13: $this->writeException(TypeErrorException::fromTypeError($_e)); Chris@13: } catch (\Error $_e) { Chris@13: $this->writeException(ErrorException::fromError($_e)); Chris@13: } catch (\Exception $_e) { Chris@13: $this->writeException($_e); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Helper for throwing an ErrorException. Chris@13: * Chris@13: * This allows us to: Chris@13: * Chris@13: * set_error_handler(array($psysh, 'handleError')); Chris@13: * Chris@13: * Unlike ErrorException::throwException, this error handler respects the Chris@13: * current error_reporting level; i.e. it logs warnings and notices, but Chris@13: * doesn't throw an exception unless it's above the current error_reporting Chris@13: * threshold. This should probably only be used in the inner execution loop Chris@13: * of the shell, as most of the time a thrown exception is much more useful. Chris@13: * Chris@13: * If the error type matches the `errorLoggingLevel` config, it will be Chris@13: * logged as well, regardless of the `error_reporting` level. Chris@13: * Chris@13: * @see \Psy\Exception\ErrorException::throwException Chris@13: * @see \Psy\Shell::writeException Chris@13: * Chris@13: * @throws \Psy\Exception\ErrorException depending on the current error_reporting level Chris@13: * Chris@13: * @param int $errno Error type Chris@13: * @param string $errstr Message Chris@13: * @param string $errfile Filename Chris@13: * @param int $errline Line number Chris@13: */ Chris@13: public function handleError($errno, $errstr, $errfile, $errline) Chris@13: { Chris@17: if ($errno & \error_reporting()) { Chris@13: ErrorException::throwException($errno, $errstr, $errfile, $errline); Chris@13: } elseif ($errno & $this->config->errorLoggingLevel()) { Chris@13: // log it and continue... Chris@13: $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline)); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Format a value for display. Chris@13: * Chris@13: * @see Presenter::present Chris@13: * Chris@13: * @param mixed $val Chris@13: * Chris@13: * @return string Formatted value Chris@13: */ Chris@13: protected function presentValue($val) Chris@13: { Chris@13: return $this->config->getPresenter()->present($val); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a command (if one exists) for the current input string. Chris@13: * Chris@13: * @param string $input Chris@13: * Chris@13: * @return null|BaseCommand Chris@13: */ Chris@13: protected function getCommand($input) Chris@13: { Chris@13: $input = new StringInput($input); Chris@13: if ($name = $input->getFirstArgument()) { Chris@13: return $this->get($name); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether a command is set for the current input string. Chris@13: * Chris@13: * @param string $input Chris@13: * Chris@13: * @return bool True if the shell has a command for the given input Chris@13: */ Chris@13: protected function hasCommand($input) Chris@13: { Chris@17: if (\preg_match('/([^\s]+?)(?:\s|$)/A', \ltrim($input), $match)) { Chris@16: return $this->has($match[1]); Chris@13: } Chris@13: Chris@13: return false; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current input prompt. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: protected function getPrompt() Chris@13: { Chris@13: if ($this->hasCode()) { Chris@13: return static::BUFF_PROMPT; Chris@13: } Chris@13: Chris@13: return $this->config->getPrompt() ?: static::PROMPT; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Read a line of user input. Chris@13: * Chris@13: * This will return a line from the input buffer (if any exist). Otherwise, Chris@13: * it will ask the user for input. Chris@13: * Chris@13: * If readline is enabled, this delegates to readline. Otherwise, it's an Chris@13: * ugly `fgets` call. Chris@13: * Chris@13: * @return string One line of user input Chris@13: */ Chris@13: protected function readline() Chris@13: { Chris@13: if (!empty($this->inputBuffer)) { Chris@17: $line = \array_shift($this->inputBuffer); Chris@13: if (!$line instanceof SilentInput) { Chris@17: $this->output->writeln(\sprintf('', static::REPLAY, OutputFormatter::escape($line))); Chris@13: } Chris@13: Chris@13: return $line; Chris@13: } Chris@13: Chris@13: if ($bracketedPaste = $this->config->useBracketedPaste()) { Chris@17: \printf("\e[?2004h"); // Enable bracketed paste Chris@13: } Chris@13: Chris@13: $line = $this->readline->readline($this->getPrompt()); Chris@13: Chris@13: if ($bracketedPaste) { Chris@17: \printf("\e[?2004l"); // ... and disable it again Chris@13: } Chris@13: Chris@13: return $line; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the shell output header. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: protected function getHeader() Chris@13: { Chris@17: return \sprintf('', $this->getVersion()); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current version of Psy Shell. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getVersion() Chris@13: { Chris@13: $separator = $this->config->useUnicode() ? '—' : '-'; Chris@13: Chris@17: return \sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, PHP_VERSION, $separator, PHP_SAPI); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a PHP manual database instance. Chris@13: * Chris@13: * @return \PDO|null Chris@13: */ Chris@13: public function getManualDb() Chris@13: { Chris@13: return $this->config->getManualDb(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * @deprecated Tab completion is provided by the AutoCompleter service Chris@13: */ Chris@13: protected function autocomplete($text) Chris@13: { Chris@17: @\trigger_error('Tab completion is provided by the AutoCompleter service', E_USER_DEPRECATED); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Initialize tab completion matchers. Chris@13: * Chris@13: * If tab completion is enabled this adds tab completion matchers to the Chris@13: * auto completer and sets context if needed. Chris@13: */ Chris@13: protected function initializeTabCompletion() Chris@13: { Chris@13: if (!$this->config->useTabCompletion()) { Chris@13: return; Chris@13: } Chris@13: Chris@13: $this->autoCompleter = $this->config->getAutoCompleter(); Chris@13: Chris@13: // auto completer needs shell to be linked to configuration because of Chris@13: // the context aware matchers Chris@13: $this->addMatchersToAutoCompleter($this->getDefaultMatchers()); Chris@13: $this->addMatchersToAutoCompleter($this->matchers); Chris@13: Chris@13: $this->autoCompleter->activate(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Add matchers to the auto completer, setting context if needed. Chris@13: * Chris@13: * @param array $matchers Chris@13: */ Chris@13: private function addMatchersToAutoCompleter(array $matchers) Chris@13: { Chris@13: foreach ($matchers as $matcher) { Chris@13: if ($matcher instanceof ContextAware) { Chris@13: $matcher->setContext($this->context); Chris@13: } Chris@13: $this->autoCompleter->addMatcher($matcher); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * @todo Implement self-update Chris@13: * @todo Implement prompt to start update Chris@13: * Chris@13: * @return void|string Chris@13: */ Chris@13: protected function writeVersionInfo() Chris@13: { Chris@13: if (PHP_SAPI !== 'cli') { Chris@13: return; Chris@13: } Chris@13: Chris@13: try { Chris@13: $client = $this->config->getChecker(); Chris@13: if (!$client->isLatest()) { Chris@17: $this->output->writeln(\sprintf('New version is available (current: %s, latest: %s)', self::VERSION, $client->getLatest())); Chris@13: } Chris@13: } catch (\InvalidArgumentException $e) { Chris@13: $this->output->writeln($e->getMessage()); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Write a startup message if set. Chris@13: */ Chris@13: protected function writeStartupMessage() Chris@13: { Chris@13: $message = $this->config->getStartupMessage(); Chris@13: if ($message !== null && $message !== '') { Chris@13: $this->output->writeln($message); Chris@13: } Chris@13: } Chris@13: }