Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 991 | Zur aktuellen Revision | Details | Vergleich mit vorheriger | 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
 *
200 lars 6
 * (c) 2012-2023 Justin Hileman
148 lars 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
{
1294 lars 52
    const VERSION = 'v0.11.15';
148 lars 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
    /**
200 lars 232
     * @return Matcher\AbstractMatcher[]
148 lars 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
    public function onInput(string $input): string
599
    {
600
        foreach ($this->loopListeners as $listeners) {
601
            if (($return = $listeners->onInput($this, $input)) !== null) {
602
                $input = $return;
603
            }
604
        }
605
 
606
        return $input;
607
    }
608
 
609
    /**
610
     * Run execution loop listeners on code to be executed.
611
     *
612
     * @param string $code
613
     */
614
    public function onExecute(string $code): string
615
    {
616
        $this->errorReporting = \error_reporting();
617
 
618
        foreach ($this->loopListeners as $listener) {
619
            if (($return = $listener->onExecute($this, $code)) !== null) {
620
                $code = $return;
621
            }
622
        }
623
 
624
        $output = $this->output;
625
        if ($output instanceof ConsoleOutput) {
626
            $output = $output->getErrorOutput();
627
        }
628
 
629
        $output->writeln(\sprintf('<aside>%s</aside>', OutputFormatter::escape($code)), ConsoleOutput::VERBOSITY_DEBUG);
630
 
631
        return $code;
632
    }
633
 
634
    /**
635
     * Run execution loop listeners after each loop.
636
     */
637
    public function afterLoop()
638
    {
639
        foreach ($this->loopListeners as $listener) {
640
            $listener->afterLoop($this);
641
        }
642
    }
643
 
644
    /**
645
     * Run execution loop listers after the shell session.
646
     */
647
    protected function afterRun()
648
    {
649
        foreach ($this->loopListeners as $listener) {
650
            $listener->afterRun($this);
651
        }
652
    }
653
 
654
    /**
655
     * Set the variables currently in scope.
656
     *
657
     * @param array $vars
658
     */
659
    public function setScopeVariables(array $vars)
660
    {
661
        $this->context->setAll($vars);
662
    }
663
 
664
    /**
665
     * Return the set of variables currently in scope.
666
     *
667
     * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
668
     *                                 passing the scope variables to `extract`
669
     *                                 in PHP 7.1+, you _must_ exclude 'this'
670
     *
671
     * @return array Associative array of scope variables
672
     */
673
    public function getScopeVariables(bool $includeBoundObject = true): array
674
    {
675
        $vars = $this->context->getAll();
676
 
677
        if (!$includeBoundObject) {
678
            unset($vars['this']);
679
        }
680
 
681
        return $vars;
682
    }
683
 
684
    /**
685
     * Return the set of magic variables currently in scope.
686
     *
687
     * @param bool $includeBoundObject Pass false to exclude 'this'. If you're
688
     *                                 passing the scope variables to `extract`
689
     *                                 in PHP 7.1+, you _must_ exclude 'this'
690
     *
691
     * @return array Associative array of magic scope variables
692
     */
693
    public function getSpecialScopeVariables(bool $includeBoundObject = true): array
694
    {
695
        $vars = $this->context->getSpecialVariables();
696
 
697
        if (!$includeBoundObject) {
698
            unset($vars['this']);
699
        }
700
 
701
        return $vars;
702
    }
703
 
704
    /**
705
     * Return the set of variables currently in scope which differ from the
706
     * values passed as $currentVars.
707
     *
708
     * This is used inside the Execution Loop Closure to pick up scope variable
709
     * changes made by commands while the loop is running.
710
     *
711
     * @param array $currentVars
712
     *
713
     * @return array Associative array of scope variables which differ from $currentVars
714
     */
715
    public function getScopeVariablesDiff(array $currentVars): array
716
    {
717
        $newVars = [];
718
 
719
        foreach ($this->getScopeVariables(false) as $key => $value) {
720
            if (!\array_key_exists($key, $currentVars) || $currentVars[$key] !== $value) {
721
                $newVars[$key] = $value;
722
            }
723
        }
724
 
725
        return $newVars;
726
    }
727
 
728
    /**
729
     * Get the set of unused command-scope variable names.
730
     *
731
     * @return array Array of unused variable names
732
     */
733
    public function getUnusedCommandScopeVariableNames(): array
734
    {
735
        return $this->context->getUnusedCommandScopeVariableNames();
736
    }
737
 
738
    /**
739
     * Get the set of variable names currently in scope.
740
     *
741
     * @return array Array of variable names
742
     */
743
    public function getScopeVariableNames(): array
744
    {
745
        return \array_keys($this->context->getAll());
746
    }
747
 
748
    /**
749
     * Get a scope variable value by name.
750
     *
751
     * @param string $name
752
     *
753
     * @return mixed
754
     */
755
    public function getScopeVariable(string $name)
756
    {
757
        return $this->context->get($name);
758
    }
759
 
760
    /**
761
     * Set the bound object ($this variable) for the interactive shell.
762
     *
763
     * @param object|null $boundObject
764
     */
765
    public function setBoundObject($boundObject)
766
    {
767
        $this->context->setBoundObject($boundObject);
768
    }
769
 
770
    /**
771
     * Get the bound object ($this variable) for the interactive shell.
772
     *
773
     * @return object|null
774
     */
775
    public function getBoundObject()
776
    {
777
        return $this->context->getBoundObject();
778
    }
779
 
780
    /**
781
     * Set the bound class (self) for the interactive shell.
782
     *
783
     * @param string|null $boundClass
784
     */
785
    public function setBoundClass($boundClass)
786
    {
787
        $this->context->setBoundClass($boundClass);
788
    }
789
 
790
    /**
791
     * Get the bound class (self) for the interactive shell.
792
     *
793
     * @return string|null
794
     */
795
    public function getBoundClass()
796
    {
797
        return $this->context->getBoundClass();
798
    }
799
 
800
    /**
801
     * Add includes, to be parsed and executed before running the interactive shell.
802
     *
803
     * @param array $includes
804
     */
805
    public function setIncludes(array $includes = [])
806
    {
807
        $this->includes = $includes;
808
    }
809
 
810
    /**
811
     * Get PHP files to be parsed and executed before running the interactive shell.
812
     *
200 lars 813
     * @return string[]
148 lars 814
     */
815
    public function getIncludes(): array
816
    {
817
        return \array_merge($this->config->getDefaultIncludes(), $this->includes);
818
    }
819
 
820
    /**
821
     * Check whether this shell's code buffer contains code.
822
     *
823
     * @return bool True if the code buffer contains code
824
     */
825
    public function hasCode(): bool
826
    {
827
        return !empty($this->codeBuffer);
828
    }
829
 
830
    /**
831
     * Check whether the code in this shell's code buffer is valid.
832
     *
833
     * If the code is valid, the code buffer should be flushed and evaluated.
834
     *
835
     * @return bool True if the code buffer content is valid
836
     */
837
    protected function hasValidCode(): bool
838
    {
839
        return !$this->codeBufferOpen && $this->code !== false;
840
    }
841
 
842
    /**
843
     * Add code to the code buffer.
844
     *
845
     * @param string $code
846
     * @param bool   $silent
847
     */
848
    public function addCode(string $code, bool $silent = false)
849
    {
850
        try {
851
            // Code lines ending in \ keep the buffer open
852
            if (\substr(\rtrim($code), -1) === '\\') {
853
                $this->codeBufferOpen = true;
854
                $code = \substr(\rtrim($code), 0, -1);
855
            } else {
856
                $this->codeBufferOpen = false;
857
            }
858
 
859
            $this->codeBuffer[] = $silent ? new SilentInput($code) : $code;
860
            $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons());
861
        } catch (\Throwable $e) {
862
            // Add failed code blocks to the readline history.
863
            $this->addCodeBufferToHistory();
864
 
865
            throw $e;
866
        }
867
    }
