Chris@13: setColorMode(self::COLOR_MODE_AUTO); Chris@13: Chris@13: // explicit configFile option Chris@13: if (isset($config['configFile'])) { Chris@13: $this->configFile = $config['configFile']; Chris@17: } elseif ($configFile = \getenv('PSYSH_CONFIG')) { Chris@13: $this->configFile = $configFile; Chris@13: } Chris@13: Chris@13: // legacy baseDir option Chris@13: if (isset($config['baseDir'])) { Chris@13: $msg = "The 'baseDir' configuration option is deprecated; " . Chris@13: "please specify 'configDir' and 'dataDir' options instead"; Chris@13: throw new DeprecatedException($msg); Chris@13: } Chris@13: Chris@13: unset($config['configFile'], $config['baseDir']); Chris@13: Chris@13: // go go gadget, config! Chris@13: $this->loadConfig($config); Chris@13: $this->init(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Initialize the configuration. Chris@13: * Chris@13: * This checks for the presence of Readline and Pcntl extensions. Chris@13: * Chris@13: * If a config file is available, it will be loaded and merged with the current config. Chris@13: * Chris@13: * If no custom config file was specified and a local project config file Chris@13: * is available, it will be loaded and merged with the current config. Chris@13: */ Chris@13: public function init() Chris@13: { Chris@13: // feature detection Chris@17: $this->hasReadline = \function_exists('readline'); Chris@17: $this->hasPcntl = \function_exists('pcntl_signal') && \function_exists('posix_getpid'); Chris@13: Chris@13: if ($configFile = $this->getConfigFile()) { Chris@13: $this->loadConfigFile($configFile); Chris@13: } Chris@13: Chris@13: if (!$this->configFile && $localConfig = $this->getLocalConfigFile()) { Chris@13: $this->loadConfigFile($localConfig); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current PsySH config file. Chris@13: * Chris@13: * If a `configFile` option was passed to the Configuration constructor, Chris@13: * this file will be returned. If not, all possible config directories will Chris@13: * be searched, and the first `config.php` or `rc.php` file which exists Chris@13: * will be returned. Chris@13: * Chris@13: * If you're trying to decide where to put your config file, pick Chris@13: * Chris@13: * ~/.config/psysh/config.php Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getConfigFile() Chris@13: { Chris@13: if (isset($this->configFile)) { Chris@13: return $this->configFile; Chris@13: } Chris@13: Chris@13: $files = ConfigPaths::getConfigFiles(['config.php', 'rc.php'], $this->configDir); Chris@13: Chris@13: if (!empty($files)) { Chris@17: if ($this->warnOnMultipleConfigs && \count($files) > 1) { Chris@17: $msg = \sprintf('Multiple configuration files found: %s. Using %s', \implode($files, ', '), $files[0]); Chris@17: \trigger_error($msg, E_USER_NOTICE); Chris@13: } Chris@13: Chris@13: return $files[0]; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the local PsySH config file. Chris@13: * Chris@13: * Searches for a project specific config file `.psysh.php` in the current Chris@13: * working directory. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getLocalConfigFile() Chris@13: { Chris@17: $localConfig = \getcwd() . '/.psysh.php'; Chris@13: Chris@17: if (@\is_file($localConfig)) { Chris@13: return $localConfig; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Load configuration values from an array of options. Chris@13: * Chris@13: * @param array $options Chris@13: */ Chris@13: public function loadConfig(array $options) Chris@13: { Chris@13: foreach (self::$AVAILABLE_OPTIONS as $option) { Chris@13: if (isset($options[$option])) { Chris@17: $method = 'set' . \ucfirst($option); Chris@13: $this->$method($options[$option]); Chris@13: } Chris@13: } Chris@13: Chris@13: // legacy `tabCompletion` option Chris@13: if (isset($options['tabCompletion'])) { Chris@13: $msg = '`tabCompletion` is deprecated; use `useTabCompletion` instead.'; Chris@17: @\trigger_error($msg, E_USER_DEPRECATED); Chris@13: Chris@13: $this->setUseTabCompletion($options['tabCompletion']); Chris@13: } Chris@13: Chris@13: foreach (['commands', 'matchers', 'casters'] as $option) { Chris@13: if (isset($options[$option])) { Chris@17: $method = 'add' . \ucfirst($option); Chris@13: $this->$method($options[$option]); Chris@13: } Chris@13: } Chris@13: Chris@13: // legacy `tabCompletionMatchers` option Chris@13: if (isset($options['tabCompletionMatchers'])) { Chris@13: $msg = '`tabCompletionMatchers` is deprecated; use `matchers` instead.'; Chris@17: @\trigger_error($msg, E_USER_DEPRECATED); Chris@13: Chris@13: $this->addMatchers($options['tabCompletionMatchers']); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Load a configuration file (default: `$HOME/.config/psysh/config.php`). Chris@13: * Chris@13: * This configuration instance will be available to the config file as $config. Chris@13: * The config file may directly manipulate the configuration, or may return Chris@13: * an array of options which will be merged with the current configuration. Chris@13: * Chris@13: * @throws \InvalidArgumentException if the config file returns a non-array result Chris@13: * Chris@13: * @param string $file Chris@13: */ Chris@13: public function loadConfigFile($file) Chris@13: { Chris@13: $__psysh_config_file__ = $file; Chris@13: $load = function ($config) use ($__psysh_config_file__) { Chris@13: $result = require $__psysh_config_file__; Chris@13: if ($result !== 1) { Chris@13: return $result; Chris@13: } Chris@13: }; Chris@13: $result = $load($this); Chris@13: Chris@13: if (!empty($result)) { Chris@17: if (\is_array($result)) { Chris@13: $this->loadConfig($result); Chris@13: } else { Chris@13: throw new \InvalidArgumentException('Psy Shell configuration must return an array of options'); Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set files to be included by default at the start of each shell session. Chris@13: * Chris@13: * @param array $includes Chris@13: */ Chris@13: public function setDefaultIncludes(array $includes = []) Chris@13: { Chris@13: $this->defaultIncludes = $includes; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get files to be included by default at the start of each shell session. Chris@13: * Chris@13: * @return array Chris@13: */ Chris@13: public function getDefaultIncludes() Chris@13: { Chris@13: return $this->defaultIncludes ?: []; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the shell's config directory location. Chris@13: * Chris@13: * @param string $dir Chris@13: */ Chris@13: public function setConfigDir($dir) Chris@13: { Chris@13: $this->configDir = (string) $dir; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current configuration directory, if any is explicitly set. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getConfigDir() Chris@13: { Chris@13: return $this->configDir; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the shell's data directory location. Chris@13: * Chris@13: * @param string $dir Chris@13: */ Chris@13: public function setDataDir($dir) Chris@13: { Chris@13: $this->dataDir = (string) $dir; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current data directory, if any is explicitly set. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getDataDir() Chris@13: { Chris@13: return $this->dataDir; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the shell's temporary directory location. Chris@13: * Chris@13: * @param string $dir Chris@13: */ Chris@13: public function setRuntimeDir($dir) Chris@13: { Chris@13: $this->runtimeDir = (string) $dir; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the shell's temporary directory location. Chris@13: * Chris@13: * Defaults to `/psysh` inside the system's temp dir unless explicitly Chris@13: * overridden. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getRuntimeDir() Chris@13: { Chris@13: if (!isset($this->runtimeDir)) { Chris@13: $this->runtimeDir = ConfigPaths::getRuntimeDir(); Chris@13: } Chris@13: Chris@17: if (!\is_dir($this->runtimeDir)) { Chris@17: \mkdir($this->runtimeDir, 0700, true); Chris@13: } Chris@13: Chris@13: return $this->runtimeDir; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the readline history file path. Chris@13: * Chris@13: * @param string $file Chris@13: */ Chris@13: public function setHistoryFile($file) Chris@13: { Chris@13: $this->historyFile = ConfigPaths::touchFileWithMkdir($file); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the readline history file path. Chris@13: * Chris@13: * Defaults to `/history` inside the shell's base config dir unless Chris@13: * explicitly overridden. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getHistoryFile() Chris@13: { Chris@13: if (isset($this->historyFile)) { Chris@13: return $this->historyFile; Chris@13: } Chris@13: Chris@13: $files = ConfigPaths::getConfigFiles(['psysh_history', 'history'], $this->configDir); Chris@13: Chris@13: if (!empty($files)) { Chris@17: if ($this->warnOnMultipleConfigs && \count($files) > 1) { Chris@17: $msg = \sprintf('Multiple history files found: %s. Using %s', \implode($files, ', '), $files[0]); Chris@17: \trigger_error($msg, E_USER_NOTICE); Chris@13: } Chris@13: Chris@13: $this->setHistoryFile($files[0]); Chris@13: } else { Chris@13: // fallback: create our own history file Chris@13: $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir(); Chris@13: $this->setHistoryFile($dir . '/psysh_history'); Chris@13: } Chris@13: Chris@13: return $this->historyFile; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the readline max history size. Chris@13: * Chris@13: * @param int $value Chris@13: */ Chris@13: public function setHistorySize($value) Chris@13: { Chris@13: $this->historySize = (int) $value; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the readline max history size. Chris@13: * Chris@13: * @return int Chris@13: */ Chris@13: public function getHistorySize() Chris@13: { Chris@13: return $this->historySize; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Sets whether readline erases old duplicate history entries. Chris@13: * Chris@13: * @param bool $value Chris@13: */ Chris@13: public function setEraseDuplicates($value) Chris@13: { Chris@13: $this->eraseDuplicates = (bool) $value; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get whether readline erases old duplicate history entries. Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public function getEraseDuplicates() Chris@13: { Chris@13: return $this->eraseDuplicates; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a temporary file of type $type for process $pid. Chris@13: * Chris@13: * The file will be created inside the current temporary directory. Chris@13: * Chris@13: * @see self::getRuntimeDir Chris@13: * Chris@13: * @param string $type Chris@13: * @param int $pid Chris@13: * Chris@13: * @return string Temporary file name Chris@13: */ Chris@13: public function getTempFile($type, $pid) Chris@13: { Chris@17: return \tempnam($this->getRuntimeDir(), $type . '_' . $pid . '_'); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a filename suitable for a FIFO pipe of $type for process $pid. Chris@13: * Chris@13: * The pipe will be created inside the current temporary directory. Chris@13: * Chris@13: * @param string $type Chris@13: * @param int $pid Chris@13: * Chris@13: * @return string Pipe name Chris@13: */ Chris@13: public function getPipe($type, $pid) Chris@13: { Chris@17: return \sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether this PHP instance has Readline available. Chris@13: * Chris@13: * @return bool True if Readline is available Chris@13: */ Chris@13: public function hasReadline() Chris@13: { Chris@13: return $this->hasReadline; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Enable or disable Readline usage. Chris@13: * Chris@13: * @param bool $useReadline Chris@13: */ Chris@13: public function setUseReadline($useReadline) Chris@13: { Chris@13: $this->useReadline = (bool) $useReadline; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether to use Readline. Chris@13: * Chris@13: * If `setUseReadline` as been set to true, but Readline is not actually Chris@13: * available, this will return false. Chris@13: * Chris@13: * @return bool True if the current Shell should use Readline Chris@13: */ Chris@13: public function useReadline() Chris@13: { Chris@13: return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the Psy Shell readline service. Chris@13: * Chris@13: * @param Readline $readline Chris@13: */ Chris@13: public function setReadline(Readline $readline) Chris@13: { Chris@13: $this->readline = $readline; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the Psy Shell readline service. Chris@13: * Chris@13: * By default, this service uses (in order of preference): Chris@13: * Chris@13: * * GNU Readline Chris@13: * * Libedit Chris@13: * * A transient array-based readline emulation. Chris@13: * Chris@13: * @return Readline Chris@13: */ Chris@13: public function getReadline() Chris@13: { Chris@13: if (!isset($this->readline)) { Chris@13: $className = $this->getReadlineClass(); Chris@13: $this->readline = new $className( Chris@13: $this->getHistoryFile(), Chris@13: $this->getHistorySize(), Chris@13: $this->getEraseDuplicates() Chris@13: ); Chris@13: } Chris@13: Chris@13: return $this->readline; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the appropriate Readline implementation class name. Chris@13: * Chris@13: * @see self::getReadline Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: private function getReadlineClass() Chris@13: { Chris@13: if ($this->useReadline()) { Chris@13: if (GNUReadline::isSupported()) { Chris@13: return 'Psy\Readline\GNUReadline'; Chris@13: } elseif (Libedit::isSupported()) { Chris@13: return 'Psy\Readline\Libedit'; Chris@13: } elseif (HoaConsole::isSupported()) { Chris@13: return 'Psy\Readline\HoaConsole'; Chris@13: } Chris@13: } Chris@13: Chris@13: return 'Psy\Readline\Transient'; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Enable or disable bracketed paste. Chris@13: * Chris@13: * Note that this only works with readline (not libedit) integration for now. Chris@13: * Chris@13: * @param bool $useBracketedPaste Chris@13: */ Chris@13: public function setUseBracketedPaste($useBracketedPaste) Chris@13: { Chris@13: $this->useBracketedPaste = (bool) $useBracketedPaste; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether to use bracketed paste with readline. Chris@13: * Chris@13: * When this works, it's magical. Tabs in pastes don't try to autcomplete. Chris@13: * Newlines in paste don't execute code until you get to the end. It makes Chris@13: * readline act like you'd expect when pasting. Chris@13: * Chris@13: * But it often (usually?) does not work. And when it doesn't, it just spews Chris@13: * escape codes all over the place and generally makes things ugly :( Chris@13: * Chris@13: * If `useBracketedPaste` has been set to true, but the current readline Chris@13: * implementation is anything besides GNU readline, this will return false. Chris@13: * Chris@13: * @return bool True if the shell should use bracketed paste Chris@13: */ Chris@13: public function useBracketedPaste() Chris@13: { Chris@13: // For now, only the GNU readline implementation supports bracketed paste. Chris@13: $supported = ($this->getReadlineClass() === 'Psy\Readline\GNUReadline'); Chris@13: Chris@13: return $supported && $this->useBracketedPaste; Chris@13: Chris@13: // @todo mebbe turn this on by default some day? Chris@13: // return isset($this->useBracketedPaste) ? ($supported && $this->useBracketedPaste) : $supported; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether this PHP instance has Pcntl available. Chris@13: * Chris@13: * @return bool True if Pcntl is available Chris@13: */ Chris@13: public function hasPcntl() Chris@13: { Chris@13: return $this->hasPcntl; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Enable or disable Pcntl usage. Chris@13: * Chris@13: * @param bool $usePcntl Chris@13: */ Chris@13: public function setUsePcntl($usePcntl) Chris@13: { Chris@13: $this->usePcntl = (bool) $usePcntl; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether to use Pcntl. Chris@13: * Chris@13: * If `setUsePcntl` has been set to true, but Pcntl is not actually Chris@13: * available, this will return false. Chris@13: * Chris@13: * @return bool True if the current Shell should use Pcntl Chris@13: */ Chris@13: public function usePcntl() Chris@13: { Chris@13: return isset($this->usePcntl) ? ($this->hasPcntl && $this->usePcntl) : $this->hasPcntl; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Enable or disable strict requirement of semicolons. Chris@13: * Chris@13: * @see self::requireSemicolons() Chris@13: * Chris@13: * @param bool $requireSemicolons Chris@13: */ Chris@13: public function setRequireSemicolons($requireSemicolons) Chris@13: { Chris@13: $this->requireSemicolons = (bool) $requireSemicolons; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether to require semicolons on all statements. Chris@13: * Chris@13: * By default, PsySH will automatically insert semicolons at the end of Chris@13: * statements if they're missing. To strictly require semicolons, set Chris@13: * `requireSemicolons` to true. Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public function requireSemicolons() Chris@13: { Chris@13: return $this->requireSemicolons; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Enable or disable Unicode in PsySH specific output. Chris@13: * Chris@13: * Note that this does not disable Unicode output in general, it just makes Chris@13: * it so PsySH won't output any itself. Chris@13: * Chris@13: * @param bool $useUnicode Chris@13: */ Chris@13: public function setUseUnicode($useUnicode) Chris@13: { Chris@13: $this->useUnicode = (bool) $useUnicode; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether to use Unicode in PsySH specific output. Chris@13: * Chris@13: * Note that this does not disable Unicode output in general, it just makes Chris@13: * it so PsySH won't output any itself. Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public function useUnicode() Chris@13: { Chris@13: if (isset($this->useUnicode)) { Chris@13: return $this->useUnicode; Chris@13: } Chris@13: Chris@13: // @todo detect `chsh` != 65001 on Windows and return false Chris@13: return true; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the error logging level. Chris@13: * Chris@13: * @see self::errorLoggingLevel Chris@13: * Chris@13: * @param bool $errorLoggingLevel Chris@13: */ Chris@13: public function setErrorLoggingLevel($errorLoggingLevel) Chris@13: { Chris@13: $this->errorLoggingLevel = (E_ALL | E_STRICT) & $errorLoggingLevel; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current error logging level. Chris@13: * Chris@13: * By default, PsySH will automatically log all errors, regardless of the Chris@13: * current `error_reporting` level. Additionally, if the `error_reporting` Chris@13: * level warrants, an ErrorException will be thrown. Chris@13: * Chris@13: * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it Chris@13: * to any valid error_reporting value to log only errors which match that Chris@13: * level. Chris@13: * Chris@13: * http://php.net/manual/en/function.error-reporting.php Chris@13: * Chris@13: * @return int Chris@13: */ Chris@13: public function errorLoggingLevel() Chris@13: { Chris@13: return $this->errorLoggingLevel; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set a CodeCleaner service instance. Chris@13: * Chris@13: * @param CodeCleaner $cleaner Chris@13: */ Chris@13: public function setCodeCleaner(CodeCleaner $cleaner) Chris@13: { Chris@13: $this->cleaner = $cleaner; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a CodeCleaner service instance. Chris@13: * Chris@13: * If none has been explicitly defined, this will create a new instance. Chris@13: * Chris@13: * @return CodeCleaner Chris@13: */ Chris@13: public function getCodeCleaner() Chris@13: { Chris@13: if (!isset($this->cleaner)) { Chris@13: $this->cleaner = new CodeCleaner(); Chris@13: } Chris@13: Chris@13: return $this->cleaner; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Enable or disable tab completion. Chris@13: * Chris@13: * @param bool $useTabCompletion Chris@13: */ Chris@13: public function setUseTabCompletion($useTabCompletion) Chris@13: { Chris@13: $this->useTabCompletion = (bool) $useTabCompletion; Chris@13: } Chris@13: Chris@13: /** Chris@13: * @deprecated Call `setUseTabCompletion` instead Chris@13: * Chris@13: * @param bool $useTabCompletion Chris@13: */ Chris@13: public function setTabCompletion($useTabCompletion) Chris@13: { Chris@13: $this->setUseTabCompletion($useTabCompletion); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether to use tab completion. Chris@13: * Chris@13: * If `setUseTabCompletion` has been set to true, but readline is not Chris@13: * actually available, this will return false. Chris@13: * Chris@13: * @return bool True if the current Shell should use tab completion Chris@13: */ Chris@13: public function useTabCompletion() Chris@13: { Chris@13: return isset($this->useTabCompletion) ? ($this->hasReadline && $this->useTabCompletion) : $this->hasReadline; Chris@13: } Chris@13: Chris@13: /** Chris@13: * @deprecated Call `useTabCompletion` instead Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public function getTabCompletion() Chris@13: { Chris@13: return $this->useTabCompletion(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the Shell Output service. Chris@13: * Chris@13: * @param ShellOutput $output Chris@13: */ Chris@13: public function setOutput(ShellOutput $output) Chris@13: { Chris@13: $this->output = $output; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a Shell Output service instance. Chris@13: * Chris@13: * If none has been explicitly provided, this will create a new instance Chris@13: * with VERBOSITY_NORMAL and the output page supplied by self::getPager Chris@13: * Chris@13: * @see self::getPager Chris@13: * Chris@13: * @return ShellOutput Chris@13: */ Chris@13: public function getOutput() Chris@13: { Chris@13: if (!isset($this->output)) { Chris@13: $this->output = new ShellOutput( Chris@13: ShellOutput::VERBOSITY_NORMAL, Chris@13: $this->getOutputDecorated(), Chris@13: null, Chris@13: $this->getPager() Chris@13: ); Chris@13: } Chris@13: Chris@13: return $this->output; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the decoration (i.e. color) setting for the Shell Output service. Chris@13: * Chris@13: * @return null|bool 3-state boolean corresponding to the current color mode Chris@13: */ Chris@13: public function getOutputDecorated() Chris@13: { Chris@13: if ($this->colorMode() === self::COLOR_MODE_AUTO) { Chris@13: return; Chris@13: } elseif ($this->colorMode() === self::COLOR_MODE_FORCED) { Chris@13: return true; Chris@13: } elseif ($this->colorMode() === self::COLOR_MODE_DISABLED) { Chris@13: return false; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the OutputPager service. Chris@13: * Chris@13: * If a string is supplied, a ProcOutputPager will be used which shells out Chris@13: * to the specified command. Chris@13: * Chris@13: * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance Chris@13: * Chris@13: * @param string|OutputPager $pager Chris@13: */ Chris@13: public function setPager($pager) Chris@13: { Chris@17: if ($pager && !\is_string($pager) && !$pager instanceof OutputPager) { Chris@13: throw new \InvalidArgumentException('Unexpected pager instance'); Chris@13: } Chris@13: Chris@13: $this->pager = $pager; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get an OutputPager instance or a command for an external Proc pager. Chris@13: * Chris@13: * If no Pager has been explicitly provided, and Pcntl is available, this Chris@13: * will default to `cli.pager` ini value, falling back to `which less`. Chris@13: * Chris@13: * @return string|OutputPager Chris@13: */ Chris@13: public function getPager() Chris@13: { Chris@13: if (!isset($this->pager) && $this->usePcntl()) { Chris@17: if ($pager = \ini_get('cli.pager')) { Chris@16: // use the default pager Chris@13: $this->pager = $pager; Chris@17: } elseif ($less = \exec('which less 2>/dev/null')) { Chris@13: // check for the presence of less... Chris@13: $this->pager = $less . ' -R -S -F -X'; Chris@13: } Chris@13: } Chris@13: Chris@13: return $this->pager; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the Shell AutoCompleter service. Chris@13: * Chris@13: * @param AutoCompleter $autoCompleter Chris@13: */ Chris@13: public function setAutoCompleter(AutoCompleter $autoCompleter) Chris@13: { Chris@13: $this->autoCompleter = $autoCompleter; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get an AutoCompleter service instance. Chris@13: * Chris@13: * @return AutoCompleter Chris@13: */ Chris@13: public function getAutoCompleter() Chris@13: { Chris@13: if (!isset($this->autoCompleter)) { Chris@13: $this->autoCompleter = new AutoCompleter(); Chris@13: } Chris@13: Chris@13: return $this->autoCompleter; Chris@13: } Chris@13: Chris@13: /** Chris@13: * @deprecated Nothing should be using this anymore Chris@13: * Chris@13: * @return array Chris@13: */ Chris@13: public function getTabCompletionMatchers() Chris@13: { Chris@13: return []; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Add tab completion matchers to the AutoCompleter. Chris@13: * Chris@13: * This will buffer new matchers in the event that the Shell has not yet Chris@13: * been instantiated. This allows the user to specify matchers in their Chris@13: * config rc file, despite the fact that their file is needed in the Shell Chris@13: * constructor. Chris@13: * Chris@13: * @param array $matchers Chris@13: */ Chris@13: public function addMatchers(array $matchers) Chris@13: { Chris@17: $this->newMatchers = \array_merge($this->newMatchers, $matchers); Chris@13: if (isset($this->shell)) { Chris@13: $this->doAddMatchers(); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Internal method for adding tab completion matchers. This will set any new Chris@13: * matchers once a Shell is available. Chris@13: */ Chris@13: private function doAddMatchers() Chris@13: { Chris@13: if (!empty($this->newMatchers)) { Chris@13: $this->shell->addMatchers($this->newMatchers); Chris@13: $this->newMatchers = []; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * @deprecated Use `addMatchers` instead Chris@13: * Chris@13: * @param array $matchers Chris@13: */ Chris@13: public function addTabCompletionMatchers(array $matchers) Chris@13: { Chris@13: $this->addMatchers($matchers); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Add commands to the Shell. Chris@13: * Chris@13: * This will buffer new commands in the event that the Shell has not yet Chris@13: * been instantiated. This allows the user to specify commands in their Chris@13: * config rc file, despite the fact that their file is needed in the Shell Chris@13: * constructor. Chris@13: * Chris@13: * @param array $commands Chris@13: */ Chris@13: public function addCommands(array $commands) Chris@13: { Chris@17: $this->newCommands = \array_merge($this->newCommands, $commands); Chris@13: if (isset($this->shell)) { Chris@13: $this->doAddCommands(); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Internal method for adding commands. This will set any new commands once Chris@13: * a Shell is available. Chris@13: */ Chris@13: private function doAddCommands() Chris@13: { Chris@13: if (!empty($this->newCommands)) { Chris@13: $this->shell->addCommands($this->newCommands); Chris@13: $this->newCommands = []; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the Shell backreference and add any new commands to the Shell. Chris@13: * Chris@13: * @param Shell $shell Chris@13: */ Chris@13: public function setShell(Shell $shell) Chris@13: { Chris@13: $this->shell = $shell; Chris@13: $this->doAddCommands(); Chris@13: $this->doAddMatchers(); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the PHP manual database file. Chris@13: * Chris@13: * This file should be an SQLite database generated from the phpdoc source Chris@13: * with the `bin/build_manual` script. Chris@13: * Chris@13: * @param string $filename Chris@13: */ Chris@13: public function setManualDbFile($filename) Chris@13: { Chris@13: $this->manualDbFile = (string) $filename; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current PHP manual database file. Chris@13: * Chris@13: * @return string Default: '~/.local/share/psysh/php_manual.sqlite' Chris@13: */ Chris@13: public function getManualDbFile() Chris@13: { Chris@13: if (isset($this->manualDbFile)) { Chris@13: return $this->manualDbFile; Chris@13: } Chris@13: Chris@13: $files = ConfigPaths::getDataFiles(['php_manual.sqlite'], $this->dataDir); Chris@13: if (!empty($files)) { Chris@17: if ($this->warnOnMultipleConfigs && \count($files) > 1) { Chris@17: $msg = \sprintf('Multiple manual database files found: %s. Using %s', \implode($files, ', '), $files[0]); Chris@17: \trigger_error($msg, E_USER_NOTICE); Chris@13: } Chris@13: Chris@13: return $this->manualDbFile = $files[0]; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a PHP manual database connection. Chris@13: * Chris@13: * @return \PDO Chris@13: */ Chris@13: public function getManualDb() Chris@13: { Chris@13: if (!isset($this->manualDb)) { Chris@13: $dbFile = $this->getManualDbFile(); Chris@17: if (\is_file($dbFile)) { Chris@13: try { Chris@13: $this->manualDb = new \PDO('sqlite:' . $dbFile); Chris@13: } catch (\PDOException $e) { Chris@13: if ($e->getMessage() === 'could not find driver') { Chris@13: throw new RuntimeException('SQLite PDO driver not found', 0, $e); Chris@13: } else { Chris@13: throw $e; Chris@13: } Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: return $this->manualDb; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Add an array of casters definitions. Chris@13: * Chris@13: * @param array $casters Chris@13: */ Chris@13: public function addCasters(array $casters) Chris@13: { Chris@13: $this->getPresenter()->addCasters($casters); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the Presenter service. Chris@13: * Chris@13: * @return Presenter Chris@13: */ Chris@13: public function getPresenter() Chris@13: { Chris@13: if (!isset($this->presenter)) { Chris@13: $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes()); Chris@13: } Chris@13: Chris@13: return $this->presenter; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Enable or disable warnings on multiple configuration or data files. Chris@13: * Chris@13: * @see self::warnOnMultipleConfigs() Chris@13: * Chris@13: * @param bool $warnOnMultipleConfigs Chris@13: */ Chris@13: public function setWarnOnMultipleConfigs($warnOnMultipleConfigs) Chris@13: { Chris@13: $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether to warn on multiple configuration or data files. Chris@13: * Chris@13: * By default, PsySH will use the file with highest precedence, and will Chris@13: * silently ignore all others. With this enabled, a warning will be emitted Chris@13: * (but not an exception thrown) if multiple configuration or data files Chris@13: * are found. Chris@13: * Chris@13: * This will default to true in a future release, but is false for now. Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public function warnOnMultipleConfigs() Chris@13: { Chris@13: return $this->warnOnMultipleConfigs; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the current color mode. Chris@13: * Chris@13: * @param string $colorMode Chris@13: */ Chris@13: public function setColorMode($colorMode) Chris@13: { Chris@13: $validColorModes = [ Chris@13: self::COLOR_MODE_AUTO, Chris@13: self::COLOR_MODE_FORCED, Chris@13: self::COLOR_MODE_DISABLED, Chris@13: ]; Chris@13: Chris@17: if (\in_array($colorMode, $validColorModes)) { Chris@13: $this->colorMode = $colorMode; Chris@13: } else { Chris@13: throw new \InvalidArgumentException('invalid color mode: ' . $colorMode); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current color mode. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function colorMode() Chris@13: { Chris@13: return $this->colorMode; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set an update checker service instance. Chris@13: * Chris@13: * @param Checker $checker Chris@13: */ Chris@13: public function setChecker(Checker $checker) Chris@13: { Chris@13: $this->checker = $checker; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get an update checker service instance. Chris@13: * Chris@13: * If none has been explicitly defined, this will create a new instance. Chris@13: * Chris@13: * @return Checker Chris@13: */ Chris@13: public function getChecker() Chris@13: { Chris@13: if (!isset($this->checker)) { Chris@13: $interval = $this->getUpdateCheck(); Chris@13: switch ($interval) { Chris@13: case Checker::ALWAYS: Chris@13: $this->checker = new GitHubChecker(); Chris@13: break; Chris@13: Chris@13: case Checker::DAILY: Chris@13: case Checker::WEEKLY: Chris@13: case Checker::MONTHLY: Chris@13: $checkFile = $this->getUpdateCheckCacheFile(); Chris@13: if ($checkFile === false) { Chris@13: $this->checker = new NoopChecker(); Chris@13: } else { Chris@13: $this->checker = new IntervalChecker($checkFile, $interval); Chris@13: } Chris@13: break; Chris@13: Chris@13: case Checker::NEVER: Chris@13: $this->checker = new NoopChecker(); Chris@13: break; Chris@13: } Chris@13: } Chris@13: Chris@13: return $this->checker; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the current update check interval. Chris@13: * Chris@13: * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is Chris@13: * explicitly set, default to 'weekly'. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getUpdateCheck() Chris@13: { Chris@13: return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the update check interval. Chris@13: * Chris@13: * @throws \InvalidArgumentDescription if the update check interval is unknown Chris@13: * Chris@13: * @param string $interval Chris@13: */ Chris@13: public function setUpdateCheck($interval) Chris@13: { Chris@13: $validIntervals = [ Chris@13: Checker::ALWAYS, Chris@13: Checker::DAILY, Chris@13: Checker::WEEKLY, Chris@13: Checker::MONTHLY, Chris@13: Checker::NEVER, Chris@13: ]; Chris@13: Chris@17: if (!\in_array($interval, $validIntervals)) { Chris@13: throw new \InvalidArgumentException('invalid update check interval: ' . $interval); Chris@13: } Chris@13: Chris@13: $this->updateCheck = $interval; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a cache file path for the update checker. Chris@13: * Chris@13: * @return string|false Return false if config file/directory is not writable Chris@13: */ Chris@13: public function getUpdateCheckCacheFile() Chris@13: { Chris@13: $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir(); Chris@13: Chris@13: return ConfigPaths::touchFileWithMkdir($dir . '/update_check.json'); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the startup message. Chris@13: * Chris@13: * @param string $message Chris@13: */ Chris@13: public function setStartupMessage($message) Chris@13: { Chris@13: $this->startupMessage = $message; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the startup message. Chris@13: * Chris@13: * @return string|null Chris@13: */ Chris@13: public function getStartupMessage() Chris@13: { Chris@13: return $this->startupMessage; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the prompt. Chris@13: * Chris@13: * @param string $prompt Chris@13: */ Chris@13: public function setPrompt($prompt) Chris@13: { Chris@13: $this->prompt = $prompt; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the prompt. Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: public function getPrompt() Chris@13: { Chris@13: return $this->prompt; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get the force array indexes. Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: public function forceArrayIndexes() Chris@13: { Chris@13: return $this->forceArrayIndexes; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Set the force array indexes. Chris@13: * Chris@13: * @param bool $forceArrayIndexes Chris@13: */ Chris@13: public function setForceArrayIndexes($forceArrayIndexes) Chris@13: { Chris@13: $this->forceArrayIndexes = $forceArrayIndexes; Chris@13: } Chris@13: }