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