annotate vendor/psy/psysh/src/Shell.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@13 1 <?php
Chris@13 2
Chris@13 3 /*
Chris@13 4 * This file is part of Psy Shell.
Chris@13 5 *
Chris@13 6 * (c) 2012-2018 Justin Hileman
Chris@13 7 *
Chris@13 8 * For the full copyright and license information, please view the LICENSE
Chris@13 9 * file that was distributed with this source code.
Chris@13 10 */
Chris@13 11
Chris@13 12 namespace Psy;
Chris@13 13
Chris@13 14 use Psy\CodeCleaner\NoReturnValue;
Chris@13 15 use Psy\Exception\BreakException;
Chris@13 16 use Psy\Exception\ErrorException;
Chris@13 17 use Psy\Exception\Exception as PsyException;
Chris@13 18 use Psy\Exception\ThrowUpException;
Chris@13 19 use Psy\Exception\TypeErrorException;
Chris@13 20 use Psy\ExecutionLoop\ProcessForker;
Chris@13 21 use Psy\ExecutionLoop\RunkitReloader;
Chris@13 22 use Psy\Input\ShellInput;
Chris@13 23 use Psy\Input\SilentInput;
Chris@13 24 use Psy\Output\ShellOutput;
Chris@13 25 use Psy\TabCompletion\Matcher;
Chris@13 26 use Psy\VarDumper\PresenterAware;
Chris@13 27 use Symfony\Component\Console\Application;
Chris@13 28 use Symfony\Component\Console\Command\Command as BaseCommand;
Chris@13 29 use Symfony\Component\Console\Formatter\OutputFormatter;
Chris@13 30 use Symfony\Component\Console\Input\ArgvInput;
Chris@13 31 use Symfony\Component\Console\Input\InputArgument;
Chris@13 32 use Symfony\Component\Console\Input\InputDefinition;
Chris@13 33 use Symfony\Component\Console\Input\InputInterface;
Chris@13 34 use Symfony\Component\Console\Input\InputOption;
Chris@13 35 use Symfony\Component\Console\Input\StringInput;
Chris@13 36 use Symfony\Component\Console\Output\OutputInterface;
Chris@13 37
Chris@13 38 /**
Chris@13 39 * The Psy Shell application.
Chris@13 40 *
Chris@13 41 * Usage:
Chris@13 42 *
Chris@13 43 * $shell = new Shell;
Chris@13 44 * $shell->run();
Chris@13 45 *
Chris@13 46 * @author Justin Hileman <justin@justinhileman.info>
Chris@13 47 */
Chris@13 48 class Shell extends Application
Chris@13 49 {
Chris@17 50 const VERSION = 'v0.9.9';
Chris@13 51
Chris@13 52 const PROMPT = '>>> ';
Chris@13 53 const BUFF_PROMPT = '... ';
Chris@13 54 const REPLAY = '--> ';
Chris@13 55 const RETVAL = '=> ';
Chris@13 56
Chris@13 57 private $config;
Chris@13 58 private $cleaner;
Chris@13 59 private $output;
Chris@13 60 private $readline;
Chris@13 61 private $inputBuffer;
Chris@13 62 private $code;
Chris@13 63 private $codeBuffer;
Chris@13 64 private $codeBufferOpen;
Chris@13 65 private $codeStack;
Chris@13 66 private $stdoutBuffer;
Chris@13 67 private $context;
Chris@13 68 private $includes;
Chris@13 69 private $loop;
Chris@13 70 private $outputWantsNewline = false;
Chris@13 71 private $prompt;
Chris@13 72 private $loopListeners;
Chris@13 73 private $autoCompleter;
Chris@13 74 private $matchers = [];
Chris@13 75 private $commandsMatcher;
Chris@17 76 private $lastExecSuccess = true;
Chris@13 77
Chris@13 78 /**
Chris@13 79 * Create a new Psy Shell.
Chris@13 80 *
Chris@13 81 * @param Configuration $config (default: null)
Chris@13 82 */
Chris@13 83 public function __construct(Configuration $config = null)
Chris@13 84 {
Chris@13 85 $this->config = $config ?: new Configuration();
Chris@13 86 $this->cleaner = $this->config->getCodeCleaner();
Chris@13 87 $this->loop = new ExecutionLoop();
Chris@13 88 $this->context = new Context();
Chris@13 89 $this->includes = [];
Chris@13 90 $this->readline = $this->config->getReadline();
Chris@13 91 $this->inputBuffer = [];
Chris@13 92 $this->codeStack = [];
Chris@13 93 $this->stdoutBuffer = '';
Chris@13 94 $this->loopListeners = $this->getDefaultLoopListeners();
Chris@13 95
Chris@13 96 parent::__construct('Psy Shell', self::VERSION);
Chris@13 97
Chris@13 98 $this->config->setShell($this);
Chris@13 99
Chris@13 100 // Register the current shell session's config with \Psy\info
Chris@13 101 \Psy\info($this->config);
Chris@13 102 }
Chris@13 103
Chris@13 104 /**
Chris@13 105 * Check whether the first thing in a backtrace is an include call.
Chris@13 106 *
Chris@13 107 * This is used by the psysh bin to decide whether to start a shell on boot,
Chris@13 108 * or to simply autoload the library.
Chris@13 109 */
Chris@13 110 public static function isIncluded(array $trace)
Chris@13 111 {
Chris@13 112 return isset($trace[0]['function']) &&
Chris@17 113 \in_array($trace[0]['function'], ['require', 'include', 'require_once', 'include_once']);
Chris@13 114 }
Chris@13 115
Chris@13 116 /**
Chris@13 117 * Invoke a Psy Shell from the current context.
Chris@13 118 *
Chris@13 119 * @see Psy\debug
Chris@13 120 * @deprecated will be removed in 1.0. Use \Psy\debug instead
Chris@13 121 *
Chris@16 122 * @param array $vars Scope variables from the calling context (default: array())
Chris@16 123 * @param object|string $bindTo Bound object ($this) or class (self) value for the shell
Chris@13 124 *
Chris@13 125 * @return array Scope variables from the debugger session
Chris@13 126 */
Chris@16 127 public static function debug(array $vars = [], $bindTo = null)
Chris@13 128 {
Chris@16 129 return \Psy\debug($vars, $bindTo);
Chris@13 130 }
Chris@13 131
Chris@13 132 /**
Chris@13 133 * Adds a command object.
Chris@13 134 *
Chris@13 135 * {@inheritdoc}
Chris@13 136 *
Chris@13 137 * @param BaseCommand $command A Symfony Console Command object
Chris@13 138 *
Chris@13 139 * @return BaseCommand The registered command
Chris@13 140 */
Chris@13 141 public function add(BaseCommand $command)
Chris@13 142 {
Chris@13 143 if ($ret = parent::add($command)) {
Chris@13 144 if ($ret instanceof ContextAware) {
Chris@13 145 $ret->setContext($this->context);
Chris@13 146 }
Chris@13 147
Chris@13 148 if ($ret instanceof PresenterAware) {
Chris@13 149 $ret->setPresenter($this->config->getPresenter());
Chris@13 150 }
Chris@13 151
Chris@13 152 if (isset($this->commandsMatcher)) {
Chris@13 153 $this->commandsMatcher->setCommands($this->all());
Chris@13 154 }
Chris@13 155 }
Chris@13 156
Chris@13 157 return $ret;
Chris@13 158 }
Chris@13 159
Chris@13 160 /**
Chris@13 161 * Gets the default input definition.
Chris@13 162 *
Chris@13 163 * @return InputDefinition An InputDefinition instance
Chris@13 164 */
Chris@13 165 protected function getDefaultInputDefinition()
Chris@13 166 {
Chris@13 167 return new InputDefinition([
Chris@13 168 new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
Chris@13 169 new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'),
Chris@13 170 ]);
Chris@13 171 }
Chris@13 172
Chris@13 173 /**
Chris@13 174 * Gets the default commands that should always be available.
Chris@13 175 *
Chris@13 176 * @return array An array of default Command instances
Chris@13 177 */
Chris@13 178 protected function getDefaultCommands()
Chris@13 179 {
Chris@13 180 $sudo = new Command\SudoCommand();
Chris@13 181 $sudo->setReadline($this->readline);
Chris@13 182
Chris@13 183 $hist = new Command\HistoryCommand();
Chris@13 184 $hist->setReadline($this->readline);
Chris@13 185
Chris@13 186 return [
Chris@13 187 new Command\HelpCommand(),
Chris@13 188 new Command\ListCommand(),
Chris@13 189 new Command\DumpCommand(),
Chris@13 190 new Command\DocCommand(),
Chris@13 191 new Command\ShowCommand($this->config->colorMode()),
Chris@13 192 new Command\WtfCommand($this->config->colorMode()),
Chris@13 193 new Command\WhereamiCommand($this->config->colorMode()),
Chris@13 194 new Command\ThrowUpCommand(),
Chris@13 195 new Command\TimeitCommand(),
Chris@13 196 new Command\TraceCommand(),
Chris@13 197 new Command\BufferCommand(),
Chris@13 198 new Command\ClearCommand(),
Chris@13 199 new Command\EditCommand($this->config->getRuntimeDir()),
Chris@13 200 // new Command\PsyVersionCommand(),
Chris@13 201 $sudo,
Chris@13 202 $hist,
Chris@13 203 new Command\ExitCommand(),
Chris@13 204 ];
Chris@13 205 }
Chris@13 206
Chris@13 207 /**
Chris@13 208 * @return array
Chris@13 209 */
Chris@13 210 protected function getDefaultMatchers()
Chris@13 211 {
Chris@13 212 // Store the Commands Matcher for later. If more commands are added,
Chris@13 213 // we'll update the Commands Matcher too.
Chris@13 214 $this->commandsMatcher = new Matcher\CommandsMatcher($this->all());
Chris@13 215
Chris@13 216 return [
Chris@13 217 $this->commandsMatcher,
Chris@13 218 new Matcher\KeywordsMatcher(),
Chris@13 219 new Matcher\VariablesMatcher(),
Chris@13 220 new Matcher\ConstantsMatcher(),
Chris@13 221 new Matcher\FunctionsMatcher(),
Chris@13 222 new Matcher\ClassNamesMatcher(),
Chris@13 223 new Matcher\ClassMethodsMatcher(),
Chris@13 224 new Matcher\ClassAttributesMatcher(),
Chris@13 225 new Matcher\ObjectMethodsMatcher(),
Chris@13 226 new Matcher\ObjectAttributesMatcher(),
Chris@13 227 new Matcher\ClassMethodDefaultParametersMatcher(),
Chris@13 228 new Matcher\ObjectMethodDefaultParametersMatcher(),
Chris@13 229 new Matcher\FunctionDefaultParametersMatcher(),
Chris@13 230 ];
Chris@13 231 }
Chris@13 232
Chris@13 233 /**
Chris@13 234 * @deprecated Nothing should use this anymore
Chris@13 235 */
Chris@13 236 protected function getTabCompletionMatchers()
Chris@13 237 {
Chris@17 238 @\trigger_error('getTabCompletionMatchers is no longer used', E_USER_DEPRECATED);
Chris@13 239 }
Chris@13 240
Chris@13 241 /**
Chris@13 242 * Gets the default command loop listeners.
Chris@13 243 *
Chris@13 244 * @return array An array of Execution Loop Listener instances
Chris@13 245 */
Chris@13 246 protected function getDefaultLoopListeners()
Chris@13 247 {
Chris@13 248 $listeners = [];
Chris@13 249
Chris@13 250 if (ProcessForker::isSupported() && $this->config->usePcntl()) {
Chris@13 251 $listeners[] = new ProcessForker();
Chris@13 252 }
Chris@13 253
Chris@13 254 if (RunkitReloader::isSupported()) {
Chris@13 255 $listeners[] = new RunkitReloader();
Chris@13 256 }
Chris@13 257
Chris@13 258 return $listeners;
Chris@13 259 }
Chris@13 260
Chris@13 261 /**
Chris@13 262 * Add tab completion matchers.
Chris@13 263 *
Chris@13 264 * @param array $matchers
Chris@13 265 */
Chris@13 266 public function addMatchers(array $matchers)
Chris@13 267 {
Chris@17 268 $this->matchers = \array_merge($this->matchers, $matchers);
Chris@13 269
Chris@13 270 if (isset($this->autoCompleter)) {
Chris@13 271 $this->addMatchersToAutoCompleter($matchers);
Chris@13 272 }
Chris@13 273 }
Chris@13 274
Chris@13 275 /**
Chris@13 276 * @deprecated Call `addMatchers` instead
Chris@13 277 *
Chris@13 278 * @param array $matchers
Chris@13 279 */
Chris@13 280 public function addTabCompletionMatchers(array $matchers)
Chris@13 281 {
Chris@13 282 $this->addMatchers($matchers);
Chris@13 283 }
Chris@13 284
Chris@13 285 /**
Chris@13 286 * Set the Shell output.
Chris@13 287 *
Chris@13 288 * @param OutputInterface $output
Chris@13 289 */
Chris@13 290 public function setOutput(OutputInterface $output)
Chris@13 291 {
Chris@13 292 $this->output = $output;
Chris@13 293 }
Chris@13 294
Chris@13 295 /**
Chris@13 296 * Runs the current application.
Chris@13 297 *
Chris@13 298 * @param InputInterface $input An Input instance
Chris@13 299 * @param OutputInterface $output An Output instance
Chris@13 300 *
Chris@13 301 * @return int 0 if everything went fine, or an error code
Chris@13 302 */
Chris@13 303 public function run(InputInterface $input = null, OutputInterface $output = null)
Chris@13 304 {
Chris@13 305 $this->initializeTabCompletion();
Chris@13 306
Chris@13 307 if ($input === null && !isset($_SERVER['argv'])) {
Chris@13 308 $input = new ArgvInput([]);
Chris@13 309 }
Chris@13 310
Chris@13 311 if ($output === null) {
Chris@13 312 $output = $this->config->getOutput();
Chris@13 313 }
Chris@13 314
Chris@13 315 try {
Chris@13 316 return parent::run($input, $output);
Chris@13 317 } catch (\Exception $e) {
Chris@13 318 $this->writeException($e);
Chris@13 319 }
Chris@13 320
Chris@13 321 return 1;
Chris@13 322 }
Chris@13 323
Chris@13 324 /**
Chris@13 325 * Runs the current application.
Chris@13 326 *
Chris@13 327 * @throws Exception if thrown via the `throw-up` command
Chris@13 328 *
Chris@13 329 * @param InputInterface $input An Input instance
Chris@13 330 * @param OutputInterface $output An Output instance
Chris@13 331 *
Chris@13 332 * @return int 0 if everything went fine, or an error code
Chris@13 333 */
Chris@13 334 public function doRun(InputInterface $input, OutputInterface $output)
Chris@13 335 {
Chris@13 336 $this->setOutput($output);
Chris@13 337
Chris@13 338 $this->resetCodeBuffer();
Chris@13 339
Chris@13 340 $this->setAutoExit(false);
Chris@13 341 $this->setCatchExceptions(false);
Chris@13 342
Chris@13 343 $this->readline->readHistory();
Chris@13 344
Chris@13 345 $this->output->writeln($this->getHeader());
Chris@13 346 $this->writeVersionInfo();
Chris@13 347 $this->writeStartupMessage();
Chris@13 348
Chris@13 349 try {
Chris@13 350 $this->beforeRun();
Chris@13 351 $this->loop->run($this);
Chris@13 352 $this->afterRun();
Chris@13 353 } catch (ThrowUpException $e) {
Chris@13 354 throw $e->getPrevious();
Chris@13 355 } catch (BreakException $e) {
Chris@13 356 // The ProcessForker throws a BreakException to finish the main thread.
Chris@13 357 return;
Chris@13 358 }
Chris@13 359 }
Chris@13 360
Chris@13 361 /**
Chris@13 362 * Read user input.
Chris@13 363 *
Chris@13 364 * This will continue fetching user input until the code buffer contains
Chris@13 365 * valid code.
Chris@13 366 *
Chris@13 367 * @throws BreakException if user hits Ctrl+D
Chris@13 368 */
Chris@13 369 public function getInput()
Chris@13 370 {
Chris@13 371 $this->codeBufferOpen = false;
Chris@13 372
Chris@13 373 do {
Chris@13 374 // reset output verbosity (in case it was altered by a subcommand)
Chris@13 375 $this->output->setVerbosity(ShellOutput::VERBOSITY_VERBOSE);
Chris@13 376
Chris@13 377 $input = $this->readline();
Chris@13 378
Chris@13 379 /*
Chris@13 380 * Handle Ctrl+D. It behaves differently in different cases:
Chris@13 381 *
Chris@13 382 * 1) In an expression, like a function or "if" block, clear the input buffer
Chris@13 383 * 2) At top-level session, behave like the exit command
Chris@13 384 */
Chris@13 385 if ($input === false) {
Chris@13 386 $this->output->writeln('');
Chris@13 387
Chris@13 388 if ($this->hasCode()) {
Chris@13 389 $this->resetCodeBuffer();
Chris@13 390 } else {
Chris@13 391 throw new BreakException('Ctrl+D');
Chris@13 392 }
Chris@13 393 }
Chris@13 394
Chris@13 395 // handle empty input
Chris@17 396 if (\trim($input) === '' && !$this->codeBufferOpen) {
Chris@13 397 continue;
Chris@13 398 }
Chris@13 399
Chris@13 400 $input = $this->onInput($input);
Chris@13 401
Chris@16 402 // If the input isn't in an open string or comment, check for commands to run.
Chris@16 403 if ($this->hasCommand($input) && !$this->inputInOpenStringOrComment($input)) {
Chris@13 404 $this->addHistory($input);
Chris@13 405 $this->runCommand($input);
Chris@13 406
Chris@13 407 continue;
Chris@13 408 }
Chris@13 409
Chris@13 410 $this->addCode($input);
Chris@13 411 } while (!$this->hasValidCode());
Chris@13 412 }
Chris@13 413
Chris@13 414 /**
Chris@16 415 * Check whether the code buffer (plus current input) is in an open string or comment.
Chris@16 416 *
Chris@16 417 * @param string $input current line of input
Chris@16 418 *
Chris@16 419 * @return bool true if the input is in an open string or comment
Chris@16 420 */
Chris@16 421 private function inputInOpenStringOrComment($input)
Chris@16 422 {
Chris@16 423 if (!$this->hasCode()) {
Chris@16 424 return;
Chris@16 425 }
Chris@16 426
Chris@16 427 $code = $this->codeBuffer;
Chris@17 428 \array_push($code, $input);
Chris@17 429 $tokens = @\token_get_all('<?php ' . \implode("\n", $code));
Chris@17 430 $last = \array_pop($tokens);
Chris@16 431
Chris@16 432 return $last === '"' || $last === '`' ||
Chris@17 433 (\is_array($last) && \in_array($last[0], [T_ENCAPSED_AND_WHITESPACE, T_START_HEREDOC, T_COMMENT]));
Chris@16 434 }
Chris@16 435
Chris@16 436 /**
Chris@13 437 * Run execution loop listeners before the shell session.
Chris@13 438 */
Chris@13 439 protected function beforeRun()
Chris@13 440 {
Chris@13 441 foreach ($this->loopListeners as $listener) {
Chris@13 442 $listener->beforeRun($this);
Chris@13 443 }
Chris@13 444 }
Chris@13 445
Chris@13 446 /**
Chris@13 447 * Run execution loop listeners at the start of each loop.
Chris@13 448 */
Chris@13 449 public function beforeLoop()
Chris@13 450 {
Chris@13 451 foreach ($this->loopListeners as $listener) {
Chris@13 452 $listener->beforeLoop($this);
Chris@13 453 }
Chris@13 454 }
Chris@13 455
Chris@13 456 /**
Chris@13 457 * Run execution loop listeners on user input.
Chris@13 458 *
Chris@13 459 * @param string $input
Chris@13 460 *
Chris@13 461 * @return string
Chris@13 462 */
Chris@13 463 public function onInput($input)
Chris@13 464 {
Chris@13 465 foreach ($this->loopListeners as $listeners) {
Chris@13 466 if (($return = $listeners->onInput($this, $input)) !== null) {
Chris@13 467 $input = $return;
Chris@13 468 }
Chris@13 469 }
Chris@13 470
Chris@13 471 return $input;
Chris@13 472 }
Chris@13 473
Chris@13 474 /**
Chris@13 475 * Run execution loop listeners on code to be executed.
Chris@13 476 *
Chris@13 477 * @param string $code
Chris@13 478 *
Chris@13 479 * @return string
Chris@13 480 */
Chris@13 481 public function onExecute($code)
Chris@13 482 {
Chris@13 483 foreach ($this->loopListeners as $listener) {
Chris@13 484 if (($return = $listener->onExecute($this, $code)) !== null) {
Chris@13 485 $code = $return;
Chris@13 486 }
Chris@13 487 }
Chris@13 488
Chris@13 489 return $code;
Chris@13 490 }
Chris@13 491
Chris@13 492 /**
Chris@13 493 * Run execution loop listeners after each loop.
Chris@13 494 */
Chris@13 495 public function afterLoop()
Chris@13 496 {
Chris@13 497 foreach ($this->loopListeners as $listener) {
Chris@13 498 $listener->afterLoop($this);
Chris@13 499 }
Chris@13 500 }
Chris@13 501
Chris@13 502 /**
Chris@13 503 * Run execution loop listers after the shell session.
Chris@13 504 */
Chris@13 505 protected function afterRun()
Chris@13 506 {
Chris@13 507 foreach ($this->loopListeners as $listener) {
Chris@13 508 $listener->afterRun($this);
Chris@13 509 }
Chris@13 510 }
Chris@13 511
Chris@13 512 /**
Chris@13 513 * Set the variables currently in scope.
Chris@13 514 *
Chris@13 515 * @param array $vars
Chris@13 516 */
Chris@13 517 public function setScopeVariables(array $vars)
Chris@13 518 {
Chris@13 519 $this->context->setAll($vars);
Chris@13 520 }
Chris@13 521
Chris@13 522 /**
Chris@13 523 * Return the set of variables currently in scope.
Chris@13 524 *
Chris@13 525 * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
Chris@13 526 * passing the scope variables to `extract`
Chris@13 527 * in PHP 7.1+, you _must_ exclude 'this'
Chris@13 528 *
Chris@13 529 * @return array Associative array of scope variables
Chris@13 530 */
Chris@13 531 public function getScopeVariables($includeBoundObject = true)
Chris@13 532 {
Chris@13 533 $vars = $this->context->getAll();
Chris@13 534
Chris@13 535 if (!$includeBoundObject) {
Chris@13 536 unset($vars['this']);
Chris@13 537 }
Chris@13 538
Chris@13 539 return $vars;
Chris@13 540 }
Chris@13 541
Chris@13 542 /**
Chris@13 543 * Return the set of magic variables currently in scope.
Chris@13 544 *
Chris@13 545 * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
Chris@13 546 * passing the scope variables to `extract`
Chris@13 547 * in PHP 7.1+, you _must_ exclude 'this'
Chris@13 548 *
Chris@13 549 * @return array Associative array of magic scope variables
Chris@13 550 */
Chris@13 551 public function getSpecialScopeVariables($includeBoundObject = true)
Chris@13 552 {
Chris@13 553 $vars = $this->context->getSpecialVariables();
Chris@13 554
Chris@13 555 if (!$includeBoundObject) {
Chris@13 556 unset($vars['this']);
Chris@13 557 }
Chris@13 558
Chris@13 559 return $vars;
Chris@13 560 }
Chris@13 561
Chris@13 562 /**
Chris@17 563 * Return the set of variables currently in scope which differ from the
Chris@17 564 * values passed as $currentVars.
Chris@17 565 *
Chris@17 566 * This is used inside the Execution Loop Closure to pick up scope variable
Chris@17 567 * changes made by commands while the loop is running.
Chris@17 568 *
Chris@17 569 * @param array $currentVars
Chris@17 570 *
Chris@17 571 * @return array Associative array of scope variables which differ from $currentVars
Chris@17 572 */
Chris@17 573 public function getScopeVariablesDiff(array $currentVars)
Chris@17 574 {
Chris@17 575 $newVars = [];
Chris@17 576
Chris@17 577 foreach ($this->getScopeVariables(false) as $key => $value) {
Chris@17 578 if (!array_key_exists($key, $currentVars) || $currentVars[$key] !== $value) {
Chris@17 579 $newVars[$key] = $value;
Chris@17 580 }
Chris@17 581 }
Chris@17 582
Chris@17 583 return $newVars;
Chris@17 584 }
Chris@17 585
Chris@17 586 /**
Chris@13 587 * Get the set of unused command-scope variable names.
Chris@13 588 *
Chris@13 589 * @return array Array of unused variable names
Chris@13 590 */
Chris@13 591 public function getUnusedCommandScopeVariableNames()
Chris@13 592 {
Chris@13 593 return $this->context->getUnusedCommandScopeVariableNames();
Chris@13 594 }
Chris@13 595
Chris@13 596 /**
Chris@13 597 * Get the set of variable names currently in scope.
Chris@13 598 *
Chris@13 599 * @return array Array of variable names
Chris@13 600 */
Chris@13 601 public function getScopeVariableNames()
Chris@13 602 {
Chris@17 603 return \array_keys($this->context->getAll());
Chris@13 604 }
Chris@13 605
Chris@13 606 /**
Chris@13 607 * Get a scope variable value by name.
Chris@13 608 *
Chris@13 609 * @param string $name
Chris@13 610 *
Chris@13 611 * @return mixed
Chris@13 612 */
Chris@13 613 public function getScopeVariable($name)
Chris@13 614 {
Chris@13 615 return $this->context->get($name);
Chris@13 616 }
Chris@13 617
Chris@13 618 /**
Chris@13 619 * Set the bound object ($this variable) for the interactive shell.
Chris@13 620 *
Chris@13 621 * @param object|null $boundObject
Chris@13 622 */
Chris@13 623 public function setBoundObject($boundObject)
Chris@13 624 {
Chris@13 625 $this->context->setBoundObject($boundObject);
Chris@13 626 }
Chris@13 627
Chris@13 628 /**
Chris@13 629 * Get the bound object ($this variable) for the interactive shell.
Chris@13 630 *
Chris@13 631 * @return object|null
Chris@13 632 */
Chris@13 633 public function getBoundObject()
Chris@13 634 {
Chris@13 635 return $this->context->getBoundObject();
Chris@13 636 }
Chris@13 637
Chris@13 638 /**
Chris@16 639 * Set the bound class (self) for the interactive shell.
Chris@16 640 *
Chris@16 641 * @param string|null $boundClass
Chris@16 642 */
Chris@16 643 public function setBoundClass($boundClass)
Chris@16 644 {
Chris@16 645 $this->context->setBoundClass($boundClass);
Chris@16 646 }
Chris@16 647
Chris@16 648 /**
Chris@16 649 * Get the bound class (self) for the interactive shell.
Chris@16 650 *
Chris@16 651 * @return string|null
Chris@16 652 */
Chris@16 653 public function getBoundClass()
Chris@16 654 {
Chris@16 655 return $this->context->getBoundClass();
Chris@16 656 }
Chris@16 657
Chris@16 658 /**
Chris@13 659 * Add includes, to be parsed and executed before running the interactive shell.
Chris@13 660 *
Chris@13 661 * @param array $includes
Chris@13 662 */
Chris@13 663 public function setIncludes(array $includes = [])
Chris@13 664 {
Chris@13 665 $this->includes = $includes;
Chris@13 666 }
Chris@13 667
Chris@13 668 /**
Chris@13 669 * Get PHP files to be parsed and executed before running the interactive shell.
Chris@13 670 *
Chris@13 671 * @return array
Chris@13 672 */
Chris@13 673 public function getIncludes()
Chris@13 674 {
Chris@17 675 return \array_merge($this->config->getDefaultIncludes(), $this->includes);
Chris@13 676 }
Chris@13 677
Chris@13 678 /**
Chris@13 679 * Check whether this shell's code buffer contains code.
Chris@13 680 *
Chris@13 681 * @return bool True if the code buffer contains code
Chris@13 682 */
Chris@13 683 public function hasCode()
Chris@13 684 {
Chris@13 685 return !empty($this->codeBuffer);
Chris@13 686 }
Chris@13 687
Chris@13 688 /**
Chris@13 689 * Check whether the code in this shell's code buffer is valid.
Chris@13 690 *
Chris@13 691 * If the code is valid, the code buffer should be flushed and evaluated.
Chris@13 692 *
Chris@13 693 * @return bool True if the code buffer content is valid
Chris@13 694 */
Chris@13 695 protected function hasValidCode()
Chris@13 696 {
Chris@13 697 return !$this->codeBufferOpen && $this->code !== false;
Chris@13 698 }
Chris@13 699
Chris@13 700 /**
Chris@13 701 * Add code to the code buffer.
Chris@13 702 *
Chris@13 703 * @param string $code
Chris@13 704 * @param bool $silent
Chris@13 705 */
Chris@13 706 public function addCode($code, $silent = false)
Chris@13 707 {
Chris@13 708 try {
Chris@13 709 // Code lines ending in \ keep the buffer open
Chris@17 710 if (\substr(\rtrim($code), -1) === '\\') {
Chris@13 711 $this->codeBufferOpen = true;
Chris@17 712 $code = \substr(\rtrim($code), 0, -1);
Chris@13 713 } else {
Chris@13 714 $this->codeBufferOpen = false;
Chris@13 715 }
Chris@13 716
Chris@13 717 $this->codeBuffer[] = $silent ? new SilentInput($code) : $code;
Chris@13 718 $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons());
Chris@13 719 } catch (\Exception $e) {
Chris@13 720 // Add failed code blocks to the readline history.
Chris@13 721 $this->addCodeBufferToHistory();
Chris@13 722
Chris@13 723 throw $e;
Chris@13 724 }
Chris@13 725 }
Chris@13 726
Chris@13 727 /**
Chris@13 728 * Set the code buffer.
Chris@13 729 *
Chris@13 730 * This is mostly used by `Shell::execute`. Any existing code in the input
Chris@13 731 * buffer is pushed onto a stack and will come back after this new code is
Chris@13 732 * executed.
Chris@13 733 *
Chris@13 734 * @throws \InvalidArgumentException if $code isn't a complete statement
Chris@13 735 *
Chris@13 736 * @param string $code
Chris@13 737 * @param bool $silent
Chris@13 738 */
Chris@13 739 private function setCode($code, $silent = false)
Chris@13 740 {
Chris@13 741 if ($this->hasCode()) {
Chris@13 742 $this->codeStack[] = [$this->codeBuffer, $this->codeBufferOpen, $this->code];
Chris@13 743 }
Chris@13 744
Chris@13 745 $this->resetCodeBuffer();
Chris@13 746 try {
Chris@13 747 $this->addCode($code, $silent);
Chris@13 748 } catch (\Throwable $e) {
Chris@13 749 $this->popCodeStack();
Chris@13 750
Chris@13 751 throw $e;
Chris@13 752 } catch (\Exception $e) {
Chris@13 753 $this->popCodeStack();
Chris@13 754
Chris@13 755 throw $e;
Chris@13 756 }
Chris@13 757
Chris@13 758 if (!$this->hasValidCode()) {
Chris@13 759 $this->popCodeStack();
Chris@13 760
Chris@13 761 throw new \InvalidArgumentException('Unexpected end of input');
Chris@13 762 }
Chris@13 763 }
Chris@13 764
Chris@13 765 /**
Chris@13 766 * Get the current code buffer.
Chris@13 767 *
Chris@13 768 * This is useful for commands which manipulate the buffer.
Chris@13 769 *
Chris@13 770 * @return array
Chris@13 771 */
Chris@13 772 public function getCodeBuffer()
Chris@13 773 {
Chris@13 774 return $this->codeBuffer;
Chris@13 775 }
Chris@13 776
Chris@13 777 /**
Chris@13 778 * Run a Psy Shell command given the user input.
Chris@13 779 *
Chris@13 780 * @throws InvalidArgumentException if the input is not a valid command
Chris@13 781 *
Chris@13 782 * @param string $input User input string
Chris@13 783 *
Chris@13 784 * @return mixed Who knows?
Chris@13 785 */
Chris@13 786 protected function runCommand($input)
Chris@13 787 {
Chris@13 788 $command = $this->getCommand($input);
Chris@13 789
Chris@13 790 if (empty($command)) {
Chris@13 791 throw new \InvalidArgumentException('Command not found: ' . $input);
Chris@13 792 }
Chris@13 793
Chris@17 794 $input = new ShellInput(\str_replace('\\', '\\\\', \rtrim($input, " \t\n\r\0\x0B;")));
Chris@13 795
Chris@13 796 if ($input->hasParameterOption(['--help', '-h'])) {
Chris@13 797 $helpCommand = $this->get('help');
Chris@13 798 $helpCommand->setCommand($command);
Chris@13 799
Chris@13 800 return $helpCommand->run($input, $this->output);
Chris@13 801 }
Chris@13 802
Chris@13 803 return $command->run($input, $this->output);
Chris@13 804 }
Chris@13 805
Chris@13 806 /**
Chris@13 807 * Reset the current code buffer.
Chris@13 808 *
Chris@13 809 * This should be run after evaluating user input, catching exceptions, or
Chris@13 810 * on demand by commands such as BufferCommand.
Chris@13 811 */
Chris@13 812 public function resetCodeBuffer()
Chris@13 813 {
Chris@13 814 $this->codeBuffer = [];
Chris@13 815 $this->code = false;
Chris@13 816 }
Chris@13 817
Chris@13 818 /**
Chris@13 819 * Inject input into the input buffer.
Chris@13 820 *
Chris@13 821 * This is useful for commands which want to replay history.
Chris@13 822 *
Chris@13 823 * @param string|array $input
Chris@13 824 * @param bool $silent
Chris@13 825 */
Chris@13 826 public function addInput($input, $silent = false)
Chris@13 827 {
Chris@13 828 foreach ((array) $input as $line) {
Chris@13 829 $this->inputBuffer[] = $silent ? new SilentInput($line) : $line;
Chris@13 830 }
Chris@13 831 }
Chris@13 832
Chris@13 833 /**
Chris@13 834 * Flush the current (valid) code buffer.
Chris@13 835 *
Chris@13 836 * If the code buffer is valid, resets the code buffer and returns the
Chris@13 837 * current code.
Chris@13 838 *
Chris@13 839 * @return string PHP code buffer contents
Chris@13 840 */
Chris@13 841 public function flushCode()
Chris@13 842 {
Chris@13 843 if ($this->hasValidCode()) {
Chris@13 844 $this->addCodeBufferToHistory();
Chris@13 845 $code = $this->code;
Chris@13 846 $this->popCodeStack();
Chris@13 847
Chris@13 848 return $code;
Chris@13 849 }
Chris@13 850 }
Chris@13 851
Chris@13 852 /**
Chris@13 853 * Reset the code buffer and restore any code pushed during `execute` calls.
Chris@13 854 */
Chris@13 855 private function popCodeStack()
Chris@13 856 {
Chris@13 857 $this->resetCodeBuffer();
Chris@13 858
Chris@13 859 if (empty($this->codeStack)) {
Chris@13 860 return;
Chris@13 861 }
Chris@13 862
Chris@17 863 list($codeBuffer, $codeBufferOpen, $code) = \array_pop($this->codeStack);
Chris@13 864
Chris@13 865 $this->codeBuffer = $codeBuffer;
Chris@13 866 $this->codeBufferOpen = $codeBufferOpen;
Chris@13 867 $this->code = $code;
Chris@13 868 }
Chris@13 869
Chris@13 870 /**
Chris@13 871 * (Possibly) add a line to the readline history.
Chris@13 872 *
Chris@13 873 * Like Bash, if the line starts with a space character, it will be omitted
Chris@13 874 * from history. Note that an entire block multi-line code input will be
Chris@13 875 * omitted iff the first line begins with a space.
Chris@13 876 *
Chris@13 877 * Additionally, if a line is "silent", i.e. it was initially added with the
Chris@13 878 * silent flag, it will also be omitted.
Chris@13 879 *
Chris@13 880 * @param string|SilentInput $line
Chris@13 881 */
Chris@13 882 private function addHistory($line)
Chris@13 883 {
Chris@13 884 if ($line instanceof SilentInput) {
Chris@13 885 return;
Chris@13 886 }
Chris@13 887
Chris@13 888 // Skip empty lines and lines starting with a space
Chris@17 889 if (\trim($line) !== '' && \substr($line, 0, 1) !== ' ') {
Chris@13 890 $this->readline->addHistory($line);
Chris@13 891 }
Chris@13 892 }
Chris@13 893
Chris@13 894 /**
Chris@13 895 * Filter silent input from code buffer, write the rest to readline history.
Chris@13 896 */
Chris@13 897 private function addCodeBufferToHistory()
Chris@13 898 {
Chris@17 899 $codeBuffer = \array_filter($this->codeBuffer, function ($line) {
Chris@13 900 return !$line instanceof SilentInput;
Chris@13 901 });
Chris@13 902
Chris@17 903 $this->addHistory(\implode("\n", $codeBuffer));
Chris@13 904 }
Chris@13 905
Chris@13 906 /**
Chris@13 907 * Get the current evaluation scope namespace.
Chris@13 908 *
Chris@13 909 * @see CodeCleaner::getNamespace
Chris@13 910 *
Chris@13 911 * @return string Current code namespace
Chris@13 912 */
Chris@13 913 public function getNamespace()
Chris@13 914 {
Chris@13 915 if ($namespace = $this->cleaner->getNamespace()) {
Chris@17 916 return \implode('\\', $namespace);
Chris@13 917 }
Chris@13 918 }
Chris@13 919
Chris@13 920 /**
Chris@13 921 * Write a string to stdout.
Chris@13 922 *
Chris@13 923 * This is used by the shell loop for rendering output from evaluated code.
Chris@13 924 *
Chris@13 925 * @param string $out
Chris@13 926 * @param int $phase Output buffering phase
Chris@13 927 */
Chris@13 928 public function writeStdout($out, $phase = PHP_OUTPUT_HANDLER_END)
Chris@13 929 {
Chris@13 930 $isCleaning = $phase & PHP_OUTPUT_HANDLER_CLEAN;
Chris@13 931
Chris@13 932 // Incremental flush
Chris@13 933 if ($out !== '' && !$isCleaning) {
Chris@13 934 $this->output->write($out, false, ShellOutput::OUTPUT_RAW);
Chris@17 935 $this->outputWantsNewline = (\substr($out, -1) !== "\n");
Chris@13 936 $this->stdoutBuffer .= $out;
Chris@13 937 }
Chris@13 938
Chris@13 939 // Output buffering is done!
Chris@13 940 if ($phase & PHP_OUTPUT_HANDLER_END) {
Chris@13 941 // Write an extra newline if stdout didn't end with one
Chris@13 942 if ($this->outputWantsNewline) {
Chris@17 943 $this->output->writeln(\sprintf('<aside>%s</aside>', $this->config->useUnicode() ? '⏎' : '\\n'));
Chris@13 944 $this->outputWantsNewline = false;
Chris@13 945 }
Chris@13 946
Chris@13 947 // Save the stdout buffer as $__out
Chris@13 948 if ($this->stdoutBuffer !== '') {
Chris@13 949 $this->context->setLastStdout($this->stdoutBuffer);
Chris@13 950 $this->stdoutBuffer = '';
Chris@13 951 }
Chris@13 952 }
Chris@13 953 }
Chris@13 954
Chris@13 955 /**
Chris@13 956 * Write a return value to stdout.
Chris@13 957 *
Chris@13 958 * The return value is formatted or pretty-printed, and rendered in a
Chris@13 959 * visibly distinct manner (in this case, as cyan).
Chris@13 960 *
Chris@13 961 * @see self::presentValue
Chris@13 962 *
Chris@13 963 * @param mixed $ret
Chris@13 964 */
Chris@13 965 public function writeReturnValue($ret)
Chris@13 966 {
Chris@17 967 $this->lastExecSuccess = true;
Chris@17 968
Chris@13 969 if ($ret instanceof NoReturnValue) {
Chris@13 970 return;
Chris@13 971 }
Chris@13 972
Chris@13 973 $this->context->setReturnValue($ret);
Chris@13 974 $ret = $this->presentValue($ret);
Chris@17 975 $indent = \str_repeat(' ', \strlen(static::RETVAL));
Chris@13 976
Chris@17 977 $this->output->writeln(static::RETVAL . \str_replace(PHP_EOL, PHP_EOL . $indent, $ret));
Chris@13 978 }
Chris@13 979
Chris@13 980 /**
Chris@13 981 * Renders a caught Exception.
Chris@13 982 *
Chris@13 983 * Exceptions are formatted according to severity. ErrorExceptions which were
Chris@13 984 * warnings or Strict errors aren't rendered as harshly as real errors.
Chris@13 985 *
Chris@13 986 * Stores $e as the last Exception in the Shell Context.
Chris@13 987 *
Chris@13 988 * @param \Exception $e An exception instance
Chris@13 989 */
Chris@13 990 public function writeException(\Exception $e)
Chris@13 991 {
Chris@17 992 $this->lastExecSuccess = false;
Chris@13 993 $this->context->setLastException($e);
Chris@13 994 $this->output->writeln($this->formatException($e));
Chris@13 995 $this->resetCodeBuffer();
Chris@13 996 }
Chris@13 997
Chris@13 998 /**
Chris@17 999 * Check whether the last exec was successful.
Chris@17 1000 *
Chris@17 1001 * Returns true if a return value was logged rather than an exception.
Chris@17 1002 *
Chris@17 1003 * @return bool
Chris@17 1004 */
Chris@17 1005 public function getLastExecSuccess()
Chris@17 1006 {
Chris@17 1007 return $this->lastExecSuccess;
Chris@17 1008 }
Chris@17 1009
Chris@17 1010 /**
Chris@13 1011 * Helper for formatting an exception for writeException().
Chris@13 1012 *
Chris@13 1013 * @todo extract this to somewhere it makes more sense
Chris@13 1014 *
Chris@13 1015 * @param \Exception $e
Chris@13 1016 *
Chris@13 1017 * @return string
Chris@13 1018 */
Chris@13 1019 public function formatException(\Exception $e)
Chris@13 1020 {
Chris@13 1021 $message = $e->getMessage();
Chris@13 1022 if (!$e instanceof PsyException) {
Chris@13 1023 if ($message === '') {
Chris@17 1024 $message = \get_class($e);
Chris@13 1025 } else {
Chris@17 1026 $message = \sprintf('%s with message \'%s\'', \get_class($e), $message);
Chris@13 1027 }
Chris@13 1028 }
Chris@13 1029
Chris@17 1030 $message = \preg_replace(
Chris@16 1031 "#(\\w:)?(/\\w+)*/src/Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code#",
Chris@13 1032 "eval()'d code",
Chris@17 1033 \str_replace('\\', '/', $message)
Chris@13 1034 );
Chris@13 1035
Chris@17 1036 $message = \str_replace(" in eval()'d code", ' in Psy Shell code', $message);
Chris@13 1037
Chris@13 1038 $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error';
Chris@13 1039
Chris@17 1040 return \sprintf('<%s>%s</%s>', $severity, OutputFormatter::escape($message), $severity);
Chris@13 1041 }
Chris@13 1042
Chris@13 1043 /**
Chris@13 1044 * Helper for getting an output style for the given ErrorException's level.
Chris@13 1045 *
Chris@13 1046 * @param \ErrorException $e
Chris@13 1047 *
Chris@13 1048 * @return string
Chris@13 1049 */
Chris@13 1050 protected function getSeverity(\ErrorException $e)
Chris@13 1051 {
Chris@13 1052 $severity = $e->getSeverity();
Chris@17 1053 if ($severity & \error_reporting()) {
Chris@13 1054 switch ($severity) {
Chris@13 1055 case E_WARNING:
Chris@13 1056 case E_NOTICE:
Chris@13 1057 case E_CORE_WARNING:
Chris@13 1058 case E_COMPILE_WARNING:
Chris@13 1059 case E_USER_WARNING:
Chris@13 1060 case E_USER_NOTICE:
Chris@13 1061 case E_STRICT:
Chris@13 1062 return 'warning';
Chris@13 1063
Chris@13 1064 default:
Chris@13 1065 return 'error';
Chris@13 1066 }
Chris@13 1067 } else {
Chris@13 1068 // Since this is below the user's reporting threshold, it's always going to be a warning.
Chris@13 1069 return 'warning';
Chris@13 1070 }
Chris@13 1071 }
Chris@13 1072
Chris@13 1073 /**
Chris@13 1074 * Execute code in the shell execution context.
Chris@13 1075 *
Chris@13 1076 * @param string $code
Chris@13 1077 * @param bool $throwExceptions
Chris@13 1078 *
Chris@13 1079 * @return mixed
Chris@13 1080 */
Chris@13 1081 public function execute($code, $throwExceptions = false)
Chris@13 1082 {
Chris@13 1083 $this->setCode($code, true);
Chris@13 1084 $closure = new ExecutionClosure($this);
Chris@13 1085
Chris@13 1086 if ($throwExceptions) {
Chris@13 1087 return $closure->execute();
Chris@13 1088 }
Chris@13 1089
Chris@13 1090 try {
Chris@13 1091 return $closure->execute();
Chris@13 1092 } catch (\TypeError $_e) {
Chris@13 1093 $this->writeException(TypeErrorException::fromTypeError($_e));
Chris@13 1094 } catch (\Error $_e) {
Chris@13 1095 $this->writeException(ErrorException::fromError($_e));
Chris@13 1096 } catch (\Exception $_e) {
Chris@13 1097 $this->writeException($_e);
Chris@13 1098 }
Chris@13 1099 }
Chris@13 1100
Chris@13 1101 /**
Chris@13 1102 * Helper for throwing an ErrorException.
Chris@13 1103 *
Chris@13 1104 * This allows us to:
Chris@13 1105 *
Chris@13 1106 * set_error_handler(array($psysh, 'handleError'));
Chris@13 1107 *
Chris@13 1108 * Unlike ErrorException::throwException, this error handler respects the
Chris@13 1109 * current error_reporting level; i.e. it logs warnings and notices, but
Chris@13 1110 * doesn't throw an exception unless it's above the current error_reporting
Chris@13 1111 * threshold. This should probably only be used in the inner execution loop
Chris@13 1112 * of the shell, as most of the time a thrown exception is much more useful.
Chris@13 1113 *
Chris@13 1114 * If the error type matches the `errorLoggingLevel` config, it will be
Chris@13 1115 * logged as well, regardless of the `error_reporting` level.
Chris@13 1116 *
Chris@13 1117 * @see \Psy\Exception\ErrorException::throwException
Chris@13 1118 * @see \Psy\Shell::writeException
Chris@13 1119 *
Chris@13 1120 * @throws \Psy\Exception\ErrorException depending on the current error_reporting level
Chris@13 1121 *
Chris@13 1122 * @param int $errno Error type
Chris@13 1123 * @param string $errstr Message
Chris@13 1124 * @param string $errfile Filename
Chris@13 1125 * @param int $errline Line number
Chris@13 1126 */
Chris@13 1127 public function handleError($errno, $errstr, $errfile, $errline)
Chris@13 1128 {
Chris@17 1129 if ($errno & \error_reporting()) {
Chris@13 1130 ErrorException::throwException($errno, $errstr, $errfile, $errline);
Chris@13 1131 } elseif ($errno & $this->config->errorLoggingLevel()) {
Chris@13 1132 // log it and continue...
Chris@13 1133 $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline));
Chris@13 1134 }
Chris@13 1135 }
Chris@13 1136
Chris@13 1137 /**
Chris@13 1138 * Format a value for display.
Chris@13 1139 *
Chris@13 1140 * @see Presenter::present
Chris@13 1141 *
Chris@13 1142 * @param mixed $val
Chris@13 1143 *
Chris@13 1144 * @return string Formatted value
Chris@13 1145 */
Chris@13 1146 protected function presentValue($val)
Chris@13 1147 {
Chris@13 1148 return $this->config->getPresenter()->present($val);
Chris@13 1149 }
Chris@13 1150
Chris@13 1151 /**
Chris@13 1152 * Get a command (if one exists) for the current input string.
Chris@13 1153 *
Chris@13 1154 * @param string $input
Chris@13 1155 *
Chris@13 1156 * @return null|BaseCommand
Chris@13 1157 */
Chris@13 1158 protected function getCommand($input)
Chris@13 1159 {
Chris@13 1160 $input = new StringInput($input);
Chris@13 1161 if ($name = $input->getFirstArgument()) {
Chris@13 1162 return $this->get($name);
Chris@13 1163 }
Chris@13 1164 }
Chris@13 1165
Chris@13 1166 /**
Chris@13 1167 * Check whether a command is set for the current input string.
Chris@13 1168 *
Chris@13 1169 * @param string $input
Chris@13 1170 *
Chris@13 1171 * @return bool True if the shell has a command for the given input
Chris@13 1172 */
Chris@13 1173 protected function hasCommand($input)
Chris@13 1174 {
Chris@17 1175 if (\preg_match('/([^\s]+?)(?:\s|$)/A', \ltrim($input), $match)) {
Chris@16 1176 return $this->has($match[1]);
Chris@13 1177 }
Chris@13 1178
Chris@13 1179 return false;
Chris@13 1180 }
Chris@13 1181
Chris@13 1182 /**
Chris@13 1183 * Get the current input prompt.
Chris@13 1184 *
Chris@13 1185 * @return string
Chris@13 1186 */
Chris@13 1187 protected function getPrompt()
Chris@13 1188 {
Chris@13 1189 if ($this->hasCode()) {
Chris@13 1190 return static::BUFF_PROMPT;
Chris@13 1191 }
Chris@13 1192
Chris@13 1193 return $this->config->getPrompt() ?: static::PROMPT;
Chris@13 1194 }
Chris@13 1195
Chris@13 1196 /**
Chris@13 1197 * Read a line of user input.
Chris@13 1198 *
Chris@13 1199 * This will return a line from the input buffer (if any exist). Otherwise,
Chris@13 1200 * it will ask the user for input.
Chris@13 1201 *
Chris@13 1202 * If readline is enabled, this delegates to readline. Otherwise, it's an
Chris@13 1203 * ugly `fgets` call.
Chris@13 1204 *
Chris@13 1205 * @return string One line of user input
Chris@13 1206 */
Chris@13 1207 protected function readline()
Chris@13 1208 {
Chris@13 1209 if (!empty($this->inputBuffer)) {
Chris@17 1210 $line = \array_shift($this->inputBuffer);
Chris@13 1211 if (!$line instanceof SilentInput) {
Chris@17 1212 $this->output->writeln(\sprintf('<aside>%s %s</aside>', static::REPLAY, OutputFormatter::escape($line)));
Chris@13 1213 }
Chris@13 1214
Chris@13 1215 return $line;
Chris@13 1216 }
Chris@13 1217
Chris@13 1218 if ($bracketedPaste = $this->config->useBracketedPaste()) {
Chris@17 1219 \printf("\e[?2004h"); // Enable bracketed paste
Chris@13 1220 }
Chris@13 1221
Chris@13 1222 $line = $this->readline->readline($this->getPrompt());
Chris@13 1223
Chris@13 1224 if ($bracketedPaste) {
Chris@17 1225 \printf("\e[?2004l"); // ... and disable it again
Chris@13 1226 }
Chris@13 1227
Chris@13 1228 return $line;
Chris@13 1229 }
Chris@13 1230
Chris@13 1231 /**
Chris@13 1232 * Get the shell output header.
Chris@13 1233 *
Chris@13 1234 * @return string
Chris@13 1235 */
Chris@13 1236 protected function getHeader()
Chris@13 1237 {
Chris@17 1238 return \sprintf('<aside>%s by Justin Hileman</aside>', $this->getVersion());
Chris@13 1239 }
Chris@13 1240
Chris@13 1241 /**
Chris@13 1242 * Get the current version of Psy Shell.
Chris@13 1243 *
Chris@13 1244 * @return string
Chris@13 1245 */
Chris@13 1246 public function getVersion()
Chris@13 1247 {
Chris@13 1248 $separator = $this->config->useUnicode() ? '—' : '-';
Chris@13 1249
Chris@17 1250 return \sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, PHP_VERSION, $separator, PHP_SAPI);
Chris@13 1251 }
Chris@13 1252
Chris@13 1253 /**
Chris@13 1254 * Get a PHP manual database instance.
Chris@13 1255 *
Chris@13 1256 * @return \PDO|null
Chris@13 1257 */
Chris@13 1258 public function getManualDb()
Chris@13 1259 {
Chris@13 1260 return $this->config->getManualDb();
Chris@13 1261 }
Chris@13 1262
Chris@13 1263 /**
Chris@13 1264 * @deprecated Tab completion is provided by the AutoCompleter service
Chris@13 1265 */
Chris@13 1266 protected function autocomplete($text)
Chris@13 1267 {
Chris@17 1268 @\trigger_error('Tab completion is provided by the AutoCompleter service', E_USER_DEPRECATED);
Chris@13 1269 }
Chris@13 1270
Chris@13 1271 /**
Chris@13 1272 * Initialize tab completion matchers.
Chris@13 1273 *
Chris@13 1274 * If tab completion is enabled this adds tab completion matchers to the
Chris@13 1275 * auto completer and sets context if needed.
Chris@13 1276 */
Chris@13 1277 protected function initializeTabCompletion()
Chris@13 1278 {
Chris@13 1279 if (!$this->config->useTabCompletion()) {
Chris@13 1280 return;
Chris@13 1281 }
Chris@13 1282
Chris@13 1283 $this->autoCompleter = $this->config->getAutoCompleter();
Chris@13 1284
Chris@13 1285 // auto completer needs shell to be linked to configuration because of
Chris@13 1286 // the context aware matchers
Chris@13 1287 $this->addMatchersToAutoCompleter($this->getDefaultMatchers());
Chris@13 1288 $this->addMatchersToAutoCompleter($this->matchers);
Chris@13 1289
Chris@13 1290 $this->autoCompleter->activate();
Chris@13 1291 }
Chris@13 1292
Chris@13 1293 /**
Chris@13 1294 * Add matchers to the auto completer, setting context if needed.
Chris@13 1295 *
Chris@13 1296 * @param array $matchers
Chris@13 1297 */
Chris@13 1298 private function addMatchersToAutoCompleter(array $matchers)
Chris@13 1299 {
Chris@13 1300 foreach ($matchers as $matcher) {
Chris@13 1301 if ($matcher instanceof ContextAware) {
Chris@13 1302 $matcher->setContext($this->context);
Chris@13 1303 }
Chris@13 1304 $this->autoCompleter->addMatcher($matcher);
Chris@13 1305 }
Chris@13 1306 }
Chris@13 1307
Chris@13 1308 /**
Chris@13 1309 * @todo Implement self-update
Chris@13 1310 * @todo Implement prompt to start update
Chris@13 1311 *
Chris@13 1312 * @return void|string
Chris@13 1313 */
Chris@13 1314 protected function writeVersionInfo()
Chris@13 1315 {
Chris@13 1316 if (PHP_SAPI !== 'cli') {
Chris@13 1317 return;
Chris@13 1318 }
Chris@13 1319
Chris@13 1320 try {
Chris@13 1321 $client = $this->config->getChecker();
Chris@13 1322 if (!$client->isLatest()) {
Chris@17 1323 $this->output->writeln(\sprintf('New version is available (current: %s, latest: %s)', self::VERSION, $client->getLatest()));
Chris@13 1324 }
Chris@13 1325 } catch (\InvalidArgumentException $e) {
Chris@13 1326 $this->output->writeln($e->getMessage());
Chris@13 1327 }
Chris@13 1328 }
Chris@13 1329
Chris@13 1330 /**
Chris@13 1331 * Write a startup message if set.
Chris@13 1332 */
Chris@13 1333 protected function writeStartupMessage()
Chris@13 1334 {
Chris@13 1335 $message = $this->config->getStartupMessage();
Chris@13 1336 if ($message !== null && $message !== '') {
Chris@13 1337 $this->output->writeln($message);
Chris@13 1338 }
Chris@13 1339 }
Chris@13 1340 }