Subversion-Projekte lars-tiefland.laravel_shop

Revision

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

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of Psy Shell.
5
 *
6
 * (c) 2012-2022 Justin Hileman
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Psy;
13
 
14
use Psy\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;
121
    private $output;
122
    private $shell;
123
    private $cleaner;
124
    private $pager;
125
    private $manualDb;
126
    private $presenter;
127
    private $autoCompleter;
128
    private $checker;
129
    private $prompt;
130
    private $configPaths;
131
 
132
    /**
133
     * Construct a Configuration instance.
134
     *
135
     * Optionally, supply an array of configuration values to load.
136
     *
137
     * @param array $config Optional array of configuration values
138
     */
139
    public function __construct(array $config = [])
140
    {
141
        $this->configPaths = new ConfigPaths();
142
 
143
        // explicit configFile option
144
        if (isset($config['configFile'])) {
145
            $this->configFile = $config['configFile'];
146
        } elseif (isset($_SERVER['PSYSH_CONFIG']) && $_SERVER['PSYSH_CONFIG']) {
147
            $this->configFile = $_SERVER['PSYSH_CONFIG'];
148
        }
149
 
150
        // legacy baseDir option
151
        if (isset($config['baseDir'])) {
152
            $msg = "The 'baseDir' configuration option is deprecated; ".
153
                "please specify 'configDir' and 'dataDir' options instead";
154
            throw new DeprecatedException($msg);
155
        }
156
 
157
        unset($config['configFile'], $config['baseDir']);
158
 
159
        // go go gadget, config!
160
        $this->loadConfig($config);
161
        $this->init();
162
    }
163
 
164
    /**
165
     * Construct a Configuration object from Symfony Console input.
166
     *
167
     * This is great for adding psysh-compatible command line options to framework- or app-specific
168
     * wrappers.
169
     *
170
     * $input should already be bound to an appropriate InputDefinition (see self::getInputOptions
171
     * if you want to build your own) before calling this method. It's not required, but things work
172
     * a lot better if we do.
173
     *
174
     * @see self::getInputOptions
175
     *
176
     * @throws \InvalidArgumentException
177
     *
178
     * @param InputInterface $input
179
     *
180
     * @return self
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
     *
539
     * @return array
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
     * @return string
623
     */
624
    public function getRuntimeDir(): string
625
    {
626
        $runtimeDir = $this->configPaths->runtimeDir();
627
 
628
        if (!\is_dir($runtimeDir)) {
629
            if (!@\mkdir($runtimeDir, 0700, true)) {
630
                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)));
631
            }
632
        }
633
 
634
        return $runtimeDir;
635
    }
636
 
637
    /**
638
     * Set the readline history file path.
639
     *
640
     * @param string $file
641
     */
642
    public function setHistoryFile(string $file)
643
    {
644
        $this->historyFile = ConfigPaths::touchFileWithMkdir($file);
645
    }
646
 
647
    /**
648
     * Get the readline history file path.
649
     *
650
     * Defaults to `/history` inside the shell's base config dir unless
651
     * explicitly overridden.
652
     *
653
     * @return string
654
     */
655
    public function getHistoryFile(): string
656
    {
657
        if (isset($this->historyFile)) {
658
            return $this->historyFile;
659
        }
660
 
661
        $files = $this->configPaths->configFiles(['psysh_history', 'history']);
662
 
663
        if (!empty($files)) {
664
            if ($this->warnOnMultipleConfigs && \count($files) > 1) {
665
                $msg = \sprintf('Multiple history files found: %s. Using %s', \implode(', ', $files), $files[0]);
666
                \trigger_error($msg, \E_USER_NOTICE);
667
            }
668
 
669
            $this->setHistoryFile($files[0]);
670
        } else {
671
            // fallback: create our own history file
672
            $this->setHistoryFile($this->configPaths->currentConfigDir().'/psysh_history');
673
        }
674
 
675
        return $this->historyFile;
676
    }
677
 
678
    /**
679
     * Set the readline max history size.
680
     *
681
     * @param int $value
682
     */
683
    public function setHistorySize(int $value)
684
    {
685
        $this->historySize = (int) $value;
686
    }
687
 
688
    /**
689
     * Get the readline max history size.
690
     *
691
     * @return int
692
     */
693
    public function getHistorySize()
694
    {
695
        return $this->historySize;
696
    }
697
 
698
    /**
699
     * Sets whether readline erases old duplicate history entries.
700
     *
701
     * @param bool $value
702
     */
703
    public function setEraseDuplicates(bool $value)
704
    {
705
        $this->eraseDuplicates = (bool) $value;
706
    }
707
 
708
    /**
709
     * Get whether readline erases old duplicate history entries.
710
     *
711
     * @return bool|null
712
     */
713
    public function getEraseDuplicates()
714
    {
715
        return $this->eraseDuplicates;
716
    }
717
 
718
    /**
719
     * Get a temporary file of type $type for process $pid.
720
     *
721
     * The file will be created inside the current temporary directory.
722
     *
723
     * @see self::getRuntimeDir
724
     *
725
     * @param string $type
726
     * @param int    $pid
727
     *
728
     * @return string Temporary file name
729
     */
730
    public function getTempFile(string $type, int $pid): string
