comparison vendor/psy/psysh/src/Shell.php @ 13:5fb285c0d0e3

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