ci4

Форк
0
/
CLI.php 
1152 строки · 35.0 Кб
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\CLI;
15

16
use CodeIgniter\CLI\Exceptions\CLIException;
17
use Config\Services;
18
use InvalidArgumentException;
19
use Throwable;
20

21
/**
22
 * Set of static methods useful for CLI request handling.
23
 *
24
 * Portions of this code were initially from the FuelPHP Framework,
25
 * version 1.7.x, and used here under the MIT license they were
26
 * originally made available under. Reference: http://fuelphp.com
27
 *
28
 * Some of the code in this class is Windows-specific, and not
29
 * possible to test using travis-ci. It has been phpunit-annotated
30
 * to prevent messing up code coverage.
31
 *
32
 * @see \CodeIgniter\CLI\CLITest
33
 */
34
class CLI
35
{
36
    /**
37
     * Is the readline library on the system?
38
     *
39
     * @var bool
40
     *
41
     * @deprecated 4.4.2 Should be protected, and no longer used.
42
     * @TODO Fix to camelCase in the next major version.
43
     */
44
    public static $readline_support = false;
45

46
    /**
47
     * The message displayed at prompts.
48
     *
49
     * @var string
50
     *
51
     * @deprecated 4.4.2 Should be protected.
52
     * @TODO Fix to camelCase in the next major version.
53
     */
54
    public static $wait_msg = 'Press any key to continue...';
55

56
    /**
57
     * Has the class already been initialized?
58
     *
59
     * @var bool
60
     */
61
    protected static $initialized = false;
62

63
    /**
64
     * Foreground color list
65
     *
66
     * @var array<string, string>
67
     *
68
     * @TODO Fix to camelCase in the next major version.
69
     */
70
    protected static $foreground_colors = [
71
        'black'        => '0;30',
72
        'dark_gray'    => '1;30',
73
        'blue'         => '0;34',
74
        'dark_blue'    => '0;34',
75
        'light_blue'   => '1;34',
76
        'green'        => '0;32',
77
        'light_green'  => '1;32',
78
        'cyan'         => '0;36',
79
        'light_cyan'   => '1;36',
80
        'red'          => '0;31',
81
        'light_red'    => '1;31',
82
        'purple'       => '0;35',
83
        'light_purple' => '1;35',
84
        'yellow'       => '0;33',
85
        'light_yellow' => '1;33',
86
        'light_gray'   => '0;37',
87
        'white'        => '1;37',
88
    ];
89

90
    /**
91
     * Background color list
92
     *
93
     * @var array<string, string>
94
     *
95
     * @TODO Fix to camelCase in the next major version.
96
     */
97
    protected static $background_colors = [
98
        'black'      => '40',
99
        'red'        => '41',
100
        'green'      => '42',
101
        'yellow'     => '43',
102
        'blue'       => '44',
103
        'magenta'    => '45',
104
        'cyan'       => '46',
105
        'light_gray' => '47',
106
    ];
107

108
    /**
109
     * List of array segments.
110
     *
111
     * @var array
112
     */
113
    protected static $segments = [];
114

115
    /**
116
     * @var array
117
     */
118
    protected static $options = [];
119

120
    /**
121
     * Helps track internally whether the last
122
     * output was a "write" or a "print" to
123
     * keep the output clean and as expected.
124
     *
125
     * @var string|null
126
     */
127
    protected static $lastWrite;
128

129
    /**
130
     * Height of the CLI window
131
     *
132
     * @var int|null
133
     */
134
    protected static $height;
135

136
    /**
137
     * Width of the CLI window
138
     *
139
     * @var int|null
140
     */
141
    protected static $width;
142

143
    /**
144
     * Whether the current stream supports colored output.
145
     *
146
     * @var bool
147
     */
148
    protected static $isColored = false;
149

150
    /**
151
     * Input and Output for CLI.
152
     */
153
    protected static ?InputOutput $io = null;
154

155
    /**
156
     * Static "constructor".
157
     *
158
     * @return void
159
     */
160
    public static function init()
161
    {
162
        if (is_cli()) {
163
            // Readline is an extension for PHP that makes interactivity with PHP
164
            // much more bash-like.
165
            // http://www.php.net/manual/en/readline.installation.php
166
            static::$readline_support = extension_loaded('readline');
167

168
            // clear segments & options to keep testing clean
169
            static::$segments = [];
170
            static::$options  = [];
171

172
            // Check our stream resource for color support
173
            static::$isColored = static::hasColorSupport(STDOUT);
174

175
            static::parseCommandLine();
176

177
            static::$initialized = true;
178
        } elseif (! defined('STDOUT')) {
179
            // If the command is being called from a controller
180
            // we need to define STDOUT ourselves
181
            // For "! defined('STDOUT')" see: https://github.com/codeigniter4/CodeIgniter4/issues/7047
182
            define('STDOUT', 'php://output'); // @codeCoverageIgnore
183
        }
184

185
        static::resetInputOutput();
186
    }
187

188
    /**
189
     * Get input from the shell, using readline or the standard STDIN
190
     *
191
     * Named options must be in the following formats:
192
     * php index.php user -v --v -name=John --name=John
193
     *
194
     * @param string|null $prefix You may specify a string with which to prompt the user.
195
     */
196
    public static function input(?string $prefix = null): string
197
    {
198
        return static::$io->input($prefix);
199
    }
200

201
    /**
202
     * Asks the user for input.
203
     *
204
     * Usage:
205
     *
206
     * // Takes any input
207
     * $color = CLI::prompt('What is your favorite color?');
208
     *
209
     * // Takes any input, but offers default
210
     * $color = CLI::prompt('What is your favourite color?', 'white');
211
     *
212
     * // Will validate options with the in_list rule and accept only if one of the list
213
     * $color = CLI::prompt('What is your favourite color?', array('red','blue'));
214
     *
215
     * // Do not provide options but requires a valid email
216
     * $email = CLI::prompt('What is your email?', null, 'required|valid_email');
217
     *
218
     * @param string                  $field      Output "field" question
219
     * @param list<int|string>|string $options    String to a default value, array to a list of options (the first option will be the default value)
220
     * @param array|string|null       $validation Validation rules
221
     *
222
     * @return string The user input
223
     */
224
    public static function prompt(string $field, $options = null, $validation = null): string
225
    {
226
        $extraOutput = '';
227
        $default     = '';
228

229
        if ($validation && ! is_array($validation) && ! is_string($validation)) {
230
            throw new InvalidArgumentException('$rules can only be of type string|array');
231
        }
232

233
        if (! is_array($validation)) {
234
            $validation = $validation ? explode('|', $validation) : [];
235
        }
236

237
        if (is_string($options)) {
238
            $extraOutput = ' [' . static::color($options, 'green') . ']';
239
            $default     = $options;
240
        }
241

242
        if (is_array($options) && $options !== []) {
243
            $opts               = $options;
244
            $extraOutputDefault = static::color((string) $opts[0], 'green');
245

246
            unset($opts[0]);
247

248
            if ($opts === []) {
249
                $extraOutput = $extraOutputDefault;
250
            } else {
251
                $extraOutput  = '[' . $extraOutputDefault . ', ' . implode(', ', $opts) . ']';
252
                $validation[] = 'in_list[' . implode(', ', $options) . ']';
253
            }
254

255
            $default = $options[0];
256
        }
257

258
        static::fwrite(STDOUT, $field . (trim($field) !== '' ? ' ' : '') . $extraOutput . ': ');
259

260
        // Read the input from keyboard.
261
        $input = trim(static::$io->input());
262
        $input = ($input === '') ? (string) $default : $input;
263

264
        if ($validation !== []) {
265
            while (! static::validate('"' . trim($field) . '"', $input, $validation)) {
266
                $input = static::prompt($field, $options, $validation);
267
            }
268
        }
269

270
        return $input;
271
    }
272

273
    /**
274
     * prompt(), but based on the option's key
275
     *
276
     * @param array|string      $text       Output "field" text or an one or two value array where the first value is the text before listing the options
277
     *                                      and the second value the text before asking to select one option. Provide empty string to omit
278
     * @param array             $options    A list of options (array(key => description)), the first option will be the default value
279
     * @param array|string|null $validation Validation rules
280
     *
281
     * @return string The selected key of $options
282
     */
283
    public static function promptByKey($text, array $options, $validation = null): string
284
    {
285
        if (is_string($text)) {
286
            $text = [$text];
287
        } elseif (! is_array($text)) {
288
            throw new InvalidArgumentException('$text can only be of type string|array');
289
        }
290

291
        CLI::isZeroOptions($options);
292

293
        if ($line = array_shift($text)) {
294
            CLI::write($line);
295
        }
296

297
        CLI::printKeysAndValues($options);
298

299
        return static::prompt(PHP_EOL . array_shift($text), array_keys($options), $validation);
300
    }
301

302
    /**
303
     * This method is the same as promptByKey(), but this method supports multiple keys, separated by commas.
304
     *
305
     * @param string $text    Output "field" text or an one or two value array where the first value is the text before listing the options
306
     *                        and the second value the text before asking to select one option. Provide empty string to omit
307
     * @param array  $options A list of options (array(key => description)), the first option will be the default value
308
     *
309
     * @return array The selected key(s) and value(s) of $options
310
     */
311
    public static function promptByMultipleKeys(string $text, array $options): array
312
    {
313
        CLI::isZeroOptions($options);
314

315
        $extraOutputDefault = static::color('0', 'green');
316
        $opts               = $options;
317
        unset($opts[0]);
318

319
        if ($opts === []) {
320
            $extraOutput = $extraOutputDefault;
321
        } else {
322
            $optsKey = [];
323

324
            foreach (array_keys($opts) as $key) {
325
                $optsKey[] = $key;
326
            }
327
            $extraOutput = '[' . $extraOutputDefault . ', ' . implode(', ', $optsKey) . ']';
328
            $extraOutput = 'You can specify multiple values separated by commas.' . PHP_EOL . $extraOutput;
329
        }
330

331
        CLI::write($text);
332
        CLI::printKeysAndValues($options);
333
        CLI::newLine();
334

335
        $input = static::prompt($extraOutput);
336
        $input = ($input === '') ? '0' : $input; // 0 is default
337

338
        // validation
339
        while (true) {
340
            $pattern = preg_match_all('/^\d+(,\d+)*$/', trim($input));
341

342
            // separate input by comma and convert all to an int[]
343
            $inputToArray = array_map(static fn ($value) => (int) $value, explode(',', $input));
344
            // find max from key of $options
345
            $maxOptions = array_key_last($options);
346
            // find max from input
347
            $maxInput = max($inputToArray);
348

349
            // return the prompt again if $input contain(s) non-numeric character, except a comma.
350
            // And if max from $options less than max from input,
351
            // it means user tried to access null value in $options
352
            if (! $pattern || $maxOptions < $maxInput) {
353
                static::error('Please select correctly.');
354
                CLI::newLine();
355

356
                $input = static::prompt($extraOutput);
357
                $input = ($input === '') ? '0' : $input;
358
            } else {
359
                break;
360
            }
361
        }
362

363
        $input = [];
364

365
        foreach ($options as $key => $description) {
366
            foreach ($inputToArray as $inputKey) {
367
                if ($key === $inputKey) {
368
                    $input[$key] = $description;
369
                }
370
            }
371
        }
372

373
        return $input;
374
    }
375

376
    // --------------------------------------------------------------------
377
    // Utility for promptBy...
378
    // --------------------------------------------------------------------
379

380
    /**
381
     * Validation for $options in promptByKey() and promptByMultipleKeys(). Return an error if $options is an empty array.
382
     */
383
    private static function isZeroOptions(array $options): void
384
    {
385
        if ($options === []) {
386
            throw new InvalidArgumentException('No options to select from were provided');
387
        }
388
    }
389

390
    /**
391
     * Print each key and value one by one
392
     */
393
    private static function printKeysAndValues(array $options): void
394
    {
395
        // +2 for the square brackets around the key
396
        $keyMaxLength = max(array_map(mb_strwidth(...), array_keys($options))) + 2;
397

398
        foreach ($options as $key => $description) {
399
            $name = str_pad('  [' . $key . ']  ', $keyMaxLength + 4, ' ');
400
            CLI::write(CLI::color($name, 'green') . CLI::wrap($description, 125, $keyMaxLength + 4));
401
        }
402
    }
403

404
    // --------------------------------------------------------------------
405
    // End Utility for promptBy...
406
    // --------------------------------------------------------------------
407

408
    /**
409
     * Validate one prompt "field" at a time
410
     *
411
     * @param string       $field Prompt "field" output
412
     * @param string       $value Input value
413
     * @param array|string $rules Validation rules
414
     */
415
    protected static function validate(string $field, string $value, $rules): bool
416
    {
417
        $label      = $field;
418
        $field      = 'temp';
419
        $validation = Services::validation(null, false);
420
        $validation->setRules([
421
            $field => [
422
                'label' => $label,
423
                'rules' => $rules,
424
            ],
425
        ]);
426
        $validation->run([$field => $value]);
427

428
        if ($validation->hasError($field)) {
429
            static::error($validation->getError($field));
430

431
            return false;
432
        }
433

434
        return true;
435
    }
436

437
    /**
438
     * Outputs a string to the CLI without any surrounding newlines.
439
     * Useful for showing repeating elements on a single line.
440
     *
441
     * @return void
442
     */
443
    public static function print(string $text = '', ?string $foreground = null, ?string $background = null)
444
    {
445
        if ($foreground || $background) {
446
            $text = static::color($text, $foreground, $background);
447
        }
448

449
        static::$lastWrite = null;
450

451
        static::fwrite(STDOUT, $text);
452
    }
453

454
    /**
455
     * Outputs a string to the cli on its own line.
456
     *
457
     * @return void
458
     */
459
    public static function write(string $text = '', ?string $foreground = null, ?string $background = null)
460
    {
461
        if ($foreground || $background) {
462
            $text = static::color($text, $foreground, $background);
463
        }
464

465
        if (static::$lastWrite !== 'write') {
466
            $text              = PHP_EOL . $text;
467
            static::$lastWrite = 'write';
468
        }
469

470
        static::fwrite(STDOUT, $text . PHP_EOL);
471
    }
472

473
    /**
474
     * Outputs an error to the CLI using STDERR instead of STDOUT
475
     *
476
     * @return void
477
     */
478
    public static function error(string $text, string $foreground = 'light_red', ?string $background = null)
479
    {
480
        // Check color support for STDERR
481
        $stdout            = static::$isColored;
482
        static::$isColored = static::hasColorSupport(STDERR);
483

484
        if ($foreground || $background) {
485
            $text = static::color($text, $foreground, $background);
486
        }
487

488
        static::fwrite(STDERR, $text . PHP_EOL);
489

490
        // return STDOUT color support
491
        static::$isColored = $stdout;
492
    }
493

494
    /**
495
     * Beeps a certain number of times.
496
     *
497
     * @param int $num The number of times to beep
498
     *
499
     * @return void
500
     */
501
    public static function beep(int $num = 1)
502
    {
503
        echo str_repeat("\x07", $num);
504
    }
505

506
    /**
507
     * Waits a certain number of seconds, optionally showing a wait message and
508
     * waiting for a key press.
509
     *
510
     * @param int  $seconds   Number of seconds
511
     * @param bool $countdown Show a countdown or not
512
     *
513
     * @return void
514
     */
515
    public static function wait(int $seconds, bool $countdown = false)
516
    {
517
        if ($countdown === true) {
518
            $time = $seconds;
519

520
            while ($time > 0) {
521
                static::fwrite(STDOUT, $time . '... ');
522
                sleep(1);
523
                $time--;
524
            }
525

526
            static::write();
527
        } elseif ($seconds > 0) {
528
            sleep($seconds);
529
        } else {
530
            static::write(static::$wait_msg);
531
            static::$io->input();
532
        }
533
    }
534

535
    /**
536
     * if operating system === windows
537
     *
538
     * @deprecated 4.3.0 Use `is_windows()` instead
539
     */
540
    public static function isWindows(): bool
541
    {
542
        return is_windows();
543
    }
544

545
    /**
546
     * Enter a number of empty lines
547
     *
548
     * @return void
549
     */
550
    public static function newLine(int $num = 1)
551
    {
552
        // Do it once or more, write with empty string gives us a new line
553
        for ($i = 0; $i < $num; $i++) {
554
            static::write();
555
        }
556
    }
557

558
    /**
559
     * Clears the screen of output
560
     *
561
     * @return void
562
     */
563
    public static function clearScreen()
564
    {
565
        // Unix systems, and Windows with VT100 Terminal support (i.e. Win10)
566
        // can handle CSI sequences. For lower than Win10 we just shove in 40 new lines.
567
        is_windows() && ! static::streamSupports('sapi_windows_vt100_support', STDOUT)
568
            ? static::newLine(40)
569
            : static::fwrite(STDOUT, "\033[H\033[2J");
570
    }
571

572
    /**
573
     * Returns the given text with the correct color codes for a foreground and
574
     * optionally a background color.
575
     *
576
     * @param string      $text       The text to color
577
     * @param string      $foreground The foreground color
578
     * @param string|null $background The background color
579
     * @param string|null $format     Other formatting to apply. Currently only 'underline' is understood
580
     *
581
     * @return string The color coded string
582
     */
583
    public static function color(string $text, string $foreground, ?string $background = null, ?string $format = null): string
584
    {
585
        if (! static::$isColored || $text === '') {
586
            return $text;
587
        }
588

589
        if (! array_key_exists($foreground, static::$foreground_colors)) {
590
            throw CLIException::forInvalidColor('foreground', $foreground);
591
        }
592

593
        if ($background !== null && ! array_key_exists($background, static::$background_colors)) {
594
            throw CLIException::forInvalidColor('background', $background);
595
        }
596

597
        $newText = '';
598

599
        // Detect if color method was already in use with this text
600
        if (str_contains($text, "\033[0m")) {
601
            $pattern = '/\\033\\[0;.+?\\033\\[0m/u';
602

603
            preg_match_all($pattern, $text, $matches);
604
            $coloredStrings = $matches[0];
605

606
            // No colored string found. Invalid strings with no `\033[0;??`.
607
            if ($coloredStrings === []) {
608
                return $newText . self::getColoredText($text, $foreground, $background, $format);
609
            }
610

611
            $nonColoredText = preg_replace(
612
                $pattern,
613
                '<<__colored_string__>>',
614
                $text
615
            );
616
            $nonColoredChunks = preg_split(
617
                '/<<__colored_string__>>/u',
618
                $nonColoredText
619
            );
620

621
            foreach ($nonColoredChunks as $i => $chunk) {
622
                if ($chunk !== '') {
623
                    $newText .= self::getColoredText($chunk, $foreground, $background, $format);
624
                }
625

626
                if (isset($coloredStrings[$i])) {
627
                    $newText .= $coloredStrings[$i];
628
                }
629
            }
630
        } else {
631
            $newText .= self::getColoredText($text, $foreground, $background, $format);
632
        }
633

634
        return $newText;
635
    }
636

637
    private static function getColoredText(string $text, string $foreground, ?string $background, ?string $format): string
638
    {
639
        $string = "\033[" . static::$foreground_colors[$foreground] . 'm';
640

641
        if ($background !== null) {
642
            $string .= "\033[" . static::$background_colors[$background] . 'm';
643
        }
644

645
        if ($format === 'underline') {
646
            $string .= "\033[4m";
647
        }
648

649
        return $string . $text . "\033[0m";
650
    }
651

652
    /**
653
     * Get the number of characters in string having encoded characters
654
     * and ignores styles set by the color() function
655
     */
656
    public static function strlen(?string $string): int
657
    {
658
        if ($string === null) {
659
            return 0;
660
        }
661

662
        foreach (static::$foreground_colors as $color) {
663
            $string = strtr($string, ["\033[" . $color . 'm' => '']);
664
        }
665

666
        foreach (static::$background_colors as $color) {
667
            $string = strtr($string, ["\033[" . $color . 'm' => '']);
668
        }
669

670
        $string = strtr($string, ["\033[4m" => '', "\033[0m" => '']);
671

672
        return mb_strwidth($string);
673
    }
674

675
    /**
676
     * Checks whether the current stream resource supports or
677
     * refers to a valid terminal type device.
678
     *
679
     * @param resource $resource
680
     */
681
    public static function streamSupports(string $function, $resource): bool
682
    {
683
        if (ENVIRONMENT === 'testing') {
684
            // In the current setup of the tests we cannot fully check
685
            // if the stream supports the function since we are using
686
            // filtered streams.
687
            return function_exists($function);
688
        }
689

690
        return function_exists($function) && @$function($resource); // @codeCoverageIgnore
691
    }
692

693
    /**
694
     * Returns true if the stream resource supports colors.
695
     *
696
     * This is tricky on Windows, because Cygwin, Msys2 etc. emulate pseudo
697
     * terminals via named pipes, so we can only check the environment.
698
     *
699
     * Reference: https://github.com/composer/xdebug-handler/blob/master/src/Process.php
700
     *
701
     * @param resource $resource
702
     */
703
    public static function hasColorSupport($resource): bool
704
    {
705
        // Follow https://no-color.org/
706
        if (isset($_SERVER['NO_COLOR']) || getenv('NO_COLOR') !== false) {
707
            return false;
708
        }
709

710
        if (getenv('TERM_PROGRAM') === 'Hyper') {
711
            return true;
712
        }
713

714
        if (is_windows()) {
715
            // @codeCoverageIgnoreStart
716
            return static::streamSupports('sapi_windows_vt100_support', $resource)
717
                || isset($_SERVER['ANSICON'])
718
                || getenv('ANSICON') !== false
719
                || getenv('ConEmuANSI') === 'ON'
720
                || getenv('TERM') === 'xterm';
721
            // @codeCoverageIgnoreEnd
722
        }
723

724
        return static::streamSupports('stream_isatty', $resource);
725
    }
726

727
    /**
728
     * Attempts to determine the width of the viewable CLI window.
729
     */
730
    public static function getWidth(int $default = 80): int
731
    {
732
        if (static::$width === null) {
733
            static::generateDimensions();
734
        }
735

736
        return static::$width ?: $default;
737
    }
738

739
    /**
740
     * Attempts to determine the height of the viewable CLI window.
741
     */
742
    public static function getHeight(int $default = 32): int
743
    {
744
        if (static::$height === null) {
745
            static::generateDimensions();
746
        }
747

748
        return static::$height ?: $default;
749
    }
750

751
    /**
752
     * Populates the CLI's dimensions.
753
     *
754
     * @return void
755
     */
756
    public static function generateDimensions()
757
    {
758
        try {
759
            if (is_windows()) {
760
                // Shells such as `Cygwin` and `Git bash` returns incorrect values
761
                // when executing `mode CON`, so we use `tput` instead
762
                if (getenv('TERM') || (($shell = getenv('SHELL')) && preg_match('/(?:bash|zsh)(?:\.exe)?$/', $shell))) {
763
                    static::$height = (int) exec('tput lines');
764
                    static::$width  = (int) exec('tput cols');
765
                } else {
766
                    $return = -1;
767
                    $output = [];
768
                    exec('mode CON', $output, $return);
769

770
                    // Look for the next lines ending in ": <number>"
771
                    // Searching for "Columns:" or "Lines:" will fail on non-English locales
772
                    if ($return === 0 && $output && preg_match('/:\s*(\d+)\n[^:]+:\s*(\d+)\n/', implode("\n", $output), $matches)) {
773
                        static::$height = (int) $matches[1];
774
                        static::$width  = (int) $matches[2];
775
                    }
776
                }
777
            } elseif (($size = exec('stty size')) && preg_match('/(\d+)\s+(\d+)/', $size, $matches)) {
778
                static::$height = (int) $matches[1];
779
                static::$width  = (int) $matches[2];
780
            } else {
781
                static::$height = (int) exec('tput lines');
782
                static::$width  = (int) exec('tput cols');
783
            }
784
        } catch (Throwable $e) {
785
            // Reset the dimensions so that the default values will be returned later.
786
            // Then let the developer know of the error.
787
            static::$height = null;
788
            static::$width  = null;
789
            log_message('error', (string) $e);
790
        }
791
    }
792

793
    /**
794
     * Displays a progress bar on the CLI. You must call it repeatedly
795
     * to update it. Set $thisStep = false to erase the progress bar.
796
     *
797
     * @param bool|int $thisStep
798
     *
799
     * @return void
800
     */
801
    public static function showProgress($thisStep = 1, int $totalSteps = 10)
802
    {
803
        static $inProgress = false;
804

805
        // restore cursor position when progress is continuing.
806
        if ($inProgress !== false && $inProgress <= $thisStep) {
807
            static::fwrite(STDOUT, "\033[1A");
808
        }
809
        $inProgress = $thisStep;
810

811
        if ($thisStep !== false) {
812
            // Don't allow div by zero or negative numbers....
813
            $thisStep   = abs($thisStep);
814
            $totalSteps = $totalSteps < 1 ? 1 : $totalSteps;
815

816
            $percent = (int) (($thisStep / $totalSteps) * 100);
817
            $step    = (int) round($percent / 10);
818

819
            // Write the progress bar
820
            static::fwrite(STDOUT, "[\033[32m" . str_repeat('#', $step) . str_repeat('.', 10 - $step) . "\033[0m]");
821
            // Textual representation...
822
            static::fwrite(STDOUT, sprintf(' %3d%% Complete', $percent) . PHP_EOL);
823
        } else {
824
            static::fwrite(STDOUT, "\007");
825
        }
826
    }
827

828
    /**
829
     * Takes a string and writes it to the command line, wrapping to a maximum
830
     * width. If no maximum width is specified, will wrap to the window's max
831
     * width.
832
     *
833
     * If an int is passed into $pad_left, then all strings after the first
834
     * will pad with that many spaces to the left. Useful when printing
835
     * short descriptions that need to start on an existing line.
836
     */
837
    public static function wrap(?string $string = null, int $max = 0, int $padLeft = 0): string
838
    {
839
        if ($string === null || $string === '') {
840
            return '';
841
        }
842

843
        if ($max === 0) {
844
            $max = self::getWidth();
845
        }
846

847
        if (self::getWidth() < $max) {
848
            $max = self::getWidth();
849
        }
850

851
        $max -= $padLeft;
852

853
        $lines = wordwrap($string, $max, PHP_EOL);
854

855
        if ($padLeft > 0) {
856
            $lines = explode(PHP_EOL, $lines);
857

858
            $first = true;
859

860
            array_walk($lines, static function (&$line) use ($padLeft, &$first): void {
861
                if (! $first) {
862
                    $line = str_repeat(' ', $padLeft) . $line;
863
                } else {
864
                    $first = false;
865
                }
866
            });
867

868
            $lines = implode(PHP_EOL, $lines);
869
        }
870

871
        return $lines;
872
    }
873

874
    // --------------------------------------------------------------------
875
    // Command-Line 'URI' support
876
    // --------------------------------------------------------------------
877

878
    /**
879
     * Parses the command line it was called from and collects all
880
     * options and valid segments.
881
     *
882
     * @return void
883
     */
884
    protected static function parseCommandLine()
885
    {
886
        $args = $_SERVER['argv'] ?? [];
887
        array_shift($args); // scrap invoking program
888
        $optionValue = false;
889

890
        foreach ($args as $i => $arg) {
891
            // If there's no "-" at the beginning, then
892
            // this is probably an argument or an option value
893
            if (mb_strpos($arg, '-') !== 0) {
894
                if ($optionValue) {
895
                    // We have already included this in the previous
896
                    // iteration, so reset this flag
897
                    $optionValue = false;
898
                } else {
899
                    // Yup, it's a segment
900
                    static::$segments[] = $arg;
901
                }
902

903
                continue;
904
            }
905

906
            $arg   = ltrim($arg, '-');
907
            $value = null;
908

909
            if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) {
910
                $value       = $args[$i + 1];
911
                $optionValue = true;
912
            }
913

914
            static::$options[$arg] = $value;
915
        }
916
    }