731
    {
732
        return \tempnam($this->getRuntimeDir(), $type.'_'.$pid.'_');
733
    }
734
 
735
    /**
736
     * Get a filename suitable for a FIFO pipe of $type for process $pid.
737
     *
738
     * The pipe will be created inside the current temporary directory.
739
     *
740
     * @param string $type
741
     * @param int    $pid
742
     *
743
     * @return string Pipe name
744
     */
745
    public function getPipe(string $type, int $pid): string
746
    {
747
        return \sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid);
748
    }
749
 
750
    /**
751
     * Check whether this PHP instance has Readline available.
752
     *
753
     * @return bool True if Readline is available
754
     */
755
    public function hasReadline(): bool
756
    {
757
        return $this->hasReadline;
758
    }
759
 
760
    /**
761
     * Enable or disable Readline usage.
762
     *
763
     * @param bool $useReadline
764
     */
765
    public function setUseReadline(bool $useReadline)
766
    {
767
        $this->useReadline = (bool) $useReadline;
768
    }
769
 
770
    /**
771
     * Check whether to use Readline.
772
     *
773
     * If `setUseReadline` as been set to true, but Readline is not actually
774
     * available, this will return false.
775
     *
776
     * @return bool True if the current Shell should use Readline
777
     */
778
    public function useReadline(): bool
779
    {
780
        return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline;
781
    }
782
 
783
    /**
784
     * Set the Psy Shell readline service.
785
     *
786
     * @param Readline\Readline $readline
787
     */
788
    public function setReadline(Readline\Readline $readline)
789
    {
790
        $this->readline = $readline;
791
    }
792
 
793
    /**
794
     * Get the Psy Shell readline service.
795
     *
796
     * By default, this service uses (in order of preference):
797
     *
798
     *  * GNU Readline
799
     *  * Libedit
800
     *  * A transient array-based readline emulation.
801
     *
802
     * @return Readline\Readline
803
     */
804
    public function getReadline(): Readline\Readline
805
    {
806
        if (!isset($this->readline)) {
807
            $className = $this->getReadlineClass();
808
            $this->readline = new $className(
809
                $this->getHistoryFile(),
810
                $this->getHistorySize(),
811
                $this->getEraseDuplicates()
812
            );
813
        }
814
 
815
        return $this->readline;
816
    }
817
 
818
    /**
819
     * Get the appropriate Readline implementation class name.
820
     *
821
     * @see self::getReadline
822
     *
823
     * @return string
824
     */
825
    private function getReadlineClass(): string
826
    {
827
        if ($this->useReadline()) {
828
            if (Readline\GNUReadline::isSupported()) {
829
                return Readline\GNUReadline::class;
830
            } elseif (Readline\Libedit::isSupported()) {
831
                return Readline\Libedit::class;
832
            }
833
        }
834
 
835
        if (Readline\Userland::isSupported()) {
836
            return Readline\Userland::class;
837
        }
838
 
839
        return Readline\Transient::class;
840
    }
841
 
842
    /**
843
     * Enable or disable bracketed paste.
844
     *
845
     * Note that this only works with readline (not libedit) integration for now.
846
     *
847
     * @param bool $useBracketedPaste
848
     */
849
    public function setUseBracketedPaste(bool $useBracketedPaste)
850
    {
851
        $this->useBracketedPaste = (bool) $useBracketedPaste;
852
    }
853
 
854
    /**
855
     * Check whether to use bracketed paste with readline.
856
     *
857
     * When this works, it's magical. Tabs in pastes don't try to autcomplete.
858
     * Newlines in paste don't execute code until you get to the end. It makes
859
     * readline act like you'd expect when pasting.
860
     *
861
     * But it often (usually?) does not work. And when it doesn't, it just spews
862
     * escape codes all over the place and generally makes things ugly :(
863
     *
864
     * If `useBracketedPaste` has been set to true, but the current readline
865
     * implementation is anything besides GNU readline, this will return false.
866
     *
867
     * @return bool True if the shell should use bracketed paste
868
     */
869
    public function useBracketedPaste(): bool
870
    {
871
        $readlineClass = $this->getReadlineClass();
872
 
873
        return $this->useBracketedPaste && $readlineClass::supportsBracketedPaste();
874
 
875
        // @todo mebbe turn this on by default some day?
876
        // return $readlineClass::supportsBracketedPaste() && $this->useBracketedPaste !== false;
877
    }
878
 
879
    /**
880
     * Check whether this PHP instance has Pcntl available.
881
     *
882
     * @return bool True if Pcntl is available
883
     */
884
    public function hasPcntl(): bool
885
    {
886
        return $this->hasPcntl;
887
    }
888
 
889
    /**
890
     * Enable or disable Pcntl usage.
891
     *
892
     * @param bool $usePcntl
893
     */
894
    public function setUsePcntl(bool $usePcntl)
895
    {
896
        $this->usePcntl = (bool) $usePcntl;
897
    }
898
 
899
    /**
900
     * Check whether to use Pcntl.
901
     *
902
     * If `setUsePcntl` has been set to true, but Pcntl is not actually
903
     * available, this will return false.
904
     *
905
     * @return bool True if the current Shell should use Pcntl
906
     */
907
    public function usePcntl(): bool
