Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 200 | 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\Exception\DeprecatedException;
15
use Psy\Exception\RuntimeException;
16
use Psy\ExecutionLoop\ProcessForker;
17
use Psy\Output\OutputPager;
18
use Psy\Output\ShellOutput;
19
use Psy\Output\Theme;
20
use Psy\TabCompletion\AutoCompleter;
21
use Psy\VarDumper\Presenter;
22
use Psy\VersionUpdater\Checker;
23
use Psy\VersionUpdater\GitHubChecker;
24
use Psy\VersionUpdater\IntervalChecker;
25
use Psy\VersionUpdater\NoopChecker;
26
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Input\InputOption;
29
use Symfony\Component\Console\Output\OutputInterface;
30
 
31
/**
32
 * The Psy Shell configuration.
33
 */
34
class Configuration
35
{
36
    const COLOR_MODE_AUTO = 'auto';
37
    const COLOR_MODE_FORCED = 'forced';
38
    const COLOR_MODE_DISABLED = 'disabled';
39
 
40
    const INTERACTIVE_MODE_AUTO = 'auto';
41
    const INTERACTIVE_MODE_FORCED = 'forced';
42
    const INTERACTIVE_MODE_DISABLED = 'disabled';
43
 
44
    const VERBOSITY_QUIET = 'quiet';
45
    const VERBOSITY_NORMAL = 'normal';
46
    const VERBOSITY_VERBOSE = 'verbose';
47
    const VERBOSITY_VERY_VERBOSE = 'very_verbose';
48
    const VERBOSITY_DEBUG = 'debug';
49
 
50
    private static $AVAILABLE_OPTIONS = [
51
        'codeCleaner',
52
        'colorMode',
53
        'configDir',
54
        'dataDir',
55
        'defaultIncludes',
56
        'eraseDuplicates',
57
        'errorLoggingLevel',
58
        'forceArrayIndexes',
59
        'formatterStyles',
60
        'historyFile',
61
        'historySize',
62
        'interactiveMode',
63
        'manualDbFile',
64
        'pager',
65
        'prompt',
66
        'rawOutput',
67
        'requireSemicolons',
68
        'runtimeDir',
69
        'startupMessage',
70
        'theme',
71
        'updateCheck',
72
        'useBracketedPaste',
73
        'usePcntl',
74
        'useReadline',
75
        'useTabCompletion',
76
        'useUnicode',
77
        'verbosity',
78
        'warnOnMultipleConfigs',
79
        'yolo',
80
    ];
81
 
82
    private $defaultIncludes;
83
    private $configDir;
84
    private $dataDir;
85
    private $runtimeDir;
86
    private $configFile;
87
    /** @var string|false */
88
    private $historyFile;
89
    private $historySize;
90
    private $eraseDuplicates;
91
    private $manualDbFile;
92
    private $hasReadline;
93
    private $useReadline;
94
    private $useBracketedPaste;
95
    private $hasPcntl;
96
    private $usePcntl;
97
    private $newCommands = [];
98
    private $pipedInput;
99
    private $pipedOutput;
100
    private $rawOutput = false;
101
    private $requireSemicolons = false;
102
    private $useUnicode;
103
    private $useTabCompletion;
104
    private $newMatchers = [];
105
    private $errorLoggingLevel = \E_ALL;
106
    private $warnOnMultipleConfigs = false;
107
    private $colorMode = self::COLOR_MODE_AUTO;
108
    private $interactiveMode = self::INTERACTIVE_MODE_AUTO;
109
    private $updateCheck;
110
    private $startupMessage;
111
    private $forceArrayIndexes = false;
112
    /** @deprecated */
113
    private $formatterStyles = [];
114
    private $verbosity = self::VERBOSITY_NORMAL;
115
    private $yolo = false;
116
    /** @var Theme */
117
    private $theme;
118
 
119
    // services
120
    private $readline;
200 lars 121
    /** @var ShellOutput */
148 lars 122
    private $output;
123
    private $shell;
124
    private $cleaner;
125
    private $pager;
126
    private $manualDb;
127
    private $presenter;
128
    private $autoCompleter;
129
    private $checker;
991 lars 130
    /** @deprecated */
148 lars 131
    private $prompt;
132
    private $configPaths;
133
 
134
    /**
135
     * Construct a Configuration instance.
136
     *
137
     * Optionally, supply an array of configuration values to load.
138
     *
139
     * @param array $config Optional array of configuration values
140
     */
141
    public function __construct(array $config = [])
142
    {
143
        $this->configPaths = new ConfigPaths();
144
 
145
        // explicit configFile option
146
        if (isset($config['configFile'])) {
147
            $this->configFile = $config['configFile'];
148
        } elseif (isset($_SERVER['PSYSH_CONFIG']) && $_SERVER['PSYSH_CONFIG']) {
149
            $this->configFile = $_SERVER['PSYSH_CONFIG'];
150
        }
151
 
152
        // legacy baseDir option
153
        if (isset($config['baseDir'])) {
154
            $msg = "The 'baseDir' configuration option is deprecated; ".
155
                "please specify 'configDir' and 'dataDir' options instead";
156
            throw new DeprecatedException($msg);
157
        }
158
 
159
        unset($config['configFile'], $config['baseDir']);
160
 
161
        // go go gadget, config!
162
        $this->loadConfig($config);
163
        $this->init();
164
    }
165
 
166
    /**
167
     * Construct a Configuration object from Symfony Console input.
168
     *
169
     * This is great for adding psysh-compatible command line options to framework- or app-specific
170
     * wrappers.
171
     *
172
     * $input should already be bound to an appropriate InputDefinition (see self::getInputOptions
173
     * if you want to build your own) before calling this method. It's not required, but things work
174
     * a lot better if we do.
175
     *
176
     * @see self::getInputOptions
177
     *
178
     * @throws \InvalidArgumentException
179
     *
180
     * @param InputInterface $input
181
     */
182
    public static function fromInput(InputInterface $input): self
183
    {
184
        $config = new self(['configFile' => self::getConfigFileFromInput($input)]);
185
 
186
        // Handle --color and --no-color (and --ansi and --no-ansi aliases)
187
        if (self::getOptionFromInput($input, ['color', 'ansi'])) {
188
            $config->setColorMode(self::COLOR_MODE_FORCED);
189
        } elseif (self::getOptionFromInput($input, ['no-color', 'no-ansi'])) {
190
            $config->setColorMode(self::COLOR_MODE_DISABLED);
191
        }
192
 
193
        // Handle verbosity options
194
        if ($verbosity = self::getVerbosityFromInput($input)) {
195
            $config->setVerbosity($verbosity);
196
        }
197
 
198
        // Handle interactive mode
199
        if (self::getOptionFromInput($input, ['interactive', 'interaction'], ['-a', '-i'])) {
200
            $config->setInteractiveMode(self::INTERACTIVE_MODE_FORCED);
201
        } elseif (self::getOptionFromInput($input, ['no-interactive', 'no-interaction'], ['-n'])) {
202
            $config->setInteractiveMode(self::INTERACTIVE_MODE_DISABLED);
203
        }
204
 
205
        // Handle --compact
206
        if (self::getOptionFromInput($input, ['compact'])) {
207
            $config->setTheme('compact');
208
        }
209
 
210
        // Handle --raw-output
211
        // @todo support raw output with interactive input?
212
        if (!$config->getInputInteractive()) {
213
            if (self::getOptionFromInput($input, ['raw-output'], ['-r'])) {
214
                $config->setRawOutput(true);
215
            }
216
        }
217
 
218
        // Handle --yolo
219
        if (self::getOptionFromInput($input, ['yolo'])) {
220
            $config->setYolo(true);
221
        }
222
 
223
        return $config;
224
    }
225
 
226
    /**
227
     * Get the desired config file from the given input.
228
     *
229
     * @return string|null config file path, or null if none is specified
230
     */
231
    private static function getConfigFileFromInput(InputInterface $input)
232
    {
233
        // Best case, input is properly bound and validated.
234
        if ($input->hasOption('config')) {
235
            return $input->getOption('config');
236
        }
237
 
238
        return $input->getParameterOption('--config', null, true) ?: $input->getParameterOption('-c', null, true);
239
    }
240
 
241
    /**
242
     * Get a boolean option from the given input.
243
     *
244
     * This helper allows fallback for unbound and unvalidated input. It's not perfect--for example,
245
     * it can't deal with several short options squished together--but it's better than falling over
246
     * any time someone gives us unbound input.
247
     *
248
     * @return bool true if the option (or an alias) is present
249
     */
250
    private static function getOptionFromInput(InputInterface $input, array $names, array $otherParams = []): bool
251
    {
252
        // Best case, input is properly bound and validated.
253
        foreach ($names as $name) {
254
            if ($input->hasOption($name) && $input->getOption($name)) {
255
                return true;
256
            }
257
        }
258
 
259
        foreach ($names as $name) {
260
            $otherParams[] = '--'.$name;
261
        }
262
 
263
        foreach ($otherParams as $name) {
264
            if ($input->hasParameterOption($name, true)) {
265
                return true;
266
            }
267
        }
268
 
269
        return false;
270
    }
271
 
272
    /**
273
     * Get the desired verbosity from the given input.
274
     *
275
     * This is a bit more complext than the other options parsers. It handles `--quiet` and
276
     * `--verbose`, along with their short aliases, and fancy things like `-vvv`.
277
     *
278
     * @return string|null configuration constant, or null if no verbosity option is specified
279
     */
280
    private static function getVerbosityFromInput(InputInterface $input)
281
    {
282
        // --quiet wins!
283
        if (self::getOptionFromInput($input, ['quiet'], ['-q'])) {
284
            return self::VERBOSITY_QUIET;
285
        }
286
 
287
        // Best case, input is properly bound and validated.
288
        //
289
        // Note that if the `--verbose` option is incorrectly defined as `VALUE_NONE` rather than
290
        // `VALUE_OPTIONAL` (as it is in Symfony Console by default) it doesn't actually work with
291
        // multiple verbosity levels as it claims.
292
        //
293
        // We can detect this by checking whether the the value === true, and fall back to unbound
294
        // parsing for this option.
295
        if ($input->hasOption('verbose') && $input->getOption('verbose') !== true) {
296
            switch ($input->getOption('verbose')) {
297
                case '-1':
298
                    return self::VERBOSITY_QUIET;
299
                case '0': // explicitly normal, overrides config file default
300
                    return self::VERBOSITY_NORMAL;
301
                case '1':
302
                case null: // `--verbose` and `-v`
303
                    return self::VERBOSITY_VERBOSE;
304
                case '2':
305
                case 'v': // `-vv`
306
                    return self::VERBOSITY_VERY_VERBOSE;
307
                case '3':
308
                case 'vv': // `-vvv`
309
                    return self::VERBOSITY_DEBUG;
310
                default: // implicitly normal, config file default wins
311
                    return;
312
            }
313
        }
314
 
315
        // quiet and normal have to come before verbose, because it eats everything else.
316
        if ($input->hasParameterOption('--verbose=-1', true) || $input->getParameterOption('--verbose', false, true) === '-1') {
317
            return self::VERBOSITY_QUIET;
318
        }
319
 
320
        if ($input->hasParameterOption('--verbose=0', true) || $input->getParameterOption('--verbose', false, true) === '0') {
321
            return self::VERBOSITY_NORMAL;
322
        }
323
 
324
        // `-vvv`, `-vv` and `-v` have to come in descending length order, because `hasParameterOption` matches prefixes.
325
        if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || $input->getParameterOption('--verbose', false, true) === '3') {
326
            return self::VERBOSITY_DEBUG;
327
        }
328
 
329
        if ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || $input->getParameterOption('--verbose', false, true) === '2') {
330
            return self::VERBOSITY_VERY_VERBOSE;
331
        }
332
 
333
        if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true)) {
334
            return self::VERBOSITY_VERBOSE;
335
        }
336
    }
337
 
338
    /**
339
     * Get a list of input options expected when initializing Configuration via input.
340
     *
341
     * @see self::fromInput
342
     *
343
     * @return InputOption[]
344
     */
345
    public static function getInputOptions(): array
346
    {
347
        return [
348
            new InputOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use an alternate PsySH config file location.'),
349
            new InputOption('cwd', null, InputOption::VALUE_REQUIRED, 'Use an alternate working directory.'),
350
 
351
            new InputOption('color', null, InputOption::VALUE_NONE, 'Force colors in output.'),
352
            new InputOption('no-color', null, InputOption::VALUE_NONE, 'Disable colors in output.'),
353
            // --ansi and --no-ansi aliases to match Symfony, Composer, etc.
354
            new InputOption('ansi', null, InputOption::VALUE_NONE, 'Force colors in output.'),
355
            new InputOption('no-ansi', null, InputOption::VALUE_NONE, 'Disable colors in output.'),
356
 
357
            new InputOption('quiet', 'q', InputOption::VALUE_NONE, 'Shhhhhh.'),
358
            new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_OPTIONAL, 'Increase the verbosity of messages.', '0'),
359
            new InputOption('compact', null, InputOption::VALUE_NONE, 'Run PsySH with compact output.'),
360
            new InputOption('interactive', 'i|a', InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'),
361
            new InputOption('no-interactive', 'n', InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'),
362
            // --interaction and --no-interaction aliases for compatibility with Symfony, Composer, etc
363
            new InputOption('interaction', null, InputOption::VALUE_NONE, 'Force PsySH to run in interactive mode.'),
364
            new InputOption('no-interaction', null, InputOption::VALUE_NONE, 'Run PsySH without interactive input. Requires input from stdin.'),
365
            new InputOption('raw-output', 'r', InputOption::VALUE_NONE, 'Print var_export-style return values (for non-interactive input)'),
366
 
367
            new InputOption('self-update', 'u', InputOption::VALUE_NONE, 'Update to the latest version'),
368
 
369
            new InputOption('yolo', null, InputOption::VALUE_NONE, 'Run PsySH with minimal input validation. You probably don\'t want this.'),
370
        ];
371
    }
372
 
373
    /**
374
     * Initialize the configuration.
375
     *
376
     * This checks for the presence of Readline and Pcntl extensions.
377
     *
378
     * If a config file is available, it will be loaded and merged with the current config.
379
     *
380
     * If no custom config file was specified and a local project config file
381
     * is available, it will be loaded and merged with the current config.
382
     */
383
    public function init()
384
    {
385
        // feature detection
386
        $this->hasReadline = \function_exists('readline');
387
        $this->hasPcntl = ProcessForker::isSupported();
388
 
389
        if ($configFile = $this->getConfigFile()) {
390
            $this->loadConfigFile($configFile);
391
        }
392
 
393
        if (!$this->configFile && $localConfig = $this->getLocalConfigFile()) {
394
            $this->loadConfigFile($localConfig);
395
        }
396
 
397
        $this->configPaths->overrideDirs([
398
            'configDir'  => $this->configDir,
399
            'dataDir'    => $this->dataDir,
400
            'runtimeDir' => $this->runtimeDir,
401
        ]);
402
    }
403
 
404
    /**
405
     * Get the current PsySH config file.
406
     *
407
     * If a `configFile` option was passed to the Configuration constructor,
408
     * this file will be returned. If not, all possible config directories will
409
     * be searched, and the first `config.php` or `rc.php` file which exists
410
     * will be returned.
411
     *
412
     * If you're trying to decide where to put your config file, pick
413
     *
414
     *     ~/.config/psysh/config.php
415
     *
416
     * @return string|null
417
     */
418
    public function getConfigFile()
419
    {
420
        if (isset($this->configFile)) {
421
            return $this->configFile;
422
        }
423
 
424
        $files = $this->configPaths->configFiles(['config.php', 'rc.php']);
425
 
426
        if (!empty($files)) {
427
            if ($this->warnOnMultipleConfigs && \count($files) > 1) {
428
                $msg = \sprintf('Multiple configuration files found: %s. Using %s', \implode(', ', $files), $files[0]);
429
                \trigger_error($msg, \E_USER_NOTICE);
430
            }
431
 
432
            return $files[0];
433
        }
434
    }
435
 
436
    /**
437
     * Get the local PsySH config file.
438
     *
439
     * Searches for a project specific config file `.psysh.php` in the current
440
     * working directory.
441
     *
442
     * @return string|null
443
     */
444
    public function getLocalConfigFile()
445
    {
446
        $localConfig = \getcwd().'/.psysh.php';
447
 
448
        if (@\is_file($localConfig)) {
449
            return $localConfig;
450
        }
451
    }
452
 
453
    /**
454
     * Load configuration values from an array of options.
455
     *
456
     * @param array $options
457
     */
458
    public function loadConfig(array $options)
459
    {
460
        foreach (self::$AVAILABLE_OPTIONS as $option) {
461
            if (isset($options[$option])) {
462
                $method = 'set'.\ucfirst($option);
463
                $this->$method($options[$option]);
464
            }
465
        }
466
 
467
        // legacy `tabCompletion` option
468
        if (isset($options['tabCompletion'])) {
469
            $msg = '`tabCompletion` is deprecated; use `useTabCompletion` instead.';
470
            @\trigger_error($msg, \E_USER_DEPRECATED);
471
 
472
            $this->setUseTabCompletion($options['tabCompletion']);
473
        }
474
 
475
        foreach (['commands', 'matchers', 'casters'] as $option) {
476
            if (isset($options[$option])) {
477
                $method = 'add'.\ucfirst($option);
478
                $this->$method($options[$option]);
479
            }
480
        }
481
 
482
        // legacy `tabCompletionMatchers` option
483
        if (isset($options['tabCompletionMatchers'])) {
484
            $msg = '`tabCompletionMatchers` is deprecated; use `matchers` instead.';
485
            @\trigger_error($msg, \E_USER_DEPRECATED);
486
 
487
            $this->addMatchers($options['tabCompletionMatchers']);
488
        }
489
    }
490
 
491
    /**
492
     * Load a configuration file (default: `$HOME/.config/psysh/config.php`).
493
     *
494
     * This configuration instance will be available to the config file as $config.
495
     * The config file may directly manipulate the configuration, or may return
496
     * an array of options which will be merged with the current configuration.
497
     *
498
     * @throws \InvalidArgumentException if the config file does not exist or returns a non-array result
499
     *
500
     * @param string $file
501
     */
502
    public function loadConfigFile(string $file)
503
    {
504
        if (!\is_file($file)) {
505
            throw new \InvalidArgumentException(\sprintf('Invalid configuration file specified, %s does not exist', $file));
506
        }
507
 
508
        $__psysh_config_file__ = $file;
509
        $load = function ($config) use ($__psysh_config_file__) {
510
            $result = require $__psysh_config_file__;
511
            if ($result !== 1) {
512
                return $result;
513
            }
514
        };
515
        $result = $load($this);
516
 
517
        if (!empty($result)) {
518
            if (\is_array($result)) {
519
                $this->loadConfig($result);
520
            } else {
521
                throw new \InvalidArgumentException('Psy Shell configuration must return an array of options');
522
            }
523
        }
524
    }
525
 
526
    /**
527
     * Set files to be included by default at the start of each shell session.
528
     *
529
     * @param array $includes
530
     */
531
    public function setDefaultIncludes(array $includes = [])
532
    {
533
        $this->defaultIncludes = $includes;
534
    }
535
 
536
    /**
537
     * Get files to be included by default at the start of each shell session.
538
     *
200 lars 539
     * @return string[]
148 lars 540
     */
541
    public function getDefaultIncludes(): array
542
    {
543
        return $this->defaultIncludes ?: [];
544
    }
545
 
546
    /**
547
     * Set the shell's config directory location.
548
     *
549
     * @param string $dir
550
     */
551
    public function setConfigDir(string $dir)
552
    {
553
        $this->configDir = (string) $dir;
554
 
555
        $this->configPaths->overrideDirs([
556
            'configDir'  => $this->configDir,
557
            'dataDir'    => $this->dataDir,
558
            'runtimeDir' => $this->runtimeDir,
559
        ]);
560
    }
561
 
562
    /**
563
     * Get the current configuration directory, if any is explicitly set.
564
     *
565
     * @return string|null
566
     */
567
    public function getConfigDir()
568
    {
569
        return $this->configDir;
570
    }
571
 
572
    /**
573
     * Set the shell's data directory location.
574
     *
575
     * @param string $dir
576
     */
577
    public function setDataDir(string $dir)
578
    {
579
        $this->dataDir = (string) $dir;
580
 
581
        $this->configPaths->overrideDirs([
582
            'configDir'  => $this->configDir,
583
            'dataDir'    => $this->dataDir,
584
            'runtimeDir' => $this->runtimeDir,
585
        ]);
586
    }
587
 
588
    /**
589
     * Get the current data directory, if any is explicitly set.
590
     *
591
     * @return string|null
592
     */
593
    public function getDataDir()
594
    {
595
        return $this->dataDir;
596
    }
597
 
598
    /**
599
     * Set the shell's temporary directory location.
600
     *
601
     * @param string $dir
602
     */
603
    public function setRuntimeDir(string $dir)
604
    {
605
        $this->runtimeDir = (string) $dir;
606
 
607
        $this->configPaths->overrideDirs([
608
            'configDir'  => $this->configDir,
609
            'dataDir'    => $this->dataDir,
610
            'runtimeDir' => $this->runtimeDir,
611
        ]);
612
    }
613
 
614
    /**
615
     * Get the shell's temporary directory location.
616
     *
617
     * Defaults to  `/psysh` inside the system's temp dir unless explicitly
618
     * overridden.
619
     *
620
     * @throws RuntimeException if no temporary directory is set and it is not possible to create one
621
     */
622
    public function getRuntimeDir(): string
623
    {
624
        $runtimeDir = $this->configPaths->runtimeDir();
625
 
626
        if (!\is_dir($runtimeDir)) {
627
            if (!@\mkdir($runtimeDir, 0700, true)) {
628
                throw new RuntimeException(\sprintf('Unable to create PsySH runtime directory. Make sure PHP is able to write to %s in order to continue.', \dirname($runtimeDir)));
629
            }
630
        }
631
 
632
        return $runtimeDir;
633
    }
634
 
635
    /**
636
     * Set the readline history file path.
637
     *
638
     * @param string $file
639
     */
640
    public function setHistoryFile(string $file)
641
    {
642
        $this->historyFile = ConfigPaths::touchFileWithMkdir($file);
643
    }
644
 
645
    /**
646
     * Get the readline history file path.
647
     *
648
     * Defaults to `/history` inside the shell's base config dir unless
649
     * explicitly overridden.
650
     */
651
    public function getHistoryFile(): string
652
    {
653
        if (isset($this->historyFile)) {
654
            return $this->historyFile;
655
        }
656
 
657
        $files = $this->configPaths->configFiles(['psysh_history', 'history']);
658
 
659
        if (!empty($files)) {
660
            if ($this->warnOnMultipleConfigs && \count($files) > 1) {
661
                $msg = \sprintf('Multiple history files found: %s. Using %s', \implode(', ', $files), $files[0]);
662
                \trigger_error($msg, \E_USER_NOTICE);
663
            }
664
 
665
            $this->setHistoryFile($files[0]);
666
        } else {
667
            // fallback: create our own history file
668
            $this->setHistoryFile($this->configPaths->currentConfigDir().'/psysh_history');
669
        }
670
 
671
        return $this->historyFile;
672
    }
673
 
674
    /**
675
     * Set the readline max history size.
676
     *
677
     * @param int $value
678
     */
679
    public function setHistorySize(int $value)
680
    {
681
        $this->historySize = (int) $value;
682
    }
683
 
684
    /**
685
     * Get the readline max history size.
686
     *
687
     * @return int
688
     */
689
    public function getHistorySize()
690
    {
691
        return $this->historySize;
692
    }
693
 
694
    /**
695
     * Sets whether readline erases old duplicate history entries.
696
     *
697
     * @param bool $value
698
     */
699
    public function setEraseDuplicates(bool $value)
700
    {
701
        $this->eraseDuplicates = (bool) $value;
702
    }
703
 
704
    /**
705
     * Get whether readline erases old duplicate history entries.
706
     *
707
     * @return bool|null
708
     */
709
    public function getEraseDuplicates()
710
    {
711
        return $this->eraseDuplicates;
712
    }
713
 
714
    /**
715
     * Get a temporary file of type $type for process $pid.
716
     *
717
     * The file will be created inside the current temporary directory.
718
     *
719
     * @see self::getRuntimeDir
720
     *
721
     * @param string $type
722
     * @param int    $pid
723
     *
724
     * @return string Temporary file name
725
     */
726
    public function getTempFile(string $type, int $pid): string
727
    {
728
        return \tempnam($this->getRuntimeDir(), $type.'_'.$pid.'_');
729
    }
730
 
731
    /**
732
     * Get a filename suitable for a FIFO pipe of $type for process $pid.
733
     *
734
     * The pipe will be created inside the current temporary directory.
735
     *
736
     * @param string $type
737
     * @param int    $pid
738
     *
739
     * @return string Pipe name
740
     */
741
    public function getPipe(string $type, int $pid): string
742
    {
743
        return \sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid);
744
    }
745
 
746
    /**
747
     * Check whether this PHP instance has Readline available.
748
     *
749
     * @return bool True if Readline is available
750
     */
751
    public function hasReadline(): bool
752
    {
753
        return $this->hasReadline;
754
    }
755
 
756
    /**
757
     * Enable or disable Readline usage.
758
     *
759
     * @param bool $useReadline
760
     */
761
    public function setUseReadline(bool $useReadline)
762
    {
763
        $this->useReadline = (bool) $useReadline;
764
    }
765
 
766
    /**
767
     * Check whether to use Readline.
768
     *
769
     * If `setUseReadline` as been set to true, but Readline is not actually
770
     * available, this will return false.
771
     *
772
     * @return bool True if the current Shell should use Readline
773
     */
774
    public function useReadline(): bool
775
    {
776
        return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline;
777
    }
778
 
779
    /**
780
     * Set the Psy Shell readline service.
781
     *
782
     * @param Readline\Readline $readline
783
     */
784
    public function setReadline(Readline\Readline $readline)
785
    {
786
        $this->readline = $readline;
787
    }
788
 
789
    /**
790
     * Get the Psy Shell readline service.
791
     *
792
     * By default, this service uses (in order of preference):
793
     *
794
     *  * GNU Readline
795
     *  * Libedit
796
     *  * A transient array-based readline emulation.
797
     *
798
     * @return Readline\Readline
799
     */