868
 
869
    /**
870
     * Set the code buffer.
871
     *
872
     * This is mostly used by `Shell::execute`. Any existing code in the input
873
     * buffer is pushed onto a stack and will come back after this new code is
874
     * executed.
875
     *
876
     * @throws \InvalidArgumentException if $code isn't a complete statement
877
     *
878
     * @param string $code
879
     * @param bool   $silent
880
     */
881
    private function setCode(string $code, bool $silent = false)
882
    {
883
        if ($this->hasCode()) {
884
            $this->codeStack[] = [$this->codeBuffer, $this->codeBufferOpen, $this->code];
885
        }
886
 
887
        $this->resetCodeBuffer();
888
        try {
889
            $this->addCode($code, $silent);
890
        } catch (\Throwable $e) {
891
            $this->popCodeStack();
892
 
893
            throw $e;
894
        }
895
 
896
        if (!$this->hasValidCode()) {
897
            $this->popCodeStack();
898
 
899
            throw new \InvalidArgumentException('Unexpected end of input');
900
        }
901
    }
902
 
903
    /**
904
     * Get the current code buffer.
905
     *
906
     * This is useful for commands which manipulate the buffer.
907
     *
200 lars 908
     * @return string[]
148 lars 909
     */
910
    public function getCodeBuffer(): array