908
    {
909
        if (!isset($this->usePcntl)) {
910
            // Unless pcntl is explicitly *enabled*, don't use it while XDebug is debugging.
911
            // See https://github.com/bobthecow/psysh/issues/742
912
            if (\function_exists('xdebug_is_debugger_active') && \xdebug_is_debugger_active()) {
913
                return false;
914
            }
915
 
916
            return $this->hasPcntl;
917
        }
918
 
919
        return $this->hasPcntl && $this->usePcntl;
920
    }
921
 
922
    /**
923
     * Check whether to use raw output.
924
     *
925
     * This is set by the --raw-output (-r) flag, and really only makes sense
926
     * when non-interactive, e.g. executing stdin.
927
     *
928
     * @return bool true if raw output is enabled
929
     */
930
    public function rawOutput(): bool
931
    {
932
        return $this->rawOutput;
933
    }
934
 
935
    /**
936
     * Enable or disable raw output.
937
     *
938
     * @param bool $rawOutput
939
     */
940
    public function setRawOutput(bool $rawOutput)
941
    {
942
        $this->rawOutput = (bool) $rawOutput;
943
    }
944
 
945
    /**
946
     * Enable or disable strict requirement of semicolons.
947
     *
948
     * @see self::requireSemicolons()
949
     *
950
     * @param bool $requireSemicolons
951
     */
952
    public function setRequireSemicolons(bool $requireSemicolons)
953
    {
954
        $this->requireSemicolons = (bool) $requireSemicolons;
955
    }
956
 
957
    /**
958
     * Check whether to require semicolons on all statements.
959
     *
960
     * By default, PsySH will automatically insert semicolons at the end of
961
     * statements if they're missing. To strictly require semicolons, set
962
     * `requireSemicolons` to true.
963
     *
964
     * @return bool
965
     */
966
    public function requireSemicolons(): bool
967
    {
968
        return $this->requireSemicolons;
969
    }
970
 
971
    /**
972
     * Enable or disable Unicode in PsySH specific output.
973
     *
974
     * Note that this does not disable Unicode output in general, it just makes
975
     * it so PsySH won't output any itself.
976
     *
977
     * @param bool $useUnicode
978
     */
979
    public function setUseUnicode(bool $useUnicode)
980
    {
981
        $this->useUnicode = (bool) $useUnicode;
982
    }
983
 
984
    /**
985
     * Check whether to use Unicode in PsySH specific output.
986
     *
987
     * Note that this does not disable Unicode output in general, it just makes
988
     * it so PsySH won't output any itself.
989
     *
990
     * @return bool
991
     */
992
    public function useUnicode(): bool
993
    {
994
        if (isset($this->useUnicode)) {
995
            return $this->useUnicode;
996
        }
997
 
998
        // @todo detect `chsh` != 65001 on Windows and return false
999
        return true;
1000
    }
1001
 
1002
    /**
1003
     * Set the error logging level.
1004
     *
1005
     * @see self::errorLoggingLevel
1006
     *
1007
     * @param int $errorLoggingLevel
1008
     */
1009
    public function setErrorLoggingLevel($errorLoggingLevel)
1010
    {
1011
        $this->errorLoggingLevel = (\E_ALL | \E_STRICT) & $errorLoggingLevel;
1012
    }
1013
 
1014
    /**
1015
     * Get the current error logging level.
1016
     *
1017
     * By default, PsySH will automatically log all errors, regardless of the
1018
     * current `error_reporting` level.
1019
     *
1020
     * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it
1021
     * to any valid error_reporting value to log only errors which match that
1022
     * level.
1023
     *
1024
     *     http://php.net/manual/en/function.error-reporting.php
1025
     *
1026
     * @return int
1027
     */
1028
    public function errorLoggingLevel(): int
1029
    {
1030
        return $this->errorLoggingLevel;
1031
    }
1032
 
1033
    /**
1034
     * Set a CodeCleaner service instance.
1035
     *
1036
     * @param CodeCleaner $cleaner
1037
     */
1038
    public function setCodeCleaner(CodeCleaner $cleaner)
1039
    {
1040
        $this->cleaner = $cleaner;
1041
    }
1042
 
1043
    /**
1044
     * Get a CodeCleaner service instance.
1045
     *
1046
     * If none has been explicitly defined, this will create a new instance.
1047
     *
1048
     * @return CodeCleaner
1049
     */
1050
    public function getCodeCleaner(): CodeCleaner
1051
    {
1052
        if (!isset($this->cleaner)) {
1053
            $this->cleaner = new CodeCleaner(null, null, null, $this->yolo());
1054
        }
1055
 
1056
        return $this->cleaner;
1057
    }
1058
 
1059
    /**
1060
     * Enable or disable running PsySH without input validation.
1061
     *
1062
     * You don't want this.
1063
     */
1064
    public function setYolo($yolo)
1065
    {
1066
        $this->yolo = (bool) $yolo;
1067
    }
1068
 
1069
    /**
1070
     * Check whether to disable input validation.
1071
     */
1072
    public function yolo(): bool
1073
    {
1074
        return $this->yolo;
1075
    }
1076
 
1077
    /**
1078
     * Enable or disable tab completion.
1079
     *
1080
     * @param bool $useTabCompletion
1081
     */
1082
    public function setUseTabCompletion(bool $useTabCompletion)
