Completed
Push — batch-query-test ( c73b11...6d2f08 )
by Carsten
12:18
created

BaseConsole   D

Complexity

Total Complexity 129

Size/Duplication

Total Lines 1003
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 65.56%

Importance

Changes 2
Bugs 2 Features 0
Metric Value
wmc 129
c 2
b 2
f 0
lcom 1
cbo 2
dl 0
loc 1003
ccs 240
cts 366
cp 0.6556
rs 4.4433

47 Methods

Rating   Name   Duplication   Size   Complexity  
A moveCursorUp() 0 4 1
A moveCursorDown() 0 4 1
A moveCursorForward() 0 4 1
A moveCursorBackward() 0 4 1
A moveCursorNextLine() 0 4 1
A moveCursorPrevLine() 0 4 1
A moveCursorTo() 0 8 2
A scrollUp() 0 4 1
A scrollDown() 0 4 1
A saveCursorPosition() 0 4 1
A restoreCursorPosition() 0 4 1
A hideCursor() 0 4 1
A showCursor() 0 4 1
A clearScreen() 0 4 1
A clearScreenBeforeCursor() 0 4 1
A clearScreenAfterCursor() 0 4 1
A clearLine() 0 4 1
A clearLineBeforeCursor() 0 4 1
A clearLineAfterCursor() 0 4 1
A ansiFormatCode() 0 4 1
A beginAnsiFormat() 0 4 1
A endAnsiFormat() 0 4 1
A ansiFormat() 0 6 2
A xtermFgColor() 0 4 1
A xtermBgColor() 0 4 1
A stripAnsiFormat() 0 4 1
D ansiToHtml() 0 99 17
A markdownToAnsi() 0 5 1
A renderColoredString() 0 55 3
A escape() 0 5 1
B wrapText() 0 18 5
A stdin() 0 4 2
A stderr() 0 4 1
A input() 0 8 2
A output() 0 4 1
A error() 0 4 1
F prompt() 0 38 10
B confirm() 0 19 8
A select() 0 17 4
A streamSupportsAnsiColors() 0 6 4
A stdout() 0 4 1
A ansiStrlen() 0 4 1
A isRunningOnWindows() 0 4 1
C getScreenSize() 0 33 12
A startProgress() 0 11 1
B endProgress() 0 19 5
F updateProgress() 0 63 20

How to fix   Complexity   

Complex Class

Complex classes like BaseConsole often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseConsole, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\helpers;
9
10
use yii\console\Markdown;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, yii\helpers\Markdown.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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