911
    {
912
        return $this->codeBuffer;
913
    }
914
 
915
    /**
916
     * Run a Psy Shell command given the user input.
917
     *
918
     * @throws \InvalidArgumentException if the input is not a valid command
919
     *
920
     * @param string $input User input string
921
     *
922
     * @return mixed Who knows?
923
     */
924
    protected function runCommand(string $input)
925
    {
926
        $command = $this->getCommand($input);
927
 
928
        if (empty($command)) {
929
            throw new \InvalidArgumentException('Command not found: '.$input);
930
        }
931
 
932
        $input = new ShellInput(\str_replace('\\', '\\\\', \rtrim($input, " \t\n\r\0\x0B;")));
933
 
934
        if ($input->hasParameterOption(['--help', '-h'])) {
935
            $helpCommand = $this->get('help');
936
            if (!$helpCommand instanceof Command\HelpCommand) {
937
                throw new RuntimeException('Invalid help command instance');
938
            }
939
            $helpCommand->setCommand($command);
940
 
941
            return $helpCommand->run(new StringInput(''), $this->output);
942
        }
943
 
944
        return $command->run($input, $this->output);
945
    }
946
 
947
    /**
948
     * Reset the current code buffer.
949
     *
950
     * This should be run after evaluating user input, catching exceptions, or
951
     * on demand by commands such as BufferCommand.
952
     */
953
    public function resetCodeBuffer()
954
    {
955
        $this->codeBuffer = [];
956
        $this->code = false;
957
    }
958
 
959
    /**
960
     * Inject input into the input buffer.
961
     *
962
     * This is useful for commands which want to replay history.
963
     *
964
     * @param string|array $input
965
     * @param bool         $silent
966
     */
967
    public function addInput($input, bool $silent = false)
968
    {
969
        foreach ((array) $input as $line) {
970
            $this->inputBuffer[] = $silent ? new SilentInput($line) : $line;
971
        }
972
    }
973
 
974
    /**
975
     * Flush the current (valid) code buffer.
976
     *
977
     * If the code buffer is valid, resets the code buffer and returns the
978
     * current code.
979
     *
980
     * @return string|null PHP code buffer contents
981
     */
982
    public function flushCode()
983
    {
984
        if ($this->hasValidCode()) {
985
            $this->addCodeBufferToHistory();
986
            $code = $this->code;
987
            $this->popCodeStack();
988
 
989
            return $code;
990
        }
991
    }
992
 
993
    /**
994
     * Reset the code buffer and restore any code pushed during `execute` calls.
995
     */
996
    private function popCodeStack()
997
    {
998
        $this->resetCodeBuffer();
999
 
1000
        if (empty($this->codeStack)) {
1001
            return;
1002
        }
1003
 
1004
        list($codeBuffer, $codeBufferOpen, $code) = \array_pop($this->codeStack);
1005
 
1006
        $this->codeBuffer = $codeBuffer;
1007
        $this->codeBufferOpen = $codeBufferOpen;
1008
        $this->code = $code;
1009
    }
1010
 
1011
    /**
1012
     * (Possibly) add a line to the readline history.
1013
     *
1014
     * Like Bash, if the line starts with a space character, it will be omitted
1015
     * from history. Note that an entire block multi-line code input will be
1016
     * omitted iff the first line begins with a space.
1017
     *
1018
     * Additionally, if a line is "silent", i.e. it was initially added with the
1019
     * silent flag, it will also be omitted.
1020
     *
1021
     * @param string|SilentInput $line
1022
     */