917

918
    /**
919
     * Returns the command line string portions of the arguments, minus
920
     * any options, as a string. This is used to pass along to the main
921
     * CodeIgniter application.
922
     */
923
    public static function getURI(): string
924
    {
925
        return implode('/', static::$segments);
926
    }
927

928
    /**
929
     * Returns an individual segment.
930
     *
931
     * This ignores any options that might have been dispersed between
932
     * valid segments in the command:
933
     *
934
     *  // segment(3) is 'three', not '-f' or 'anOption'
935
     *  > php spark one two -f anOption three
936
     *
937
     * **IMPORTANT:** The index here is one-based instead of zero-based.
938
     *
939
     * @return string|null
940
     */
941
    public static function getSegment(int $index)
942
    {
943
        return static::$segments[$index - 1] ?? null;
944
    }
945

946
    /**
947
     * Returns the raw array of segments found.
948
     */
949
    public static function getSegments(): array
950
    {
951
        return static::$segments;
952
    }
953

954
    /**
955
     * Gets a single command-line option. Returns TRUE if the option
956
     * exists, but doesn't have a value, and is simply acting as a flag.
957
     *
958
     * @return string|true|null
959
     */
960
    public static function getOption(string $name)
961
    {
962
        if (! array_key_exists($name, static::$options)) {
963
            return null;
964
        }
965

966
        // If the option didn't have a value, simply return TRUE
967
        // so they know it was set, otherwise return the actual value.
968
        $val = static::$options[$name] ?? true;
969

970
        return $val;
971
    }