1083
    {
1084
        $this->useTabCompletion = (bool) $useTabCompletion;
1085
    }
1086
 
1087
    /**
1088
     * @deprecated Call `setUseTabCompletion` instead
1089
     *
1090
     * @param bool $useTabCompletion
1091
     */
1092
    public function setTabCompletion(bool $useTabCompletion)
1093
    {
1094
        $this->setUseTabCompletion($useTabCompletion);
1095
    }
1096
 
1097
    /**
1098
     * Check whether to use tab completion.
1099
     *
1100
     * If `setUseTabCompletion` has been set to true, but readline is not
1101
     * actually available, this will return false.
1102
     *
1103
     * @return bool True if the current Shell should use tab completion
1104
     */
1105
    public function useTabCompletion(): bool
1106
    {
1107
        return isset($this->useTabCompletion) ? ($this->hasReadline && $this->useTabCompletion) : $this->hasReadline;
1108
    }
1109
 
1110
    /**
1111
     * @deprecated Call `useTabCompletion` instead
1112
     *
1113
     * @return bool
1114
     */
1115
    public function getTabCompletion(): bool
1116
    {
1117
        return $this->useTabCompletion();
1118
    }
1119
 
1120
    /**
1121
     * Set the Shell Output service.
1122
     *
1123
     * @param ShellOutput $output
1124
     */
1125
    public function setOutput(ShellOutput $output)
1126
    {
1127
        $this->output = $output;
1128
        $this->pipedOutput = null; // Reset cached pipe info
1129
 
1130
        if (isset($this->theme)) {
1131
            $output->setTheme($this->theme);
1132
        }
1133
 
1134
        $this->applyFormatterStyles();
1135
    }
1136
 
1137
    /**
1138
     * Get a Shell Output service instance.
1139
     *
1140
     * If none has been explicitly provided, this will create a new instance
1141
     * with the configured verbosity and output pager supplied by self::getPager
1142
     *
1143
     * @see self::verbosity
1144
     * @see self::getPager
1145
     *
1146
     * @return ShellOutput
1147
     */
1148
    public function getOutput(): ShellOutput
1149
    {
1150
        if (!isset($this->output)) {
1151
            $this->setOutput(new ShellOutput(
1152
                $this->getOutputVerbosity(),
1153
                null,
1154
                null,
1155
                $this->getPager() ?: null,
1156
                $this->theme()
1157
            ));
1158
 
1159
            // This is racy because `getOutputDecorated` needs access to the
1160
            // output stream to figure out if it's piped or not, so create it
1161
            // first, then update after we have a stream.
1162
            $decorated = $this->getOutputDecorated();
1163
            if ($decorated !== null) {
1164
                $this->output->setDecorated($decorated);
1165
            }
1166
        }
1167
 
1168
        return $this->output;
1169
    }
1170
 
1171
    /**
1172
     * Get the decoration (i.e. color) setting for the Shell Output service.
1173
     *
1174
     * @return bool|null 3-state boolean corresponding to the current color mode
1175
     */
1176
    public function getOutputDecorated()
1177
    {
1178
        switch ($this->colorMode()) {
1179
            case self::COLOR_MODE_FORCED:
1180
                return true;
1181
            case self::COLOR_MODE_DISABLED:
1182
                return false;
1183
            case self::COLOR_MODE_AUTO:
1184
            default:
1185
                return $this->outputIsPiped() ? false : null;
1186
        }
1187
    }
1188
 
1189
    /**
1190
     * Get the interactive setting for shell input.
1191
     *
1192
     * @return bool
1193
     */
1194
    public function getInputInteractive(): bool
1195
    {
1196
        switch ($this->interactiveMode()) {
1197
            case self::INTERACTIVE_MODE_FORCED:
1198
                return true;
1199
            case self::INTERACTIVE_MODE_DISABLED:
1200
                return false;
1201
            case self::INTERACTIVE_MODE_AUTO:
1202
            default:
1203
                return !$this->inputIsPiped();
1204
        }
1205
    }
1206
 
1207
    /**
1208
     * Set the OutputPager service.
1209
     *
1210
     * If a string is supplied, a ProcOutputPager will be used which shells out
1211
     * to the specified command.
1212
     *
1213
     * `cat` is special-cased to use the PassthruPager directly.
1214
     *
1215
     * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance
1216
     *
1217
     * @param string|OutputPager|false $pager
1218
     */
1219
    public function setPager($pager)
1220
    {
1221
        if ($pager === null || $pager === false || $pager === 'cat') {
1222
            $pager = false;
1223
        }
1224
 
1225
        if ($pager !== false && !\is_string($pager) && !$pager instanceof OutputPager) {
1226
            throw new \InvalidArgumentException('Unexpected pager instance');
1227
        }
1228
 
1229
        $this->pager = $pager;
1230
    }
1231
 
1232
    /**
1233
     * Get an OutputPager instance or a command for an external Proc pager.
1234
     *
1235
     * If no Pager has been explicitly provided, and Pcntl is available, this
1236
     * will default to `cli.pager` ini value, falling back to `which less`.
1237
     *
1238
     * @return string|OutputPager|false
1239
     */
1240
    public function getPager()