1023
    private function addHistory($line)
1024
    {
1025
        if ($line instanceof SilentInput) {
1026
            return;
1027
        }
1028
 
1029
        // Skip empty lines and lines starting with a space
1030
        if (\trim($line) !== '' && \substr($line, 0, 1) !== ' ') {
1031
            $this->readline->addHistory($line);
1032
        }
1033
    }
1034
 
1035
    /**
1036
     * Filter silent input from code buffer, write the rest to readline history.
1037
     */
1038
    private function addCodeBufferToHistory()
1039
    {
1040
        $codeBuffer = \array_filter($this->codeBuffer, function ($line) {
1041
            return !$line instanceof SilentInput;
1042
        });
1043
 
1044
        $this->addHistory(\implode("\n", $codeBuffer));
1045
    }
1046
 
1047
    /**
1048
     * Get the current evaluation scope namespace.
1049
     *
1050
     * @see CodeCleaner::getNamespace
1051
     *
1052
     * @return string|null Current code namespace
1053
     */
1054
    public function getNamespace()
1055
    {
1056
        if ($namespace = $this->cleaner->getNamespace()) {
1057
            return \implode('\\', $namespace);
1058
        }
1059
    }
1060
 
1061
    /**
1062
     * Write a string to stdout.
1063
     *
1064
     * This is used by the shell loop for rendering output from evaluated code.
1065
     *
1066
     * @param string $out
1067
     * @param int    $phase Output buffering phase
1068
     */
1069
    public function writeStdout(string $out, int $phase = \PHP_OUTPUT_HANDLER_END)
1070
    {
1071
        if ($phase & \PHP_OUTPUT_HANDLER_START) {
1072
            if ($this->output instanceof ShellOutput) {
1073
                $this->output->startPaging();
1074
            }
1075
        }
1076
 
1077
        $isCleaning = $phase & \PHP_OUTPUT_HANDLER_CLEAN;
1078
 
1079
        // Incremental flush
1080
        if ($out !== '' && !$isCleaning) {
1081
            $this->output->write($out, false, OutputInterface::OUTPUT_RAW);
1082
            $this->outputWantsNewline = (\substr($out, -1) !== "\n");
1083
            $this->stdoutBuffer .= $out;
1084
        }
1085
 
1086
        // Output buffering is done!
1087
        if ($phase & \PHP_OUTPUT_HANDLER_END) {
1088
            // Write an extra newline if stdout didn't end with one
1089
            if ($this->outputWantsNewline) {
1090
                if (!$this->config->rawOutput() && !$this->config->outputIsPiped()) {
1091
                    $this->output->writeln(\sprintf('<whisper>%s</whisper>', $this->config->useUnicode() ? '⏎' : '\\n'));
1092
                } else {
1093
                    $this->output->writeln('');
1094
                }
1095
                $this->outputWantsNewline = false;
1096
            }
1097
 
1098
            // Save the stdout buffer as $__out
1099
            if ($this->stdoutBuffer !== '') {
1100
                $this->context->setLastStdout($this->stdoutBuffer);
1101
                $this->stdoutBuffer = '';
1102
            }
1103
 
1104
            if ($this->output instanceof ShellOutput) {
1105
                $this->output->stopPaging();
1106
            }
1107
        }
1108
    }
1109
 
1110
    /**
1111
     * Write a return value to stdout.
1112
     *
1113
     * The return value is formatted or pretty-printed, and rendered in a
1114
     * visibly distinct manner (in this case, as cyan).
1115
     *
1116
     * @see self::presentValue
1117
     *
1118
     * @param mixed $ret
1119
     * @param bool  $rawOutput Write raw var_export-style values
1120
     */
1121
    public function writeReturnValue($ret, bool $rawOutput = false)
1122
    {
1123
        $this->lastExecSuccess = true;
1124
 
1125
        if ($ret instanceof NoReturnValue) {
1126
            return;
1127
        }
1128
 
1129
        $this->context->setReturnValue($ret);
1130
 
1131
        if ($rawOutput) {
1132
            $formatted = \var_export($ret, true);
1133
        } else {
1134
            $prompt = $this->config->theme()->returnValue();
1135
            $indent = \str_repeat(' ', \strlen($prompt));
1136
            $formatted = $this->presentValue($ret);
1137
            $formattedRetValue = \sprintf('<whisper>%s</whisper>', $prompt);
1138
 
1139
            $formatted = $formattedRetValue.\str_replace(\PHP_EOL, \PHP_EOL.$indent, $formatted);
1140
        }
1141
 
1142
        if ($this->output instanceof ShellOutput) {
1143
            $this->output->page($formatted.\PHP_EOL);
1144
        } else {
1145
            $this->output->writeln($formatted);
1146
        }
1147
    }