972

973
    /**
974
     * Returns the raw array of options found.
975
     */
976
    public static function getOptions(): array
977
    {
978
        return static::$options;
979
    }
980

981
    /**
982
     * Returns the options as a string, suitable for passing along on
983
     * the CLI to other commands.
984
     *
985
     * @param bool $useLongOpts Use '--' for long options?
986
     * @param bool $trim        Trim final string output?
987
     */
988
    public static function getOptionString(bool $useLongOpts = false, bool $trim = false): string
989
    {
990
        if (static::$options === []) {
991
            return '';
992
        }
993

994
        $out = '';
995

996
        foreach (static::$options as $name => $value) {
997
            if ($useLongOpts && mb_strlen($name) > 1) {
998
                $out .= "--{$name} ";
999
            } else {
1000
                $out .= "-{$name} ";
1001
            }
1002

1003
            if ($value === null) {
1004
                continue;
1005
            }
1006

1007
            if (mb_strpos($value, ' ') !== false) {
1008
                $out .= "\"{$value}\" ";
1009
            } elseif ($value !== null) {
1010
                $out .= "{$value} ";
1011
            }
1012
        }
1013

1014
        return $trim ? trim($out) : $out;
1015
    }
1016

1017
    /**
1018
     * Returns a well formatted table
1019
     *
1020
     * @param array $tbody List of rows
1021
     * @param array $thead List of columns
1022
     *
1023
     * @return void
1024
     */