1241
    {
1242
        if (!isset($this->pager) && $this->usePcntl()) {
1243
            if (\getenv('TERM') === 'dumb') {
1244
                return false;
1245
            }
1246
 
1247
            if ($pager = \ini_get('cli.pager')) {
1248
                // use the default pager
1249
                $this->pager = $pager;
1250
            } elseif ($less = $this->configPaths->which('less')) {
1251
                // check for the presence of less...
1252
                $this->pager = $less.' -R -F -X';
1253
            }
1254
        }
1255
 
1256
        return $this->pager;
1257
    }
1258
 
1259
    /**
1260
     * Set the Shell AutoCompleter service.
1261
     *
1262
     * @param AutoCompleter $autoCompleter
1263
     */
1264
    public function setAutoCompleter(AutoCompleter $autoCompleter)
1265
    {
1266
        $this->autoCompleter = $autoCompleter;
1267
    }
1268
 
1269
    /**
1270
     * Get an AutoCompleter service instance.
1271
     *
1272
     * @return AutoCompleter
1273
     */
1274
    public function getAutoCompleter(): AutoCompleter
1275
    {
1276
        if (!isset($this->autoCompleter)) {
1277
            $this->autoCompleter = new AutoCompleter();
1278
        }
1279
 
1280
        return $this->autoCompleter;
1281
    }
1282
 
1283
    /**
1284
     * @deprecated Nothing should be using this anymore
1285
     *
1286
     * @return array
1287
     */
1288
    public function getTabCompletionMatchers(): array
1289
    {
1290
        return [];
1291
    }
1292
 
1293
    /**
1294
     * Add tab completion matchers to the AutoCompleter.
1295
     *
1296
     * This will buffer new matchers in the event that the Shell has not yet
1297
     * been instantiated. This allows the user to specify matchers in their
1298
     * config rc file, despite the fact that their file is needed in the Shell
1299
     * constructor.
1300
     *
1301
     * @param array $matchers
1302
     */
1303
    public function addMatchers(array $matchers)
1304
    {
1305
        $this->newMatchers = \array_merge($this->newMatchers, $matchers);
1306
        if (isset($this->shell)) {
1307
            $this->doAddMatchers();
1308
        }
1309
    }
1310
 
1311
    /**
1312
     * Internal method for adding tab completion matchers. This will set any new
1313
     * matchers once a Shell is available.
1314
     */
1315
    private function doAddMatchers()
1316
    {
1317
        if (!empty($this->newMatchers)) {
1318
            $this->shell->addMatchers($this->newMatchers);
1319
            $this->newMatchers = [];
1320
        }
1321
    }
1322
 
1323
    /**
1324
     * @deprecated Use `addMatchers` instead
1325
     *
1326
     * @param array $matchers
1327
     */
1328
    public function addTabCompletionMatchers(array $matchers)
1329
    {
1330
        $this->addMatchers($matchers);
1331
    }
1332
 
1333
    /**
1334
     * Add commands to the Shell.
1335
     *
1336
     * This will buffer new commands in the event that the Shell has not yet
1337
     * been instantiated. This allows the user to specify commands in their
1338
     * config rc file, despite the fact that their file is needed in the Shell
1339
     * constructor.
1340
     *
1341
     * @param array $commands
1342
     */
1343
    public function addCommands(array $commands)
1344
    {
1345
        $this->newCommands = \array_merge($this->newCommands, $commands);
1346
        if (isset($this->shell)) {
1347
            $this->doAddCommands();
1348
        }
1349
    }
1350
 
1351
    /**
1352
     * Internal method for adding commands. This will set any new commands once
1353
     * a Shell is available.
1354
     */
1355
    private function doAddCommands()
1356
    {
1357
        if (!empty($this->newCommands)) {
1358
            $this->shell->addCommands($this->newCommands);
1359
            $this->newCommands = [];
1360
        }
1361
    }
1362
 
1363
    /**
1364
     * Set the Shell backreference and add any new commands to the Shell.
1365
     *
1366
     * @param Shell $shell
1367
     */
1368
    public function setShell(Shell $shell)
1369
    {
1370
        $this->shell = $shell;
1371
        $this->doAddCommands();
1372
        $this->doAddMatchers();
1373
    }
1374
 
1375
    /**
1376
     * Set the PHP manual database file.
1377
     *
1378
     * This file should be an SQLite database generated from the phpdoc source
1379
     * with the `bin/build_manual` script.
1380
     *
1381
     * @param string $filename
1382
     */
1383
    public function setManualDbFile(string $filename)
1384
    {
1385
        $this->manualDbFile = (string) $filename;
1386
    }
1387
 
1388
    /**
1389
     * Get the current PHP manual database file.
1390
     *
1391
     * @return string|null Default: '~/.local/share/psysh/php_manual.sqlite'
1392
     */
1393
    public function getManualDbFile()
1394
    {
1395
        if (isset($this->manualDbFile)) {
1396
            return $this->manualDbFile;
1397
        }
1398
 
1399
        $files = $this->configPaths->dataFiles(['php_manual.sqlite']);
1400
        if (!empty($files)) {
1401
            if ($this->warnOnMultipleConfigs && \count($files) > 1) {
1402
                $msg = \sprintf('Multiple manual database files found: %s. Using %s', \implode(', ', $files), $files[0]);
1403
                \trigger_error($msg, \E_USER_NOTICE);
1404
            }
1405
 
1406
            return $this->manualDbFile = $files[0];
1407
        }
1408
    }