1148
 
1149
    /**
1150
     * Renders a caught Exception or Error.
1151
     *
1152
     * Exceptions are formatted according to severity. ErrorExceptions which were
1153
     * warnings or Strict errors aren't rendered as harshly as real errors.
1154
     *
1155
     * Stores $e as the last Exception in the Shell Context.
1156
     *
1157
     * @param \Throwable $e An exception or error instance
1158
     */
1159
    public function writeException(\Throwable $e)
1160
    {
1161
        // No need to write the break exception during a non-interactive run.
1162
        if ($e instanceof BreakException && $this->nonInteractive) {
1163
            $this->resetCodeBuffer();
1164
 
1165
            return;
1166
        }
1167
 
1168
        // Break exceptions don't count :)
1169
        if (!$e instanceof BreakException) {
1170
            $this->lastExecSuccess = false;
1171
            $this->context->setLastException($e);
1172
        }
1173
 
1174
        $output = $this->output;
1175
        if ($output instanceof ConsoleOutput) {
1176
            $output = $output->getErrorOutput();
1177
        }
1178
 
1179
        if (!$this->config->theme()->compact()) {
1180
            $output->writeln('');
1181
        }
1182
 
1183
        $output->writeln($this->formatException($e));
1184
 
1185
        if (!$this->config->theme()->compact()) {
1186
            $output->writeln('');
1187
        }
1188
 
1189
        // Include an exception trace (as long as this isn't a BreakException).
1190
        if (!$e instanceof BreakException && $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
1191
            $trace = TraceFormatter::formatTrace($e);
1192
            if (\count($trace) !== 0) {
1193
                $output->writeln('--');
1194
                $output->write($trace, true);
1195
                $output->writeln('');
1196
            }
1197
        }
1198
 
1199
        $this->resetCodeBuffer();
1200
    }
1201
 
1202
    /**
1203
     * Check whether the last exec was successful.
1204
     *
1205
     * Returns true if a return value was logged rather than an exception.
1206
     */
1207
    public function getLastExecSuccess(): bool
1208
    {
1209
        return $this->lastExecSuccess;
1210
    }
1211
 
1212
    /**
1213
     * Helper for formatting an exception or error for writeException().
1214
     *
1215
     * @todo extract this to somewhere it makes more sense
1216
     *
1217
     * @param \Throwable $e
1218
     */
1219
    public function formatException(\Throwable $e): string
1220
    {
1221
        $indent = $this->config->theme()->compact() ? '' : '  ';
1222
 
1223
        if ($e instanceof BreakException) {
1224
            return \sprintf('%s<info> INFO </info> %s.', $indent, \rtrim($e->getRawMessage(), '.'));
1225
        } elseif ($e instanceof PsyException) {
1226
            $message = $e->getLine() > 1
1227
                ? \sprintf('%s in %s on line %d', $e->getRawMessage(), $e->getFile(), $e->getLine())
1228
                : \sprintf('%s in %s', $e->getRawMessage(), $e->getFile());
1229
 
1230
            $messageLabel = \strtoupper($this->getMessageLabel($e));
1231
        } else {
1232
            $message = $e->getMessage();
1233
            $messageLabel = $this->getMessageLabel($e);
1234
        }
1235
 
1236
        $message = \preg_replace(
1237
            "#(\\w:)?([\\\\/]\\w+)*[\\\\/]src[\\\\/]Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code#",
1238
            "eval()'d code",
1239
            $message
1240
        );
1241
 
1242
        $message = \str_replace(" in eval()'d code", '', $message);
1243
        $message = \trim($message);
1244
 
1245
        // Ensures the given string ends with punctuation...
1246
        if (!empty($message) && !\in_array(\substr($message, -1), ['.', '?', '!', ':'])) {
1247
            $message = "$message.";
1248
        }
1249
 
1250
        // Ensures the given message only contains relative paths...
1251
        $message = \str_replace(\getcwd().\DIRECTORY_SEPARATOR, '', $message);
1252
 
1253
        $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error';
1254
 
1255
        return \sprintf('%s<%s> %s </%s> %s', $indent, $severity, $messageLabel, $severity, OutputFormatter::escape($message));
1256
    }