800
    public function getReadline(): Readline\Readline
801
    {
802
        if (!isset($this->readline)) {
803
            $className = $this->getReadlineClass();
804
            $this->readline = new $className(
805
                $this->getHistoryFile(),
806
                $this->getHistorySize(),
807
                $this->getEraseDuplicates()
808
            );
809
        }
810
 
811
        return $this->readline;
812
    }
813
 
814
    /**
815
     * Get the appropriate Readline implementation class name.
816
     *
817
     * @see self::getReadline
818
     */
819
    private function getReadlineClass(): string
820
    {
821
        if ($this->useReadline()) {
822
            if (Readline\GNUReadline::isSupported()) {
823
                return Readline\GNUReadline::class;
824
            } elseif (Readline\Libedit::isSupported()) {
825
                return Readline\Libedit::class;
826
            }
827
        }
828
 
829
        if (Readline\Userland::isSupported()) {
830
            return Readline\Userland::class;
831
        }
832
 
833
        return Readline\Transient::class;
834
    }
835
 
836
    /**
837
     * Enable or disable bracketed paste.
838
     *
839
     * Note that this only works with readline (not libedit) integration for now.
840
     *
841
     * @param bool $useBracketedPaste
842
     */
843
    public function setUseBracketedPaste(bool $useBracketedPaste)
844
    {
845
        $this->useBracketedPaste = (bool) $useBracketedPaste;
846
    }
847
 
848
    /**
849
     * Check whether to use bracketed paste with readline.
850
     *
851
     * When this works, it's magical. Tabs in pastes don't try to autcomplete.
852
     * Newlines in paste don't execute code until you get to the end. It makes
853
     * readline act like you'd expect when pasting.
854
     *
855
     * But it often (usually?) does not work. And when it doesn't, it just spews
856
     * escape codes all over the place and generally makes things ugly :(
857
     *
858
     * If `useBracketedPaste` has been set to true, but the current readline
859
     * implementation is anything besides GNU readline, this will return false.
860
     *
861
     * @return bool True if the shell should use bracketed paste
862
     */
863
    public function useBracketedPaste(): bool
864
    {
865
        $readlineClass = $this->getReadlineClass();
866
 
867
        return $this->useBracketedPaste && $readlineClass::supportsBracketedPaste();
868
 
869
        // @todo mebbe turn this on by default some day?
870
        // return $readlineClass::supportsBracketedPaste() && $this->useBracketedPaste !== false;
871
    }
872
 
873
    /**
874
     * Check whether this PHP instance has Pcntl available.
875
     *
876
     * @return bool True if Pcntl is available
877
     */
878
    public function hasPcntl(): bool
879
    {
880
        return $this->hasPcntl;
881
    }
882
 
883
    /**
884
     * Enable or disable Pcntl usage.
885
     *
886
     * @param bool $usePcntl
887
     */
888
    public function setUsePcntl(bool $usePcntl)
889
    {
890
        $this->usePcntl = (bool) $usePcntl;
891
    }
892
 
893
    /**
894
     * Check whether to use Pcntl.
895
     *
896
     * If `setUsePcntl` has been set to true, but Pcntl is not actually
897
     * available, this will return false.
898
     *
899
     * @return bool True if the current Shell should use Pcntl
900
     */
901
    public function usePcntl(): bool
902
    {
903
        if (!isset($this->usePcntl)) {
904
            // Unless pcntl is explicitly *enabled*, don't use it while XDebug is debugging.
905
            // See https://github.com/bobthecow/psysh/issues/742
906
            if (\function_exists('xdebug_is_debugger_active') && \xdebug_is_debugger_active()) {
907
                return false;
908
            }
909
 
910
            return $this->hasPcntl;
911
        }
912
 
913
        return $this->hasPcntl && $this->usePcntl;
914
    }
915
 
916
    /**
917
     * Check whether to use raw output.
918
     *
919
     * This is set by the --raw-output (-r) flag, and really only makes sense
920
     * when non-interactive, e.g. executing stdin.
921
     *
922
     * @return bool true if raw output is enabled
923
     */
