├── 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 | Aplus Framework CLI Library 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 | [![tests](https://github.com/aplus-framework/cli/actions/workflows/tests.yml/badge.svg)](https://github.com/aplus-framework/cli/actions/workflows/tests.yml) 10 | [![coverage](https://coveralls.io/repos/github/aplus-framework/cli/badge.svg?branch=master)](https://coveralls.io/github/aplus-framework/cli?branch=master) 11 | [![packagist](https://img.shields.io/packagist/v/aplus/cli)](https://packagist.org/packages/aplus/cli) 12 | [![open-source](https://img.shields.io/badge/open--source-sponsor-magenta)](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 | --------------------------------------------------------------------------------