1257
 
1258
    /**
1259
     * Helper for getting an output style for the given ErrorException's level.
1260
     *
1261
     * @param \ErrorException $e
1262
     */
1263
    protected function getSeverity(\ErrorException $e): string
1264
    {
1265
        $severity = $e->getSeverity();
1266
        if ($severity & \error_reporting()) {
1267
            switch ($severity) {
1268
                case \E_WARNING:
1269
                case \E_NOTICE:
1270
                case \E_CORE_WARNING:
1271
                case \E_COMPILE_WARNING:
1272
                case \E_USER_WARNING:
1273
                case \E_USER_NOTICE:
1274
                case \E_USER_DEPRECATED:
1275
                case \E_DEPRECATED:
1276
                case \E_STRICT:
1277
                    return 'warning';
1278
 
1279
                default:
1280
                    return 'error';
1281
            }
1282
        } else {
1283
            // Since this is below the user's reporting threshold, it's always going to be a warning.
1284
            return 'warning';
1285
        }
1286
    }
1287
 
1288
    /**
1289
     * Helper for getting an output style for the given ErrorException's level.
1290
     *
1291
     * @param \Throwable $e
1292
     */
1293
    protected function getMessageLabel(\Throwable $e): string
1294
    {
1295
        if ($e instanceof \ErrorException) {
1296
            $severity = $e->getSeverity();
1297
 
1298
            if ($severity & \error_reporting()) {
1299
                switch ($severity) {
1300
                    case \E_WARNING:
1301
                        return 'Warning';
1302
                    case \E_NOTICE:
1303
                        return 'Notice';
1304
                    case \E_CORE_WARNING:
1305
                        return 'Core Warning';
1306
                    case \E_COMPILE_WARNING:
1307
                        return 'Compile Warning';
1308
                    case \E_USER_WARNING:
1309
                        return 'User Warning';
1310
                    case \E_USER_NOTICE:
1311
                        return 'User Notice';
1312
                    case \E_USER_DEPRECATED:
1313
                        return 'User Deprecated';
1314
                    case \E_DEPRECATED:
1315
                        return 'Deprecated';
1316
                    case \E_STRICT:
1317
                        return 'Strict';
1318
                }
1319
            }
1320
        }
1321
 
1322
        if ($e instanceof PsyException) {
1323
            $exceptionShortName = (new \ReflectionClass($e))->getShortName();
1324
            $typeParts = \preg_split('/(?=[A-Z])/', $exceptionShortName);
1325
            \array_pop($typeParts); // Removes "Exception"
1326
 
1327
            return \trim(\strtoupper(\implode(' ', $typeParts)));
1328
        }
1329
 
1330
        return \get_class($e);
1331
    }
1332
 
1333
    /**
1334
     * Execute code in the shell execution context.
1335
     *
1336
     * @param string $code
1337
     * @param bool   $throwExceptions
1338
     *
1339
     * @return mixed
1340
     */
1341
    public function execute(string $code, bool $throwExceptions = false)
1342
    {
1343
        $this->setCode($code, true);
1344
        $closure = new ExecutionClosure($this);
1345
 
1346
        if ($throwExceptions) {
1347
            return $closure->execute();
1348
        }
1349
 
1350
        try {
1351
            return $closure->execute();
1352
        } catch (\Throwable $_e) {
1353
            $this->writeException($_e);
1354
        }
1355
    }
1356
 