1409
 
1410
    /**
1411
     * Get a PHP manual database connection.
1412
     *
1413
     * @return \PDO|null
1414
     */
1415
    public function getManualDb()
1416
    {
1417
        if (!isset($this->manualDb)) {
1418
            $dbFile = $this->getManualDbFile();
1419
            if ($dbFile !== null && \is_file($dbFile)) {
1420
                try {
1421
                    $this->manualDb = new \PDO('sqlite:'.$dbFile);
1422
                } catch (\PDOException $e) {
1423
                    if ($e->getMessage() === 'could not find driver') {
1424
                        throw new RuntimeException('SQLite PDO driver not found', 0, $e);
1425
                    } else {
1426
                        throw $e;
1427
                    }
1428
                }
1429
            }
1430
        }
1431
 
1432
        return $this->manualDb;
1433
    }
1434
 
1435
    /**
1436
     * Add an array of casters definitions.
1437
     *
1438
     * @param array $casters
1439
     */
1440
    public function addCasters(array $casters)
1441
    {
1442
        $this->getPresenter()->addCasters($casters);
1443
    }
1444
 
1445
    /**
1446
     * Get the Presenter service.
1447
     *
1448
     * @return Presenter
1449
     */
1450
    public function getPresenter(): Presenter
1451
    {
1452
        if (!isset($this->presenter)) {
1453
            $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes());
1454
        }
1455
 
1456
        return $this->presenter;
1457
    }
1458
 
1459
    /**
1460
     * Enable or disable warnings on multiple configuration or data files.
1461
     *
1462
     * @see self::warnOnMultipleConfigs()
1463
     *
1464
     * @param bool $warnOnMultipleConfigs
1465
     */
1466
    public function setWarnOnMultipleConfigs(bool $warnOnMultipleConfigs)
1467
    {
1468
        $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs;
1469
    }
1470
 
1471
    /**
1472
     * Check whether to warn on multiple configuration or data files.
1473
     *
1474
     * By default, PsySH will use the file with highest precedence, and will
1475
     * silently ignore all others. With this enabled, a warning will be emitted
1476
     * (but not an exception thrown) if multiple configuration or data files
1477
     * are found.
1478
     *
1479
     * This will default to true in a future release, but is false for now.
1480
     *
1481
     * @return bool
1482
     */
1483
    public function warnOnMultipleConfigs(): bool
1484
    {
1485
        return $this->warnOnMultipleConfigs;
1486
    }
1487
 
1488
    /**
1489
     * Set the current color mode.
1490
     *
1491
     * @param string $colorMode
1492
     */
1493
    public function setColorMode(string $colorMode)
1494
    {
1495
        $validColorModes = [
1496
            self::COLOR_MODE_AUTO,
1497
            self::COLOR_MODE_FORCED,
1498
            self::COLOR_MODE_DISABLED,
1499
        ];
1500
 
1501
        if (!\in_array($colorMode, $validColorModes)) {
1502
            throw new \InvalidArgumentException('Invalid color mode: '.$colorMode);
1503
        }
1504
 
1505
        $this->colorMode = $colorMode;
1506
    }
1507
 
1508
    /**
1509
     * Get the current color mode.
1510
     *
1511
     * @return string
1512
     */
1513
    public function colorMode(): string
1514
    {
1515
        return $this->colorMode;
1516
    }
1517
 
1518
    /**
1519
     * Set the shell's interactive mode.
1520
     *
1521
     * @throws \InvalidArgumentException if interactive mode isn't disabled, forced, or auto
1522
     *
1523
     * @param string $interactiveMode
1524
     */
1525
    public function setInteractiveMode(string $interactiveMode)
1526
    {
1527
        $validInteractiveModes = [
1528
            self::INTERACTIVE_MODE_AUTO,
1529
            self::INTERACTIVE_MODE_FORCED,
1530
            self::INTERACTIVE_MODE_DISABLED,
1531
        ];
1532
 
1533
        if (!\in_array($interactiveMode, $validInteractiveModes)) {
1534
            throw new \InvalidArgumentException('Invalid interactive mode: '.$interactiveMode);
1535
        }
1536
 
1537
        $this->interactiveMode = $interactiveMode;
1538
    }
1539
 
1540
    /**
1541
     * Get the current interactive mode.
1542
     *
1543
     * @return string
1544
     */
1545
    public function interactiveMode(): string
1546
    {
1547
        return $this->interactiveMode;
1548
    }
1549
 
1550
    /**
1551
     * Set an update checker service instance.
1552
     *
1553
     * @param Checker $checker
1554
     */
1555
    public function setChecker(Checker $checker)
1556
    {
1557
        $this->checker = $checker;
1558
    }
1559
 
1560
    /**
1561
     * Get an update checker service instance.
1562
     *
1563
     * If none has been explicitly defined, this will create a new instance.
1564
     *
1565
     * @return Checker
1566
     */
1567
    public function getChecker(): Checker
