Configuration.php 55 KB

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