annotate vendor/psy/psysh/src/Psy/Shell.php @ 12:7a779792577d

Update Drupal core to v8.4.5 (via Composer)
author Chris Cannam
date Fri, 23 Feb 2018 15:52:07 +0000
parents 4c8ae668cc8c
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /*
Chris@0 4 * This file is part of Psy Shell.
Chris@0 5 *
Chris@0 6 * (c) 2012-2017 Justin Hileman
Chris@0 7 *
Chris@0 8 * For the full copyright and license information, please view the LICENSE
Chris@0 9 * file that was distributed with this source code.
Chris@0 10 */
Chris@0 11
Chris@0 12 namespace Psy;
Chris@0 13
Chris@0 14 use Psy\CodeCleaner\NoReturnValue;
Chris@0 15 use Psy\Exception\BreakException;
Chris@0 16 use Psy\Exception\ErrorException;
Chris@0 17 use Psy\Exception\Exception as PsyException;
Chris@0 18 use Psy\Exception\ThrowUpException;
Chris@0 19 use Psy\Input\ShellInput;
Chris@0 20 use Psy\Input\SilentInput;
Chris@0 21 use Psy\Output\ShellOutput;
Chris@0 22 use Psy\TabCompletion\Matcher;
Chris@0 23 use Psy\VarDumper\PresenterAware;
Chris@0 24 use Symfony\Component\Console\Application;
Chris@0 25 use Symfony\Component\Console\Command\Command as BaseCommand;
Chris@0 26 use Symfony\Component\Console\Formatter\OutputFormatter;
Chris@0 27 use Symfony\Component\Console\Input\ArgvInput;
Chris@0 28 use Symfony\Component\Console\Input\InputArgument;
Chris@0 29 use Symfony\Component\Console\Input\InputDefinition;
Chris@0 30 use Symfony\Component\Console\Input\InputInterface;
Chris@0 31 use Symfony\Component\Console\Input\InputOption;
Chris@0 32 use Symfony\Component\Console\Input\StringInput;
Chris@0 33 use Symfony\Component\Console\Output\OutputInterface;
Chris@0 34
Chris@0 35 /**
Chris@0 36 * The Psy Shell application.
Chris@0 37 *
Chris@0 38 * Usage:
Chris@0 39 *
Chris@0 40 * $shell = new Shell;
Chris@0 41 * $shell->run();
Chris@0 42 *
Chris@0 43 * @author Justin Hileman <justin@justinhileman.info>
Chris@0 44 */
Chris@0 45 class Shell extends Application
Chris@0 46 {
Chris@12 47 const VERSION = 'v0.8.17';
Chris@0 48
Chris@0 49 const PROMPT = '>>> ';
Chris@0 50 const BUFF_PROMPT = '... ';
Chris@0 51 const REPLAY = '--> ';
Chris@0 52 const RETVAL = '=> ';
Chris@0 53
Chris@0 54 private $config;
Chris@0 55 private $cleaner;
Chris@0 56 private $output;
Chris@0 57 private $readline;
Chris@0 58 private $inputBuffer;
Chris@0 59 private $code;
Chris@0 60 private $codeBuffer;
Chris@0 61 private $codeBufferOpen;
Chris@0 62 private $context;
Chris@0 63 private $includes;
Chris@0 64 private $loop;
Chris@0 65 private $outputWantsNewline = false;
Chris@0 66 private $completion;
Chris@0 67 private $tabCompletionMatchers = array();
Chris@0 68 private $stdoutBuffer;
Chris@0 69 private $prompt;
Chris@0 70
Chris@0 71 /**
Chris@0 72 * Create a new Psy Shell.
Chris@0 73 *
Chris@0 74 * @param Configuration $config (default: null)
Chris@0 75 */
Chris@0 76 public function __construct(Configuration $config = null)
Chris@0 77 {
Chris@12 78 $this->config = $config ?: new Configuration();
Chris@12 79 $this->cleaner = $this->config->getCodeCleaner();
Chris@12 80 $this->loop = $this->config->getLoop();
Chris@12 81 $this->context = new Context();
Chris@12 82 $this->includes = array();
Chris@12 83 $this->readline = $this->config->getReadline();
Chris@12 84 $this->inputBuffer = array();
Chris@0 85 $this->stdoutBuffer = '';
Chris@0 86
Chris@0 87 parent::__construct('Psy Shell', self::VERSION);
Chris@0 88
Chris@0 89 $this->config->setShell($this);
Chris@0 90
Chris@0 91 // Register the current shell session's config with \Psy\info
Chris@0 92 \Psy\info($this->config);
Chris@0 93 }
Chris@0 94
Chris@0 95 /**
Chris@0 96 * Check whether the first thing in a backtrace is an include call.
Chris@0 97 *
Chris@0 98 * This is used by the psysh bin to decide whether to start a shell on boot,
Chris@0 99 * or to simply autoload the library.
Chris@0 100 */
Chris@0 101 public static function isIncluded(array $trace)
Chris@0 102 {
Chris@0 103 return isset($trace[0]['function']) &&
Chris@0 104 in_array($trace[0]['function'], array('require', 'include', 'require_once', 'include_once'));
Chris@0 105 }
Chris@0 106
Chris@0 107 /**
Chris@0 108 * Invoke a Psy Shell from the current context.
Chris@0 109 *
Chris@0 110 * @see Psy\debug
Chris@0 111 * @deprecated will be removed in 1.0. Use \Psy\debug instead
Chris@0 112 *
Chris@0 113 * @param array $vars Scope variables from the calling context (default: array())
Chris@0 114 * @param object $boundObject Bound object ($this) value for the shell
Chris@0 115 *
Chris@0 116 * @return array Scope variables from the debugger session
Chris@0 117 */
Chris@0 118 public static function debug(array $vars = array(), $boundObject = null)
Chris@0 119 {
Chris@0 120 return \Psy\debug($vars, $boundObject);
Chris@0 121 }
Chris@0 122
Chris@0 123 /**
Chris@0 124 * Adds a command object.
Chris@0 125 *
Chris@0 126 * {@inheritdoc}
Chris@0 127 *
Chris@0 128 * @param BaseCommand $command A Symfony Console Command object
Chris@0 129 *
Chris@0 130 * @return BaseCommand The registered command
Chris@0 131 */
Chris@0 132 public function add(BaseCommand $command)
Chris@0 133 {
Chris@0 134 if ($ret = parent::add($command)) {
Chris@0 135 if ($ret instanceof ContextAware) {
Chris@0 136 $ret->setContext($this->context);
Chris@0 137 }
Chris@0 138
Chris@0 139 if ($ret instanceof PresenterAware) {
Chris@0 140 $ret->setPresenter($this->config->getPresenter());
Chris@0 141 }
Chris@0 142 }
Chris@0 143
Chris@0 144 return $ret;
Chris@0 145 }
Chris@0 146
Chris@0 147 /**
Chris@0 148 * Gets the default input definition.
Chris@0 149 *
Chris@0 150 * @return InputDefinition An InputDefinition instance
Chris@0 151 */
Chris@0 152 protected function getDefaultInputDefinition()
Chris@0 153 {
Chris@0 154 return new InputDefinition(array(
Chris@0 155 new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
Chris@0 156 new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'),
Chris@0 157 ));
Chris@0 158 }
Chris@0 159
Chris@0 160 /**
Chris@0 161 * Gets the default commands that should always be available.
Chris@0 162 *
Chris@0 163 * @return array An array of default Command instances
Chris@0 164 */
Chris@0 165 protected function getDefaultCommands()
Chris@0 166 {
Chris@0 167 $sudo = new Command\SudoCommand();
Chris@0 168 $sudo->setReadline($this->readline);
Chris@0 169
Chris@0 170 $hist = new Command\HistoryCommand();
Chris@0 171 $hist->setReadline($this->readline);
Chris@0 172
Chris@0 173 return array(
Chris@0 174 new Command\HelpCommand(),
Chris@0 175 new Command\ListCommand(),
Chris@0 176 new Command\DumpCommand(),
Chris@0 177 new Command\DocCommand(),
Chris@0 178 new Command\ShowCommand($this->config->colorMode()),
Chris@0 179 new Command\WtfCommand($this->config->colorMode()),
Chris@0 180 new Command\WhereamiCommand($this->config->colorMode()),
Chris@0 181 new Command\ThrowUpCommand(),
Chris@0 182 new Command\TraceCommand(),
Chris@0 183 new Command\BufferCommand(),
Chris@0 184 new Command\ClearCommand(),
Chris@0 185 new Command\EditCommand($this->config->getRuntimeDir()),
Chris@0 186 // new Command\PsyVersionCommand(),
Chris@0 187 $sudo,
Chris@0 188 $hist,
Chris@0 189 new Command\ExitCommand(),
Chris@0 190 );
Chris@0 191 }
Chris@0 192
Chris@0 193 /**
Chris@0 194 * @return array
Chris@0 195 */
Chris@0 196 protected function getTabCompletionMatchers()
Chris@0 197 {
Chris@0 198 if (empty($this->tabCompletionMatchers)) {
Chris@0 199 $this->tabCompletionMatchers = array(
Chris@0 200 new Matcher\CommandsMatcher($this->all()),
Chris@0 201 new Matcher\KeywordsMatcher(),
Chris@0 202 new Matcher\VariablesMatcher(),
Chris@0 203 new Matcher\ConstantsMatcher(),
Chris@0 204 new Matcher\FunctionsMatcher(),
Chris@0 205 new Matcher\ClassNamesMatcher(),
Chris@0 206 new Matcher\ClassMethodsMatcher(),
Chris@0 207 new Matcher\ClassAttributesMatcher(),
Chris@0 208 new Matcher\ObjectMethodsMatcher(),
Chris@0 209 new Matcher\ObjectAttributesMatcher(),
Chris@0 210 new Matcher\ClassMethodDefaultParametersMatcher(),
Chris@0 211 new Matcher\ObjectMethodDefaultParametersMatcher(),
Chris@0 212 new Matcher\FunctionDefaultParametersMatcher(),
Chris@0 213 );
Chris@0 214 }
Chris@0 215
Chris@0 216 return $this->tabCompletionMatchers;
Chris@0 217 }
Chris@0 218
Chris@0 219 /**
Chris@0 220 * @param array $matchers
Chris@0 221 */
Chris@0 222 public function addTabCompletionMatchers(array $matchers)
Chris@0 223 {
Chris@0 224 $this->tabCompletionMatchers = array_merge($matchers, $this->getTabCompletionMatchers());
Chris@0 225 }
Chris@0 226
Chris@0 227 /**
Chris@0 228 * Set the Shell output.
Chris@0 229 *
Chris@0 230 * @param OutputInterface $output
Chris@0 231 */
Chris@0 232 public function setOutput(OutputInterface $output)
Chris@0 233 {
Chris@0 234 $this->output = $output;
Chris@0 235 }
Chris@0 236
Chris@0 237 /**
Chris@0 238 * Runs the current application.
Chris@0 239 *
Chris@0 240 * @param InputInterface $input An Input instance
Chris@0 241 * @param OutputInterface $output An Output instance
Chris@0 242 *
Chris@0 243 * @return int 0 if everything went fine, or an error code
Chris@0 244 */
Chris@0 245 public function run(InputInterface $input = null, OutputInterface $output = null)
Chris@0 246 {
Chris@0 247 $this->initializeTabCompletion();
Chris@0 248
Chris@0 249 if ($input === null && !isset($_SERVER['argv'])) {
Chris@0 250 $input = new ArgvInput(array());
Chris@0 251 }
Chris@0 252
Chris@0 253 if ($output === null) {
Chris@0 254 $output = $this->config->getOutput();
Chris@0 255 }
Chris@0 256
Chris@0 257 try {
Chris@0 258 return parent::run($input, $output);
Chris@0 259 } catch (\Exception $e) {
Chris@0 260 $this->writeException($e);
Chris@0 261 }
Chris@0 262 }
Chris@0 263
Chris@0 264 /**
Chris@0 265 * Runs the current application.
Chris@0 266 *
Chris@0 267 * @throws Exception if thrown via the `throw-up` command
Chris@0 268 *
Chris@0 269 * @param InputInterface $input An Input instance
Chris@0 270 * @param OutputInterface $output An Output instance
Chris@0 271 *
Chris@0 272 * @return int 0 if everything went fine, or an error code
Chris@0 273 */
Chris@0 274 public function doRun(InputInterface $input, OutputInterface $output)
Chris@0 275 {
Chris@0 276 $this->setOutput($output);
Chris@0 277
Chris@0 278 $this->resetCodeBuffer();
Chris@0 279
Chris@0 280 $this->setAutoExit(false);
Chris@0 281 $this->setCatchExceptions(false);
Chris@0 282
Chris@0 283 $this->readline->readHistory();
Chris@0 284
Chris@0 285 // if ($this->config->useReadline()) {
Chris@0 286 // readline_completion_function(array($this, 'autocomplete'));
Chris@0 287 // }
Chris@0 288
Chris@0 289 $this->output->writeln($this->getHeader());
Chris@0 290 $this->writeVersionInfo();
Chris@0 291 $this->writeStartupMessage();
Chris@0 292
Chris@0 293 try {
Chris@0 294 $this->loop->run($this);
Chris@0 295 } catch (ThrowUpException $e) {
Chris@0 296 throw $e->getPrevious();
Chris@0 297 }
Chris@0 298 }
Chris@0 299
Chris@0 300 /**
Chris@0 301 * Read user input.
Chris@0 302 *
Chris@0 303 * This will continue fetching user input until the code buffer contains
Chris@0 304 * valid code.
Chris@0 305 *
Chris@0 306 * @throws BreakException if user hits Ctrl+D
Chris@0 307 */
Chris@0 308 public function getInput()
Chris@0 309 {
Chris@0 310 $this->codeBufferOpen = false;
Chris@0 311
Chris@0 312 do {
Chris@0 313 // reset output verbosity (in case it was altered by a subcommand)
Chris@0 314 $this->output->setVerbosity(ShellOutput::VERBOSITY_VERBOSE);
Chris@0 315
Chris@0 316 $input = $this->readline();
Chris@0 317
Chris@0 318 /*
Chris@0 319 * Handle Ctrl+D. It behaves differently in different cases:
Chris@0 320 *
Chris@0 321 * 1) In an expression, like a function or "if" block, clear the input buffer
Chris@0 322 * 2) At top-level session, behave like the exit command
Chris@0 323 */
Chris@0 324 if ($input === false) {
Chris@0 325 $this->output->writeln('');
Chris@0 326
Chris@0 327 if ($this->hasCode()) {
Chris@0 328 $this->resetCodeBuffer();
Chris@0 329 } else {
Chris@0 330 throw new BreakException('Ctrl+D');
Chris@0 331 }
Chris@0 332 }
Chris@0 333
Chris@0 334 // handle empty input
Chris@0 335 if (trim($input) === '') {
Chris@0 336 continue;
Chris@0 337 }
Chris@0 338
Chris@0 339 if ($this->hasCommand($input)) {
Chris@0 340 $this->readline->addHistory($input);
Chris@0 341 $this->runCommand($input);
Chris@0 342
Chris@0 343 continue;
Chris@0 344 }
Chris@0 345
Chris@0 346 $this->addCode($input);
Chris@0 347 } while (!$this->hasValidCode());
Chris@0 348 }
Chris@0 349
Chris@0 350 /**
Chris@0 351 * Pass the beforeLoop callback through to the Loop instance.
Chris@0 352 *
Chris@0 353 * @see Loop::beforeLoop
Chris@0 354 */
Chris@0 355 public function beforeLoop()
Chris@0 356 {
Chris@0 357 $this->loop->beforeLoop();
Chris@0 358 }
Chris@0 359
Chris@0 360 /**
Chris@0 361 * Pass the afterLoop callback through to the Loop instance.
Chris@0 362 *
Chris@0 363 * @see Loop::afterLoop
Chris@0 364 */
Chris@0 365 public function afterLoop()
Chris@0 366 {
Chris@0 367 $this->loop->afterLoop();
Chris@0 368 }
Chris@0 369
Chris@0 370 /**
Chris@0 371 * Set the variables currently in scope.
Chris@0 372 *
Chris@0 373 * @param array $vars
Chris@0 374 */
Chris@0 375 public function setScopeVariables(array $vars)
Chris@0 376 {
Chris@0 377 $this->context->setAll($vars);
Chris@0 378 }
Chris@0 379
Chris@0 380 /**
Chris@0 381 * Return the set of variables currently in scope.
Chris@0 382 *
Chris@0 383 * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
Chris@0 384 * passing the scope variables to `extract`
Chris@0 385 * in PHP 7.1+, you _must_ exclude 'this'
Chris@0 386 *
Chris@0 387 * @return array Associative array of scope variables
Chris@0 388 */
Chris@0 389 public function getScopeVariables($includeBoundObject = true)
Chris@0 390 {
Chris@0 391 $vars = $this->context->getAll();
Chris@0 392
Chris@0 393 if (!$includeBoundObject) {
Chris@0 394 unset($vars['this']);
Chris@0 395 }
Chris@0 396
Chris@0 397 return $vars;
Chris@0 398 }
Chris@0 399
Chris@0 400 /**
Chris@0 401 * Return the set of magic variables currently in scope.
Chris@0 402 *
Chris@0 403 * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
Chris@0 404 * passing the scope variables to `extract`
Chris@0 405 * in PHP 7.1+, you _must_ exclude 'this'
Chris@0 406 *
Chris@0 407 * @return array Associative array of magic scope variables
Chris@0 408 */
Chris@0 409 public function getSpecialScopeVariables($includeBoundObject = true)
Chris@0 410 {
Chris@0 411 $vars = $this->context->getSpecialVariables();
Chris@0 412
Chris@0 413 if (!$includeBoundObject) {
Chris@0 414 unset($vars['this']);
Chris@0 415 }
Chris@0 416
Chris@0 417 return $vars;
Chris@0 418 }
Chris@0 419
Chris@0 420 /**
Chris@0 421 * Get the set of unused command-scope variable names.
Chris@0 422 *
Chris@0 423 * @return array Array of unused variable names
Chris@0 424 */
Chris@0 425 public function getUnusedCommandScopeVariableNames()
Chris@0 426 {
Chris@0 427 return $this->context->getUnusedCommandScopeVariableNames();
Chris@0 428 }
Chris@0 429
Chris@0 430 /**
Chris@0 431 * Get the set of variable names currently in scope.
Chris@0 432 *
Chris@0 433 * @return array Array of variable names
Chris@0 434 */
Chris@0 435 public function getScopeVariableNames()
Chris@0 436 {
Chris@0 437 return array_keys($this->context->getAll());
Chris@0 438 }
Chris@0 439
Chris@0 440 /**
Chris@0 441 * Get a scope variable value by name.
Chris@0 442 *
Chris@0 443 * @param string $name
Chris@0 444 *
Chris@0 445 * @return mixed
Chris@0 446 */
Chris@0 447 public function getScopeVariable($name)
Chris@0 448 {
Chris@0 449 return $this->context->get($name);
Chris@0 450 }
Chris@0 451
Chris@0 452 /**
Chris@0 453 * Set the bound object ($this variable) for the interactive shell.
Chris@0 454 *
Chris@0 455 * @param object|null $boundObject
Chris@0 456 */
Chris@0 457 public function setBoundObject($boundObject)
Chris@0 458 {
Chris@0 459 $this->context->setBoundObject($boundObject);
Chris@0 460 }
Chris@0 461
Chris@0 462 /**
Chris@0 463 * Get the bound object ($this variable) for the interactive shell.
Chris@0 464 *
Chris@0 465 * @return object|null
Chris@0 466 */
Chris@0 467 public function getBoundObject()
Chris@0 468 {
Chris@0 469 return $this->context->getBoundObject();
Chris@0 470 }
Chris@0 471
Chris@0 472 /**
Chris@0 473 * Add includes, to be parsed and executed before running the interactive shell.
Chris@0 474 *
Chris@0 475 * @param array $includes
Chris@0 476 */
Chris@0 477 public function setIncludes(array $includes = array())
Chris@0 478 {
Chris@0 479 $this->includes = $includes;
Chris@0 480 }
Chris@0 481
Chris@0 482 /**
Chris@0 483 * Get PHP files to be parsed and executed before running the interactive shell.
Chris@0 484 *
Chris@0 485 * @return array
Chris@0 486 */
Chris@0 487 public function getIncludes()
Chris@0 488 {
Chris@0 489 return array_merge($this->config->getDefaultIncludes(), $this->includes);
Chris@0 490 }
Chris@0 491
Chris@0 492 /**
Chris@0 493 * Check whether this shell's code buffer contains code.
Chris@0 494 *
Chris@0 495 * @return bool True if the code buffer contains code
Chris@0 496 */
Chris@0 497 public function hasCode()
Chris@0 498 {
Chris@0 499 return !empty($this->codeBuffer);
Chris@0 500 }
Chris@0 501
Chris@0 502 /**
Chris@0 503 * Check whether the code in this shell's code buffer is valid.
Chris@0 504 *
Chris@0 505 * If the code is valid, the code buffer should be flushed and evaluated.
Chris@0 506 *
Chris@0 507 * @return bool True if the code buffer content is valid
Chris@0 508 */
Chris@0 509 protected function hasValidCode()
Chris@0 510 {
Chris@0 511 return !$this->codeBufferOpen && $this->code !== false;
Chris@0 512 }
Chris@0 513
Chris@0 514 /**
Chris@0 515 * Add code to the code buffer.
Chris@0 516 *
Chris@0 517 * @param string $code
Chris@0 518 */
Chris@0 519 public function addCode($code)
Chris@0 520 {
Chris@0 521 try {
Chris@0 522 // Code lines ending in \ keep the buffer open
Chris@0 523 if (substr(rtrim($code), -1) === '\\') {
Chris@0 524 $this->codeBufferOpen = true;
Chris@0 525 $code = substr(rtrim($code), 0, -1);
Chris@0 526 } else {
Chris@0 527 $this->codeBufferOpen = false;
Chris@0 528 }
Chris@0 529
Chris@0 530 $this->codeBuffer[] = $code;
Chris@0 531 $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons());
Chris@0 532 } catch (\Exception $e) {
Chris@0 533 // Add failed code blocks to the readline history.
Chris@0 534 $this->addCodeBufferToHistory();
Chris@0 535
Chris@0 536 throw $e;
Chris@0 537 }
Chris@0 538 }
Chris@0 539
Chris@0 540 /**
Chris@0 541 * Get the current code buffer.
Chris@0 542 *
Chris@0 543 * This is useful for commands which manipulate the buffer.
Chris@0 544 *
Chris@0 545 * @return array
Chris@0 546 */
Chris@0 547 public function getCodeBuffer()
Chris@0 548 {
Chris@0 549 return $this->codeBuffer;
Chris@0 550 }
Chris@0 551
Chris@0 552 /**
Chris@0 553 * Run a Psy Shell command given the user input.
Chris@0 554 *
Chris@0 555 * @throws InvalidArgumentException if the input is not a valid command
Chris@0 556 *
Chris@0 557 * @param string $input User input string
Chris@0 558 *
Chris@0 559 * @return mixed Who knows?
Chris@0 560 */
Chris@0 561 protected function runCommand($input)
Chris@0 562 {
Chris@0 563 $command = $this->getCommand($input);
Chris@0 564
Chris@0 565 if (empty($command)) {
Chris@0 566 throw new \InvalidArgumentException('Command not found: ' . $input);
Chris@0 567 }
Chris@0 568
Chris@0 569 $input = new ShellInput(str_replace('\\', '\\\\', rtrim($input, " \t\n\r\0\x0B;")));
Chris@0 570
Chris@0 571 if ($input->hasParameterOption(array('--help', '-h'))) {
Chris@0 572 $helpCommand = $this->get('help');
Chris@0 573 $helpCommand->setCommand($command);
Chris@0 574
Chris@0 575 return $helpCommand->run($input, $this->output);
Chris@0 576 }
Chris@0 577
Chris@0 578 return $command->run($input, $this->output);
Chris@0 579 }
Chris@0 580
Chris@0 581 /**
Chris@0 582 * Reset the current code buffer.
Chris@0 583 *
Chris@0 584 * This should be run after evaluating user input, catching exceptions, or
Chris@0 585 * on demand by commands such as BufferCommand.
Chris@0 586 */
Chris@0 587 public function resetCodeBuffer()
Chris@0 588 {
Chris@0 589 $this->codeBuffer = array();
Chris@0 590 $this->code = false;
Chris@0 591 }
Chris@0 592
Chris@0 593 /**
Chris@0 594 * Inject input into the input buffer.
Chris@0 595 *
Chris@0 596 * This is useful for commands which want to replay history.
Chris@0 597 *
Chris@0 598 * @param string|array $input
Chris@0 599 * @param bool $silent
Chris@0 600 */
Chris@0 601 public function addInput($input, $silent = false)
Chris@0 602 {
Chris@0 603 foreach ((array) $input as $line) {
Chris@0 604 $this->inputBuffer[] = $silent ? new SilentInput($line) : $line;
Chris@0 605 }
Chris@0 606 }
Chris@0 607
Chris@0 608 /**
Chris@0 609 * Flush the current (valid) code buffer.
Chris@0 610 *
Chris@0 611 * If the code buffer is valid, resets the code buffer and returns the
Chris@0 612 * current code.
Chris@0 613 *
Chris@0 614 * @return string PHP code buffer contents
Chris@0 615 */
Chris@0 616 public function flushCode()
Chris@0 617 {
Chris@0 618 if ($this->hasValidCode()) {
Chris@0 619 $this->addCodeBufferToHistory();
Chris@0 620 $code = $this->code;
Chris@0 621 $this->resetCodeBuffer();
Chris@0 622
Chris@0 623 return $code;
Chris@0 624 }
Chris@0 625 }
Chris@0 626
Chris@0 627 /**
Chris@0 628 * Filter silent input from code buffer, write the rest to readline history.
Chris@0 629 */
Chris@0 630 private function addCodeBufferToHistory()
Chris@0 631 {
Chris@0 632 $codeBuffer = array_filter($this->codeBuffer, function ($line) {
Chris@0 633 return !$line instanceof SilentInput;
Chris@0 634 });
Chris@0 635
Chris@0 636 $code = implode("\n", $codeBuffer);
Chris@0 637
Chris@0 638 if (trim($code) !== '') {
Chris@0 639 $this->readline->addHistory($code);
Chris@0 640 }
Chris@0 641 }
Chris@0 642
Chris@0 643 /**
Chris@0 644 * Get the current evaluation scope namespace.
Chris@0 645 *
Chris@0 646 * @see CodeCleaner::getNamespace
Chris@0 647 *
Chris@0 648 * @return string Current code namespace
Chris@0 649 */
Chris@0 650 public function getNamespace()
Chris@0 651 {
Chris@0 652 if ($namespace = $this->cleaner->getNamespace()) {
Chris@0 653 return implode('\\', $namespace);
Chris@0 654 }
Chris@0 655 }
Chris@0 656
Chris@0 657 /**
Chris@0 658 * Write a string to stdout.
Chris@0 659 *
Chris@0 660 * This is used by the shell loop for rendering output from evaluated code.
Chris@0 661 *
Chris@0 662 * @param string $out
Chris@0 663 * @param int $phase Output buffering phase
Chris@0 664 */
Chris@0 665 public function writeStdout($out, $phase = PHP_OUTPUT_HANDLER_END)
Chris@0 666 {
Chris@0 667 $isCleaning = false;
Chris@0 668 if (version_compare(PHP_VERSION, '5.4', '>=')) {
Chris@0 669 $isCleaning = $phase & PHP_OUTPUT_HANDLER_CLEAN;
Chris@0 670 }
Chris@0 671
Chris@0 672 // Incremental flush
Chris@0 673 if ($out !== '' && !$isCleaning) {
Chris@0 674 $this->output->write($out, false, ShellOutput::OUTPUT_RAW);
Chris@0 675 $this->outputWantsNewline = (substr($out, -1) !== "\n");
Chris@0 676 $this->stdoutBuffer .= $out;
Chris@0 677 }
Chris@0 678
Chris@0 679 // Output buffering is done!
Chris@0 680 if ($phase & PHP_OUTPUT_HANDLER_END) {
Chris@0 681 // Write an extra newline if stdout didn't end with one
Chris@0 682 if ($this->outputWantsNewline) {
Chris@0 683 $this->output->writeln(sprintf('<aside>%s</aside>', $this->config->useUnicode() ? '⏎' : '\\n'));
Chris@0 684 $this->outputWantsNewline = false;
Chris@0 685 }
Chris@0 686
Chris@0 687 // Save the stdout buffer as $__out
Chris@0 688 if ($this->stdoutBuffer !== '') {
Chris@0 689 $this->context->setLastStdout($this->stdoutBuffer);
Chris@0 690 $this->stdoutBuffer = '';
Chris@0 691 }
Chris@0 692 }
Chris@0 693 }
Chris@0 694
Chris@0 695 /**
Chris@0 696 * Write a return value to stdout.
Chris@0 697 *
Chris@0 698 * The return value is formatted or pretty-printed, and rendered in a
Chris@0 699 * visibly distinct manner (in this case, as cyan).
Chris@0 700 *
Chris@0 701 * @see self::presentValue
Chris@0 702 *
Chris@0 703 * @param mixed $ret
Chris@0 704 */
Chris@0 705 public function writeReturnValue($ret)
Chris@0 706 {
Chris@0 707 if ($ret instanceof NoReturnValue) {
Chris@0 708 return;
Chris@0 709 }
Chris@0 710
Chris@0 711 $this->context->setReturnValue($ret);
Chris@0 712 $ret = $this->presentValue($ret);
Chris@0 713 $indent = str_repeat(' ', strlen(static::RETVAL));
Chris@0 714
Chris@0 715 $this->output->writeln(static::RETVAL . str_replace(PHP_EOL, PHP_EOL . $indent, $ret));
Chris@0 716 }
Chris@0 717
Chris@0 718 /**
Chris@0 719 * Renders a caught Exception.
Chris@0 720 *
Chris@0 721 * Exceptions are formatted according to severity. ErrorExceptions which were
Chris@0 722 * warnings or Strict errors aren't rendered as harshly as real errors.
Chris@0 723 *
Chris@0 724 * Stores $e as the last Exception in the Shell Context.
Chris@0 725 *
Chris@0 726 * @param \Exception $e An exception instance
Chris@0 727 * @param OutputInterface $output An OutputInterface instance
Chris@0 728 */
Chris@0 729 public function writeException(\Exception $e)
Chris@0 730 {
Chris@0 731 $this->context->setLastException($e);
Chris@0 732 $this->output->writeln($this->formatException($e));
Chris@0 733 $this->resetCodeBuffer();
Chris@0 734 }
Chris@0 735
Chris@0 736 /**
Chris@0 737 * Helper for formatting an exception for writeException().
Chris@0 738 *
Chris@0 739 * @todo extract this to somewhere it makes more sense
Chris@0 740 *
Chris@0 741 * @param \Exception $e
Chris@0 742 *
Chris@0 743 * @return string
Chris@0 744 */
Chris@0 745 public function formatException(\Exception $e)
Chris@0 746 {
Chris@0 747 $message = $e->getMessage();
Chris@0 748 if (!$e instanceof PsyException) {
Chris@0 749 if ($message === '') {
Chris@0 750 $message = get_class($e);
Chris@0 751 } else {
Chris@0 752 $message = sprintf('%s with message \'%s\'', get_class($e), $message);
Chris@0 753 }
Chris@0 754 }
Chris@0 755
Chris@0 756 $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error';
Chris@0 757
Chris@0 758 return sprintf('<%s>%s</%s>', $severity, OutputFormatter::escape($message), $severity);
Chris@0 759 }
Chris@0 760
Chris@0 761 /**
Chris@0 762 * Helper for getting an output style for the given ErrorException's level.
Chris@0 763 *
Chris@0 764 * @param \ErrorException $e
Chris@0 765 *
Chris@0 766 * @return string
Chris@0 767 */
Chris@0 768 protected function getSeverity(\ErrorException $e)
Chris@0 769 {
Chris@0 770 $severity = $e->getSeverity();
Chris@0 771 if ($severity & error_reporting()) {
Chris@0 772 switch ($severity) {
Chris@0 773 case E_WARNING:
Chris@0 774 case E_NOTICE:
Chris@0 775 case E_CORE_WARNING:
Chris@0 776 case E_COMPILE_WARNING:
Chris@0 777 case E_USER_WARNING:
Chris@0 778 case E_USER_NOTICE:
Chris@0 779 case E_STRICT:
Chris@0 780 return 'warning';
Chris@0 781
Chris@0 782 default:
Chris@0 783 return 'error';
Chris@0 784 }
Chris@0 785 } else {
Chris@0 786 // Since this is below the user's reporting threshold, it's always going to be a warning.
Chris@0 787 return 'warning';
Chris@0 788 }
Chris@0 789 }
Chris@0 790
Chris@0 791 /**
Chris@0 792 * Helper for throwing an ErrorException.
Chris@0 793 *
Chris@0 794 * This allows us to:
Chris@0 795 *
Chris@0 796 * set_error_handler(array($psysh, 'handleError'));
Chris@0 797 *
Chris@0 798 * Unlike ErrorException::throwException, this error handler respects the
Chris@0 799 * current error_reporting level; i.e. it logs warnings and notices, but
Chris@0 800 * doesn't throw an exception unless it's above the current error_reporting
Chris@0 801 * threshold. This should probably only be used in the inner execution loop
Chris@0 802 * of the shell, as most of the time a thrown exception is much more useful.
Chris@0 803 *
Chris@0 804 * If the error type matches the `errorLoggingLevel` config, it will be
Chris@0 805 * logged as well, regardless of the `error_reporting` level.
Chris@0 806 *
Chris@0 807 * @see \Psy\Exception\ErrorException::throwException
Chris@0 808 * @see \Psy\Shell::writeException
Chris@0 809 *
Chris@0 810 * @throws \Psy\Exception\ErrorException depending on the current error_reporting level
Chris@0 811 *
Chris@0 812 * @param int $errno Error type
Chris@0 813 * @param string $errstr Message
Chris@0 814 * @param string $errfile Filename
Chris@0 815 * @param int $errline Line number
Chris@0 816 */
Chris@0 817 public function handleError($errno, $errstr, $errfile, $errline)
Chris@0 818 {
Chris@0 819 if ($errno & error_reporting()) {
Chris@0 820 ErrorException::throwException($errno, $errstr, $errfile, $errline);
Chris@0 821 } elseif ($errno & $this->config->errorLoggingLevel()) {
Chris@0 822 // log it and continue...
Chris@0 823 $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline));
Chris@0 824 }
Chris@0 825 }
Chris@0 826
Chris@0 827 /**
Chris@0 828 * Format a value for display.
Chris@0 829 *
Chris@0 830 * @see Presenter::present
Chris@0 831 *
Chris@0 832 * @param mixed $val
Chris@0 833 *
Chris@0 834 * @return string Formatted value
Chris@0 835 */
Chris@0 836 protected function presentValue($val)
Chris@0 837 {
Chris@0 838 return $this->config->getPresenter()->present($val);
Chris@0 839 }
Chris@0 840
Chris@0 841 /**
Chris@0 842 * Get a command (if one exists) for the current input string.
Chris@0 843 *
Chris@0 844 * @param string $input
Chris@0 845 *
Chris@0 846 * @return null|BaseCommand
Chris@0 847 */
Chris@0 848 protected function getCommand($input)
Chris@0 849 {
Chris@0 850 $input = new StringInput($input);
Chris@0 851 if ($name = $input->getFirstArgument()) {
Chris@0 852 return $this->get($name);
Chris@0 853 }
Chris@0 854 }
Chris@0 855
Chris@0 856 /**
Chris@0 857 * Check whether a command is set for the current input string.
Chris@0 858 *
Chris@0 859 * @param string $input
Chris@0 860 *
Chris@0 861 * @return bool True if the shell has a command for the given input
Chris@0 862 */
Chris@0 863 protected function hasCommand($input)
Chris@0 864 {
Chris@0 865 $input = new StringInput($input);
Chris@0 866 if ($name = $input->getFirstArgument()) {
Chris@0 867 return $this->has($name);
Chris@0 868 }
Chris@0 869
Chris@0 870 return false;
Chris@0 871 }
Chris@0 872
Chris@0 873 /**
Chris@0 874 * Get the current input prompt.
Chris@0 875 *
Chris@0 876 * @return string
Chris@0 877 */
Chris@0 878 protected function getPrompt()
Chris@0 879 {
Chris@0 880 if ($this->hasCode()) {
Chris@0 881 return static::BUFF_PROMPT;
Chris@0 882 }
Chris@0 883
Chris@0 884 return $this->config->getPrompt() ?: static::PROMPT;
Chris@0 885 }
Chris@0 886
Chris@0 887 /**
Chris@0 888 * Read a line of user input.
Chris@0 889 *
Chris@0 890 * This will return a line from the input buffer (if any exist). Otherwise,
Chris@0 891 * it will ask the user for input.
Chris@0 892 *
Chris@0 893 * If readline is enabled, this delegates to readline. Otherwise, it's an
Chris@0 894 * ugly `fgets` call.
Chris@0 895 *
Chris@0 896 * @return string One line of user input
Chris@0 897 */
Chris@0 898 protected function readline()
Chris@0 899 {
Chris@0 900 if (!empty($this->inputBuffer)) {
Chris@0 901 $line = array_shift($this->inputBuffer);
Chris@0 902 if (!$line instanceof SilentInput) {
Chris@0 903 $this->output->writeln(sprintf('<aside>%s %s</aside>', static::REPLAY, OutputFormatter::escape($line)));
Chris@0 904 }
Chris@0 905
Chris@0 906 return $line;
Chris@0 907 }
Chris@0 908
Chris@0 909 if ($bracketedPaste = $this->config->useBracketedPaste()) {
Chris@0 910 printf("\e[?2004h"); // Enable bracketed paste
Chris@0 911 }
Chris@0 912
Chris@0 913 $line = $this->readline->readline($this->getPrompt());
Chris@0 914
Chris@0 915 if ($bracketedPaste) {
Chris@0 916 printf("\e[?2004l"); // ... and disable it again
Chris@0 917 }
Chris@0 918
Chris@0 919 return $line;
Chris@0 920 }
Chris@0 921
Chris@0 922 /**
Chris@0 923 * Get the shell output header.
Chris@0 924 *
Chris@0 925 * @return string
Chris@0 926 */
Chris@0 927 protected function getHeader()
Chris@0 928 {
Chris@0 929 return sprintf('<aside>%s by Justin Hileman</aside>', $this->getVersion());
Chris@0 930 }
Chris@0 931
Chris@0 932 /**
Chris@0 933 * Get the current version of Psy Shell.
Chris@0 934 *
Chris@0 935 * @return string
Chris@0 936 */
Chris@0 937 public function getVersion()
Chris@0 938 {
Chris@0 939 $separator = $this->config->useUnicode() ? '—' : '-';
Chris@0 940
Chris@0 941 return sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, phpversion(), $separator, php_sapi_name());
Chris@0 942 }
Chris@0 943
Chris@0 944 /**
Chris@0 945 * Get a PHP manual database instance.
Chris@0 946 *
Chris@0 947 * @return \PDO|null
Chris@0 948 */
Chris@0 949 public function getManualDb()
Chris@0 950 {
Chris@0 951 return $this->config->getManualDb();
Chris@0 952 }
Chris@0 953
Chris@0 954 /**
Chris@0 955 * Autocomplete variable names.
Chris@0 956 *
Chris@0 957 * This is used by `readline` for tab completion.
Chris@0 958 *
Chris@0 959 * @param string $text
Chris@0 960 *
Chris@0 961 * @return mixed Array possible completions for the given input, if any
Chris@0 962 */
Chris@0 963 protected function autocomplete($text)
Chris@0 964 {
Chris@0 965 $info = readline_info();
Chris@0 966 // $line = substr($info['line_buffer'], 0, $info['end']);
Chris@0 967
Chris@0 968 // Check whether there's a command for this
Chris@0 969 // $words = explode(' ', $line);
Chris@0 970 // $firstWord = reset($words);
Chris@0 971
Chris@0 972 // check whether this is a variable...
Chris@0 973 $firstChar = substr($info['line_buffer'], max(0, $info['end'] - strlen($text) - 1), 1);
Chris@0 974 if ($firstChar === '$') {
Chris@0 975 return $this->getScopeVariableNames();
Chris@0 976 }
Chris@0 977 }
Chris@0 978
Chris@0 979 /**
Chris@0 980 * Initialize tab completion matchers.
Chris@0 981 *
Chris@0 982 * If tab completion is enabled this adds tab completion matchers to the
Chris@0 983 * auto completer and sets context if needed.
Chris@0 984 */
Chris@0 985 protected function initializeTabCompletion()
Chris@0 986 {
Chris@0 987 // auto completer needs shell to be linked to configuration because of the context aware matchers
Chris@0 988 if ($this->config->getTabCompletion()) {
Chris@0 989 $this->completion = $this->config->getAutoCompleter();
Chris@0 990 $this->addTabCompletionMatchers($this->config->getTabCompletionMatchers());
Chris@0 991 foreach ($this->getTabCompletionMatchers() as $matcher) {
Chris@0 992 if ($matcher instanceof ContextAware) {
Chris@0 993 $matcher->setContext($this->context);
Chris@0 994 }
Chris@0 995 $this->completion->addMatcher($matcher);
Chris@0 996 }
Chris@0 997 $this->completion->activate();
Chris@0 998 }
Chris@0 999 }
Chris@0 1000
Chris@0 1001 /**
Chris@0 1002 * @todo Implement self-update
Chris@0 1003 * @todo Implement prompt to start update
Chris@0 1004 *
Chris@0 1005 * @return void|string
Chris@0 1006 */
Chris@0 1007 protected function writeVersionInfo()
Chris@0 1008 {
Chris@0 1009 if (PHP_SAPI !== 'cli') {
Chris@0 1010 return;
Chris@0 1011 }
Chris@0 1012
Chris@0 1013 try {
Chris@0 1014 $client = $this->config->getChecker();
Chris@0 1015 if (!$client->isLatest()) {
Chris@12 1016 $this->output->writeln(sprintf('New version is available (current: %s, latest: %s)', self::VERSION, $client->getLatest()));
Chris@0 1017 }
Chris@0 1018 } catch (\InvalidArgumentException $e) {
Chris@0 1019 $this->output->writeln($e->getMessage());
Chris@0 1020 }
Chris@0 1021 }
Chris@0 1022
Chris@0 1023 /**
Chris@0 1024 * Write a startup message if set.
Chris@0 1025 */
Chris@0 1026 protected function writeStartupMessage()
Chris@0 1027 {
Chris@0 1028 $message = $this->config->getStartupMessage();
Chris@0 1029 if ($message !== null && $message !== '') {
Chris@0 1030 $this->output->writeln($message);
Chris@0 1031 }
Chris@0 1032 }
Chris@0 1033 }