1357
    /**
1358
     * Helper for throwing an ErrorException.
1359
     *
1360
     * This allows us to:
1361
     *
1362
     *     set_error_handler([$psysh, 'handleError']);
1363
     *
1364
     * Unlike ErrorException::throwException, this error handler respects error
1365
     * levels; i.e. it logs warnings and notices, but doesn't throw exceptions.
1366
     * This should probably only be used in the inner execution loop of the
1367
     * shell, as most of the time a thrown exception is much more useful.
1368
     *
1369
     * If the error type matches the `errorLoggingLevel` config, it will be
1370
     * logged as well, regardless of the `error_reporting` level.
1371
     *
1372
     * @see \Psy\Exception\ErrorException::throwException
1373
     * @see \Psy\Shell::writeException
1374
     *
1375
     * @throws \Psy\Exception\ErrorException depending on the error level
1376
     *
1377
     * @param int    $errno   Error type
1378
     * @param string $errstr  Message
1379
     * @param string $errfile Filename
1380
     * @param int    $errline Line number
1381
     */
1382
    public function handleError($errno, $errstr, $errfile, $errline)
1383
    {
1384
        // This is an error worth throwing.
1385
        //
1386
        // n.b. Technically we can't handle all of these in userland code, but
1387
        // we'll list 'em all for good measure
1388
        if ($errno & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR)) {
1389
            ErrorException::throwException($errno, $errstr, $errfile, $errline);
1390
        }
1391
 
1392
        // When errors are suppressed, the error_reporting value will differ
1393
        // from when we started executing. In that case, we won't log errors.
1394
        $errorsSuppressed = $this->errorReporting !== null && $this->errorReporting !== \error_reporting();
1395
 
1396
        // Otherwise log it and continue.
1397
        if ($errno & \error_reporting() || (!$errorsSuppressed && ($errno & $this->config->errorLoggingLevel()))) {
1398
            $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline));
1399
        }
1400
    }
1401
 
1402
    /**
1403
     * Format a value for display.
1404
     *
1405
     * @see Presenter::present
1406
     *
1407
     * @param mixed $val
1408
     *
1409
     * @return string Formatted value
1410
     */
1411
    protected function presentValue($val): string
1412
    {
1413
        return $this->config->getPresenter()->present($val);
1414
    }
1415
 
1416
    /**
1417
     * Get a command (if one exists) for the current input string.
1418
     *
1419
     * @param string $input
1420
     *
1421
     * @return BaseCommand|null
1422
     */
1423
    protected function getCommand(string $input)
1424
    {
1425
        $input = new StringInput($input);
1426
        if ($name = $input->getFirstArgument()) {
1427
            return $this->get($name);
1428
        }
1429
    }
1430
 
1431
    /**
1432
     * Check whether a command is set for the current input string.
1433
     *
1434
     * @param string $input
1435
     *
1436
     * @return bool True if the shell has a command for the given input
1437
     */
1438
    protected function hasCommand(string $input): bool
1439
    {
1440
        if (\preg_match('/([^\s]+?)(?:\s|$)/A', \ltrim($input), $match)) {
1441
            return $this->has($match[1]);
1442
        }
1443
 
1444
        return false;
1445
    }
1446
 
1447
    /**
1448
     * Get the current input prompt.
1449
     *
1450
     * @return string|null
1451
     */
1452
    protected function getPrompt()
1453
    {
1454
        if ($this->output->isQuiet()) {
1455
            return null;
1456
        }
1457
 
1458
        $theme = $this->config->theme();
1459
 
1460
        if ($this->hasCode()) {
1461
            return $theme->bufferPrompt();
1462
        }
1463
 
1464
        return $theme->prompt();
1465
    }
1466
 
1467
    /**
1468
     * Read a line of user input.
1469
     *
1470
     * This will return a line from the input buffer (if any exist). Otherwise,
1471
     * it will ask the user for input.
1472
     *
1473
     * If readline is enabled, this delegates to readline. Otherwise, it's an
1474
     * ugly `fgets` call.
1475
     *
1476
     * @param bool $interactive
1477
     *
1478
     * @return string|false One line of user input
1479
     */
1480
    protected function readline(bool $interactive = true)
1481
    {
1482
        $prompt = $this->config->theme()->replayPrompt();
1483
 
1484
        if (!empty($this->inputBuffer)) {
1485
            $line = \array_shift($this->inputBuffer);
1486
            if (!$line instanceof SilentInput) {
1487
                $this->output->writeln(\sprintf('<whisper>%s</whisper><aside>%s</aside>', $prompt, OutputFormatter::escape($line)));
1488
            }
1489
 
1490
            return $line;
1491
        }
1492
 
1493
        $bracketedPaste = $interactive && $this->config->useBracketedPaste();
1494
 
1495
        if ($bracketedPaste) {
1496
            \printf("\e[?2004h"); // Enable bracketed paste
1497
        }
1498
 
1499
        $line = $this->readline->readline($this->getPrompt());
1500
 
1501
        if ($bracketedPaste) {
1502
            \printf("\e[?2004l"); // ... and disable it again
1503
        }
1504
 
1505
        return $line;
1506
    }
