Mercurial > hg > isophonics-drupal-site
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 } |