1568
    {
1569
        if (!isset($this->checker)) {
1570
            $interval = $this->getUpdateCheck();
1571
            switch ($interval) {
1572
                case Checker::ALWAYS:
1573
                    $this->checker = new GitHubChecker();
1574
                    break;
1575
 
1576
                case Checker::DAILY:
1577
                case Checker::WEEKLY:
1578
                case Checker::MONTHLY:
1579
                    $checkFile = $this->getUpdateCheckCacheFile();
1580
                    if ($checkFile === false) {
1581
                        $this->checker = new NoopChecker();
1582
                    } else {
1583
                        $this->checker = new IntervalChecker($checkFile, $interval);
1584
                    }
1585
                    break;
1586
 
1587
                case Checker::NEVER:
1588
                    $this->checker = new NoopChecker();
1589
                    break;
1590
            }
1591
        }
1592
 
1593
        return $this->checker;
1594
    }
1595
 
1596
    /**
1597
     * Get the current update check interval.
1598
     *
1599
     * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is
1600
     * explicitly set, default to 'weekly'.
1601
     *
1602
     * @return string
1603
     */
1604
    public function getUpdateCheck(): string
1605
    {
1606
        return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY;
1607
    }
1608
 
1609
    /**
1610
     * Set the update check interval.
1611
     *
1612
     * @throws \InvalidArgumentException if the update check interval is unknown
1613
     *
1614
     * @param string $interval
1615
     */
1616
    public function setUpdateCheck(string $interval)
1617
    {
1618
        $validIntervals = [
1619
            Checker::ALWAYS,
1620
            Checker::DAILY,
1621
            Checker::WEEKLY,
1622
            Checker::MONTHLY,
1623
            Checker::NEVER,
1624
        ];
1625
 
1626
        if (!\in_array($interval, $validIntervals)) {
1627
            throw new \InvalidArgumentException('Invalid update check interval: '.$interval);
1628
        }
1629
 
1630
        $this->updateCheck = $interval;
1631
    }
1632
 
1633
    /**
1634
     * Get a cache file path for the update checker.
1635
     *
1636
     * @return string|false Return false if config file/directory is not writable
1637
     */
1638
    public function getUpdateCheckCacheFile()
1639
    {
1640
        return ConfigPaths::touchFileWithMkdir($this->configPaths->currentConfigDir().'/update_check.json');
1641
    }
1642
 
1643
    /**
1644
     * Set the startup message.
1645
     *
1646
     * @param string $message
1647
     */
1648
    public function setStartupMessage(string $message)
1649
    {
1650
        $this->startupMessage = $message;
1651
    }
1652
 
1653
    /**
1654
     * Get the startup message.
1655
     *
1656
     * @return string|null
1657
     */
1658
    public function getStartupMessage()
1659
    {
1660
        return $this->startupMessage;
1661
    }
1662
 
1663
    /**
1664
     * Set the prompt.
1665
     *
1666
     * @param string $prompt
1667
     */
1668
    public function setPrompt(string $prompt)
1669
    {
1670
        $this->prompt = $prompt;
1671
    }
1672
 
1673
    /**
1674
     * Get the prompt.
1675
     *
1676
     * @return string|null
1677
     */
1678
    public function getPrompt()
1679
    {
1680
        return $this->prompt;
1681
    }
1682
 
1683
    /**
1684
     * Get the force array indexes.
1685
     *
1686
     * @return bool
1687
     */
1688
    public function forceArrayIndexes(): bool
1689
    {
1690
        return $this->forceArrayIndexes;
1691
    }
1692
 
1693
    /**
1694
     * Set the force array indexes.
1695
     *
1696
     * @param bool $forceArrayIndexes
1697
     */
1698
    public function setForceArrayIndexes(bool $forceArrayIndexes)
1699
    {
1700
        $this->forceArrayIndexes = $forceArrayIndexes;
1701
    }
1702
 
1703
    /**
1704
     * Set the current output Theme.
1705
     *
1706
     * @param Theme|string|array $theme Theme (or Theme config)
1707
     */
1708
    public function setTheme($theme)
1709
    {
1710
        if (!$theme instanceof Theme) {
1711
            $theme = new Theme($theme);
1712
        }
1713
 
1714
        $this->theme = $theme;
1715
 
1716
        if (isset($this->output)) {
1717
            $this->output->setTheme($theme);
1718
            $this->applyFormatterStyles();
1719
        }
1720
    }
1721
 
1722
    /**
1723
     * Get the current output Theme.
1724
     */
1725
    public function theme(): Theme
1726
    {
1727
        if (!isset($this->theme)) {
1728
            $this->theme = new Theme();
1729
        }
1730
 
1731
        return $this->theme;
1732
    }
1733
 
1734
    /**
1735
     * Set the shell output formatter styles.
1736
     *
1737
     * Accepts a map from style name to [fg, bg, options], for example:
1738
     *
1739
     *     [
1740
     *         'error' => ['white', 'red', ['bold']],
1741
     *         'warning' => ['black', 'yellow'],
1742
     *     ]
1743
     *
1744
     * Foreground, background or options can be null, or even omitted entirely.
1745
     *
1746
     * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will
1747
     * eventually be removed. In the meantime, styles are applied first by the Theme, then
1748
     * overridden by any explicitly defined formatter styles.
1749
     */
1750
    public function setFormatterStyles(array $formatterStyles)
