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

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