924
    public function rawOutput(): bool
925
    {
926
        return $this->rawOutput;
927
    }
928
 
929
    /**
930
     * Enable or disable raw output.
931
     *
932
     * @param bool $rawOutput
933
     */
934
    public function setRawOutput(bool $rawOutput)
935
    {
936
        $this->rawOutput = (bool) $rawOutput;
937
    }
938
 
939
    /**
940
     * Enable or disable strict requirement of semicolons.
941
     *
942
     * @see self::requireSemicolons()
943
     *
944
     * @param bool $requireSemicolons
945
     */
946
    public function setRequireSemicolons(bool $requireSemicolons)
947
    {
948
        $this->requireSemicolons = (bool) $requireSemicolons;
949
    }
950
 
951
    /**
952
     * Check whether to require semicolons on all statements.
953
     *
954
     * By default, PsySH will automatically insert semicolons at the end of
955
     * statements if they're missing. To strictly require semicolons, set
956
     * `requireSemicolons` to true.
957
     */
958
    public function requireSemicolons(): bool
959
    {
960
        return $this->requireSemicolons;
961
    }
962
 
963
    /**
964
     * Enable or disable Unicode in PsySH specific output.
965
     *
966
     * Note that this does not disable Unicode output in general, it just makes
967
     * it so PsySH won't output any itself.
968
     *
969
     * @param bool $useUnicode
970
     */
971
    public function setUseUnicode(bool $useUnicode)
972
    {
973
        $this->useUnicode = (bool) $useUnicode;
974
    }
975
 
976
    /**
977
     * Check whether to use Unicode in PsySH specific output.
978
     *
979
     * Note that this does not disable Unicode output in general, it just makes
980
     * it so PsySH won't output any itself.
981
     */
982
    public function useUnicode(): bool
983
    {
984
        if (isset($this->useUnicode)) {
985
            return $this->useUnicode;
986
        }
987
 
988
        // @todo detect `chsh` != 65001 on Windows and return false
989
        return true;
990
    }
991
 
992
    /**
993
     * Set the error logging level.
994
     *
995
     * @see self::errorLoggingLevel
996
     *
997
     * @param int $errorLoggingLevel
998
     */
999
    public function setErrorLoggingLevel($errorLoggingLevel)
1000
    {
1001
        $this->errorLoggingLevel = (\E_ALL | \E_STRICT) & $errorLoggingLevel;
1002
    }
1003
 
1004
    /**
1005
     * Get the current error logging level.
1006
     *
1007
     * By default, PsySH will automatically log all errors, regardless of the
1008
     * current `error_reporting` level.
1009
     *
1010
     * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it
1011
     * to any valid error_reporting value to log only errors which match that
1012
     * level.
1013
     *
1014
     *     http://php.net/manual/en/function.error-reporting.php
1015
     */
1016
    public function errorLoggingLevel(): int
1017
    {
1018
        return $this->errorLoggingLevel;
1019
    }
1020
 
1021
    /**
1022
     * Set a CodeCleaner service instance.
1023
     *
1024
     * @param CodeCleaner $cleaner
1025
     */
1026
    public function setCodeCleaner(CodeCleaner $cleaner)
1027
    {
1028
        $this->cleaner = $cleaner;
1029
    }
1030
 
1031
    /**
1032
     * Get a CodeCleaner service instance.
1033
     *
1034
     * If none has been explicitly defined, this will create a new instance.
1035
     */
1036
    public function getCodeCleaner(): CodeCleaner
1037
    {
1038
        if (!isset($this->cleaner)) {
1039
            $this->cleaner = new CodeCleaner(null, null, null, $this->yolo());
1040
        }
1041
 
1042
        return $this->cleaner;
1043
    }
1044
 
1045
    /**
1046
     * Enable or disable running PsySH without input validation.
1047
     *
1048
     * You don't want this.
1049
     */
1050
    public function setYolo($yolo)
1051
    {
1052
        $this->yolo = (bool) $yolo;
1053
    }
1054
 
1055
    /**
1056
     * Check whether to disable input validation.
1057
     */
1058
    public function yolo(): bool
1059
    {
1060
        return $this->yolo;
1061
    }
1062
 
1063
    /**
1064
     * Enable or disable tab completion.
1065
     *
1066
     * @param bool $useTabCompletion
1067
     */
1068
    public function setUseTabCompletion(bool $useTabCompletion)
1069
    {
1070
        $this->useTabCompletion = (bool) $useTabCompletion;
1071
    }
1072
 
1073
    /**
1074
     * @deprecated Call `setUseTabCompletion` instead
1075
     *
1076
     * @param bool $useTabCompletion
1077
     */
1078
    public function setTabCompletion(bool $useTabCompletion)
1079
    {
1080
        $this->setUseTabCompletion($useTabCompletion);
1081
    }
1082
 
1083
    /**
1084
     * Check whether to use tab completion.
1085
     *
1086
     * If `setUseTabCompletion` has been set to true, but readline is not
1087
     * actually available, this will return false.
1088
     *
1089
     * @return bool True if the current Shell should use tab completion
1090
     */
1091
    public function useTabCompletion(): bool
1092
    {
1093
        return isset($this->useTabCompletion) ? ($this->hasReadline && $this->useTabCompletion) : $this->hasReadline;
1094
    }
1095
 
1096
    /**
1097
     * @deprecated Call `useTabCompletion` instead
1098
     */
1099
    public function getTabCompletion(): bool
1100
    {
1101
        return $this->useTabCompletion();
1102
    }
1103
 
1104
    /**
1105
     * Set the Shell Output service.
1106
     *
1107
     * @param ShellOutput $output
1108
     */
1109
    public function setOutput(ShellOutput $output)
1110
    {
1111
        $this->output = $output;
1112
        $this->pipedOutput = null; // Reset cached pipe info
1113
 
1114
        if (isset($this->theme)) {
1115
            $output->setTheme($this->theme);
1116
        }
1117
 
1118
        $this->applyFormatterStyles();
1119
    }
1120
 
1121
    /**
1122
     * Get a Shell Output service instance.
1123
     *
1124
     * If none has been explicitly provided, this will create a new instance
1125
     * with the configured verbosity and output pager supplied by self::getPager
1126
     *
1127
     * @see self::verbosity
1128
     * @see self::getPager
1129
     */
1130
    public function getOutput(): ShellOutput
1131
    {
1132
        if (!isset($this->output)) {
1133
            $this->setOutput(new ShellOutput(
1134
                $this->getOutputVerbosity(),
1135
                null,
1136
                null,
1137
                $this->getPager() ?: null,
1138
                $this->theme()
1139
            ));
1140
 
1141
            // This is racy because `getOutputDecorated` needs access to the
1142
            // output stream to figure out if it's piped or not, so create it
1143
            // first, then update after we have a stream.
1144
            $decorated = $this->getOutputDecorated();
1145
            if ($decorated !== null) {
1146
                $this->output->setDecorated($decorated);
1147
            }
1148
        }
1149
 
1150
        return $this->output;
1151
    }
1152
 
1153
    /**
1154
     * Get the decoration (i.e. color) setting for the Shell Output service.
1155
     *
1156
     * @return bool|null 3-state boolean corresponding to the current color mode
1157
     */
1158
    public function getOutputDecorated()
1159
    {
1160
        switch ($this->colorMode()) {
1161
            case self::COLOR_MODE_FORCED:
1162
                return true;
1163
            case self::COLOR_MODE_DISABLED:
1164
                return false;
1165
            case self::COLOR_MODE_AUTO:
1166
            default:
1167
                return $this->outputIsPiped() ? false : null;
1168
        }
1169
    }
1170
 
1171
    /**
1172
     * Get the interactive setting for shell input.
1173
     */
1174
    public function getInputInteractive(): bool
1175
    {
1176
        switch ($this->interactiveMode()) {
1177
            case self::INTERACTIVE_MODE_FORCED:
1178
                return true;
1179
            case self::INTERACTIVE_MODE_DISABLED:
1180
                return false;
1181
            case self::INTERACTIVE_MODE_AUTO:
1182
            default:
1183
                return !$this->inputIsPiped();
1184
        }
1185
    }
1186
 
1187
    /**
1188
     * Set the OutputPager service.
1189
     *
1190
     * If a string is supplied, a ProcOutputPager will be used which shells out
1191
     * to the specified command.
1192
     *
1193
     * `cat` is special-cased to use the PassthruPager directly.
1194
     *
1195
     * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance
1196
     *
1197
     * @param string|OutputPager|false $pager
1198
     */
1199
    public function setPager($pager)
1200
    {
1201
        if ($pager === null || $pager === false || $pager === 'cat') {
1202
            $pager = false;
1203
        }
1204
 
1205
        if ($pager !== false && !\is_string($pager) && !$pager instanceof OutputPager) {
1206
            throw new \InvalidArgumentException('Unexpected pager instance');
1207
        }
1208
 
1209
        $this->pager = $pager;
1210
    }
1211
 
1212
    /**
1213
     * Get an OutputPager instance or a command for an external Proc pager.
1214
     *
1215
     * If no Pager has been explicitly provided, and Pcntl is available, this
1216
     * will default to `cli.pager` ini value, falling back to `which less`.
1217
     *
1218
     * @return string|OutputPager|false
1219
     */
1220
    public function getPager()
1221
    {
1222
        if (!isset($this->pager) && $this->usePcntl()) {
1223
            if (\getenv('TERM') === 'dumb') {
1224
                return false;
1225
            }
1226
 
1227
            if ($pager = \ini_get('cli.pager')) {
1228
                // use the default pager
1229
                $this->pager = $pager;
1230
            } elseif ($less = $this->configPaths->which('less')) {
1231
                // check for the presence of less...
1232
                $this->pager = $less.' -R -F -X';
1233
            }
1234
        }
1235
 
1236
        return $this->pager;
1237
    }
1238
 
1239
    /**
1240
     * Set the Shell AutoCompleter service.
1241
     *
1242
     * @param AutoCompleter $autoCompleter
1243
     */
1244
    public function setAutoCompleter(AutoCompleter $autoCompleter)
1245
    {
1246
        $this->autoCompleter = $autoCompleter;
1247
    }
1248
 
1249
    /**
1250
     * Get an AutoCompleter service instance.
1251
     */
1252
    public function getAutoCompleter(): AutoCompleter
1253
    {
1254
        if (!isset($this->autoCompleter)) {
1255
            $this->autoCompleter = new AutoCompleter();
1256
        }
1257
 
1258
        return $this->autoCompleter;
1259
    }
1260
 
1261
    /**
1262
     * @deprecated Nothing should be using this anymore
1263
     */
1264
    public function getTabCompletionMatchers(): array
1265
    {
1266
        return [];
1267
    }
1268
 
1269
    /**
1270
     * Add tab completion matchers to the AutoCompleter.
1271
     *
1272
     * This will buffer new matchers in the event that the Shell has not yet
1273
     * been instantiated. This allows the user to specify matchers in their
1274
     * config rc file, despite the fact that their file is needed in the Shell
1275
     * constructor.
1276
     *
1277
     * @param array $matchers
1278
     */
1279
    public function addMatchers(array $matchers)
1280
    {
1281
        $this->newMatchers = \array_merge($this->newMatchers, $matchers);
1282
        if (isset($this->shell)) {
1283
            $this->doAddMatchers();
1284
        }
1285
    }
1286
 
1287
    /**
1288
     * Internal method for adding tab completion matchers. This will set any new
1289
     * matchers once a Shell is available.
1290
     */
1291
    private function doAddMatchers()
1292
    {
1293
        if (!empty($this->newMatchers)) {
1294
            $this->shell->addMatchers($this->newMatchers);
1295
            $this->newMatchers = [];
1296
        }
1297
    }
1298
 
1299
    /**
1300
     * @deprecated Use `addMatchers` instead
1301
     *
1302
     * @param array $matchers
1303
     */
1304
    public function addTabCompletionMatchers(array $matchers)
1305
    {
1306
        $this->addMatchers($matchers);
1307
    }
1308
 
1309
    /**
1310
     * Add commands to the Shell.
1311
     *
1312
     * This will buffer new commands in the event that the Shell has not yet
1313
     * been instantiated. This allows the user to specify commands in their
1314
     * config rc file, despite the fact that their file is needed in the Shell
1315
     * constructor.
1316
     *
1317
     * @param array $commands
1318
     */
1319
    public function addCommands(array $commands)
1320
    {
1321
        $this->newCommands = \array_merge($this->newCommands, $commands);
1322
        if (isset($this->shell)) {
1323
            $this->doAddCommands();
1324
        }
1325
    }
1326
 
1327
    /**
1328
     * Internal method for adding commands. This will set any new commands once
1329
     * a Shell is available.
1330
     */
1331
    private function doAddCommands()
1332
    {
1333
        if (!empty($this->newCommands)) {
1334
            $this->shell->addCommands($this->newCommands);
1335
            $this->newCommands = [];
1336
        }
1337
    }
1338
 
1339
    /**
1340
     * Set the Shell backreference and add any new commands to the Shell.
1341
     *
1342
     * @param Shell $shell
1343
     */
1344
    public function setShell(Shell $shell)
1345
    {
1346
        $this->shell = $shell;
1347
        $this->doAddCommands();
1348
        $this->doAddMatchers();
1349
    }
1350
 
1351
    /**
1352
     * Set the PHP manual database file.
1353
     *
1354
     * This file should be an SQLite database generated from the phpdoc source
1355
     * with the `bin/build_manual` script.
1356
     *
1357
     * @param string $filename
1358
     */
1359
    public function setManualDbFile(string $filename)
1360
    {
1361
        $this->manualDbFile = (string) $filename;
1362
    }
1363
 
1364
    /**
1365
     * Get the current PHP manual database file.
1366
     *
1367
     * @return string|null Default: '~/.local/share/psysh/php_manual.sqlite'
1368
     */
1369
    public function getManualDbFile()
1370
    {
1371
        if (isset($this->manualDbFile)) {
1372
            return $this->manualDbFile;
1373
        }
1374
 
1375
        $files = $this->configPaths->dataFiles(['php_manual.sqlite']);
1376
        if (!empty($files)) {
1377
            if ($this->warnOnMultipleConfigs && \count($files) > 1) {
1378
                $msg = \sprintf('Multiple manual database files found: %s. Using %s', \implode(', ', $files), $files[0]);
1379
                \trigger_error($msg, \E_USER_NOTICE);
1380
            }
1381
 
1382
            return $this->manualDbFile = $files[0];
1383
        }
1384
    }
1385
 
1386
    /**
1387
     * Get a PHP manual database connection.
1388
     *
1389
     * @return \PDO|null
1390
     */
1391
    public function getManualDb()
1392
    {
1393
        if (!isset($this->manualDb)) {
1394
            $dbFile = $this->getManualDbFile();
1395
            if ($dbFile !== null && \is_file($dbFile)) {
1396
                try {
1397
                    $this->manualDb = new \PDO('sqlite:'.$dbFile);
1398
                } catch (\PDOException $e) {
1399
                    if ($e->getMessage() === 'could not find driver') {
1400
                        throw new RuntimeException('SQLite PDO driver not found', 0, $e);
1401
                    } else {
1402
                        throw $e;
1403
                    }
1404
                }
1405
            }
1406
        }
1407
 
1408
        return $this->manualDb;
1409
    }
1410
 
1411
    /**
1412
     * Add an array of casters definitions.
1413
     *
1414
     * @param array $casters
1415
     */
1416
    public function addCasters(array $casters)
1417
    {
1418
        $this->getPresenter()->addCasters($casters);
1419
    }
1420
 
1421
    /**
1422
     * Get the Presenter service.
1423
     */
1424
    public function getPresenter(): Presenter
1425
    {
1426
        if (!isset($this->presenter)) {
1427
            $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes());
1428
        }
1429
 
1430
        return $this->presenter;
1431
    }
1432
 
1433
    /**
1434
     * Enable or disable warnings on multiple configuration or data files.
1435
     *
1436
     * @see self::warnOnMultipleConfigs()
1437
     *
1438
     * @param bool $warnOnMultipleConfigs
1439
     */
1440
    public function setWarnOnMultipleConfigs(bool $warnOnMultipleConfigs)
1441
    {
1442
        $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs;
1443
    }
1444
 
1445
    /**
1446
     * Check whether to warn on multiple configuration or data files.
1447
     *
1448
     * By default, PsySH will use the file with highest precedence, and will
1449
     * silently ignore all others. With this enabled, a warning will be emitted
1450
     * (but not an exception thrown) if multiple configuration or data files
1451
     * are found.
1452
     *
1453
     * This will default to true in a future release, but is false for now.
1454
     */
1455
    public function warnOnMultipleConfigs(): bool
1456
    {
1457
        return $this->warnOnMultipleConfigs;
1458
    }
1459
 
1460
    /**
1461
     * Set the current color mode.
1462
     *
200 lars 1463
     * @throws \InvalidArgumentException if the color mode isn't auto, forced or disabled
1464
     *
148 lars 1465
     * @param string $colorMode
1466
     */
1467
    public function setColorMode(string $colorMode)
1468
    {
1469
        $validColorModes = [
1470
            self::COLOR_MODE_AUTO,
1471
            self::COLOR_MODE_FORCED,
1472
            self::COLOR_MODE_DISABLED,
1473
        ];
1474
 
1475
        if (!\in_array($colorMode, $validColorModes)) {
1476
            throw new \InvalidArgumentException('Invalid color mode: '.$colorMode);
1477
        }
1478
 
1479
        $this->colorMode = $colorMode;
1480
    }
1481
 
1482
    /**
1483
     * Get the current color mode.
1484
     */
1485
    public function colorMode(): string
1486
    {
1487
        return $this->colorMode;
1488
    }
1489
 
1490
    /**
1491
     * Set the shell's interactive mode.
1492
     *
1493
     * @throws \InvalidArgumentException if interactive mode isn't disabled, forced, or auto
1494
     *
1495
     * @param string $interactiveMode
1496
     */
1497
    public function setInteractiveMode(string $interactiveMode)
1498
    {
1499
        $validInteractiveModes = [
1500
            self::INTERACTIVE_MODE_AUTO,
1501
            self::INTERACTIVE_MODE_FORCED,
1502
            self::INTERACTIVE_MODE_DISABLED,
1503
        ];
1504
 
1505
        if (!\in_array($interactiveMode, $validInteractiveModes)) {
1506
            throw new \InvalidArgumentException('Invalid interactive mode: '.$interactiveMode);
1507
        }
1508
 
1509
        $this->interactiveMode = $interactiveMode;
1510
    }
1511
 
1512
    /**
1513
     * Get the current interactive mode.
1514
     */
1515
    public function interactiveMode(): string
1516
    {
1517
        return $this->interactiveMode;
1518
    }
1519
 
1520
    /**
1521
     * Set an update checker service instance.
1522
     *
1523
     * @param Checker $checker
1524
     */
1525
    public function setChecker(Checker $checker)
1526
    {
1527
        $this->checker = $checker;
1528
    }
1529
 
1530
    /**
1531
     * Get an update checker service instance.
1532
     *
1533
     * If none has been explicitly defined, this will create a new instance.
1534
     */
1535
    public function getChecker(): Checker
1536
    {
1537
        if (!isset($this->checker)) {
1538
            $interval = $this->getUpdateCheck();
1539
            switch ($interval) {
1540
                case Checker::ALWAYS:
1541
                    $this->checker = new GitHubChecker();
1542
                    break;
1543
 
1544
                case Checker::DAILY:
1545
                case Checker::WEEKLY:
1546
                case Checker::MONTHLY:
1547
                    $checkFile = $this->getUpdateCheckCacheFile();
1548
                    if ($checkFile === false) {
1549
                        $this->checker = new NoopChecker();
1550
                    } else {
1551
                        $this->checker = new IntervalChecker($checkFile, $interval);
1552
                    }
1553
                    break;
1554
 
1555
                case Checker::NEVER:
1556
                    $this->checker = new NoopChecker();
1557
                    break;
1558
            }
1559
        }
1560
 
1561
        return $this->checker;
1562
    }
1563
 
1564
    /**
1565
     * Get the current update check interval.
1566
     *
1567
     * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is
1568
     * explicitly set, default to 'weekly'.
1569
     */
1570
    public function getUpdateCheck(): string
1571
    {
1572
        return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY;
1573
    }
1574
 
1575
    /**
1576
     * Set the update check interval.
1577
     *
1578
     * @throws \InvalidArgumentException if the update check interval is unknown
1579
     *
1580
     * @param string $interval
1581
     */
1582
    public function setUpdateCheck(string $interval)
1583
    {
1584
        $validIntervals = [
1585
            Checker::ALWAYS,
1586
            Checker::DAILY,
1587
            Checker::WEEKLY,
1588
            Checker::MONTHLY,
1589
            Checker::NEVER,
1590
        ];
1591
 
1592
        if (!\in_array($interval, $validIntervals)) {
1593
            throw new \InvalidArgumentException('Invalid update check interval: '.$interval);
1594
        }
1595
 
1596
        $this->updateCheck = $interval;
1597
    }
1598
 
1599
    /**
1600
     * Get a cache file path for the update checker.
1601
     *
1602
     * @return string|false Return false if config file/directory is not writable
1603
     */
1604
    public function getUpdateCheckCacheFile()
1605
    {
1606
        return ConfigPaths::touchFileWithMkdir($this->configPaths->currentConfigDir().'/update_check.json');
1607
    }
1608
 
1609
    /**
1610
     * Set the startup message.
1611
     *
1612
     * @param string $message
1613
     */
1614
    public function setStartupMessage(string $message)
1615
    {
1616
        $this->startupMessage = $message;
1617
    }
1618
 
1619
    /**
1620
     * Get the startup message.
1621
     *
1622
     * @return string|null
1623
     */
1624
    public function getStartupMessage()
1625
    {
1626
        return $this->startupMessage;
1627
    }
1628
 
1629
    /**
1630
     * Set the prompt.
1631
     *
991 lars 1632
     * @deprecated The `prompt` configuration has been replaced by Themes and support will
1633
     * eventually be removed. In the meantime, prompt is applied first by the Theme, then overridden
1634
     * by any explicitly defined prompt.
1635
     *
1636
     * Note that providing a prompt but not a theme config will implicitly use the `classic` theme.
148 lars 1637
     */
1638
    public function setPrompt(string $prompt)
1639
    {
1640
        $this->prompt = $prompt;
991 lars 1641
 
1642
        if (isset($this->theme)) {
1643
            $this->theme->setPrompt($prompt);
1644
        }
148 lars 1645
    }
1646
 
1647
    /**
1648
     * Get the prompt.
1649
     *
1650
     * @return string|null
1651
     */
1652
    public function getPrompt()
1653
    {
1654
        return $this->prompt;
1655
    }
1656
 
1657
    /**
1658
     * Get the force array indexes.
1659
     */
1660
    public function forceArrayIndexes(): bool
1661
    {
1662
        return $this->forceArrayIndexes;
1663
    }
1664
 
1665
    /**
1666
     * Set the force array indexes.
1667
     *
1668
     * @param bool $forceArrayIndexes
1669
     */
1670
    public function setForceArrayIndexes(bool $forceArrayIndexes)
1671
    {
1672
        $this->forceArrayIndexes = $forceArrayIndexes;
1673
    }
1674
 
1675
    /**
1676
     * Set the current output Theme.
1677
     *
1678
     * @param Theme|string|array $theme Theme (or Theme config)
1679
     */
1680
    public function setTheme($theme)
1681
    {
1682
        if (!$theme instanceof Theme) {
1683
            $theme = new Theme($theme);
1684
        }
1685
 
1686
        $this->theme = $theme;
1687
 
991 lars 1688
        if (isset($this->prompt)) {
1689
            $this->theme->setPrompt($this->prompt);
1690
        }
1691
 
148 lars 1692
        if (isset($this->output)) {
1693
            $this->output->setTheme($theme);
1694
            $this->applyFormatterStyles();
1695
        }
1696
    }
1697
 
1698
    /**
1699
     * Get the current output Theme.
1700
     */
1701
    public function theme(): Theme
1702
    {
1703
        if (!isset($this->theme)) {
991 lars 1704
            // If a prompt is explicitly set, and a theme is not, base it on the `classic` theme.
1705
            $this->theme = $this->prompt ? new Theme('classic') : new Theme();
148 lars 1706
        }
1707
 
991 lars 1708
        if (isset($this->prompt)) {
1709
            $this->theme->setPrompt($this->prompt);
1710
        }
1711
 
148 lars 1712
        return $this->theme;
1713
    }
1714
 
1715
    /**
1716
     * Set the shell output formatter styles.
1717
     *
1718
     * Accepts a map from style name to [fg, bg, options], for example:
1719
     *
1720
     *     [
1721
     *         'error' => ['white', 'red', ['bold']],
1722
     *         'warning' => ['black', 'yellow'],
1723
     *     ]
1724
     *
1725
     * Foreground, background or options can be null, or even omitted entirely.
1726
     *
1727
     * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will
1728
     * eventually be removed. In the meantime, styles are applied first by the Theme, then
1729
     * overridden by any explicitly defined formatter styles.
1730
     */
1731
    public function setFormatterStyles(array $formatterStyles)
1732
    {
1733
        foreach ($formatterStyles as $name => $style) {
1734
            $this->formatterStyles[$name] = new OutputFormatterStyle(...$style);
1735
        }
1736
 
1737
        if (isset($this->output)) {
1738
            $this->applyFormatterStyles();
1739
        }
1740
    }
1741
 
1742
    /**
1743
     * Internal method for applying output formatter style customization.
1744
     *
1745
     * This is called on initialization of the shell output, and again if the
1746
     * formatter styles config is updated.
1747
     *
1748
     * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will
1749
     * eventually be removed. In the meantime, styles are applied first by the Theme, then
1750
     * overridden by any explicitly defined formatter styles.
1751
     */
1752
    private function applyFormatterStyles()
1753
    {
1754
        $formatter = $this->output->getFormatter();
1755
        foreach ($this->formatterStyles as $name => $style) {
1756
            $formatter->setStyle($name, $style);
1757
        }
1758
 
1759
        $errorFormatter = $this->output->getErrorOutput()->getFormatter();
1760
        foreach (Theme::ERROR_STYLES as $name) {
1761
            if (isset($this->formatterStyles[$name])) {
1762
                $errorFormatter->setStyle($name, $this->formatterStyles[$name]);
1763
            }
1764
        }
1765
    }
1766
 
1767
    /**
1768
     * Get the configured output verbosity.
1769
     */
1770
    public function verbosity(): string
1771
    {
1772
        return $this->verbosity;
1773
    }
1774
 
1775
    /**
1776
     * Set the shell output verbosity.
1777
     *
1778
     * Accepts OutputInterface verbosity constants.
1779
     *
1780
     * @throws \InvalidArgumentException if verbosity level is invalid
1781
     *
1782
     * @param string $verbosity
1783
     */
1784
    public function setVerbosity(string $verbosity)
1785
    {
1786
        $validVerbosityLevels = [
1787
            self::VERBOSITY_QUIET,
1788
            self::VERBOSITY_NORMAL,
1789
            self::VERBOSITY_VERBOSE,
1790
            self::VERBOSITY_VERY_VERBOSE,
1791
            self::VERBOSITY_DEBUG,
1792
        ];
1793
 
1794
        if (!\in_array($verbosity, $validVerbosityLevels)) {
1795
            throw new \InvalidArgumentException('Invalid verbosity level: '.$verbosity);
1796
        }
1797
 
1798
        $this->verbosity = $verbosity;
1799
 
1800
        if (isset($this->output)) {
1801
            $this->output->setVerbosity($this->getOutputVerbosity());
1802
        }
1803
    }
1804
 
1805
    /**
1806
     * Map the verbosity configuration to OutputInterface verbosity constants.
1807
     *
1808
     * @return int OutputInterface verbosity level
1809
     */
1810
    public function getOutputVerbosity(): int
1811
    {
1812
        switch ($this->verbosity()) {
1813
            case self::VERBOSITY_QUIET:
1814
                return OutputInterface::VERBOSITY_QUIET;
1815
            case self::VERBOSITY_VERBOSE:
1816
                return OutputInterface::VERBOSITY_VERBOSE;
1817
            case self::VERBOSITY_VERY_VERBOSE:
1818
                return OutputInterface::VERBOSITY_VERY_VERBOSE;
1819
            case self::VERBOSITY_DEBUG:
1820
                return OutputInterface::VERBOSITY_DEBUG;
1821
            case self::VERBOSITY_NORMAL:
1822
            default:
1823
                return OutputInterface::VERBOSITY_NORMAL;
1824
        }
1825
    }
1826
 
1827
    /**
1828
     * Guess whether stdin is piped.
1829
     *
1830
     * This is mostly useful for deciding whether to use non-interactive mode.
1831
     */
1832
    public function inputIsPiped(): bool
1833
    {
1834
        if ($this->pipedInput === null) {
200 lars 1835
            $this->pipedInput = \defined('STDIN') && self::looksLikeAPipe(\STDIN);
148 lars 1836
        }
1837
 
1838
        return $this->pipedInput;
1839
    }
1840
 
1841
    /**
1842
     * Guess whether shell output is piped.
1843
     *
1844
     * This is mostly useful for deciding whether to use non-decorated output.
1845
     */
1846
    public function outputIsPiped(): bool
1847
    {
1848
        if ($this->pipedOutput === null) {
200 lars 1849
            $this->pipedOutput = self::looksLikeAPipe($this->getOutput()->getStream());
148 lars 1850
        }
1851
 
1852
        return $this->pipedOutput;
1853
    }
1854
 
1855
    /**
1856
     * Guess whether an input or output stream is piped.
1857
     *
1858
     * @param resource|int $stream
1859
     */
1860
    private static function looksLikeAPipe($stream): bool
1861
    {
1862
        if (\function_exists('posix_isatty')) {
1863
            return !\posix_isatty($stream);
1864
        }
1865
 
1866
        $stat = \fstat($stream);
1867
        $mode = $stat['mode'] & 0170000;
1868
 
1869
        return $mode === 0010000 || $mode === 0040000 || $mode === 0100000 || $mode === 0120000;
1870
    }
1871
}