├── .actrc ├── .editorconfig ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── code-quality.yml │ ├── regenerate-readme.yml │ └── testing.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── examples ├── arguments.php ├── colors.php ├── common.php ├── menu.php ├── notify.php ├── output.php ├── progress.php ├── table.php └── tree.php ├── http-console.php ├── lib └── cli │ ├── Arguments.php │ ├── Colors.php │ ├── Memoize.php │ ├── Notify.php │ ├── Progress.php │ ├── Shell.php │ ├── Streams.php │ ├── Table.php │ ├── Tree.php │ ├── arguments │ ├── Argument.php │ ├── HelpScreen.php │ ├── InvalidArguments.php │ └── Lexer.php │ ├── cli.php │ ├── notify │ ├── Dots.php │ └── Spinner.php │ ├── progress │ └── Bar.php │ ├── table │ ├── Ascii.php │ ├── Renderer.php │ └── Tabular.php │ ├── tree │ ├── Ascii.php │ ├── Markdown.php │ └── Renderer.php │ └── unicode │ └── regex.php ├── phpunit.xml.dist ├── test.php └── tests ├── Test_Arguments.php ├── Test_Cli.php ├── Test_Colors.php ├── Test_Shell.php ├── Test_Table.php ├── Test_Table_Ascii.php └── bootstrap.php /.actrc: -------------------------------------------------------------------------------- 1 | # Configuration file for nektos/act. 2 | # See https://github.com/nektos/act#configuration 3 | -P ubuntu-latest=shivammathur/node:latest 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | # From https://github.com/WordPress/wordpress-develop/blob/trunk/.editorconfig with a couple of additions. 8 | 9 | root = true 10 | 11 | [*] 12 | charset = utf-8 13 | end_of_line = lf 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | indent_style = tab 17 | 18 | [{*.yml,*.feature,.jshintrc,*.json}] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | 25 | [{*.txt,wp-config-sample.php}] 26 | end_of_line = crlf 27 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @wp-cli/committers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - scope:distribution 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | open-pull-requests-limit: 10 15 | labels: 16 | - scope:distribution 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality Checks 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | 10 | jobs: 11 | code-quality: 12 | uses: wp-cli/.github/.github/workflows/reusable-code-quality.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/regenerate-readme.yml: -------------------------------------------------------------------------------- 1 | name: Regenerate README file 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | paths-ignore: 10 | - "features/**" 11 | - "README.md" 12 | 13 | jobs: 14 | regenerate-readme: 15 | uses: wp-cli/.github/.github/workflows/reusable-regenerate-readme.yml@main 16 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | - master 10 | schedule: 11 | - cron: '17 1 * * *' # Run every day on a seemly random time. 12 | 13 | jobs: 14 | test: 15 | uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | .*.swp 4 | composer.lock 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 James Logsdon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP Command Line Tools 2 | ====================== 3 | 4 | A collection of functions and classes to assist with command line development. 5 | 6 | Requirements 7 | 8 | * PHP >= 5.6 9 | 10 | Suggested PHP extensions 11 | 12 | * mbstring - Used for calculating string widths. 13 | 14 | Function List 15 | ------------- 16 | 17 | * `cli\out($msg, ...)` 18 | * `cli\out_padded($msg, ...)` 19 | * `cli\err($msg, ...)` 20 | * `cli\line($msg = '', ...)` 21 | * `cli\input()` 22 | * `cli\prompt($question, $default = false, $marker = ':')` 23 | * `cli\choose($question, $choices = 'yn', $default = 'n')` 24 | * `cli\menu($items, $default = false, $title = 'Choose an Item')` 25 | 26 | Progress Indicators 27 | ------------------- 28 | 29 | * `cli\notify\Dots($msg, $dots = 3, $interval = 100)` 30 | * `cli\notify\Spinner($msg, $interval = 100)` 31 | * `cli\progress\Bar($msg, $total, $interval = 100)` 32 | 33 | Tabular Display 34 | --------------- 35 | 36 | * `cli\Table::__construct(array $headers = null, array $rows = null)` 37 | * `cli\Table::setHeaders(array $headers)` 38 | * `cli\Table::setRows(array $rows)` 39 | * `cli\Table::setRenderer(cli\table\Renderer $renderer)` 40 | * `cli\Table::addRow(array $row)` 41 | * `cli\Table::sort($column)` 42 | * `cli\Table::display()` 43 | 44 | The display function will detect if output is piped and, if it is, render a tab delimited table instead of the ASCII 45 | table rendered for visual display. 46 | 47 | You can also explicitly set the renderer used by calling `cli\Table::setRenderer()` and giving it an instance of one 48 | of the concrete `cli\table\Renderer` classes. 49 | 50 | Tree Display 51 | ------------ 52 | 53 | * `cli\Tree::__construct()` 54 | * `cli\Tree::setData(array $data)` 55 | * `cli\Tree::setRenderer(cli\tree\Renderer $renderer)` 56 | * `cli\Tree::render()` 57 | * `cli\Tree::display()` 58 | 59 | Argument Parser 60 | --------------- 61 | 62 | Argument parsing uses a simple framework for taking a list of command line arguments, 63 | usually straight from `$_SERVER['argv']`, and parses the input against a set of 64 | defined rules. 65 | 66 | Check `examples/arguments.php` for an example. 67 | 68 | Usage 69 | ----- 70 | 71 | See `examples/` directory for examples. 72 | 73 | 74 | Todo 75 | ---- 76 | 77 | * Expand this README 78 | * Add doc blocks to rest of code 79 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-cli/php-cli-tools", 3 | "type": "library", 4 | "description": "Console utilities for PHP", 5 | "keywords": ["console", "cli"], 6 | "homepage": "http://github.com/wp-cli/php-cli-tools", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Daniel Bachhuber", 11 | "email": "daniel@handbuilt.co", 12 | "role": "Maintainer" 13 | }, 14 | { 15 | "name": "James Logsdon", 16 | "email": "jlogsdon@php.net", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": ">= 7.2.24" 22 | }, 23 | "require-dev": { 24 | "roave/security-advisories": "dev-latest", 25 | "wp-cli/wp-cli-tests": "^4" 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "0.11.x-dev" 30 | } 31 | }, 32 | "minimum-stability": "dev", 33 | "prefer-stable": true, 34 | "autoload": { 35 | "psr-0": { 36 | "cli": "lib/" 37 | }, 38 | "files": [ 39 | "lib/cli/cli.php" 40 | ] 41 | }, 42 | "config": { 43 | "allow-plugins": { 44 | "dealerdirect/phpcodesniffer-composer-installer": true, 45 | "johnpbloch/wordpress-core-installer": true 46 | } 47 | }, 48 | "scripts": { 49 | "behat": "run-behat-tests", 50 | "behat-rerun": "rerun-behat-tests", 51 | "lint": "run-linter-tests", 52 | "phpcs": "run-phpcs-tests", 53 | "phpunit": "run-php-unit-tests", 54 | "prepare-tests": "install-package-tests", 55 | "test": [ 56 | "@lint", 57 | "@phpcs", 58 | "@phpunit", 59 | "@behat" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/arguments.php: -------------------------------------------------------------------------------- 1 | addFlag(array('verbose', 'v'), 'Turn on verbose output'); 20 | $arguments->addFlag('version', 'Display the version'); 21 | $arguments->addFlag(array('quiet', 'q'), 'Disable all output'); 22 | $arguments->addFlag(array('help', 'h'), 'Show this help screen'); 23 | 24 | $arguments->addOption(array('cache', 'C'), array( 25 | 'default' => getcwd(), 26 | 'description' => 'Set the cache directory')); 27 | $arguments->addOption(array('name', 'n'), array( 28 | 'default' => 'James', 29 | 'description' => 'Set a name with a really long description and a default so we can see what line wrapping looks like which is probably a goo idea')); 30 | 31 | $arguments->parse(); 32 | if ($arguments['help']) { 33 | echo $arguments->getHelpScreen(); 34 | echo "\n\n"; 35 | } 36 | 37 | echo $arguments->asJSON() . "\n"; 38 | -------------------------------------------------------------------------------- /examples/colors.php: -------------------------------------------------------------------------------- 1 | tick(); 22 | if ($sleep) usleep($sleep); 23 | } 24 | $notify->finish(); 25 | } 26 | 27 | function test_notify_msg(cli\Notify $notify, $cycle = 1000000, $sleep = null) { 28 | $notify->display(); 29 | for ($i = 0; $i < $cycle; $i++) { 30 | // Sleep before tick to simulate time-intensive work and give time 31 | // for the initial message to display before it is changed 32 | if ($sleep) usleep($sleep); 33 | $msg = sprintf(' Finished step %d', $i + 1); 34 | $notify->tick(1, $msg); 35 | } 36 | $notify->finish(); 37 | } 38 | -------------------------------------------------------------------------------- /examples/menu.php: -------------------------------------------------------------------------------- 1 | 'Output Examples', 7 | 'notify' => 'cli\Notify Examples', 8 | 'progress' => 'cli\Progress Examples', 9 | 'table' => 'cli\Table Example', 10 | 'colors' => 'cli\Colors example', 11 | 'quit' => 'Quit', 12 | ); 13 | 14 | while (true) { 15 | $choice = \cli\menu($menu, null, 'Choose an example'); 16 | \cli\line(); 17 | 18 | if ($choice == 'quit') { 19 | break; 20 | } 21 | 22 | include "{$choice}.php"; 23 | \cli\line(); 24 | } 25 | -------------------------------------------------------------------------------- /examples/notify.php: -------------------------------------------------------------------------------- 1 | 'you', 'b' => 'array')); 9 | 10 | \cli\err(' \cli\err sends output to STDERR'); 11 | \cli\err(' It does automatically append a new line'); 12 | \cli\err(' It does accept any number of %s which are then %s to %s for formatting', 'arguments', 'passed', 'sprintf'); 13 | \cli\err(" Alternatively, {:a} can use an {:b} as the second argument.\n", array('a' => 'you', 'b' => 'array')); 14 | 15 | \cli\line(' \cli\line forwards to \cli\out for output'); 16 | \cli\line(' It does automatically append a new line'); 17 | \cli\line(' It does accept any number of %s which are then %s to %s for formatting', 'arguments', 'passed', 'sprintf'); 18 | \cli\line(" Alternatively, {:a} can use an {:b} as the second argument.\n", array('a' => 'you', 'b' => 'array')); 19 | -------------------------------------------------------------------------------- /examples/progress.php: -------------------------------------------------------------------------------- 1 | setHeaders($headers); 40 | $table->setRows($data); 41 | $table->setRenderer(new \cli\table\Ascii([10, 10, 20, 5])); 42 | $table->display(); 43 | -------------------------------------------------------------------------------- /examples/tree.php: -------------------------------------------------------------------------------- 1 | array( 7 | 'Something Cool' => array( 8 | 'This is a 3rd layer', 9 | ), 10 | 'This is a 2nd layer', 11 | ), 12 | 'Other test' => array( 13 | 'This is awesome' => array( 14 | 'This is also cool', 15 | 'This is even cooler', 16 | 'Wow like what is this' => array( 17 | 'Awesome eh?', 18 | 'Totally' => array( 19 | 'Yep!' 20 | ), 21 | ), 22 | ), 23 | ), 24 | ); 25 | 26 | printf("ASCII:\n"); 27 | 28 | /** 29 | * ASCII should look something like this: 30 | * 31 | * -Test 32 | * |\-Something Cool 33 | * ||\-This is a 3rd layer 34 | * |\-This is a 2nd layer 35 | * \-Other test 36 | * \-This is awesome 37 | * \-This is also cool 38 | * \-This is even cooler 39 | * \-Wow like what is this 40 | * \-Awesome eh? 41 | * \-Totally 42 | * \-Yep! 43 | */ 44 | 45 | $tree = new \cli\Tree; 46 | $tree->setData($data); 47 | $tree->setRenderer(new \cli\tree\Ascii); 48 | $tree->display(); 49 | 50 | printf("\nMarkdown:\n"); 51 | 52 | /** 53 | * Markdown looks like this: 54 | * 55 | * - Test 56 | * - Something Cool 57 | * - This is a 3rd layer 58 | * - This is a 2nd layer 59 | * - Other test 60 | * - This is awesome 61 | * - This is also cool 62 | * - This is even cooler 63 | * - Wow like what is this 64 | * - Awesome eh? 65 | * - Totally 66 | * - Yep! 67 | */ 68 | 69 | $tree = new \cli\Tree; 70 | $tree->setData($data); 71 | $tree->setRenderer(new \cli\tree\Markdown(4)); 72 | $tree->display(); 73 | -------------------------------------------------------------------------------- /http-console.php: -------------------------------------------------------------------------------- 1 | _host = 'http://' . $host; 20 | $this->_prompt = '%K' . $this->_host . '%n/%K>%n '; 21 | } 22 | 23 | public function handleRequest($type, $path) { 24 | $request = new Buzz\Message\Request($type, $path, $this->_host); 25 | $response = new Buzz\Message\Response; 26 | 27 | $client = new Buzz\Client\FileGetContents(); 28 | $client->send($request, $response); 29 | 30 | // Display headers 31 | foreach ($response->getHeaders() as $i => $header) { 32 | if ($i == 0) { 33 | \cli\line('%G{:header}%n', compact('header')); 34 | continue; 35 | } 36 | 37 | list($key, $value) = explode(': ', $header, 2); 38 | \cli\line('%W{:key}%n: {:value}', compact('key', 'value')); 39 | } 40 | \cli\line("\n"); 41 | print $response->getContent() . "\n"; 42 | 43 | switch ($type) { 44 | } 45 | } 46 | 47 | public function run() { 48 | while (true) { 49 | $cmd = \cli\prompt($this->_prompt, false, null); 50 | 51 | if (preg_match('/^(HEAD|GET|POST|PUT|DELETE) (\S+)$/', $cmd, $matches)) { 52 | $this->handleRequest($matches[1], $matches[2]); 53 | continue; 54 | } 55 | 56 | if ($cmd == '\q') { 57 | break; 58 | } 59 | } 60 | } 61 | } 62 | 63 | try { 64 | $console = new HttpConsole(array_shift($argv) ?: '127.0.0.1:80'); 65 | $console->run(); 66 | } catch (\Exception $e) { 67 | \cli\err("\n\n%R" . $e->getMessage() . "%n\n"); 68 | } 69 | 70 | ?> 71 | -------------------------------------------------------------------------------- /lib/cli/Arguments.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | use cli\arguments\Argument; 16 | use cli\arguments\HelpScreen; 17 | use cli\arguments\InvalidArguments; 18 | use cli\arguments\Lexer; 19 | 20 | /** 21 | * Parses command line arguments. 22 | */ 23 | class Arguments implements \ArrayAccess { 24 | protected $_flags = array(); 25 | protected $_options = array(); 26 | protected $_strict = false; 27 | protected $_input = array(); 28 | protected $_invalid = array(); 29 | protected $_parsed; 30 | protected $_lexer; 31 | 32 | /** 33 | * Initializes the argument parser. If you wish to change the default behaviour 34 | * you may pass an array of options as the first argument. Valid options are 35 | * `'help'` and `'strict'`, each a boolean. 36 | * 37 | * `'help'` is `true` by default, `'strict'` is false by default. 38 | * 39 | * @param array $options An array of options for this parser. 40 | */ 41 | public function __construct($options = array()) { 42 | $options += array( 43 | 'strict' => false, 44 | 'input' => array_slice($_SERVER['argv'], 1) 45 | ); 46 | 47 | $this->_input = $options['input']; 48 | $this->setStrict($options['strict']); 49 | 50 | if (isset($options['flags'])) { 51 | $this->addFlags($options['flags']); 52 | } 53 | if (isset($options['options'])) { 54 | $this->addOptions($options['options']); 55 | } 56 | } 57 | 58 | /** 59 | * Get the list of arguments found by the defined definitions. 60 | * 61 | * @return array 62 | */ 63 | public function getArguments() { 64 | if (!isset($this->_parsed)) { 65 | $this->parse(); 66 | } 67 | return $this->_parsed; 68 | } 69 | 70 | public function getHelpScreen() { 71 | return new HelpScreen($this); 72 | } 73 | 74 | /** 75 | * Encodes the parsed arguments as JSON. 76 | * 77 | * @return string 78 | */ 79 | public function asJSON() { 80 | return json_encode($this->_parsed); 81 | } 82 | 83 | /** 84 | * Returns true if a given argument was parsed. 85 | * 86 | * @param mixed $offset An Argument object or the name of the argument. 87 | * @return bool 88 | */ 89 | #[\ReturnTypeWillChange] 90 | public function offsetExists($offset) { 91 | if ($offset instanceOf Argument) { 92 | $offset = $offset->key; 93 | } 94 | 95 | return array_key_exists($offset, $this->_parsed); 96 | } 97 | 98 | /** 99 | * Get the parsed argument's value. 100 | * 101 | * @param mixed $offset An Argument object or the name of the argument. 102 | * @return mixed 103 | */ 104 | #[\ReturnTypeWillChange] 105 | public function offsetGet($offset) { 106 | if ($offset instanceOf Argument) { 107 | $offset = $offset->key; 108 | } 109 | 110 | if (isset($this->_parsed[$offset])) { 111 | return $this->_parsed[$offset]; 112 | } 113 | } 114 | 115 | /** 116 | * Sets the value of a parsed argument. 117 | * 118 | * @param mixed $offset An Argument object or the name of the argument. 119 | * @param mixed $value The value to set 120 | */ 121 | #[\ReturnTypeWillChange] 122 | public function offsetSet($offset, $value) { 123 | if ($offset instanceOf Argument) { 124 | $offset = $offset->key; 125 | } 126 | 127 | $this->_parsed[$offset] = $value; 128 | } 129 | 130 | /** 131 | * Unset a parsed argument. 132 | * 133 | * @param mixed $offset An Argument object or the name of the argument. 134 | */ 135 | #[\ReturnTypeWillChange] 136 | public function offsetUnset($offset) { 137 | if ($offset instanceOf Argument) { 138 | $offset = $offset->key; 139 | } 140 | 141 | unset($this->_parsed[$offset]); 142 | } 143 | 144 | /** 145 | * Adds a flag (boolean argument) to the argument list. 146 | * 147 | * @param mixed $flag A string representing the flag, or an array of strings. 148 | * @param array $settings An array of settings for this flag. 149 | * @setting string description A description to be shown in --help. 150 | * @setting bool default The default value for this flag. 151 | * @setting bool stackable Whether the flag is repeatable to increase the value. 152 | * @setting array aliases Other ways to trigger this flag. 153 | * @return $this 154 | */ 155 | public function addFlag($flag, $settings = array()) { 156 | if (is_string($settings)) { 157 | $settings = array('description' => $settings); 158 | } 159 | if (is_array($flag)) { 160 | $settings['aliases'] = $flag; 161 | $flag = array_shift($settings['aliases']); 162 | } 163 | if (isset($this->_flags[$flag])) { 164 | $this->_warn('flag already exists: ' . $flag); 165 | return $this; 166 | } 167 | 168 | $settings += array( 169 | 'default' => false, 170 | 'stackable' => false, 171 | 'description' => null, 172 | 'aliases' => array() 173 | ); 174 | 175 | $this->_flags[$flag] = $settings; 176 | return $this; 177 | } 178 | 179 | /** 180 | * Add multiple flags at once. The input array should be keyed with the 181 | * primary flag character, and the values should be the settings array 182 | * used by {addFlag}. 183 | * 184 | * @param array $flags An array of flags to add 185 | * @return $this 186 | */ 187 | public function addFlags($flags) { 188 | foreach ($flags as $flag => $settings) { 189 | if (is_numeric($flag)) { 190 | $this->_warn('No flag character given'); 191 | continue; 192 | } 193 | 194 | $this->addFlag($flag, $settings); 195 | } 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Adds an option (string argument) to the argument list. 202 | * 203 | * @param mixed $option A string representing the option, or an array of strings. 204 | * @param array $settings An array of settings for this option. 205 | * @setting string description A description to be shown in --help. 206 | * @setting bool default The default value for this option. 207 | * @setting array aliases Other ways to trigger this option. 208 | * @return $this 209 | */ 210 | public function addOption($option, $settings = array()) { 211 | if (is_string($settings)) { 212 | $settings = array('description' => $settings); 213 | } 214 | if (is_array($option)) { 215 | $settings['aliases'] = $option; 216 | $option = array_shift($settings['aliases']); 217 | } 218 | if (isset($this->_options[$option])) { 219 | $this->_warn('option already exists: ' . $option); 220 | return $this; 221 | } 222 | 223 | $settings += array( 224 | 'default' => null, 225 | 'description' => null, 226 | 'aliases' => array() 227 | ); 228 | 229 | $this->_options[$option] = $settings; 230 | return $this; 231 | } 232 | 233 | /** 234 | * Add multiple options at once. The input array should be keyed with the 235 | * primary option string, and the values should be the settings array 236 | * used by {addOption}. 237 | * 238 | * @param array $options An array of options to add 239 | * @return $this 240 | */ 241 | public function addOptions($options) { 242 | foreach ($options as $option => $settings) { 243 | if (is_numeric($option)) { 244 | $this->_warn('No option string given'); 245 | continue; 246 | } 247 | 248 | $this->addOption($option, $settings); 249 | } 250 | 251 | return $this; 252 | } 253 | 254 | /** 255 | * Enable or disable strict mode. If strict mode is active any invalid 256 | * arguments found by the parser will throw `cli\arguments\InvalidArguments`. 257 | * 258 | * Even if strict is disabled, invalid arguments are logged and can be 259 | * retrieved with `cli\Arguments::getInvalidArguments()`. 260 | * 261 | * @param bool $strict True to enable, false to disable. 262 | * @return $this 263 | */ 264 | public function setStrict($strict) { 265 | $this->_strict = (bool)$strict; 266 | return $this; 267 | } 268 | 269 | /** 270 | * Get the list of invalid arguments the parser found. 271 | * 272 | * @return array 273 | */ 274 | public function getInvalidArguments() { 275 | return $this->_invalid; 276 | } 277 | 278 | /** 279 | * Get a flag by primary matcher or any defined aliases. 280 | * 281 | * @param mixed $flag Either a string representing the flag or an 282 | * cli\arguments\Argument object. 283 | * @return array 284 | */ 285 | public function getFlag($flag) { 286 | if ($flag instanceOf Argument) { 287 | $obj = $flag; 288 | $flag = $flag->value; 289 | } 290 | 291 | if (isset($this->_flags[$flag])) { 292 | return $this->_flags[$flag]; 293 | } 294 | 295 | foreach ($this->_flags as $master => $settings) { 296 | if (in_array($flag, (array)$settings['aliases'])) { 297 | if (isset($obj)) { 298 | $obj->key = $master; 299 | } 300 | 301 | $cache[$flag] =& $settings; 302 | return $settings; 303 | } 304 | } 305 | } 306 | 307 | public function getFlags() { 308 | return $this->_flags; 309 | } 310 | 311 | public function hasFlags() { 312 | return !empty($this->_flags); 313 | } 314 | 315 | /** 316 | * Returns true if the given argument is defined as a flag. 317 | * 318 | * @param mixed $argument Either a string representing the flag or an 319 | * cli\arguments\Argument object. 320 | * @return bool 321 | */ 322 | public function isFlag($argument) { 323 | return (null !== $this->getFlag($argument)); 324 | } 325 | 326 | /** 327 | * Returns true if the given flag is stackable. 328 | * 329 | * @param mixed $flag Either a string representing the flag or an 330 | * cli\arguments\Argument object. 331 | * @return bool 332 | */ 333 | public function isStackable($flag) { 334 | $settings = $this->getFlag($flag); 335 | 336 | return isset($settings) && (true === $settings['stackable']); 337 | } 338 | 339 | /** 340 | * Get an option by primary matcher or any defined aliases. 341 | * 342 | * @param mixed $option Either a string representing the option or an 343 | * cli\arguments\Argument object. 344 | * @return array 345 | */ 346 | public function getOption($option) { 347 | if ($option instanceOf Argument) { 348 | $obj = $option; 349 | $option = $option->value; 350 | } 351 | 352 | if (isset($this->_options[$option])) { 353 | return $this->_options[$option]; 354 | } 355 | 356 | foreach ($this->_options as $master => $settings) { 357 | if (in_array($option, (array)$settings['aliases'])) { 358 | if (isset($obj)) { 359 | $obj->key = $master; 360 | } 361 | 362 | return $settings; 363 | } 364 | } 365 | } 366 | 367 | public function getOptions() { 368 | return $this->_options; 369 | } 370 | 371 | public function hasOptions() { 372 | return !empty($this->_options); 373 | } 374 | 375 | /** 376 | * Returns true if the given argument is defined as an option. 377 | * 378 | * @param mixed $argument Either a string representing the option or an 379 | * cli\arguments\Argument object. 380 | * @return bool 381 | */ 382 | public function isOption($argument) { 383 | return (null != $this->getOption($argument)); 384 | } 385 | 386 | /** 387 | * Parses the argument list with the given options. The returned argument list 388 | * will use either the first long name given or the first name in the list 389 | * if a long name is not given. 390 | * 391 | * @return array 392 | * @throws arguments\InvalidArguments 393 | */ 394 | public function parse() { 395 | $this->_invalid = array(); 396 | $this->_parsed = array(); 397 | $this->_lexer = new Lexer($this->_input); 398 | 399 | $this->_applyDefaults(); 400 | 401 | foreach ($this->_lexer as $argument) { 402 | if ($this->_parseFlag($argument)) { 403 | continue; 404 | } 405 | if ($this->_parseOption($argument)) { 406 | continue; 407 | } 408 | 409 | array_push($this->_invalid, $argument->raw); 410 | } 411 | 412 | if ($this->_strict && !empty($this->_invalid)) { 413 | throw new InvalidArguments($this->_invalid); 414 | } 415 | } 416 | 417 | /** 418 | * This applies the default values, if any, of all of the 419 | * flags and options, so that if there is a default value 420 | * it will be available. 421 | */ 422 | private function _applyDefaults() { 423 | foreach($this->_flags as $flag => $settings) { 424 | $this[$flag] = $settings['default']; 425 | } 426 | 427 | foreach($this->_options as $option => $settings) { 428 | // If the default is 0 we should still let it be set. 429 | if (!empty($settings['default']) || $settings['default'] === 0) { 430 | $this[$option] = $settings['default']; 431 | } 432 | } 433 | } 434 | 435 | private function _warn($message) { 436 | trigger_error('[' . __CLASS__ .'] ' . $message, E_USER_WARNING); 437 | } 438 | 439 | private function _parseFlag($argument) { 440 | if (!$this->isFlag($argument)) { 441 | return false; 442 | } 443 | 444 | if ($this->isStackable($argument)) { 445 | if (!isset($this[$argument])) { 446 | $this[$argument->key] = 0; 447 | } 448 | 449 | $this[$argument->key] += 1; 450 | } else { 451 | $this[$argument->key] = true; 452 | } 453 | 454 | return true; 455 | } 456 | 457 | private function _parseOption($option) { 458 | if (!$this->isOption($option)) { 459 | return false; 460 | } 461 | 462 | // Peak ahead to make sure we get a value. 463 | if ($this->_lexer->end() || !$this->_lexer->peek->isValue) { 464 | $optionSettings = $this->getOption($option->key); 465 | 466 | if (empty($optionSettings['default']) && $optionSettings !== 0) { 467 | // Oops! Got no value and no default , throw a warning and continue. 468 | $this->_warn('no value given for ' . $option->raw); 469 | $this[$option->key] = null; 470 | } else { 471 | // No value and we have a default, so we set to the default 472 | $this[$option->key] = $optionSettings['default']; 473 | } 474 | return true; 475 | } 476 | 477 | // Store as array and join to string after looping for values 478 | $values = array(); 479 | 480 | // Loop until we find a flag in peak-ahead 481 | foreach ($this->_lexer as $value) { 482 | array_push($values, $value->raw); 483 | 484 | if (!$this->_lexer->end() && !$this->_lexer->peek->isValue) { 485 | break; 486 | } 487 | } 488 | 489 | $this[$option->key] = join(' ', $values); 490 | return true; 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /lib/cli/Colors.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | /** 16 | * Change the color of text. 17 | * 18 | * Reference: http://graphcomp.com/info/specs/ansi_col.html#colors 19 | */ 20 | class Colors { 21 | static protected $_colors = array( 22 | 'color' => array( 23 | 'black' => 30, 24 | 'red' => 31, 25 | 'green' => 32, 26 | 'yellow' => 33, 27 | 'blue' => 34, 28 | 'magenta' => 35, 29 | 'cyan' => 36, 30 | 'white' => 37 31 | ), 32 | 'style' => array( 33 | 'bright' => 1, 34 | 'dim' => 2, 35 | 'underline' => 4, 36 | 'blink' => 5, 37 | 'reverse' => 7, 38 | 'hidden' => 8 39 | ), 40 | 'background' => array( 41 | 'black' => 40, 42 | 'red' => 41, 43 | 'green' => 42, 44 | 'yellow' => 43, 45 | 'blue' => 44, 46 | 'magenta' => 45, 47 | 'cyan' => 46, 48 | 'white' => 47 49 | ) 50 | ); 51 | static protected $_enabled = null; 52 | 53 | static protected $_string_cache = array(); 54 | 55 | static public function enable($force = true) { 56 | self::$_enabled = $force === true ? true : null; 57 | } 58 | 59 | static public function disable($force = true) { 60 | self::$_enabled = $force === true ? false : null; 61 | } 62 | 63 | /** 64 | * Check if we should colorize output based on local flags and shell type. 65 | * 66 | * Only check the shell type if `Colors::$_enabled` is null and `$colored` is null. 67 | */ 68 | static public function shouldColorize($colored = null) { 69 | return self::$_enabled === true || 70 | (self::$_enabled !== false && 71 | ($colored === true || 72 | ($colored !== false && Streams::isTty()))); 73 | } 74 | 75 | /** 76 | * Set the color. 77 | * 78 | * @param string $color The name of the color or style to set. 79 | * @return string 80 | */ 81 | static public function color($color) { 82 | if (!is_array($color)) { 83 | $color = compact('color'); 84 | } 85 | 86 | $color += array('color' => null, 'style' => null, 'background' => null); 87 | 88 | if ($color['color'] == 'reset') { 89 | return "\033[0m"; 90 | } 91 | 92 | $colors = array(); 93 | foreach (array('color', 'style', 'background') as $type) { 94 | $code = $color[$type]; 95 | if (isset(self::$_colors[$type][$code])) { 96 | $colors[] = self::$_colors[$type][$code]; 97 | } 98 | } 99 | 100 | if (empty($colors)) { 101 | $colors[] = 0; 102 | } 103 | 104 | return "\033[" . join(';', $colors) . "m"; 105 | } 106 | 107 | /** 108 | * Colorize a string using helpful string formatters. If the `Streams::$out` points to a TTY coloring will be enabled, 109 | * otherwise disabled. You can control this check with the `$colored` parameter. 110 | * 111 | * @param string $string 112 | * @param boolean $colored Force enable or disable the colorized output. If left as `null` the TTY will control coloring. 113 | * @return string 114 | */ 115 | static public function colorize($string, $colored = null) { 116 | $passed = $string; 117 | 118 | if (!self::shouldColorize($colored)) { 119 | $return = self::decolorize( $passed, 2 /*keep_encodings*/ ); 120 | self::cacheString($passed, $return); 121 | return $return; 122 | } 123 | 124 | $md5 = md5($passed); 125 | if (isset(self::$_string_cache[$md5]['colorized'])) { 126 | return self::$_string_cache[$md5]['colorized']; 127 | } 128 | 129 | $string = str_replace('%%', '%¾', $string); 130 | 131 | foreach (self::getColors() as $key => $value) { 132 | $string = str_replace($key, self::color($value), $string); 133 | } 134 | 135 | $string = str_replace('%¾', '%', $string); 136 | self::cacheString($passed, $string); 137 | 138 | return $string; 139 | } 140 | 141 | /** 142 | * Remove color information from a string. 143 | * 144 | * @param string $string A string with color information. 145 | * @param int $keep Optional. If the 1 bit is set, color tokens (eg "%n") won't be stripped. If the 2 bit is set, color encodings (ANSI escapes) won't be stripped. Default 0. 146 | * @return string A string with color information removed. 147 | */ 148 | static public function decolorize( $string, $keep = 0 ) { 149 | $string = (string) $string; 150 | 151 | if ( ! ( $keep & 1 ) ) { 152 | // Get rid of color tokens if they exist 153 | $string = str_replace('%%', '%¾', $string); 154 | $string = str_replace(array_keys(self::getColors()), '', $string); 155 | $string = str_replace('%¾', '%', $string); 156 | } 157 | 158 | if ( ! ( $keep & 2 ) ) { 159 | // Remove color encoding if it exists 160 | foreach (self::getColors() as $key => $value) { 161 | $string = str_replace(self::color($value), '', $string); 162 | } 163 | } 164 | 165 | return $string; 166 | } 167 | 168 | /** 169 | * Cache the original, colorized, and decolorized versions of a string. 170 | * 171 | * @param string $passed The original string before colorization. 172 | * @param string $colorized The string after running through self::colorize. 173 | * @param string $deprecated Optional. Not used. Default null. 174 | */ 175 | static public function cacheString( $passed, $colorized, $deprecated = null ) { 176 | self::$_string_cache[md5($passed)] = array( 177 | 'passed' => $passed, 178 | 'colorized' => $colorized, 179 | 'decolorized' => self::decolorize($passed), // Not very useful but keep for BC. 180 | ); 181 | } 182 | 183 | /** 184 | * Return the length of the string without color codes. 185 | * 186 | * @param string $string the string to measure 187 | * @return int 188 | */ 189 | static public function length($string) { 190 | return safe_strlen( self::decolorize( $string ) ); 191 | } 192 | 193 | /** 194 | * Return the width (length in characters) of the string without color codes if enabled. 195 | * 196 | * @param string $string The string to measure. 197 | * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. 198 | * @param string|bool $encoding Optional. The encoding of the string. Default false. 199 | * @return int 200 | */ 201 | static public function width( $string, $pre_colorized = false, $encoding = false ) { 202 | return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized ? 1 /*keep_tokens*/ : 0 ) : $string, $encoding ); 203 | } 204 | 205 | /** 206 | * Pad the string to a certain display length. 207 | * 208 | * @param string $string The string to pad. 209 | * @param int $length The display length. 210 | * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. 211 | * @param string|bool $encoding Optional. The encoding of the string. Default false. 212 | * @param int $pad_type Optional. Can be STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH. If pad_type is not specified it is assumed to be STR_PAD_RIGHT. 213 | * @return string 214 | */ 215 | static public function pad( $string, $length, $pre_colorized = false, $encoding = false, $pad_type = STR_PAD_RIGHT ) { 216 | $string = (string) $string; 217 | 218 | $real_length = self::width( $string, $pre_colorized, $encoding ); 219 | $diff = strlen( $string ) - $real_length; 220 | $length += $diff; 221 | 222 | return str_pad( $string, $length, ' ', $pad_type ); 223 | } 224 | 225 | /** 226 | * Get the color mapping array. 227 | * 228 | * @return array Array of color tokens mapped to colors and styles. 229 | */ 230 | static public function getColors() { 231 | return array( 232 | '%y' => array('color' => 'yellow'), 233 | '%g' => array('color' => 'green'), 234 | '%b' => array('color' => 'blue'), 235 | '%r' => array('color' => 'red'), 236 | '%p' => array('color' => 'magenta'), 237 | '%m' => array('color' => 'magenta'), 238 | '%c' => array('color' => 'cyan'), 239 | '%w' => array('color' => 'white'), 240 | '%k' => array('color' => 'black'), 241 | '%n' => array('color' => 'reset'), 242 | '%Y' => array('color' => 'yellow', 'style' => 'bright'), 243 | '%G' => array('color' => 'green', 'style' => 'bright'), 244 | '%B' => array('color' => 'blue', 'style' => 'bright'), 245 | '%R' => array('color' => 'red', 'style' => 'bright'), 246 | '%P' => array('color' => 'magenta', 'style' => 'bright'), 247 | '%M' => array('color' => 'magenta', 'style' => 'bright'), 248 | '%C' => array('color' => 'cyan', 'style' => 'bright'), 249 | '%W' => array('color' => 'white', 'style' => 'bright'), 250 | '%K' => array('color' => 'black', 'style' => 'bright'), 251 | '%N' => array('color' => 'reset', 'style' => 'bright'), 252 | '%3' => array('background' => 'yellow'), 253 | '%2' => array('background' => 'green'), 254 | '%4' => array('background' => 'blue'), 255 | '%1' => array('background' => 'red'), 256 | '%5' => array('background' => 'magenta'), 257 | '%6' => array('background' => 'cyan'), 258 | '%7' => array('background' => 'white'), 259 | '%0' => array('background' => 'black'), 260 | '%F' => array('style' => 'blink'), 261 | '%U' => array('style' => 'underline'), 262 | '%8' => array('style' => 'reverse'), 263 | '%9' => array('style' => 'bright'), 264 | '%_' => array('style' => 'bright') 265 | ); 266 | } 267 | 268 | /** 269 | * Get the cached string values. 270 | * 271 | * @return array The cached string values. 272 | */ 273 | static public function getStringCache() { 274 | return self::$_string_cache; 275 | } 276 | 277 | /** 278 | * Clear the string cache. 279 | */ 280 | static public function clearStringCache() { 281 | self::$_string_cache = array(); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /lib/cli/Memoize.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | abstract class Memoize { 16 | protected $_memoCache = array(); 17 | 18 | public function __get($name) { 19 | if (isset($this->_memoCache[$name])) { 20 | return $this->_memoCache[$name]; 21 | } 22 | 23 | // Hide probable private methods 24 | if (0 == strncmp($name, '_', 1)) { 25 | return ($this->_memoCache[$name] = null); 26 | } 27 | 28 | if (!method_exists($this, $name)) { 29 | return ($this->_memoCache[$name] = null); 30 | } 31 | 32 | $method = array($this, $name); 33 | ($this->_memoCache[$name] = call_user_func($method)); 34 | return $this->_memoCache[$name]; 35 | } 36 | 37 | protected function _unmemo($name) { 38 | if ($name === true) { 39 | $this->_memoCache = array(); 40 | } else { 41 | unset($this->_memoCache[$name]); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/cli/Notify.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | use cli\Streams; 16 | 17 | /** 18 | * The `Notify` class is the basis of all feedback classes, such as Indicators 19 | * and Progress meters. The default behaviour is to refresh output after 100ms 20 | * have passed. This is done to preventing the screen from flickering and keep 21 | * slowdowns from output to a minimum. 22 | * 23 | * The most basic form of Notifier has no maxim, and simply displays a series 24 | * of characters to indicate progress is being made. 25 | */ 26 | abstract class Notify { 27 | protected $_current = 0; 28 | protected $_first = true; 29 | protected $_interval; 30 | protected $_message; 31 | protected $_start; 32 | protected $_timer; 33 | protected $_tick; 34 | protected $_iteration = 0; 35 | protected $_speed = 0; 36 | 37 | /** 38 | * Instatiates a Notification object. 39 | * 40 | * @param string $msg The text to display next to the Notifier. 41 | * @param int $interval The interval in milliseconds between updates. 42 | */ 43 | public function __construct($msg, $interval = 100) { 44 | $this->_message = $msg; 45 | $this->_interval = (int)$interval; 46 | } 47 | 48 | /** 49 | * This method should be used to print out the Notifier. This method is 50 | * called from `cli\Notify::tick()` after `cli\Notify::$_interval` has passed. 51 | * 52 | * @abstract 53 | * @param boolean $finish 54 | * @see cli\Notify::tick() 55 | */ 56 | abstract public function display($finish = false); 57 | 58 | /** 59 | * Reset the notifier state so the same instance can be used in multiple loops. 60 | */ 61 | public function reset() { 62 | $this->_current = 0; 63 | $this->_first = true; 64 | $this->_start = null; 65 | $this->_timer = null; 66 | } 67 | 68 | /** 69 | * Returns the formatted tick count. 70 | * 71 | * @return string The formatted tick count. 72 | */ 73 | public function current() { 74 | return number_format($this->_current); 75 | } 76 | 77 | /** 78 | * Calculates the time elapsed since the Notifier was first ticked. 79 | * 80 | * @return int The elapsed time in seconds. 81 | */ 82 | public function elapsed() { 83 | if (!$this->_start) { 84 | return 0; 85 | } 86 | 87 | $elapsed = time() - $this->_start; 88 | return $elapsed; 89 | } 90 | 91 | /** 92 | * Calculates the speed (number of ticks per second) at which the Notifier 93 | * is being updated. 94 | * 95 | * @return int The number of ticks performed in 1 second. 96 | */ 97 | public function speed() { 98 | if (!$this->_start) { 99 | return 0; 100 | } else if (!$this->_tick) { 101 | $this->_tick = $this->_start; 102 | } 103 | 104 | $now = microtime(true); 105 | $span = $now - $this->_tick; 106 | if ($span > 1) { 107 | $this->_iteration++; 108 | $this->_tick = $now; 109 | $this->_speed = ($this->_current / $this->_iteration) / $span; 110 | } 111 | 112 | return $this->_speed; 113 | } 114 | 115 | /** 116 | * Takes a time span given in seconds and formats it for display. The 117 | * returned string will be in MM:SS form. 118 | * 119 | * @param int $time The time span in seconds to format. 120 | * @return string The formatted time span. 121 | */ 122 | public function formatTime($time) { 123 | return floor($time / 60) . ':' . str_pad($time % 60, 2, 0, STR_PAD_LEFT); 124 | } 125 | 126 | /** 127 | * Finish our Notification display. Should be called after the Notifier is 128 | * no longer needed. 129 | * 130 | * @see cli\Notify::display() 131 | */ 132 | public function finish() { 133 | Streams::out("\r"); 134 | $this->display(true); 135 | Streams::line(); 136 | } 137 | 138 | /** 139 | * Increments are tick counter by the given amount. If no amount is provided, 140 | * the ticker is incremented by 1. 141 | * 142 | * @param int $increment The amount to increment by. 143 | */ 144 | public function increment($increment = 1) { 145 | $this->_current += $increment; 146 | } 147 | 148 | /** 149 | * Determines whether the display should be updated or not according to 150 | * our interval setting. 151 | * 152 | * @return boolean `true` if the display should be updated, `false` otherwise. 153 | */ 154 | public function shouldUpdate() { 155 | $now = microtime(true) * 1000; 156 | 157 | if (empty($this->_timer)) { 158 | $this->_start = (int)(($this->_timer = $now) / 1000); 159 | return true; 160 | } 161 | 162 | if (($now - $this->_timer) > $this->_interval) { 163 | $this->_timer = $now; 164 | return true; 165 | } 166 | return false; 167 | } 168 | 169 | /** 170 | * This method is the meat of all Notifiers. First we increment the ticker 171 | * and then update the display if enough time has passed since our last tick. 172 | * 173 | * @param int $increment The amount to increment by. 174 | * @see cli\Notify::increment() 175 | * @see cli\Notify::shouldUpdate() 176 | * @see cli\Notify::display() 177 | */ 178 | public function tick($increment = 1) { 179 | $this->increment($increment); 180 | 181 | if ($this->shouldUpdate()) { 182 | Streams::out("\r"); 183 | $this->display(); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /lib/cli/Progress.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | /** 16 | * A more complex type of Notifier, `Progress` Notifiers always have a maxim 17 | * value and generally show some form of percent complete or estimated time 18 | * to completion along with the standard Notifier displays. 19 | * 20 | * @see cli\Notify 21 | */ 22 | abstract class Progress extends \cli\Notify { 23 | protected $_total = 0; 24 | 25 | /** 26 | * Instantiates a Progress Notifier. 27 | * 28 | * @param string $msg The text to display next to the Notifier. 29 | * @param int $total The total number of ticks we will be performing. 30 | * @param int $interval The interval in milliseconds between updates. 31 | * @see cli\Progress::setTotal() 32 | */ 33 | public function __construct($msg, $total, $interval = 100) { 34 | parent::__construct($msg, $interval); 35 | $this->setTotal($total); 36 | } 37 | 38 | /** 39 | * Set the max increments for this progress notifier. 40 | * 41 | * @param int $total The total number of times this indicator should be `tick`ed. 42 | * @throws \InvalidArgumentException Thrown if the `$total` is less than 0. 43 | */ 44 | public function setTotal($total) { 45 | $this->_total = (int)$total; 46 | 47 | if ($this->_total < 0) { 48 | throw new \InvalidArgumentException('Maximum value out of range, must be positive.'); 49 | } 50 | } 51 | 52 | /** 53 | * Reset the progress state so the same instance can be used in multiple loops. 54 | */ 55 | public function reset($total = null) { 56 | parent::reset(); 57 | 58 | if ($total) { 59 | $this->setTotal($total); 60 | } 61 | } 62 | 63 | /** 64 | * Behaves in a similar manner to `cli\Notify::current()`, but the output 65 | * is padded to match the length of `cli\Progress::total()`. 66 | * 67 | * @return string The formatted and padded tick count. 68 | * @see cli\Progress::total() 69 | */ 70 | public function current() { 71 | $size = strlen($this->total()); 72 | return str_pad(parent::current(), $size); 73 | } 74 | 75 | /** 76 | * Returns the formatted total expected ticks. 77 | * 78 | * @return string The formatted total ticks. 79 | */ 80 | public function total() { 81 | return number_format($this->_total); 82 | } 83 | 84 | /** 85 | * Calculates the estimated total time for the tick count to reach the 86 | * total ticks given. 87 | * 88 | * @return int The estimated total number of seconds for all ticks to be 89 | * completed. This is not the estimated time left, but total. 90 | * @see cli\Notify::speed() 91 | * @see cli\Notify::elapsed() 92 | */ 93 | public function estimated() { 94 | $speed = $this->speed(); 95 | if (!$speed || !$this->elapsed()) { 96 | return 0; 97 | } 98 | 99 | $estimated = round($this->_total / $speed); 100 | return $estimated; 101 | } 102 | 103 | /** 104 | * Forces the current tick count to the total ticks given at instatiation 105 | * time before passing on to `cli\Notify::finish()`. 106 | */ 107 | public function finish() { 108 | $this->_current = $this->_total; 109 | parent::finish(); 110 | } 111 | 112 | /** 113 | * Increments are tick counter by the given amount. If no amount is provided, 114 | * the ticker is incremented by 1. 115 | * 116 | * @param int $increment The amount to increment by. 117 | */ 118 | public function increment($increment = 1) { 119 | $this->_current = min($this->_total, $this->_current + $increment); 120 | } 121 | 122 | /** 123 | * Calculate the percentage completed. 124 | * 125 | * @return float The percent completed. 126 | */ 127 | public function percent() { 128 | if ($this->_total == 0) { 129 | return 1; 130 | } 131 | 132 | return ($this->_current / $this->_total); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/cli/Shell.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | /** 16 | * The `Shell` class is a utility class for shell related tasks such as 17 | * information on width. 18 | */ 19 | class Shell { 20 | 21 | /** 22 | * Returns the number of columns the current shell has for display. 23 | * 24 | * @return int The number of columns. 25 | * @todo Test on more systems. 26 | */ 27 | static public function columns() { 28 | static $columns; 29 | 30 | if ( getenv( 'PHP_CLI_TOOLS_TEST_SHELL_COLUMNS_RESET' ) ) { 31 | $columns = null; 32 | } 33 | if ( null === $columns ) { 34 | if ( function_exists( 'exec' ) ) { 35 | if ( self::is_windows() ) { 36 | // Cater for shells such as Cygwin and Git bash where `mode CON` returns an incorrect value for columns. 37 | if ( ( $shell = getenv( 'SHELL' ) ) && preg_match( '/(?:bash|zsh)(?:\.exe)?$/', $shell ) && getenv( 'TERM' ) ) { 38 | $columns = (int) exec( 'tput cols' ); 39 | } 40 | if ( ! $columns ) { 41 | $return_var = -1; 42 | $output = array(); 43 | exec( 'mode CON', $output, $return_var ); 44 | if ( 0 === $return_var && $output ) { 45 | // Look for second line ending in ": " (searching for "Columns:" will fail on non-English locales). 46 | if ( preg_match( '/:\s*[0-9]+\n[^:]+:\s*([0-9]+)\n/', implode( "\n", $output ), $matches ) ) { 47 | $columns = (int) $matches[1]; 48 | } 49 | } 50 | } 51 | } else { 52 | if ( ! ( $columns = (int) getenv( 'COLUMNS' ) ) ) { 53 | $size = exec( '/usr/bin/env stty size 2>/dev/null' ); 54 | if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) { 55 | $columns = (int) $matches[1]; 56 | } 57 | if ( ! $columns ) { 58 | if ( getenv( 'TERM' ) ) { 59 | $columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' ); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | if ( ! $columns ) { 67 | $columns = 80; // default width of cmd window on Windows OS 68 | } 69 | } 70 | 71 | return $columns; 72 | } 73 | 74 | /** 75 | * Checks whether the output of the current script is a TTY or a pipe / redirect 76 | * 77 | * Returns true if STDOUT output is being redirected to a pipe or a file; false is 78 | * output is being sent directly to the terminal. 79 | * 80 | * If an env variable SHELL_PIPE exists, returned result depends it's 81 | * value. Strings like 1, 0, yes, no, that validate to booleans are accepted. 82 | * 83 | * To enable ASCII formatting even when shell is piped, use the 84 | * ENV variable SHELL_PIPE=0 85 | * 86 | * @return bool 87 | */ 88 | static public function isPiped() { 89 | $shellPipe = getenv('SHELL_PIPE'); 90 | 91 | if ($shellPipe !== false) { 92 | return filter_var($shellPipe, FILTER_VALIDATE_BOOLEAN); 93 | } else { 94 | if ( function_exists('stream_isatty') ) { 95 | return !stream_isatty(STDOUT); 96 | } else { 97 | return (function_exists('posix_isatty') && !posix_isatty(STDOUT)); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Uses `stty` to hide input/output completely. 104 | * @param boolean $hidden Will hide/show the next data. Defaults to true. 105 | */ 106 | static public function hide($hidden = true) { 107 | system( 'stty ' . ( $hidden? '-echo' : 'echo' ) ); 108 | } 109 | 110 | /** 111 | * Is this shell in Windows? 112 | * 113 | * @return bool 114 | */ 115 | static private function is_windows() { 116 | return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 117 | } 118 | 119 | } 120 | 121 | ?> 122 | -------------------------------------------------------------------------------- /lib/cli/Streams.php: -------------------------------------------------------------------------------- 1 | $value ) { 57 | $msg = str_replace( '{:' . $key . '}', $value, $msg ); 58 | } 59 | return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg; 60 | } 61 | 62 | /** 63 | * Shortcut for printing to `STDOUT`. The message and parameters are passed 64 | * through `sprintf` before output. 65 | * 66 | * @param string $msg The message to output in `printf` format. 67 | * @param mixed ... Either scalar arguments or a single array argument. 68 | * @return void 69 | * @see \cli\render() 70 | */ 71 | public static function out( $msg ) { 72 | fwrite( static::$out, self::_call( 'render', func_get_args() ) ); 73 | } 74 | 75 | /** 76 | * Pads `$msg` to the width of the shell before passing to `cli\out`. 77 | * 78 | * @param string $msg The message to pad and pass on. 79 | * @param mixed ... Either scalar arguments or a single array argument. 80 | * @return void 81 | * @see cli\out() 82 | */ 83 | public static function out_padded( $msg ) { 84 | $msg = self::_call( 'render', func_get_args() ); 85 | self::out( str_pad( $msg, \cli\Shell::columns() ) ); 86 | } 87 | 88 | /** 89 | * Prints a message to `STDOUT` with a newline appended. See `\cli\out` for 90 | * more documentation. 91 | * 92 | * @see cli\out() 93 | */ 94 | public static function line( $msg = '' ) { 95 | // func_get_args is empty if no args are passed even with the default above. 96 | $args = array_merge( func_get_args(), array( '' ) ); 97 | $args[0] .= "\n"; 98 | 99 | self::_call( 'out', $args ); 100 | } 101 | 102 | /** 103 | * Shortcut for printing to `STDERR`. The message and parameters are passed 104 | * through `sprintf` before output. 105 | * 106 | * @param string $msg The message to output in `printf` format. With no string, 107 | * a newline is printed. 108 | * @param mixed ... Either scalar arguments or a single array argument. 109 | * @return void 110 | */ 111 | public static function err( $msg = '' ) { 112 | // func_get_args is empty if no args are passed even with the default above. 113 | $args = array_merge( func_get_args(), array( '' ) ); 114 | $args[0] .= "\n"; 115 | fwrite( static::$err, self::_call( 'render', $args ) ); 116 | } 117 | 118 | /** 119 | * Takes input from `STDIN` in the given format. If an end of transmission 120 | * character is sent (^D), an exception is thrown. 121 | * 122 | * @param string $format A valid input format. See `fscanf` for documentation. 123 | * If none is given, all input up to the first newline 124 | * is accepted. 125 | * @param boolean $hide If true will hide what the user types in. 126 | * @return string The input with whitespace trimmed. 127 | * @throws \Exception Thrown if ctrl-D (EOT) is sent as input. 128 | */ 129 | public static function input( $format = null, $hide = false ) { 130 | if ( $hide ) 131 | Shell::hide(); 132 | 133 | if( $format ) { 134 | fscanf( static::$in, $format . "\n", $line ); 135 | } else { 136 | $line = fgets( static::$in ); 137 | } 138 | 139 | if ( $hide ) { 140 | Shell::hide( false ); 141 | echo "\n"; 142 | } 143 | 144 | if( $line === false ) { 145 | throw new \Exception( 'Caught ^D during input' ); 146 | } 147 | 148 | return trim( $line ); 149 | } 150 | 151 | /** 152 | * Displays an input prompt. If no default value is provided the prompt will 153 | * continue displaying until input is received. 154 | * 155 | * @param string $question The question to ask the user. 156 | * @param bool|string $default A default value if the user provides no input. 157 | * @param string $marker A string to append to the question and default value 158 | * on display. 159 | * @param boolean $hide Optionally hides what the user types in. 160 | * @return string The users input. 161 | * @see cli\input() 162 | */ 163 | public static function prompt( $question, $default = false, $marker = ': ', $hide = false ) { 164 | if( $default && strpos( $question, '[' ) === false ) { 165 | $question .= ' [' . $default . ']'; 166 | } 167 | 168 | while( true ) { 169 | self::out( $question . $marker ); 170 | $line = self::input( null, $hide ); 171 | 172 | if ( trim( $line ) !== '' ) 173 | return $line; 174 | if( $default !== false ) 175 | return $default; 176 | } 177 | } 178 | 179 | /** 180 | * Presents a user with a multiple choice question, useful for 'yes/no' type 181 | * questions (which this public static function defaults too). 182 | * 183 | * @param string $question The question to ask the user. 184 | * @param string $choice A string of characters allowed as a response. Case is ignored. 185 | * @param string $default The default choice. NULL if a default is not allowed. 186 | * @return string The users choice. 187 | * @see cli\prompt() 188 | */ 189 | public static function choose( $question, $choice = 'yn', $default = 'n' ) { 190 | if( !is_string( $choice ) ) { 191 | $choice = join( '', $choice ); 192 | } 193 | 194 | // Make every choice character lowercase except the default 195 | $choice = str_ireplace( $default, strtoupper( $default ), strtolower( $choice ) ); 196 | // Seperate each choice with a forward-slash 197 | $choices = trim( join( '/', preg_split( '//', $choice ) ), '/' ); 198 | 199 | while( true ) { 200 | $line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default, '' ); 201 | 202 | if( stripos( $choice, $line ) !== false ) { 203 | return strtolower( $line ); 204 | } 205 | if( !empty( $default ) ) { 206 | return strtolower( $default ); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * Displays an array of strings as a menu where a user can enter a number to 213 | * choose an option. The array must be a single dimension with either strings 214 | * or objects with a `__toString()` method. 215 | * 216 | * @param array $items The list of items the user can choose from. 217 | * @param string $default The index of the default item. 218 | * @param string $title The message displayed to the user when prompted. 219 | * @return string The index of the chosen item. 220 | * @see cli\line() 221 | * @see cli\input() 222 | * @see cli\err() 223 | */ 224 | public static function menu( $items, $default = null, $title = 'Choose an item' ) { 225 | $map = array_values( $items ); 226 | 227 | if( $default && strpos( $title, '[' ) === false && isset( $items[$default] ) ) { 228 | $title .= ' [' . $items[$default] . ']'; 229 | } 230 | 231 | foreach( $map as $idx => $item ) { 232 | self::line( ' %d. %s', $idx + 1, (string)$item ); 233 | } 234 | self::line(); 235 | 236 | while( true ) { 237 | fwrite( static::$out, sprintf( '%s: ', $title ) ); 238 | $line = self::input(); 239 | 240 | if( is_numeric( $line ) ) { 241 | $line--; 242 | if( isset( $map[$line] ) ) { 243 | return array_search( $map[$line], $items ); 244 | } 245 | 246 | if( $line < 0 || $line >= count( $map ) ) { 247 | self::err( 'Invalid menu selection: out of range' ); 248 | } 249 | } else if( isset( $default ) ) { 250 | return $default; 251 | } 252 | } 253 | } 254 | 255 | /** 256 | * Sets one of the streams (input, output, or error) to a `stream` type resource. 257 | * 258 | * Valid $whichStream values are: 259 | * - 'in' (default: STDIN) 260 | * - 'out' (default: STDOUT) 261 | * - 'err' (default: STDERR) 262 | * 263 | * Any custom streams will be closed for you on shutdown, so please don't close stream 264 | * resources used with this method. 265 | * 266 | * @param string $whichStream The stream property to update 267 | * @param resource $stream The new stream resource to use 268 | * @return void 269 | * @throws \Exception Thrown if $stream is not a resource of the 'stream' type. 270 | */ 271 | public static function setStream( $whichStream, $stream ) { 272 | if( !is_resource( $stream ) || get_resource_type( $stream ) !== 'stream' ) { 273 | throw new \Exception( 'Invalid resource type!' ); 274 | } 275 | if( property_exists( __CLASS__, $whichStream ) ) { 276 | static::${$whichStream} = $stream; 277 | } 278 | register_shutdown_function( function() use ($stream) { 279 | fclose( $stream ); 280 | } ); 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /lib/cli/Table.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | use cli\Shell; 16 | use cli\Streams; 17 | use cli\table\Ascii; 18 | use cli\table\Renderer; 19 | use cli\table\Tabular; 20 | 21 | /** 22 | * The `Table` class is used to display data in a tabular format. 23 | */ 24 | class Table { 25 | protected $_renderer; 26 | protected $_headers = array(); 27 | protected $_footers = array(); 28 | protected $_width = array(); 29 | protected $_rows = array(); 30 | 31 | /** 32 | * Initializes the `Table` class. 33 | * 34 | * There are 3 ways to instantiate this class: 35 | * 36 | * 1. Pass an array of strings as the first parameter for the column headers 37 | * and a 2-dimensional array as the second parameter for the data rows. 38 | * 2. Pass an array of hash tables (string indexes instead of numerical) 39 | * where each hash table is a row and the indexes of the *first* hash 40 | * table are used as the header values. 41 | * 3. Pass nothing and use `setHeaders()` and `addRow()` or `setRows()`. 42 | * 43 | * @param array $headers Headers used in this table. Optional. 44 | * @param array $rows The rows of data for this table. Optional. 45 | * @param array $footers Footers used in this table. Optional. 46 | */ 47 | public function __construct(array $headers = array(), array $rows = array(), array $footers = array()) { 48 | if (!empty($headers)) { 49 | // If all the rows is given in $headers we use the keys from the 50 | // first row for the header values 51 | if ($rows === array()) { 52 | $rows = $headers; 53 | $keys = array_keys(array_shift($headers)); 54 | $headers = array(); 55 | 56 | foreach ($keys as $header) { 57 | $headers[$header] = $header; 58 | } 59 | } 60 | 61 | $this->setHeaders($headers); 62 | $this->setRows($rows); 63 | } 64 | 65 | if (!empty($footers)) { 66 | $this->setFooters($footers); 67 | } 68 | 69 | if (Shell::isPiped()) { 70 | $this->setRenderer(new Tabular()); 71 | } else { 72 | $this->setRenderer(new Ascii()); 73 | } 74 | } 75 | 76 | public function resetTable() 77 | { 78 | $this->_headers = array(); 79 | $this->_width = array(); 80 | $this->_rows = array(); 81 | $this->_footers = array(); 82 | return $this; 83 | } 84 | 85 | /** 86 | * Sets the renderer used by this table. 87 | * 88 | * @param table\Renderer $renderer The renderer to use for output. 89 | * @see table\Renderer 90 | * @see table\Ascii 91 | * @see table\Tabular 92 | */ 93 | public function setRenderer(Renderer $renderer) { 94 | $this->_renderer = $renderer; 95 | } 96 | 97 | /** 98 | * Loops through the row and sets the maximum width for each column. 99 | * 100 | * @param array $row The table row. 101 | * @return array $row 102 | */ 103 | protected function checkRow(array $row) { 104 | foreach ($row as $column => $str) { 105 | $width = Colors::width( $str, $this->isAsciiPreColorized( $column ) ); 106 | if (!isset($this->_width[$column]) || $width > $this->_width[$column]) { 107 | $this->_width[$column] = $width; 108 | } 109 | } 110 | 111 | return $row; 112 | } 113 | 114 | /** 115 | * Output the table to `STDOUT` using `cli\line()`. 116 | * 117 | * If STDOUT is a pipe or redirected to a file, should output simple 118 | * tab-separated text. Otherwise, renders table with ASCII table borders 119 | * 120 | * @uses cli\Shell::isPiped() Determine what format to output 121 | * 122 | * @see cli\Table::renderRow() 123 | */ 124 | public function display() { 125 | foreach( $this->getDisplayLines() as $line ) { 126 | Streams::line( $line ); 127 | } 128 | } 129 | 130 | /** 131 | * Get the table lines to output. 132 | * 133 | * @see cli\Table::display() 134 | * @see cli\Table::renderRow() 135 | * 136 | * @return array 137 | */ 138 | public function getDisplayLines() { 139 | $this->_renderer->setWidths($this->_width, $fallback = true); 140 | $border = $this->_renderer->border(); 141 | 142 | $out = array(); 143 | if (isset($border)) { 144 | $out[] = $border; 145 | } 146 | $out[] = $this->_renderer->row($this->_headers); 147 | if (isset($border)) { 148 | $out[] = $border; 149 | } 150 | 151 | foreach ($this->_rows as $row) { 152 | $row = $this->_renderer->row($row); 153 | $row = explode( PHP_EOL, $row ); 154 | $out = array_merge( $out, $row ); 155 | } 156 | 157 | if (isset($border)) { 158 | $out[] = $border; 159 | } 160 | 161 | if ($this->_footers) { 162 | $out[] = $this->_renderer->row($this->_footers); 163 | if (isset($border)) { 164 | $out[] = $border; 165 | } 166 | } 167 | return $out; 168 | } 169 | 170 | /** 171 | * Sort the table by a column. Must be called before `cli\Table::display()`. 172 | * 173 | * @param int $column The index of the column to sort by. 174 | */ 175 | public function sort($column) { 176 | if (!isset($this->_headers[$column])) { 177 | trigger_error('No column with index ' . $column, E_USER_NOTICE); 178 | return; 179 | } 180 | 181 | usort($this->_rows, function($a, $b) use ($column) { 182 | return strcmp($a[$column], $b[$column]); 183 | }); 184 | } 185 | 186 | /** 187 | * Set the headers of the table. 188 | * 189 | * @param array $headers An array of strings containing column header names. 190 | */ 191 | public function setHeaders(array $headers) { 192 | $this->_headers = $this->checkRow($headers); 193 | } 194 | 195 | /** 196 | * Set the footers of the table. 197 | * 198 | * @param array $footers An array of strings containing column footers names. 199 | */ 200 | public function setFooters(array $footers) { 201 | $this->_footers = $this->checkRow($footers); 202 | } 203 | 204 | 205 | /** 206 | * Add a row to the table. 207 | * 208 | * @param array $row The row data. 209 | * @see cli\Table::checkRow() 210 | */ 211 | public function addRow(array $row) { 212 | $this->_rows[] = $this->checkRow($row); 213 | } 214 | 215 | /** 216 | * Clears all previous rows and adds the given rows. 217 | * 218 | * @param array $rows A 2-dimensional array of row data. 219 | * @see cli\Table::addRow() 220 | */ 221 | public function setRows(array $rows) { 222 | $this->_rows = array(); 223 | foreach ($rows as $row) { 224 | $this->addRow($row); 225 | } 226 | } 227 | 228 | public function countRows() { 229 | return count($this->_rows); 230 | } 231 | 232 | /** 233 | * Set whether items in an Ascii table are pre-colorized. 234 | * 235 | * @param bool|array $precolorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. 236 | * @see cli\Ascii::setPreColorized() 237 | */ 238 | public function setAsciiPreColorized( $pre_colorized ) { 239 | if ( $this->_renderer instanceof Ascii ) { 240 | $this->_renderer->setPreColorized( $pre_colorized ); 241 | } 242 | } 243 | 244 | /** 245 | * Is a column in an Ascii table pre-colorized? 246 | * 247 | * @param int $column Column index to check. 248 | * @return bool True if whole Ascii table is marked as pre-colorized, or if the individual column is pre-colorized; else false. 249 | * @see cli\Ascii::isPreColorized() 250 | */ 251 | private function isAsciiPreColorized( $column ) { 252 | if ( $this->_renderer instanceof Ascii ) { 253 | return $this->_renderer->isPreColorized( $column ); 254 | } 255 | return false; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /lib/cli/Tree.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli; 14 | 15 | /** 16 | * The `Tree` class is used to display data in a tree-like format. 17 | */ 18 | class Tree { 19 | 20 | protected $_renderer; 21 | protected $_data = array(); 22 | 23 | /** 24 | * Sets the renderer used by this tree. 25 | * 26 | * @param tree\Renderer $renderer The renderer to use for output. 27 | * @see tree\Renderer 28 | * @see tree\Ascii 29 | * @see tree\Markdown 30 | */ 31 | public function setRenderer(tree\Renderer $renderer) { 32 | $this->_renderer = $renderer; 33 | } 34 | 35 | /** 36 | * Set the data. 37 | * Format: 38 | * [ 39 | * 'Label' => [ 40 | * 'Thing' => ['Thing'], 41 | * ], 42 | * 'Thing', 43 | * ] 44 | * @param array $data 45 | */ 46 | public function setData(array $data) 47 | { 48 | $this->_data = $data; 49 | } 50 | 51 | /** 52 | * Render the tree and return it as a string. 53 | * 54 | * @return string|null 55 | */ 56 | public function render() 57 | { 58 | return $this->_renderer->render($this->_data); 59 | } 60 | 61 | /** 62 | * Display the rendered tree 63 | */ 64 | public function display() 65 | { 66 | echo $this->render(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /lib/cli/arguments/Argument.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\arguments; 14 | 15 | use cli\Memoize; 16 | 17 | /** 18 | * Represents an Argument or a value and provides several helpers related to parsing an argument list. 19 | */ 20 | class Argument extends Memoize { 21 | /** 22 | * The canonical name of this argument, used for aliasing. 23 | * 24 | * @param string 25 | */ 26 | public $key; 27 | 28 | private $_argument; 29 | private $_raw; 30 | 31 | /** 32 | * @param string $argument The raw argument, leading dashes included. 33 | */ 34 | public function __construct($argument) { 35 | $this->_raw = $argument; 36 | $this->key =& $this->_argument; 37 | 38 | if ($this->isLong) { 39 | $this->_argument = substr($this->_raw, 2); 40 | } else if ($this->isShort) { 41 | $this->_argument = substr($this->_raw, 1); 42 | } else { 43 | $this->_argument = $this->_raw; 44 | } 45 | } 46 | 47 | /** 48 | * Returns the raw input as a string. 49 | * 50 | * @return string 51 | */ 52 | public function __toString() { 53 | return (string)$this->_raw; 54 | } 55 | 56 | /** 57 | * Returns the formatted argument string. 58 | * 59 | * @return string 60 | */ 61 | public function value() { 62 | return $this->_argument; 63 | } 64 | 65 | /** 66 | * Returns the raw input. 67 | * 68 | * @return mixed 69 | */ 70 | public function raw() { 71 | return $this->_raw; 72 | } 73 | 74 | /** 75 | * Returns true if the string matches the pattern for long arguments. 76 | * 77 | * @return bool 78 | */ 79 | public function isLong() { 80 | return (0 == strncmp((string)$this->_raw, '--', 2)); 81 | } 82 | 83 | /** 84 | * Returns true if the string matches the pattern for short arguments. 85 | * 86 | * @return bool 87 | */ 88 | public function isShort() { 89 | return !$this->isLong && (0 == strncmp((string)$this->_raw, '-', 1)); 90 | } 91 | 92 | /** 93 | * Returns true if the string matches the pattern for arguments. 94 | * 95 | * @return bool 96 | */ 97 | public function isArgument() { 98 | return $this->isShort() || $this->isLong(); 99 | } 100 | 101 | /** 102 | * Returns true if the string matches the pattern for values. 103 | * 104 | * @return bool 105 | */ 106 | public function isValue() { 107 | return !$this->isArgument; 108 | } 109 | 110 | /** 111 | * Returns true if the argument is short but contains several characters. Each 112 | * character is considered a separate argument. 113 | * 114 | * @return bool 115 | */ 116 | public function canExplode() { 117 | return $this->isShort && strlen($this->_argument) > 1; 118 | } 119 | 120 | /** 121 | * Returns all but the first character of the argument, removing them from the 122 | * objects representation at the same time. 123 | * 124 | * @return array 125 | */ 126 | public function exploded() { 127 | $exploded = array(); 128 | 129 | for ($i = strlen($this->_argument); $i > 0; $i--) { 130 | array_push($exploded, $this->_argument[$i - 1]); 131 | } 132 | 133 | $this->_argument = array_pop($exploded); 134 | $this->_raw = '-' . $this->_argument; 135 | return $exploded; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/cli/arguments/HelpScreen.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\arguments; 14 | 15 | use cli\Arguments; 16 | 17 | /** 18 | * Arguments help screen renderer 19 | */ 20 | class HelpScreen { 21 | protected $_flags = array(); 22 | protected $_flagMax = 0; 23 | protected $_options = array(); 24 | protected $_optionMax = 0; 25 | 26 | public function __construct(Arguments $arguments) { 27 | $this->setArguments($arguments); 28 | } 29 | 30 | public function __toString() { 31 | return $this->render(); 32 | } 33 | 34 | public function setArguments(Arguments $arguments) { 35 | $this->consumeArgumentFlags($arguments); 36 | $this->consumeArgumentOptions($arguments); 37 | } 38 | 39 | public function consumeArgumentFlags(Arguments $arguments) { 40 | $data = $this->_consume($arguments->getFlags()); 41 | 42 | $this->_flags = $data[0]; 43 | $this->_flagMax = $data[1]; 44 | } 45 | 46 | public function consumeArgumentOptions(Arguments $arguments) { 47 | $data = $this->_consume($arguments->getOptions()); 48 | 49 | $this->_options = $data[0]; 50 | $this->_optionMax = $data[1]; 51 | } 52 | 53 | public function render() { 54 | $help = array(); 55 | 56 | array_push($help, $this->_renderFlags()); 57 | array_push($help, $this->_renderOptions()); 58 | 59 | return join("\n\n", $help); 60 | } 61 | 62 | private function _renderFlags() { 63 | if (empty($this->_flags)) { 64 | return null; 65 | } 66 | 67 | return "Flags\n" . $this->_renderScreen($this->_flags, $this->_flagMax); 68 | } 69 | 70 | private function _renderOptions() { 71 | if (empty($this->_options)) { 72 | return null; 73 | } 74 | 75 | return "Options\n" . $this->_renderScreen($this->_options, $this->_optionMax); 76 | } 77 | 78 | private function _renderScreen($options, $max) { 79 | $help = array(); 80 | foreach ($options as $option => $settings) { 81 | $formatted = ' ' . str_pad($option, $max); 82 | 83 | $dlen = 80 - 4 - $max; 84 | 85 | $description = str_split($settings['description'], $dlen); 86 | $formatted.= ' ' . array_shift($description); 87 | 88 | if ($settings['default']) { 89 | $formatted .= ' [default: ' . $settings['default'] . ']'; 90 | } 91 | 92 | $pad = str_repeat(' ', $max + 3); 93 | while ($desc = array_shift($description)) { 94 | $formatted .= "\n{$pad}{$desc}"; 95 | } 96 | 97 | array_push($help, $formatted); 98 | } 99 | 100 | return join("\n", $help); 101 | } 102 | 103 | private function _consume($options) { 104 | $max = 0; 105 | $out = array(); 106 | 107 | foreach ($options as $option => $settings) { 108 | $names = array('--' . $option); 109 | 110 | foreach ($settings['aliases'] as $alias) { 111 | array_push($names, '-' . $alias); 112 | } 113 | 114 | $names = join(', ', $names); 115 | $max = max(strlen($names), $max); 116 | $out[$names] = $settings; 117 | } 118 | 119 | return array($out, $max); 120 | } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /lib/cli/arguments/InvalidArguments.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\arguments; 14 | 15 | /** 16 | * Thrown when undefined arguments are detected in strict mode. 17 | */ 18 | class InvalidArguments extends \InvalidArgumentException { 19 | protected $arguments; 20 | 21 | /** 22 | * @param array $arguments A list of arguments that do not fit the profile. 23 | */ 24 | public function __construct(array $arguments) { 25 | $this->arguments = $arguments; 26 | $this->message = $this->_generateMessage(); 27 | } 28 | 29 | /** 30 | * Get the arguments that caused the exception. 31 | * 32 | * @return array 33 | */ 34 | public function getArguments() { 35 | return $this->arguments; 36 | } 37 | 38 | private function _generateMessage() { 39 | return 'unknown argument' . 40 | (count($this->arguments) > 1 ? 's' : '') . 41 | ': ' . join(', ', $this->arguments); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/cli/arguments/Lexer.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\arguments; 14 | 15 | use cli\Memoize; 16 | 17 | class Lexer extends Memoize implements \Iterator { 18 | private $_item; 19 | private $_items = array(); 20 | private $_index = 0; 21 | private $_length = 0; 22 | private $_first = true; 23 | 24 | /** 25 | * @param array $items A list of strings to process as tokens. 26 | */ 27 | public function __construct(array $items) { 28 | $this->_items = $items; 29 | $this->_length = count($items); 30 | } 31 | 32 | /** 33 | * The current token. 34 | * 35 | * @return string 36 | */ 37 | #[\ReturnTypeWillChange] 38 | public function current() { 39 | return $this->_item; 40 | } 41 | 42 | /** 43 | * Peek ahead to the next token without moving the cursor. 44 | * 45 | * @return Argument 46 | */ 47 | public function peek() { 48 | return new Argument($this->_items[0]); 49 | } 50 | 51 | /** 52 | * Move the cursor forward 1 element if it is valid. 53 | */ 54 | #[\ReturnTypeWillChange] 55 | public function next() { 56 | if ($this->valid()) { 57 | $this->_shift(); 58 | } 59 | } 60 | 61 | /** 62 | * Return the current position of the cursor. 63 | * 64 | * @return int 65 | */ 66 | #[\ReturnTypeWillChange] 67 | public function key() { 68 | return $this->_index; 69 | } 70 | 71 | /** 72 | * Move forward 1 element and, if the method hasn't been called before, reset 73 | * the cursor's position to 0. 74 | */ 75 | #[\ReturnTypeWillChange] 76 | public function rewind() { 77 | $this->_shift(); 78 | if ($this->_first) { 79 | $this->_index = 0; 80 | $this->_first = false; 81 | } 82 | } 83 | 84 | /** 85 | * Returns true if the cursor has not reached the end of the list. 86 | * 87 | * @return bool 88 | */ 89 | #[\ReturnTypeWillChange] 90 | public function valid() { 91 | return ($this->_index < $this->_length); 92 | } 93 | 94 | /** 95 | * Push an element to the front of the stack. 96 | * 97 | * @param mixed $item The value to set 98 | */ 99 | public function unshift($item) { 100 | array_unshift($this->_items, $item); 101 | $this->_length += 1; 102 | } 103 | 104 | /** 105 | * Returns true if the cursor is at the end of the list. 106 | * 107 | * @return bool 108 | */ 109 | public function end() { 110 | return ($this->_index + 1) == $this->_length; 111 | } 112 | 113 | private function _shift() { 114 | $this->_item = new Argument(array_shift($this->_items)); 115 | $this->_index += 1; 116 | $this->_explode(); 117 | $this->_unmemo('peek'); 118 | } 119 | 120 | private function _explode() { 121 | if (!$this->_item->canExplode) { 122 | return false; 123 | } 124 | 125 | foreach ($this->_item->exploded as $piece) { 126 | $this->unshift('-' . $piece); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/cli/cli.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2010 James Logsdom (http://girsbrain.org) 11 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 12 | */ 13 | 14 | namespace cli; 15 | 16 | /** 17 | * Handles rendering strings. If extra scalar arguments are given after the `$msg` 18 | * the string will be rendered with `sprintf`. If the second argument is an `array` 19 | * then each key in the array will be the placeholder name. Placeholders are of the 20 | * format {:key}. 21 | * 22 | * @param string $msg The message to render. 23 | * @param mixed ... Either scalar arguments or a single array argument. 24 | * @return string The rendered string. 25 | */ 26 | function render( $msg ) { 27 | return Streams::_call( 'render', func_get_args() ); 28 | } 29 | 30 | /** 31 | * Shortcut for printing to `STDOUT`. The message and parameters are passed 32 | * through `sprintf` before output. 33 | * 34 | * @param string $msg The message to output in `printf` format. 35 | * @param mixed ... Either scalar arguments or a single array argument. 36 | * @return void 37 | * @see \cli\render() 38 | */ 39 | function out( $msg ) { 40 | Streams::_call( 'out', func_get_args() ); 41 | } 42 | 43 | /** 44 | * Pads `$msg` to the width of the shell before passing to `cli\out`. 45 | * 46 | * @param string $msg The message to pad and pass on. 47 | * @param mixed ... Either scalar arguments or a single array argument. 48 | * @return void 49 | * @see cli\out() 50 | */ 51 | function out_padded( $msg ) { 52 | Streams::_call( 'out_padded', func_get_args() ); 53 | } 54 | 55 | /** 56 | * Prints a message to `STDOUT` with a newline appended. See `\cli\out` for 57 | * more documentation. 58 | * 59 | * @see cli\out() 60 | */ 61 | function line( $msg = '' ) { 62 | Streams::_call( 'line', func_get_args() ); 63 | } 64 | 65 | /** 66 | * Shortcut for printing to `STDERR`. The message and parameters are passed 67 | * through `sprintf` before output. 68 | * 69 | * @param string $msg The message to output in `printf` format. With no string, 70 | * a newline is printed. 71 | * @param mixed ... Either scalar arguments or a single array argument. 72 | * @return void 73 | */ 74 | function err( $msg = '' ) { 75 | Streams::_call( 'err', func_get_args() ); 76 | } 77 | 78 | /** 79 | * Takes input from `STDIN` in the given format. If an end of transmission 80 | * character is sent (^D), an exception is thrown. 81 | * 82 | * @param string $format A valid input format. See `fscanf` for documentation. 83 | * If none is given, all input up to the first newline 84 | * is accepted. 85 | * @return string The input with whitespace trimmed. 86 | * @throws \Exception Thrown if ctrl-D (EOT) is sent as input. 87 | */ 88 | function input( $format = null ) { 89 | return Streams::input( $format ); 90 | } 91 | 92 | /** 93 | * Displays an input prompt. If no default value is provided the prompt will 94 | * continue displaying until input is received. 95 | * 96 | * @param string $question The question to ask the user. 97 | * @param string|false $default A default value if the user provides no input. Default false. 98 | * @param string $marker A string to append to the question and default value on display. 99 | * @param boolean $hide If the user input should be hidden 100 | * @return string The users input. 101 | * @see cli\input() 102 | */ 103 | function prompt( $question, $default = false, $marker = ': ', $hide = false ) { 104 | return Streams::prompt( $question, $default, $marker, $hide ); 105 | } 106 | 107 | /** 108 | * Presents a user with a multiple choice question, useful for 'yes/no' type 109 | * questions (which this function defaults too). 110 | * 111 | * @param string $question The question to ask the user. 112 | * @param string $choice 113 | * @param string|null $default The default choice. NULL if a default is not allowed. 114 | * @internal param string $valid A string of characters allowed as a response. Case 115 | * is ignored. 116 | * @return string The users choice. 117 | * @see cli\prompt() 118 | */ 119 | function choose( $question, $choice = 'yn', $default = 'n' ) { 120 | return Streams::choose( $question, $choice, $default ); 121 | } 122 | 123 | /** 124 | * Does the same as {@see choose()}, but always asks yes/no and returns a boolean 125 | * 126 | * @param string $question The question to ask the user. 127 | * @param bool|null $default The default choice, in a boolean format. 128 | * @return bool 129 | */ 130 | function confirm( $question, $default = false ) { 131 | if ( is_bool( $default ) ) { 132 | $default = $default? 'y' : 'n'; 133 | } 134 | $result = choose( $question, 'yn', $default ); 135 | return $result == 'y'; 136 | } 137 | 138 | /** 139 | * Displays an array of strings as a menu where a user can enter a number to 140 | * choose an option. The array must be a single dimension with either strings 141 | * or objects with a `__toString()` method. 142 | * 143 | * @param array $items The list of items the user can choose from. 144 | * @param string $default The index of the default item. 145 | * @param string $title The message displayed to the user when prompted. 146 | * @return string The index of the chosen item. 147 | * @see cli\line() 148 | * @see cli\input() 149 | * @see cli\err() 150 | */ 151 | function menu( $items, $default = null, $title = 'Choose an item' ) { 152 | return Streams::menu( $items, $default, $title ); 153 | } 154 | 155 | /** 156 | * Attempts an encoding-safe way of getting string length. If intl extension or PCRE with '\X' or mb_string extension aren't 157 | * available, falls back to basic strlen. 158 | * 159 | * @param string $str The string to check. 160 | * @param string|bool $encoding Optional. The encoding of the string. Default false. 161 | * @return int Numeric value that represents the string's length 162 | */ 163 | function safe_strlen( $str, $encoding = false ) { 164 | // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strlen(), "other" strlen(). 165 | $test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); 166 | 167 | // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. 168 | if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $length = grapheme_strlen( $str ) ) ) { 169 | if ( ! $test_safe_strlen || ( $test_safe_strlen & 1 ) ) { 170 | return $length; 171 | } 172 | } 173 | // Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string. 174 | if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $length = preg_match_all( '/\X/u', $str, $dummy /*needed for PHP 5.3*/ ) ) ) { 175 | if ( ! $test_safe_strlen || ( $test_safe_strlen & 2 ) ) { 176 | return $length; 177 | } 178 | } 179 | // Legacy encodings and old PHPs will reach here. 180 | if ( function_exists( 'mb_strlen' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { 181 | if ( ! $encoding ) { 182 | $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); 183 | } 184 | $length = $encoding ? mb_strlen( $str, $encoding ) : mb_strlen( $str ); // mbstring funcs can fail if given `$encoding` arg that evals to false. 185 | if ( 'UTF-8' === $encoding ) { 186 | // Subtract combining characters. 187 | $length -= preg_match_all( get_unicode_regexs( 'm' ), $str, $dummy /*needed for PHP 5.3*/ ); 188 | } 189 | if ( ! $test_safe_strlen || ( $test_safe_strlen & 4 ) ) { 190 | return $length; 191 | } 192 | } 193 | return strlen( $str ); 194 | } 195 | 196 | /** 197 | * Attempts an encoding-safe way of getting a substring. If intl extension or PCRE with '\X' or mb_string extension aren't 198 | * available, falls back to substr(). 199 | * 200 | * @param string $str The input string. 201 | * @param int $start The starting position of the substring. 202 | * @param int|bool|null $length Optional, unless $is_width is set. Maximum length of the substring. Default false. Negative not supported. 203 | * @param int|bool $is_width Optional. If set and encoding is UTF-8, $length (which must be specified) is interpreted as spacing width. Default false. 204 | * @param string|bool $encoding Optional. The encoding of the string. Default false. 205 | * @return bool|string False if given unsupported args, otherwise substring of string specified by start and length parameters 206 | */ 207 | function safe_substr( $str, $start, $length = false, $is_width = false, $encoding = false ) { 208 | // Negative $length or $is_width and $length not specified not supported. 209 | if ( $length < 0 || ( $is_width && ( null === $length || false === $length ) ) ) { 210 | return false; 211 | } 212 | // Need this for normalization below and other uses. 213 | $safe_strlen = safe_strlen( $str, $encoding ); 214 | 215 | // Normalize `$length` when not specified - PHP 5.3 substr takes false as full length, PHP > 5.3 takes null. 216 | if ( null === $length || false === $length ) { 217 | $length = $safe_strlen; 218 | } 219 | // Normalize `$start` - various methods treat this differently. 220 | if ( $start > $safe_strlen ) { 221 | return ''; 222 | } 223 | if ( $start < 0 && -$start > $safe_strlen ) { 224 | $start = 0; 225 | } 226 | 227 | // Allow for selective testings - "1" bit set tests grapheme_substr(), "2" preg_split( '/\X/' ), "4" mb_substr(), "8" substr(). 228 | $test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); 229 | 230 | // Assume UTF-8 if no encoding given - `grapheme_substr()` will return false (not null like `grapheme_strlen()`) if given non-UTF-8 string. 231 | if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) { 232 | if ( ! $test_safe_substr || ( $test_safe_substr & 1 ) ) { 233 | return $is_width ? _safe_substr_eaw( $try, $length ) : $try; 234 | } 235 | } 236 | // Assume UTF-8 if no encoding given - `preg_split()` returns a one element array if given non-UTF-8 string (PHP bug) so need to check `preg_last_error()`. 237 | if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() ) { 238 | if ( false !== ( $try = preg_split( '/(\X)/u', $str, $safe_strlen + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) ) && ! preg_last_error() ) { 239 | $try = implode( '', array_slice( $try, $start, $length ) ); 240 | if ( ! $test_safe_substr || ( $test_safe_substr & 2 ) ) { 241 | return $is_width ? _safe_substr_eaw( $try, $length ) : $try; 242 | } 243 | } 244 | } 245 | // Legacy encodings and old PHPs will reach here. 246 | if ( function_exists( 'mb_substr' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { 247 | if ( ! $encoding ) { 248 | $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); 249 | } 250 | // Bug: not adjusting for combining chars. 251 | $try = $encoding ? mb_substr( $str, $start, $length, $encoding ) : mb_substr( $str, $start, $length ); // mbstring funcs can fail if given `$encoding` arg that evals to false. 252 | if ( 'UTF-8' === $encoding && $is_width ) { 253 | $try = _safe_substr_eaw( $try, $length ); 254 | } 255 | if ( ! $test_safe_substr || ( $test_safe_substr & 4 ) ) { 256 | return $try; 257 | } 258 | } 259 | return substr( $str, $start, $length ); 260 | } 261 | 262 | /** 263 | * Internal function used by `safe_substr()` to adjust for East Asian double-width chars. 264 | * 265 | * @return string 266 | */ 267 | function _safe_substr_eaw( $str, $length ) { 268 | // Set the East Asian Width regex. 269 | $eaw_regex = get_unicode_regexs( 'eaw' ); 270 | 271 | // If there's any East Asian double-width chars... 272 | if ( preg_match( $eaw_regex, $str ) ) { 273 | // Note that if the length ends in the middle of a double-width char, the char is excluded, not included. 274 | 275 | // See if it's all EAW. 276 | if ( function_exists( 'mb_substr' ) && preg_match_all( $eaw_regex, $str, $dummy /*needed for PHP 5.3*/ ) === $length ) { 277 | // Just halve the length so (rounded down to a minimum of 1). 278 | $str = mb_substr( $str, 0, max( (int) ( $length / 2 ), 1 ), 'UTF-8' ); 279 | } else { 280 | // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". 281 | $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $str, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); 282 | $cnt = min( count( $chars ), $length ); 283 | $width = $length; 284 | 285 | for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { 286 | $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; 287 | } 288 | // Round down to a minimum of 1. 289 | if ( $width < 0 && $length > 1 ) { 290 | $length--; 291 | } 292 | return join( '', array_slice( $chars, 0, $length ) ); 293 | } 294 | } 295 | return $str; 296 | } 297 | 298 | /** 299 | * An encoding-safe way of padding string length for display 300 | * 301 | * @param string $string The string to pad. 302 | * @param int $length The length to pad it to. 303 | * @param string|bool $encoding Optional. The encoding of the string. Default false. 304 | * @return string 305 | */ 306 | function safe_str_pad( $string, $length, $encoding = false ) { 307 | $real_length = strwidth( $string, $encoding ); 308 | $diff = strlen( $string ) - $real_length; 309 | $length += $diff; 310 | 311 | return str_pad( $string, $length ); 312 | } 313 | 314 | /** 315 | * Get width of string, ie length in characters, taking into account multi-byte and mark characters for UTF-8, and multi-byte for non-UTF-8. 316 | * 317 | * @param string $string The string to check. 318 | * @param string|bool $encoding Optional. The encoding of the string. Default false. 319 | * @return int The string's width. 320 | */ 321 | function strwidth( $string, $encoding = false ) { 322 | $string = (string) $string; 323 | 324 | // Set the East Asian Width and Mark regexs. 325 | list( $eaw_regex, $m_regex ) = get_unicode_regexs(); 326 | 327 | // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen(). 328 | $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); 329 | 330 | // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. 331 | if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $width = grapheme_strlen( $string ) ) ) { 332 | if ( ! $test_strwidth || ( $test_strwidth & 1 ) ) { 333 | return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); 334 | } 335 | } 336 | // Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string. 337 | if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $width = preg_match_all( '/\X/u', $string, $dummy /*needed for PHP 5.3*/ ) ) ) { 338 | if ( ! $test_strwidth || ( $test_strwidth & 2 ) ) { 339 | return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); 340 | } 341 | } 342 | // Legacy encodings and old PHPs will reach here. 343 | if ( function_exists( 'mb_strwidth' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { 344 | if ( ! $encoding ) { 345 | $encoding = mb_detect_encoding( $string, null, true /*strict*/ ); 346 | } 347 | $width = $encoding ? mb_strwidth( $string, $encoding ) : mb_strwidth( $string ); // mbstring funcs can fail if given `$encoding` arg that evals to false. 348 | if ( 'UTF-8' === $encoding ) { 349 | // Subtract combining characters. 350 | $width -= preg_match_all( $m_regex, $string, $dummy /*needed for PHP 5.3*/ ); 351 | } 352 | if ( ! $test_strwidth || ( $test_strwidth & 4 ) ) { 353 | return $width; 354 | } 355 | } 356 | return safe_strlen( $string, $encoding ); 357 | } 358 | 359 | /** 360 | * Returns whether ICU is modern enough not to flake out. 361 | * 362 | * @return bool 363 | */ 364 | function can_use_icu() { 365 | static $can_use_icu = null; 366 | 367 | if ( null === $can_use_icu ) { 368 | // Choosing ICU 54, Unicode 7.0. 369 | $can_use_icu = defined( 'INTL_ICU_VERSION' ) && version_compare( INTL_ICU_VERSION, '54.1', '>=' ) && function_exists( 'grapheme_strlen' ) && function_exists( 'grapheme_substr' ); 370 | } 371 | 372 | return $can_use_icu; 373 | } 374 | 375 | /** 376 | * Returns whether PCRE Unicode extended grapheme cluster '\X' is available for use. 377 | * 378 | * @return bool 379 | */ 380 | function can_use_pcre_x() { 381 | static $can_use_pcre_x = null; 382 | 383 | if ( null === $can_use_pcre_x ) { 384 | // '\X' introduced (as Unicde extended grapheme cluster) in PCRE 8.32 - see https://vcs.pcre.org/pcre/code/tags/pcre-8.32/ChangeLog?view=markup line 53. 385 | // Older versions of PCRE were bundled with PHP <= 5.3.23 & <= 5.4.13. 386 | $pcre_version = substr( PCRE_VERSION, 0, strspn( PCRE_VERSION, '0123456789.' ) ); // Remove any trailing date stuff. 387 | $can_use_pcre_x = version_compare( $pcre_version, '8.32', '>=' ) && false !== @preg_match( '/\X/u', '' ); 388 | } 389 | 390 | return $can_use_pcre_x; 391 | } 392 | 393 | /** 394 | * Get the regexs generated from Unicode data. 395 | * 396 | * @param string $idx Optional. Return a specific regex only. Default null. 397 | * @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string. 398 | */ 399 | function get_unicode_regexs( $idx = null ) { 400 | static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html 401 | static $m_regex; // Mark characters regex (Unicode property "M") - mark combining "Mc", mark enclosing "Me" and mark non-spacing "Mn" chars that should be ignored for spacing purposes. 402 | if ( null === $eaw_regex ) { 403 | // Load both regexs generated from Unicode data. 404 | require __DIR__ . '/unicode/regex.php'; 405 | } 406 | 407 | if ( null !== $idx ) { 408 | if ( 'eaw' === $idx ) { 409 | return $eaw_regex; 410 | } 411 | if ( 'm' === $idx ) { 412 | return $m_regex; 413 | } 414 | } 415 | 416 | return array( $eaw_regex, $m_regex, ); 417 | } 418 | -------------------------------------------------------------------------------- /lib/cli/notify/Dots.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\notify; 14 | 15 | use cli\Notify; 16 | use cli\Streams; 17 | 18 | /** 19 | * A Notifer that displays a string of periods. 20 | */ 21 | class Dots extends Notify { 22 | protected $_dots; 23 | protected $_format = '{:msg}{:dots} ({:elapsed}, {:speed}/s)'; 24 | protected $_iteration; 25 | 26 | /** 27 | * Instatiates a Notification object. 28 | * 29 | * @param string $msg The text to display next to the Notifier. 30 | * @param int $dots The number of dots to iterate through. 31 | * @param int $interval The interval in milliseconds between updates. 32 | * @throws \InvalidArgumentException 33 | */ 34 | public function __construct($msg, $dots = 3, $interval = 100) { 35 | parent::__construct($msg, $interval); 36 | $this->_dots = (int)$dots; 37 | 38 | if ($this->_dots <= 0) { 39 | throw new \InvalidArgumentException('Dot count out of range, must be positive.'); 40 | } 41 | } 42 | 43 | /** 44 | * Prints the correct number of dots to `STDOUT` with the time elapsed and 45 | * tick speed. 46 | * 47 | * @param boolean $finish `true` if this was called from 48 | * `cli\Notify::finish()`, `false` otherwise. 49 | * @see cli\out_padded() 50 | * @see cli\Notify::formatTime() 51 | * @see cli\Notify::speed() 52 | */ 53 | public function display($finish = false) { 54 | $repeat = $this->_dots; 55 | if (!$finish) { 56 | $repeat = $this->_iteration++ % $repeat; 57 | } 58 | 59 | $msg = $this->_message; 60 | $dots = str_pad(str_repeat('.', $repeat), $this->_dots); 61 | $speed = number_format(round($this->speed())); 62 | $elapsed = $this->formatTime($this->elapsed()); 63 | 64 | Streams::out_padded($this->_format, compact('msg', 'dots', 'speed', 'elapsed')); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/cli/notify/Spinner.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\notify; 14 | 15 | use cli\Notify; 16 | use cli\Streams; 17 | 18 | /** 19 | * The `Spinner` Notifier displays an ASCII spinner. 20 | */ 21 | class Spinner extends Notify { 22 | protected $_chars = '-\|/'; 23 | protected $_format = '{:msg} {:char} ({:elapsed}, {:speed}/s)'; 24 | protected $_iteration = 0; 25 | 26 | /** 27 | * Prints the current spinner position to `STDOUT` with the time elapsed 28 | * and tick speed. 29 | * 30 | * @param boolean $finish `true` if this was called from 31 | * `cli\Notify::finish()`, `false` otherwise. 32 | * @see cli\out_padded() 33 | * @see cli\Notify::formatTime() 34 | * @see cli\Notify::speed() 35 | */ 36 | public function display($finish = false) { 37 | $msg = $this->_message; 38 | $idx = $this->_iteration++ % strlen($this->_chars); 39 | $char = $this->_chars[$idx]; 40 | $speed = number_format(round($this->speed())); 41 | $elapsed = $this->formatTime($this->elapsed()); 42 | 43 | Streams::out_padded($this->_format, compact('msg', 'char', 'elapsed', 'speed')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/cli/progress/Bar.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\progress; 14 | 15 | use cli; 16 | use cli\Notify; 17 | use cli\Progress; 18 | use cli\Shell; 19 | use cli\Streams; 20 | 21 | /** 22 | * Displays a progress bar spanning the entire shell. 23 | * 24 | * Basic format: 25 | * 26 | * ^MSG PER% [======================= ] 00:00 / 00:00$ 27 | */ 28 | class Bar extends Progress { 29 | protected $_bars = '=>'; 30 | protected $_formatMessage = '{:msg} {:percent}% ['; 31 | protected $_formatTiming = '] {:elapsed} / {:estimated}'; 32 | protected $_format = '{:msg}{:bar}{:timing}'; 33 | 34 | /** 35 | * Prints the progress bar to the screen with percent complete, elapsed time 36 | * and estimated total time. 37 | * 38 | * @param boolean $finish `true` if this was called from 39 | * `cli\Notify::finish()`, `false` otherwise. 40 | * @see cli\out() 41 | * @see cli\Notify::formatTime() 42 | * @see cli\Notify::elapsed() 43 | * @see cli\Progress::estimated(); 44 | * @see cli\Progress::percent() 45 | * @see cli\Shell::columns() 46 | */ 47 | public function display($finish = false) { 48 | $_percent = $this->percent(); 49 | 50 | $percent = str_pad(floor($_percent * 100), 3); 51 | $msg = $this->_message; 52 | $msg = Streams::render($this->_formatMessage, compact('msg', 'percent')); 53 | 54 | $estimated = $this->formatTime($this->estimated()); 55 | $elapsed = str_pad($this->formatTime($this->elapsed()), strlen($estimated)); 56 | $timing = Streams::render($this->_formatTiming, compact('elapsed', 'estimated')); 57 | 58 | $size = Shell::columns(); 59 | $size -= strlen($msg . $timing); 60 | if ( $size < 0 ) { 61 | $size = 0; 62 | } 63 | 64 | $bar = str_repeat($this->_bars[0], floor($_percent * $size)) . $this->_bars[1]; 65 | // substr is needed to trim off the bar cap at 100% 66 | $bar = substr(str_pad($bar, $size, ' '), 0, $size); 67 | 68 | Streams::out($this->_format, compact('msg', 'bar', 'timing')); 69 | } 70 | 71 | /** 72 | * This method augments the base definition from cli\Notify to optionally 73 | * allow passing a new message. 74 | * 75 | * @param int $increment The amount to increment by. 76 | * @param string $msg The text to display next to the Notifier. (optional) 77 | * @see cli\Notify::tick() 78 | */ 79 | public function tick($increment = 1, $msg = null) { 80 | if ($msg) { 81 | $this->_message = $msg; 82 | } 83 | Notify::tick($increment); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/cli/table/Ascii.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\table; 14 | 15 | use cli\Colors; 16 | use cli\Shell; 17 | 18 | /** 19 | * The ASCII renderer renders tables with ASCII borders. 20 | */ 21 | class Ascii extends Renderer { 22 | protected $_characters = array( 23 | 'corner' => '+', 24 | 'line' => '-', 25 | 'border' => '|', 26 | 'padding' => ' ', 27 | ); 28 | protected $_border = null; 29 | protected $_constraintWidth = null; 30 | protected $_pre_colorized = false; 31 | 32 | /** 33 | * Set the widths of each column in the table. 34 | * 35 | * @param array $widths The widths of the columns. 36 | * @param bool $fallback Whether to use these values as fallback only. 37 | */ 38 | public function setWidths(array $widths, $fallback = false) { 39 | if ($fallback) { 40 | foreach ( $this->_widths as $index => $value ) { 41 | $widths[$index] = $value; 42 | } 43 | } 44 | $this->_widths = $widths; 45 | 46 | if ( is_null( $this->_constraintWidth ) ) { 47 | $this->_constraintWidth = (int) Shell::columns(); 48 | } 49 | $col_count = count( $widths ); 50 | $col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0; 51 | $table_borders_count = strlen( $this->_characters['border'] ) * 2; 52 | $col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2; 53 | $max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count; 54 | 55 | if ( $widths && $max_width && array_sum( $widths ) > $max_width ) { 56 | 57 | $avg = floor( $max_width / count( $widths ) ); 58 | $resize_widths = array(); 59 | $extra_width = 0; 60 | foreach( $widths as $width ) { 61 | if ( $width > $avg ) { 62 | $resize_widths[] = $width; 63 | } else { 64 | $extra_width = $extra_width + ( $avg - $width ); 65 | } 66 | } 67 | 68 | if ( ! empty( $resize_widths ) && $extra_width ) { 69 | $avg_extra_width = floor( $extra_width / count( $resize_widths ) ); 70 | foreach( $widths as &$width ) { 71 | if ( in_array( $width, $resize_widths ) ) { 72 | $width = $avg + $avg_extra_width; 73 | array_shift( $resize_widths ); 74 | // Last item gets the cake 75 | if ( empty( $resize_widths ) ) { 76 | $width = 0; // Zero it so not in sum. 77 | $width = $max_width - array_sum( $widths ); 78 | } 79 | } 80 | } 81 | } 82 | 83 | } 84 | 85 | $this->_widths = $widths; 86 | } 87 | 88 | /** 89 | * Set the constraint width for the table 90 | * 91 | * @param int $constraintWidth 92 | */ 93 | public function setConstraintWidth( $constraintWidth ) { 94 | $this->_constraintWidth = $constraintWidth; 95 | } 96 | 97 | /** 98 | * Set the characters used for rendering the Ascii table. 99 | * 100 | * The keys `corner`, `line` and `border` are used in rendering. 101 | * 102 | * @param $characters array Characters used in rendering. 103 | */ 104 | public function setCharacters(array $characters) { 105 | $this->_characters = array_merge($this->_characters, $characters); 106 | } 107 | 108 | /** 109 | * Render a border for the top and bottom and separating the headers from the 110 | * table rows. 111 | * 112 | * @return string The table border. 113 | */ 114 | public function border() { 115 | if (!isset($this->_border)) { 116 | $this->_border = $this->_characters['corner']; 117 | foreach ($this->_widths as $width) { 118 | $this->_border .= str_repeat($this->_characters['line'], $width + 2); 119 | $this->_border .= $this->_characters['corner']; 120 | } 121 | } 122 | 123 | return $this->_border; 124 | } 125 | 126 | /** 127 | * Renders a row for output. 128 | * 129 | * @param array $row The table row. 130 | * @return string The formatted table row. 131 | */ 132 | public function row( array $row ) { 133 | 134 | $extra_row_count = 0; 135 | 136 | if ( count( $row ) > 0 ) { 137 | $extra_rows = array_fill( 0, count( $row ), array() ); 138 | 139 | foreach ( $row as $col => $value ) { 140 | $value = $value ?: ''; 141 | $col_width = $this->_widths[ $col ]; 142 | $encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false; 143 | $original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding ); 144 | if ( $col_width && ( $original_val_width > $col_width || strpos( $value, "\n" ) !== false ) ) { 145 | $split_lines = preg_split( '/\r\n|\n/', $value ); 146 | 147 | $wrapped_lines = []; 148 | foreach ( $split_lines as $line ) { 149 | do { 150 | $wrapped_value = \cli\safe_substr( $line, 0, $col_width, true /*is_width*/, $encoding ); 151 | $val_width = Colors::width( $wrapped_value, self::isPreColorized( $col ), $encoding ); 152 | if ( $val_width ) { 153 | $wrapped_lines[] = $wrapped_value; 154 | $line = \cli\safe_substr( $line, \cli\safe_strlen( $wrapped_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); 155 | } 156 | } while ( $line ); 157 | } 158 | 159 | $row[ $col ] = array_shift( $wrapped_lines ); 160 | foreach ( $wrapped_lines as $wrapped_line ) { 161 | $extra_rows[ $col ][] = $wrapped_line; 162 | ++$extra_row_count; 163 | } 164 | } 165 | } 166 | } 167 | 168 | $row = array_map(array($this, 'padColumn'), $row, array_keys($row)); 169 | array_unshift($row, ''); // First border 170 | array_push($row, ''); // Last border 171 | 172 | $ret = join($this->_characters['border'], $row); 173 | if ( $extra_row_count ) { 174 | foreach( $extra_rows as $col => $col_values ) { 175 | while( count( $col_values ) < $extra_row_count ) { 176 | $col_values[] = ''; 177 | } 178 | } 179 | 180 | do { 181 | $row_values = array(); 182 | $has_more = false; 183 | foreach( $extra_rows as $col => &$col_values ) { 184 | $row_values[ $col ] = ! empty( $col_values ) ? array_shift( $col_values ) : ''; 185 | if ( count( $col_values ) ) { 186 | $has_more = true; 187 | } 188 | } 189 | 190 | $row_values = array_map(array($this, 'padColumn'), $row_values, array_keys($row_values)); 191 | array_unshift($row_values, ''); // First border 192 | array_push($row_values, ''); // Last border 193 | 194 | $ret .= PHP_EOL . join($this->_characters['border'], $row_values); 195 | } while( $has_more ); 196 | } 197 | return $ret; 198 | } 199 | 200 | private function padColumn($content, $column) { 201 | $content = str_replace( "\t", ' ', (string) $content ); 202 | return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ) ) . $this->_characters['padding']; 203 | } 204 | 205 | /** 206 | * Set whether items are pre-colorized. 207 | * 208 | * @param bool|array $colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. 209 | */ 210 | public function setPreColorized( $pre_colorized ) { 211 | $this->_pre_colorized = $pre_colorized; 212 | } 213 | 214 | /** 215 | * Is a column pre-colorized? 216 | * 217 | * @param int $column Column index to check. 218 | * @return bool True if whole table is marked as pre-colorized, or if the individual column is pre-colorized; else false. 219 | */ 220 | public function isPreColorized( $column ) { 221 | if ( is_bool( $this->_pre_colorized ) ) { 222 | return $this->_pre_colorized; 223 | } 224 | if ( is_array( $this->_pre_colorized ) && isset( $this->_pre_colorized[ $column ] ) ) { 225 | return $this->_pre_colorized[ $column ]; 226 | } 227 | return false; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /lib/cli/table/Renderer.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\table; 14 | 15 | /** 16 | * Table renderers are used to change how a table is displayed. 17 | */ 18 | abstract class Renderer { 19 | protected $_widths = array(); 20 | 21 | public function __construct(array $widths = array()) { 22 | $this->setWidths($widths); 23 | } 24 | 25 | /** 26 | * Set the widths of each column in the table. 27 | * 28 | * @param array $widths The widths of the columns. 29 | * @param bool $fallback Whether to use these values as fallback only. 30 | */ 31 | public function setWidths(array $widths, $fallback = false) { 32 | if ($fallback) { 33 | foreach ( $this->_widths as $index => $value ) { 34 | $widths[$index] = $value; 35 | } 36 | } 37 | $this->_widths = $widths; 38 | } 39 | 40 | /** 41 | * Render a border for the top and bottom and separating the headers from the 42 | * table rows. 43 | * 44 | * @return string The table border. 45 | */ 46 | public function border() { 47 | return null; 48 | } 49 | 50 | /** 51 | * Renders a row for output. 52 | * 53 | * @param array $row The table row. 54 | * @return string The formatted table row. 55 | */ 56 | abstract public function row(array $row); 57 | } 58 | -------------------------------------------------------------------------------- /lib/cli/table/Tabular.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\table; 14 | 15 | /** 16 | * The tabular renderer is used for displaying data in a tabular format. 17 | */ 18 | class Tabular extends Renderer { 19 | /** 20 | * Renders a row for output. 21 | * 22 | * @param array $row The table row. 23 | * @return string The formatted table row. 24 | */ 25 | public function row( array $row ) { 26 | $rows = []; 27 | $output = ''; 28 | 29 | foreach ( $row as $col => $value ) { 30 | $value = isset( $value ) ? (string) $value : ''; 31 | $value = str_replace( "\t", ' ', $value ); 32 | $split_lines = preg_split( '/\r\n|\n/', $value ); 33 | // Keep anything before the first line break on the original line 34 | $row[ $col ] = array_shift( $split_lines ); 35 | } 36 | 37 | $rows[] = $row; 38 | 39 | foreach ( $split_lines as $i => $line ) { 40 | if ( ! isset( $rows[ $i + 1 ] ) ) { 41 | $rows[ $i + 1 ] = array_fill_keys( array_keys( $row ), '' ); 42 | } 43 | $rows[ $i + 1 ][ $col ] = $line; 44 | } 45 | 46 | foreach ( $rows as $r ) { 47 | $output .= implode( "\t", array_values( $r ) ) . PHP_EOL; 48 | } 49 | return rtrim( $output, PHP_EOL ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/cli/tree/Ascii.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\tree; 14 | 15 | /** 16 | * The ASCII renderer renders trees with ASCII lines. 17 | */ 18 | class Ascii extends Renderer { 19 | 20 | /** 21 | * @param array $tree 22 | * @return string 23 | */ 24 | public function render(array $tree) 25 | { 26 | $output = ''; 27 | 28 | $treeIterator = new \RecursiveTreeIterator( 29 | new \RecursiveArrayIterator($tree), 30 | \RecursiveTreeIterator::SELF_FIRST 31 | ); 32 | 33 | foreach ($treeIterator as $val) 34 | { 35 | $output .= $val . "\n"; 36 | } 37 | 38 | return $output; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /lib/cli/tree/Markdown.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\tree; 14 | 15 | /** 16 | * The ASCII renderer renders trees with ASCII lines. 17 | */ 18 | class Markdown extends Renderer { 19 | 20 | /** 21 | * How many spaces to indent by 22 | * @var int 23 | */ 24 | protected $_padding = 2; 25 | 26 | /** 27 | * @param int $padding Optional. Default 2. 28 | */ 29 | function __construct($padding = null) 30 | { 31 | if ($padding) 32 | { 33 | $this->_padding = $padding; 34 | } 35 | } 36 | 37 | /** 38 | * Renders the tree 39 | * 40 | * @param array $tree 41 | * @param int $level Optional 42 | * @return string 43 | */ 44 | public function render(array $tree, $level = 0) 45 | { 46 | $output = ''; 47 | 48 | foreach ($tree as $label => $next) 49 | { 50 | 51 | if (is_string($next)) 52 | { 53 | $label = $next; 54 | } 55 | 56 | // Output the label 57 | $output .= sprintf("%s- %s\n", str_repeat(' ', $level * $this->_padding), $label); 58 | 59 | // Next level 60 | if (is_array($next)) 61 | { 62 | $output .= $this->render($next, $level + 1); 63 | } 64 | 65 | } 66 | 67 | return $output; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /lib/cli/tree/Renderer.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2010 James Logsdom (http://girsbrain.org) 10 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License 11 | */ 12 | 13 | namespace cli\tree; 14 | 15 | /** 16 | * Tree renderers are used to change how a tree is displayed. 17 | */ 18 | abstract class Renderer { 19 | 20 | /** 21 | * @param array $tree 22 | * @return string|null 23 | */ 24 | abstract public function render(array $tree); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /lib/cli/unicode/regex.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | tests/ 17 | 18 | 19 | 20 | 21 | lib/ 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | array( 9 | 'verbose' => array( 10 | 'description' => 'Turn on verbose mode', 11 | 'aliases' => array('v') 12 | ), 13 | 'c' => array( 14 | 'description' => 'A counter to test stackable', 15 | 'stackable' => true 16 | ) 17 | ), 18 | 'options' => array( 19 | 'user' => array( 20 | 'description' => 'Username for authentication', 21 | 'aliases' => array('u') 22 | ) 23 | ), 24 | 'strict' => true 25 | )); 26 | 27 | try { 28 | $args->parse(); 29 | } catch (cli\InvalidArguments $e) { 30 | echo $e->getMessage() . "\n\n"; 31 | } 32 | 33 | print_r($args->getArguments()); 34 | -------------------------------------------------------------------------------- /tests/Test_Arguments.php: -------------------------------------------------------------------------------- 1 | flags = array( 68 | 'flag1' => array( 69 | 'aliases' => 'f', 70 | 'description' => 'Test flag 1' 71 | ), 72 | 'flag2' => array( 73 | 'description' => 'Test flag 2' 74 | ) 75 | ); 76 | 77 | $this->options = array( 78 | 'option1' => array( 79 | 'aliases' => 'o', 80 | 'description' => 'Test option 1' 81 | ), 82 | 'option2' => array( 83 | 'aliases' => array('x', 'y'), 84 | 'description' => 'Test option 2 with default', 85 | 'default' => 'some default value' 86 | ) 87 | ); 88 | 89 | $this->settings = array( 90 | 'strict' => true, 91 | 'flags' => $this->flags, 92 | 'options' => $this->options 93 | ); 94 | 95 | set_error_handler( 96 | static function ( $errno, $errstr ) { 97 | throw new \Exception( $errstr, $errno ); 98 | }, 99 | E_ALL 100 | ); 101 | } 102 | 103 | /** 104 | * Tear down fixtures 105 | */ 106 | public function tear_down() 107 | { 108 | $this->flags = null; 109 | $this->options = null; 110 | $this->settings = null; 111 | self::clearArgv(); 112 | restore_error_handler(); 113 | } 114 | 115 | /** 116 | * Test adding a flag, getting a flag and getting all flags 117 | */ 118 | public function testAddFlags() 119 | { 120 | $args = new cli\Arguments($this->settings); 121 | 122 | $expectedFlags = $this->flags; 123 | $expectedFlags['flag1']['default'] = false; 124 | $expectedFlags['flag1']['stackable'] = false; 125 | $expectedFlags['flag2']['default'] = false; 126 | $expectedFlags['flag2']['stackable'] = false; 127 | $expectedFlags['flag2']['aliases'] = array(); 128 | 129 | $this->assertSame($expectedFlags, $args->getFlags()); 130 | 131 | $this->assertSame($expectedFlags['flag1'], $args->getFlag('flag1')); 132 | $this->assertSame($expectedFlags['flag1'], $args->getFlag('f')); 133 | 134 | $expectedFlag1Argument = new cli\arguments\Argument('-f'); 135 | $this->assertSame($expectedFlags['flag1'], $args->getFlag($expectedFlag1Argument)); 136 | } 137 | 138 | /** 139 | * Test adding a option, getting a option and getting all options 140 | */ 141 | public function testAddOptions() 142 | { 143 | $args = new cli\Arguments($this->settings); 144 | 145 | $expectedOptions = $this->options; 146 | $expectedOptions['option1']['default'] = null; 147 | 148 | $this->assertSame($expectedOptions, $args->getOptions()); 149 | 150 | $this->assertSame($expectedOptions['option1'], $args->getOption('option1')); 151 | $this->assertSame($expectedOptions['option1'], $args->getOption('o')); 152 | 153 | $expectedOption1Argument = new cli\arguments\Argument('-o'); 154 | $this->assertSame($expectedOptions['option1'], $args->getOption($expectedOption1Argument)); 155 | } 156 | 157 | /** 158 | * Data provider with valid args and options 159 | * 160 | * @return array set of args and expected parsed values 161 | */ 162 | public static function settingsWithValidOptions() 163 | { 164 | return array( 165 | array( 166 | array('-o', 'option_value', '-f'), 167 | array('option1' => 'option_value', 'flag1' => true) 168 | ), 169 | array( 170 | array('--option1', 'option_value', '--flag1'), 171 | array('option1' => 'option_value', 'flag1' => true) 172 | ), 173 | array( 174 | array('-f', '--option1', 'option_value'), 175 | array('flag1' => true, 'option1' => 'option_value') 176 | ) 177 | ); 178 | } 179 | 180 | /** 181 | * Data provider with missing options 182 | * 183 | * @return array set of args and expected parsed values 184 | */ 185 | public static function settingsWithMissingOptions() 186 | { 187 | return array( 188 | array( 189 | array('-f', '--option1'), 190 | array('flag1' => true, 'option1' => 'Error should be triggered') 191 | ), 192 | array( 193 | array('--option1', '-f'), 194 | array('option1' => 'Error should be triggered', 'flag1' => true) 195 | ) 196 | ); 197 | } 198 | 199 | /** 200 | * Data provider with missing options. The default value should be populated 201 | * 202 | * @return array set of args and expected parsed values 203 | */ 204 | public static function settingsWithMissingOptionsWithDefault() 205 | { 206 | return array( 207 | array( 208 | array('-f', '--option2'), 209 | array('flag1' => true, 'option2' => 'some default value') 210 | ), 211 | array( 212 | array('--option2', '-f'), 213 | array('option2' => 'some default value', 'flag1' => true) 214 | ) 215 | ); 216 | } 217 | 218 | public static function settingsWithNoOptionsWithDefault() 219 | { 220 | return array( 221 | array( 222 | array(), 223 | array('flag1' => false, 'flag2' => false, 'option2' => 'some default value') 224 | ) 225 | ); 226 | } 227 | 228 | /** 229 | * Generic private testParse method. 230 | * 231 | * @param array $args arguments as they appear in the cli 232 | * @param array $expectedValues expected values after parsing 233 | */ 234 | private function _testParse($cliParams, $expectedValues) 235 | { 236 | self::pushToArgv($cliParams); 237 | 238 | $args = new cli\Arguments($this->settings); 239 | $args->parse(); 240 | 241 | foreach ($expectedValues as $name => $value) { 242 | if ($args->isFlag($name)) { 243 | $this->assertEquals($value, $args[$name]); 244 | } 245 | 246 | if ($args->isOption($name)) { 247 | $this->assertEquals($value, $args[$name]); 248 | } 249 | } 250 | } 251 | 252 | /** 253 | * @param array $args arguments as they appear in the cli 254 | * @param array $expectedValues expected values after parsing 255 | * 256 | * @dataProvider settingsWithValidOptions 257 | */ 258 | public function testParseWithValidOptions($cliParams, $expectedValues) 259 | { 260 | $this->_testParse($cliParams, $expectedValues); 261 | } 262 | 263 | /** 264 | * @param array $args arguments as they appear in the cli 265 | * @param array $expectedValues expected values after parsing 266 | * @dataProvider settingsWithMissingOptions 267 | */ 268 | public function testParseWithMissingOptions($cliParams, $expectedValues) 269 | { 270 | $this->expectException(\Exception::class); 271 | $this->expectExceptionMessage('no value given for --option1'); 272 | $this->_testParse($cliParams, $expectedValues); 273 | } 274 | 275 | /** 276 | * @param array $args arguments as they appear in the cli 277 | * @param array $expectedValues expected values after parsing 278 | * @dataProvider settingsWithMissingOptionsWithDefault 279 | */ 280 | public function testParseWithMissingOptionsWithDefault($cliParams, $expectedValues) 281 | { 282 | $this->_testParse($cliParams, $expectedValues); 283 | } 284 | 285 | /** 286 | * @param array $args arguments as they appear in the cli 287 | * @param array $expectedValues expected values after parsing 288 | * @dataProvider settingsWithNoOptionsWithDefault 289 | */ 290 | public function testParseWithNoOptionsWithDefault($cliParams, $expectedValues) { 291 | $this->_testParse($cliParams, $expectedValues); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /tests/Test_Cli.php: -------------------------------------------------------------------------------- 1 | assertEquals( \cli\Colors::length( 'x' ), 1 ); 18 | $this->assertEquals( \cli\Colors::length( '日' ), 1 ); 19 | } 20 | 21 | function test_string_width() { 22 | $this->assertEquals( \cli\Colors::width( 'x' ), 1 ); 23 | $this->assertEquals( \cli\Colors::width( '日' ), 2 ); // Double-width char. 24 | } 25 | 26 | function test_encoded_string_length() { 27 | 28 | $this->assertEquals( \cli\Colors::length( 'hello' ), 5 ); 29 | $this->assertEquals( \cli\Colors::length( 'óra' ), 3 ); 30 | $this->assertEquals( \cli\Colors::length( '日本語' ), 3 ); 31 | 32 | } 33 | 34 | function test_encoded_string_width() { 35 | 36 | $this->assertEquals( \cli\Colors::width( 'hello' ), 5 ); 37 | $this->assertEquals( \cli\Colors::width( 'óra' ), 3 ); 38 | $this->assertEquals( \cli\Colors::width( '日本語' ), 6 ); // 3 double-width chars. 39 | 40 | } 41 | 42 | function test_encoded_string_pad() { 43 | 44 | $this->assertEquals( 6, strlen( \cli\Colors::pad( 'hello', 6 ) ) ); 45 | $this->assertEquals( 7, strlen( \cli\Colors::pad( 'óra', 6 ) ) ); // special characters take one byte 46 | $this->assertEquals( 9, strlen( \cli\Colors::pad( '日本語', 6 ) ) ); // each character takes two bytes 47 | $this->assertEquals( 17, strlen( \cli\Colors::pad( 'עִבְרִית', 6 ) ) ); // process Hebrew vowels 48 | $this->assertEquals( 6, strlen( \cli\Colors::pad( 'hello', 6, false, false, STR_PAD_RIGHT ) ) ); 49 | $this->assertEquals( 7, strlen( \cli\Colors::pad( 'óra', 6, false, false, STR_PAD_LEFT ) ) ); // special characters take one byte 50 | $this->assertEquals( 9, strlen( \cli\Colors::pad( '日本語', 6, false, false, STR_PAD_BOTH ) ) ); // each character takes two bytes 51 | $this->assertSame( 4, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_RIGHT ), 'o' ) ); 52 | $this->assertSame( 9, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_LEFT ), 'o' ) ); 53 | $this->assertSame( 6, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_BOTH ), 'o' ) ); 54 | $this->assertSame( 1, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_RIGHT ), 'e' ) ); 55 | $this->assertSame( 6, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_LEFT ), 'e' ) ); 56 | $this->assertSame( 3, strpos( \cli\Colors::pad( 'hello', 10, false, false, STR_PAD_BOTH ), 'e' ) ); 57 | } 58 | 59 | function test_colorized_string_pad() { 60 | // Colors enabled. 61 | Colors::enable( true ); 62 | 63 | $x = Colors::colorize( '%Gx%n', true ); // colorized `x` string 64 | $ora = Colors::colorize( "%Góra%n", true ); // colorized `óra` string 65 | 66 | $this->assertSame( 22, strlen( Colors::pad( $x, 11 ) ) ); 67 | $this->assertSame( 22, strlen( Colors::pad( $x, 11, false /*pre_colorized*/ ) ) ); 68 | $this->assertSame( 22, strlen( Colors::pad( $x, 11, true /*pre_colorized*/ ) ) ); 69 | 70 | $this->assertSame( 23, strlen( Colors::pad( $ora, 11 ) ) ); // +1 for two-byte "ó". 71 | $this->assertSame( 23, strlen( Colors::pad( $ora, 11, false /*pre_colorized*/ ) ) ); 72 | $this->assertSame( 23, strlen( Colors::pad( $ora, 11, true /*pre_colorized*/ ) ) ); 73 | 74 | // Colors disabled. 75 | Colors::disable( true ); 76 | $this->assertFalse( Colors::shouldColorize() ); 77 | 78 | $this->assertSame( 20, strlen( Colors::pad( $x, 20 ) ) ); 79 | $this->assertSame( 20, strlen( Colors::pad( $x, 20, false /*pre_colorized*/ ) ) ); 80 | $this->assertSame( 31, strlen( Colors::pad( $x, 20, true /*pre_colorized*/ ) ) ); 81 | 82 | $this->assertSame( 21, strlen( Colors::pad( $ora, 20 ) ) ); // +1 for two-byte "ó". 83 | $this->assertSame( 21, strlen( Colors::pad( $ora, 20, false /*pre_colorized*/ ) ) ); 84 | $this->assertSame( 32, strlen( Colors::pad( $ora, 20, true /*pre_colorized*/ ) ) ); 85 | } 86 | 87 | function test_encoded_substr() { 88 | 89 | $this->assertEquals( \cli\safe_substr( \cli\Colors::pad( 'hello', 6), 0, 2 ), 'he' ); 90 | $this->assertEquals( \cli\safe_substr( \cli\Colors::pad( 'óra', 6), 0, 2 ), 'ór' ); 91 | $this->assertEquals( \cli\safe_substr( \cli\Colors::pad( '日本語', 6), 0, 2 ), '日本' ); 92 | 93 | $this->assertSame( 'el', \cli\safe_substr( Colors::pad( 'hello', 6 ), 1, 2 ) ); 94 | 95 | $this->assertSame( 'a ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 2, 2 ) ); 96 | $this->assertSame( ' ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 5, 2 ) ); 97 | 98 | $this->assertSame( '本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 1, 2 ) ); 99 | $this->assertSame( '語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 2, 2 ) ); 100 | $this->assertSame( ' ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -1 ) ); 101 | $this->assertSame( ' ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -1, 2 ) ); 102 | $this->assertSame( '語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -3, 3 ) ); 103 | } 104 | 105 | function test_various_substr() { 106 | // Save. 107 | $test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); 108 | if ( function_exists( 'mb_detect_order' ) ) { 109 | $mb_detect_order = mb_detect_order(); 110 | } 111 | 112 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); 113 | 114 | // Latin, kana, Latin, Latin combining, Thai combining, Hangul. 115 | $str = 'lムnöม้p를'; // 18 bytes. 116 | 117 | // Large string. 118 | $large_str_str_start = 65536 * 2; 119 | $large_str = str_repeat( 'a', $large_str_str_start ) . $str; 120 | $large_str_len = strlen( $large_str ); // 128K + 18 bytes. 121 | 122 | if ( \cli\can_use_icu() ) { 123 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=1' ); // Tests grapheme_substr(). 124 | $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); 125 | $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); 126 | $this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) ); 127 | $this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) ); 128 | $this->assertSame( 'lムnö', \cli\safe_substr( $str, 0, 4 ) ); 129 | $this->assertSame( 'lムnöม้', \cli\safe_substr( $str, 0, 5 ) ); 130 | $this->assertSame( 'lムnöม้p', \cli\safe_substr( $str, 0, 6 ) ); 131 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 7 ) ); 132 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 8 ) ); 133 | $this->assertSame( '', \cli\safe_substr( $str, 19 ) ); // Start too large. 134 | $this->assertSame( '', \cli\safe_substr( $str, 19, 7 ) ); // Start too large, with length. 135 | $this->assertSame( '', \cli\safe_substr( $str, 8 ) ); // Start same as length. 136 | $this->assertSame( '', \cli\safe_substr( $str, 8, 0 ) ); // Start same as length, with zero length. 137 | $this->assertSame( '를', \cli\safe_substr( $str, -1 ) ); 138 | $this->assertSame( 'p를', \cli\safe_substr( $str, -2 ) ); 139 | $this->assertSame( 'ม้p를', \cli\safe_substr( $str, -3 ) ); 140 | $this->assertSame( 'öม้p를', \cli\safe_substr( $str, -4 ) ); 141 | $this->assertSame( 'öม้p', \cli\safe_substr( $str, -4, 3 ) ); 142 | $this->assertSame( 'nö', \cli\safe_substr( $str, -5, 2 ) ); 143 | $this->assertSame( 'ム', \cli\safe_substr( $str, -6, 1 ) ); 144 | $this->assertSame( 'ムnöม้p를', \cli\safe_substr( $str, -6 ) ); 145 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -7 ) ); 146 | $this->assertSame( 'lムnö', \cli\safe_substr( $str, -7, 4 ) ); 147 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -8 ) ); 148 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -9 ) ); // Negative start too large. 149 | 150 | $this->assertSame( $large_str, \cli\safe_substr( $large_str, 0 ) ); 151 | $this->assertSame( '', \cli\safe_substr( $large_str, $large_str_str_start, 0 ) ); 152 | $this->assertSame( 'l', \cli\safe_substr( $large_str, $large_str_str_start, 1 ) ); 153 | $this->assertSame( 'lム', \cli\safe_substr( $large_str, $large_str_str_start, 2 ) ); 154 | $this->assertSame( 'p를', \cli\safe_substr( $large_str, -2 ) ); 155 | } 156 | 157 | if ( \cli\can_use_pcre_x() ) { 158 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=2' ); // Tests preg_split( '/\X/u' ). 159 | $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); 160 | $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); 161 | $this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) ); 162 | $this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) ); 163 | $this->assertSame( 'lムnö', \cli\safe_substr( $str, 0, 4 ) ); 164 | $this->assertSame( 'lムnöม้', \cli\safe_substr( $str, 0, 5 ) ); 165 | $this->assertSame( 'lムnöม้p', \cli\safe_substr( $str, 0, 6 ) ); 166 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 7 ) ); 167 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 8 ) ); 168 | $this->assertSame( '', \cli\safe_substr( $str, 19 ) ); // Start too large. 169 | $this->assertSame( '', \cli\safe_substr( $str, 19, 7 ) ); // Start too large, with length. 170 | $this->assertSame( '', \cli\safe_substr( $str, 8 ) ); // Start same as length. 171 | $this->assertSame( '', \cli\safe_substr( $str, 8, 0 ) ); // Start same as length, with zero length. 172 | $this->assertSame( '를', \cli\safe_substr( $str, -1 ) ); 173 | $this->assertSame( 'p를', \cli\safe_substr( $str, -2 ) ); 174 | $this->assertSame( 'ม้p를', \cli\safe_substr( $str, -3 ) ); 175 | $this->assertSame( 'öม้p를', \cli\safe_substr( $str, -4 ) ); 176 | $this->assertSame( 'öม้p', \cli\safe_substr( $str, -4, 3 ) ); 177 | $this->assertSame( 'nö', \cli\safe_substr( $str, -5, 2 ) ); 178 | $this->assertSame( 'ム', \cli\safe_substr( $str, -6, 1 ) ); 179 | $this->assertSame( 'ムnöม้p를', \cli\safe_substr( $str, -6 ) ); 180 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -7 ) ); 181 | $this->assertSame( 'lムnö', \cli\safe_substr( $str, -7, 4 ) ); 182 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -8 ) ); 183 | $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -9 ) ); // Negative start too large. 184 | 185 | $this->assertSame( $large_str, \cli\safe_substr( $large_str, 0 ) ); 186 | $this->assertSame( '', \cli\safe_substr( $large_str, $large_str_str_start, 0 ) ); 187 | $this->assertSame( 'l', \cli\safe_substr( $large_str, $large_str_str_start, 1 ) ); 188 | $this->assertSame( 'lム', \cli\safe_substr( $large_str, $large_str_str_start, 2 ) ); 189 | $this->assertSame( 'p를', \cli\safe_substr( $large_str, -2 ) ); 190 | } 191 | 192 | if ( function_exists( 'mb_substr' ) ) { 193 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=4' ); // Tests mb_substr(). 194 | $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); 195 | $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); 196 | $this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) ); 197 | $this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) ); 198 | $this->assertSame( 'lムno', \cli\safe_substr( $str, 0, 4 ) ); // Wrong. 199 | } 200 | 201 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=8' ); // Tests substr(). 202 | $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); 203 | $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); 204 | $this->assertSame( "l\xe3", \cli\safe_substr( $str, 0, 2 ) ); // Corrupt. 205 | $this->assertSame( '', \cli\safe_substr( $str, strlen( $str ) + 1 ) ); // Return '' not false to match behavior of other methods when `$start` > strlen. 206 | 207 | // Non-UTF-8 - both grapheme_substr() and preg_split( '/\X/u' ) will fail. 208 | 209 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); 210 | 211 | if ( function_exists( 'mb_substr' ) && function_exists( 'mb_detect_order' ) ) { 212 | // Latin-1 213 | mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); 214 | $str = "\xe0b\xe7"; // "àbç" in ISO-8859-1 215 | $this->assertSame( "\xe0b", \cli\safe_substr( $str, 0, 2 ) ); 216 | $this->assertSame( "\xe0b", mb_substr( $str, 0, 2, 'ISO-8859-1' ) ); 217 | } 218 | 219 | // Restore. 220 | putenv( false == $test_safe_substr ? 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' : "PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=$test_safe_substr" ); 221 | if ( function_exists( 'mb_detect_order' ) ) { 222 | mb_detect_order( $mb_detect_order ); 223 | } 224 | } 225 | 226 | function test_is_width_encoded_substr() { 227 | 228 | $this->assertSame( 'he', \cli\safe_substr( Colors::pad( 'hello', 6 ), 0, 2, true /*is_width*/ ) ); 229 | $this->assertSame( 'ór', \cli\safe_substr( Colors::pad( 'óra', 6 ), 0, 2, true /*is_width*/ ) ); 230 | $this->assertSame( '日', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 2, true /*is_width*/ ) ); 231 | $this->assertSame( '日', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 3, true /*is_width*/ ) ); 232 | $this->assertSame( '日本', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 4, true /*is_width*/ ) ); 233 | $this->assertSame( '日本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 6, true /*is_width*/ ) ); 234 | $this->assertSame( '日本語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 7, true /*is_width*/ ) ); 235 | 236 | $this->assertSame( 'el', \cli\safe_substr( Colors::pad( 'hello', 6 ), 1, 2, true /*is_width*/ ) ); 237 | 238 | $this->assertSame( 'a ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 2, 2, true /*is_width*/ ) ); 239 | $this->assertSame( ' ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 5, 2, true /*is_width*/ ) ); 240 | 241 | $this->assertSame( '', \cli\safe_substr( '1日4本語90', 0, 0, true /*is_width*/ ) ); 242 | $this->assertSame( '1', \cli\safe_substr( '1日4本語90', 0, 1, true /*is_width*/ ) ); 243 | $this->assertSame( '1', \cli\safe_substr( '1日4本語90', 0, 2, true /*is_width*/ ) ); 244 | $this->assertSame( '1日', \cli\safe_substr( '1日4本語90', 0, 3, true /*is_width*/ ) ); 245 | $this->assertSame( '1日4', \cli\safe_substr( '1日4本語90', 0, 4, true /*is_width*/ ) ); 246 | $this->assertSame( '1日4', \cli\safe_substr( '1日4本語90', 0, 5, true /*is_width*/ ) ); 247 | $this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 6, true /*is_width*/ ) ); 248 | $this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 7, true /*is_width*/ ) ); 249 | $this->assertSame( '1日4本語', \cli\safe_substr( '1日4本語90', 0, 8, true /*is_width*/ ) ); 250 | $this->assertSame( '1日4本語9', \cli\safe_substr( '1日4本語90', 0, 9, true /*is_width*/ ) ); 251 | $this->assertSame( '1日4本語90', \cli\safe_substr( '1日4本語90', 0, 10, true /*is_width*/ ) ); 252 | $this->assertSame( '1日4本語90', \cli\safe_substr( '1日4本語90', 0, 11, true /*is_width*/ ) ); 253 | 254 | $this->assertSame( '日', \cli\safe_substr( '1日4本語90', 1, 2, true /*is_width*/ ) ); 255 | $this->assertSame( '日4', \cli\safe_substr( '1日4本語90', 1, 3, true /*is_width*/ ) ); 256 | $this->assertSame( '4本語9', \cli\safe_substr( '1日4本語90', 2, 6, true /*is_width*/ ) ); 257 | 258 | $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 1, true /*is_width*/ ) ); 259 | $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 2, true /*is_width*/ ) ); 260 | $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 3, true /*is_width*/ ) ); 261 | $this->assertSame( '本語', \cli\safe_substr( '1日4本語90', 3, 4, true /*is_width*/ ) ); 262 | $this->assertSame( '本語9', \cli\safe_substr( '1日4本語90', 3, 5, true /*is_width*/ ) ); 263 | 264 | $this->assertSame( '0', \cli\safe_substr( '1日4本語90', 6, 1, true /*is_width*/ ) ); 265 | $this->assertSame( '', \cli\safe_substr( '1日4本語90', 7, 1, true /*is_width*/ ) ); 266 | $this->assertSame( '', \cli\safe_substr( '1日4本語90', 6, 0, true /*is_width*/ ) ); 267 | 268 | $this->assertSame( '0', \cli\safe_substr( '1日4本語90', -1, 3, true /*is_width*/ ) ); 269 | $this->assertSame( '90', \cli\safe_substr( '1日4本語90', -2, 3, true /*is_width*/ ) ); 270 | $this->assertSame( '語9', \cli\safe_substr( '1日4本語90', -3, 3, true /*is_width*/ ) ); 271 | $this->assertSame( '本語9', \cli\safe_substr( '1日4本語90', -4, 5, true /*is_width*/ ) ); 272 | } 273 | 274 | function test_colorized_string_length() { 275 | $this->assertEquals( \cli\Colors::length( \cli\Colors::colorize( '%Gx%n', true ) ), 1 ); 276 | $this->assertEquals( \cli\Colors::length( \cli\Colors::colorize( '%G日%n', true ) ), 1 ); 277 | } 278 | 279 | function test_colorized_string_width() { 280 | // Colors enabled. 281 | Colors::enable( true ); 282 | 283 | $x = Colors::colorize( '%Gx%n', true ); 284 | $dw = Colors::colorize( '%G日%n', true ); // Double-width char. 285 | 286 | $this->assertSame( 1, Colors::width( $x ) ); 287 | $this->assertSame( 1, Colors::width( $x, false /*pre_colorized*/ ) ); 288 | $this->assertSame( 1, Colors::width( $x, true /*pre_colorized*/ ) ); 289 | 290 | $this->assertSame( 2, Colors::width( $dw ) ); 291 | $this->assertSame( 2, Colors::width( $dw, false /*pre_colorized*/ ) ); 292 | $this->assertSame( 2, Colors::width( $dw, true /*pre_colorized*/ ) ); 293 | 294 | // Colors disabled. 295 | Colors::disable( true ); 296 | $this->assertFalse( Colors::shouldColorize() ); 297 | 298 | $this->assertSame( 12, Colors::width( $x ) ); 299 | $this->assertSame( 12, Colors::width( $x, false /*pre_colorized*/ ) ); 300 | $this->assertSame( 1, Colors::width( $x, true /*pre_colorized*/ ) ); 301 | 302 | $this->assertSame( 13, Colors::width( $dw ) ); 303 | $this->assertSame( 13, Colors::width( $dw, false /*pre_colorized*/ ) ); 304 | $this->assertSame( 2, Colors::width( $dw, true /*pre_colorized*/ ) ); 305 | } 306 | 307 | function test_colorize_string_is_colored() { 308 | $original = '%Gx'; 309 | $colorized = "\033[32;1mx"; 310 | 311 | $this->assertEquals( \cli\Colors::colorize( $original, true ), $colorized ); 312 | } 313 | 314 | function test_colorize_when_colorize_is_forced() { 315 | $original = '%gx%n'; 316 | 317 | $this->assertEquals( \cli\Colors::colorize( $original, false ), 'x' ); 318 | } 319 | 320 | function test_binary_string_is_converted_back_to_original_string() { 321 | $string = 'x'; 322 | $string_with_color = '%b' . $string; 323 | $colorized_string = "\033[34m$string"; 324 | 325 | // Ensure colorization is applied correctly 326 | $this->assertEquals( \cli\Colors::colorize( $string_with_color, true ), $colorized_string ); 327 | 328 | // Ensure that the colorization is reverted 329 | $this->assertEquals( \cli\Colors::decolorize( $colorized_string ), $string ); 330 | } 331 | 332 | function test_string_cache() { 333 | $string = 'x'; 334 | $string_with_color = '%k' . $string; 335 | $colorized_string = "\033[30m$string"; 336 | 337 | // Ensure colorization works 338 | $this->assertEquals( \cli\Colors::colorize( $string_with_color, true ), $colorized_string ); 339 | 340 | // Test that the value was cached appropriately 341 | $test_cache = array( 342 | 'passed' => $string_with_color, 343 | 'colorized' => $colorized_string, 344 | 'decolorized' => $string, 345 | ); 346 | 347 | $real_cache = \cli\Colors::getStringCache(); 348 | 349 | // Test that the cache value exists 350 | $this->assertTrue( isset( $real_cache[ md5( $string_with_color ) ] ) ); 351 | 352 | // Test that the cache value is correctly set 353 | $this->assertEquals( $test_cache, $real_cache[ md5( $string_with_color ) ] ); 354 | } 355 | 356 | function test_string_cache_colorize() { 357 | $string = 'x'; 358 | $string_with_color = '%k' . $string; 359 | $colorized_string = "\033[30m$string"; 360 | 361 | // Colors enabled. 362 | Colors::enable( true ); 363 | 364 | // Ensure colorization works 365 | $this->assertSame( $colorized_string, Colors::colorize( $string_with_color ) ); 366 | $this->assertSame( $colorized_string, Colors::colorize( $string_with_color ) ); 367 | 368 | // Colors disabled. 369 | Colors::disable( true ); 370 | $this->assertFalse( Colors::shouldColorize() ); 371 | 372 | // Ensure it doesn't come from the cache. 373 | $this->assertSame( $string, Colors::colorize( $string_with_color ) ); 374 | $this->assertSame( $string, Colors::colorize( $string_with_color ) ); 375 | 376 | // Check that escaped % isn't stripped on putting into cache. 377 | $string = 'x%%n'; 378 | $string_with_color = '%k' . $string; 379 | $this->assertSame( 'x%n', Colors::colorize( $string_with_color ) ); 380 | $this->assertSame( 'x%n', Colors::colorize( $string_with_color ) ); 381 | } 382 | 383 | function test_decolorize() { 384 | // Colors enabled. 385 | Colors::enable( true ); 386 | 387 | $string = '%kx%%n%n'; 388 | $colorized_string = Colors::colorize( $string ); 389 | $both_string = '%gfoo' . $colorized_string . 'bar%%%n'; 390 | 391 | $this->assertSame( 'x%n', Colors::decolorize( $string ) ); 392 | $this->assertSame( 'x', Colors::decolorize( $colorized_string ) ); 393 | $this->assertSame( 'fooxbar%', Colors::decolorize( $both_string ) ); 394 | 395 | $this->assertSame( $string, Colors::decolorize( $string, 1 /*keep_tokens*/ ) ); 396 | $this->assertSame( 'x%n', Colors::decolorize( $colorized_string, 1 /*keep_tokens*/ ) ); 397 | $this->assertSame( '%gfoox%nbar%%%n', Colors::decolorize( $both_string, 1 /*keep_tokens*/ ) ); 398 | 399 | $this->assertSame( 'x%n', Colors::decolorize( $string, 2 /*keep_encodings*/ ) ); 400 | $this->assertSame( 'x', Colors::decolorize( $colorized_string, 2 /*keep_encodings*/ ) ); 401 | $this->assertSame( 'fooxbar%', Colors::decolorize( $both_string, 2 /*keep_encodings*/ ) ); 402 | 403 | $this->assertSame( $string, Colors::decolorize( $string, 3 /*noop*/ ) ); 404 | $this->assertSame( $colorized_string, Colors::decolorize( $colorized_string, 3 /*noop*/ ) ); 405 | $this->assertSame( $both_string, Colors::decolorize( $both_string, 3 /*noop*/ ) ); 406 | } 407 | 408 | function test_strwidth() { 409 | $this->markTestSkipped('Unknown failure'); 410 | // Save. 411 | $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); 412 | if ( function_exists( 'mb_detect_order' ) ) { 413 | $mb_detect_order = mb_detect_order(); 414 | } 415 | 416 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); 417 | 418 | // UTF-8. 419 | 420 | // 4 characters, one a double-width Han = 5 spacing chars, with 2 combining chars. Adapted from http://unicode.org/faq/char_combmark.html#7 (combining acute accent added after "a"). 421 | $str = "a\xCC\x81\xE0\xA4\xA8\xE0\xA4\xBF\xE4\xBA\x9C\xF0\x90\x82\x83"; 422 | 423 | if ( \cli\can_use_icu() ) { 424 | $this->assertSame( 5, \cli\strwidth( $str ) ); // Tests grapheme_strlen(). 425 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_split( '/\X/u' ). 426 | $this->assertSame( 5, \cli\strwidth( $str ) ); 427 | } else { 428 | $this->assertSame( 5, \cli\strwidth( $str ) ); // Tests preg_split( '/\X/u' ). 429 | } 430 | 431 | if ( function_exists( 'mb_strwidth' ) && function_exists( 'mb_detect_order' ) ) { 432 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=4' ); // Test mb_strwidth(). 433 | mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); 434 | $this->assertSame( 5, \cli\strwidth( $str ) ); 435 | } 436 | 437 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=8' ); // Test safe_strlen(). 438 | if ( \cli\can_use_icu() || \cli\can_use_pcre_x() ) { 439 | $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. 440 | } elseif ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { 441 | $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. 442 | $this->assertSame( 6, mb_strlen( $str, 'UTF-8' ) ); 443 | } else { 444 | $this->assertSame( 16, \cli\strwidth( $str ) ); // strlen() - no. of bytes. 445 | $this->assertSame( 16, strlen( $str ) ); 446 | } 447 | 448 | // Nepali जस्ट ट॓स्ट गर्दै - 1st word: 3 spacing + 1 combining, 2nd word: 3 spacing + 2 combining, 3rd word: 3 spacing + 2 combining = 9 spacing chars + 2 spaces = 11 chars. 449 | $str = "\xe0\xa4\x9c\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\x9f \xe0\xa4\x9f\xe0\xa5\x93\xe0\xa4\xb8\xe0\xa5\x8d\xe0\xa4\x9f \xe0\xa4\x97\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\xa6\xe0\xa5\x88"; 450 | 451 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); 452 | 453 | if ( \cli\can_use_icu() ) { 454 | $this->assertSame( 11, \cli\strwidth( $str ) ); // Tests grapheme_strlen(). 455 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_split( '/\X/u' ). 456 | $this->assertSame( 11, \cli\strwidth( $str ) ); 457 | } else { 458 | $this->assertSame( 11, \cli\strwidth( $str ) ); // Tests preg_split( '/\X/u' ). 459 | } 460 | 461 | if ( function_exists( 'mb_strwidth' ) && function_exists( 'mb_detect_order' ) ) { 462 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=4' ); // Test mb_strwidth(). 463 | mb_detect_order( array( 'UTF-8' ) ); 464 | $this->assertSame( 11, \cli\strwidth( $str ) ); 465 | } 466 | 467 | // Non-UTF-8 - both grapheme_strlen() and preg_split( '/\X/u' ) will fail. 468 | 469 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); 470 | 471 | if ( function_exists( 'mb_strwidth' ) && function_exists( 'mb_detect_order' ) ) { 472 | // Latin-1 473 | mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); 474 | $str = "\xe0b\xe7"; // "àbç" in ISO-8859-1 475 | $this->assertSame( 3, \cli\strwidth( $str ) ); // Test mb_strwidth(). 476 | $this->assertSame( 3, mb_strwidth( $str, 'ISO-8859-1' ) ); 477 | 478 | // Shift JIS. 479 | mb_detect_order( array( 'UTF-8', 'SJIS' ) ); 480 | $str = "\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd\x90\xa2\x8a\x45!"; // "こャにちは世界!" ("Hello world!") in Shift JIS - 7 double-width chars plus Latin exclamation mark. 481 | $this->assertSame( 15, \cli\strwidth( $str ) ); // Test mb_strwidth(). 482 | $this->assertSame( 15, mb_strwidth( $str, 'SJIS' ) ); 483 | 484 | putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=8' ); // Test safe_strlen(). 485 | if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { 486 | $this->assertSame( 8, \cli\strwidth( $str ) ); // mb_strlen() - doesn't allow for double-width. 487 | $this->assertSame( 8, mb_strlen( $str, 'SJIS' ) ); 488 | } else { 489 | $this->assertSame( 15, \cli\strwidth( $str ) ); // strlen() - no. of bytes. 490 | $this->assertSame( 15, strlen( $str ) ); 491 | } 492 | } 493 | 494 | // Restore. 495 | putenv( false == $test_strwidth ? 'PHP_CLI_TOOLS_TEST_STRWIDTH' : "PHP_CLI_TOOLS_TEST_STRWIDTH=$test_strwidth" ); 496 | if ( function_exists( 'mb_detect_order' ) ) { 497 | mb_detect_order( $mb_detect_order ); 498 | } 499 | } 500 | 501 | function test_safe_strlen() { 502 | // Save. 503 | $test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); 504 | if ( function_exists( 'mb_detect_order' ) ) { 505 | $mb_detect_order = mb_detect_order(); 506 | } 507 | 508 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); 509 | 510 | // UTF-8. 511 | 512 | // ASCII l, 3-byte kana, ASCII n, ASCII o + 2-byte combining umlaut, 6-byte Thai combining, ASCII, 3-byte Hangul. grapheme length 7, bytes 18. 513 | $str = 'lムnöม้p를'; 514 | 515 | if ( \cli\can_use_icu() ) { 516 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Test grapheme_strlen(). 517 | $this->assertSame( 7, \cli\safe_strlen( $str ) ); 518 | if ( \cli\can_use_pcre_x() ) { 519 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=2' ); // Test preg_split( '/\X/u' ). 520 | $this->assertSame( 7, \cli\safe_strlen( $str ) ); 521 | } 522 | } elseif ( \cli\can_use_pcre_x() ) { 523 | $this->assertSame( 7, \cli\safe_strlen( $str ) ); // Tests preg_split( '/\X/u' ). 524 | } else { 525 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=8' ); // Test strlen(). 526 | $this->assertSame( 18, \cli\safe_strlen( $str ) ); // strlen() - no. of bytes. 527 | $this->assertSame( 18, strlen( $str ) ); 528 | } 529 | 530 | if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { 531 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=4' ); // Test mb_strlen(). 532 | mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); 533 | $this->assertSame( 7, \cli\safe_strlen( $str ) ); 534 | $this->assertSame( 9, mb_strlen( $str, 'UTF-8' ) ); // mb_strlen() - counts the 2 combining chars. 535 | } 536 | 537 | // Non-UTF-8 - both grapheme_strlen() and preg_split( '/\X/u' ) will fail. 538 | 539 | putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); 540 | 541 | if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { 542 | // Latin-1 543 | mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); 544 | $str = "\xe0b\xe7"; // "àbç" in ISO-8859-1 545 | $this->assertSame( 3, \cli\safe_strlen( $str ) ); // Test mb_strlen(). 546 | $this->assertSame( 3, mb_strlen( $str, 'ISO-8859-1' ) ); 547 | } 548 | 549 | // Restore. 550 | putenv( false == $test_safe_strlen ? 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' : "PHP_CLI_TOOLS_TEST_SAFE_STRLEN=$test_safe_strlen" ); 551 | if ( function_exists( 'mb_detect_order' ) ) { 552 | mb_detect_order( $mb_detect_order ); 553 | } 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /tests/Test_Colors.php: -------------------------------------------------------------------------------- 1 | assertSame( Colors::colorize( $str ), Colors::color( $color ) ); 17 | if ( in_array( 'reset', $color ) ) { 18 | $this->assertTrue( false !== strpos( $colored, '[0m' ) ); 19 | } else { 20 | $this->assertTrue( false === strpos( $colored, '[0m' ) ); 21 | } 22 | } 23 | 24 | public static function dataColors() { 25 | $ret = array(); 26 | foreach ( Colors::getColors() as $str => $color ) { 27 | $ret[] = array( $str, $color ); 28 | } 29 | return $ret; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Test_Shell.php: -------------------------------------------------------------------------------- 1 | assertSame( 80, $columns ); 31 | 32 | putenv( 'WP_CLI_TEST_IS_WINDOWS=1' ); 33 | $columns = cli\Shell::columns(); 34 | $this->assertSame( 80, $columns ); 35 | 36 | // TERM and COLUMNS should result in whatever COLUMNS is. 37 | 38 | putenv( 'TERM=vt100' ); 39 | putenv( 'COLUMNS=100' ); 40 | 41 | putenv( 'WP_CLI_TEST_IS_WINDOWS=0' ); 42 | $columns = cli\Shell::columns(); 43 | $this->assertSame( 100, $columns ); 44 | 45 | putenv( 'WP_CLI_TEST_IS_WINDOWS=1' ); 46 | $columns = cli\Shell::columns(); 47 | $this->assertSame( 100, $columns ); 48 | 49 | // Restore. 50 | putenv( false === $env_term ? 'TERM' : "TERM=$env_term" ); 51 | putenv( false === $env_columns ? 'COLUMNS' : "COLUMNS=$env_columns" ); 52 | putenv( false === $env_is_windows ? 'WP_CLI_TEST_IS_WINDOWS' : "WP_CLI_TEST_IS_WINDOWS=$env_is_windows" ); 53 | putenv( false === $env_shell_columns_reset ? 'PHP_CLI_TOOLS_TEST_SHELL_COLUMNS_RESET' : "PHP_CLI_TOOLS_TEST_SHELL_COLUMNS_RESET=$env_shell_columns_reset" ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Test_Table.php: -------------------------------------------------------------------------------- 1 | setConstraintWidth( $constraint_width ); 20 | $table->setRenderer( $renderer ); 21 | $table->setHeaders( array( 'Field', 'Value' ) ); 22 | $table->addRow( array( 'description', 'The 2012 theme for WordPress is a fully responsive theme that looks great on any device. Features include a front page template with its own widgets, an optional display font, styling for post formats on both index and single views, and an optional no-sidebar page template. Make it yours with a custom menu, header image, and background.' ) ); 23 | $table->addRow( array( 'author', 'the WordPress team' ) ); 24 | 25 | $out = $table->getDisplayLines(); 26 | $this->assertCount( 12, $out ); 27 | $this->assertEquals( $constraint_width, strlen( $out[0] ) ); 28 | $this->assertEquals( $constraint_width, strlen( $out[1] ) ); 29 | $this->assertEquals( $constraint_width, strlen( $out[2] ) ); 30 | $this->assertEquals( $constraint_width, strlen( $out[3] ) ); 31 | $this->assertEquals( $constraint_width, strlen( $out[4] ) ); 32 | $this->assertEquals( $constraint_width, strlen( $out[5] ) ); 33 | $this->assertEquals( $constraint_width, strlen( $out[6] ) ); 34 | $this->assertEquals( $constraint_width, strlen( $out[7] ) ); 35 | $this->assertEquals( $constraint_width, strlen( $out[8] ) ); 36 | $this->assertEquals( $constraint_width, strlen( $out[9] ) ); 37 | $this->assertEquals( $constraint_width, strlen( $out[10] ) ); 38 | $this->assertEquals( $constraint_width, strlen( $out[11] ) ); 39 | 40 | $constraint_width = 81; 41 | 42 | $renderer = new cli\Table\Ascii; 43 | $renderer->setConstraintWidth( $constraint_width ); 44 | $table->setRenderer( $renderer ); 45 | 46 | $out = $table->getDisplayLines(); 47 | for ( $i = 0; $i < count( $out ); $i++ ) { 48 | $this->assertEquals( $constraint_width, strlen( $out[ $i ] ) ); 49 | } 50 | } 51 | 52 | public function test_column_value_too_long_with_multibytes() { 53 | 54 | $constraint_width = 80; 55 | 56 | $table = new cli\Table; 57 | $renderer = new cli\Table\Ascii; 58 | $renderer->setConstraintWidth( $constraint_width ); 59 | $table->setRenderer( $renderer ); 60 | $table->setHeaders( array( 'Field', 'Value' ) ); 61 | $table->addRow( array( '1この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。2この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。', 'こんにちは' ) ); 62 | $table->addRow( array( 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 'Hello' ) ); 63 | 64 | $out = $table->getDisplayLines(); 65 | for ( $i = 0; $i < count( $out ); $i++ ) { 66 | $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); 67 | } 68 | 69 | $constraint_width = 81; 70 | 71 | $renderer = new cli\Table\Ascii; 72 | $renderer->setConstraintWidth( $constraint_width ); 73 | $table->setRenderer( $renderer ); 74 | 75 | $out = $table->getDisplayLines(); 76 | for ( $i = 0; $i < count( $out ); $i++ ) { 77 | $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); 78 | } 79 | } 80 | 81 | public function test_column_odd_single_width_with_double_width() { 82 | 83 | $dummy = new cli\Table; 84 | $renderer = new cli\Table\Ascii; 85 | 86 | $strip_borders = function ( $a ) { 87 | return array_map( function ( $v ) { 88 | return substr( $v, 2, -2 ); 89 | }, $a ); 90 | }; 91 | 92 | $renderer->setWidths( array( 10 ) ); 93 | 94 | // 1 single-width, 6 double-width, 1 single-width, 2 double-width, 1 half-width, 2 double-width. 95 | $out = $renderer->row( array( '1あいうえおか2きくカけこ' ) ); 96 | $result = $strip_borders( explode( "\n", $out ) ); 97 | 98 | $this->assertSame( 3, count( $result ) ); 99 | $this->assertSame( '1あいうえ ', $result[0] ); // 1 single width, 4 double-width, space = 10. 100 | $this->assertSame( 'おか2きくカ', $result[1] ); // 2 double-width, 1 single-width, 2 double-width, 1 half-width = 10. 101 | $this->assertSame( 'けこ ', $result[2] ); // 2 double-width, 8 spaces = 10. 102 | 103 | // Minimum width 1. 104 | 105 | $renderer->setWidths( array( 1 ) ); 106 | 107 | $out = $renderer->row( array( '1あいうえおか2きくカけこ' ) ); 108 | $result = $strip_borders( explode( "\n", $out ) ); 109 | 110 | $this->assertSame( 13, count( $result ) ); 111 | // Uneven rows. 112 | $this->assertSame( '1', $result[0] ); 113 | $this->assertSame( 'あ', $result[1] ); 114 | 115 | // Zero width does no wrapping. 116 | 117 | $renderer->setWidths( array( 0 ) ); 118 | 119 | $out = $renderer->row( array( '1あいうえおか2きくカけこ' ) ); 120 | $result = $strip_borders( explode( "\n", $out ) ); 121 | 122 | $this->assertSame( 1, count( $result ) ); 123 | } 124 | 125 | public function test_column_fullwidth_and_combining() { 126 | 127 | $constraint_width = 80; 128 | 129 | $table = new cli\Table; 130 | $renderer = new cli\Table\Ascii; 131 | $renderer->setConstraintWidth( $constraint_width ); 132 | $table->setRenderer( $renderer ); 133 | $table->setHeaders( array( 'Field', 'Value' ) ); 134 | $table->addRow( array( 'ID', 2151 ) ); 135 | $table->addRow( array( 'post_author', 1 ) ); 136 | $table->addRow( array( 'post_title', 'only-english-lorem-ipsum-dolor-sit-amet-consectetur-adipisicing-elit-sed-do-eiusmod-tempor-incididunt-ut-labore' ) ); 137 | $table->addRow( array( 'post_content', 138 | //'ให้รู้จัก ให้หาหนทางใหม่' . 139 | '♫ มีอีกหลายต่อหลายคน เขาอดทนก็เพื่อรัก' . "\n" . 140 | 'รักผลักดันให้รู้จัก ให้หาหนทางใหม่' . "\r\n" . 141 | 'ฉันจะล้มตั้งหลายที ดีที่รักมาฉุดไว้' . "\r\n" . 142 | 'รักสร้างสรรค์สิ่งมากมาย และหลอมละลายทุกหัวใจ' . "\r\n" . 143 | 'จะมาร้ายดียังไง แต่ใจก็ยังต้องการ' . "\r\n" . 144 | 'ในทุกๆ วัน โลกหมุนด้วยความรัก ♫' . "\n" . 145 | 'ขอแสดงความยินดี งานแต่งพี่ Earn & Menn' ."\r\n" . 146 | 'เที่ยวปายหน้าร้อน ก็เที่ยวได้เหมือนกันน่ะ' . "\r\n" . 147 | ' ジョバンニはまっ赤になってうなずきました。けれどもいつかジョバンニの眼のなかには涙がいっぱいになりました。そうだ僕は知っていたのだ、もちろんカムパネルラも知っている。' ."\r\n" . 148 | 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore' . "\n" . 149 | '' 150 | ) ); 151 | 152 | $out = $table->getDisplayLines(); 153 | for ( $i = 0; $i < count( $out ); $i++ ) { 154 | $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); 155 | } 156 | 157 | $constraint_width = 81; 158 | 159 | $renderer = new cli\Table\Ascii; 160 | $renderer->setConstraintWidth( $constraint_width ); 161 | $table->setRenderer( $renderer ); 162 | 163 | $out = $table->getDisplayLines(); 164 | for ( $i = 0; $i < count( $out ); $i++ ) { 165 | $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); 166 | } 167 | 168 | $constraint_width = 200; 169 | 170 | $renderer = new cli\Table\Ascii; 171 | $renderer->setConstraintWidth( $constraint_width ); 172 | $table->setRenderer( $renderer ); 173 | 174 | $out = $table->getDisplayLines(); 175 | for ( $i = 0; $i < count( $out ); $i++ ) { 176 | $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); 177 | } 178 | } 179 | 180 | public function test_ascii_pre_colorized_widths() { 181 | 182 | Colors::enable( true ); 183 | 184 | $headers = array( 'package', 'version', 'result' ); 185 | $items = array( 186 | array( Colors::colorize( '%ygaa/gaa-kabes%n' ), 'dev-master', Colors::colorize( "%rx%n" ) ), 187 | array( Colors::colorize( '%ygaa/gaa-log%n' ), '*', Colors::colorize( "%gok%n" ) ), 188 | array( Colors::colorize( '%ygaa/gaa-nonsense%n' ), 'v3.0.11', Colors::colorize( "%rx%n" ) ), 189 | array( Colors::colorize( '%ygaa/gaa-100%%new%n' ), 'v100%new', Colors::colorize( "%gok%n" ) ), 190 | ); 191 | 192 | // Disable colorization, as `\WP_CLI\Formatter::show_table()` does for Ascii tables. 193 | Colors::disable( true ); 194 | $this->assertFalse( Colors::shouldColorize() ); 195 | 196 | // Account for colorization of columns 0 & 2. 197 | 198 | $table = new Table; 199 | $renderer = new Ascii; 200 | $table->setRenderer( $renderer ); 201 | $table->setAsciiPreColorized( array( true, false, true ) ); 202 | $table->setHeaders( $headers ); 203 | $table->setRows( $items ); 204 | 205 | $out = $table->getDisplayLines(); 206 | 207 | // "+ 4" accommodates 3 borders and header. 208 | $this->assertSame( 4 + 4, count( $out ) ); 209 | 210 | // Borders & header. 211 | $this->assertSame( 42, strlen( $out[0] ) ); 212 | $this->assertSame( 42, strlen( $out[1] ) ); 213 | $this->assertSame( 42, strlen( $out[2] ) ); 214 | $this->assertSame( 42, strlen( $out[7] ) ); 215 | 216 | // Data. 217 | $this->assertSame( 60, strlen( $out[3] ) ); 218 | $this->assertSame( 60, strlen( $out[4] ) ); 219 | $this->assertSame( 60, strlen( $out[5] ) ); 220 | $this->assertSame( 60, strlen( $out[6] ) ); 221 | 222 | // Don't account for colorization of columns 0 & 2. 223 | 224 | $table = new Table; 225 | $renderer = new Ascii; 226 | $table->setRenderer( $renderer ); 227 | $table->setHeaders( $headers ); 228 | $table->setRows( $items ); 229 | 230 | $out = $table->getDisplayLines(); 231 | 232 | // "+ 4" accommodates 3 borders and header. 233 | $this->assertSame( 4 + 4, count( $out ) ); 234 | 235 | // Borders & header. 236 | $this->assertSame( 56, strlen( $out[0] ) ); 237 | $this->assertSame( 56, strlen( $out[1] ) ); 238 | $this->assertSame( 56, strlen( $out[2] ) ); 239 | $this->assertSame( 56, strlen( $out[7] ) ); 240 | 241 | // Data. 242 | $this->assertSame( 56, strlen( $out[3] ) ); 243 | $this->assertSame( 56, strlen( $out[4] ) ); 244 | $this->assertSame( 56, strlen( $out[5] ) ); 245 | $this->assertSame( 56, strlen( $out[6] ) ); 246 | } 247 | 248 | public function test_preserve_trailing_tabs() { 249 | $table = new cli\Table(); 250 | $renderer = new cli\Table\Tabular(); 251 | $table->setRenderer( $renderer ); 252 | 253 | $table->setHeaders( array( 'Field', 'Type', 'Null', 'Key', 'Default', 'Extra' ) ); 254 | 255 | // Add row with missing values at the end 256 | $table->addRow( array( 'date', 'date', 'NO', 'PRI', '', '' ) ); 257 | $table->addRow( array( 'awesome_stuff', 'text', 'YES', '', '', '' ) ); 258 | 259 | $out = $table->getDisplayLines(); 260 | 261 | $expected = [ 262 | "Field\tType\tNull\tKey\tDefault\tExtra", 263 | "date\tdate\tNO\tPRI\t\t", 264 | "awesome_stuff\ttext\tYES\t\t\t", 265 | ]; 266 | 267 | $this->assertSame( $expected, $out, 'Trailing tabs should be preserved in table output.' ); 268 | } 269 | 270 | public function test_null_values_are_handled() { 271 | $table = new cli\Table(); 272 | $renderer = new cli\Table\Tabular(); 273 | $table->setRenderer( $renderer ); 274 | 275 | $table->setHeaders( array( 'Field', 'Type', 'Null', 'Key', 'Default', 'Extra' ) ); 276 | 277 | // Add row with a null value in the middle 278 | $table->addRow( array( 'id', 'int', 'NO', 'PRI', null, 'auto_increment' ) ); 279 | 280 | // Add row with a null value at the end 281 | $table->addRow( array( 'name', 'varchar(255)', 'YES', '', 'NULL', null ) ); 282 | 283 | $out = $table->getDisplayLines(); 284 | 285 | $expected = [ 286 | "Field\tType\tNull\tKey\tDefault\tExtra", 287 | "id\tint\tNO\tPRI\t\tauto_increment", 288 | "name\tvarchar(255)\tYES\t\tNULL\t", 289 | ]; 290 | $this->assertSame( $expected, $out, 'Null values should be safely converted to empty strings in table output.' ); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /tests/Test_Table_Ascii.php: -------------------------------------------------------------------------------- 1 | _mockFile = tempnam(sys_get_temp_dir(), 'temp'); 30 | $resource = fopen($this->_mockFile, 'wb'); 31 | Streams::setStream('out', $resource); 32 | 33 | $this->_instance = new Table(); 34 | $this->_instance->setRenderer(new Ascii()); 35 | } 36 | 37 | /** 38 | * Cleans temporary file 39 | */ 40 | public function tear_down() { 41 | if (file_exists($this->_mockFile)) { 42 | unlink($this->_mockFile); 43 | } 44 | } 45 | 46 | /** 47 | * Draw simple One column table 48 | */ 49 | public function testDrawOneColumnTable() { 50 | $headers = array('Test Header'); 51 | $rows = array( 52 | array('x'), 53 | ); 54 | $output = <<<'OUT' 55 | +-------------+ 56 | | Test Header | 57 | +-------------+ 58 | | x | 59 | +-------------+ 60 | 61 | OUT; 62 | $this->assertInOutEquals(array($headers, $rows), $output); 63 | } 64 | 65 | /** 66 | * Draw simple One column table with colored string 67 | * Output should look like: 68 | * +-------------+ 69 | * | Test Header | 70 | * +-------------+ 71 | * | x | 72 | * +-------------+ 73 | * 74 | * where `x` character has green color. 75 | * At the same time it checks that `green` defined in `cli\Colors` really looks as `green`. 76 | */ 77 | public function testDrawOneColumnColoredTable() { 78 | Colors::enable( true ); 79 | $headers = array('Test Header'); 80 | $rows = array( 81 | array(Colors::colorize('%Gx%n', true)), 82 | ); 83 | // green `x` 84 | $x = "\x1B\x5B\x33\x32\x3B\x31\x6Dx\x1B\x5B\x30\x6D"; 85 | $output = <<assertInOutEquals(array($headers, $rows), $output); 94 | } 95 | 96 | /** 97 | * Check it works with colors disabled. 98 | */ 99 | public function testDrawOneColumnColorDisabledTable() { 100 | Colors::disable( true ); 101 | $this->assertFalse( Colors::shouldColorize() ); 102 | $headers = array('Test Header'); 103 | $rows = array( 104 | array('%Gx%n'), 105 | ); 106 | $output = <<assertInOutEquals(array($headers, $rows), $output); 115 | } 116 | 117 | /** 118 | * Checks that spacing and borders are handled correctly in table 119 | */ 120 | public function testSpacingInTable() { 121 | $headers = array('A', ' ', 'C', ''); 122 | $rows = array( 123 | array(' ', 'B1', '', 'D1'), 124 | array('A2', '', ' C2', null), 125 | ); 126 | $output = <<<'OUT' 127 | +-------+------+-----+----+ 128 | | A | | C | | 129 | +-------+------+-----+----+ 130 | | | B1 | | D1 | 131 | | A2 | | C2 | | 132 | +-------+------+-----+----+ 133 | 134 | OUT; 135 | $this->assertInOutEquals(array($headers, $rows), $output); 136 | } 137 | 138 | /** 139 | * Test correct table indentation and border positions for multibyte strings 140 | */ 141 | public function testTableWithMultibyteStrings() { 142 | $headers = array('German', 'French', 'Russian', 'Chinese'); 143 | $rows = array( 144 | array('Schätzen', 'Apprécier', 'Оценить', '欣賞'), 145 | ); 146 | $output = <<<'OUT' 147 | +----------+-----------+---------+---------+ 148 | | German | French | Russian | Chinese | 149 | +----------+-----------+---------+---------+ 150 | | Schätzen | Apprécier | Оценить | 欣賞 | 151 | +----------+-----------+---------+---------+ 152 | 153 | OUT; 154 | $this->assertInOutEquals(array($headers, $rows), $output); 155 | } 156 | 157 | /** 158 | * Test that % gets escaped correctly. 159 | */ 160 | public function testTableWithPercentCharacters() { 161 | $headers = array( 'Heading', 'Heading2', 'Heading3' ); 162 | $rows = array( 163 | array( '% at start', 'at end %', 'in % middle' ) 164 | ); 165 | $output = <<<'OUT' 166 | +------------+----------+-------------+ 167 | | Heading | Heading2 | Heading3 | 168 | +------------+----------+-------------+ 169 | | % at start | at end % | in % middle | 170 | +------------+----------+-------------+ 171 | 172 | OUT; 173 | $this->assertInOutEquals(array($headers, $rows), $output); 174 | } 175 | 176 | /** 177 | * Test that a % is appropriately padded in the table 178 | */ 179 | public function testTablePaddingWithPercentCharacters() { 180 | $headers = array( 'ID', 'post_title', 'post_name' ); 181 | $rows = array( 182 | array( 183 | 3, 184 | '10%', 185 | '' 186 | ), 187 | array( 188 | 1, 189 | 'Hello world!', 190 | 'hello-world' 191 | ), 192 | ); 193 | $output = <<<'OUT' 194 | +----+--------------+-------------+ 195 | | ID | post_title | post_name | 196 | +----+--------------+-------------+ 197 | | 3 | 10% | | 198 | | 1 | Hello world! | hello-world | 199 | +----+--------------+-------------+ 200 | 201 | OUT; 202 | $this->assertInOutEquals(array($headers, $rows), $output); 203 | } 204 | 205 | /** 206 | * Draw wide multiplication Table. 207 | * Example with many columns, many rows 208 | */ 209 | public function testDrawMultiplicationTable() { 210 | $maxFactor = 16; 211 | $headers = array_merge(array('x'), range(1, $maxFactor)); 212 | for ($i = 1, $rows = array(); $i <= $maxFactor; ++$i) { 213 | $rows[] = array_merge(array($i), range($i, $i * $maxFactor, $i)); 214 | } 215 | 216 | $output = <<<'OUT' 217 | +----+----+----+----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 218 | | x | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 219 | +----+----+----+----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 220 | | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 221 | | 2 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 222 | | 3 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 223 | | 4 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 224 | | 5 | 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 225 | | 6 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 | 54 | 60 | 66 | 72 | 78 | 84 | 90 | 96 | 226 | | 7 | 7 | 14 | 21 | 28 | 35 | 42 | 49 | 56 | 63 | 70 | 77 | 84 | 91 | 98 | 105 | 112 | 227 | | 8 | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 104 | 112 | 120 | 128 | 228 | | 9 | 9 | 18 | 27 | 36 | 45 | 54 | 63 | 72 | 81 | 90 | 99 | 108 | 117 | 126 | 135 | 144 | 229 | | 10 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | 110 | 120 | 130 | 140 | 150 | 160 | 230 | | 11 | 11 | 22 | 33 | 44 | 55 | 66 | 77 | 88 | 99 | 110 | 121 | 132 | 143 | 154 | 165 | 176 | 231 | | 12 | 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 108 | 120 | 132 | 144 | 156 | 168 | 180 | 192 | 232 | | 13 | 13 | 26 | 39 | 52 | 65 | 78 | 91 | 104 | 117 | 130 | 143 | 156 | 169 | 182 | 195 | 208 | 233 | | 14 | 14 | 28 | 42 | 56 | 70 | 84 | 98 | 112 | 126 | 140 | 154 | 168 | 182 | 196 | 210 | 224 | 234 | | 15 | 15 | 30 | 45 | 60 | 75 | 90 | 105 | 120 | 135 | 150 | 165 | 180 | 195 | 210 | 225 | 240 | 235 | | 16 | 16 | 32 | 48 | 64 | 80 | 96 | 112 | 128 | 144 | 160 | 176 | 192 | 208 | 224 | 240 | 256 | 236 | +----+----+----+----+----+----+----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 237 | 238 | OUT; 239 | $this->assertInOutEquals(array($headers, $rows), $output); 240 | } 241 | 242 | /** 243 | * Draw a table with headers but no data 244 | */ 245 | public function testDrawWithHeadersNoData() { 246 | $headers = array('header 1', 'header 2'); 247 | $rows = array(); 248 | $output = <<<'OUT' 249 | +----------+----------+ 250 | | header 1 | header 2 | 251 | +----------+----------+ 252 | +----------+----------+ 253 | 254 | OUT; 255 | $this->assertInOutEquals(array($headers, $rows), $output); 256 | } 257 | 258 | /** 259 | * Verifies that Input and Output equals, 260 | * Sugar method for fast access from tests 261 | * 262 | * @param array $input First element is header array, second element is rows array 263 | * @param mixed $output Expected output 264 | */ 265 | private function assertInOutEquals(array $input, $output) { 266 | $this->_instance->setHeaders($input[0]); 267 | $this->_instance->setRows($input[1]); 268 | $this->_instance->display(); 269 | $this->assertOutFileEqualsWith($output); 270 | } 271 | 272 | /** 273 | * Checks that contents of input string and temporary file match 274 | * 275 | * @param mixed $expected Expected output 276 | */ 277 | private function assertOutFileEqualsWith($expected) { 278 | $this->assertEquals($expected, file_get_contents($this->_mockFile)); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |