4 * This file is part of Psy Shell.
6 * (c) 2012-2017 Justin Hileman
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
14 use Psy\CodeCleaner\NoReturnValue;
15 use Psy\Exception\BreakException;
16 use Psy\Exception\ErrorException;
17 use Psy\Exception\Exception as PsyException;
18 use Psy\Exception\ThrowUpException;
19 use Psy\Input\ShellInput;
20 use Psy\Input\SilentInput;
21 use Psy\Output\ShellOutput;
22 use Psy\TabCompletion\Matcher;
23 use Psy\VarDumper\PresenterAware;
24 use Symfony\Component\Console\Application;
25 use Symfony\Component\Console\Command\Command as BaseCommand;
26 use Symfony\Component\Console\Formatter\OutputFormatter;
27 use Symfony\Component\Console\Input\ArgvInput;
28 use Symfony\Component\Console\Input\InputArgument;
29 use Symfony\Component\Console\Input\InputDefinition;
30 use Symfony\Component\Console\Input\InputInterface;
31 use Symfony\Component\Console\Input\InputOption;
32 use Symfony\Component\Console\Input\StringInput;
33 use Symfony\Component\Console\Output\OutputInterface;
36 * The Psy Shell application.
43 * @author Justin Hileman <justin@justinhileman.info>
45 class Shell extends Application
47 const VERSION = 'v0.8.17';
49 const PROMPT = '>>> ';
50 const BUFF_PROMPT = '... ';
51 const REPLAY = '--> ';
61 private $codeBufferOpen;
65 private $outputWantsNewline = false;
67 private $tabCompletionMatchers = array();
68 private $stdoutBuffer;
72 * Create a new Psy Shell.
74 * @param Configuration $config (default: null)
76 public function __construct(Configuration $config = null)
78 $this->config = $config ?: new Configuration();
79 $this->cleaner = $this->config->getCodeCleaner();
80 $this->loop = $this->config->getLoop();
81 $this->context = new Context();
82 $this->includes = array();
83 $this->readline = $this->config->getReadline();
84 $this->inputBuffer = array();
85 $this->stdoutBuffer = '';
87 parent::__construct('Psy Shell', self::VERSION);
89 $this->config->setShell($this);
91 // Register the current shell session's config with \Psy\info
92 \Psy\info($this->config);
96 * Check whether the first thing in a backtrace is an include call.
98 * This is used by the psysh bin to decide whether to start a shell on boot,
99 * or to simply autoload the library.
101 public static function isIncluded(array $trace)
103 return isset($trace[0]['function']) &&
104 in_array($trace[0]['function'], array('require', 'include', 'require_once', 'include_once'));
108 * Invoke a Psy Shell from the current context.
111 * @deprecated will be removed in 1.0. Use \Psy\debug instead
113 * @param array $vars Scope variables from the calling context (default: array())
114 * @param object $boundObject Bound object ($this) value for the shell
116 * @return array Scope variables from the debugger session
118 public static function debug(array $vars = array(), $boundObject = null)
120 return \Psy\debug($vars, $boundObject);
124 * Adds a command object.
128 * @param BaseCommand $command A Symfony Console Command object
130 * @return BaseCommand The registered command
132 public function add(BaseCommand $command)
134 if ($ret = parent::add($command)) {
135 if ($ret instanceof ContextAware) {
136 $ret->setContext($this->context);
139 if ($ret instanceof PresenterAware) {
140 $ret->setPresenter($this->config->getPresenter());
148 * Gets the default input definition.
150 * @return InputDefinition An InputDefinition instance
152 protected function getDefaultInputDefinition()
154 return new InputDefinition(array(
155 new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
156 new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'),
161 * Gets the default commands that should always be available.
163 * @return array An array of default Command instances
165 protected function getDefaultCommands()
167 $sudo = new Command\SudoCommand();
168 $sudo->setReadline($this->readline);
170 $hist = new Command\HistoryCommand();
171 $hist->setReadline($this->readline);
174 new Command\HelpCommand(),
175 new Command\ListCommand(),
176 new Command\DumpCommand(),
177 new Command\DocCommand(),
178 new Command\ShowCommand($this->config->colorMode()),
179 new Command\WtfCommand($this->config->colorMode()),
180 new Command\WhereamiCommand($this->config->colorMode()),
181 new Command\ThrowUpCommand(),
182 new Command\TraceCommand(),
183 new Command\BufferCommand(),
184 new Command\ClearCommand(),
185 new Command\EditCommand($this->config->getRuntimeDir()),
186 // new Command\PsyVersionCommand(),
189 new Command\ExitCommand(),
196 protected function getTabCompletionMatchers()
198 if (empty($this->tabCompletionMatchers)) {
199 $this->tabCompletionMatchers = array(
200 new Matcher\CommandsMatcher($this->all()),
201 new Matcher\KeywordsMatcher(),
202 new Matcher\VariablesMatcher(),
203 new Matcher\ConstantsMatcher(),
204 new Matcher\FunctionsMatcher(),
205 new Matcher\ClassNamesMatcher(),
206 new Matcher\ClassMethodsMatcher(),
207 new Matcher\ClassAttributesMatcher(),
208 new Matcher\ObjectMethodsMatcher(),
209 new Matcher\ObjectAttributesMatcher(),
210 new Matcher\ClassMethodDefaultParametersMatcher(),
211 new Matcher\ObjectMethodDefaultParametersMatcher(),
212 new Matcher\FunctionDefaultParametersMatcher(),
216 return $this->tabCompletionMatchers;
220 * @param array $matchers
222 public function addTabCompletionMatchers(array $matchers)
224 $this->tabCompletionMatchers = array_merge($matchers, $this->getTabCompletionMatchers());
228 * Set the Shell output.
230 * @param OutputInterface $output
232 public function setOutput(OutputInterface $output)
234 $this->output = $output;
238 * Runs the current application.
240 * @param InputInterface $input An Input instance
241 * @param OutputInterface $output An Output instance
243 * @return int 0 if everything went fine, or an error code
245 public function run(InputInterface $input = null, OutputInterface $output = null)
247 $this->initializeTabCompletion();
249 if ($input === null && !isset($_SERVER['argv'])) {
250 $input = new ArgvInput(array());
253 if ($output === null) {
254 $output = $this->config->getOutput();
258 return parent::run($input, $output);
259 } catch (\Exception $e) {
260 $this->writeException($e);
265 * Runs the current application.
267 * @throws Exception if thrown via the `throw-up` command
269 * @param InputInterface $input An Input instance
270 * @param OutputInterface $output An Output instance
272 * @return int 0 if everything went fine, or an error code
274 public function doRun(InputInterface $input, OutputInterface $output)
276 $this->setOutput($output);
278 $this->resetCodeBuffer();
280 $this->setAutoExit(false);
281 $this->setCatchExceptions(false);
283 $this->readline->readHistory();
285 // if ($this->config->useReadline()) {
286 // readline_completion_function(array($this, 'autocomplete'));
289 $this->output->writeln($this->getHeader());
290 $this->writeVersionInfo();
291 $this->writeStartupMessage();
294 $this->loop->run($this);
295 } catch (ThrowUpException $e) {
296 throw $e->getPrevious();
303 * This will continue fetching user input until the code buffer contains
306 * @throws BreakException if user hits Ctrl+D
308 public function getInput()
310 $this->codeBufferOpen = false;
313 // reset output verbosity (in case it was altered by a subcommand)
314 $this->output->setVerbosity(ShellOutput::VERBOSITY_VERBOSE);
316 $input = $this->readline();
319 * Handle Ctrl+D. It behaves differently in different cases:
321 * 1) In an expression, like a function or "if" block, clear the input buffer
322 * 2) At top-level session, behave like the exit command
324 if ($input === false) {
325 $this->output->writeln('');
327 if ($this->hasCode()) {
328 $this->resetCodeBuffer();
330 throw new BreakException('Ctrl+D');
334 // handle empty input
335 if (trim($input) === '') {
339 if ($this->hasCommand($input)) {
340 $this->readline->addHistory($input);
341 $this->runCommand($input);
346 $this->addCode($input);
347 } while (!$this->hasValidCode());
351 * Pass the beforeLoop callback through to the Loop instance.
353 * @see Loop::beforeLoop
355 public function beforeLoop()
357 $this->loop->beforeLoop();
361 * Pass the afterLoop callback through to the Loop instance.
363 * @see Loop::afterLoop
365 public function afterLoop()
367 $this->loop->afterLoop();
371 * Set the variables currently in scope.
375 public function setScopeVariables(array $vars)
377 $this->context->setAll($vars);
381 * Return the set of variables currently in scope.
383 * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
384 * passing the scope variables to `extract`
385 * in PHP 7.1+, you _must_ exclude 'this'
387 * @return array Associative array of scope variables
389 public function getScopeVariables($includeBoundObject = true)
391 $vars = $this->context->getAll();
393 if (!$includeBoundObject) {
394 unset($vars['this']);
401 * Return the set of magic variables currently in scope.
403 * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
404 * passing the scope variables to `extract`
405 * in PHP 7.1+, you _must_ exclude 'this'
407 * @return array Associative array of magic scope variables
409 public function getSpecialScopeVariables($includeBoundObject = true)
411 $vars = $this->context->getSpecialVariables();
413 if (!$includeBoundObject) {
414 unset($vars['this']);
421 * Get the set of unused command-scope variable names.
423 * @return array Array of unused variable names
425 public function getUnusedCommandScopeVariableNames()
427 return $this->context->getUnusedCommandScopeVariableNames();
431 * Get the set of variable names currently in scope.
433 * @return array Array of variable names
435 public function getScopeVariableNames()
437 return array_keys($this->context->getAll());
441 * Get a scope variable value by name.
443 * @param string $name
447 public function getScopeVariable($name)
449 return $this->context->get($name);
453 * Set the bound object ($this variable) for the interactive shell.
455 * @param object|null $boundObject
457 public function setBoundObject($boundObject)
459 $this->context->setBoundObject($boundObject);
463 * Get the bound object ($this variable) for the interactive shell.
465 * @return object|null
467 public function getBoundObject()
469 return $this->context->getBoundObject();
473 * Add includes, to be parsed and executed before running the interactive shell.
475 * @param array $includes
477 public function setIncludes(array $includes = array())
479 $this->includes = $includes;
483 * Get PHP files to be parsed and executed before running the interactive shell.
487 public function getIncludes()
489 return array_merge($this->config->getDefaultIncludes(), $this->includes);
493 * Check whether this shell's code buffer contains code.
495 * @return bool True if the code buffer contains code
497 public function hasCode()
499 return !empty($this->codeBuffer);
503 * Check whether the code in this shell's code buffer is valid.
505 * If the code is valid, the code buffer should be flushed and evaluated.
507 * @return bool True if the code buffer content is valid
509 protected function hasValidCode()
511 return !$this->codeBufferOpen && $this->code !== false;
515 * Add code to the code buffer.
517 * @param string $code
519 public function addCode($code)
522 // Code lines ending in \ keep the buffer open
523 if (substr(rtrim($code), -1) === '\\') {
524 $this->codeBufferOpen = true;
525 $code = substr(rtrim($code), 0, -1);
527 $this->codeBufferOpen = false;
530 $this->codeBuffer[] = $code;
531 $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons());
532 } catch (\Exception $e) {
533 // Add failed code blocks to the readline history.
534 $this->addCodeBufferToHistory();
541 * Get the current code buffer.
543 * This is useful for commands which manipulate the buffer.
547 public function getCodeBuffer()
549 return $this->codeBuffer;
553 * Run a Psy Shell command given the user input.
555 * @throws InvalidArgumentException if the input is not a valid command
557 * @param string $input User input string
559 * @return mixed Who knows?
561 protected function runCommand($input)
563 $command = $this->getCommand($input);
565 if (empty($command)) {
566 throw new \InvalidArgumentException('Command not found: ' . $input);
569 $input = new ShellInput(str_replace('\\', '\\\\', rtrim($input, " \t\n\r\0\x0B;")));
571 if ($input->hasParameterOption(array('--help', '-h'))) {
572 $helpCommand = $this->get('help');
573 $helpCommand->setCommand($command);
575 return $helpCommand->run($input, $this->output);
578 return $command->run($input, $this->output);
582 * Reset the current code buffer.
584 * This should be run after evaluating user input, catching exceptions, or
585 * on demand by commands such as BufferCommand.
587 public function resetCodeBuffer()
589 $this->codeBuffer = array();
594 * Inject input into the input buffer.
596 * This is useful for commands which want to replay history.
598 * @param string|array $input
599 * @param bool $silent
601 public function addInput($input, $silent = false)
603 foreach ((array) $input as $line) {
604 $this->inputBuffer[] = $silent ? new SilentInput($line) : $line;
609 * Flush the current (valid) code buffer.
611 * If the code buffer is valid, resets the code buffer and returns the
614 * @return string PHP code buffer contents
616 public function flushCode()
618 if ($this->hasValidCode()) {
619 $this->addCodeBufferToHistory();
621 $this->resetCodeBuffer();
628 * Filter silent input from code buffer, write the rest to readline history.
630 private function addCodeBufferToHistory()
632 $codeBuffer = array_filter($this->codeBuffer, function ($line) {
633 return !$line instanceof SilentInput;
636 $code = implode("\n", $codeBuffer);
638 if (trim($code) !== '') {
639 $this->readline->addHistory($code);
644 * Get the current evaluation scope namespace.
646 * @see CodeCleaner::getNamespace
648 * @return string Current code namespace
650 public function getNamespace()
652 if ($namespace = $this->cleaner->getNamespace()) {
653 return implode('\\', $namespace);
658 * Write a string to stdout.
660 * This is used by the shell loop for rendering output from evaluated code.
663 * @param int $phase Output buffering phase
665 public function writeStdout($out, $phase = PHP_OUTPUT_HANDLER_END)
668 if (version_compare(PHP_VERSION, '5.4', '>=')) {
669 $isCleaning = $phase & PHP_OUTPUT_HANDLER_CLEAN;
673 if ($out !== '' && !$isCleaning) {
674 $this->output->write($out, false, ShellOutput::OUTPUT_RAW);
675 $this->outputWantsNewline = (substr($out, -1) !== "\n");
676 $this->stdoutBuffer .= $out;
679 // Output buffering is done!
680 if ($phase & PHP_OUTPUT_HANDLER_END) {
681 // Write an extra newline if stdout didn't end with one
682 if ($this->outputWantsNewline) {
683 $this->output->writeln(sprintf('<aside>%s</aside>', $this->config->useUnicode() ? '⏎' : '\\n'));
684 $this->outputWantsNewline = false;
687 // Save the stdout buffer as $__out
688 if ($this->stdoutBuffer !== '') {
689 $this->context->setLastStdout($this->stdoutBuffer);
690 $this->stdoutBuffer = '';
696 * Write a return value to stdout.
698 * The return value is formatted or pretty-printed, and rendered in a
699 * visibly distinct manner (in this case, as cyan).
701 * @see self::presentValue
705 public function writeReturnValue($ret)
707 if ($ret instanceof NoReturnValue) {
711 $this->context->setReturnValue($ret);
712 $ret = $this->presentValue($ret);
713 $indent = str_repeat(' ', strlen(static::RETVAL));
715 $this->output->writeln(static::RETVAL . str_replace(PHP_EOL, PHP_EOL . $indent, $ret));
719 * Renders a caught Exception.
721 * Exceptions are formatted according to severity. ErrorExceptions which were
722 * warnings or Strict errors aren't rendered as harshly as real errors.
724 * Stores $e as the last Exception in the Shell Context.
726 * @param \Exception $e An exception instance
727 * @param OutputInterface $output An OutputInterface instance
729 public function writeException(\Exception $e)
731 $this->context->setLastException($e);
732 $this->output->writeln($this->formatException($e));
733 $this->resetCodeBuffer();
737 * Helper for formatting an exception for writeException().
739 * @todo extract this to somewhere it makes more sense
741 * @param \Exception $e
745 public function formatException(\Exception $e)
747 $message = $e->getMessage();
748 if (!$e instanceof PsyException) {
749 if ($message === '') {
750 $message = get_class($e);
752 $message = sprintf('%s with message \'%s\'', get_class($e), $message);
756 $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error';
758 return sprintf('<%s>%s</%s>', $severity, OutputFormatter::escape($message), $severity);
762 * Helper for getting an output style for the given ErrorException's level.
764 * @param \ErrorException $e
768 protected function getSeverity(\ErrorException $e)
770 $severity = $e->getSeverity();
771 if ($severity & error_reporting()) {
776 case E_COMPILE_WARNING:
786 // Since this is below the user's reporting threshold, it's always going to be a warning.
792 * Helper for throwing an ErrorException.
796 * set_error_handler(array($psysh, 'handleError'));
798 * Unlike ErrorException::throwException, this error handler respects the
799 * current error_reporting level; i.e. it logs warnings and notices, but
800 * doesn't throw an exception unless it's above the current error_reporting
801 * threshold. This should probably only be used in the inner execution loop
802 * of the shell, as most of the time a thrown exception is much more useful.
804 * If the error type matches the `errorLoggingLevel` config, it will be
805 * logged as well, regardless of the `error_reporting` level.
807 * @see \Psy\Exception\ErrorException::throwException
808 * @see \Psy\Shell::writeException
810 * @throws \Psy\Exception\ErrorException depending on the current error_reporting level
812 * @param int $errno Error type
813 * @param string $errstr Message
814 * @param string $errfile Filename
815 * @param int $errline Line number
817 public function handleError($errno, $errstr, $errfile, $errline)
819 if ($errno & error_reporting()) {
820 ErrorException::throwException($errno, $errstr, $errfile, $errline);
821 } elseif ($errno & $this->config->errorLoggingLevel()) {
822 // log it and continue...
823 $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline));
828 * Format a value for display.
830 * @see Presenter::present
834 * @return string Formatted value
836 protected function presentValue($val)
838 return $this->config->getPresenter()->present($val);
842 * Get a command (if one exists) for the current input string.
844 * @param string $input
846 * @return null|BaseCommand
848 protected function getCommand($input)
850 $input = new StringInput($input);
851 if ($name = $input->getFirstArgument()) {
852 return $this->get($name);
857 * Check whether a command is set for the current input string.
859 * @param string $input
861 * @return bool True if the shell has a command for the given input
863 protected function hasCommand($input)
865 $input = new StringInput($input);
866 if ($name = $input->getFirstArgument()) {
867 return $this->has($name);
874 * Get the current input prompt.
878 protected function getPrompt()
880 if ($this->hasCode()) {
881 return static::BUFF_PROMPT;
884 return $this->config->getPrompt() ?: static::PROMPT;
888 * Read a line of user input.
890 * This will return a line from the input buffer (if any exist). Otherwise,
891 * it will ask the user for input.
893 * If readline is enabled, this delegates to readline. Otherwise, it's an
896 * @return string One line of user input
898 protected function readline()
900 if (!empty($this->inputBuffer)) {
901 $line = array_shift($this->inputBuffer);
902 if (!$line instanceof SilentInput) {
903 $this->output->writeln(sprintf('<aside>%s %s</aside>', static::REPLAY, OutputFormatter::escape($line)));
909 if ($bracketedPaste = $this->config->useBracketedPaste()) {
910 printf("\e[?2004h"); // Enable bracketed paste
913 $line = $this->readline->readline($this->getPrompt());
915 if ($bracketedPaste) {
916 printf("\e[?2004l"); // ... and disable it again
923 * Get the shell output header.
927 protected function getHeader()
929 return sprintf('<aside>%s by Justin Hileman</aside>', $this->getVersion());
933 * Get the current version of Psy Shell.
937 public function getVersion()
939 $separator = $this->config->useUnicode() ? '—' : '-';
941 return sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, phpversion(), $separator, php_sapi_name());
945 * Get a PHP manual database instance.
949 public function getManualDb()
951 return $this->config->getManualDb();
955 * Autocomplete variable names.
957 * This is used by `readline` for tab completion.
959 * @param string $text
961 * @return mixed Array possible completions for the given input, if any
963 protected function autocomplete($text)
965 $info = readline_info();
966 // $line = substr($info['line_buffer'], 0, $info['end']);
968 // Check whether there's a command for this
969 // $words = explode(' ', $line);
970 // $firstWord = reset($words);
972 // check whether this is a variable...
973 $firstChar = substr($info['line_buffer'], max(0, $info['end'] - strlen($text) - 1), 1);
974 if ($firstChar === '$') {
975 return $this->getScopeVariableNames();
980 * Initialize tab completion matchers.
982 * If tab completion is enabled this adds tab completion matchers to the
983 * auto completer and sets context if needed.
985 protected function initializeTabCompletion()
987 // auto completer needs shell to be linked to configuration because of the context aware matchers
988 if ($this->config->getTabCompletion()) {
989 $this->completion = $this->config->getAutoCompleter();
990 $this->addTabCompletionMatchers($this->config->getTabCompletionMatchers());
991 foreach ($this->getTabCompletionMatchers() as $matcher) {
992 if ($matcher instanceof ContextAware) {
993 $matcher->setContext($this->context);
995 $this->completion->addMatcher($matcher);
997 $this->completion->activate();
1002 * @todo Implement self-update
1003 * @todo Implement prompt to start update
1005 * @return void|string
1007 protected function writeVersionInfo()
1009 if (PHP_SAPI !== 'cli') {
1014 $client = $this->config->getChecker();
1015 if (!$client->isLatest()) {
1016 $this->output->writeln(sprintf('New version is available (current: %s, latest: %s)', self::VERSION, $client->getLatest()));
1018 } catch (\InvalidArgumentException $e) {
1019 $this->output->writeln($e->getMessage());
1024 * Write a startup message if set.
1026 protected function writeStartupMessage()
1028 $message = $this->config->getStartupMessage();
1029 if ($message !== null && $message !== '') {
1030 $this->output->writeln($message);