1751
    {
1752
        foreach ($formatterStyles as $name => $style) {
1753
            $this->formatterStyles[$name] = new OutputFormatterStyle(...$style);
1754
        }
1755
 
1756
        if (isset($this->output)) {
1757
            $this->applyFormatterStyles();
1758
        }
1759
    }
1760
 
1761
    /**
1762
     * Internal method for applying output formatter style customization.
1763
     *
1764
     * This is called on initialization of the shell output, and again if the
1765
     * formatter styles config is updated.
1766
     *
1767
     * @deprecated The `formatterStyles` configuration has been replaced by Themes and support will
1768
     * eventually be removed. In the meantime, styles are applied first by the Theme, then
1769
     * overridden by any explicitly defined formatter styles.
1770
     */
1771
    private function applyFormatterStyles()
1772
    {
1773
        $formatter = $this->output->getFormatter();
1774
        foreach ($this->formatterStyles as $name => $style) {
1775
            $formatter->setStyle($name, $style);
1776
        }
1777
 
1778
        $errorFormatter = $this->output->getErrorOutput()->getFormatter();
1779
        foreach (Theme::ERROR_STYLES as $name) {
1780
            if (isset($this->formatterStyles[$name])) {
1781
                $errorFormatter->setStyle($name, $this->formatterStyles[$name]);
1782
            }
1783
        }
1784
    }
1785
 
1786
    /**
1787
     * Get the configured output verbosity.
1788
     *
1789
     * @return string
1790
     */
1791
    public function verbosity(): string
1792
    {
1793
        return $this->verbosity;
1794
    }
1795
 
1796
    /**
1797
     * Set the shell output verbosity.
1798
     *
1799
     * Accepts OutputInterface verbosity constants.
1800
     *
1801
     * @throws \InvalidArgumentException if verbosity level is invalid
1802
     *
1803
     * @param string $verbosity
1804
     */
1805
    public function setVerbosity(string $verbosity)
1806
    {
1807
        $validVerbosityLevels = [
1808
            self::VERBOSITY_QUIET,
1809
            self::VERBOSITY_NORMAL,
1810
            self::VERBOSITY_VERBOSE,
1811
            self::VERBOSITY_VERY_VERBOSE,
1812
            self::VERBOSITY_DEBUG,
1813
        ];
1814
 
1815
        if (!\in_array($verbosity, $validVerbosityLevels)) {
1816
            throw new \InvalidArgumentException('Invalid verbosity level: '.$verbosity);
1817
        }
1818
 
1819
        $this->verbosity = $verbosity;
1820
 
1821
        if (isset($this->output)) {
1822
            $this->output->setVerbosity($this->getOutputVerbosity());
1823
        }
1824
    }
1825
 
1826
    /**
1827
     * Map the verbosity configuration to OutputInterface verbosity constants.
1828
     *
1829
     * @return int OutputInterface verbosity level
1830
     */
1831
    public function getOutputVerbosity(): int
1832
    {
1833
        switch ($this->verbosity()) {
1834
            case self::VERBOSITY_QUIET:
1835
                return OutputInterface::VERBOSITY_QUIET;
1836
            case self::VERBOSITY_VERBOSE:
1837
                return OutputInterface::VERBOSITY_VERBOSE;
1838
            case self::VERBOSITY_VERY_VERBOSE:
1839
                return OutputInterface::VERBOSITY_VERY_VERBOSE;
1840
            case self::VERBOSITY_DEBUG:
1841
                return OutputInterface::VERBOSITY_DEBUG;
1842
            case self::VERBOSITY_NORMAL:
1843
            default:
1844
                return OutputInterface::VERBOSITY_NORMAL;
1845
        }
1846
    }
1847
 
1848
    /**
1849
     * Guess whether stdin is piped.
1850
     *
1851
     * This is mostly useful for deciding whether to use non-interactive mode.
1852
     *
1853
     * @return bool
1854
     */
1855
    public function inputIsPiped(): bool
1856
    {
1857
        if ($this->pipedInput === null) {
1858
            $this->pipedInput = \defined('STDIN') && static::looksLikeAPipe(\STDIN);
1859
        }
1860
 
1861
        return $this->pipedInput;
1862
    }
1863
 
1864
    /**
1865
     * Guess whether shell output is piped.
1866
     *
1867
     * This is mostly useful for deciding whether to use non-decorated output.
1868
     *
1869
     * @return bool
1870
     */
1871
    public function outputIsPiped(): bool
1872
    {
1873
        if ($this->pipedOutput === null) {
1874
            $this->pipedOutput = static::looksLikeAPipe($this->getOutput()->getStream());
1875
        }
1876
 
1877
        return $this->pipedOutput;
1878
    }
1879
 
1880
    /**
1881
     * Guess whether an input or output stream is piped.
1882
     *
1883
     * @param resource|int $stream
1884
     *
1885
     * @return bool
1886
     */
1887
    private static function looksLikeAPipe($stream): bool
1888
    {
1889
        if (\function_exists('posix_isatty')) {
1890
            return !\posix_isatty($stream);
1891
        }
1892
 
1893
        $stat = \fstat($stream);
1894
        $mode = $stat['mode'] & 0170000;
1895
 
1896
        return $mode === 0010000 || $mode === 0040000 || $mode === 0100000 || $mode === 0120000;
1897
    }
1898
}