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