ConsoleIo.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Console;
  16. use Cake\Console\Exception\StopException;
  17. use Cake\Log\Engine\ConsoleLog;
  18. use Cake\Log\Log;
  19. use RuntimeException;
  20. use SplFileObject;
  21. /**
  22. * A wrapper around the various IO operations shell tasks need to do.
  23. *
  24. * Packages up the stdout, stderr, and stdin streams providing a simple
  25. * consistent interface for shells to use. This class also makes mocking streams
  26. * easy to do in unit tests.
  27. */
  28. class ConsoleIo
  29. {
  30. /**
  31. * The output stream
  32. *
  33. * @var \Cake\Console\ConsoleOutput
  34. */
  35. protected $_out;
  36. /**
  37. * The error stream
  38. *
  39. * @var \Cake\Console\ConsoleOutput
  40. */
  41. protected $_err;
  42. /**
  43. * The input stream
  44. *
  45. * @var \Cake\Console\ConsoleInput
  46. */
  47. protected $_in;
  48. /**
  49. * The helper registry.
  50. *
  51. * @var \Cake\Console\HelperRegistry
  52. */
  53. protected $_helpers;
  54. /**
  55. * Output constant making verbose shells.
  56. *
  57. * @var int
  58. */
  59. const VERBOSE = 2;
  60. /**
  61. * Output constant for making normal shells.
  62. *
  63. * @var int
  64. */
  65. const NORMAL = 1;
  66. /**
  67. * Output constants for making quiet shells.
  68. *
  69. * @var int
  70. */
  71. const QUIET = 0;
  72. /**
  73. * The current output level.
  74. *
  75. * @var int
  76. */
  77. protected $_level = self::NORMAL;
  78. /**
  79. * The number of bytes last written to the output stream
  80. * used when overwriting the previous message.
  81. *
  82. * @var int
  83. */
  84. protected $_lastWritten = 0;
  85. /**
  86. * Whether or not files should be overwritten
  87. *
  88. * @var bool
  89. */
  90. protected $forceOverwrite = false;
  91. /**
  92. * Constructor
  93. *
  94. * @param \Cake\Console\ConsoleOutput|null $out A ConsoleOutput object for stdout.
  95. * @param \Cake\Console\ConsoleOutput|null $err A ConsoleOutput object for stderr.
  96. * @param \Cake\Console\ConsoleInput|null $in A ConsoleInput object for stdin.
  97. * @param \Cake\Console\HelperRegistry|null $helpers A HelperRegistry instance
  98. */
  99. public function __construct(ConsoleOutput $out = null, ConsoleOutput $err = null, ConsoleInput $in = null, HelperRegistry $helpers = null)
  100. {
  101. $this->_out = $out ?: new ConsoleOutput('php://stdout');
  102. $this->_err = $err ?: new ConsoleOutput('php://stderr');
  103. $this->_in = $in ?: new ConsoleInput('php://stdin');
  104. $this->_helpers = $helpers ?: new HelperRegistry();
  105. $this->_helpers->setIo($this);
  106. }
  107. /**
  108. * Get/set the current output level.
  109. *
  110. * @param null|int $level The current output level.
  111. * @return int The current output level.
  112. */
  113. public function level($level = null)
  114. {
  115. if ($level !== null) {
  116. $this->_level = $level;
  117. }
  118. return $this->_level;
  119. }
  120. /**
  121. * Output at the verbose level.
  122. *
  123. * @param string|array $message A string or an array of strings to output
  124. * @param int $newlines Number of newlines to append
  125. * @return int|bool The number of bytes returned from writing to stdout.
  126. */
  127. public function verbose($message, $newlines = 1)
  128. {
  129. return $this->out($message, $newlines, self::VERBOSE);
  130. }
  131. /**
  132. * Output at all levels.
  133. *
  134. * @param string|array $message A string or an array of strings to output
  135. * @param int $newlines Number of newlines to append
  136. * @return int|bool The number of bytes returned from writing to stdout.
  137. */
  138. public function quiet($message, $newlines = 1)
  139. {
  140. return $this->out($message, $newlines, self::QUIET);
  141. }
  142. /**
  143. * Outputs a single or multiple messages to stdout. If no parameters
  144. * are passed outputs just a newline.
  145. *
  146. * ### Output levels
  147. *
  148. * There are 3 built-in output level. ConsoleIo::QUIET, ConsoleIo::NORMAL, ConsoleIo::VERBOSE.
  149. * The verbose and quiet output levels, map to the `verbose` and `quiet` output switches
  150. * present in most shells. Using ConsoleIo::QUIET for a message means it will always display.
  151. * While using ConsoleIo::VERBOSE means it will only display when verbose output is toggled.
  152. *
  153. * @param string|array $message A string or an array of strings to output
  154. * @param int $newlines Number of newlines to append
  155. * @param int $level The message's output level, see above.
  156. * @return int|bool The number of bytes returned from writing to stdout.
  157. */
  158. public function out($message = '', $newlines = 1, $level = self::NORMAL)
  159. {
  160. if ($level <= $this->_level) {
  161. $this->_lastWritten = (int)$this->_out->write($message, $newlines);
  162. return $this->_lastWritten;
  163. }
  164. return true;
  165. }
  166. /**
  167. * Convenience method for out() that wraps message between <info /> tag
  168. *
  169. * @param string|array|null $message A string or an array of strings to output
  170. * @param int $newlines Number of newlines to append
  171. * @param int $level The message's output level, see above.
  172. * @return int|bool The number of bytes returned from writing to stdout.
  173. * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::out
  174. */
  175. public function info($message = null, $newlines = 1, $level = self::NORMAL)
  176. {
  177. $messageType = 'info';
  178. $message = $this->wrapMessageWithType($messageType, $message);
  179. return $this->out($message, $newlines, $level);
  180. }
  181. /**
  182. * Convenience method for err() that wraps message between <warning /> tag
  183. *
  184. * @param string|array|null $message A string or an array of strings to output
  185. * @param int $newlines Number of newlines to append
  186. * @return int|bool The number of bytes returned from writing to stderr.
  187. * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::err
  188. */
  189. public function warning($message = null, $newlines = 1)
  190. {
  191. $messageType = 'warning';
  192. $message = $this->wrapMessageWithType($messageType, $message);
  193. return $this->err($message, $newlines);
  194. }
  195. /**
  196. * Convenience method for err() that wraps message between <error /> tag
  197. *
  198. * @param string|array|null $message A string or an array of strings to output
  199. * @param int $newlines Number of newlines to append
  200. * @return int|bool The number of bytes returned from writing to stderr.
  201. * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::err
  202. */
  203. public function error($message = null, $newlines = 1)
  204. {
  205. $messageType = 'error';
  206. $message = $this->wrapMessageWithType($messageType, $message);
  207. return $this->err($message, $newlines);
  208. }
  209. /**
  210. * Convenience method for out() that wraps message between <success /> tag
  211. *
  212. * @param string|array|null $message A string or an array of strings to output
  213. * @param int $newlines Number of newlines to append
  214. * @param int $level The message's output level, see above.
  215. * @return int|bool The number of bytes returned from writing to stdout.
  216. * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::out
  217. */
  218. public function success($message = null, $newlines = 1, $level = self::NORMAL)
  219. {
  220. $messageType = 'success';
  221. $message = $this->wrapMessageWithType($messageType, $message);
  222. return $this->out($message, $newlines, $level);
  223. }
  224. /**
  225. * Wraps a message with a given message type, e.g. <warning>
  226. *
  227. * @param string $messageType The message type, e.g. "warning".
  228. * @param string|array $message The message to wrap.
  229. * @return array|string The message wrapped with the given message type.
  230. */
  231. protected function wrapMessageWithType($messageType, $message)
  232. {
  233. if (is_array($message)) {
  234. foreach ($message as $k => $v) {
  235. $message[$k] = "<{$messageType}>{$v}</{$messageType}>";
  236. }
  237. } else {
  238. $message = "<{$messageType}>{$message}</{$messageType}>";
  239. }
  240. return $message;
  241. }
  242. /**
  243. * Overwrite some already output text.
  244. *
  245. * Useful for building progress bars, or when you want to replace
  246. * text already output to the screen with new text.
  247. *
  248. * **Warning** You cannot overwrite text that contains newlines.
  249. *
  250. * @param array|string $message The message to output.
  251. * @param int $newlines Number of newlines to append.
  252. * @param int|null $size The number of bytes to overwrite. Defaults to the
  253. * length of the last message output.
  254. * @return void
  255. */
  256. public function overwrite($message, $newlines = 1, $size = null)
  257. {
  258. $size = $size ?: $this->_lastWritten;
  259. // Output backspaces.
  260. $this->out(str_repeat("\x08", $size), 0);
  261. $newBytes = $this->out($message, 0);
  262. // Fill any remaining bytes with spaces.
  263. $fill = $size - $newBytes;
  264. if ($fill > 0) {
  265. $this->out(str_repeat(' ', $fill), 0);
  266. }
  267. if ($newlines) {
  268. $this->out($this->nl($newlines), 0);
  269. }
  270. // Store length of content + fill so if the new content
  271. // is shorter than the old content the next overwrite
  272. // will work.
  273. if ($fill > 0) {
  274. $this->_lastWritten = $newBytes + $fill;
  275. }
  276. }
  277. /**
  278. * Outputs a single or multiple error messages to stderr. If no parameters
  279. * are passed outputs just a newline.
  280. *
  281. * @param string|array $message A string or an array of strings to output
  282. * @param int $newlines Number of newlines to append
  283. * @return int|bool The number of bytes returned from writing to stderr.
  284. */
  285. public function err($message = '', $newlines = 1)
  286. {
  287. return $this->_err->write($message, $newlines);
  288. }
  289. /**
  290. * Returns a single or multiple linefeeds sequences.
  291. *
  292. * @param int $multiplier Number of times the linefeed sequence should be repeated
  293. * @return string
  294. */
  295. public function nl($multiplier = 1)
  296. {
  297. return str_repeat(ConsoleOutput::LF, $multiplier);
  298. }
  299. /**
  300. * Outputs a series of minus characters to the standard output, acts as a visual separator.
  301. *
  302. * @param int $newlines Number of newlines to pre- and append
  303. * @param int $width Width of the line, defaults to 79
  304. * @return void
  305. */
  306. public function hr($newlines = 0, $width = 79)
  307. {
  308. $this->out(null, $newlines);
  309. $this->out(str_repeat('-', $width));
  310. $this->out(null, $newlines);
  311. }
  312. /**
  313. * Prompts the user for input, and returns it.
  314. *
  315. * @param string $prompt Prompt text.
  316. * @param string|null $default Default input value.
  317. * @return mixed Either the default value, or the user-provided input.
  318. */
  319. public function ask($prompt, $default = null)
  320. {
  321. return $this->_getInput($prompt, null, $default);
  322. }
  323. /**
  324. * Change the output mode of the stdout stream
  325. *
  326. * @param int $mode The output mode.
  327. * @return void
  328. * @see \Cake\Console\ConsoleOutput::setOutputAs()
  329. */
  330. public function setOutputAs($mode)
  331. {
  332. $this->_out->setOutputAs($mode);
  333. }
  334. /**
  335. * Change the output mode of the stdout stream
  336. *
  337. * @deprecated 3.5.0 Use setOutputAs() instead.
  338. * @param int $mode The output mode.
  339. * @return void
  340. * @see \Cake\Console\ConsoleOutput::outputAs()
  341. */
  342. public function outputAs($mode)
  343. {
  344. deprecationWarning('ConsoleIo::outputAs() is deprecated. Use ConsoleIo::setOutputAs() instead.');
  345. $this->_out->setOutputAs($mode);
  346. }
  347. /**
  348. * Add a new output style or get defined styles.
  349. *
  350. * @param string|null $style The style to get or create.
  351. * @param array|bool|null $definition The array definition of the style to change or create a style
  352. * or false to remove a style.
  353. * @return mixed If you are getting styles, the style or null will be returned. If you are creating/modifying
  354. * styles true will be returned.
  355. * @see \Cake\Console\ConsoleOutput::styles()
  356. */
  357. public function styles($style = null, $definition = null)
  358. {
  359. $this->_out->styles($style, $definition);
  360. }
  361. /**
  362. * Prompts the user for input based on a list of options, and returns it.
  363. *
  364. * @param string $prompt Prompt text.
  365. * @param string|array $options Array or string of options.
  366. * @param string|null $default Default input value.
  367. * @return mixed Either the default value, or the user-provided input.
  368. */
  369. public function askChoice($prompt, $options, $default = null)
  370. {
  371. if ($options && is_string($options)) {
  372. if (strpos($options, ',')) {
  373. $options = explode(',', $options);
  374. } elseif (strpos($options, '/')) {
  375. $options = explode('/', $options);
  376. } else {
  377. $options = [$options];
  378. }
  379. }
  380. $printOptions = '(' . implode('/', $options) . ')';
  381. $options = array_merge(
  382. array_map('strtolower', $options),
  383. array_map('strtoupper', $options),
  384. $options
  385. );
  386. $in = '';
  387. while ($in === '' || !in_array($in, $options)) {
  388. $in = $this->_getInput($prompt, $printOptions, $default);
  389. }
  390. return $in;
  391. }
  392. /**
  393. * Prompts the user for input, and returns it.
  394. *
  395. * @param string $prompt Prompt text.
  396. * @param string|null $options String of options. Pass null to omit.
  397. * @param string|null $default Default input value. Pass null to omit.
  398. * @return string Either the default value, or the user-provided input.
  399. */
  400. protected function _getInput($prompt, $options, $default)
  401. {
  402. $optionsText = '';
  403. if (isset($options)) {
  404. $optionsText = " $options ";
  405. }
  406. $defaultText = '';
  407. if ($default !== null) {
  408. $defaultText = "[$default] ";
  409. }
  410. $this->_out->write('<question>' . $prompt . "</question>$optionsText\n$defaultText> ", 0);
  411. $result = $this->_in->read();
  412. $result = trim($result);
  413. if ($default !== null && ($result === '' || $result === null)) {
  414. return $default;
  415. }
  416. return $result;
  417. }
  418. /**
  419. * Connects or disconnects the loggers to the console output.
  420. *
  421. * Used to enable or disable logging stream output to stdout and stderr
  422. * If you don't wish all log output in stdout or stderr
  423. * through Cake's Log class, call this function with `$enable=false`.
  424. *
  425. * @param int|bool $enable Use a boolean to enable/toggle all logging. Use
  426. * one of the verbosity constants (self::VERBOSE, self::QUIET, self::NORMAL)
  427. * to control logging levels. VERBOSE enables debug logs, NORMAL does not include debug logs,
  428. * QUIET disables notice, info and debug logs.
  429. * @return void
  430. */
  431. public function setLoggers($enable)
  432. {
  433. Log::drop('stdout');
  434. Log::drop('stderr');
  435. if ($enable === false) {
  436. return;
  437. }
  438. $outLevels = ['notice', 'info'];
  439. if ($enable === static::VERBOSE || $enable === true) {
  440. $outLevels[] = 'debug';
  441. }
  442. if ($enable !== static::QUIET) {
  443. $stdout = new ConsoleLog([
  444. 'types' => $outLevels,
  445. 'stream' => $this->_out
  446. ]);
  447. Log::setConfig('stdout', ['engine' => $stdout]);
  448. }
  449. $stderr = new ConsoleLog([
  450. 'types' => ['emergency', 'alert', 'critical', 'error', 'warning'],
  451. 'stream' => $this->_err,
  452. ]);
  453. Log::setConfig('stderr', ['engine' => $stderr]);
  454. }
  455. /**
  456. * Render a Console Helper
  457. *
  458. * Create and render the output for a helper object. If the helper
  459. * object has not already been loaded, it will be loaded and constructed.
  460. *
  461. * @param string $name The name of the helper to render
  462. * @param array $settings Configuration data for the helper.
  463. * @return \Cake\Console\Helper The created helper instance.
  464. */
  465. public function helper($name, array $settings = [])
  466. {
  467. $name = ucfirst($name);
  468. return $this->_helpers->load($name, $settings);
  469. }
  470. /**
  471. * Create a file at the given path.
  472. *
  473. * This method will prompt the user if a file will be overwritten.
  474. * Setting `forceOverwrite` to true will suppress this behavior
  475. * and always overwrite the file.
  476. *
  477. * If the user replies `a` subsequent `forceOverwrite` parameters will
  478. * be coerced to true and all files will be overwritten.
  479. *
  480. * @param string $path The path to create the file at.
  481. * @param string $contents The contents to put into the file.
  482. * @param bool $forceOverwrite Whether or not the file should be overwritten.
  483. * If true, no question will be asked about whether or not to overwrite existing files.
  484. * @return bool Success.
  485. * @throws \Cake\Console\Exception\StopException When `q` is given as an answer
  486. * to whether or not a file should be overwritten.
  487. */
  488. public function createFile($path, $contents, $forceOverwrite = false)
  489. {
  490. $this->out();
  491. $forceOverwrite = $forceOverwrite || $this->forceOverwrite;
  492. if (file_exists($path) && $forceOverwrite === false) {
  493. $this->warning("File `{$path}` exists");
  494. $key = $this->askChoice('Do you want to overwrite?', ['y', 'n', 'a', 'q'], 'n');
  495. $key = strtolower($key);
  496. if ($key === 'q') {
  497. $this->error('Quitting.', 2);
  498. throw new StopException('Not creating file. Quitting.');
  499. }
  500. if ($key === 'a') {
  501. $this->forceOverwrite = true;
  502. $key = 'y';
  503. }
  504. if ($key !== 'y') {
  505. $this->out("Skip `{$path}`", 2);
  506. return false;
  507. }
  508. } else {
  509. $this->out("Creating file {$path}");
  510. }
  511. try {
  512. $file = new SplFileObject($path, 'w');
  513. } catch (RuntimeException $e) {
  514. $this->error("Could not write to `{$path}`. Permission denied.", 2);
  515. return false;
  516. }
  517. $file->rewind();
  518. if ($file->fwrite($contents) > 0) {
  519. $this->out("<success>Wrote</success> `{$path}`");
  520. return true;
  521. }
  522. $this->error("Could not write to `{$path}`.", 2);
  523. return false;
  524. }
  525. }