1507
 
1508
    /**
1509
     * Get the shell output header.
1510
     */
1511
    protected function getHeader(): string
1512
    {
1513
        return \sprintf('<whisper>%s by Justin Hileman</whisper>', $this->getVersion());
1514
    }
1515
 
1516
    /**
1517
     * Get the current version of Psy Shell.
1518
     *
1519
     * @deprecated call self::getVersionHeader instead
1520
     */
1521
    public function getVersion(): string
1522
    {
1523
        return self::getVersionHeader($this->config->useUnicode());
1524
    }
1525
 
1526
    /**
1527
     * Get a pretty header including the current version of Psy Shell.
1528
     *
1529
     * @param bool $useUnicode
1530
     */
1531
    public static function getVersionHeader(bool $useUnicode = false): string
1532
    {
1533
        $separator = $useUnicode ? '—' : '-';
1534
 
1535
        return \sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, \PHP_VERSION, $separator, \PHP_SAPI);
1536
    }
1537
 
1538
    /**
1539
     * Get a PHP manual database instance.
1540
     *
1541
     * @return \PDO|null
1542
     */
1543
    public function getManualDb()
1544
    {
1545
        return $this->config->getManualDb();
1546
    }
1547
 
1548
    /**
1549
     * @deprecated Tab completion is provided by the AutoCompleter service
1550
     */
1551
    protected function autocomplete($text)
1552
    {
1553
        @\trigger_error('Tab completion is provided by the AutoCompleter service', \E_USER_DEPRECATED);
1554
    }
1555
 
1556
    /**
1557
     * Initialize tab completion matchers.
1558
     *
1559
     * If tab completion is enabled this adds tab completion matchers to the
1560
     * auto completer and sets context if needed.
1561
     */
1562
    protected function initializeTabCompletion()
1563
    {
1564
        if (!$this->config->useTabCompletion()) {
1565
            return;
1566
        }
1567
 
1568
        $this->autoCompleter = $this->config->getAutoCompleter();
1569
 
1570
        // auto completer needs shell to be linked to configuration because of
1571
        // the context aware matchers
1572
        $this->addMatchersToAutoCompleter($this->getDefaultMatchers());
1573
        $this->addMatchersToAutoCompleter($this->matchers);
1574
 
1575
        $this->autoCompleter->activate();
1576
    }
1577
 
1578
    /**
1579
     * Add matchers to the auto completer, setting context if needed.
1580
     *
1581
     * @param array $matchers
1582
     */
1583
    private function addMatchersToAutoCompleter(array $matchers)
1584
    {
1585
        foreach ($matchers as $matcher) {
1586
            if ($matcher instanceof ContextAware) {
1587
                $matcher->setContext($this->context);
1588
            }
1589
            $this->autoCompleter->addMatcher($matcher);
1590
        }
1591
    }
1592
 
1593
    /**
1594
     * @todo Implement prompt to start update
1595
     *
1596
     * @return void|string
1597
     */
1598
    protected function writeVersionInfo()
1599
    {
1600
        if (\PHP_SAPI !== 'cli') {
1601
            return;
1602
        }
1603
 
1604
        try {
1605
            $client = $this->config->getChecker();
1606
            if (!$client->isLatest()) {
1607
                $this->output->writeln(\sprintf('<whisper>New version is available at psysh.org/psysh (current: %s, latest: %s)</whisper>', self::VERSION, $client->getLatest()));
1608
            }
1609
        } catch (\InvalidArgumentException $e) {
1610
            $this->output->writeln($e->getMessage());
1611
        }
1612
    }
1613
 
1614
    /**
1615
     * Write a startup message if set.
1616
     */
1617
    protected function writeStartupMessage()
1618
    {
1619
        $message = $this->config->getStartupMessage();
1620
        if ($message !== null && $message !== '') {
1621
            $this->output->writeln($message);
1622
        }
1623
    }
1624
}