├── .github ├── FUNDING.yml └── workflows │ └── integration.yml ├── LICENSE ├── composer.json ├── phive.xml ├── phpstan.neon └── src ├── Command.php ├── Command ├── Executable.php ├── OutputFormatter.php ├── Result.php ├── Runner.php └── Runner │ ├── Result.php │ └── Simple.php ├── CommandLine.php ├── Output └── Util.php ├── Processor.php ├── Processor ├── ProcOpen.php └── Symfony.php ├── Reader.php ├── Reader ├── Abstraction.php └── StandardInput.php └── Util.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: sebastianfeldmann 2 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: "CI-Build" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | max-parallel: 3 10 | matrix: 11 | operating-system: [ubuntu-latest] 12 | php-versions: ['8.0', '8.1', '8.2', '8.3', '8.4'] 13 | steps: 14 | - uses: actions/checkout@master 15 | 16 | - name: Install PHP 17 | uses: shivammathur/setup-php@master 18 | with: 19 | php-version: ${{ matrix.php-versions }} 20 | extensions: xdebug 21 | 22 | - name: Validate composer.json and composer.lock 23 | run: composer validate 24 | 25 | - name: Install dependencies 26 | run: composer install --prefer-dist --no-progress --no-suggest 27 | 28 | - name: Install phive 29 | run: wget -O phive.phar https://phar.io/releases/phive.phar 30 | 31 | - name: Make phive executable 32 | run: chmod +x ./phive.phar 33 | 34 | - name: Install tooling 35 | run: ./phive.phar --no-progress --home ./.phive install --trust-gpg-keys 4AA394086372C20A,31C7E470E2138192,8E730BA25823D8B5,CF1A108D0E7AE720,A978220305CD5C32,51C67305FFC2E5C0 --force-accept-unsigned 36 | 37 | - name: Execute unit tests 38 | run: tools/phpunit 39 | 40 | - name: Check coding style 41 | run: tools/phpcs --standard=psr12 src tests 42 | 43 | - name: Static code analysis 44 | run: tools/phpstan analyse 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sebastian Feldmann 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sebastianfeldmann/cli", 3 | "description": "PHP cli helper classes", 4 | "type": "library", 5 | "keywords": ["cli"], 6 | "homepage": "https://github.com/sebastianfeldmann/cli", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Sebastian Feldmann", 11 | "email": "sf@sebastian-feldmann.info" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/sebastianfeldmann/cli/issues" 16 | }, 17 | "require": { 18 | "php": ">=7.2" 19 | }, 20 | "require-dev": { 21 | "symfony/process": "^4.3 | ^5.0 | ^6.0 | ^7.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "SebastianFeldmann\\Cli\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "SebastianFeldmann\\Cli\\": "tests/cli/" 31 | } 32 | }, 33 | "scripts": { 34 | "post-install-cmd": "tools/phive install", 35 | "test": "tools/phpunit", 36 | "static": "tools/phpstan analyse", 37 | "style": "tools/phpcs --standard=psr12 src tests" 38 | }, 39 | "extra": { 40 | "branch-alias": { 41 | "dev-master": "3.4.x-dev" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phive.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - %currentWorkingDirectory%/src/ 5 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli; 13 | 14 | /** 15 | * Class Command 16 | * 17 | * @package SebastianFeldmann\Cli 18 | * @author Sebastian Feldmann 19 | * @link https://github.com/sebastianfeldmann/cli 20 | * @since Class available since Release 0.9.0 21 | */ 22 | interface Command 23 | { 24 | /** 25 | * Get the cli command. 26 | * 27 | * @return string 28 | */ 29 | public function getCommand(): string; 30 | 31 | /** 32 | * Returns a list of exit codes that are valid. 33 | * 34 | * @return array 35 | */ 36 | public function getAcceptableExitCodes(): array; 37 | 38 | /** 39 | * Convert command to string 40 | * 41 | * @return string 42 | */ 43 | public function __toString(): string; 44 | } 45 | -------------------------------------------------------------------------------- /src/Command/Executable.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Command; 13 | 14 | use SebastianFeldmann\Cli\Command; 15 | use SebastianFeldmann\Cli\Util; 16 | 17 | /** 18 | * Class Executable 19 | * 20 | * @package SebastianFeldmann\Cli 21 | * @author Sebastian Feldmann 22 | * @link https://github.com/sebastianfeldmann/cli 23 | * @since Class available since Release 0.9.0 24 | */ 25 | class Executable implements Command 26 | { 27 | /** 28 | * Command name 29 | * 30 | * @var string 31 | */ 32 | private $cmd; 33 | 34 | /** 35 | * Display stdErr 36 | * 37 | * @var boolean 38 | */ 39 | private $isSilent = false; 40 | 41 | /** 42 | * Command options 43 | * 44 | * @var string[] 45 | */ 46 | private $options = []; 47 | 48 | /** 49 | * List of variables to define 50 | * 51 | * @var string[] 52 | */ 53 | private $vars = []; 54 | 55 | /** 56 | * List of acceptable exit codes. 57 | * 58 | * @var array 59 | */ 60 | private $acceptableExitCodes = []; 61 | 62 | /** 63 | * Constructor. 64 | * 65 | * @param string $cmd 66 | * @param int[] $exitCodes 67 | */ 68 | public function __construct(string $cmd, array $exitCodes = [0]) 69 | { 70 | $this->cmd = $cmd; 71 | $this->acceptableExitCodes = $exitCodes; 72 | } 73 | 74 | /** 75 | * Returns the string to execute on the command line. 76 | * 77 | * @return string 78 | */ 79 | public function getCommand(): string 80 | { 81 | $cmd = $this->getVars() . sprintf('"%s"', $this->cmd) 82 | . (count($this->options) ? ' ' . implode(' ', $this->options) : '') 83 | . ($this->isSilent ? ' 2> /dev/null' : ''); 84 | 85 | return Util::escapeSpacesIfOnWindows($cmd); 86 | } 87 | 88 | /** 89 | * Returns a list of exit codes that are valid. 90 | * 91 | * @return int[] 92 | */ 93 | public function getAcceptableExitCodes(): array 94 | { 95 | return $this->acceptableExitCodes; 96 | } 97 | 98 | /** 99 | * Silence the 'Cmd' by redirecting its stdErr output to /dev/null. 100 | * The silence feature is disabled for Windows systems. 101 | * 102 | * @param bool $bool 103 | * @return \SebastianFeldmann\Cli\Command\Executable 104 | */ 105 | public function silence($bool = true): Executable 106 | { 107 | $this->isSilent = $bool && !defined('PHP_WINDOWS_VERSION_BUILD'); 108 | return $this; 109 | } 110 | 111 | /** 112 | * Add option to list. 113 | * 114 | * @param string $option 115 | * @param mixed $value 116 | * @param string $glue 117 | * @return \SebastianFeldmann\Cli\Command\Executable 118 | */ 119 | public function addOption(string $option, $value = null, string $glue = '='): Executable 120 | { 121 | if ($value !== null) { 122 | // force space for multiple arguments e.g. --option 'foo' 'bar' 123 | if (is_array($value)) { 124 | $glue = ' '; 125 | } 126 | $value = $glue . $this->escapeArgument($value); 127 | } else { 128 | $value = ''; 129 | } 130 | $this->options[] = $option . $value; 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Add a var definition to a command 137 | * 138 | * @param string $name 139 | * @param string $value 140 | * @return $this 141 | */ 142 | public function addVar(string $name, string $value): Executable 143 | { 144 | $this->vars[$name] = $value; 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Return variable definition string e.g. "MYFOO='sometext' MYBAR='nothing' " 151 | * 152 | * @return string 153 | */ 154 | protected function getVars(): string 155 | { 156 | $varStrings = []; 157 | 158 | foreach ($this->vars as $name => $value) { 159 | $varStrings[] = $name . '=' . escapeshellarg($value); 160 | } 161 | 162 | return count($varStrings) ? implode(' ', $varStrings) . ' ' : ''; 163 | } 164 | 165 | /** 166 | * Adds an option to a command if it is not empty. 167 | * 168 | * @param string $option 169 | * @param mixed $check 170 | * @param bool $asValue 171 | * @param string $glue 172 | * @return \SebastianFeldmann\Cli\Command\Executable 173 | */ 174 | public function addOptionIfNotEmpty(string $option, $check, bool $asValue = true, string $glue = '='): Executable 175 | { 176 | if (!empty($check)) { 177 | if ($asValue) { 178 | $this->addOption($option, $check, $glue); 179 | } else { 180 | $this->addOption($option); 181 | } 182 | } 183 | return $this; 184 | } 185 | 186 | /** 187 | * Add argument to list. 188 | * 189 | * @param mixed $argument 190 | * @return \SebastianFeldmann\Cli\Command\Executable 191 | */ 192 | public function addArgument($argument): Executable 193 | { 194 | $this->options[] = $this->escapeArgument($argument); 195 | return $this; 196 | } 197 | 198 | /** 199 | * Escape a shell argument. 200 | * 201 | * @param mixed $argument 202 | * @return string 203 | */ 204 | protected function escapeArgument($argument): string 205 | { 206 | if (is_array($argument)) { 207 | $argument = array_map('escapeshellarg', $argument); 208 | $escaped = implode(' ', $argument); 209 | } else { 210 | $escaped = escapeshellarg($argument); 211 | } 212 | return $escaped; 213 | } 214 | 215 | /** 216 | * Returns the command to execute. 217 | * 218 | * @return string 219 | */ 220 | public function __toString(): string 221 | { 222 | return $this->getCommand(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Command/OutputFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Command; 13 | 14 | /** 15 | * Interface OutputFormatter 16 | * 17 | * @package SebastianFeldmann\Cli 18 | * @author Sebastian Feldmann 19 | * @link https://github.com/sebastianfeldmann/cli 20 | * @since Class available since Release 0.9.0 21 | */ 22 | interface OutputFormatter 23 | { 24 | /** 25 | * Format the output. 26 | * 27 | * @param array $output 28 | * @return iterable 29 | */ 30 | public function format(array $output): iterable; 31 | } 32 | -------------------------------------------------------------------------------- /src/Command/Result.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Command; 13 | 14 | use SebastianFeldmann\Cli\Output\Util as OutputUtil; 15 | 16 | /** 17 | * Class Result 18 | * 19 | * @package SebastianFeldmann\Cli 20 | * @author Sebastian Feldmann 21 | * @link https://github.com/sebastianfeldmann/cli 22 | * @since Class available since Release 0.9.0 23 | */ 24 | class Result 25 | { 26 | /** 27 | * Command that got executed. 28 | * 29 | * @var string 30 | */ 31 | private $cmd; 32 | 33 | /** 34 | * Result code. 35 | * 36 | * @var int 37 | */ 38 | private $code; 39 | 40 | /** 41 | * List of valid exit codes. 42 | * 43 | * @var int[] 44 | */ 45 | private $validExitCodes; 46 | 47 | /** 48 | * Output buffer. 49 | * 50 | * @var array 51 | */ 52 | private $buffer; 53 | 54 | /** 55 | * StdOut. 56 | * 57 | * @var string 58 | */ 59 | private $stdOut; 60 | 61 | /** 62 | * StdErr. 63 | * 64 | * @var string 65 | */ 66 | private $stdErr; 67 | 68 | /** 69 | * Path where the output is redirected to. 70 | * 71 | * @var string 72 | */ 73 | private $redirectPath; 74 | 75 | /** 76 | * Result constructor. 77 | * 78 | * @param string $cmd 79 | * @param int $code 80 | * @param string $stdOut 81 | * @param string $stdErr 82 | * @param string $redirectPath 83 | * @param int[] $validExitCodes 84 | */ 85 | public function __construct( 86 | string $cmd, 87 | int $code, 88 | string $stdOut = '', 89 | string $stdErr = '', 90 | string $redirectPath = '', 91 | array $validExitCodes = [0] 92 | ) { 93 | $this->cmd = $cmd; 94 | $this->code = $code; 95 | $this->stdOut = $stdOut; 96 | $this->stdErr = $stdErr; 97 | $this->redirectPath = $redirectPath; 98 | $this->validExitCodes = $validExitCodes; 99 | } 100 | 101 | /** 102 | * Cmd getter. 103 | * 104 | * @return string 105 | */ 106 | public function getCmd(): string 107 | { 108 | return $this->cmd; 109 | } 110 | 111 | /** 112 | * Code getter. 113 | * 114 | * @return int 115 | */ 116 | public function getCode(): int 117 | { 118 | return $this->code; 119 | } 120 | 121 | /** 122 | * Command executed successful. 123 | */ 124 | public function isSuccessful(): bool 125 | { 126 | return in_array($this->code, $this->validExitCodes); 127 | } 128 | 129 | /** 130 | * StdOutput getter. 131 | * 132 | * @return string 133 | */ 134 | public function getStdOut(): string 135 | { 136 | return $this->stdOut; 137 | } 138 | 139 | /** 140 | * StdError getter. 141 | * 142 | * @return string 143 | */ 144 | public function getStdErr(): string 145 | { 146 | return $this->stdErr; 147 | } 148 | 149 | /** 150 | * Is the output redirected to a file. 151 | * 152 | * @return bool 153 | */ 154 | public function isOutputRedirected(): bool 155 | { 156 | return !empty($this->redirectPath); 157 | } 158 | 159 | /** 160 | * Return path to the file where the output is redirected to. 161 | * 162 | * @return string 163 | */ 164 | public function getRedirectPath(): string 165 | { 166 | return $this->redirectPath; 167 | } 168 | 169 | /** 170 | * Return the output as array. 171 | * 172 | * @return array 173 | */ 174 | public function getStdOutAsArray(): array 175 | { 176 | if (null === $this->buffer) { 177 | $this->buffer = $this->textToBuffer(); 178 | } 179 | return $this->buffer; 180 | } 181 | 182 | /** 183 | * Converts a string into an array. 184 | * 185 | * @return array 186 | */ 187 | private function textToBuffer(): array 188 | { 189 | return OutputUtil::trimEmptyLines(explode("\n", OutputUtil::normalizeLineEndings($this->stdOut))); 190 | } 191 | 192 | /** 193 | * Magic to string method. 194 | * 195 | * @return string 196 | */ 197 | public function __toString(): string 198 | { 199 | return $this->stdOut; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Command/Runner.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Command; 13 | 14 | use SebastianFeldmann\Cli\Command; 15 | use SebastianFeldmann\Cli\Command\Runner\Result as RunnerResult; 16 | 17 | /** 18 | * Interface Runner 19 | * 20 | * @package SebastianFeldmann\Cli 21 | * @author Sebastian Feldmann 22 | * @link https://github.com/sebastianfeldmann/cli 23 | * @since Class available since Release 0.9.0 24 | */ 25 | interface Runner 26 | { 27 | /** 28 | * Execute a command. 29 | * 30 | * @param \SebastianFeldmann\Cli\Command $command 31 | * @param \SebastianFeldmann\Cli\Command\OutputFormatter|null $formatter 32 | * @return \SebastianFeldmann\Cli\Command\Runner\Result 33 | */ 34 | public function run(Command $command, ?OutputFormatter $formatter = null): RunnerResult; 35 | } 36 | -------------------------------------------------------------------------------- /src/Command/Runner/Result.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Command\Runner; 13 | 14 | use SebastianFeldmann\Cli\Command\Result as CommandResult; 15 | 16 | /** 17 | * Class Result 18 | * 19 | * @package SebastianFeldmann\Cli 20 | * @author Sebastian Feldmann 21 | * @link https://github.com/sebastianfeldmann/cli 22 | * @since Class available since Release 0.9.0 23 | */ 24 | class Result 25 | { 26 | /** 27 | * Result of executed command. 28 | * 29 | * @var \SebastianFeldmann\Cli\Command\Result 30 | */ 31 | private $cmdResult; 32 | 33 | /** 34 | * Formatted output of executed result. 35 | * 36 | * @var iterable 37 | */ 38 | private $formatted; 39 | 40 | /** 41 | * Result constructor. 42 | * 43 | * @param \SebastianFeldmann\Cli\Command\Result $cmdResult 44 | * @param iterable $formatted 45 | */ 46 | public function __construct(CommandResult $cmdResult, iterable $formatted = []) 47 | { 48 | $this->cmdResult = $cmdResult; 49 | $this->formatted = $formatted; 50 | } 51 | 52 | /** 53 | * Get the raw command result. 54 | * 55 | * @return \SebastianFeldmann\Cli\Command\Result 56 | */ 57 | public function getCommandResult(): CommandResult 58 | { 59 | return $this->cmdResult; 60 | } 61 | 62 | /** 63 | * Return true if command execution was successful. 64 | * 65 | * @return bool 66 | */ 67 | public function isSuccessful(): bool 68 | { 69 | return $this->cmdResult->isSuccessful(); 70 | } 71 | 72 | /** 73 | * Return the command exit code. 74 | * 75 | * @return int 76 | */ 77 | public function getCode(): int 78 | { 79 | return $this->cmdResult->getCode(); 80 | } 81 | 82 | /** 83 | * Return the executed cli command. 84 | * 85 | * @return string 86 | */ 87 | public function getCmd(): string 88 | { 89 | return $this->cmdResult->getCmd(); 90 | } 91 | 92 | /** 93 | * Return commands output to stdOut. 94 | * 95 | * @return string 96 | */ 97 | public function getStdOut(): string 98 | { 99 | return $this->cmdResult->getStdOut(); 100 | } 101 | 102 | /** 103 | * Return commands error output to stdErr. 104 | * 105 | * @return string 106 | */ 107 | public function getStdErr(): string 108 | { 109 | return $this->cmdResult->getStdErr(); 110 | } 111 | 112 | /** 113 | * Is the output redirected to a file. 114 | * 115 | * @return bool 116 | */ 117 | public function isOutputRedirected(): bool 118 | { 119 | return $this->cmdResult->isOutputRedirected(); 120 | } 121 | 122 | /** 123 | * Return path to the file where the output is redirected to. 124 | * 125 | * @return string 126 | */ 127 | public function getRedirectPath(): string 128 | { 129 | return $this->cmdResult->getRedirectPath(); 130 | } 131 | 132 | /** 133 | * Return cmd output as array. 134 | * 135 | * @return array 136 | */ 137 | public function getBufferedOutput(): array 138 | { 139 | return $this->cmdResult->getStdOutAsArray(); 140 | } 141 | 142 | /** 143 | * Return formatted output. 144 | * 145 | * @return iterable 146 | */ 147 | public function getFormattedOutput(): iterable 148 | { 149 | return $this->formatted; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Command/Runner/Simple.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Command\Runner; 13 | 14 | use RuntimeException; 15 | use SebastianFeldmann\Cli\Command; 16 | use SebastianFeldmann\Cli\Command\Runner; 17 | use SebastianFeldmann\Cli\Command\OutputFormatter; 18 | use SebastianFeldmann\Cli\Processor; 19 | 20 | /** 21 | * Class Simple 22 | * 23 | * @package SebastianFeldmann\Cli 24 | * @author Sebastian Feldmann 25 | * @link https://github.com/sebastianfeldmann/cli 26 | * @since Class available since Release 0.9.0 27 | */ 28 | class Simple implements Runner 29 | { 30 | /** 31 | * Class handling system calls. 32 | * 33 | * @var \SebastianFeldmann\Cli\Processor 34 | */ 35 | private $processor; 36 | 37 | /** 38 | * Exec constructor. 39 | * 40 | * @param \SebastianFeldmann\Cli\Processor|null $processor 41 | */ 42 | public function __construct(?Processor $processor = null) 43 | { 44 | $this->processor = $processor !== null 45 | ? $processor 46 | : new Processor\ProcOpen(); 47 | } 48 | 49 | /** 50 | * Execute a cli command. 51 | * 52 | * @param \SebastianFeldmann\Cli\Command $command 53 | * @param \SebastianFeldmann\Cli\Command\OutputFormatter|null $formatter 54 | * @return \SebastianFeldmann\Cli\Command\Runner\Result 55 | */ 56 | public function run(Command $command, ?OutputFormatter $formatter = null): Result 57 | { 58 | $cmd = $this->processor->run($command->getCommand(), $command->getAcceptableExitCodes()); 59 | 60 | if (!$cmd->isSuccessful()) { 61 | throw new RuntimeException( 62 | 'Command failed:' . PHP_EOL 63 | . ' exit-code: ' . $cmd->getCode() . PHP_EOL 64 | . ' message: ' . $cmd->getStdErr() . PHP_EOL, 65 | $cmd->getCode() 66 | ); 67 | } 68 | 69 | $formatted = $formatter !== null ? $formatter->format($cmd->getStdOutAsArray()) : []; 70 | $result = new Result($cmd, $formatted); 71 | 72 | return $result; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/CommandLine.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Class CommandLine 18 | * 19 | * @package SebastianFeldmann\Cli 20 | * @author Sebastian Feldmann 21 | * @link https://github.com/sebastianfeldmann/cli 22 | * @since Class available since Release 0.9.0 23 | */ 24 | class CommandLine implements Command 25 | { 26 | /** 27 | * List of system commands to execute 28 | * 29 | * @var \SebastianFeldmann\Cli\Command[] 30 | */ 31 | private $commands = []; 32 | 33 | /** 34 | * Redirect the output 35 | * 36 | * @var string 37 | */ 38 | private $redirectOutput; 39 | 40 | /** 41 | * Output pipeline 42 | * 43 | * @var \SebastianFeldmann\Cli\Command[] 44 | */ 45 | private $pipeline = []; 46 | 47 | /** 48 | * Should 'pipefail' be set? 49 | * 50 | * @var bool 51 | */ 52 | private $pipeFail = false; 53 | 54 | /** 55 | * List of acceptable exit codes. 56 | * 57 | * @var array 58 | */ 59 | private $acceptedExitCodes = [0]; 60 | 61 | /** 62 | * Set the list of accepted exit codes 63 | * 64 | * @param int[] $codes 65 | * @return void 66 | */ 67 | public function acceptExitCodes(array $codes): void 68 | { 69 | $this->acceptedExitCodes = $codes; 70 | } 71 | 72 | /** 73 | * Redirect the stdOut. 74 | * 75 | * @param string $path 76 | * @return void 77 | */ 78 | public function redirectOutputTo($path): void 79 | { 80 | $this->redirectOutput = $path; 81 | } 82 | 83 | /** 84 | * Should the output be redirected 85 | * 86 | * @return bool 87 | */ 88 | public function isOutputRedirected(): bool 89 | { 90 | return !empty($this->redirectOutput); 91 | } 92 | 93 | /** 94 | * Redirect getter. 95 | * 96 | * @return string 97 | */ 98 | public function getRedirectPath(): string 99 | { 100 | return $this->redirectOutput; 101 | } 102 | 103 | /** 104 | * Pipe the command into given command 105 | * 106 | * @param \SebastianFeldmann\Cli\Command $cmd 107 | * @return void 108 | */ 109 | public function pipeOutputTo(Command $cmd): void 110 | { 111 | if (!$this->canPipe()) { 112 | throw new RuntimeException('Can\'t pipe output'); 113 | } 114 | $this->pipeline[] = $cmd; 115 | } 116 | 117 | /** 118 | * Get the 'pipefail' option command snippet 119 | * 120 | * @return string 121 | */ 122 | public function getPipeFail(): string 123 | { 124 | return ($this->isPiped() && $this->pipeFail) ? 'set -o pipefail; ' : ''; 125 | } 126 | 127 | /** 128 | * Can the pipe '|' operator be used 129 | * 130 | * @return bool 131 | */ 132 | public function canPipe(): bool 133 | { 134 | return !defined('PHP_WINDOWS_VERSION_BUILD'); 135 | } 136 | 137 | /** 138 | * Is there a command pipeline 139 | * 140 | * @return bool 141 | */ 142 | public function isPiped(): bool 143 | { 144 | return !empty($this->pipeline); 145 | } 146 | 147 | /** 148 | * Should the pipefail option be set 149 | * 150 | * @param bool $pipeFail 151 | */ 152 | public function pipeFail(bool $pipeFail) 153 | { 154 | $this->pipeFail = $pipeFail; 155 | } 156 | 157 | /** 158 | * Return command pipeline 159 | * 160 | * @return string 161 | */ 162 | public function getPipeline(): string 163 | { 164 | return $this->isPiped() ? ' | ' . implode(' | ', $this->pipeline) : ''; 165 | } 166 | 167 | /** 168 | * Adds a cli command to list of commands to execute 169 | * 170 | * @param \SebastianFeldmann\Cli\Command $cmd 171 | * @return void 172 | */ 173 | public function addCommand(Command $cmd): void 174 | { 175 | $this->commands[] = $cmd; 176 | } 177 | 178 | /** 179 | * Generates the system command 180 | * 181 | * @return string 182 | */ 183 | public function getCommand(): string 184 | { 185 | $amount = count($this->commands); 186 | if ($amount < 1) { 187 | throw new RuntimeException('no command to execute'); 188 | } 189 | $cmd = $this->getPipeFail() 190 | . ($amount > 1 ? '(' . implode(' && ', $this->commands) . ')' : $this->commands[0]) 191 | . $this->getPipeline() 192 | . (!empty($this->redirectOutput) ? ' > ' . $this->redirectOutput : ''); 193 | 194 | return $cmd; 195 | } 196 | 197 | /** 198 | * Returns a list of exit codes that are valid 199 | * 200 | * @return array 201 | */ 202 | public function getAcceptableExitCodes(): array 203 | { 204 | return $this->acceptedExitCodes; 205 | } 206 | 207 | /** 208 | * Returns the command to execute 209 | * 210 | * @return string 211 | */ 212 | public function __toString(): string 213 | { 214 | return $this->getCommand(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Output/Util.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Output; 13 | 14 | /** 15 | * Class Util 16 | * 17 | * @package SebastianFeldmann\Cli 18 | * @author Sebastian Feldmann 19 | * @link https://github.com/sebastianfeldmann/cli 20 | * @since Class available since Release 2.1.0 21 | */ 22 | class Util 23 | { 24 | /** 25 | * Remove empty entries at the end of an array. 26 | * 27 | * @param array $lines 28 | * @return array 29 | */ 30 | public static function trimEmptyLines(array $lines): array 31 | { 32 | for ($last = count($lines) - 1; $last > -1; $last--) { 33 | if (!empty($lines[$last])) { 34 | return $lines; 35 | } 36 | unset($lines[$last]); 37 | } 38 | return $lines; 39 | } 40 | 41 | /** 42 | * Replaces all 'known' line endings with unix \n line endings 43 | * 44 | * @param string $text 45 | * @return string 46 | */ 47 | public static function normalizeLineEndings(string $text): string 48 | { 49 | $mod = preg_match('/[\p{Cyrillic}]/u', $text) ? 'u' : ''; 50 | return preg_replace('~(*BSR_UNICODE)\R~' . $mod, "\n", $text); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Processor.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli; 13 | 14 | use SebastianFeldmann\Cli\Command\Result; 15 | 16 | /** 17 | * Interface Processor 18 | * 19 | * @package SebastianFeldmann\Cli 20 | * @author Sebastian Feldmann 21 | * @link https://github.com/sebastianfeldmann/cli 22 | * @since Class available since Release 0.9.0 23 | */ 24 | interface Processor 25 | { 26 | /** 27 | * Execute a system call. 28 | * 29 | * @param string $cmd 30 | * @param int[] $acceptableExitCodes 31 | * @return \SebastianFeldmann\Cli\Command\Result 32 | */ 33 | public function run(string $cmd, array $acceptableExitCodes = [0]): Result; 34 | } 35 | -------------------------------------------------------------------------------- /src/Processor/ProcOpen.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Processor; 13 | 14 | use RuntimeException; 15 | use SebastianFeldmann\Cli\Command\Result; 16 | use SebastianFeldmann\Cli\Processor; 17 | 18 | /** 19 | * Class ProcOpen 20 | * 21 | * @package SebastianFeldmann\Cli 22 | * @author Sebastian Feldmann 23 | * @link https://github.com/sebastianfeldmann/cli 24 | * @since Class available since Release 0.9.0 25 | */ 26 | class ProcOpen implements Processor 27 | { 28 | /** 29 | * Execute the command 30 | * 31 | * @param string $cmd 32 | * @param int[] $acceptableExitCodes 33 | * @return \SebastianFeldmann\Cli\Command\Result 34 | */ 35 | public function run(string $cmd, array $acceptableExitCodes = [0]): Result 36 | { 37 | $old = error_reporting(0); 38 | $descriptorSpec = [ 39 | ['pipe', 'r'], 40 | ['pipe', 'w'], 41 | ['pipe', 'w'], 42 | ]; 43 | 44 | $process = proc_open($cmd, $descriptorSpec, $pipes); 45 | if (!is_resource($process)) { 46 | throw new RuntimeException('can\'t execute \'proc_open\''); 47 | } 48 | 49 | // Loop on process until it exits normally. 50 | $stdOut = ""; 51 | $stdErr = ""; 52 | do { 53 | // Consume output streams while the process runs. The buffer will block process updates when full 54 | $status = proc_get_status($process); 55 | $stdOut .= stream_get_contents($pipes[1]); 56 | $stdErr .= stream_get_contents($pipes[2]); 57 | } while ($status['running']); 58 | 59 | // make sure all pipes are closed before calling proc_close 60 | foreach ($pipes as $index => $pipe) { 61 | fclose($pipe); 62 | unset($pipes[$index]); 63 | } 64 | 65 | proc_close($process); 66 | error_reporting($old); 67 | 68 | return new Result($cmd, $status['exitcode'], $stdOut, $stdErr, '', $acceptableExitCodes); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Processor/Symfony.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Processor; 13 | 14 | use SebastianFeldmann\Cli\Command\Result; 15 | use SebastianFeldmann\Cli\Processor; 16 | use Symfony\Component\Process\Process; 17 | 18 | /** 19 | * Class ProcOpen 20 | * 21 | * @package SebastianFeldmann\Cli 22 | * @author Sebastian Feldmann 23 | * @link https://github.com/sebastianfeldmann/cli 24 | * @since Class available since Release 3.2.2 25 | */ 26 | class Symfony implements Processor 27 | { 28 | /** 29 | * Execute the command 30 | * 31 | * @param string $cmd 32 | * @param int[] $acceptableExitCodes 33 | * @return \SebastianFeldmann\Cli\Command\Result 34 | */ 35 | public function run(string $cmd, array $acceptableExitCodes = [0]): Result 36 | { 37 | // the else (:) variant is there to keep backwards compatibility with previous symfony versions 38 | // and is only getting executed in those. This is the reason why the Process constructor is 39 | // given a string instead of an array. The whole ternary can be removed if Symfony versions 40 | // below 4.2 are not supported anymore. 41 | $process = method_exists(Process::class, 'fromShellCommandline') 42 | ? Process::fromShellCommandline($cmd) 43 | : new Process($cmd); // @phpstan-ignore-line 44 | 45 | $process->setTimeout(null); 46 | $process->run(); 47 | return new Result( 48 | $cmd, 49 | $process->getExitCode(), 50 | $process->getOutput(), 51 | $process->getErrorOutput(), 52 | '', 53 | $acceptableExitCodes 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Reader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli; 13 | 14 | use Iterator; 15 | 16 | /** 17 | * Interface Reader 18 | * 19 | * @package SebastianFeldmann\Cli 20 | * @author Sebastian Feldmann 21 | * @link https://github.com/sebastianfeldmann/cli 22 | * @since Class available since Release 3.3.0 23 | */ 24 | interface Reader extends Iterator 25 | { 26 | /** 27 | * Get the current line. 28 | * 29 | * @return string 30 | */ 31 | public function current(): string; 32 | } 33 | -------------------------------------------------------------------------------- /src/Reader/Abstraction.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Reader; 13 | 14 | use Iterator; 15 | use SebastianFeldmann\Cli\Reader; 16 | 17 | /** 18 | * Abstract Reader class 19 | * 20 | * @package SebastianFeldmann\Cli 21 | * @author Sebastian Feldmann 22 | * @link https://github.com/sebastianfeldmann/cli 23 | * @since Class available since Release 3.3.0 24 | */ 25 | abstract class Abstraction implements Reader 26 | { 27 | /** 28 | * Internal iterator to handle foreach 29 | * 30 | * @var \Iterator 31 | */ 32 | private $iterator; 33 | 34 | /** 35 | * Return the internal iterator 36 | * 37 | * @return \Iterator 38 | */ 39 | private function getIterator(): Iterator 40 | { 41 | return $this->iterator; 42 | } 43 | 44 | /** 45 | * Set the pointer to the next line 46 | * 47 | * @return void 48 | */ 49 | public function next(): void 50 | { 51 | $this->getIterator()->next(); 52 | } 53 | 54 | /** 55 | * Get the line number of the current line 56 | * 57 | * @return int 58 | */ 59 | public function key(): int 60 | { 61 | return $this->getIterator()->key(); 62 | } 63 | 64 | /** 65 | * Check whether the current line is valid 66 | * 67 | * @return bool 68 | */ 69 | public function valid(): bool 70 | { 71 | return $this->getIterator()->valid(); 72 | } 73 | 74 | /** 75 | * Recreate/rewind the iterator 76 | * 77 | * @return void 78 | */ 79 | public function rewind(): void 80 | { 81 | $this->iterator = $this->createIterator(); 82 | } 83 | 84 | /** 85 | * Get the current line 86 | * 87 | * @return string 88 | */ 89 | public function current(): string 90 | { 91 | return $this->getIterator()->current(); 92 | } 93 | 94 | /** 95 | * Create the internal iterator 96 | * 97 | * @return iterable 98 | */ 99 | abstract protected function createIterator(): iterable; 100 | } 101 | -------------------------------------------------------------------------------- /src/Reader/StandardInput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli\Reader; 13 | 14 | use Exception; 15 | 16 | /** 17 | * StandardInput 18 | * 19 | * @package SebastianFeldmann\Cli 20 | * @author Sebastian Feldmann 21 | * @link https://github.com/sebastianfeldmann/cli 22 | * @since Class available since Release 3.3.0 23 | */ 24 | class StandardInput extends Abstraction 25 | { 26 | /** 27 | * Standard Input stream handle 28 | * 29 | * @var resource 30 | */ 31 | private $handle; 32 | 33 | /** 34 | * StandardInput constructor. 35 | * 36 | * @param resource $stdInHandle 37 | */ 38 | public function __construct($stdInHandle) 39 | { 40 | $this->handle = $stdInHandle; 41 | } 42 | 43 | /** 44 | * Create the generator 45 | * 46 | * @return iterable 47 | * @throws \Exception 48 | */ 49 | protected function createIterator(): iterable 50 | { 51 | $read = [$this->handle]; 52 | $write = []; 53 | $except = []; 54 | $result = stream_select($read, $write, $except, 0); 55 | 56 | if ($result === false) { 57 | throw new Exception('stream_select failed'); 58 | } 59 | if ($result !== 0) { 60 | while (!\feof($this->handle)) { 61 | yield \fgets($this->handle); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace SebastianFeldmann\Cli; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Interface Processor 18 | * 19 | * @package SebastianFeldmann\Cli 20 | * @author Sebastian Feldmann 21 | * @link https://github.com/sebastianfeldmann/cli 22 | * @since Class available since Release 1.0.4 23 | */ 24 | abstract class Util 25 | { 26 | /** 27 | * List of console style codes. 28 | * 29 | * @var array 30 | */ 31 | private static $ansiCodes = [ 32 | 'bold' => 1, 33 | 'fg-black' => 30, 34 | 'fg-red' => 31, 35 | 'fg-green' => 32, 36 | 'fg-yellow' => 33, 37 | 'fg-cyan' => 36, 38 | 'fg-white' => 37, 39 | 'bg-red' => 41, 40 | 'bg-green' => 42, 41 | 'bg-yellow' => 43 42 | ]; 43 | 44 | /** 45 | * Detect a given command's location. 46 | * 47 | * @param string $cmd The command to locate 48 | * @param string $path Directory where the command should be 49 | * @param array $optionalLocations Some fallback locations where to search for the command 50 | * @return string Absolute path to detected command including command itself 51 | * @throws \RuntimeException 52 | */ 53 | public static function detectCmdLocation(string $cmd, string $path = '', array $optionalLocations = []): string 54 | { 55 | $detectionSteps = [ 56 | function ($cmd) use ($path) { 57 | if (!empty($path)) { 58 | return self::detectCmdLocationInPath($cmd, $path); 59 | } 60 | return ''; 61 | }, 62 | function ($cmd) { 63 | return self::detectCmdLocationWithWhich($cmd); 64 | }, 65 | function ($cmd) { 66 | $paths = explode(PATH_SEPARATOR, self::getEnvPath()); 67 | return self::detectCmdLocationInPaths($cmd, $paths); 68 | }, 69 | function ($cmd) use ($optionalLocations) { 70 | return self::detectCmdLocationInPaths($cmd, $optionalLocations); 71 | } 72 | ]; 73 | 74 | foreach ($detectionSteps as $step) { 75 | $bin = $step($cmd); 76 | if (!empty($bin)) { 77 | return $bin; 78 | } 79 | } 80 | 81 | throw new RuntimeException(sprintf('\'%s\' was nowhere to be found please specify the correct path', $cmd)); 82 | } 83 | 84 | /** 85 | * Detect a command in a given path. 86 | * 87 | * @param string $cmd 88 | * @param string $path 89 | * @return string 90 | * @throws \RuntimeException 91 | */ 92 | public static function detectCmdLocationInPath(string $cmd, string $path): string 93 | { 94 | $command = $path . DIRECTORY_SEPARATOR . $cmd; 95 | $bin = self::getExecutable($command); 96 | if (empty($bin)) { 97 | throw new RuntimeException(sprintf('wrong path specified for \'%s\': %s', $cmd, $path)); 98 | } 99 | return $bin; 100 | } 101 | 102 | /** 103 | * Detect command location using which cli command. 104 | * 105 | * @param string $cmd 106 | * @return string 107 | */ 108 | public static function detectCmdLocationWithWhich($cmd): string 109 | { 110 | $bin = ''; 111 | // on nx systems use 'which' command. 112 | if (!defined('PHP_WINDOWS_VERSION_BUILD')) { 113 | $command = trim(`which $cmd`); 114 | $bin = self::getExecutable($command); 115 | } 116 | return $bin; 117 | } 118 | 119 | /** 120 | * Check path list for executable command. 121 | * 122 | * @param string $cmd 123 | * @param array $paths 124 | * @return string 125 | */ 126 | public static function detectCmdLocationInPaths($cmd, array $paths): string 127 | { 128 | foreach ($paths as $path) { 129 | $command = $path . DIRECTORY_SEPARATOR . $cmd; 130 | $bin = self::getExecutable($command); 131 | if (null !== $bin) { 132 | return $bin; 133 | } 134 | } 135 | return ''; 136 | } 137 | 138 | /** 139 | * Return local $PATH variable. 140 | * 141 | * @return string 142 | * @throws \RuntimeException 143 | */ 144 | public static function getEnvPath(): string 145 | { 146 | // check for unix and windows case $_SERVER index 147 | foreach (['PATH', 'Path', 'path'] as $index) { 148 | if (isset($_SERVER[$index])) { 149 | return $_SERVER[$index]; 150 | } 151 | } 152 | throw new RuntimeException('cant find local PATH variable'); 153 | } 154 | 155 | /** 156 | * Returns the executable command if the command is executable, empty string otherwise. 157 | * Search for $command.exe on Windows systems. 158 | * 159 | * @param string $command 160 | * @return string 161 | */ 162 | public static function getExecutable($command): string 163 | { 164 | if (is_executable($command)) { 165 | return $command; 166 | } 167 | // on windows check the .exe suffix 168 | if (defined('PHP_WINDOWS_VERSION_BUILD')) { 169 | $command .= '.exe'; 170 | if (is_executable($command)) { 171 | return $command; 172 | } 173 | } 174 | return ''; 175 | } 176 | 177 | /** 178 | * Is given path absolute. 179 | * 180 | * @param string $path 181 | * @return bool 182 | */ 183 | public static function isAbsolutePath($path): bool 184 | { 185 | // path already absolute? 186 | if ($path[0] === '/') { 187 | return true; 188 | } 189 | 190 | // Matches the following on Windows: 191 | // - \\NetworkComputer\Path 192 | // - \\.\D: 193 | // - \\.\c: 194 | // - C:\Windows 195 | // - C:\windows 196 | // - C:/windows 197 | // - c:/windows 198 | if (defined('PHP_WINDOWS_VERSION_BUILD') && self::isAbsoluteWindowsPath($path)) { 199 | return true; 200 | } 201 | 202 | // Stream 203 | if (strpos($path, '://') !== false) { 204 | return true; 205 | } 206 | 207 | return false; 208 | } 209 | 210 | /** 211 | * Is given path an absolute windows path. 212 | * 213 | * @param string $path 214 | * @return bool 215 | */ 216 | public static function isAbsoluteWindowsPath($path): bool 217 | { 218 | return ($path[0] === '\\' || (strlen($path) >= 3 && preg_match('#^[A-Z]\:[/\\\]#i', substr($path, 0, 3)))); 219 | } 220 | 221 | /** 222 | * Converts a path to an absolute one if necessary relative to a given base path. 223 | * 224 | * @param string $path 225 | * @param string $base 226 | * @param bool $useIncludePath 227 | * @return string 228 | */ 229 | public static function toAbsolutePath(string $path, string $base, bool $useIncludePath = false): string 230 | { 231 | if (self::isAbsolutePath($path)) { 232 | return $path; 233 | } 234 | 235 | $file = $base . DIRECTORY_SEPARATOR . $path; 236 | 237 | if ($useIncludePath && !file_exists($file)) { 238 | $includePathFile = stream_resolve_include_path($path); 239 | if ($includePathFile) { 240 | $file = $includePathFile; 241 | } 242 | } 243 | return $file; 244 | } 245 | 246 | /** 247 | * Formats a buffer with a specified ANSI color sequence if colors are enabled. 248 | * 249 | * @author Sebastian Bergmann 250 | * @param string $color 251 | * @param string $buffer 252 | * @return string 253 | */ 254 | public static function formatWithColor(string $color, string $buffer): string 255 | { 256 | $codes = array_map('trim', explode(',', $color)); 257 | $lines = explode("\n", $buffer); 258 | $padding = max(array_map('strlen', $lines)); 259 | 260 | $styles = []; 261 | foreach ($codes as $code) { 262 | $styles[] = self::$ansiCodes[$code]; 263 | } 264 | $style = sprintf("\x1b[%sm", implode(';', $styles)); 265 | 266 | $styledLines = []; 267 | foreach ($lines as $line) { 268 | $styledLines[] = strlen($line) ? $style . str_pad($line, $padding) . "\x1b[0m" : ''; 269 | } 270 | 271 | return implode(PHP_EOL, $styledLines); 272 | } 273 | 274 | /** 275 | * Fills up a text buffer with '*' to consume by default 72 chars. 276 | * 277 | * @param string $buffer 278 | * @param int $length 279 | * @return string 280 | */ 281 | public static function formatWithAsterisk(string $buffer, int $length = 72): string 282 | { 283 | return $buffer . str_repeat('*', $length - strlen($buffer)) . PHP_EOL; 284 | } 285 | 286 | /** 287 | * Can command pipe operator be used. 288 | * 289 | * @return bool 290 | */ 291 | public static function canPipe(): bool 292 | { 293 | return !defined('PHP_WINDOWS_VERSION_BUILD'); 294 | } 295 | 296 | /** 297 | * Removes a directory that is not empty. 298 | * 299 | * @param string $dir 300 | */ 301 | public static function removeDir(string $dir) 302 | { 303 | foreach (scandir($dir) as $file) { 304 | if ('.' === $file || '..' === $file) { 305 | continue; 306 | } 307 | if (is_dir($dir . '/' . $file)) { 308 | self::removeDir($dir . '/' . $file); 309 | } else { 310 | unlink($dir . '/' . $file); 311 | } 312 | } 313 | rmdir($dir); 314 | } 315 | 316 | /** 317 | * Wraps windows command with a double quote to escape spaces 318 | * i.e: `E:/Program Files/tar.exe -zcf ...` escaped to `"E:/Program Files/tar.exe -zcf ..."` 319 | * @param string $cmd 320 | * @return string 321 | */ 322 | public static function escapeSpacesIfOnWindows(string $cmd): string 323 | { 324 | return defined('PHP_WINDOWS_VERSION_BUILD') ? sprintf('"%s"', $cmd) : $cmd; 325 | } 326 | } 327 | --------------------------------------------------------------------------------