functions.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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\ExecutionLoop\ProcessForker;
  12. use Psy\VersionUpdater\GitHubChecker;
  13. use Psy\VersionUpdater\Installer;
  14. use Psy\VersionUpdater\SelfUpdate;
  15. use Symfony\Component\Console\Input\ArgvInput;
  16. use Symfony\Component\Console\Input\InputArgument;
  17. use Symfony\Component\Console\Input\InputDefinition;
  18. use Symfony\Component\Console\Input\InputOption;
  19. if (!\function_exists('Psy\\sh')) {
  20. /**
  21. * Command to return the eval-able code to startup PsySH.
  22. *
  23. * eval(\Psy\sh());
  24. */
  25. function sh(): string
  26. {
  27. if (\version_compare(\PHP_VERSION, '8.0', '<')) {
  28. return '\extract(\Psy\debug(\get_defined_vars(), isset($this) ? $this : @\get_called_class()));';
  29. }
  30. return <<<'EOS'
  31. if (isset($this)) {
  32. \extract(\Psy\debug(\get_defined_vars(), $this));
  33. } else {
  34. try {
  35. static::class;
  36. \extract(\Psy\debug(\get_defined_vars(), static::class));
  37. } catch (\Error $e) {
  38. \extract(\Psy\debug(\get_defined_vars()));
  39. }
  40. }
  41. EOS;
  42. }
  43. }
  44. if (!\function_exists('Psy\\debug')) {
  45. /**
  46. * Invoke a Psy Shell from the current context.
  47. *
  48. * For example:
  49. *
  50. * foreach ($items as $item) {
  51. * \Psy\debug(get_defined_vars());
  52. * }
  53. *
  54. * If you would like your shell interaction to affect the state of the
  55. * current context, you can extract() the values returned from this call:
  56. *
  57. * foreach ($items as $item) {
  58. * extract(\Psy\debug(get_defined_vars()));
  59. * var_dump($item); // will be whatever you set $item to in Psy Shell
  60. * }
  61. *
  62. * Optionally, supply an object as the `$bindTo` parameter. This determines
  63. * the value `$this` will have in the shell, and sets up class scope so that
  64. * private and protected members are accessible:
  65. *
  66. * class Foo {
  67. * function bar() {
  68. * \Psy\debug(get_defined_vars(), $this);
  69. * }
  70. * }
  71. *
  72. * For the static equivalent, pass a class name as the `$bindTo` parameter.
  73. * This makes `self` work in the shell, and sets up static scope so that
  74. * private and protected static members are accessible:
  75. *
  76. * class Foo {
  77. * static function bar() {
  78. * \Psy\debug(get_defined_vars(), get_called_class());
  79. * }
  80. * }
  81. *
  82. * @param array $vars Scope variables from the calling context (default: [])
  83. * @param object|string $bindTo Bound object ($this) or class (self) value for the shell
  84. *
  85. * @return array Scope variables from the debugger session
  86. */
  87. function debug(array $vars = [], $bindTo = null): array
  88. {
  89. echo \PHP_EOL;
  90. $sh = new Shell();
  91. $sh->setScopeVariables($vars);
  92. // Show a couple of lines of call context for the debug session.
  93. //
  94. // @todo come up with a better way of doing this which doesn't involve injecting input :-P
  95. if ($sh->has('whereami')) {
  96. $sh->addInput('whereami -n2', true);
  97. }
  98. if (\is_string($bindTo)) {
  99. $sh->setBoundClass($bindTo);
  100. } elseif ($bindTo !== null) {
  101. $sh->setBoundObject($bindTo);
  102. }
  103. $sh->run();
  104. return $sh->getScopeVariables(false);
  105. }
  106. }
  107. if (!\function_exists('Psy\\info')) {
  108. /**
  109. * Get a bunch of debugging info about the current PsySH environment and
  110. * configuration.
  111. *
  112. * If a Configuration param is passed, that configuration is stored and
  113. * used for the current shell session, and no debugging info is returned.
  114. *
  115. * @param Configuration|null $config
  116. *
  117. * @return array|null
  118. */
  119. function info(Configuration $config = null)
  120. {
  121. static $lastConfig;
  122. if ($config !== null) {
  123. $lastConfig = $config;
  124. return;
  125. }
  126. $prettyPath = function ($path) {
  127. return $path;
  128. };
  129. $homeDir = (new ConfigPaths())->homeDir();
  130. if ($homeDir && $homeDir = \rtrim($homeDir, '/')) {
  131. $homePattern = '#^'.\preg_quote($homeDir, '#').'/#';
  132. $prettyPath = function ($path) use ($homePattern) {
  133. if (\is_string($path)) {
  134. return \preg_replace($homePattern, '~/', $path);
  135. } else {
  136. return $path;
  137. }
  138. };
  139. }
  140. $config = $lastConfig ?: new Configuration();
  141. $configEnv = (isset($_SERVER['PSYSH_CONFIG']) && $_SERVER['PSYSH_CONFIG']) ? $_SERVER['PSYSH_CONFIG'] : false;
  142. if ($configEnv === false && \PHP_SAPI === 'cli-server') {
  143. $configEnv = \getenv('PSYSH_CONFIG');
  144. }
  145. $shellInfo = [
  146. 'PsySH version' => Shell::VERSION,
  147. ];
  148. $core = [
  149. 'PHP version' => \PHP_VERSION,
  150. 'OS' => \PHP_OS,
  151. 'default includes' => $config->getDefaultIncludes(),
  152. 'require semicolons' => $config->requireSemicolons(),
  153. 'error logging level' => $config->errorLoggingLevel(),
  154. 'config file' => [
  155. 'default config file' => $prettyPath($config->getConfigFile()),
  156. 'local config file' => $prettyPath($config->getLocalConfigFile()),
  157. 'PSYSH_CONFIG env' => $prettyPath($configEnv),
  158. ],
  159. // 'config dir' => $config->getConfigDir(),
  160. // 'data dir' => $config->getDataDir(),
  161. // 'runtime dir' => $config->getRuntimeDir(),
  162. ];
  163. // Use an explicit, fresh update check here, rather than relying on whatever is in $config.
  164. $checker = new GitHubChecker();
  165. $updateAvailable = null;
  166. $latest = null;
  167. try {
  168. $updateAvailable = !$checker->isLatest();
  169. $latest = $checker->getLatest();
  170. } catch (\Throwable $e) {
  171. }
  172. $updates = [
  173. 'update available' => $updateAvailable,
  174. 'latest release version' => $latest,
  175. 'update check interval' => $config->getUpdateCheck(),
  176. 'update cache file' => $prettyPath($config->getUpdateCheckCacheFile()),
  177. ];
  178. $input = [
  179. 'interactive mode' => $config->interactiveMode(),
  180. 'input interactive' => $config->getInputInteractive(),
  181. 'yolo' => $config->yolo(),
  182. ];
  183. if ($config->hasReadline()) {
  184. $info = \readline_info();
  185. $readline = [
  186. 'readline available' => true,
  187. 'readline enabled' => $config->useReadline(),
  188. 'readline service' => \get_class($config->getReadline()),
  189. ];
  190. if (isset($info['library_version'])) {
  191. $readline['readline library'] = $info['library_version'];
  192. }
  193. if (isset($info['readline_name']) && $info['readline_name'] !== '') {
  194. $readline['readline name'] = $info['readline_name'];
  195. }
  196. } else {
  197. $readline = [
  198. 'readline available' => false,
  199. ];
  200. }
  201. $output = [
  202. 'color mode' => $config->colorMode(),
  203. 'output decorated' => $config->getOutputDecorated(),
  204. 'output verbosity' => $config->verbosity(),
  205. 'output pager' => $config->getPager(),
  206. ];
  207. $theme = $config->theme();
  208. // TODO: show styles (but only if they're different than default?)
  209. $output['theme'] = [
  210. 'compact' => $theme->compact(),
  211. 'prompt' => $theme->prompt(),
  212. 'bufferPrompt' => $theme->bufferPrompt(),
  213. 'replayPrompt' => $theme->replayPrompt(),
  214. 'returnValue' => $theme->returnValue(),
  215. ];
  216. $pcntl = [
  217. 'pcntl available' => ProcessForker::isPcntlSupported(),
  218. 'posix available' => ProcessForker::isPosixSupported(),
  219. ];
  220. if ($disabledPcntl = ProcessForker::disabledPcntlFunctions()) {
  221. $pcntl['disabled pcntl functions'] = $disabledPcntl;
  222. }
  223. if ($disabledPosix = ProcessForker::disabledPosixFunctions()) {
  224. $pcntl['disabled posix functions'] = $disabledPosix;
  225. }
  226. $pcntl['use pcntl'] = $config->usePcntl();
  227. $history = [
  228. 'history file' => $prettyPath($config->getHistoryFile()),
  229. 'history size' => $config->getHistorySize(),
  230. 'erase duplicates' => $config->getEraseDuplicates(),
  231. ];
  232. $docs = [
  233. 'manual db file' => $prettyPath($config->getManualDbFile()),
  234. 'sqlite available' => true,
  235. ];
  236. try {
  237. if ($db = $config->getManualDb()) {
  238. if ($q = $db->query('SELECT * FROM meta;')) {
  239. $q->setFetchMode(\PDO::FETCH_KEY_PAIR);
  240. $meta = $q->fetchAll();
  241. foreach ($meta as $key => $val) {
  242. switch ($key) {
  243. case 'built_at':
  244. $d = new \DateTime('@'.$val);
  245. $val = $d->format(\DateTime::RFC2822);
  246. break;
  247. }
  248. $key = 'db '.\str_replace('_', ' ', $key);
  249. $docs[$key] = $val;
  250. }
  251. } else {
  252. $docs['db schema'] = '0.1.0';
  253. }
  254. }
  255. } catch (Exception\RuntimeException $e) {
  256. if ($e->getMessage() === 'SQLite PDO driver not found') {
  257. $docs['sqlite available'] = false;
  258. } else {
  259. throw $e;
  260. }
  261. }
  262. $autocomplete = [
  263. 'tab completion enabled' => $config->useTabCompletion(),
  264. 'bracketed paste' => $config->useBracketedPaste(),
  265. ];
  266. // Shenanigans, but totally justified.
  267. try {
  268. if ($shell = Sudo::fetchProperty($config, 'shell')) {
  269. $shellClass = \get_class($shell);
  270. if ($shellClass !== 'Psy\\Shell') {
  271. $shellInfo = [
  272. 'PsySH version' => $shell::VERSION,
  273. 'Shell class' => $shellClass,
  274. ];
  275. }
  276. try {
  277. $core['loop listeners'] = \array_map('get_class', Sudo::fetchProperty($shell, 'loopListeners'));
  278. } catch (\ReflectionException $e) {
  279. // shrug
  280. }
  281. $core['commands'] = \array_map('get_class', $shell->all());
  282. try {
  283. $autocomplete['custom matchers'] = \array_map('get_class', Sudo::fetchProperty($shell, 'matchers'));
  284. } catch (\ReflectionException $e) {
  285. // shrug
  286. }
  287. }
  288. } catch (\ReflectionException $e) {
  289. // shrug
  290. }
  291. // @todo Show Presenter / custom casters.
  292. return \array_merge($shellInfo, $core, \compact('updates', 'pcntl', 'input', 'readline', 'output', 'history', 'docs', 'autocomplete'));
  293. }
  294. }
  295. if (!\function_exists('Psy\\bin')) {
  296. /**
  297. * `psysh` command line executable.
  298. *
  299. * @return \Closure
  300. */
  301. function bin(): \Closure
  302. {
  303. return function () {
  304. if (!isset($_SERVER['PSYSH_IGNORE_ENV']) || !$_SERVER['PSYSH_IGNORE_ENV']) {
  305. if (\defined('HHVM_VERSION_ID')) {
  306. \fwrite(\STDERR, 'PsySH v0.11 and higher does not support HHVM. Install an older version, or set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL);
  307. exit(1);
  308. }
  309. if (\PHP_VERSION_ID < 70000) {
  310. \fwrite(\STDERR, 'PHP 7.0.0 or higher is required. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL);
  311. exit(1);
  312. }
  313. if (\PHP_VERSION_ID > 89999) {
  314. \fwrite(\STDERR, 'PHP 9 or higher is not supported. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL);
  315. exit(1);
  316. }
  317. if (!\function_exists('json_encode')) {
  318. \fwrite(\STDERR, 'The JSON extension is required. Please install it. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL);
  319. exit(1);
  320. }
  321. if (!\function_exists('token_get_all')) {
  322. \fwrite(\STDERR, 'The Tokenizer extension is required. Please install it. You can set the environment variable PSYSH_IGNORE_ENV=1 to override this restriction and proceed anyway.'.\PHP_EOL);
  323. exit(1);
  324. }
  325. }
  326. $usageException = null;
  327. $shellIsPhar = Shell::isPhar();
  328. $input = new ArgvInput();
  329. try {
  330. $input->bind(new InputDefinition(\array_merge(Configuration::getInputOptions(), [
  331. new InputOption('help', 'h', InputOption::VALUE_NONE),
  332. new InputOption('version', 'V', InputOption::VALUE_NONE),
  333. new InputOption('self-update', 'u', InputOption::VALUE_NONE),
  334. new InputArgument('include', InputArgument::IS_ARRAY),
  335. ])));
  336. } catch (\RuntimeException $e) {
  337. $usageException = $e;
  338. }
  339. try {
  340. $config = Configuration::fromInput($input);
  341. } catch (\InvalidArgumentException $e) {
  342. $usageException = $e;
  343. }
  344. // Handle --help
  345. if ($usageException !== null || $input->getOption('help')) {
  346. if ($usageException !== null) {
  347. echo $usageException->getMessage().\PHP_EOL.\PHP_EOL;
  348. }
  349. $version = Shell::getVersionHeader(false);
  350. $argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : [];
  351. $name = $argv ? \basename(\reset($argv)) : 'psysh';
  352. echo <<<EOL
  353. $version
  354. Usage:
  355. $name [--version] [--help] [files...]
  356. Options:
  357. -h, --help Display this help message.
  358. -c, --config FILE Use an alternate PsySH config file location.
  359. --cwd PATH Use an alternate working directory.
  360. -V, --version Display the PsySH version.
  361. EOL;
  362. if ($shellIsPhar) {
  363. echo <<<EOL
  364. -u, --self-update Install a newer version if available.
  365. EOL;
  366. }
  367. echo <<<EOL
  368. --color Force colors in output.
  369. --no-color Disable colors in output.
  370. -i, --interactive Force PsySH to run in interactive mode.
  371. -n, --no-interactive Run PsySH without interactive input. Requires input from stdin.
  372. -r, --raw-output Print var_export-style return values (for non-interactive input)
  373. --compact Run PsySH with compact output.
  374. -q, --quiet Shhhhhh.
  375. -v|vv|vvv, --verbose Increase the verbosity of messages.
  376. --yolo Run PsySH without input validation. You don't want this.
  377. EOL;
  378. exit($usageException === null ? 0 : 1);
  379. }
  380. // Handle --version
  381. if ($input->getOption('version')) {
  382. echo Shell::getVersionHeader($config->useUnicode()).\PHP_EOL;
  383. exit(0);
  384. }
  385. // Handle --self-update
  386. if ($input->getOption('self-update')) {
  387. if (!$shellIsPhar) {
  388. \fwrite(\STDERR, 'The --self-update option can only be used with with a phar based install.'.\PHP_EOL);
  389. exit(1);
  390. }
  391. $selfUpdate = new SelfUpdate(new GitHubChecker(), new Installer());
  392. $result = $selfUpdate->run($input, $config->getOutput());
  393. exit($result);
  394. }
  395. $shell = new Shell($config);
  396. // Pass additional arguments to Shell as 'includes'
  397. $shell->setIncludes($input->getArgument('include'));
  398. try {
  399. // And go!
  400. $shell->run();
  401. } catch (\Throwable $e) {
  402. \fwrite(\STDERR, $e->getMessage().\PHP_EOL);
  403. // @todo this triggers the "exited unexpectedly" logic in the
  404. // ForkingLoop, so we can't exit(1) after starting the shell...
  405. // fix this :)
  406. // exit(1);
  407. }
  408. };
  409. }
  410. }