Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Zur aktuellen Revision | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

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