1025
    public static function table(array $tbody, array $thead = [])
1026
    {
1027
        // All the rows in the table will be here until the end
1028
        $tableRows = [];
1029

1030
        // We need only indexes and not keys
1031
        if ($thead !== []) {
1032
            $tableRows[] = array_values($thead);
1033
        }
1034

1035
        foreach ($tbody as $tr) {
1036
            $tableRows[] = array_values($tr);
1037
        }
1038

1039
        // Yes, it really is necessary to know this count
1040
        $totalRows = count($tableRows);
1041

1042
        // Store all columns lengths
1043
        // $all_cols_lengths[row][column] = length
1044
        $allColsLengths = [];
1045

1046
        // Store maximum lengths by column
1047
        // $max_cols_lengths[column] = length
1048
        $maxColsLengths = [];
1049

1050
        // Read row by row and define the longest columns
1051
        for ($row = 0; $row < $totalRows; $row++) {
1052
            $column = 0; // Current column index
1053

1054
            foreach ($tableRows[$row] as $col) {
1055
                // Sets the size of this column in the current row
1056
                $allColsLengths[$row][$column] = static::strlen((string) $col);
1057

1058
                // If the current column does not have a value among the larger ones
1059
                // or the value of this is greater than the existing one
1060
                // then, now, this assumes the maximum length
1061
                if (! isset($maxColsLengths[$column]) || $allColsLengths[$row][$column] > $maxColsLengths[$column]) {
1062
                    $maxColsLengths[$column] = $allColsLengths[$row][$column];
1063
                }
1064

1065
                // We can go check the size of the next column...
1066
                $column++;
1067
            }
1068
        }
1069

1070
        // Read row by row and add spaces at the end of the columns
1071
        // to match the exact column length
1072
        for ($row = 0; $row < $totalRows; $row++) {
1073
            $column = 0;
1074

1075
            foreach ($tableRows[$row] as $col) {
1076
                $diff = $maxColsLengths[$column] - static::strlen((string) $col);
1077

1078
                if ($diff !== 0) {
1079
                    $tableRows[$row][$column] .= str_repeat(' ', $diff);
1080
                }
1081

1082
                $column++;
1083
            }
1084
        }
1085

1086
        $table = '';
1087
        $cols  = '';
1088

1089
        // Joins columns and append the well formatted rows to the table
1090
        for ($row = 0; $row < $totalRows; $row++) {
1091
            // Set the table border-top
1092
            if ($row === 0) {
1093
                $cols = '+';
1094

1095
                foreach ($tableRows[$row] as $col) {
1096
                    $cols .= str_repeat('-', static::strlen((string) $col) + 2) . '+';
1097
                }
1098
                $table .= $cols . PHP_EOL;
1099
            }
1100

1101
            // Set the columns borders
1102
            $table .= '| ' . implode(' | ', $tableRows[$row]) . ' |' . PHP_EOL;
1103

1104
            // Set the thead and table borders-bottom
1105
            if (($row === 0 && $thead !== []) || ($row + 1 === $totalRows)) {
1106
                $table .= $cols . PHP_EOL;
1107
            }
1108
        }
1109

1110
        static::write($table);
1111
    }
1112

1113
    /**
1114
     * While the library is intended for use on CLI commands,
1115
     * commands can be called from controllers and elsewhere
1116
     * so we need a way to allow them to still work.
1117
     *
1118
     * For now, just echo the content, but look into a better
1119
     * solution down the road.
1120
     *
1121
     * @param resource $handle
1122
     *
1123
     * @return void
1124
     */
1125
    protected static function fwrite($handle, string $string)
1126
    {
1127
        static::$io->fwrite($handle, $string);
1128
    }
1129

1130
    /**
1131
     * Testing purpose only
1132
     *
1133
     * @testTag
1134
     */
1135
    public static function setInputOutput(InputOutput $io): void
1136
    {
1137
        static::$io = $io;
1138
    }
1139

1140
    /**
1141
     * Testing purpose only
1142
     *
1143
     * @testTag
1144
     */
1145
    public static function resetInputOutput(): void
1146
    {
1147
        static::$io = new InputOutput();
1148
    }
1149
}
1150

1151
// Ensure the class is initialized. Done outside of code coverage
1152
CLI::init(); // @codeCoverageIgnore
1153

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.