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

aplus-framework / cli / 9023639403

09 May 2024 09:00PM UTC coverage: 91.573% (+1.7%) from 89.831%
9023639403

push

github

natanfelles
Test help command error

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

40 existing lines in 2 files now uncovered.

489 of 534 relevant lines covered (91.57%)

5.66 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 Framework\CLI\Styles\BackgroundColor;
13
use Framework\CLI\Styles\ForegroundColor;
14
use Framework\CLI\Styles\Format;
15
use JetBrains\PhpStorm\Pure;
16
use ValueError;
17

18
/**
19
 * Class CLI.
20
 *
21
 * @see https://en.wikipedia.org/wiki/ANSI_escape_code
22
 *
23
 * @package cli
24
 */
25
class CLI
26
{
27
    protected static string $reset = "\033[0m";
28

29
    /**
30
     * Tells if it is running on a Windows OS.
31
     *
32
     * @return bool
33
     */
34
    #[Pure]
35
    public static function isWindows() : bool
36
    {
37
        return \PHP_OS_FAMILY === 'Windows';
5✔
38
    }
39

40
    /**
41
     * Get the screen width.
42
     *
43
     * @param int $default
44
     *
45
     * @return int
46
     */
47
    public static function getWidth(int $default = 80) : int
48
    {
49
        if (static::isWindows()) {
3✔
50
            return $default;
1✔
51
        }
52
        $width = (int) \shell_exec('tput cols');
3✔
53
        if (!$width) {
3✔
54
            return $default;
3✔
55
        }
UNCOV
56
        return $width;
×
57
    }
58

59
    /**
60
     * Displays text wrapped to a certain width.
61
     *
62
     * @param string $text
63
     * @param int|null $width
64
     *
65
     * @return string Returns the wrapped text
66
     */
67
    public static function wrap(string $text, int $width = null) : string
68
    {
69
        $width ??= static::getWidth();
3✔
70
        return \wordwrap($text, $width, \PHP_EOL, true);
3✔
71
    }
72

73
    /**
74
     * Calculate the multibyte length of a text without style characters.
75
     *
76
     * @param string $text The text being checked for length
77
     *
78
     * @return int
79
     */
80
    public static function strlen(string $text) : int
81
    {
82
        $codes = [];
2✔
83
        foreach (ForegroundColor::cases() as $case) {
2✔
84
            $codes[] = $case->getCode();
2✔
85
        }
86
        foreach (BackgroundColor::cases() as $case) {
2✔
87
            $codes[] = $case->getCode();
2✔
88
        }
89
        foreach (Format::cases() as $case) {
2✔
90
            $codes[] = $case->getCode();
2✔
91
        }
92
        $codes[] = static::$reset;
2✔
93
        $text = \str_replace($codes, '', $text);
2✔
94
        return \mb_strlen($text);
2✔
95
    }
96

97
    /**
98
     * Applies styles to a text.
99
     *
100
     * @param string $text The text to be styled
101
     * @param ForegroundColor|string|null $color Foreground color
102
     * @param BackgroundColor|string|null $background Background color
103
     * @param array<Format|string> $formats The text formats
104
     *
105
     * @throws ValueError For invalid color, background or format
106
     *
107
     * @return string Returns the styled text
108
     */
109
    public static function style(
110
        string $text,
111
        ForegroundColor | string $color = null,
112
        BackgroundColor | string $background = null,
113
        array $formats = []
114
    ) : string {
115
        $string = '';
15✔
116
        if ($color !== null) {
15✔
117
            $string = \is_string($color)
13✔
118
                ? ForegroundColor::from($color)->getCode()
2✔
119
                : $color->getCode();
12✔
120
        }
121
        if ($background !== null) {
14✔
122
            $string .= \is_string($background)
3✔
123
                ? BackgroundColor::from($background)->getCode()
2✔
124
                : $background->getCode();
2✔
125
        }
126
        if ($formats) {
13✔
127
            foreach ($formats as $format) {
2✔
128
                $string .= \is_string($format)
2✔
129
                    ? Format::from($format)->getCode()
2✔
130
                    : $format->getCode();
2✔
131
            }
132
        }
133
        $string .= $text . static::$reset;
12✔
134
        return $string;
12✔
135
    }
136

137
    /**
138
     * Write a text in the output.
139
     *
140
     * Optionally with styles and width wrapping.
141
     *
142
     * @param string $text The text to be written
143
     * @param ForegroundColor|string|null $color Foreground color
144
     * @param BackgroundColor|string|null $background Background color
145
     * @param int|null $width Width to wrap the text. Null to do not wrap.
146
     */
147
    public static function write(
148
        string $text,
149
        ForegroundColor | string $color = null,
150
        BackgroundColor | string $background = null,
151
        int $width = null
152
    ) : void {
153
        if ($width !== null) {
10✔
154
            $text = static::wrap($text, $width);
1✔
155
        }
156
        if ($color !== null || $background !== null) {
10✔
157
            $text = static::style($text, $color, $background);
7✔
158
        }
159
        \fwrite(\STDOUT, $text . \PHP_EOL);
10✔
160
    }
161

162
    /**
163
     * Prints a new line in the output.
164
     *
165
     * @param int $lines Number of lines to be printed
166
     */
167
    public static function newLine(int $lines = 1) : void
168
    {
169
        for ($i = 0; $i < $lines; $i++) {
3✔
170
            \fwrite(\STDOUT, \PHP_EOL);
3✔
171
        }
172
    }
173

174
    /**
175
     * Creates a "live line".
176
     *
177
     * Erase the current line, move the cursor to the beginning of the line and
178
     * writes a text.
179
     *
180
     * @param string $text The text to be written
181
     * @param bool $finalize If true the "live line" activity ends, creating a
182
     * new line after the text
183
     */
184
    public static function liveLine(string $text, bool $finalize = false) : void
185
    {
186
        // See: https://stackoverflow.com/a/35190285
187
        $string = '';
1✔
188
        if (!static::isWindows()) {
1✔
189
            $string .= "\33[2K";
1✔
190
        }
191
        $string .= "\r";
1✔
192
        $string .= $text;
1✔
193
        if ($finalize) {
1✔
194
            $string .= \PHP_EOL;
1✔
195
        }
196
        \fwrite(\STDOUT, $string);
1✔
197
    }
198

199
    /**
200
     * Performs audible beep alarms.
201
     *
202
     * @param int $times How many times should the beep be played
203
     * @param int $usleep Interval in microseconds
204
     */
205
    public static function beep(int $times = 1, int $usleep = 0) : void
206
    {
207
        for ($i = 0; $i < $times; $i++) {
4✔
208
            \fwrite(\STDOUT, "\x07");
4✔
209
            \usleep($usleep);
4✔
210
        }
211
    }
212

213
    /**
214
     * Writes a message box.
215
     *
216
     * @param array<int,string>|string $lines One line as string or multi-lines as array
217
     * @param BackgroundColor|string $background Background color
218
     * @param ForegroundColor|string $color Foreground color
219
     */
220
    public static function box(
221
        array | string $lines,
222
        BackgroundColor | string $background = BackgroundColor::black,
223
        ForegroundColor | string $color = ForegroundColor::white
224
    ) : void {
225
        $width = static::getWidth();
1✔
226
        $width -= 2;
1✔
227
        if (!\is_array($lines)) {
1✔
228
            $lines = [
1✔
229
                $lines,
1✔
230
            ];
1✔
231
        }
232
        $allLines = [];
1✔
233
        foreach ($lines as &$line) {
1✔
234
            $length = static::strlen($line);
1✔
235
            if ($length > $width) {
1✔
236
                $line = static::wrap($line, $width);
1✔
237
            }
238
            foreach (\explode(\PHP_EOL, $line) as $subLine) {
1✔
239
                $allLines[] = $subLine;
1✔
240
            }
241
        }
242
        unset($line);
1✔
243
        $blankLine = \str_repeat(' ', $width + 2);
1✔
244
        $text = static::style($blankLine, $color, $background);
1✔
245
        foreach ($allLines as $line) {
1✔
246
            $end = \str_repeat(' ', $width - static::strlen($line)) . ' ';
1✔
247
            $end = static::style($end, $color, $background);
1✔
248
            $text .= static::style(' ' . $line . $end, $color, $background);
1✔
249
        }
250
        $text .= static::style($blankLine, $color, $background);
1✔
251
        static::write($text);
1✔
252
    }
253

254
    /**
255
     * Writes a message to STDERR and optionally exit with a custom code.
256
     *
257
     * @param string $message The error message
258
     * @param int|null $exitCode Set null to do not exit
259
     */
260
    public static function error(string $message, ?int $exitCode = 1) : void
261
    {
262
        static::beep();
3✔
263
        \fwrite(\STDERR, static::style($message, ForegroundColor::red) . \PHP_EOL);
3✔
264
        if ($exitCode !== null) {
3✔
UNCOV
265
            exit($exitCode);
×
266
        }
267
    }
268

269
    /**
270
     * Clear the terminal screen.
271
     */
272
    public static function clear() : void
273
    {
274
        \fwrite(\STDOUT, "\e[H\e[2J");
1✔
275
    }
276

277
    /**
278
     * Get user input.
279
     *
280
     * NOTE: It is possible pass multiple lines ending each line with a backslash.
281
     *
282
     * @param string $prepend Text prepended in the input. Used internally to
283
     * allow multiple lines
284
     *
285
     * @return string Returns the user input
286
     */
287
    public static function getInput(string $prepend = '') : string
288
    {
UNCOV
289
        $input = \fgets(\STDIN);
×
UNCOV
290
        $input = $input === false ? '' : \trim($input);
×
UNCOV
291
        $prepend .= $input;
×
UNCOV
292
        $eolPos = false;
×
UNCOV
293
        if ($prepend) {
×
UNCOV
294
            $eolPos = \strrpos($prepend, '\\', -1);
×
295
        }
UNCOV
296
        if ($eolPos !== false) {
×
UNCOV
297
            $prepend = \substr_replace($prepend, \PHP_EOL, $eolPos);
×
UNCOV
298
            $prepend = static::getInput($prepend);
×
299
        }
UNCOV
300
        return $prepend;
×
301
    }
302

303
    /**
304
     * Prompt a question.
305
     *
306
     * @param string $question The question to prompt
307
     * @param array<int,string>|string|null $options Answer options. If an array
308
     * is set, the default answer is the first value. If is a string, it will
309
     * be the default.
310
     *
311
     * @return string The answer
312
     */
313
    public static function prompt(string $question, array | string $options = null) : string
314
    {
UNCOV
315
        if ($options !== null) {
×
UNCOV
316
            $options = \is_array($options)
×
UNCOV
317
                ? \array_values($options)
×
UNCOV
318
                : [$options];
×
319
        }
UNCOV
320
        if ($options) {
×
UNCOV
321
            $opt = $options;
×
UNCOV
322
            $opt[0] = static::style($opt[0], null, null, [Format::bold]);
×
UNCOV
323
            $optionsText = isset($opt[1])
×
UNCOV
324
                ? \implode(', ', $opt)
×
UNCOV
325
                : $opt[0];
×
UNCOV
326
            $question .= ' [' . $optionsText . ']';
×
327
        }
UNCOV
328
        $question .= ': ';
×
UNCOV
329
        \fwrite(\STDOUT, $question);
×
UNCOV
330
        $answer = static::getInput();
×
UNCOV
331
        if ($answer === '' && isset($options[0])) {
×
UNCOV
332
            $answer = $options[0];
×
333
        }
UNCOV
334
        return $answer;
×
335
    }
336

337
    /**
338
     * Prompt a question with secret answer.
339
     *
340
     * @param string $question The question to prompt
341
     *
342
     * @see https://dev.to/mykeels/reading-passwords-from-stdin-in-php-1np9
343
     *
344
     * @return string The secret answer
345
     */
346
    public static function secret(string $question) : string
347
    {
UNCOV
348
        $question .= ': ';
×
UNCOV
349
        \fwrite(\STDOUT, $question);
×
UNCOV
350
        \exec('stty -echo');
×
UNCOV
351
        $secret = \trim((string) \fgets(\STDIN));
×
UNCOV
352
        \exec('stty echo');
×
UNCOV
353
        return $secret;
×
354
    }
355

356
    /**
357
     * Creates a well formatted table.
358
     *
359
     * @param array<array<scalar|\Stringable>> $tbody Table body rows
360
     * @param array<scalar|\Stringable> $thead Table head fields
361
     */
362
    public static function table(array $tbody, array $thead = []) : void
363
    {
364
        // All the rows in the table will be here until the end
365
        $tableRows = [];
1✔
366
        // We need only indexes and not keys
367
        if (!empty($thead)) {
1✔
368
            $tableRows[] = \array_values($thead);
1✔
369
        }
370
        foreach ($tbody as $tr) {
1✔
371
            // cast tr to array if is not - (objects...)
372
            $tableRows[] = \array_values((array) $tr);
1✔
373
        }
374
        // Yes, it really is necessary to know this count
375
        $totalRows = \count($tableRows);
1✔
376
        // Store all columns lengths
377
        // $allColsLengths[row][column] = length
378
        $allColsLengths = [];
1✔
379
        // Store maximum lengths by column
380
        // $maxColsLengths[column] = length
381
        $maxColsLengths = [];
1✔
382
        // Read row by row and define the longest columns
383
        for ($row = 0; $row < $totalRows; $row++) {
1✔
384
            $column = 0; // Current column index
1✔
385
            foreach ($tableRows[$row] as $col) {
1✔
386
                // Sets the size of this column in the current row
387
                $allColsLengths[$row][$column] = static::strlen((string) $col);
1✔
388
                // If the current column does not have a value among the larger ones
389
                // or the value of this is greater than the existing one
390
                // then, now, this assumes the maximum length
391
                if (!isset($maxColsLengths[$column])
1✔
392
                    || $allColsLengths[$row][$column] > $maxColsLengths[$column]) {
1✔
393
                    $maxColsLengths[$column] = $allColsLengths[$row][$column];
1✔
394
                }
395
                // We can go check the size of the next column...
396
                $column++;
1✔
397
            }
398
        }
399
        // Read row by row and add spaces at the end of the columns
400
        // to match the exact column length
401
        for ($row = 0; $row < $totalRows; $row++) {
1✔
402
            $column = 0;
1✔
403
            foreach ($tableRows[$row] as $col => $value) {
1✔
404
                $diff = $maxColsLengths[$column] - $allColsLengths[$row][$col];
1✔
405
                if ($diff) {
1✔
406
                    $tableRows[$row][$column] .= \str_repeat(' ', $diff);
1✔
407
                }
408
                $column++;
1✔
409
            }
410
        }
411
        $table = $line = '';
1✔
412
        // Joins columns and append the well formatted rows to the table
413
        foreach ($tableRows as $row => $value) {
1✔
414
            // Set the table border-top
415
            if ($row === 0) {
1✔
416
                $line = '+';
1✔
417
                foreach (\array_keys($value) as $col) {
1✔
418
                    $line .= \str_repeat('-', $maxColsLengths[$col] + 2) . '+';
1✔
419
                }
420
                $table .= $line . \PHP_EOL;
1✔
421
            }
422
            // Set the vertical borders
423
            $table .= '| ' . \implode(' | ', $value) . ' |' . \PHP_EOL;
1✔
424
            // Set the thead and table borders-bottom
425
            if (($row === 0 && !empty($thead)) || $row + 1 === $totalRows) {
1✔
426
                $table .= $line . \PHP_EOL;
1✔
427
            }
428
        }
429
        \fwrite(\STDOUT, $table);
1✔
430
    }
431
}
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