• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aplus-framework / cli / 5676407842

pending completion
5676407842

push

github

natanfelles
Add CLI::secret method

6 of 6 new or added lines in 1 file covered. (100.0%)

421 of 469 relevant lines covered (89.77%)

5.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.19
/src/CLI.php
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of Aplus Framework CLI Library.
4
 *
5
 * (c) Natan Felles <natanfelles@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Framework\CLI;
11

12
use InvalidArgumentException;
13
use JetBrains\PhpStorm\Pure;
14

15
/**
16
 * Class CLI.
17
 *
18
 * @see https://en.wikipedia.org/wiki/ANSI_escape_code
19
 *
20
 * @package cli
21
 */
22
class CLI
23
{
24
    /**
25
     * Background color "black".
26
     *
27
     * @var string
28
     */
29
    public const BG_BLACK = 'black';
30
    /**
31
     * Background color "red".
32
     *
33
     * @var string
34
     */
35
    public const BG_RED = 'red';
36
    /**
37
     * Background color "green".
38
     *
39
     * @var string
40
     */
41
    public const BG_GREEN = 'green';
42
    /**
43
     * Background color "yellow".
44
     *
45
     * @var string
46
     */
47
    public const BG_YELLOW = 'yellow';
48
    /**
49
     * Background color "blue".
50
     *
51
     * @var string
52
     */
53
    public const BG_BLUE = 'blue';
54
    /**
55
     * Background color "magenta".
56
     *
57
     * @var string
58
     */
59
    public const BG_MAGENTA = 'magenta';
60
    /**
61
     * Background color "cyan".
62
     *
63
     * @var string
64
     */
65
    public const BG_CYAN = 'cyan';
66
    /**
67
     * Background color "white".
68
     *
69
     * @var string
70
     */
71
    public const BG_WHITE = 'white';
72
    /**
73
     * Background color "bright black".
74
     *
75
     * @var string
76
     */
77
    public const BG_BRIGHT_BLACK = 'bright_black';
78
    /**
79
     * Background color "bright red".
80
     *
81
     * @var string
82
     */
83
    public const BG_BRIGHT_RED = 'bright_red';
84
    /**
85
     * Background color "bright green".
86
     *
87
     * @var string
88
     */
89
    public const BG_BRIGHT_GREEN = 'bright_green';
90
    /**
91
     * Background color "bright yellow".
92
     *
93
     * @var string
94
     */
95
    public const BG_BRIGHT_YELLOW = 'bright_yellow';
96
    /**
97
     * Background color "bright blue".
98
     *
99
     * @var string
100
     */
101
    public const BG_BRIGHT_BLUE = 'bright_blue';
102
    /**
103
     * Background color "bright magenta".
104
     *
105
     * @var string
106
     */
107
    public const BG_BRIGHT_MAGENTA = 'bright_magenta';
108
    /**
109
     * Background color "bright cyan".
110
     *
111
     * @var string
112
     */
113
    public const BG_BRIGHT_CYAN = 'bright_cyan';
114
    /**
115
     * Foreground color "black".
116
     *
117
     * @var string
118
     */
119
    public const FG_BLACK = 'black';
120
    /**
121
     * Foreground color "red".
122
     *
123
     * @var string
124
     */
125
    public const FG_RED = 'red';
126
    /**
127
     * Foreground color "green".
128
     *
129
     * @var string
130
     */
131
    public const FG_GREEN = 'green';
132
    /**
133
     * Foreground color "yellow".
134
     *
135
     * @var string
136
     */
137
    public const FG_YELLOW = 'yellow';
138
    /**
139
     * Foreground color "blue".
140
     *
141
     * @var string
142
     */
143
    public const FG_BLUE = 'blue';
144
    /**
145
     * Foreground color "magenta".
146
     *
147
     * @var string
148
     */
149
    public const FG_MAGENTA = 'magenta';
150
    /**
151
     * Foreground color "cyan".
152
     *
153
     * @var string
154
     */
155
    public const FG_CYAN = 'cyan';
156
    /**
157
     * Foreground color "white".
158
     *
159
     * @var string
160
     */
161
    public const FG_WHITE = 'white';
162
    /**
163
     * Foreground color "bright black".
164
     *
165
     * @var string
166
     */
167
    public const FG_BRIGHT_BLACK = 'bright_black';
168
    /**
169
     * Foreground color "bright red".
170
     *
171
     * @var string
172
     */
173
    public const FG_BRIGHT_RED = 'bright_red';
174
    /**
175
     * Foreground color "bright green".
176
     *
177
     * @var string
178
     */
179
    public const FG_BRIGHT_GREEN = 'bright_green';
180
    /**
181
     * Foreground color "bright yellow".
182
     *
183
     * @var string
184
     */
185
    public const FG_BRIGHT_YELLOW = 'bright_yellow';
186
    /**
187
     * Foreground color "bright blue".
188
     *
189
     * @var string
190
     */
191
    public const FG_BRIGHT_BLUE = 'bright_blue';
192
    /**
193
     * Foreground color "bright magenta".
194
     *
195
     * @var string
196
     */
197
    public const FG_BRIGHT_MAGENTA = 'bright_magenta';
198
    /**
199
     * Foreground color "bright cyan".
200
     *
201
     * @var string
202
     */
203
    public const FG_BRIGHT_CYAN = 'bright_cyan';
204
    /**
205
     * Foreground color "bright white".
206
     *
207
     * @var string
208
     */
209
    public const FG_BRIGHT_WHITE = 'bright_white';
210
    /**
211
     * SGR format "bold".
212
     *
213
     * @var string
214
     */
215
    public const FM_BOLD = 'bold';
216
    /**
217
     * SGR format "faint".
218
     *
219
     * @var string
220
     */
221
    public const FM_FAINT = 'faint';
222
    /**
223
     * SGR format "italic".
224
     *
225
     * @var string
226
     */
227
    public const FM_ITALIC = 'italic';
228
    /**
229
     * SGR format "underline".
230
     *
231
     * @var string
232
     */
233
    public const FM_UNDERLINE = 'underline';
234
    /**
235
     * SGR format "slow blink".
236
     *
237
     * @var string
238
     */
239
    public const FM_SLOW_BLINK = 'slow_blink';
240
    /**
241
     * SGR format "rapid blink".
242
     *
243
     * @var string
244
     */
245
    public const FM_RAPID_BLINK = 'rapid_blink';
246
    /**
247
     * SGR format "reverse video".
248
     *
249
     * @var string
250
     */
251
    public const FM_REVERSE_VIDEO = 'reverse_video';
252
    /**
253
     * SGR format "conceal".
254
     *
255
     * @var string
256
     */
257
    public const FM_CONCEAL = 'conceal';
258
    /**
259
     * SGR format "crossed out".
260
     *
261
     * @var string
262
     */
263
    public const FM_CROSSED_OUT = 'crossed_out';
264
    /**
265
     * SGR format "primary font".
266
     *
267
     * @var string
268
     */
269
    public const FM_PRIMARY_FONT = 'primary_font';
270
    /**
271
     * SGR format "fraktur".
272
     *
273
     * @var string
274
     */
275
    public const FM_FRAKTUR = 'fraktur';
276
    /**
277
     * SGR format "doubly underline".
278
     *
279
     * @var string
280
     */
281
    public const FM_DOUBLY_UNDERLINE = 'doubly_underline';
282
    /**
283
     * SGR format "encircled".
284
     *
285
     * @var string
286
     */
287
    public const FM_ENCIRCLED = 'encircled';
288
    /**
289
     * @var array<string,string>
290
     */
291
    protected static array $backgroundColors = [
292
        'black' => "\033[40m",
293
        'red' => "\033[41m",
294
        'green' => "\033[42m",
295
        'yellow' => "\033[43m",
296
        'blue' => "\033[44m",
297
        'magenta' => "\033[45m",
298
        'cyan' => "\033[46m",
299
        'white' => "\033[47m",
300
        'bright_black' => "\033[100m",
301
        'bright_red' => "\033[101m",
302
        'bright_green' => "\033[102m",
303
        'bright_yellow' => "\033[103m",
304
        'bright_blue' => "\033[104m",
305
        'bright_magenta' => "\033[105m",
306
        'bright_cyan' => "\033[106m",
307
        'bright_white' => "\033[107m",
308
    ];
309
    /**
310
     * @var array<string,string>
311
     */
312
    protected static array $foregroundColors = [
313
        'black' => "\033[0;30m",
314
        'red' => "\033[0;31m",
315
        'green' => "\033[0;32m",
316
        'yellow' => "\033[0;33m",
317
        'blue' => "\033[0;34m",
318
        'magenta' => "\033[0;35m",
319
        'cyan' => "\033[0;36m",
320
        'white' => "\033[0;37m",
321
        'bright_black' => "\033[0;90m",
322
        'bright_red' => "\033[0;91m",
323
        'bright_green' => "\033[0;92m",
324
        'bright_yellow' => "\033[0;93m",
325
        'bright_blue' => "\033[0;94m",
326
        'bright_magenta' => "\033[0;95m",
327
        'bright_cyan' => "\033[0;96m",
328
        'bright_white' => "\033[0;97m",
329
    ];
330
    /**
331
     * @var array<string,string>
332
     */
333
    protected static array $formats = [
334
        'bold' => "\033[1m",
335
        'faint' => "\033[2m",
336
        'italic' => "\033[3m",
337
        'underline' => "\033[4m",
338
        'slow_blink' => "\033[5m",
339
        'rapid_blink' => "\033[6m",
340
        'reverse_video' => "\033[7m",
341
        'conceal' => "\033[8m",
342
        'crossed_out' => "\033[9m",
343
        'primary_font' => "\033[10m",
344
        'fraktur' => "\033[20m",
345
        'doubly_underline' => "\033[21m",
346
        'encircled' => "\033[52m",
347
    ];
348
    protected static string $reset = "\033[0m";
349

350
    /**
351
     * Tells if it is running on a Windows OS.
352
     *
353
     * @return bool
354
     */
355
    #[Pure]
356
    public static function isWindows() : bool
357
    {
358
        return \PHP_OS_FAMILY === 'Windows';
5✔
359
    }
360

361
    /**
362
     * Get the screen width.
363
     *
364
     * @param int $default
365
     *
366
     * @return int
367
     */
368
    public static function getWidth(int $default = 80) : int
369
    {
370
        if (static::isWindows()) {
3✔
371
            return $default;
1✔
372
        }
373
        $width = (int) \shell_exec('tput cols');
3✔
374
        if ( ! $width) {
3✔
375
            return $default;
3✔
376
        }
377
        return $width;
×
378
    }
379

380
    /**
381
     * Displays text wrapped to a certain width.
382
     *
383
     * @param string $text
384
     * @param int|null $width
385
     *
386
     * @return string Returns the wrapped text
387
     */
388
    public static function wrap(string $text, int $width = null) : string
389
    {
390
        $width ??= static::getWidth();
3✔
391
        return \wordwrap($text, $width, \PHP_EOL, true);
3✔
392
    }
393

394
    /**
395
     * Calculate the multibyte length of a text without style characters.
396
     *
397
     * @param string $text The text being checked for length
398
     *
399
     * @return int
400
     */
401
    public static function strlen(string $text) : int
402
    {
403
        $codes = [];
2✔
404
        foreach (static::$foregroundColors as $color) {
2✔
405
            $codes[] = $color;
2✔
406
        }
407
        foreach (static::$backgroundColors as $background) {
2✔
408
            $codes[] = $background;
2✔
409
        }
410
        foreach (static::$formats as $format) {
2✔
411
            $codes[] = $format;
2✔
412
        }
413
        $codes[] = static::$reset;
2✔
414
        $text = \str_replace($codes, '', $text);
2✔
415
        return \mb_strlen($text);
2✔
416
    }
417

418
    /**
419
     * Applies styles to a text.
420
     *
421
     * @param string $text The text to be styled
422
     * @param string|null $color Foreground color. One of the FG_* constants
423
     * @param string|null $background Background color. One of the BG_* constants
424
     * @param array<int,string> $formats The text format. A list of FM_* constants
425
     *
426
     * @throws InvalidArgumentException For invalid color, background or format
427
     *
428
     * @return string Returns the styled text
429
     */
430
    public static function style(
431
        string $text,
432
        string $color = null,
433
        string $background = null,
434
        array $formats = []
435
    ) : string {
436
        $string = '';
15✔
437
        if ($color !== null) {
15✔
438
            if (empty(static::$foregroundColors[$color])) {
13✔
439
                throw new InvalidArgumentException('Invalid color: ' . $color);
1✔
440
            }
441
            $string = static::$foregroundColors[$color];
12✔
442
        }
443
        if ($background !== null) {
14✔
444
            if (empty(static::$backgroundColors[$background])) {
4✔
445
                throw new InvalidArgumentException('Invalid background color: ' . $background);
1✔
446
            }
447
            $string .= static::$backgroundColors[$background];
3✔
448
        }
449
        if ($formats) {
13✔
450
            foreach ($formats as $format) {
3✔
451
                if (empty(static::$formats[$format])) {
3✔
452
                    throw new InvalidArgumentException('Invalid format: ' . $format);
1✔
453
                }
454
                $string .= static::$formats[$format];
3✔
455
            }
456
        }
457
        $string .= $text . static::$reset;
12✔
458
        return $string;
12✔
459
    }
460

461
    /**
462
     * Write a text in the output.
463
     *
464
     * Optionally with styles and width wrapping.
465
     *
466
     * @param string $text The text to be written
467
     * @param string|null $color Foreground color. One of the FG_* constants
468
     * @param string|null $background Background color. One of the BG_* constants
469
     * @param int|null $width Width to wrap the text. Null to do not wrap.
470
     */
471
    public static function write(
472
        string $text,
473
        string $color = null,
474
        string $background = null,
475
        int $width = null
476
    ) : void {
477
        if ($width !== null) {
10✔
478
            $text = static::wrap($text, $width);
1✔
479
        }
480
        if ($color !== null || $background !== null) {
10✔
481
            $text = static::style($text, $color, $background);
7✔
482
        }
483
        \fwrite(\STDOUT, $text . \PHP_EOL);
10✔
484
    }
485

486
    /**
487
     * Prints a new line in the output.
488
     *
489
     * @param int $lines Number of lines to be printed
490
     */
491
    public static function newLine(int $lines = 1) : void
492
    {
493
        for ($i = 0; $i < $lines; $i++) {
3✔
494
            \fwrite(\STDOUT, \PHP_EOL);
3✔
495
        }
496
    }
497

498
    /**
499
     * Creates a "live line".
500
     *
501
     * Erase the current line, move the cursor to the beginning of the line and
502
     * writes a text.
503
     *
504
     * @param string $text The text to be written
505
     * @param bool $finalize If true the "live line" activity ends, creating a
506
     * new line after the text
507
     */
508
    public static function liveLine(string $text, bool $finalize = false) : void
509
    {
510
        // See: https://stackoverflow.com/a/35190285
511
        $string = '';
1✔
512
        if ( ! static::isWindows()) {
1✔
513
            $string .= "\33[2K";
1✔
514
        }
515
        $string .= "\r";
1✔
516
        $string .= $text;
1✔
517
        if ($finalize) {
1✔
518
            $string .= \PHP_EOL;
1✔
519
        }
520
        \fwrite(\STDOUT, $string);
1✔
521
    }
522

523
    /**
524
     * Performs audible beep alarms.
525
     *
526
     * @param int $times How many times should the beep be played
527
     * @param int $usleep Interval in microseconds
528
     */
529
    public static function beep(int $times = 1, int $usleep = 0) : void
530
    {
531
        for ($i = 0; $i < $times; $i++) {
3✔
532
            \fwrite(\STDOUT, "\x07");
3✔
533
            \usleep($usleep);
3✔
534
        }
535
    }
536

537
    /**
538
     * Writes a message box.
539
     *
540
     * @param array<int,string>|string $lines One line as string or multi-lines as array
541
     * @param string $background Background color. One of the BG_* constants
542
     * @param string $color Foreground color. One of the FG_* constants
543
     */
544
    public static function box(
545
        array | string $lines,
546
        string $background = CLI::BG_BLACK,
547
        string $color = CLI::FG_WHITE
548
    ) : void {
549
        $width = static::getWidth();
1✔
550
        $width -= 2;
1✔
551
        if ( ! \is_array($lines)) {
1✔
552
            $lines = [
1✔
553
                $lines,
1✔
554
            ];
1✔
555
        }
556
        $allLines = [];
1✔
557
        foreach ($lines as &$line) {
1✔
558
            $length = static::strlen($line);
1✔
559
            if ($length > $width) {
1✔
560
                $line = static::wrap($line, $width);
1✔
561
            }
562
            foreach (\explode(\PHP_EOL, $line) as $subLine) {
1✔
563
                $allLines[] = $subLine;
1✔
564
            }
565
        }
566
        unset($line);
1✔
567
        $blankLine = \str_repeat(' ', $width + 2);
1✔
568
        $text = static::style($blankLine, $color, $background);
1✔
569
        foreach ($allLines as $line) {
1✔
570
            $end = \str_repeat(' ', $width - static::strlen($line)) . ' ';
1✔
571
            $end = static::style($end, $color, $background);
1✔
572
            $text .= static::style(' ' . $line . $end, $color, $background);
1✔
573
        }
574
        $text .= static::style($blankLine, $color, $background);
1✔
575
        static::write($text);
1✔
576
    }
577

578
    /**
579
     * Writes a message to STDERR and optionally exit with a custom code.
580
     *
581
     * @param string $message The error message
582
     * @param int|null $exitCode Set null to do not exit
583
     */
584
    public static function error(string $message, ?int $exitCode = 1) : void
585
    {
586
        static::beep();
2✔
587
        \fwrite(\STDERR, static::style($message, static::FG_RED) . \PHP_EOL);
2✔
588
        if ($exitCode !== null) {
2✔
589
            exit($exitCode);
×
590
        }
591
    }
592

593
    /**
594
     * Clear the terminal screen.
595
     */
596
    public static function clear() : void
597
    {
598
        \fwrite(\STDOUT, "\e[H\e[2J");
1✔
599
    }
600

601
    /**
602
     * Get user input.
603
     *
604
     * NOTE: It is possible pass multiple lines ending each line with a backslash.
605
     *
606
     * @param string $prepend Text prepended in the input. Used internally to
607
     * allow multiple lines
608
     *
609
     * @return string Returns the user input
610
     */
611
    public static function getInput(string $prepend = '') : string
612
    {
613
        $input = \fgets(\STDIN);
×
614
        $input = $input === false ? '' : \trim($input);
×
615
        $prepend .= $input;
×
616
        $eolPos = false;
×
617
        if ($prepend) {
×
618
            $eolPos = \strrpos($prepend, '\\', -1);
×
619
        }
620
        if ($eolPos !== false) {
×
621
            $prepend = \substr_replace($prepend, \PHP_EOL, $eolPos);
×
622
            $prepend = static::getInput($prepend);
×
623
        }
624
        return $prepend;
×
625
    }
626

627
    /**
628
     * Prompt a question.
629
     *
630
     * @param string $question The question to prompt
631
     * @param array<int,string>|string|null $options Answer options. If an array
632
     * is set, the default answer is the first value. If is a string, it will
633
     * be the default.
634
     *
635
     * @return string The answer
636
     */
637
    public static function prompt(string $question, array | string $options = null) : string
638
    {
639
        if ($options !== null) {
×
640
            $options = \is_array($options)
×
641
                ? \array_values($options)
×
642
                : [$options];
×
643
        }
644
        if ($options) {
×
645
            $opt = $options;
×
646
            $opt[0] = static::style($opt[0], null, null, [static::FM_BOLD]);
×
647
            $optionsText = isset($opt[1])
×
648
                ? \implode(', ', $opt)
×
649
                : $opt[0];
×
650
            $question .= ' [' . $optionsText . ']';
×
651
        }
652
        $question .= ': ';
×
653
        \fwrite(\STDOUT, $question);
×
654
        $answer = static::getInput();
×
655
        if ($answer === '' && isset($options[0])) {
×
656
            $answer = $options[0];
×
657
        }
658
        return $answer;
×
659
    }
660

661
    /**
662
     * Prompt a question with secret answer.
663
     *
664
     * @param string $question The question to prompt
665
     *
666
     * @see https://dev.to/mykeels/reading-passwords-from-stdin-in-php-1np9
667
     *
668
     * @return string The secret answer
669
     */
670
    public static function secret(string $question) : string
671
    {
672
        $question .= ': ';
×
673
        \fwrite(\STDOUT, $question);
×
674
        \exec('stty -echo');
×
675
        $secret = \trim((string) \fgets(\STDIN));
×
676
        \exec('stty echo');
×
677
        return $secret;
×
678
    }
679

680
    /**
681
     * Creates a well formatted table.
682
     *
683
     * @param array<array<scalar|\Stringable>> $tbody Table body rows
684
     * @param array<scalar|\Stringable> $thead Table head fields
685
     */
686
    public static function table(array $tbody, array $thead = []) : void
687
    {
688
        // All the rows in the table will be here until the end
689
        $tableRows = [];
1✔
690
        // We need only indexes and not keys
691
        if ( ! empty($thead)) {
1✔
692
            $tableRows[] = \array_values($thead);
1✔
693
        }
694
        foreach ($tbody as $tr) {
1✔
695
            // cast tr to array if is not - (objects...)
696
            $tableRows[] = \array_values((array) $tr);
1✔
697
        }
698
        // Yes, it really is necessary to know this count
699
        $totalRows = \count($tableRows);
1✔
700
        // Store all columns lengths
701
        // $allColsLengths[row][column] = length
702
        $allColsLengths = [];
1✔
703
        // Store maximum lengths by column
704
        // $maxColsLengths[column] = length
705
        $maxColsLengths = [];
1✔
706
        // Read row by row and define the longest columns
707
        for ($row = 0; $row < $totalRows; $row++) {
1✔
708
            $column = 0; // Current column index
1✔
709
            foreach ($tableRows[$row] as $col) {
1✔
710
                // Sets the size of this column in the current row
711
                $allColsLengths[$row][$column] = static::strlen((string) $col);
1✔
712
                // If the current column does not have a value among the larger ones
713
                // or the value of this is greater than the existing one
714
                // then, now, this assumes the maximum length
715
                if ( ! isset($maxColsLengths[$column])
1✔
716
                    || $allColsLengths[$row][$column] > $maxColsLengths[$column]) {
1✔
717
                    $maxColsLengths[$column] = $allColsLengths[$row][$column];
1✔
718
                }
719
                // We can go check the size of the next column...
720
                $column++;
1✔
721
            }
722
        }
723
        // Read row by row and add spaces at the end of the columns
724
        // to match the exact column length
725
        for ($row = 0; $row < $totalRows; $row++) {
1✔
726
            $column = 0;
1✔
727
            foreach ($tableRows[$row] as $col => $value) {
1✔
728
                $diff = $maxColsLengths[$column] - $allColsLengths[$row][$col];
1✔
729
                if ($diff) {
1✔
730
                    $tableRows[$row][$column] .= \str_repeat(' ', $diff);
1✔
731
                }
732
                $column++;
1✔
733
            }
734
        }
735
        $table = $line = '';
1✔
736
        // Joins columns and append the well formatted rows to the table
737
        foreach ($tableRows as $row => $value) {
1✔
738
            // Set the table border-top
739
            if ($row === 0) {
1✔
740
                $line = '+';
1✔
741
                foreach (\array_keys($value) as $col) {
1✔
742
                    $line .= \str_repeat('-', $maxColsLengths[$col] + 2) . '+';
1✔
743
                }
744
                $table .= $line . \PHP_EOL;
1✔
745
            }
746
            // Set the vertical borders
747
            $table .= '| ' . \implode(' | ', $value) . ' |' . \PHP_EOL;
1✔
748
            // Set the thead and table borders-bottom
749
            if (($row === 0 && ! empty($thead)) || $row + 1 === $totalRows) {
1✔
750
                $table .= $line . \PHP_EOL;
1✔
751
            }
752
        }
753
        \fwrite(\STDOUT, $table);
1✔
754
    }
755
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc