├── src
├── Streams
│ ├── Stderr.php
│ ├── Stdout.php
│ ├── FilterStream.php
│ └── Stream.php
├── Commands
│ ├── About.php
│ ├── Help.php
│ └── Index.php
├── Languages
│ ├── en
│ │ └── cli.php
│ ├── pt-br
│ │ └── cli.php
│ └── es
│ │ └── cli.php
├── Command.php
├── Console.php
└── CLI.php
├── README.md
├── LICENSE
├── composer.json
└── .phpstorm.meta.php
/src/Streams/Stderr.php:
--------------------------------------------------------------------------------
1 |
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\Streams;
11 |
12 | /**
13 | * Class Stderr.
14 | *
15 | * @package cli
16 | */
17 | class Stderr extends \php_user_filter
18 | {
19 | use FilterStream;
20 |
21 | public static function init() : void
22 | {
23 | \stream_filter_register(static::class, static::class);
24 | \stream_filter_append(\STDERR, static::class);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Streams/Stdout.php:
--------------------------------------------------------------------------------
1 |
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\Streams;
11 |
12 | /**
13 | * Class Stdout.
14 | *
15 | * @package cli
16 | */
17 | class Stdout extends \php_user_filter
18 | {
19 | use FilterStream;
20 |
21 | public static function init() : void
22 | {
23 | \stream_filter_register(static::class, static::class);
24 | \stream_filter_append(\STDOUT, static::class);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Aplus Framework CLI Library
4 |
5 | - [Home](https://aplus-framework.com/packages/cli)
6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/cli/index.html)
7 | - [API Documentation](https://docs.aplus-framework.com/packages/cli.html)
8 |
9 | [](https://github.com/aplus-framework/cli/actions/workflows/tests.yml)
10 | [](https://coveralls.io/github/aplus-framework/cli?branch=master)
11 | [](https://packagist.org/packages/aplus/cli)
12 | [](https://aplus-framework.com/sponsor)
13 |
--------------------------------------------------------------------------------
/src/Commands/About.php:
--------------------------------------------------------------------------------
1 |
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\Commands;
11 |
12 | use Framework\CLI\CLI;
13 | use Framework\CLI\Command;
14 |
15 | /**
16 | * Class About.
17 | *
18 | * @package cli
19 | */
20 | class About extends Command
21 | {
22 | public function run() : void
23 | {
24 | $lang = $this->console->getLanguage();
25 | CLI::write($lang->render('cli', 'about.line1'), CLI::FG_BRIGHT_GREEN);
26 | CLI::write($lang->render('cli', 'about.line2'));
27 | CLI::write($lang->render('cli', 'about.line3'));
28 | CLI::write($lang->render('cli', 'about.line4'));
29 | CLI::write($lang->render('cli', 'about.line5'));
30 | }
31 |
32 | public function getDescription() : string
33 | {
34 | return $this->console->getLanguage()->render('cli', 'about.description');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Natan Felles
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Languages/en/cli.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | return [
11 | 'about.description' => 'Shows information about this command-line interface.',
12 | 'about.line1' => 'About',
13 | 'about.line2' => 'This interface is built on Aplus Framework CLI Library.',
14 | 'about.line3' => 'Aplus Framework CLI Library is Open Source Software (OSS).',
15 | 'about.line4' => 'Visit our website to know more: https://aplus-framework.com',
16 | 'about.line5' => 'Thanks for using Aplus Framework!',
17 | 'command' => 'Command',
18 | 'commandNotFound' => 'Command not found: "{0}"',
19 | 'commands' => 'Commands',
20 | 'description' => 'Description',
21 | 'friend' => 'friend',
22 | 'greet.afternoon' => 'Good afternoon, {0}!',
23 | 'greet.evening' => 'Good evening, {0}!',
24 | 'greet.morning' => 'Good morning, {0}!',
25 | 'help.description' => 'Shows command usage help.',
26 | 'index.description' => 'Shows commands list.',
27 | 'index.option.greet' => 'Shows greeting.',
28 | 'noDescription' => 'This command does not provide a description.',
29 | 'options' => 'Options',
30 | 'usage' => 'Usage',
31 | ];
32 |
--------------------------------------------------------------------------------
/src/Languages/pt-br/cli.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | return [
11 | 'about.description' => 'Mostra informações sobre essa interface de linha de comandos.',
12 | 'about.line1' => 'Sobre',
13 | 'about.line2' => 'Essa interface é construída sobre Aplus Framework CLI Library.',
14 | 'about.line3' => 'Aplus Framework CLI Library é Software de Código Aberto (OSS).',
15 | 'about.line4' => 'Visite nosso website para saber mais: https://aplus-framework.com',
16 | 'about.line5' => 'Obrigado por usar o Aplus Framework!',
17 | 'command' => 'Comando',
18 | 'commandNotFound' => 'Comando não encontrado: "{0}"',
19 | 'commands' => 'Comandos',
20 | 'description' => 'Descrição',
21 | 'friend' => 'amigo',
22 | 'greet.afternoon' => 'Boa tarde, {0}!',
23 | 'greet.evening' => 'Boa noite, {0}!',
24 | 'greet.morning' => 'Bom dia, {0}!',
25 | 'help.description' => 'Mostra a ajuda de uso do comando.',
26 | 'index.description' => 'Mostra a lista de comandos.',
27 | 'index.option.greet' => 'Mostra saudação.',
28 | 'noDescription' => 'Este comando não fornece uma descrição.',
29 | 'options' => 'Opções',
30 | 'usage' => 'Uso',
31 | ];
32 |
--------------------------------------------------------------------------------
/src/Languages/es/cli.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | return [
11 | 'about.description' => 'Muestra información sobre esta interfaz de línea de comandos.',
12 | 'about.line1' => 'Sobre',
13 | 'about.line2' => 'Esta interfaz se basa en Aplus Framework CLI Library.',
14 | 'about.line3' => 'Aplus Framework CLI Library es Software de Código Abierto (OSS).',
15 | 'about.line4' => 'Visite nuestro website para obtener más información: https://aplus-framework.com',
16 | 'about.line5' => '¡Gracias por usar Aplus Framework!',
17 | 'command' => 'Comando',
18 | 'commandNotFound' => 'Comando no encontrado: "{0}"',
19 | 'commands' => 'Comandos',
20 | 'description' => 'Descripción',
21 | 'friend' => 'amigo',
22 | 'greet.afternoon' => '¡Buenas tardes, {0}!',
23 | 'greet.evening' => '¡Buenas noches, {0}!',
24 | 'greet.morning' => '¡Buenos días, {0}!',
25 | 'help.description' => 'Muestra ayuda de uso de comando.',
26 | 'index.description' => 'Muestra la lista de comandos.',
27 | 'index.option.greet' => 'Muestra saludo.',
28 | 'noDescription' => 'Este comando no proporciona una descripción.',
29 | 'options' => 'Opciones',
30 | 'usage' => 'Uso',
31 | ];
32 |
--------------------------------------------------------------------------------
/src/Streams/FilterStream.php:
--------------------------------------------------------------------------------
1 |
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\Streams;
11 |
12 | use JetBrains\PhpStorm\Pure;
13 |
14 | /**
15 | * Trait FilterStream.
16 | *
17 | * @package cli
18 | *
19 | * @since 2.3.1
20 | */
21 | trait FilterStream
22 | {
23 | protected static string $contents = '';
24 |
25 | /**
26 | * @param resource $in
27 | * @param resource $out
28 | * @param int $consumed
29 | * @param bool $closing
30 | *
31 | * @see https://php.net/manual/en/php-user-filter.filter.php
32 | *
33 | * @return int
34 | */
35 | public function filter($in, $out, &$consumed, $closing) : int
36 | {
37 | while ($bucket = \stream_bucket_make_writeable($in)) {
38 | static::$contents .= $bucket->data;
39 | $consumed += $bucket->datalen;
40 | \stream_bucket_append($out, $bucket);
41 | }
42 | return \PSFS_FEED_ME;
43 | }
44 |
45 | #[Pure]
46 | public static function getContents() : string
47 | {
48 | return static::$contents;
49 | }
50 |
51 | public static function reset() : void
52 | {
53 | static::$contents = '';
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Streams/Stream.php:
--------------------------------------------------------------------------------
1 |
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\Streams;
11 |
12 | use JetBrains\PhpStorm\Pure;
13 |
14 | /**
15 | * Class Stream.
16 | *
17 | * @package cli
18 | *
19 | * @deprecated
20 | *
21 | * @codeCoverageIgnore
22 | */
23 | abstract class Stream extends \php_user_filter
24 | {
25 | protected static string $contents = '';
26 |
27 | /**
28 | * @param resource $in
29 | * @param resource $out
30 | * @param int $consumed
31 | * @param bool $closing
32 | *
33 | * @see https://php.net/manual/en/php-user-filter.filter.php
34 | *
35 | * @return int
36 | */
37 | public function filter($in, $out, &$consumed, $closing) : int
38 | {
39 | while ($bucket = \stream_bucket_make_writeable($in)) {
40 | static::$contents .= $bucket->data;
41 | $consumed += $bucket->datalen;
42 | \stream_bucket_append($out, $bucket);
43 | }
44 | return \PSFS_FEED_ME;
45 | }
46 |
47 | abstract public static function init() : void;
48 |
49 | #[Pure]
50 | public static function getContents() : string
51 | {
52 | return static::$contents;
53 | }
54 |
55 | public static function reset() : void
56 | {
57 | static::$contents = '';
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aplus/cli",
3 | "description": "Aplus Framework CLI Library",
4 | "license": "MIT",
5 | "type": "library",
6 | "keywords": [
7 | "cli",
8 | "console",
9 | "terminal",
10 | "cron",
11 | "command",
12 | "commands"
13 | ],
14 | "authors": [
15 | {
16 | "name": "Natan Felles",
17 | "email": "natanfelles@gmail.com",
18 | "homepage": "https://natanfelles.github.io"
19 | }
20 | ],
21 | "homepage": "https://aplus-framework.com/packages/cli",
22 | "support": {
23 | "email": "support@aplus-framework.com",
24 | "issues": "https://github.com/aplus-framework/cli/issues",
25 | "forum": "https://aplus-framework.com/forum",
26 | "source": "https://github.com/aplus-framework/cli",
27 | "docs": "https://docs.aplus-framework.com/guides/libraries/cli/"
28 | },
29 | "funding": [
30 | {
31 | "type": "Aplus Sponsor",
32 | "url": "https://aplus-framework.com/sponsor"
33 | }
34 | ],
35 | "require": {
36 | "php": ">=8.1",
37 | "ext-posix": "*",
38 | "aplus/language": "^3.0"
39 | },
40 | "require-dev": {
41 | "ext-xdebug": "*",
42 | "aplus/coding-standard": "^1.14",
43 | "ergebnis/composer-normalize": "^2.25",
44 | "jetbrains/phpstorm-attributes": "^1.0",
45 | "phpmd/phpmd": "^2.13",
46 | "phpstan/phpstan": "^1.7",
47 | "phpunit/phpunit": "^9.5"
48 | },
49 | "minimum-stability": "dev",
50 | "prefer-stable": true,
51 | "autoload": {
52 | "psr-4": {
53 | "Framework\\CLI\\": "src/"
54 | }
55 | },
56 | "autoload-dev": {
57 | "psr-4": {
58 | "Tests\\CLI\\": "tests/"
59 | }
60 | },
61 | "config": {
62 | "allow-plugins": {
63 | "ergebnis/composer-normalize": true
64 | },
65 | "optimize-autoloader": true,
66 | "preferred-install": "dist",
67 | "sort-packages": true
68 | },
69 | "extra": {
70 | "branch-alias": {
71 | "dev-master": "2.x-dev"
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Commands/Help.php:
--------------------------------------------------------------------------------
1 |
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\Commands;
11 |
12 | use Framework\CLI\CLI;
13 | use Framework\CLI\Command;
14 |
15 | /**
16 | * Class Help.
17 | *
18 | * @package cli
19 | */
20 | class Help extends Command
21 | {
22 | protected string $name = 'help';
23 | protected string $usage = 'help [command_name]';
24 |
25 | public function run() : void
26 | {
27 | $command = $this->console->getArgument(0) ?? 'help';
28 | $this->showCommand($command);
29 | }
30 |
31 | protected function showCommand(string $commandName) : void
32 | {
33 | $command = $this->console->getCommand($commandName);
34 | if ($command === null) {
35 | CLI::error(
36 | $this->console->getLanguage()->render('cli', 'commandNotFound', [$commandName])
37 | );
38 | }
39 | CLI::write(CLI::style(
40 | $this->console->getLanguage()->render('cli', 'command') . ': ',
41 | CLI::FG_GREEN
42 | ) . $command->getName());
43 | $value = $command->getDescription();
44 | if ($value !== '') {
45 | CLI::write(CLI::style(
46 | $this->console->getLanguage()->render('cli', 'description') . ': ',
47 | CLI::FG_GREEN
48 | ) . $value);
49 | }
50 | $value = $command->getUsage();
51 | if ($value !== '') {
52 | CLI::write(CLI::style(
53 | $this->console->getLanguage()->render('cli', 'usage') . ': ',
54 | CLI::FG_GREEN
55 | ) . $value);
56 | }
57 | $value = $command->getOptions();
58 | if ($value) {
59 | CLI::write(
60 | $this->console->getLanguage()->render('cli', 'options') . ': ',
61 | CLI::FG_GREEN
62 | );
63 | $lastKey = \array_key_last($value);
64 | foreach ($value as $option => $description) {
65 | CLI::write(' ' . $this->renderOption($option));
66 | CLI::write(' ' . $description);
67 | if ($option !== $lastKey) {
68 | CLI::newLine();
69 | }
70 | }
71 | }
72 | }
73 |
74 | protected function renderOption(string $text) : string
75 | {
76 | $text = \trim(\preg_replace('/\s+/', '', $text));
77 | $text = \explode(',', $text);
78 | \sort($text);
79 | foreach ($text as &$item) {
80 | $item = CLI::style($item, CLI::FG_YELLOW);
81 | }
82 | return \implode(', ', $text);
83 | }
84 |
85 | public function getDescription() : string
86 | {
87 | return $this->console->getLanguage()->render('cli', 'help.description');
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 |
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 PHPSTORM_META;
11 |
12 | registerArgumentsSet(
13 | 'cli_foreground_colors',
14 | \Framework\CLI\CLI::FG_BLACK,
15 | \Framework\CLI\CLI::FG_BLUE,
16 | \Framework\CLI\CLI::FG_BRIGHT_BLACK,
17 | \Framework\CLI\CLI::FG_BRIGHT_BLUE,
18 | \Framework\CLI\CLI::FG_BRIGHT_CYAN,
19 | \Framework\CLI\CLI::FG_BRIGHT_GREEN,
20 | \Framework\CLI\CLI::FG_BRIGHT_MAGENTA,
21 | \Framework\CLI\CLI::FG_BRIGHT_RED,
22 | \Framework\CLI\CLI::FG_BRIGHT_WHITE,
23 | \Framework\CLI\CLI::FG_BRIGHT_YELLOW,
24 | \Framework\CLI\CLI::FG_CYAN,
25 | \Framework\CLI\CLI::FG_GREEN,
26 | \Framework\CLI\CLI::FG_MAGENTA,
27 | \Framework\CLI\CLI::FG_RED,
28 | \Framework\CLI\CLI::FG_WHITE,
29 | \Framework\CLI\CLI::FG_YELLOW,
30 | 'black',
31 | 'blue',
32 | 'bright_black',
33 | 'bright_blue',
34 | 'bright_cyan',
35 | 'bright_green',
36 | 'bright_magenta',
37 | 'bright_red',
38 | 'bright_white',
39 | 'bright_yellow',
40 | 'cyan',
41 | 'green',
42 | 'magenta',
43 | 'red',
44 | 'white',
45 | 'yellow',
46 | );
47 | registerArgumentsSet(
48 | 'cli_background_colors',
49 | \Framework\CLI\CLI::BG_BLACK,
50 | \Framework\CLI\CLI::BG_BLUE,
51 | \Framework\CLI\CLI::BG_BRIGHT_BLACK,
52 | \Framework\CLI\CLI::BG_BRIGHT_BLUE,
53 | \Framework\CLI\CLI::BG_BRIGHT_CYAN,
54 | \Framework\CLI\CLI::BG_BRIGHT_GREEN,
55 | \Framework\CLI\CLI::BG_BRIGHT_MAGENTA,
56 | \Framework\CLI\CLI::BG_BRIGHT_RED,
57 | \Framework\CLI\CLI::BG_BRIGHT_YELLOW,
58 | \Framework\CLI\CLI::BG_CYAN,
59 | \Framework\CLI\CLI::BG_GREEN,
60 | \Framework\CLI\CLI::BG_MAGENTA,
61 | \Framework\CLI\CLI::BG_RED,
62 | \Framework\CLI\CLI::BG_WHITE,
63 | \Framework\CLI\CLI::BG_YELLOW,
64 | 'black',
65 | 'blue',
66 | 'bright_black',
67 | 'bright_blue',
68 | 'bright_cyan',
69 | 'bright_green',
70 | 'bright_magenta',
71 | 'bright_red',
72 | 'bright_yellow',
73 | 'cyan',
74 | 'green',
75 | 'magenta',
76 | 'red',
77 | 'white',
78 | 'yellow',
79 | );
80 | registerArgumentsSet(
81 | 'cli_formats',
82 | \Framework\CLI\CLI::FM_BOLD,
83 | \Framework\CLI\CLI::FM_CONCEAL,
84 | \Framework\CLI\CLI::FM_CROSSED_OUT,
85 | \Framework\CLI\CLI::FM_DOUBLY_UNDERLINE,
86 | \Framework\CLI\CLI::FM_ENCIRCLED,
87 | \Framework\CLI\CLI::FM_FAINT,
88 | \Framework\CLI\CLI::FM_FRAKTUR,
89 | \Framework\CLI\CLI::FM_ITALIC,
90 | \Framework\CLI\CLI::FM_PRIMARY_FONT,
91 | \Framework\CLI\CLI::FM_RAPID_BLINK,
92 | \Framework\CLI\CLI::FM_REVERSE_VIDEO,
93 | \Framework\CLI\CLI::FM_SLOW_BLINK,
94 | \Framework\CLI\CLI::FM_UNDERLINE,
95 | 'bold',
96 | 'conceal',
97 | 'crossed_out',
98 | 'doubly_underline',
99 | 'encircled',
100 | 'faint',
101 | 'fraktur',
102 | 'italic',
103 | 'primary_font',
104 | 'rapid_blink',
105 | 'reverse_video',
106 | 'slow_blink',
107 | 'underline',
108 | );
109 | expectedArguments(
110 | \Framework\CLI\CLI::box(),
111 | 1,
112 | argumentsSet('cli_background_colors')
113 | );
114 | expectedArguments(
115 | \Framework\CLI\CLI::box(),
116 | 2,
117 | argumentsSet('cli_foreground_colors')
118 | );
119 | expectedArguments(
120 | \Framework\CLI\CLI::write(),
121 | 1,
122 | argumentsSet('cli_foreground_colors')
123 | );
124 | expectedArguments(
125 | \Framework\CLI\CLI::write(),
126 | 2,
127 | argumentsSet('cli_background_colors')
128 | );
129 | expectedArguments(
130 | \Framework\CLI\CLI::style(),
131 | 1,
132 | argumentsSet('cli_foreground_colors')
133 | );
134 | expectedArguments(
135 | \Framework\CLI\CLI::style(),
136 | 2,
137 | argumentsSet('cli_background_colors')
138 | );
139 | /*expectedArguments(
140 | \Framework\CLI\CLI::style(),
141 | 3,
142 | argumentsSet('cli_formats')
143 | );*/
144 |
--------------------------------------------------------------------------------
/src/Commands/Index.php:
--------------------------------------------------------------------------------
1 |
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\Commands;
11 |
12 | use Framework\CLI\CLI;
13 | use Framework\CLI\Command;
14 |
15 | /**
16 | * Class Index.
17 | *
18 | * @package cli
19 | */
20 | class Index extends Command
21 | {
22 | protected string $name = 'index';
23 | protected string $description = 'Show commands list';
24 | protected string $usage = 'index';
25 | protected array $options = [
26 | '-g' => 'Shows greeting.',
27 | ];
28 |
29 | public function run() : void
30 | {
31 | $this->showHeader();
32 | $this->showDate();
33 | if ($this->console->getOption('g')) {
34 | $this->greet();
35 | }
36 | $this->listCommands();
37 | }
38 |
39 | public function getDescription() : string
40 | {
41 | return $this->console->getLanguage()->render('cli', 'index.description');
42 | }
43 |
44 | public function getOptions() : array
45 | {
46 | return [
47 | '-g' => $this->console->getLanguage()->render('cli', 'index.option.greet'),
48 | ];
49 | }
50 |
51 | protected function listCommands() : void
52 | {
53 | $width = 0;
54 | $lengths = [];
55 | foreach (\array_keys($this->console->getCommands()) as $name) {
56 | $lengths[$name] = \mb_strlen($name);
57 | if ($lengths[$name] > $width) {
58 | $width = $lengths[$name];
59 | }
60 | }
61 | CLI::write(
62 | $this->console->getLanguage()->render('cli', 'commands') . ':',
63 | CLI::FG_YELLOW
64 | );
65 | foreach ($this->console->getCommands() as $name => $command) {
66 | CLI::write(
67 | ' ' . CLI::style($name, CLI::FG_GREEN) . ' '
68 | . \str_repeat(' ', $width - $lengths[$name])
69 | . $command->getDescription()
70 | );
71 | }
72 | }
73 |
74 | protected function showHeader() : void
75 | {
76 | $text = <<<'EOL'
77 | _ _ ____ _ ___
78 | / \ _ __ | |_ _ ___ / ___| | |_ _|
79 | / _ \ | '_ \| | | | / __| | | | | | |
80 | / ___ \| |_) | | |_| \__ \ | |___| |___ | |
81 | /_/ \_\ .__/|_|\__,_|___/ \____|_____|___|
82 | |_|
83 |
84 | EOL;
85 | CLI::write($text, CLI::FG_GREEN);
86 | }
87 |
88 | protected function showDate() : void
89 | {
90 | $text = $this->console->getLanguage()->date(\time(), 'full');
91 | $text = \ucfirst($text) . ' - '
92 | . \date('H:i:s') . ' - '
93 | . \date_default_timezone_get() . \PHP_EOL;
94 | CLI::write($text);
95 | }
96 |
97 | protected function greet() : void
98 | {
99 | $hour = \date('H');
100 | $timing = 'evening';
101 | if ($hour > 4 && $hour < 12) {
102 | $timing = 'morning';
103 | } elseif ($hour > 4 && $hour < 18) {
104 | $timing = 'afternoon';
105 | }
106 | $greeting = $this->console->getLanguage()
107 | ->render('cli', 'greet.' . $timing, [$this->getUser()]);
108 | CLI::write($greeting);
109 | CLI::newLine();
110 | }
111 |
112 | protected function getUser() : string
113 | {
114 | $username = \posix_getlogin();
115 | if ($username === false) {
116 | return $this->console->getLanguage()->render('cli', 'friend');
117 | }
118 | $info = \posix_getpwnam($username);
119 | if ( ! $info) {
120 | return $username;
121 | }
122 | $gecos = $info['gecos'] ?? '';
123 | if ( ! $gecos) {
124 | return $username;
125 | }
126 | $length = \strpos($gecos, ',') ?: \strlen($gecos);
127 | return \substr($gecos, 0, $length);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Command.php:
--------------------------------------------------------------------------------
1 |
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 JetBrains\PhpStorm\Pure;
13 |
14 | /**
15 | * Class Command.
16 | *
17 | * @package cli
18 | */
19 | abstract class Command
20 | {
21 | /**
22 | * Console instance of the current command.
23 | */
24 | protected Console $console;
25 | /**
26 | * Command name.
27 | */
28 | protected string $name;
29 | /**
30 | * Command description.
31 | */
32 | protected string $description;
33 | /**
34 | * Command usage.
35 | */
36 | protected string $usage = 'command [options] -- [arguments]';
37 | /**
38 | * Command options.
39 | *
40 | * @var array
41 | */
42 | protected array $options = [];
43 | /**
44 | * Tells if command is active.
45 | */
46 | protected bool $active = true;
47 |
48 | /**
49 | * Command constructor.
50 | *
51 | * @param Console|null $console
52 | */
53 | public function __construct(Console $console = null)
54 | {
55 | if ($console) {
56 | $this->console = $console;
57 | }
58 | }
59 |
60 | /**
61 | * Run the command.
62 | */
63 | abstract public function run() : void;
64 |
65 | /**
66 | * Get console instance.
67 | *
68 | * @return Console
69 | */
70 | public function getConsole() : Console
71 | {
72 | return $this->console;
73 | }
74 |
75 | /**
76 | * Set console instance.
77 | *
78 | * @param Console $console
79 | *
80 | * @return static
81 | */
82 | public function setConsole(Console $console) : static
83 | {
84 | $this->console = $console;
85 | return $this;
86 | }
87 |
88 | /**
89 | * Get command name.
90 | *
91 | * @return string
92 | */
93 | public function getName() : string
94 | {
95 | if (isset($this->name)) {
96 | return $this->name;
97 | }
98 | $name = static::class;
99 | $pos = \strrpos($name, '\\');
100 | if ($pos !== false) {
101 | $name = \substr($name, $pos + 1);
102 | }
103 | if (\str_ends_with($name, 'Command')) {
104 | $name = \substr($name, 0, -7);
105 | }
106 | $name = \strtolower($name);
107 | return $this->name = $name;
108 | }
109 |
110 | /**
111 | * Set command name.
112 | *
113 | * @param string $name
114 | *
115 | * @return static
116 | */
117 | public function setName(string $name) : static
118 | {
119 | $this->name = $name;
120 | return $this;
121 | }
122 |
123 | /**
124 | * Get command description.
125 | *
126 | * @return string
127 | */
128 | public function getDescription() : string
129 | {
130 | if (isset($this->description)) {
131 | return $this->description;
132 | }
133 | $description = $this->console->getLanguage()->render('cli', 'noDescription');
134 | return $this->description = $description;
135 | }
136 |
137 | /**
138 | * Set command description.
139 | *
140 | * @param string $description
141 | *
142 | * @return static
143 | */
144 | public function setDescription(string $description) : static
145 | {
146 | $this->description = $description;
147 | return $this;
148 | }
149 |
150 | /**
151 | * Get command usage.
152 | *
153 | * @return string
154 | */
155 | #[Pure]
156 | public function getUsage() : string
157 | {
158 | return $this->usage;
159 | }
160 |
161 | /**
162 | * Set command usage.
163 | *
164 | * @param string $usage
165 | *
166 | * @return static
167 | */
168 | public function setUsage(string $usage) : static
169 | {
170 | $this->usage = $usage;
171 | return $this;
172 | }
173 |
174 | /**
175 | * Get command options.
176 | *
177 | * @return array
178 | */
179 | #[Pure]
180 | public function getOptions() : array
181 | {
182 | return $this->options;
183 | }
184 |
185 | /**
186 | * Set command options.
187 | *
188 | * @param array $options
189 | *
190 | * @return static
191 | */
192 | public function setOptions(array $options) : static
193 | {
194 | $this->options = $options;
195 | return $this;
196 | }
197 |
198 | /**
199 | * Tells if the command is active.
200 | *
201 | * @return bool
202 | */
203 | #[Pure]
204 | public function isActive() : bool
205 | {
206 | return $this->active;
207 | }
208 |
209 | /**
210 | * Activate the command.
211 | *
212 | * @return static
213 | */
214 | public function activate() : static
215 | {
216 | $this->active = true;
217 | return $this;
218 | }
219 |
220 | /**
221 | * Deactivate the command.
222 | *
223 | * @return static
224 | */
225 | public function deactivate() : static
226 | {
227 | $this->active = false;
228 | return $this;
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/Console.php:
--------------------------------------------------------------------------------
1 |
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\Commands\About;
13 | use Framework\CLI\Commands\Help;
14 | use Framework\CLI\Commands\Index;
15 | use Framework\Language\Language;
16 | use JetBrains\PhpStorm\Pure;
17 |
18 | /**
19 | * Class Console.
20 | *
21 | * @package cli
22 | */
23 | class Console
24 | {
25 | /**
26 | * List of commands.
27 | *
28 | * @var array The command name as key and the object as value
29 | */
30 | protected array $commands = [];
31 | /**
32 | * The current command name.
33 | */
34 | protected string $command = '';
35 | /**
36 | * Input options.
37 | *
38 | * @var array The option value as string or TRUE if it
39 | * was passed without a value
40 | */
41 | protected array $options = [];
42 | /**
43 | * Input arguments.
44 | *
45 | * @var array
46 | */
47 | protected array $arguments = [];
48 | /**
49 | * The Language instance.
50 | */
51 | protected Language $language;
52 |
53 | /**
54 | * Console constructor.
55 | *
56 | * @param Language|null $language
57 | */
58 | public function __construct(Language $language = null)
59 | {
60 | if ($language === null) {
61 | $language = new Language('en');
62 | }
63 | $this->language = $language->addDirectory(__DIR__ . '/Languages');
64 | global $argv;
65 | $this->prepare($argv ?? []);
66 | $this->setDefaultCommands();
67 | }
68 |
69 | protected function setDefaultCommands() : static
70 | {
71 | if ($this->getCommand('index') === null) {
72 | $this->addCommand(new Index($this));
73 | }
74 | if ($this->getCommand('help') === null) {
75 | $this->addCommand(new Help($this));
76 | }
77 | if ($this->getCommand('about') === null) {
78 | $this->addCommand(new About($this));
79 | }
80 | return $this;
81 | }
82 |
83 | /**
84 | * Get all CLI options.
85 | *
86 | * @return array
87 | */
88 | #[Pure]
89 | public function getOptions() : array
90 | {
91 | return $this->options;
92 | }
93 |
94 | /**
95 | * Get a specific option or null.
96 | *
97 | * @param string $option
98 | *
99 | * @return bool|string|null The option value as string, TRUE if it
100 | * was passed without a value or NULL if the option was not set
101 | */
102 | #[Pure]
103 | public function getOption(string $option) : bool | string | null
104 | {
105 | return $this->options[$option] ?? null;
106 | }
107 |
108 | /**
109 | * Get all arguments.
110 | *
111 | * @return array
112 | */
113 | #[Pure]
114 | public function getArguments() : array
115 | {
116 | return $this->arguments;
117 | }
118 |
119 | /**
120 | * Get a specific argument or null.
121 | *
122 | * @param int $position Argument position, starting from zero
123 | *
124 | * @return string|null The argument value or null if it was not set
125 | */
126 | #[Pure]
127 | public function getArgument(int $position) : ?string
128 | {
129 | return $this->arguments[$position] ?? null;
130 | }
131 |
132 | /**
133 | * Get the Language instance.
134 | *
135 | * @return Language
136 | */
137 | #[Pure]
138 | public function getLanguage() : Language
139 | {
140 | return $this->language;
141 | }
142 |
143 | /**
144 | * Add a command to the console.
145 | *
146 | * @param class-string|Command $command A Command instance or the class FQN
147 | *
148 | * @return static
149 | */
150 | public function addCommand(Command | string $command) : static
151 | {
152 | if (\is_string($command)) {
153 | $command = new $command();
154 | }
155 | $command->setConsole($this);
156 | $this->commands[$command->getName()] = $command;
157 | return $this;
158 | }
159 |
160 | /**
161 | * Add many commands to the console.
162 | *
163 | * @param array|Command> $commands A list of Command
164 | * instances or the classes FQN
165 | *
166 | * @return static
167 | */
168 | public function addCommands(array $commands) : static
169 | {
170 | foreach ($commands as $command) {
171 | $this->addCommand($command);
172 | }
173 | return $this;
174 | }
175 |
176 | /**
177 | * Get an active command.
178 | *
179 | * @param string $name Command name
180 | *
181 | * @return Command|null The Command on success or null if not found
182 | */
183 | public function getCommand(string $name) : ?Command
184 | {
185 | if (isset($this->commands[$name]) && $this->commands[$name]->isActive()) {
186 | return $this->commands[$name];
187 | }
188 | return null;
189 | }
190 |
191 | /**
192 | * Get a list of active commands.
193 | *
194 | * @return array
195 | */
196 | public function getCommands() : array
197 | {
198 | $commands = $this->commands;
199 | foreach ($commands as $name => $command) {
200 | if ( ! $command->isActive()) {
201 | unset($commands[$name]);
202 | }
203 | }
204 | \ksort($commands);
205 | return $commands;
206 | }
207 |
208 | /**
209 | * Remove a command.
210 | *
211 | * @param string $name Command name
212 | *
213 | * @return static
214 | */
215 | public function removeCommand(string $name) : static
216 | {
217 | unset($this->commands[$name]);
218 | return $this;
219 | }
220 |
221 | /**
222 | * Remove commands.
223 | *
224 | * @param array $names Command names
225 | *
226 | * @return static
227 | */
228 | public function removeCommands(array $names) : static
229 | {
230 | foreach ($names as $name) {
231 | $this->removeCommand($name);
232 | }
233 | return $this;
234 | }
235 |
236 | /**
237 | * Tells if it has a command.
238 | *
239 | * @param string $name Command name
240 | *
241 | * @return bool
242 | */
243 | public function hasCommand(string $name) : bool
244 | {
245 | return $this->getCommand($name) !== null;
246 | }
247 |
248 | /**
249 | * Run the Console.
250 | */
251 | public function run() : void
252 | {
253 | if ($this->command === '') {
254 | $this->command = 'index';
255 | }
256 | $command = $this->getCommand($this->command);
257 | if ($command === null) {
258 | CLI::error(CLI::style(
259 | $this->getLanguage()->render('cli', 'commandNotFound', [$this->command]),
260 | CLI::FG_BRIGHT_RED
261 | ), \defined('TESTING') ? null : 1);
262 | return;
263 | }
264 | $command->run();
265 | }
266 |
267 | public function exec(string $command) : void
268 | {
269 | $argumentValues = static::commandToArgs($command);
270 | \array_unshift($argumentValues, 'removed');
271 | $this->prepare($argumentValues);
272 | $this->run();
273 | }
274 |
275 | protected function reset() : void
276 | {
277 | $this->command = '';
278 | $this->options = [];
279 | $this->arguments = [];
280 | }
281 |
282 | /**
283 | * Prepare information of the command line.
284 | *
285 | * [options] [arguments] [options]
286 | * [options] -- [arguments]
287 | * [command]
288 | * [command] [options] [arguments] [options]
289 | * [command] [options] -- [arguments]
290 | * Short option: -l, -la === l = true, a = true
291 | * Long option: --list, --all=vertical === list = true, all = vertical
292 | * Only Long Options receive values:
293 | * --foo=bar or --f=bar - "foo" and "f" are bar
294 | * -foo=bar or -f=bar - all characters are true (f, o, =, b, a, r)
295 | * After -- all values are arguments, also if is prefixed with -
296 | * Without --, arguments and options can be mixed: -ls foo -x abc --a=e.
297 | *
298 | * @param array $argumentValues
299 | */
300 | protected function prepare(array $argumentValues) : void
301 | {
302 | $this->reset();
303 | unset($argumentValues[0]);
304 | if (isset($argumentValues[1]) && $argumentValues[1][0] !== '-') {
305 | $this->command = $argumentValues[1];
306 | unset($argumentValues[1]);
307 | }
308 | $endOptions = false;
309 | foreach ($argumentValues as $value) {
310 | if ($endOptions === false && $value === '--') {
311 | $endOptions = true;
312 | continue;
313 | }
314 | if ($endOptions === false && $value[0] === '-') {
315 | if (isset($value[1]) && $value[1] === '-') {
316 | $option = \substr($value, 2);
317 | if (\str_contains($option, '=')) {
318 | [$option, $value] = \explode('=', $option, 2);
319 | $this->options[$option] = $value;
320 | continue;
321 | }
322 | $this->options[$option] = true;
323 | continue;
324 | }
325 | foreach (\str_split(\substr($value, 1)) as $item) {
326 | $this->options[$item] = true;
327 | }
328 | continue;
329 | }
330 | //$endOptions = true;
331 | $this->arguments[] = $value;
332 | }
333 | }
334 |
335 | /**
336 | * @param string $command
337 | *
338 | * @see https://someguyjeremy.com/2017/07/adventures-in-parsing-strings-to-argv-in-php.html
339 | *
340 | * @return array
341 | */
342 | #[Pure]
343 | public static function commandToArgs(string $command) : array
344 | {
345 | $charCount = \strlen($command);
346 | $argv = [];
347 | $arg = '';
348 | $inDQuote = false;
349 | $inSQuote = false;
350 | for ($i = 0; $i < $charCount; $i++) {
351 | $char = $command[$i];
352 | if ($char === ' ' && ! $inDQuote && ! $inSQuote) {
353 | if ($arg !== '') {
354 | $argv[] = $arg;
355 | }
356 | $arg = '';
357 | continue;
358 | }
359 | if ($inSQuote && $char === "'") {
360 | $inSQuote = false;
361 | continue;
362 | }
363 | if ($inDQuote && $char === '"') {
364 | $inDQuote = false;
365 | continue;
366 | }
367 | if ($char === '"' && ! $inSQuote) {
368 | $inDQuote = true;
369 | continue;
370 | }
371 | if ($char === "'" && ! $inDQuote) {
372 | $inSQuote = true;
373 | continue;
374 | }
375 | $arg .= $char;
376 | }
377 | $argv[] = $arg;
378 | return $argv;
379 | }
380 | }
381 |
--------------------------------------------------------------------------------
/src/CLI.php:
--------------------------------------------------------------------------------
1 |
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
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
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
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';
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()) {
371 | return $default;
372 | }
373 | $width = (int) \shell_exec('tput cols');
374 | if ( ! $width) {
375 | return $default;
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();
391 | return \wordwrap($text, $width, \PHP_EOL, true);
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 = [];
404 | foreach (static::$foregroundColors as $color) {
405 | $codes[] = $color;
406 | }
407 | foreach (static::$backgroundColors as $background) {
408 | $codes[] = $background;
409 | }
410 | foreach (static::$formats as $format) {
411 | $codes[] = $format;
412 | }
413 | $codes[] = static::$reset;
414 | $text = \str_replace($codes, '', $text);
415 | return \mb_strlen($text);
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 $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 = '';
437 | if ($color !== null) {
438 | if (empty(static::$foregroundColors[$color])) {
439 | throw new InvalidArgumentException('Invalid color: ' . $color);
440 | }
441 | $string = static::$foregroundColors[$color];
442 | }
443 | if ($background !== null) {
444 | if (empty(static::$backgroundColors[$background])) {
445 | throw new InvalidArgumentException('Invalid background color: ' . $background);
446 | }
447 | $string .= static::$backgroundColors[$background];
448 | }
449 | if ($formats) {
450 | foreach ($formats as $format) {
451 | if (empty(static::$formats[$format])) {
452 | throw new InvalidArgumentException('Invalid format: ' . $format);
453 | }
454 | $string .= static::$formats[$format];
455 | }
456 | }
457 | $string .= $text . static::$reset;
458 | return $string;
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) {
478 | $text = static::wrap($text, $width);
479 | }
480 | if ($color !== null || $background !== null) {
481 | $text = static::style($text, $color, $background);
482 | }
483 | \fwrite(\STDOUT, $text . \PHP_EOL);
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++) {
494 | \fwrite(\STDOUT, \PHP_EOL);
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 = '';
512 | if ( ! static::isWindows()) {
513 | $string .= "\33[2K";
514 | }
515 | $string .= "\r";
516 | $string .= $text;
517 | if ($finalize) {
518 | $string .= \PHP_EOL;
519 | }
520 | \fwrite(\STDOUT, $string);
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++) {
532 | \fwrite(\STDOUT, "\x07");
533 | \usleep($usleep);
534 | }
535 | }
536 |
537 | /**
538 | * Writes a message box.
539 | *
540 | * @param array|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();
550 | $width -= 2;
551 | if ( ! \is_array($lines)) {
552 | $lines = [
553 | $lines,
554 | ];
555 | }
556 | $allLines = [];
557 | foreach ($lines as &$line) {
558 | $length = static::strlen($line);
559 | if ($length > $width) {
560 | $line = static::wrap($line, $width);
561 | }
562 | foreach (\explode(\PHP_EOL, $line) as $subLine) {
563 | $allLines[] = $subLine;
564 | }
565 | }
566 | unset($line);
567 | $blankLine = \str_repeat(' ', $width + 2);
568 | $text = static::style($blankLine, $color, $background);
569 | foreach ($allLines as $line) {
570 | $end = \str_repeat(' ', $width - static::strlen($line)) . ' ';
571 | $end = static::style($end, $color, $background);
572 | $text .= static::style(' ' . $line . $end, $color, $background);
573 | }
574 | $text .= static::style($blankLine, $color, $background);
575 | static::write($text);
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();
587 | \fwrite(\STDERR, static::style($message, static::FG_RED) . \PHP_EOL);
588 | if ($exitCode !== null) {
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");
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|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> $tbody Table body rows
684 | * @param array $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 = [];
690 | // We need only indexes and not keys
691 | if ( ! empty($thead)) {
692 | $tableRows[] = \array_values($thead);
693 | }
694 | foreach ($tbody as $tr) {
695 | // cast tr to array if is not - (objects...)
696 | $tableRows[] = \array_values((array) $tr);
697 | }
698 | // Yes, it really is necessary to know this count
699 | $totalRows = \count($tableRows);
700 | // Store all columns lengths
701 | // $allColsLengths[row][column] = length
702 | $allColsLengths = [];
703 | // Store maximum lengths by column
704 | // $maxColsLengths[column] = length
705 | $maxColsLengths = [];
706 | // Read row by row and define the longest columns
707 | for ($row = 0; $row < $totalRows; $row++) {
708 | $column = 0; // Current column index
709 | foreach ($tableRows[$row] as $col) {
710 | // Sets the size of this column in the current row
711 | $allColsLengths[$row][$column] = static::strlen((string) $col);
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])
716 | || $allColsLengths[$row][$column] > $maxColsLengths[$column]) {
717 | $maxColsLengths[$column] = $allColsLengths[$row][$column];
718 | }
719 | // We can go check the size of the next column...
720 | $column++;
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++) {
726 | $column = 0;
727 | foreach ($tableRows[$row] as $col => $value) {
728 | $diff = $maxColsLengths[$column] - $allColsLengths[$row][$col];
729 | if ($diff) {
730 | $tableRows[$row][$column] .= \str_repeat(' ', $diff);
731 | }
732 | $column++;
733 | }
734 | }
735 | $table = $line = '';
736 | // Joins columns and append the well formatted rows to the table
737 | foreach ($tableRows as $row => $value) {
738 | // Set the table border-top
739 | if ($row === 0) {
740 | $line = '+';
741 | foreach (\array_keys($value) as $col) {
742 | $line .= \str_repeat('-', $maxColsLengths[$col] + 2) . '+';
743 | }
744 | $table .= $line . \PHP_EOL;
745 | }
746 | // Set the vertical borders
747 | $table .= '| ' . \implode(' | ', $value) . ' |' . \PHP_EOL;
748 | // Set the thead and table borders-bottom
749 | if (($row === 0 && ! empty($thead)) || $row + 1 === $totalRows) {
750 | $table .= $line . \PHP_EOL;
751 | }
752 | }
753 | \fwrite(\STDOUT, $table);
754 | }
755 | }
756 |
--------------------------------------------------------------------------------