Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 200 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

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