Passed
Pull Request — 2.2 (#19879)
by Wilmer
05:27
created

BaseConsole::getScreenSize()   C

Complexity

Conditions 17
Paths 32

Size

Total Lines 51
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 45.4061

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 26
c 1
b 0
f 0
nc 32
nop 1
dl 0
loc 51
ccs 14
cts 26
cp 0.5385
crap 45.4061
rs 5.2166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\helpers;
9
10
use Yii;
11
use yii\console\Markdown as ConsoleMarkdown;
12
use yii\base\Model;
13
14
/**
15
 * BaseConsole provides concrete implementation for [[Console]].
16
 *
17
 * Do not use BaseConsole. Use [[Console]] instead.
18
 *
19
 * @author Carsten Brandt <[email protected]>
20
 * @since 2.0
21
 */
22
class BaseConsole
23
{
24
    // foreground color control codes
25
    const FG_BLACK = 30;
26
    const FG_RED = 31;
27
    const FG_GREEN = 32;
28
    const FG_YELLOW = 33;
29
    const FG_BLUE = 34;
30
    const FG_PURPLE = 35;
31
    const FG_CYAN = 36;
32
    const FG_GREY = 37;
33
    // background color control codes
34
    const BG_BLACK = 40;
35
    const BG_RED = 41;
36
    const BG_GREEN = 42;
37
    const BG_YELLOW = 43;
38
    const BG_BLUE = 44;
39
    const BG_PURPLE = 45;
40
    const BG_CYAN = 46;
41
    const BG_GREY = 47;
42
    // fonts style control codes
43
    const RESET = 0;
44
    const NORMAL = 0;
45
    const BOLD = 1;
46
    const ITALIC = 3;
47
    const UNDERLINE = 4;
48
    const BLINK = 5;
49
    const NEGATIVE = 7;
50
    const CONCEALED = 8;
51
    const CROSSED_OUT = 9;
52
    const FRAMED = 51;
53
    const ENCIRCLED = 52;
54
    const OVERLINED = 53;
55
56
57
    /**
58
     * Moves the terminal cursor up by sending ANSI control code CUU to the terminal.
59
     * If the cursor is already at the edge of the screen, this has no effect.
60
     * @param int $rows number of rows the cursor should be moved up
61
     */
62 1
    public static function moveCursorUp($rows = 1)
63
    {
64 1
        echo "\033[" . (int) $rows . 'A';
65
    }
66
67
    /**
68
     * Moves the terminal cursor down by sending ANSI control code CUD to the terminal.
69
     * If the cursor is already at the edge of the screen, this has no effect.
70
     * @param int $rows number of rows the cursor should be moved down
71
     */
72 1
    public static function moveCursorDown($rows = 1)
73
    {
74 1
        echo "\033[" . (int) $rows . 'B';
75
    }
76
77
    /**
78
     * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal.
79
     * If the cursor is already at the edge of the screen, this has no effect.
80
     * @param int $steps number of steps the cursor should be moved forward
81
     */
82 1
    public static function moveCursorForward($steps = 1)
83
    {
84 1
        echo "\033[" . (int) $steps . 'C';
85
    }
86
87
    /**
88
     * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal.
89
     * If the cursor is already at the edge of the screen, this has no effect.
90
     * @param int $steps number of steps the cursor should be moved backward
91
     */
92 1
    public static function moveCursorBackward($steps = 1)
93
    {
94 1
        echo "\033[" . (int) $steps . 'D';
95
    }
96
97
    /**
98
     * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal.
99
     * @param int $lines number of lines the cursor should be moved down
100
     */
101 1
    public static function moveCursorNextLine($lines = 1)
102
    {
103 1
        echo "\033[" . (int) $lines . 'E';
104
    }
105
106
    /**
107
     * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal.
108
     * @param int $lines number of lines the cursor should be moved up
109
     */
110 1
    public static function moveCursorPrevLine($lines = 1)
111
    {
112 1
        echo "\033[" . (int) $lines . 'F';
113
    }
114
115
    /**
116
     * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal.
117
     * @param int $column 1-based column number, 1 is the left edge of the screen.
118
     * @param int|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line.
119
     */
120 1
    public static function moveCursorTo($column, $row = null)
121
    {
122 1
        if ($row === null) {
123 1
            echo "\033[" . (int) $column . 'G';
124
        } else {
125 1
            echo "\033[" . (int) $row . ';' . (int) $column . 'H';
126
        }
127
    }
128
129
    /**
130
     * Scrolls whole page up by sending ANSI control code SU to the terminal.
131
     * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows.
132
     * @param int $lines number of lines to scroll up
133
     */
134 1
    public static function scrollUp($lines = 1)
135
    {
136 1
        echo "\033[" . (int) $lines . 'S';
137
    }
138
139
    /**
140
     * Scrolls whole page down by sending ANSI control code SD to the terminal.
141
     * New lines are added at the top. This is not supported by ANSI.SYS used in windows.
142
     * @param int $lines number of lines to scroll down
143
     */
144 1
    public static function scrollDown($lines = 1)
145
    {
146 1
        echo "\033[" . (int) $lines . 'T';
147
    }
148
149
    /**
150
     * Saves the current cursor position by sending ANSI control code SCP to the terminal.
151
     * Position can then be restored with [[restoreCursorPosition()]].
152
     */
153 1
    public static function saveCursorPosition()
154
    {
155 1
        echo "\033[s";
156
    }
157
158
    /**
159
     * Restores the cursor position saved with [[saveCursorPosition()]] by sending ANSI control code RCP to the terminal.
160
     */
161 1
    public static function restoreCursorPosition()
162
    {
163 1
        echo "\033[u";
164
    }
165
166
    /**
167
     * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal.
168
     * Use [[showCursor()]] to bring it back.
169
     * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit.
170
     */
171 1
    public static function hideCursor()
172
    {
173 1
        echo "\033[?25l";
174
    }
175
176
    /**
177
     * Will show a cursor again when it has been hidden by [[hideCursor()]]  by sending ANSI DECTCEM code ?25h to the terminal.
178
     */
179 1
    public static function showCursor()
180
    {
181 1
        echo "\033[?25h";
182
    }
183
184
    /**
185
     * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal.
186
     * Cursor position will not be changed.
187
     * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen.
188
     */
189 1
    public static function clearScreen()
190
    {
191 1
        echo "\033[2J";
192
    }
193
194
    /**
195
     * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal.
196
     * Cursor position will not be changed.
197
     */
198 1
    public static function clearScreenBeforeCursor()
199
    {
200 1
        echo "\033[1J";
201
    }
202
203
    /**
204
     * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal.
205
     * Cursor position will not be changed.
206
     */
207 1
    public static function clearScreenAfterCursor()
208
    {
209 1
        echo "\033[0J";
210
    }
211
212
    /**
213
     * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal.
214
     * Cursor position will not be changed.
215
     */
216 1
    public static function clearLine()
217
    {
218 1
        echo "\033[2K";
219
    }
220
221
    /**
222
     * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal.
223
     * Cursor position will not be changed.
224
     */
225 1
    public static function clearLineBeforeCursor()
226
    {
227 1
        echo "\033[1K";
228
    }
229
230
    /**
231
     * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal.
232
     * Cursor position will not be changed.
233
     */
234 1
    public static function clearLineAfterCursor()
235
    {
236 1
        echo "\033[0K";
237
    }
238
239
    /**
240
     * Returns the ANSI format code.
241
     *
242
     * @param array $format An array containing formatting values.
243
     * You can pass any of the `FG_*`, `BG_*` and `TEXT_*` constants
244
     * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format.
245
     * @return string The ANSI format code according to the given formatting constants.
246
     */
247 32
    public static function ansiFormatCode($format)
248
    {
249 32
        return "\033[" . implode(';', $format) . 'm';
250
    }
251
252
    /**
253
     * Echoes an ANSI format code that affects the formatting of any text that is printed afterwards.
254
     *
255
     * @param array $format An array containing formatting values.
256
     * You can pass any of the `FG_*`, `BG_*` and `TEXT_*` constants
257
     * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format.
258
     * @see ansiFormatCode()
259
     * @see endAnsiFormat()
260
     */
261 1
    public static function beginAnsiFormat($format)
262
    {
263 1
        echo "\033[" . implode(';', $format) . 'm';
264
    }
265
266
    /**
267
     * Resets any ANSI format set by previous method [[beginAnsiFormat()]]
268
     * Any output after this will have default text format.
269
     * This is equal to calling.
270
     *
271
     * ```php
272
     * echo Console::ansiFormatCode([Console::RESET])
273
     * ```
274
     */
275 1
    public static function endAnsiFormat()
276
    {
277 1
        echo "\033[0m";
278
    }
279
280
    /**
281
     * Will return a string formatted with the given ANSI style.
282
     *
283
     * @param string $string the string to be formatted
284
     * @param array $format An array containing formatting values.
285
     * You can pass any of the `FG_*`, `BG_*` and `TEXT_*` constants
286
     * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format.
287
     * @return string
288
     */
289 31
    public static function ansiFormat($string, $format = [])
290
    {
291 31
        $code = implode(';', $format);
292
293 31
        return "\033[0m" . ($code !== '' ? "\033[" . $code . 'm' : '') . $string . "\033[0m";
294
    }
295
296
    /**
297
     * Returns the ansi format code for xterm foreground color.
298
     *
299
     * You can pass the return value of this to one of the formatting methods:
300
     * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]].
301
     *
302
     * @param int $colorCode xterm color code
303
     * @return string
304
     * @see https://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors
305
     */
306 1
    public static function xtermFgColor($colorCode)
307
    {
308 1
        return '38;5;' . $colorCode;
309
    }
310
311
    /**
312
     * Returns the ansi format code for xterm background color.
313
     *
314
     * You can pass the return value of this to one of the formatting methods:
315
     * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]].
316
     *
317
     * @param int $colorCode xterm color code
318
     * @return string
319
     * @see https://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors
320
     */
321 1
    public static function xtermBgColor($colorCode)
322
    {
323 1
        return '48;5;' . $colorCode;
324
    }
325
326
    /**
327
     * Strips ANSI control codes from a string.
328
     *
329
     * @param string $string String to strip
330
     * @return string
331
     */
332 39
    public static function stripAnsiFormat($string)
333
    {
334 39
        return preg_replace(self::ansiCodesPattern(), '', (string)$string);
335
    }
336
337
    /**
338
     * Returns the length of the string without ANSI color codes.
339
     * @param string $string the string to measure
340
     * @return int the length of the string not counting ANSI format characters
341
     */
342 1
    public static function ansiStrlen($string)
343
    {
344 1
        return mb_strlen(static::stripAnsiFormat($string));
345
    }
346
347
    /**
348
     * Returns the width of the string without ANSI color codes.
349
     * @param string $string the string to measure
350
     * @return int the width of the string not counting ANSI format characters
351
     * @since 2.0.36
352
     */
353 28
    public static function ansiStrwidth($string)
354
    {
355 28
        return mb_strwidth(static::stripAnsiFormat($string), Yii::$app->charset);
356
    }
357
358
    /**
359
     * Returns the portion with ANSI color codes of string specified by the start and length parameters.
360
     * If string has color codes, then will be return "TEXT_COLOR + TEXT_STRING + DEFAULT_COLOR",
361
     * else will be simple "TEXT_STRING".
362
     * @param string $string
363
     * @param int $start
364
     * @param int $length
365
     * @return string
366
     */
367 28
    public static function ansiColorizedSubstr($string, $start, $length)
368
    {
369 28
        if ($start < 0 || $length <= 0) {
370 1
            return '';
371
        }
372
373 28
        $textItems = preg_split(self::ansiCodesPattern(), (string)$string);
374
375 28
        preg_match_all(self::ansiCodesPattern(), (string)$string, $colors);
376 28
        $colors = count($colors) ? $colors[0] : [];
377 28
        array_unshift($colors, '');
378
379 28
        $result = '';
380 28
        $curPos = 0;
381 28
        $inRange = false;
382
383 28
        foreach ($textItems as $k => $textItem) {
384 28
            $color = $colors[$k];
385
386 28
            if ($curPos <= $start && $start < $curPos + Console::ansiStrwidth($textItem)) {
387 28
                $text = mb_substr($textItem, $start - $curPos, null, Yii::$app->charset);
388 28
                $inRange = true;
389
            } else {
390 18
                $text = $textItem;
391
            }
392
393 28
            if ($inRange) {
394 28
                $result .= $color . $text;
395 28
                $diff = $length - Console::ansiStrwidth($result);
396 28
                if ($diff <= 0) {
397 28
                    if ($diff < 0) {
398 11
                        $result = mb_substr($result, 0, $diff, Yii::$app->charset);
399
                    }
400 28
                    $defaultColor = static::renderColoredString('%n');
401 28
                    if ($color && $color != $defaultColor) {
402 6
                        $result .= $defaultColor;
403
                    }
404 28
                    break;
405
                }
406
            }
407
408 26
            $curPos += mb_strlen($textItem, Yii::$app->charset);
409
        }
410
411 28
        return $result;
412
    }
413
414 39
    private static function ansiCodesPattern()
415
    {
416 39
        return /** @lang PhpRegExp */ '/\033\[[\d;?]*\w/';
417
    }
418
419
    /**
420
     * Converts an ANSI formatted string to HTML.
421
     *
422
     * Note: xTerm 256 bit colors are currently not supported.
423
     *
424
     * @param string $string the string to convert.
425
     * @param array $styleMap an optional mapping of ANSI control codes such as
426
     * FG\_*COLOR* or [[BOLD]] to a set of css style definitions.
427
     * The CSS style definitions are represented as an array where the array keys correspond
428
     * to the css style attribute names and the values are the css values.
429
     * values may be arrays that will be merged and imploded with `' '` when rendered.
430
     * @return string HTML representation of the ANSI formatted string
431
     */
432 15
    public static function ansiToHtml($string, $styleMap = [])
433
    {
434 15
        $styleMap = [
435
            // https://www.w3.org/TR/CSS2/syndata.html#value-def-color
436 15
            self::FG_BLACK => ['color' => 'black'],
437 15
            self::FG_BLUE => ['color' => 'blue'],
438 15
            self::FG_CYAN => ['color' => 'aqua'],
439 15
            self::FG_GREEN => ['color' => 'lime'],
440 15
            self::FG_GREY => ['color' => 'silver'],
441
            // https://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/
442
            // https://drafts.csswg.org/css-color/#valuedef-rebeccapurple
443 15
            self::FG_PURPLE => ['color' => 'rebeccapurple'],
444 15
            self::FG_RED => ['color' => 'red'],
445 15
            self::FG_YELLOW => ['color' => 'yellow'],
446 15
            self::BG_BLACK => ['background-color' => 'black'],
447 15
            self::BG_BLUE => ['background-color' => 'blue'],
448 15
            self::BG_CYAN => ['background-color' => 'aqua'],
449 15
            self::BG_GREEN => ['background-color' => 'lime'],
450 15
            self::BG_GREY => ['background-color' => 'silver'],
451 15
            self::BG_PURPLE => ['background-color' => 'rebeccapurple'],
452 15
            self::BG_RED => ['background-color' => 'red'],
453 15
            self::BG_YELLOW => ['background-color' => 'yellow'],
454 15
            self::BOLD => ['font-weight' => 'bold'],
455 15
            self::ITALIC => ['font-style' => 'italic'],
456 15
            self::UNDERLINE => ['text-decoration' => ['underline']],
457 15
            self::OVERLINED => ['text-decoration' => ['overline']],
458 15
            self::CROSSED_OUT => ['text-decoration' => ['line-through']],
459 15
            self::BLINK => ['text-decoration' => ['blink']],
460 15
            self::CONCEALED => ['visibility' => 'hidden'],
461 15
        ] + $styleMap;
462
463 15
        $tags = 0;
464 15
        $result = preg_replace_callback(
465 15
            '/\033\[([\d;]+)m/',
466 15
            function ($ansi) use (&$tags, $styleMap) {
467 14
                $style = [];
468 14
                $reset = false;
469 14
                $negative = false;
470 14
                foreach (explode(';', $ansi[1]) as $controlCode) {
471 14
                    if ($controlCode == 0) {
472 14
                        $style = [];
473 14
                        $reset = true;
474 11
                    } elseif ($controlCode == self::NEGATIVE) {
475 2
                        $negative = true;
476 10
                    } elseif (isset($styleMap[$controlCode])) {
477 10
                        $style[] = $styleMap[$controlCode];
478
                    }
479
                }
480
481 14
                $return = '';
482 14
                while ($reset && $tags > 0) {
483 10
                    $return .= '</span>';
484 10
                    $tags--;
485
                }
486 14
                if (empty($style)) {
487 14
                    return $return;
488
                }
489
490 10
                $currentStyle = [];
491 10
                foreach ($style as $content) {
492 10
                    $currentStyle = ArrayHelper::merge($currentStyle, $content);
493
                }
494
495
                // if negative is set, invert background and foreground
496 10
                if ($negative) {
497 1
                    if (isset($currentStyle['color'])) {
498 1
                        $fgColor = $currentStyle['color'];
499 1
                        unset($currentStyle['color']);
500
                    }
501 1
                    if (isset($currentStyle['background-color'])) {
502 1
                        $bgColor = $currentStyle['background-color'];
503 1
                        unset($currentStyle['background-color']);
504
                    }
505 1
                    if (isset($fgColor)) {
506 1
                        $currentStyle['background-color'] = $fgColor;
507
                    }
508 1
                    if (isset($bgColor)) {
509 1
                        $currentStyle['color'] = $bgColor;
510
                    }
511
                }
512
513 10
                $styleString = '';
514 10
                foreach ($currentStyle as $name => $value) {
515 10
                    if (is_array($value)) {
516 1
                        $value = implode(' ', $value);
517
                    }
518 10
                    $styleString .= "$name: $value;";
519
                }
520 10
                $tags++;
521 10
                return "$return<span style=\"$styleString\">";
522 15
            },
523 15
            $string
524 15
        );
525 15
        while ($tags > 0) {
526
            $result .= '</span>';
527
            $tags--;
528
        }
529
530 15
        return $result;
531
    }
532
533
    /**
534
     * Converts Markdown to be better readable in console environments by applying some ANSI format.
535
     * @param string $markdown the markdown string.
536
     * @return string the parsed result as ANSI formatted string.
537
     */
538 2
    public static function markdownToAnsi($markdown)
539
    {
540 2
        $parser = new ConsoleMarkdown();
541 2
        return $parser->parse($markdown);
542
    }
543
544
    /**
545
     * Converts a string to ansi formatted by replacing patterns like %y (for yellow) with ansi control codes.
546
     *
547
     * Uses almost the same syntax as https://github.com/pear/Console_Color2/blob/master/Console/Color2.php
548
     * The conversion table is: ('bold' meaning 'light' on some
549
     * terminals). It's almost the same conversion table irssi uses.
550
     * <pre>
551
     *                  text      text            background
552
     *      ------------------------------------------------
553
     *      %k %K %0    black     dark grey       black
554
     *      %r %R %1    red       bold red        red
555
     *      %g %G %2    green     bold green      green
556
     *      %y %Y %3    yellow    bold yellow     yellow
557
     *      %b %B %4    blue      bold blue       blue
558
     *      %m %M %5    magenta   bold magenta    magenta
559
     *      %p %P       magenta (think: purple)
560
     *      %c %C %6    cyan      bold cyan       cyan
561
     *      %w %W %7    white     bold white      white
562
     *
563
     *      %F     Blinking, Flashing
564
     *      %U     Underline
565
     *      %8     Reverse
566
     *      %_,%9  Bold
567
     *
568
     *      %n     Resets the color
569
     *      %%     A single %
570
     * </pre>
571
     * First param is the string to convert, second is an optional flag if
572
     * colors should be used. It defaults to true, if set to false, the
573
     * color codes will just be removed (And %% will be transformed into %)
574
     *
575
     * @param string $string String to convert
576
     * @param bool $colored Should the string be colored?
577
     * @return string
578
     */
579 32
    public static function renderColoredString($string, $colored = true)
580
    {
581
        // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746
582 32
        static $conversions = [
583 32
            '%y' => [self::FG_YELLOW],
584 32
            '%g' => [self::FG_GREEN],
585 32
            '%b' => [self::FG_BLUE],
586 32
            '%r' => [self::FG_RED],
587 32
            '%p' => [self::FG_PURPLE],
588 32
            '%m' => [self::FG_PURPLE],
589 32
            '%c' => [self::FG_CYAN],
590 32
            '%w' => [self::FG_GREY],
591 32
            '%k' => [self::FG_BLACK],
592 32
            '%n' => [0], // reset
593 32
            '%Y' => [self::FG_YELLOW, self::BOLD],
594 32
            '%G' => [self::FG_GREEN, self::BOLD],
595 32
            '%B' => [self::FG_BLUE, self::BOLD],
596 32
            '%R' => [self::FG_RED, self::BOLD],
597 32
            '%P' => [self::FG_PURPLE, self::BOLD],
598 32
            '%M' => [self::FG_PURPLE, self::BOLD],
599 32
            '%C' => [self::FG_CYAN, self::BOLD],
600 32
            '%W' => [self::FG_GREY, self::BOLD],
601 32
            '%K' => [self::FG_BLACK, self::BOLD],
602 32
            '%N' => [0, self::BOLD],
603 32
            '%3' => [self::BG_YELLOW],
604 32
            '%2' => [self::BG_GREEN],
605 32
            '%4' => [self::BG_BLUE],
606 32
            '%1' => [self::BG_RED],
607 32
            '%5' => [self::BG_PURPLE],
608 32
            '%6' => [self::BG_CYAN],
609 32
            '%7' => [self::BG_GREY],
610 32
            '%0' => [self::BG_BLACK],
611 32
            '%F' => [self::BLINK],
612 32
            '%U' => [self::UNDERLINE],
613 32
            '%8' => [self::NEGATIVE],
614 32
            '%9' => [self::BOLD],
615 32
            '%_' => [self::BOLD],
616 32
        ];
617
618 32
        if ($colored) {
619 32
            $string = str_replace('%%', '% ', $string);
620 32
            foreach ($conversions as $key => $value) {
621 32
                $string = str_replace(
622 32
                    $key,
623 32
                    static::ansiFormatCode($value),
624 32
                    $string
625 32
                );
626
            }
627 32
            $string = str_replace('% ', '%', $string);
628
        } else {
629 1
            $string = preg_replace('/%((%)|.)/', '$2', $string);
630
        }
631
632 32
        return $string;
633
    }
634
635
    /**
636
     * Escapes % so they don't get interpreted as color codes when
637
     * the string is parsed by [[renderColoredString]].
638
     *
639
     * @param string $string String to escape
640
     *
641
     * @return string
642
     */
643
    public static function escape($string)
644
    {
645
        // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746
646
        return str_replace('%', '%%', $string);
647
    }
648
649
    /**
650
     * Returns true if the stream supports colorization. ANSI colors are disabled if not supported by the stream.
651
     *
652
     * - windows without ansicon
653
     * - not tty consoles
654
     *
655
     * @param mixed $stream
656
     * @return bool true if the stream supports ANSI colors, otherwise false.
657
     */
658 6
    public static function streamSupportsAnsiColors($stream)
659
    {
660 6
        return DIRECTORY_SEPARATOR === '\\'
661
            ? getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON'
662 6
            : function_exists('posix_isatty') && @posix_isatty($stream);
663
    }
664
665
    /**
666
     * Returns true if the console is running on windows.
667
     * @return bool
668
     */
669 1
    public static function isRunningOnWindows()
670
    {
671 1
        return DIRECTORY_SEPARATOR === '\\';
672
    }
673
674
    /**
675
     * Returns terminal screen size.
676
     *
677
     * Usage:
678
     *
679
     * ```php
680
     * list($width, $height) = ConsoleHelper::getScreenSize();
681
     * ```
682
     *
683
     * @param bool $refresh whether to force checking and not re-use cached size value.
684
     * This is useful to detect changing window size while the application is running but may
685
     * not get up to date values on every terminal.
686
     * @return array|bool An array of ($width, $height) or false when it was not able to determine size.
687
     */
688 4
    public static function getScreenSize($refresh = false)
689
    {
690 4
        static $size;
691 4
        static $execDisabled;
692
693 4
        if ($size !== null && ($execDisabled || !$refresh)) {
694 4
            return $size;
695
        }
696
697 1
        if ($execDisabled === null) {
698 1
            $execDisabled = !function_exists('ini_get') || preg_match('/(\bexec\b)/i', ini_get('disable_functions'));
699 1
            if ($execDisabled) {
700
                return $size = false;
701
            }
702
        }
703
704 1
        if (static::isRunningOnWindows()) {
705
            $output = [];
706
            exec('mode con', $output);
707
            if (isset($output[1]) && strpos($output[1], 'CON') !== false) {
708
                return $size = [(int) preg_replace('~\D~', '', $output[4]), (int) preg_replace('~\D~', '', $output[3])];
709
            }
710
        } else {
711
            // try stty if available
712 1
            $stty = [];
713 1
            if (exec('stty -a 2>&1', $stty)) {
714 1
                $stty = implode(' ', $stty);
715
716
                // Linux stty output
717 1
                if (preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', $stty, $matches)) {
718 1
                    return $size = [(int) $matches[2], (int) $matches[1]];
719
                }
720
721
                // MacOS stty output
722
                if (preg_match('/(\d+)\s+rows;\s*(\d+)\s+columns;/mi', $stty, $matches)) {
723
                    return $size = [(int) $matches[2], (int) $matches[1]];
724
                }
725
            }
726
727
            // fallback to tput, which may not be updated on terminal resize
728
            if (($width = (int) exec('tput cols 2>&1')) > 0 && ($height = (int) exec('tput lines 2>&1')) > 0) {
729
                return $size = [$width, $height];
730
            }
731
732
            // fallback to ENV variables, which may not be updated on terminal resize
733
            if (($width = (int) getenv('COLUMNS')) > 0 && ($height = (int) getenv('LINES')) > 0) {
734
                return $size = [$width, $height];
735
            }
736
        }
737
738
        return $size = false;
739
    }
740
741
    /**
742
     * Word wrap text with indentation to fit the screen size.
743
     *
744
     * If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped.
745
     *
746
     * The first line will **not** be indented, so `Console::wrapText("Lorem ipsum dolor sit amet.", 4)` will result in the
747
     * following output, given the screen width is 16 characters:
748
     *
749
     * ```
750
     * Lorem ipsum
751
     *     dolor sit
752
     *     amet.
753
     * ```
754
     *
755
     * @param string $text the text to be wrapped
756
     * @param int $indent number of spaces to use for indentation.
757
     * @param bool $refresh whether to force refresh of screen size.
758
     * This will be passed to [[getScreenSize()]].
759
     * @return string the wrapped text.
760
     * @since 2.0.4
761
     */
762 2
    public static function wrapText($text, $indent = 0, $refresh = false)
763
    {
764 2
        $size = static::getScreenSize($refresh);
765 2
        if ($size === false || $size[0] <= $indent) {
766 2
            return $text;
767
        }
768
        $pad = str_repeat(' ', $indent);
769
        $lines = explode("\n", wordwrap($text, $size[0] - $indent, "\n"));
770
        $first = true;
771
        foreach ($lines as $i => $line) {
772
            if ($first) {
773
                $first = false;
774
                continue;
775
            }
776
            $lines[$i] = $pad . $line;
777
        }
778
779
        return implode("\n", $lines);
780
    }
781
782
    /**
783
     * Gets input from STDIN and returns a string right-trimmed for EOLs.
784
     *
785
     * @param bool $raw If set to true, returns the raw string without trimming
786
     * @return string the string read from stdin
787
     */
788
    public static function stdin($raw = false)
789
    {
790
        return $raw ? fgets(\STDIN) : rtrim(fgets(\STDIN), PHP_EOL);
791
    }
792
793
    /**
794
     * Prints a string to STDOUT.
795
     *
796
     * @param string $string the string to print
797
     * @return int|bool Number of bytes printed or false on error
798
     */
799
    public static function stdout($string)
800
    {
801
        return fwrite(\STDOUT, $string);
802
    }
803
804
    /**
805
     * Prints a string to STDERR.
806
     *
807
     * @param string $string the string to print
808
     * @return int|bool Number of bytes printed or false on error
809
     */
810
    public static function stderr($string)
811
    {
812
        return fwrite(\STDERR, $string);
813
    }
814
815
    /**
816
     * Asks the user for input. Ends when the user types a carriage return (PHP_EOL). Optionally, It also provides a
817
     * prompt.
818
     *
819
     * @param string|null $prompt the prompt to display before waiting for input (optional)
820
     * @return string the user's input
821
     */
822 1
    public static function input($prompt = null)
823
    {
824 1
        if (isset($prompt)) {
825 1
            static::stdout($prompt);
826
        }
827
828 1
        return static::stdin();
829
    }
830
831
    /**
832
     * Prints text to STDOUT appended with a carriage return (PHP_EOL).
833
     *
834
     * @param string|null $string the text to print
835
     * @return int|bool number of bytes printed or false on error.
836
     */
837 1
    public static function output($string = null)
838
    {
839 1
        return static::stdout($string . PHP_EOL);
840
    }
841
842
    /**
843
     * Prints text to STDERR appended with a carriage return (PHP_EOL).
844
     *
845
     * @param string|null $string the text to print
846
     * @return int|bool number of bytes printed or false on error.
847
     */
848 1
    public static function error($string = null)
849
    {
850 1
        return static::stderr($string . PHP_EOL);
851
    }
852
853
    /**
854
     * Prompts the user for input and validates it.
855
     *
856
     * @param string $text prompt string
857
     * @param array $options the options to validate the input:
858
     *
859
     * - `required`: whether it is required or not
860
     * - `default`: default value if no input is inserted by the user
861
     * - `pattern`: regular expression pattern to validate user input
862
     * - `validator`: a callable function to validate input. The function must accept two parameters:
863
     * - `input`: the user input to validate
864
     * - `error`: the error value passed by reference if validation failed.
865
     *
866
     * @return string the user input
867
     */
868 1
    public static function prompt($text, $options = [])
869
    {
870 1
        $options = ArrayHelper::merge(
871 1
            [
872 1
                'required' => false,
873 1
                'default' => null,
874 1
                'pattern' => null,
875 1
                'validator' => null,
876 1
                'error' => 'Invalid input.',
877 1
            ],
878 1
            $options
879 1
        );
880 1
        $error = null;
881
882
        top:
883 1
        $input = $options['default']
884 1
            ? static::input("$text [" . $options['default'] . '] ')
885 1
            : static::input("$text ");
886
887 1
        if ($input === '') {
888 1
            if (isset($options['default'])) {
889 1
                $input = $options['default'];
890 1
            } elseif ($options['required']) {
891 1
                static::output($options['error']);
892 1
                goto top;
893
            }
894 1
        } elseif ($options['pattern'] && !preg_match($options['pattern'], $input)) {
895 1
            static::output($options['error']);
896 1
            goto top;
897 1
        } elseif ($options['validator'] &&
898 1
            !call_user_func_array($options['validator'], [$input, &$error])
899
        ) {
900 1
            static::output(isset($error) ? $error : $options['error']);
901 1
            goto top;
902
        }
903
904 1
        return $input;
905
    }
906
907
    /**
908
     * Asks user to confirm by typing y or n.
909
     *
910
     * A typical usage looks like the following:
911
     *
912
     * ```php
913
     * if (Console::confirm("Are you sure?")) {
914
     *     echo "user typed yes\n";
915
     * } else {
916
     *     echo "user typed no\n";
917
     * }
918
     * ```
919
     *
920
     * @param string $message to print out before waiting for user input
921
     * @param bool $default this value is returned if no selection is made.
922
     * @return bool whether user confirmed
923
     */
924 1
    public static function confirm($message, $default = false)
925
    {
926 1
        while (true) {
927 1
            static::stdout($message . ' (yes|no) [' . ($default ? 'yes' : 'no') . ']:');
928 1
            $input = trim(static::stdin());
929
930 1
            if (empty($input)) {
931 1
                return $default;
932
            }
933
934 1
            if (!strcasecmp($input, 'y') || !strcasecmp($input, 'yes')) {
935 1
                return true;
936
            }
937
938 1
            if (!strcasecmp($input, 'n') || !strcasecmp($input, 'no')) {
939 1
                return false;
940
            }
941
        }
942
    }
943
944
    /**
945
     * Gives the user an option to choose from. Giving '?' as an input will show
946
     * a list of options to choose from and their explanations.
947
     *
948
     * @param string $prompt the prompt message
949
     * @param array $options Key-value array of options to choose from. Key is what is inputed and used, value is
950
     * what's displayed to end user by help command.
951
     * @param string|null $default value to use when the user doesn't provide an option.
952
     * If the default is `null`, the user is required to select an option.
953
     *
954
     * @return string An option character the user chose
955
     * @since 2.0.49 Added the $default argument
956
     */
957 1
    public static function select($prompt, $options = [], $default = null)
958
    {
959
        top:
960 1
        static::stdout("$prompt (" . implode(',', array_keys($options)) . ',?)'
961 1
            . ($default !== null ? '[' . $default . ']' : '') . ': ');
962 1
        $input = static::stdin();
963 1
        if ($input === '?') {
964 1
            foreach ($options as $key => $value) {
965 1
                static::output(" $key - $value");
966
            }
967 1
            static::output(' ? - Show help');
968 1
            goto top;
969 1
        } elseif ($default !== null && $input === '') {
970 1
            return $default;
971 1
        } elseif (!array_key_exists($input, $options)) {
972 1
            goto top;
973
        }
974
975 1
        return $input;
976
    }
977
978
    private static $_progressStart;
979
    private static $_progressWidth;
980
    private static $_progressPrefix;
981
    private static $_progressEta;
982
    private static $_progressEtaLastDone = 0;
983
    private static $_progressEtaLastUpdate;
984
985
    /**
986
     * Starts display of a progress bar on screen.
987
     *
988
     * This bar will be updated by [[updateProgress()]] and may be ended by [[endProgress()]].
989
     *
990
     * The following example shows a simple usage of a progress bar:
991
     *
992
     * ```php
993
     * Console::startProgress(0, 1000);
994
     * for ($n = 1; $n <= 1000; $n++) {
995
     *     usleep(1000);
996
     *     Console::updateProgress($n, 1000);
997
     * }
998
     * Console::endProgress();
999
     * ```
1000
     *
1001
     * Git clone like progress (showing only status information):
1002
     *
1003
     * ```php
1004
     * Console::startProgress(0, 1000, 'Counting objects: ', false);
1005
     * for ($n = 1; $n <= 1000; $n++) {
1006
     *     usleep(1000);
1007
     *     Console::updateProgress($n, 1000);
1008
     * }
1009
     * Console::endProgress("done." . PHP_EOL);
1010
     * ```
1011
     *
1012
     * @param int $done the number of items that are completed.
1013
     * @param int $total the total value of items that are to be done.
1014
     * @param string $prefix an optional string to display before the progress bar.
1015
     * Default to empty string which results in no prefix to be displayed.
1016
     * @param int|float|bool|null $width optional width of the progressbar. This can be an integer representing
1017
     * the number of characters to display for the progress bar or a float between 0 and 1 representing the
1018
     * percentage of screen with the progress bar may take. It can also be set to false to disable the
1019
     * bar and only show progress information like percent, number of items and ETA.
1020
     * If not set, the bar will be as wide as the screen. Screen size will be detected using [[getScreenSize()]].
1021
     * @see startProgress
1022
     * @see updateProgress
1023
     * @see endProgress
1024
     */
1025
    public static function startProgress($done, $total, $prefix = '', $width = null)
1026
    {
1027
        self::$_progressStart = time();
1028
        self::$_progressWidth = $width;
1029
        self::$_progressPrefix = $prefix;
1030
        self::$_progressEta = null;
1031
        self::$_progressEtaLastDone = 0;
1032
        self::$_progressEtaLastUpdate = time();
1033
1034
        static::updateProgress($done, $total);
1035
    }
1036
1037
    /**
1038
     * Updates a progress bar that has been started by [[startProgress()]].
1039
     *
1040
     * @param int $done the number of items that are completed.
1041
     * @param int $total the total value of items that are to be done.
1042
     * @param string|null $prefix an optional string to display before the progress bar.
1043
     * Defaults to null meaning the prefix specified by [[startProgress()]] will be used.
1044
     * If prefix is specified it will update the prefix that will be used by later calls.
1045
     * @see startProgress
1046
     * @see endProgress
1047
     */
1048
    public static function updateProgress($done, $total, $prefix = null)
1049
    {
1050
        if ($prefix === null) {
1051
            $prefix = self::$_progressPrefix;
1052
        } else {
1053
            self::$_progressPrefix = $prefix;
1054
        }
1055
        $width = static::getProgressbarWidth($prefix);
1056
        $percent = ($total == 0) ? 1 : $done / $total;
1057
        $info = sprintf('%d%% (%d/%d)', $percent * 100, $done, $total);
1058
        self::setETA($done, $total);
1059
        $info .= self::$_progressEta === null ? ' ETA: n/a' : sprintf(' ETA: %d sec.', self::$_progressEta);
1060
1061
        // Number extra characters outputted. These are opening [, closing ], and space before info
1062
        // Since Windows uses \r\n\ for line endings, there's one more in the case
1063
        $extraChars = static::isRunningOnWindows() ? 4 : 3;
1064
        $width -= $extraChars + static::ansiStrlen($info);
1065
        // skipping progress bar on very small display or if forced to skip
1066
        if ($width < 5) {
1067
            static::stdout("\r$prefix$info   ");
1068
        } else {
1069
            if ($percent < 0) {
1070
                $percent = 0;
1071
            } elseif ($percent > 1) {
1072
                $percent = 1;
1073
            }
1074
            $bar = floor($percent * $width);
1075
            $status = str_repeat('=', $bar);
0 ignored issues
show
Bug introduced by
$bar of type double is incompatible with the type integer expected by parameter $times of str_repeat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1075
            $status = str_repeat('=', /** @scrutinizer ignore-type */ $bar);
Loading history...
1076
            if ($bar < $width) {
1077
                $status .= '>';
1078
                $status .= str_repeat(' ', $width - $bar - 1);
1079
            }
1080
            static::stdout("\r$prefix" . "[$status] $info");
1081
        }
1082
        flush();
1083
    }
1084
1085
    /**
1086
     * Return width of the progressbar
1087
     * @param string $prefix an optional string to display before the progress bar.
1088
     * @see updateProgress
1089
     * @return int screen width
1090
     * @since 2.0.14
1091
     */
1092
    private static function getProgressbarWidth($prefix)
1093
    {
1094
        $width = self::$_progressWidth;
1095
1096
        if ($width === false) {
1097
            return 0;
1098
        }
1099
1100
        $screenSize = static::getScreenSize(true);
1101
        if ($screenSize === false && $width < 1) {
0 ignored issues
show
introduced by
The condition $screenSize === false is always false.
Loading history...
1102
            return 0;
1103
        }
1104
1105
        if ($width === null) {
1106
            $width = $screenSize[0];
1107
        } elseif ($width > 0 && $width < 1) {
1108
            $width = floor($screenSize[0] * $width);
1109
        }
1110
1111
        $width -= static::ansiStrlen($prefix);
1112
1113
        return $width;
1114
    }
1115
1116
    /**
1117
     * Calculate $_progressEta, $_progressEtaLastUpdate and $_progressEtaLastDone
1118
     * @param int $done the number of items that are completed.
1119
     * @param int $total the total value of items that are to be done.
1120
     * @see updateProgress
1121
     * @since 2.0.14
1122
     */
1123
    private static function setETA($done, $total)
1124
    {
1125
        if ($done > $total || $done == 0) {
1126
            self::$_progressEta = null;
1127
            self::$_progressEtaLastUpdate = time();
1128
            return;
1129
        }
1130
1131
        if ($done < $total && (time() - self::$_progressEtaLastUpdate > 1 && $done > self::$_progressEtaLastDone)) {
1132
            $rate = (time() - (self::$_progressEtaLastUpdate ?: self::$_progressStart)) / ($done - self::$_progressEtaLastDone);
1133
            self::$_progressEta = $rate * ($total - $done);
1134
            self::$_progressEtaLastUpdate = time();
1135
            self::$_progressEtaLastDone = $done;
1136
        }
1137
    }
1138
1139
    /**
1140
     * Ends a progress bar that has been started by [[startProgress()]].
1141
     *
1142
     * @param string|bool $remove This can be `false` to leave the progress bar on screen and just print a newline.
1143
     * If set to `true`, the line of the progress bar will be cleared. This may also be a string to be displayed instead
1144
     * of the progress bar.
1145
     * @param bool $keepPrefix whether to keep the prefix that has been specified for the progressbar when progressbar
1146
     * gets removed. Defaults to true.
1147
     * @see startProgress
1148
     * @see updateProgress
1149
     */
1150
    public static function endProgress($remove = false, $keepPrefix = true)
1151
    {
1152
        if ($remove === false) {
1153
            static::stdout(PHP_EOL);
1154
        } else {
1155
            if (static::streamSupportsAnsiColors(STDOUT)) {
1156
                static::clearLine();
1157
            }
1158
            static::stdout("\r" . ($keepPrefix ? self::$_progressPrefix : '') . (is_string($remove) ? $remove : ''));
1159
        }
1160
        flush();
1161
1162
        self::$_progressStart = null;
1163
        self::$_progressWidth = null;
1164
        self::$_progressPrefix = '';
1165
        self::$_progressEta = null;
1166
        self::$_progressEtaLastDone = 0;
1167
        self::$_progressEtaLastUpdate = null;
1168
    }
1169
1170
    /**
1171
     * Generates a summary of the validation errors.
1172
     * @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
1173
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
1174
     *
1175
     * - showAllErrors: boolean, if set to true every error message for each attribute will be shown otherwise
1176
     *   only the first error message for each attribute will be shown. Defaults to `false`.
1177
     *
1178
     * @return string the generated error summary
1179
     * @since 2.0.14
1180
     */
1181 1
    public static function errorSummary($models, $options = [])
1182
    {
1183 1
        $showAllErrors = ArrayHelper::remove($options, 'showAllErrors', false);
1184 1
        $lines = self::collectErrors($models, $showAllErrors);
1185
1186 1
        return implode(PHP_EOL, $lines);
1187
    }
1188
1189
    /**
1190
     * Return array of the validation errors
1191
     * @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
1192
     * @param $showAllErrors boolean, if set to true every error message for each attribute will be shown otherwise
1193
     * only the first error message for each attribute will be shown.
1194
     * @return array of the validation errors
1195
     * @since 2.0.14
1196
     */
1197 1
    private static function collectErrors($models, $showAllErrors)
1198
    {
1199 1
        $lines = [];
1200 1
        if (!is_array($models)) {
1201 1
            $models = [$models];
1202
        }
1203
1204 1
        foreach ($models as $model) {
1205 1
            $lines = array_unique(array_merge($lines, $model->getErrorSummary($showAllErrors)));
1206
        }
1207
1208 1
        return $lines;
1209
    }
1210
}
1211