Updated from some -dev modules to alpha, beta or full releases
[yaffs-website] / vendor / psy / psysh / src / Shell.php
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.4';
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|string $bindTo Bound object ($this) or class (self) value for the shell
123      *
124      * @return array Scope variables from the debugger session
125      */
126     public static function debug(array $vars = [], $bindTo = null)
127     {
128         return \Psy\debug($vars, $bindTo);
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) === '' && !$this->codeBufferOpen) {
396                 continue;
397             }
398
399             $input = $this->onInput($input);
400
401             // If the input isn't in an open string or comment, check for commands to run.
402             if ($this->hasCommand($input) && !$this->inputInOpenStringOrComment($input)) {
403                 $this->addHistory($input);
404                 $this->runCommand($input);
405
406                 continue;
407             }
408
409             $this->addCode($input);
410         } while (!$this->hasValidCode());
411     }
412
413     /**
414      * Check whether the code buffer (plus current input) is in an open string or comment.
415      *
416      * @param string $input current line of input
417      *
418      * @return bool true if the input is in an open string or comment
419      */
420     private function inputInOpenStringOrComment($input)
421     {
422         if (!$this->hasCode()) {
423             return;
424         }
425
426         $code = $this->codeBuffer;
427         array_push($code, $input);
428         $tokens = @token_get_all('<?php ' . implode("\n", $code));
429         $last = array_pop($tokens);
430
431         return $last === '"' || $last === '`' ||
432             (is_array($last) && in_array($last[0], [T_ENCAPSED_AND_WHITESPACE, T_START_HEREDOC, T_COMMENT]));
433     }
434
435     /**
436      * Run execution loop listeners before the shell session.
437      */
438     protected function beforeRun()
439     {
440         foreach ($this->loopListeners as $listener) {
441             $listener->beforeRun($this);
442         }
443     }
444
445     /**
446      * Run execution loop listeners at the start of each loop.
447      */
448     public function beforeLoop()
449     {
450         foreach ($this->loopListeners as $listener) {
451             $listener->beforeLoop($this);
452         }
453     }
454
455     /**
456      * Run execution loop listeners on user input.
457      *
458      * @param string $input
459      *
460      * @return string
461      */
462     public function onInput($input)
463     {
464         foreach ($this->loopListeners as $listeners) {
465             if (($return = $listeners->onInput($this, $input)) !== null) {
466                 $input = $return;
467             }
468         }
469
470         return $input;
471     }
472
473     /**
474      * Run execution loop listeners on code to be executed.
475      *
476      * @param string $code
477      *
478      * @return string
479      */
480     public function onExecute($code)
481     {
482         foreach ($this->loopListeners as $listener) {
483             if (($return = $listener->onExecute($this, $code)) !== null) {
484                 $code = $return;
485             }
486         }
487
488         return $code;
489     }
490
491     /**
492      * Run execution loop listeners after each loop.
493      */
494     public function afterLoop()
495     {
496         foreach ($this->loopListeners as $listener) {
497             $listener->afterLoop($this);
498         }
499     }
500
501     /**
502      * Run execution loop listers after the shell session.
503      */
504     protected function afterRun()
505     {
506         foreach ($this->loopListeners as $listener) {
507             $listener->afterRun($this);
508         }
509     }
510
511     /**
512      * Set the variables currently in scope.
513      *
514      * @param array $vars
515      */
516     public function setScopeVariables(array $vars)
517     {
518         $this->context->setAll($vars);
519     }
520
521     /**
522      * Return the set of variables currently in scope.
523      *
524      * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
525      *                                 passing the scope variables to `extract`
526      *                                 in PHP 7.1+, you _must_ exclude 'this'
527      *
528      * @return array Associative array of scope variables
529      */
530     public function getScopeVariables($includeBoundObject = true)
531     {
532         $vars = $this->context->getAll();
533
534         if (!$includeBoundObject) {
535             unset($vars['this']);
536         }
537
538         return $vars;
539     }
540
541     /**
542      * Return the set of magic variables currently in scope.
543      *
544      * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
545      *                                 passing the scope variables to `extract`
546      *                                 in PHP 7.1+, you _must_ exclude 'this'
547      *
548      * @return array Associative array of magic scope variables
549      */
550     public function getSpecialScopeVariables($includeBoundObject = true)
551     {
552         $vars = $this->context->getSpecialVariables();
553
554         if (!$includeBoundObject) {
555             unset($vars['this']);
556         }
557
558         return $vars;
559     }
560
561     /**
562      * Get the set of unused command-scope variable names.
563      *
564      * @return array Array of unused variable names
565      */
566     public function getUnusedCommandScopeVariableNames()
567     {
568         return $this->context->getUnusedCommandScopeVariableNames();
569     }
570
571     /**
572      * Get the set of variable names currently in scope.
573      *
574      * @return array Array of variable names
575      */
576     public function getScopeVariableNames()
577     {
578         return array_keys($this->context->getAll());
579     }
580
581     /**
582      * Get a scope variable value by name.
583      *
584      * @param string $name
585      *
586      * @return mixed
587      */
588     public function getScopeVariable($name)
589     {
590         return $this->context->get($name);
591     }
592
593     /**
594      * Set the bound object ($this variable) for the interactive shell.
595      *
596      * @param object|null $boundObject
597      */
598     public function setBoundObject($boundObject)
599     {
600         $this->context->setBoundObject($boundObject);
601     }
602
603     /**
604      * Get the bound object ($this variable) for the interactive shell.
605      *
606      * @return object|null
607      */
608     public function getBoundObject()
609     {
610         return $this->context->getBoundObject();
611     }
612
613     /**
614      * Set the bound class (self) for the interactive shell.
615      *
616      * @param string|null $boundClass
617      */
618     public function setBoundClass($boundClass)
619     {
620         $this->context->setBoundClass($boundClass);
621     }
622
623     /**
624      * Get the bound class (self) for the interactive shell.
625      *
626      * @return string|null
627      */
628     public function getBoundClass()
629     {
630         return $this->context->getBoundClass();
631     }
632
633     /**
634      * Add includes, to be parsed and executed before running the interactive shell.
635      *
636      * @param array $includes
637      */
638     public function setIncludes(array $includes = [])
639     {
640         $this->includes = $includes;
641     }
642
643     /**
644      * Get PHP files to be parsed and executed before running the interactive shell.
645      *
646      * @return array
647      */
648     public function getIncludes()
649     {
650         return array_merge($this->config->getDefaultIncludes(), $this->includes);
651     }
652
653     /**
654      * Check whether this shell's code buffer contains code.
655      *
656      * @return bool True if the code buffer contains code
657      */
658     public function hasCode()
659     {
660         return !empty($this->codeBuffer);
661     }
662
663     /**
664      * Check whether the code in this shell's code buffer is valid.
665      *
666      * If the code is valid, the code buffer should be flushed and evaluated.
667      *
668      * @return bool True if the code buffer content is valid
669      */
670     protected function hasValidCode()
671     {
672         return !$this->codeBufferOpen && $this->code !== false;
673     }
674
675     /**
676      * Add code to the code buffer.
677      *
678      * @param string $code
679      * @param bool   $silent
680      */
681     public function addCode($code, $silent = false)
682     {
683         try {
684             // Code lines ending in \ keep the buffer open
685             if (substr(rtrim($code), -1) === '\\') {
686                 $this->codeBufferOpen = true;
687                 $code = substr(rtrim($code), 0, -1);
688             } else {
689                 $this->codeBufferOpen = false;
690             }
691
692             $this->codeBuffer[] = $silent ? new SilentInput($code) : $code;
693             $this->code         = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons());
694         } catch (\Exception $e) {
695             // Add failed code blocks to the readline history.
696             $this->addCodeBufferToHistory();
697
698             throw $e;
699         }
700     }
701
702     /**
703      * Set the code buffer.
704      *
705      * This is mostly used by `Shell::execute`. Any existing code in the input
706      * buffer is pushed onto a stack and will come back after this new code is
707      * executed.
708      *
709      * @throws \InvalidArgumentException if $code isn't a complete statement
710      *
711      * @param string $code
712      * @param bool   $silent
713      */
714     private function setCode($code, $silent = false)
715     {
716         if ($this->hasCode()) {
717             $this->codeStack[] = [$this->codeBuffer, $this->codeBufferOpen, $this->code];
718         }
719
720         $this->resetCodeBuffer();
721         try {
722             $this->addCode($code, $silent);
723         } catch (\Throwable $e) {
724             $this->popCodeStack();
725
726             throw $e;
727         } catch (\Exception $e) {
728             $this->popCodeStack();
729
730             throw $e;
731         }
732
733         if (!$this->hasValidCode()) {
734             $this->popCodeStack();
735
736             throw new \InvalidArgumentException('Unexpected end of input');
737         }
738     }
739
740     /**
741      * Get the current code buffer.
742      *
743      * This is useful for commands which manipulate the buffer.
744      *
745      * @return array
746      */
747     public function getCodeBuffer()
748     {
749         return $this->codeBuffer;
750     }
751
752     /**
753      * Run a Psy Shell command given the user input.
754      *
755      * @throws InvalidArgumentException if the input is not a valid command
756      *
757      * @param string $input User input string
758      *
759      * @return mixed Who knows?
760      */
761     protected function runCommand($input)
762     {
763         $command = $this->getCommand($input);
764
765         if (empty($command)) {
766             throw new \InvalidArgumentException('Command not found: ' . $input);
767         }
768
769         $input = new ShellInput(str_replace('\\', '\\\\', rtrim($input, " \t\n\r\0\x0B;")));
770
771         if ($input->hasParameterOption(['--help', '-h'])) {
772             $helpCommand = $this->get('help');
773             $helpCommand->setCommand($command);
774
775             return $helpCommand->run($input, $this->output);
776         }
777
778         return $command->run($input, $this->output);
779     }
780
781     /**
782      * Reset the current code buffer.
783      *
784      * This should be run after evaluating user input, catching exceptions, or
785      * on demand by commands such as BufferCommand.
786      */
787     public function resetCodeBuffer()
788     {
789         $this->codeBuffer = [];
790         $this->code       = false;
791     }
792
793     /**
794      * Inject input into the input buffer.
795      *
796      * This is useful for commands which want to replay history.
797      *
798      * @param string|array $input
799      * @param bool         $silent
800      */
801     public function addInput($input, $silent = false)
802     {
803         foreach ((array) $input as $line) {
804             $this->inputBuffer[] = $silent ? new SilentInput($line) : $line;
805         }
806     }
807
808     /**
809      * Flush the current (valid) code buffer.
810      *
811      * If the code buffer is valid, resets the code buffer and returns the
812      * current code.
813      *
814      * @return string PHP code buffer contents
815      */
816     public function flushCode()
817     {
818         if ($this->hasValidCode()) {
819             $this->addCodeBufferToHistory();
820             $code = $this->code;
821             $this->popCodeStack();
822
823             return $code;
824         }
825     }
826
827     /**
828      * Reset the code buffer and restore any code pushed during `execute` calls.
829      */
830     private function popCodeStack()
831     {
832         $this->resetCodeBuffer();
833
834         if (empty($this->codeStack)) {
835             return;
836         }
837
838         list($codeBuffer, $codeBufferOpen, $code) = array_pop($this->codeStack);
839
840         $this->codeBuffer     = $codeBuffer;
841         $this->codeBufferOpen = $codeBufferOpen;
842         $this->code           = $code;
843     }
844
845     /**
846      * (Possibly) add a line to the readline history.
847      *
848      * Like Bash, if the line starts with a space character, it will be omitted
849      * from history. Note that an entire block multi-line code input will be
850      * omitted iff the first line begins with a space.
851      *
852      * Additionally, if a line is "silent", i.e. it was initially added with the
853      * silent flag, it will also be omitted.
854      *
855      * @param string|SilentInput $line
856      */
857     private function addHistory($line)
858     {
859         if ($line instanceof SilentInput) {
860             return;
861         }
862
863         // Skip empty lines and lines starting with a space
864         if (trim($line) !== '' && substr($line, 0, 1) !== ' ') {
865             $this->readline->addHistory($line);
866         }
867     }
868
869     /**
870      * Filter silent input from code buffer, write the rest to readline history.
871      */
872     private function addCodeBufferToHistory()
873     {
874         $codeBuffer = array_filter($this->codeBuffer, function ($line) {
875             return !$line instanceof SilentInput;
876         });
877
878         $this->addHistory(implode("\n", $codeBuffer));
879     }
880
881     /**
882      * Get the current evaluation scope namespace.
883      *
884      * @see CodeCleaner::getNamespace
885      *
886      * @return string Current code namespace
887      */
888     public function getNamespace()
889     {
890         if ($namespace = $this->cleaner->getNamespace()) {
891             return implode('\\', $namespace);
892         }
893     }
894
895     /**
896      * Write a string to stdout.
897      *
898      * This is used by the shell loop for rendering output from evaluated code.
899      *
900      * @param string $out
901      * @param int    $phase Output buffering phase
902      */
903     public function writeStdout($out, $phase = PHP_OUTPUT_HANDLER_END)
904     {
905         $isCleaning = $phase & PHP_OUTPUT_HANDLER_CLEAN;
906
907         // Incremental flush
908         if ($out !== '' && !$isCleaning) {
909             $this->output->write($out, false, ShellOutput::OUTPUT_RAW);
910             $this->outputWantsNewline = (substr($out, -1) !== "\n");
911             $this->stdoutBuffer .= $out;
912         }
913
914         // Output buffering is done!
915         if ($phase & PHP_OUTPUT_HANDLER_END) {
916             // Write an extra newline if stdout didn't end with one
917             if ($this->outputWantsNewline) {
918                 $this->output->writeln(sprintf('<aside>%s</aside>', $this->config->useUnicode() ? '⏎' : '\\n'));
919                 $this->outputWantsNewline = false;
920             }
921
922             // Save the stdout buffer as $__out
923             if ($this->stdoutBuffer !== '') {
924                 $this->context->setLastStdout($this->stdoutBuffer);
925                 $this->stdoutBuffer = '';
926             }
927         }
928     }
929
930     /**
931      * Write a return value to stdout.
932      *
933      * The return value is formatted or pretty-printed, and rendered in a
934      * visibly distinct manner (in this case, as cyan).
935      *
936      * @see self::presentValue
937      *
938      * @param mixed $ret
939      */
940     public function writeReturnValue($ret)
941     {
942         if ($ret instanceof NoReturnValue) {
943             return;
944         }
945
946         $this->context->setReturnValue($ret);
947         $ret    = $this->presentValue($ret);
948         $indent = str_repeat(' ', strlen(static::RETVAL));
949
950         $this->output->writeln(static::RETVAL . str_replace(PHP_EOL, PHP_EOL . $indent, $ret));
951     }
952
953     /**
954      * Renders a caught Exception.
955      *
956      * Exceptions are formatted according to severity. ErrorExceptions which were
957      * warnings or Strict errors aren't rendered as harshly as real errors.
958      *
959      * Stores $e as the last Exception in the Shell Context.
960      *
961      * @param \Exception $e An exception instance
962      */
963     public function writeException(\Exception $e)
964     {
965         $this->context->setLastException($e);
966         $this->output->writeln($this->formatException($e));
967         $this->resetCodeBuffer();
968     }
969
970     /**
971      * Helper for formatting an exception for writeException().
972      *
973      * @todo extract this to somewhere it makes more sense
974      *
975      * @param \Exception $e
976      *
977      * @return string
978      */
979     public function formatException(\Exception $e)
980     {
981         $message = $e->getMessage();
982         if (!$e instanceof PsyException) {
983             if ($message === '') {
984                 $message = get_class($e);
985             } else {
986                 $message = sprintf('%s with message \'%s\'', get_class($e), $message);
987             }
988         }
989
990         $message = preg_replace(
991             "#(\\w:)?(/\\w+)*/src/Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code#",
992             "eval()'d code",
993             str_replace('\\', '/', $message)
994         );
995
996         $message = str_replace(" in eval()'d code", ' in Psy Shell code', $message);
997
998         $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error';
999
1000         return sprintf('<%s>%s</%s>', $severity, OutputFormatter::escape($message), $severity);
1001     }
1002
1003     /**
1004      * Helper for getting an output style for the given ErrorException's level.
1005      *
1006      * @param \ErrorException $e
1007      *
1008      * @return string
1009      */
1010     protected function getSeverity(\ErrorException $e)
1011     {
1012         $severity = $e->getSeverity();
1013         if ($severity & error_reporting()) {
1014             switch ($severity) {
1015                 case E_WARNING:
1016                 case E_NOTICE:
1017                 case E_CORE_WARNING:
1018                 case E_COMPILE_WARNING:
1019                 case E_USER_WARNING:
1020                 case E_USER_NOTICE:
1021                 case E_STRICT:
1022                     return 'warning';
1023
1024                 default:
1025                     return 'error';
1026             }
1027         } else {
1028             // Since this is below the user's reporting threshold, it's always going to be a warning.
1029             return 'warning';
1030         }
1031     }
1032
1033     /**
1034      * Execute code in the shell execution context.
1035      *
1036      * @param string $code
1037      * @param bool   $throwExceptions
1038      *
1039      * @return mixed
1040      */
1041     public function execute($code, $throwExceptions = false)
1042     {
1043         $this->setCode($code, true);
1044         $closure = new ExecutionClosure($this);
1045
1046         if ($throwExceptions) {
1047             return $closure->execute();
1048         }
1049
1050         try {
1051             return $closure->execute();
1052         } catch (\TypeError $_e) {
1053             $this->writeException(TypeErrorException::fromTypeError($_e));
1054         } catch (\Error $_e) {
1055             $this->writeException(ErrorException::fromError($_e));
1056         } catch (\Exception $_e) {
1057             $this->writeException($_e);
1058         }
1059     }
1060
1061     /**
1062      * Helper for throwing an ErrorException.
1063      *
1064      * This allows us to:
1065      *
1066      *     set_error_handler(array($psysh, 'handleError'));
1067      *
1068      * Unlike ErrorException::throwException, this error handler respects the
1069      * current error_reporting level; i.e. it logs warnings and notices, but
1070      * doesn't throw an exception unless it's above the current error_reporting
1071      * threshold. This should probably only be used in the inner execution loop
1072      * of the shell, as most of the time a thrown exception is much more useful.
1073      *
1074      * If the error type matches the `errorLoggingLevel` config, it will be
1075      * logged as well, regardless of the `error_reporting` level.
1076      *
1077      * @see \Psy\Exception\ErrorException::throwException
1078      * @see \Psy\Shell::writeException
1079      *
1080      * @throws \Psy\Exception\ErrorException depending on the current error_reporting level
1081      *
1082      * @param int    $errno   Error type
1083      * @param string $errstr  Message
1084      * @param string $errfile Filename
1085      * @param int    $errline Line number
1086      */
1087     public function handleError($errno, $errstr, $errfile, $errline)
1088     {
1089         if ($errno & error_reporting()) {
1090             ErrorException::throwException($errno, $errstr, $errfile, $errline);
1091         } elseif ($errno & $this->config->errorLoggingLevel()) {
1092             // log it and continue...
1093             $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline));
1094         }
1095     }
1096
1097     /**
1098      * Format a value for display.
1099      *
1100      * @see Presenter::present
1101      *
1102      * @param mixed $val
1103      *
1104      * @return string Formatted value
1105      */
1106     protected function presentValue($val)
1107     {
1108         return $this->config->getPresenter()->present($val);
1109     }
1110
1111     /**
1112      * Get a command (if one exists) for the current input string.
1113      *
1114      * @param string $input
1115      *
1116      * @return null|BaseCommand
1117      */
1118     protected function getCommand($input)
1119     {
1120         $input = new StringInput($input);
1121         if ($name = $input->getFirstArgument()) {
1122             return $this->get($name);
1123         }
1124     }
1125
1126     /**
1127      * Check whether a command is set for the current input string.
1128      *
1129      * @param string $input
1130      *
1131      * @return bool True if the shell has a command for the given input
1132      */
1133     protected function hasCommand($input)
1134     {
1135         if (preg_match('/([^\s]+?)(?:\s|$)/A', ltrim($input), $match)) {
1136             return $this->has($match[1]);
1137         }
1138
1139         return false;
1140     }
1141
1142     /**
1143      * Get the current input prompt.
1144      *
1145      * @return string
1146      */
1147     protected function getPrompt()
1148     {
1149         if ($this->hasCode()) {
1150             return static::BUFF_PROMPT;
1151         }
1152
1153         return $this->config->getPrompt() ?: static::PROMPT;
1154     }
1155
1156     /**
1157      * Read a line of user input.
1158      *
1159      * This will return a line from the input buffer (if any exist). Otherwise,
1160      * it will ask the user for input.
1161      *
1162      * If readline is enabled, this delegates to readline. Otherwise, it's an
1163      * ugly `fgets` call.
1164      *
1165      * @return string One line of user input
1166      */
1167     protected function readline()
1168     {
1169         if (!empty($this->inputBuffer)) {
1170             $line = array_shift($this->inputBuffer);
1171             if (!$line instanceof SilentInput) {
1172                 $this->output->writeln(sprintf('<aside>%s %s</aside>', static::REPLAY, OutputFormatter::escape($line)));
1173             }
1174
1175             return $line;
1176         }
1177
1178         if ($bracketedPaste = $this->config->useBracketedPaste()) {
1179             printf("\e[?2004h"); // Enable bracketed paste
1180         }
1181
1182         $line = $this->readline->readline($this->getPrompt());
1183
1184         if ($bracketedPaste) {
1185             printf("\e[?2004l"); // ... and disable it again
1186         }
1187
1188         return $line;
1189     }
1190
1191     /**
1192      * Get the shell output header.
1193      *
1194      * @return string
1195      */
1196     protected function getHeader()
1197     {
1198         return sprintf('<aside>%s by Justin Hileman</aside>', $this->getVersion());
1199     }
1200
1201     /**
1202      * Get the current version of Psy Shell.
1203      *
1204      * @return string
1205      */
1206     public function getVersion()
1207     {
1208         $separator = $this->config->useUnicode() ? '—' : '-';
1209
1210         return sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, phpversion(), $separator, php_sapi_name());
1211     }
1212
1213     /**
1214      * Get a PHP manual database instance.
1215      *
1216      * @return \PDO|null
1217      */
1218     public function getManualDb()
1219     {
1220         return $this->config->getManualDb();
1221     }
1222
1223     /**
1224      * @deprecated Tab completion is provided by the AutoCompleter service
1225      */
1226     protected function autocomplete($text)
1227     {
1228         @trigger_error('Tab completion is provided by the AutoCompleter service', E_USER_DEPRECATED);
1229     }
1230
1231     /**
1232      * Initialize tab completion matchers.
1233      *
1234      * If tab completion is enabled this adds tab completion matchers to the
1235      * auto completer and sets context if needed.
1236      */
1237     protected function initializeTabCompletion()
1238     {
1239         if (!$this->config->useTabCompletion()) {
1240             return;
1241         }
1242
1243         $this->autoCompleter = $this->config->getAutoCompleter();
1244
1245         // auto completer needs shell to be linked to configuration because of
1246         // the context aware matchers
1247         $this->addMatchersToAutoCompleter($this->getDefaultMatchers());
1248         $this->addMatchersToAutoCompleter($this->matchers);
1249
1250         $this->autoCompleter->activate();
1251     }
1252
1253     /**
1254      * Add matchers to the auto completer, setting context if needed.
1255      *
1256      * @param array $matchers
1257      */
1258     private function addMatchersToAutoCompleter(array $matchers)
1259     {
1260         foreach ($matchers as $matcher) {
1261             if ($matcher instanceof ContextAware) {
1262                 $matcher->setContext($this->context);
1263             }
1264             $this->autoCompleter->addMatcher($matcher);
1265         }
1266     }
1267
1268     /**
1269      * @todo Implement self-update
1270      * @todo Implement prompt to start update
1271      *
1272      * @return void|string
1273      */
1274     protected function writeVersionInfo()
1275     {
1276         if (PHP_SAPI !== 'cli') {
1277             return;
1278         }
1279
1280         try {
1281             $client = $this->config->getChecker();
1282             if (!$client->isLatest()) {
1283                 $this->output->writeln(sprintf('New version is available (current: %s, latest: %s)', self::VERSION, $client->getLatest()));
1284             }
1285         } catch (\InvalidArgumentException $e) {
1286             $this->output->writeln($e->getMessage());
1287         }
1288     }
1289
1290     /**
1291      * Write a startup message if set.
1292      */
1293     protected function writeStartupMessage()
1294     {
1295         $message = $this->config->getStartupMessage();
1296         if ($message !== null && $message !== '') {
1297             $this->output->writeln($message);
1298         }
1299     }
1300 }