├── src ├── Charset │ ├── CharsetInterface.php │ ├── Utf8Heavy.php │ ├── Utf8.php │ ├── Ascii.php │ ├── DECSG.php │ └── AsciiExtended.php ├── Exception │ ├── ExceptionInterface.php │ ├── BadMethodCallException.php │ ├── InvalidArgumentException.php │ └── RuntimeException.php ├── RouteMatcher │ ├── RouteMatcherInterface.php │ └── DefaultRouteMatcher.php ├── ColorInterface.php ├── Prompt │ ├── PromptInterface.php │ ├── Password.php │ ├── AbstractPrompt.php │ ├── Confirm.php │ ├── Line.php │ ├── Select.php │ ├── Char.php │ ├── Checkbox.php │ └── Number.php ├── Color │ └── Xterm256.php ├── Response.php ├── Adapter │ ├── Virtual.php │ ├── AdapterInterface.php │ ├── WindowsAnsicon.php │ ├── Windows.php │ ├── Posix.php │ └── AbstractAdapter.php ├── Request.php ├── Console.php └── Getopt.php ├── README.md ├── LICENSE.md ├── composer.json └── CHANGELOG.md /src/Charset/CharsetInterface.php: -------------------------------------------------------------------------------- 1 | usage = $usage; 30 | parent::__construct($message); 31 | } 32 | 33 | /** 34 | * Returns the usage 35 | * 36 | * @return string 37 | */ 38 | public function getUsageMessage() 39 | { 40 | return $this->usage; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-console 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-console](https://github.com/laminas/laminas-console). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-console.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-console) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-console/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-console?branch=master) 9 | 10 | `Zend\Console` is a component to design and implement console applications in PHP. 11 | 12 | > ### Deprecated 13 | > 14 | > Both the zend-console and the [zend-mvc-console](https://docs.zendframework.com/zend-mvc-console/) 15 | > components will likely not be maintained long-term, as there are more complete 16 | > implementations available elsewhere. We strongly urge developers to start 17 | > migrating their console tooling to use other libraries, such as 18 | > [symfony/console](https://github.com/symfony/console). 19 | 20 | - File issues at https://github.com/zendframework/zend-console/issues 21 | - Documentation is at https://docs.zendframework.com/zend-console/ 22 | -------------------------------------------------------------------------------- /src/Prompt/PromptInterface.php: -------------------------------------------------------------------------------- 1 | promptText = (string) $promptText; 33 | $this->echo = (bool) $echo; 34 | } 35 | 36 | /** 37 | * Show the prompt to user and return a string. 38 | * 39 | * @return string 40 | */ 41 | public function show() 42 | { 43 | $console = $this->getConsole(); 44 | 45 | $console->writeLine($this->promptText); 46 | 47 | $password = ''; 48 | 49 | /** 50 | * Read characters from console 51 | */ 52 | while (true) { 53 | $char = $console->readChar(); 54 | 55 | $console->clearLine(); 56 | 57 | if (PHP_EOL == $char) { 58 | break; 59 | } 60 | 61 | $password .= $char; 62 | 63 | if ($this->echo) { 64 | $console->write(str_repeat('*', strlen($password))); 65 | } 66 | } 67 | 68 | return $password; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Color/Xterm256.php: -------------------------------------------------------------------------------- 1 | 0 ? (int) $val : 0; 56 | }, $hex); 57 | 58 | $dhex = array_map('hexdec', $hex); 59 | 60 | if (array_fill(0, 3, $dhex[0]) === $dhex && (int) substr($dhex[0], -1) === 8) { 61 | $x11 = 232 + (int) floor($dhex[0] / 10); 62 | return new static($x11); 63 | } 64 | 65 | $x11 = $ahex[0] * 36 + $ahex[1] * 6 + $ahex[2] + 16; 66 | 67 | return new static($x11); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | contentSent; 31 | } 32 | 33 | /** 34 | * Set the error level that will be returned to shell. 35 | * 36 | * @param int $errorLevel 37 | * @return Response 38 | */ 39 | public function setErrorLevel($errorLevel) 40 | { 41 | if (is_string($errorLevel) && ! ctype_digit($errorLevel)) { 42 | return $this; 43 | } 44 | 45 | $this->setMetadata('errorLevel', $errorLevel); 46 | return $this; 47 | } 48 | 49 | /** 50 | * Get response error level that will be returned to shell. 51 | * 52 | * @return int|0 53 | */ 54 | public function getErrorLevel() 55 | { 56 | return $this->getMetadata('errorLevel', 0); 57 | } 58 | 59 | /** 60 | * Send content 61 | * 62 | * @return Response 63 | * @deprecated 64 | */ 65 | public function sendContent() 66 | { 67 | if ($this->contentSent()) { 68 | return $this; 69 | } 70 | echo $this->getContent(); 71 | $this->contentSent = true; 72 | return $this; 73 | } 74 | 75 | /** 76 | * @deprecated 77 | */ 78 | public function send() 79 | { 80 | $this->sendContent(); 81 | $errorLevel = (int) $this->getMetadata('errorLevel', 0); 82 | exit($errorLevel); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-console", 3 | "description": "Build console applications using getopt syntax or routing, complete with prompts", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "console" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-console/", 12 | "issues": "https://github.com/zendframework/zend-console/issues", 13 | "source": "https://github.com/zendframework/zend-console", 14 | "rss": "https://github.com/zendframework/zend-console/releases.atom", 15 | "slack": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0", 20 | "zendframework/zend-stdlib": "^3.2.1" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^5.7.23 || ^6.4.3", 24 | "zendframework/zend-coding-standard": "~1.0.0", 25 | "zendframework/zend-filter": "^2.7.2", 26 | "zendframework/zend-json": "^2.6 || ^3.0", 27 | "zendframework/zend-validator": "^2.10.1" 28 | }, 29 | "suggest": { 30 | "zendframework/zend-filter": "To support DefaultRouteMatcher usage", 31 | "zendframework/zend-validator": "To support DefaultRouteMatcher usage" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Zend\\Console\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "ZendTest\\Console\\": "test/" 41 | } 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "2.8.x-dev", 49 | "dev-develop": "2.9.x-dev" 50 | } 51 | }, 52 | "scripts": { 53 | "check": [ 54 | "@cs-check", 55 | "@test" 56 | ], 57 | "cs-check": "phpcs", 58 | "cs-fix": "phpcbf", 59 | "test": "phpunit --colors=always", 60 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Prompt/AbstractPrompt.php: -------------------------------------------------------------------------------- 1 | lastResponse; 37 | } 38 | 39 | /** 40 | * Return console adapter to use when showing prompt. 41 | * 42 | * @return ConsoleAdapter 43 | */ 44 | public function getConsole() 45 | { 46 | if (! $this->console) { 47 | $this->console = Console::getInstance(); 48 | } 49 | 50 | return $this->console; 51 | } 52 | 53 | /** 54 | * Set console adapter to use when showing prompt. 55 | * 56 | * @param ConsoleAdapter $adapter 57 | */ 58 | public function setConsole(ConsoleAdapter $adapter) 59 | { 60 | $this->console = $adapter; 61 | } 62 | 63 | /** 64 | * Create an instance of this prompt, show it and return response. 65 | * 66 | * This is a convenience method for creating statically creating prompts, i.e.: 67 | * 68 | * $name = Zend\Console\Prompt\Line::prompt("Enter your name: "); 69 | * 70 | * @return mixed 71 | * @throws Exception\BadMethodCallException 72 | */ 73 | public static function prompt() 74 | { 75 | if (get_called_class() === __CLASS__) { 76 | throw new Exception\BadMethodCallException( 77 | 'Cannot call prompt() on AbstractPrompt class. Use one of the Zend\Console\Prompt\ subclasses.' 78 | ); 79 | } 80 | 81 | $refl = new ReflectionClass(get_called_class()); 82 | $instance = $refl->newInstanceArgs(func_get_args()); 83 | return $instance->show(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.8.1 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 2.8.0 - 2019-02-04 28 | 29 | ### Added 30 | 31 | - [#41](https://github.com/zendframework/zend-console/pull/41) adds support for PHP 7.3. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - [#41](https://github.com/zendframework/zend-console/pull/41) removes support for zend-stdlib v2 releases. 44 | 45 | ### Fixed 46 | 47 | - [#44](https://github.com/zendframework/zend-console/pull/44) fixes usage of `array_unique()` within the `DefaultRouteMatcher` to 48 | properly re-assign the array when invoked. 49 | 50 | ## 2.7.0 - 2018-01-25 51 | 52 | ### Added 53 | 54 | - [#32](https://github.com/zendframework/zend-console/pull/32) adds a new route 55 | match type, the "catch-all". Such types are always optional (thus, appear in 56 | `[]` sets), and are specified using `...` within: `command [...options]`. 57 | 58 | Parameters matched this way will always be returned as an array of values. 59 | 60 | - [#39](https://github.com/zendframework/zend-console/pull/39) adds support for 61 | PHP 7.2. 62 | 63 | ### Changed 64 | 65 | - Nothing. 66 | 67 | ### Deprecated 68 | 69 | - Nothing. 70 | 71 | ### Removed 72 | 73 | - [#39](https://github.com/zendframework/zend-console/pull/39) removes support 74 | for PHP 5.5. 75 | 76 | - [#39](https://github.com/zendframework/zend-console/pull/39) removes support 77 | for HHVM. 78 | 79 | ### Fixed 80 | 81 | - [#19](https://github.com/zendframework/zend-console/pull/19) updated link 82 | to the documentation in the [README](README.md) 83 | 84 | ## 2.6.0 - 2016-02-9 85 | 86 | ### Added 87 | 88 | - [#16](https://github.com/zendframework/zend-console/pull/16) updates, 89 | reorganizes, and publishes the documentation to 90 | https://zendframework.github.io/zend-console 91 | 92 | ### Deprecated 93 | 94 | - Nothing. 95 | 96 | ### Removed 97 | 98 | - Nothing. 99 | 100 | ### Fixed 101 | 102 | - [#13](https://github.com/zendframework/zend-console/pull/13) updates the 103 | component to make it forwards-compatible with the zend-stdlib and 104 | zend-servicemanager v3 versions. 105 | - [#4](https://github.com/zendframework/zend-console/pull/4) fixes an error in 106 | `getTitle()` whereby the `$output` array was being incorrectly used as a 107 | string. 108 | - [#12](https://github.com/zendframework/zend-console/pull/12) updates the 109 | `Zend\Console\Prompt\Char::show()` method to call on the composed adapter's 110 | `write()`/`writeLine()` methods instead of calling `echo()`. 111 | -------------------------------------------------------------------------------- /src/Prompt/Confirm.php: -------------------------------------------------------------------------------- 1 | setPromptText($promptText); 53 | } 54 | 55 | if ($yesChar !== null) { 56 | $this->setYesChar($yesChar); 57 | } 58 | 59 | if ($noChar !== null) { 60 | $this->setNoChar($noChar); 61 | } 62 | } 63 | 64 | /** 65 | * Show the confirmation message and return result. 66 | * 67 | * @return bool 68 | */ 69 | public function show() 70 | { 71 | $char = parent::show(); 72 | if ($this->ignoreCase) { 73 | $response = strtolower($char) === strtolower($this->yesChar); 74 | } else { 75 | $response = $char === $this->yesChar; 76 | } 77 | return $this->lastResponse = $response; 78 | } 79 | 80 | /** 81 | * @param string $noChar 82 | */ 83 | public function setNoChar($noChar) 84 | { 85 | $this->noChar = $noChar; 86 | $this->setAllowedChars($this->yesChar . $this->noChar); 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getNoChar() 93 | { 94 | return $this->noChar; 95 | } 96 | 97 | /** 98 | * @param string $yesChar 99 | */ 100 | public function setYesChar($yesChar) 101 | { 102 | $this->yesChar = $yesChar; 103 | $this->setAllowedChars($this->yesChar . $this->noChar); 104 | } 105 | 106 | /** 107 | * @return string 108 | */ 109 | public function getYesChar() 110 | { 111 | return $this->yesChar; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Prompt/Line.php: -------------------------------------------------------------------------------- 1 | setPromptText($promptText); 40 | } 41 | 42 | if ($allowEmpty !== null) { 43 | $this->setAllowEmpty($allowEmpty); 44 | } 45 | 46 | if ($maxLength !== null) { 47 | $this->setMaxLength($maxLength); 48 | } 49 | } 50 | 51 | /** 52 | * Show the prompt to user and return the answer. 53 | * 54 | * @return string 55 | */ 56 | public function show() 57 | { 58 | do { 59 | $this->getConsole()->write($this->promptText); 60 | $line = $this->getConsole()->readLine($this->maxLength); 61 | } while (! $this->allowEmpty && ! $line); 62 | 63 | return $this->lastResponse = $line; 64 | } 65 | 66 | /** 67 | * @param bool $allowEmpty 68 | */ 69 | public function setAllowEmpty($allowEmpty) 70 | { 71 | $this->allowEmpty = $allowEmpty; 72 | } 73 | 74 | /** 75 | * @return bool 76 | */ 77 | public function getAllowEmpty() 78 | { 79 | return $this->allowEmpty; 80 | } 81 | 82 | /** 83 | * @param int $maxLength 84 | */ 85 | public function setMaxLength($maxLength) 86 | { 87 | $this->maxLength = $maxLength; 88 | } 89 | 90 | /** 91 | * @return int 92 | */ 93 | public function getMaxLength() 94 | { 95 | return $this->maxLength; 96 | } 97 | 98 | /** 99 | * @param string $promptText 100 | */ 101 | public function setPromptText($promptText) 102 | { 103 | $this->promptText = $promptText; 104 | } 105 | 106 | /** 107 | * @return string 108 | */ 109 | public function getPromptText() 110 | { 111 | return $this->promptText; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Prompt/Select.php: -------------------------------------------------------------------------------- 1 | setPromptText($promptText); 48 | } 49 | 50 | if (! count($options)) { 51 | throw new Exception\BadMethodCallException( 52 | 'Cannot construct a "select" prompt without any options' 53 | ); 54 | } 55 | 56 | $this->setOptions($options); 57 | 58 | if ($allowEmpty !== null) { 59 | $this->setAllowEmpty($allowEmpty); 60 | } 61 | 62 | if ($echo !== null) { 63 | $this->setEcho($echo); 64 | } 65 | } 66 | 67 | /** 68 | * Show a list of options and prompt the user to select one of them. 69 | * 70 | * @return string Selected option 71 | */ 72 | public function show() 73 | { 74 | // Show prompt text and available options 75 | $console = $this->getConsole(); 76 | $console->writeLine($this->promptText); 77 | foreach ($this->options as $k => $v) { 78 | $console->writeLine(' ' . $k . ') ' . $v); 79 | } 80 | 81 | // Prepare mask 82 | $mask = implode("", array_keys($this->options)); 83 | if ($this->allowEmpty) { 84 | $mask .= "\r\n"; 85 | } 86 | 87 | // Prepare other params for parent class 88 | $this->setAllowedChars($mask); 89 | $oldPrompt = $this->promptText; 90 | $oldEcho = $this->echo; 91 | $this->echo = false; 92 | $this->promptText = null; 93 | 94 | // Retrieve a single character 95 | $response = parent::show(); 96 | 97 | // Restore old params 98 | $this->promptText = $oldPrompt; 99 | $this->echo = $oldEcho; 100 | 101 | // Display selected option if echo is enabled 102 | if ($this->echo) { 103 | if (isset($this->options[$response])) { 104 | $console->writeLine($this->options[$response]); 105 | } else { 106 | $console->writeLine(); 107 | } 108 | } 109 | 110 | $this->lastResponse = $response; 111 | return $response; 112 | } 113 | 114 | /** 115 | * Set allowed options 116 | * 117 | * @param array|\Traversable $options 118 | * @throws Exception\BadMethodCallException 119 | */ 120 | public function setOptions($options) 121 | { 122 | if (! is_array($options) && ! $options instanceof \Traversable) { 123 | throw new Exception\BadMethodCallException( 124 | 'Please specify an array or Traversable object as options' 125 | ); 126 | } 127 | 128 | if (! is_array($options)) { 129 | $this->options = []; 130 | foreach ($options as $k => $v) { 131 | $this->options[$k] = $v; 132 | } 133 | } else { 134 | $this->options = $options; 135 | } 136 | } 137 | 138 | /** 139 | * @return array 140 | */ 141 | public function getOptions() 142 | { 143 | return $this->options; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Adapter/Virtual.php: -------------------------------------------------------------------------------- 1 | 0) { 42 | return $width; 43 | } 44 | 45 | // Try to read console size from "mode" command 46 | if ($this->modeResult === null) { 47 | $this->runProbeCommand(); 48 | } 49 | 50 | if (preg_match('/Columns\:\s+(\d+)/', $this->modeResult, $matches)) { 51 | $width = $matches[1]; 52 | } else { 53 | $width = parent::getWidth(); 54 | } 55 | 56 | return $width; 57 | } 58 | 59 | /** 60 | * Determine and return current console height. 61 | * 62 | * @return false|int 63 | */ 64 | public function getHeight() 65 | { 66 | static $height; 67 | if ($height > 0) { 68 | return $height; 69 | } 70 | 71 | // Try to read console size from "mode" command 72 | if ($this->modeResult === null) { 73 | $this->runProbeCommand(); 74 | } 75 | 76 | if (preg_match('/Rows\:\s+(\d+)/', $this->modeResult, $matches)) { 77 | $height = $matches[1]; 78 | } else { 79 | $height = parent::getHeight(); 80 | } 81 | 82 | return $height; 83 | } 84 | 85 | /** 86 | * Run and store the results of mode command 87 | * 88 | * @return void 89 | */ 90 | protected function runProbeCommand() 91 | { 92 | exec('mode', $output, $return); 93 | if ($return || ! count($output)) { 94 | $this->modeResult = ''; 95 | } else { 96 | $this->modeResult = trim(implode('', $output)); 97 | } 98 | } 99 | 100 | /** 101 | * Check if console is UTF-8 compatible 102 | * 103 | * @return bool 104 | */ 105 | public function isUtf8() 106 | { 107 | // Try to read code page info from "mode" command 108 | if ($this->modeResult === null) { 109 | $this->runProbeCommand(); 110 | } 111 | 112 | if (preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) { 113 | return (int) $matches[1] == 65001; 114 | } 115 | 116 | return false; 117 | } 118 | 119 | /** 120 | * Return current console window title. 121 | * 122 | * @return string 123 | */ 124 | public function getTitle() 125 | { 126 | // Try to use powershell to retrieve console window title 127 | exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result); 128 | if ($result || ! $output) { 129 | return ''; 130 | } 131 | 132 | return trim($output, "\r\n"); 133 | } 134 | 135 | /** 136 | * Set Console charset to use. 137 | * 138 | * @param Charset\CharsetInterface $charset 139 | */ 140 | public function setCharset(Charset\CharsetInterface $charset) 141 | { 142 | $this->charset = $charset; 143 | } 144 | 145 | /** 146 | * Get charset currently in use by this adapter. 147 | * 148 | * @return Charset\CharsetInterface $charset 149 | */ 150 | public function getCharset() 151 | { 152 | if ($this->charset === null) { 153 | $this->charset = $this->getDefaultCharset(); 154 | } 155 | 156 | return $this->charset; 157 | } 158 | 159 | /** 160 | * @return Charset\AsciiExtended 161 | */ 162 | public function getDefaultCharset() 163 | { 164 | return new Charset\AsciiExtended; 165 | } 166 | 167 | /** 168 | * Switch to UTF mode 169 | * 170 | * @return void 171 | */ 172 | protected function switchToUtf8() 173 | { 174 | shell_exec('mode con cp select=65001'); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Prompt/Char.php: -------------------------------------------------------------------------------- 1 | setPromptText($promptText); 56 | $this->setAllowEmpty($allowEmpty); 57 | $this->setIgnoreCase($ignoreCase); 58 | 59 | if (null != $allowedChars) { 60 | if ($this->ignoreCase) { 61 | $this->setAllowedChars(strtolower($allowedChars)); 62 | } else { 63 | $this->setAllowedChars($allowedChars); 64 | } 65 | } 66 | 67 | $this->setEcho($echo); 68 | } 69 | 70 | /** 71 | * Show the prompt to user and return a single char. 72 | * 73 | * @return string 74 | */ 75 | public function show() 76 | { 77 | $this->getConsole()->write($this->promptText); 78 | $mask = $this->getAllowedChars(); 79 | 80 | /* 81 | * Normalize the mask if case is irrelevant 82 | */ 83 | if ($this->ignoreCase) { 84 | $mask = strtolower($mask); // lowercase all 85 | $mask .= strtoupper($mask); // uppercase and append 86 | $mask = str_split($mask); // convert to array 87 | $mask = array_unique($mask); // remove duplicates 88 | $mask = implode('', $mask); // convert back to string 89 | } 90 | 91 | /* 92 | * Read char from console 93 | */ 94 | $char = $this->getConsole()->readChar($mask); 95 | 96 | if ($this->echo) { 97 | $this->getConsole()->writeLine(trim($char)); 98 | } else { 99 | if ($this->promptText) { 100 | $this->getConsole()->writeLine(); // skip to next line but only if we had any prompt text 101 | } 102 | } 103 | 104 | return $this->lastResponse = $char; 105 | } 106 | 107 | /** 108 | * @param bool $allowEmpty 109 | */ 110 | public function setAllowEmpty($allowEmpty) 111 | { 112 | $this->allowEmpty = (bool) $allowEmpty; 113 | } 114 | 115 | /** 116 | * @return bool 117 | */ 118 | public function getAllowEmpty() 119 | { 120 | return $this->allowEmpty; 121 | } 122 | 123 | /** 124 | * @param string $promptText 125 | */ 126 | public function setPromptText($promptText) 127 | { 128 | $this->promptText = $promptText; 129 | } 130 | 131 | /** 132 | * @return string 133 | */ 134 | public function getPromptText() 135 | { 136 | return $this->promptText; 137 | } 138 | 139 | /** 140 | * @param string $allowedChars 141 | */ 142 | public function setAllowedChars($allowedChars) 143 | { 144 | $this->allowedChars = $allowedChars; 145 | } 146 | 147 | /** 148 | * @return string 149 | */ 150 | public function getAllowedChars() 151 | { 152 | return $this->allowedChars; 153 | } 154 | 155 | /** 156 | * @param bool $ignoreCase 157 | */ 158 | public function setIgnoreCase($ignoreCase) 159 | { 160 | $this->ignoreCase = (bool) $ignoreCase; 161 | } 162 | 163 | /** 164 | * @return bool 165 | */ 166 | public function getIgnoreCase() 167 | { 168 | return $this->ignoreCase; 169 | } 170 | 171 | /** 172 | * @param bool $echo 173 | */ 174 | public function setEcho($echo) 175 | { 176 | $this->echo = (bool) $echo; 177 | } 178 | 179 | /** 180 | * @return bool 181 | */ 182 | public function getEcho() 183 | { 184 | return $this->echo; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Prompt/Checkbox.php: -------------------------------------------------------------------------------- 1 | promptText = (string) $promptText; 57 | 58 | $this->setOptions($options); 59 | 60 | $this->echo = (bool) $echo; 61 | 62 | $this->ignoreCase = (bool) $ignoreCase; 63 | } 64 | 65 | /** 66 | * Show a list of options and prompt the user to select any number of them. 67 | * 68 | * @return array Checked options 69 | */ 70 | public function show() 71 | { 72 | $this->checkedOptions = []; 73 | $mask = $this->prepareMask(); 74 | 75 | do { 76 | $this->showAvailableOptions(); 77 | 78 | $response = $this->readOption($mask); 79 | 80 | if ($this->echo) { 81 | $this->showResponse($response); 82 | } 83 | 84 | $this->checkOrUncheckOption($response); 85 | } while ($response != "\r" && $response != "\n"); 86 | 87 | $this->lastResponse = $this->checkedOptions; 88 | 89 | return $this->checkedOptions; 90 | } 91 | 92 | /** 93 | * Shows the selected option to the screen 94 | * @param string $response 95 | */ 96 | private function showResponse($response) 97 | { 98 | $console = $this->getConsole(); 99 | if (isset($this->options[$response])) { 100 | $console->writeLine($this->options[$response]); 101 | } else { 102 | $console->writeLine(); 103 | } 104 | } 105 | 106 | /** 107 | * Check or uncheck an option 108 | * 109 | * @param string $response 110 | */ 111 | private function checkOrUncheckOption($response) 112 | { 113 | if ($response != "\r" && $response != "\n" && isset($this->options[$response])) { 114 | $pos = array_search($this->options[$response], $this->checkedOptions); 115 | if ($pos === false) { 116 | $this->checkedOptions[] = $this->options[$response]; 117 | } else { 118 | array_splice($this->checkedOptions, $pos, 1); 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Generates a mask to to be used by the readChar method. 125 | * 126 | * @return string 127 | */ 128 | private function prepareMask() 129 | { 130 | $mask = implode("", array_keys($this->options)) . "\r\n"; 131 | 132 | /** 133 | * Normalize the mask if case is irrelevant 134 | */ 135 | if (! $this->ignoreCase) { 136 | return $mask; 137 | } 138 | 139 | $mask = implode("", array_unique(str_split(strtolower($mask) . strtoupper($mask)))); 140 | 141 | return $mask; 142 | } 143 | 144 | /** 145 | * Reads a char from console. 146 | * 147 | * @param string $mask 148 | * @return string 149 | */ 150 | private function readOption($mask) 151 | { 152 | /** 153 | * Read char from console 154 | */ 155 | 156 | return $this->getConsole()->readChar($mask); 157 | } 158 | 159 | /** 160 | * Shows the available options with checked and unchecked states 161 | */ 162 | private function showAvailableOptions() 163 | { 164 | $console = $this->getConsole(); 165 | $console->writeLine($this->promptText); 166 | foreach ($this->options as $k => $v) { 167 | $console->writeLine(' ' . $k . ') ' . (in_array($v, $this->checkedOptions) ? '[X] ' : '[ ] ') . $v); 168 | } 169 | } 170 | 171 | /** 172 | * Set allowed options 173 | * 174 | * @param array|\Traversable $options 175 | * @throws Exception\InvalidArgumentException 176 | */ 177 | private function setOptions($options) 178 | { 179 | $options = ArrayUtils::iteratorToArray($options); 180 | 181 | if (empty($options)) { 182 | throw new Exception\InvalidArgumentException('Please, specify at least one option'); 183 | } 184 | 185 | $this->options = $options; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Prompt/Number.php: -------------------------------------------------------------------------------- 1 | setPromptText($promptText); 52 | } 53 | 54 | if ($allowEmpty !== null) { 55 | $this->setAllowEmpty($allowEmpty); 56 | } 57 | 58 | if ($min !== null) { 59 | $this->setMin($min); 60 | } 61 | 62 | if ($max !== null) { 63 | $this->setMax($max); 64 | } 65 | 66 | if ($allowFloat !== null) { 67 | $this->setAllowFloat($allowFloat); 68 | } 69 | } 70 | 71 | /** 72 | * Show the prompt to user and return the answer. 73 | * 74 | * @return mixed 75 | */ 76 | public function show() 77 | { 78 | /** 79 | * Ask for a number and validate it. 80 | */ 81 | do { 82 | $valid = true; 83 | $number = parent::show(); 84 | if ($number === "" && ! $this->allowEmpty) { 85 | $valid = false; 86 | } elseif ($number === "") { 87 | $number = null; 88 | } elseif (! is_numeric($number)) { 89 | $this->getConsole()->writeLine("$number is not a number\n"); 90 | $valid = false; 91 | } elseif (! $this->allowFloat && (round($number) != $number)) { 92 | $this->getConsole()->writeLine("Please enter a non-floating number, i.e. " . round($number) . "\n"); 93 | $valid = false; 94 | } elseif ($this->max !== null && $number > $this->max) { 95 | $this->getConsole()->writeLine("Please enter a number not greater than " . $this->max . "\n"); 96 | $valid = false; 97 | } elseif ($this->min !== null && $number < $this->min) { 98 | $this->getConsole()->writeLine("Please enter a number not smaller than " . $this->min . "\n"); 99 | $valid = false; 100 | } 101 | } while (! $valid); 102 | 103 | /** 104 | * Cast proper type 105 | */ 106 | if ($number !== null) { 107 | $number = $this->allowFloat ? (double) $number : (int) $number; 108 | } 109 | 110 | return $this->lastResponse = $number; 111 | } 112 | 113 | /** 114 | * @param bool $allowEmpty 115 | */ 116 | public function setAllowEmpty($allowEmpty) 117 | { 118 | $this->allowEmpty = $allowEmpty; 119 | } 120 | 121 | /** 122 | * @return bool 123 | */ 124 | public function getAllowEmpty() 125 | { 126 | return $this->allowEmpty; 127 | } 128 | 129 | /** 130 | * @param int $maxLength 131 | */ 132 | public function setMaxLength($maxLength) 133 | { 134 | $this->maxLength = $maxLength; 135 | } 136 | 137 | /** 138 | * @return int 139 | */ 140 | public function getMaxLength() 141 | { 142 | return $this->maxLength; 143 | } 144 | 145 | /** 146 | * @param string $promptText 147 | */ 148 | public function setPromptText($promptText) 149 | { 150 | $this->promptText = $promptText; 151 | } 152 | 153 | /** 154 | * @return string 155 | */ 156 | public function getPromptText() 157 | { 158 | return $this->promptText; 159 | } 160 | 161 | /** 162 | * @param int $max 163 | */ 164 | public function setMax($max) 165 | { 166 | $this->max = $max; 167 | } 168 | 169 | /** 170 | * @return int 171 | */ 172 | public function getMax() 173 | { 174 | return $this->max; 175 | } 176 | 177 | /** 178 | * @param int $min 179 | */ 180 | public function setMin($min) 181 | { 182 | $this->min = $min; 183 | } 184 | 185 | /** 186 | * @return int 187 | */ 188 | public function getMin() 189 | { 190 | return $this->min; 191 | } 192 | 193 | /** 194 | * @param bool $allowFloat 195 | */ 196 | public function setAllowFloat($allowFloat) 197 | { 198 | $this->allowFloat = $allowFloat; 199 | } 200 | 201 | /** 202 | * @return bool 203 | */ 204 | public function getAllowFloat() 205 | { 206 | return $this->allowFloat; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 0) { 60 | $this->setScriptName(array_shift($args)); 61 | } 62 | 63 | /** 64 | * Store runtime params 65 | */ 66 | $this->params()->fromArray($args); 67 | $this->setContent($args); 68 | 69 | /** 70 | * Store environment data 71 | */ 72 | $this->env()->fromArray($env); 73 | } 74 | 75 | /** 76 | * Exchange parameters object 77 | * 78 | * @param \Zend\Stdlib\Parameters $params 79 | * @return Request 80 | */ 81 | public function setParams(Parameters $params) 82 | { 83 | $this->params = $params; 84 | $this->setContent($params); 85 | return $this; 86 | } 87 | 88 | /** 89 | * Return the container responsible for parameters 90 | * 91 | * @return \Zend\Stdlib\Parameters 92 | */ 93 | public function getParams() 94 | { 95 | if ($this->params === null) { 96 | $this->params = new Parameters(); 97 | } 98 | 99 | return $this->params; 100 | } 101 | 102 | /** 103 | * Return a single parameter. 104 | * Shortcut for $request->params()->get() 105 | * 106 | * @param string $name Parameter name 107 | * @param string $default (optional) default value in case the parameter does not exist 108 | * @return mixed 109 | */ 110 | public function getParam($name, $default = null) 111 | { 112 | return $this->params()->get($name, $default); 113 | } 114 | 115 | /** 116 | * Return the container responsible for parameters 117 | * 118 | * @return \Zend\Stdlib\Parameters 119 | */ 120 | public function params() 121 | { 122 | return $this->getParams(); 123 | } 124 | 125 | /** 126 | * Provide an alternate Parameter Container implementation for env parameters in this object, (this is NOT the 127 | * primary API for value setting, for that see env()) 128 | * 129 | * @param \Zend\Stdlib\Parameters $env 130 | * @return \Zend\Console\Request 131 | */ 132 | public function setEnv(Parameters $env) 133 | { 134 | $this->envParams = $env; 135 | return $this; 136 | } 137 | 138 | /** 139 | * Return a single parameter container responsible for env parameters 140 | * 141 | * @param string $name Parameter name 142 | * @param string $default (optional) default value in case the parameter does not exist 143 | * @return \Zend\Stdlib\Parameters 144 | */ 145 | public function getEnv($name, $default = null) 146 | { 147 | return $this->env()->get($name, $default); 148 | } 149 | 150 | /** 151 | * Return the parameter container responsible for env parameters 152 | * 153 | * @return \Zend\Stdlib\Parameters 154 | */ 155 | public function env() 156 | { 157 | if ($this->envParams === null) { 158 | $this->envParams = new Parameters(); 159 | } 160 | 161 | return $this->envParams; 162 | } 163 | 164 | /** 165 | * @return string 166 | */ 167 | public function toString() 168 | { 169 | return trim(implode(' ', $this->params()->toArray())); 170 | } 171 | 172 | /** 173 | * Allow PHP casting of this object 174 | * 175 | * @return string 176 | */ 177 | public function __toString() 178 | { 179 | return $this->toString(); 180 | } 181 | 182 | /** 183 | * @param string $scriptName 184 | */ 185 | public function setScriptName($scriptName) 186 | { 187 | $this->scriptName = $scriptName; 188 | } 189 | 190 | /** 191 | * @return string 192 | */ 193 | public function getScriptName() 194 | { 195 | return $this->scriptName; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | setCharset(new $className()); 99 | } 100 | 101 | return static::$instance; 102 | } 103 | 104 | /** 105 | * Reset the console instance 106 | */ 107 | public static function resetInstance() 108 | { 109 | static::$instance = null; 110 | } 111 | 112 | /** 113 | * Check if currently running under MS Windows 114 | * 115 | * @see http://stackoverflow.com/questions/738823/possible-values-for-php-os 116 | * @return bool 117 | */ 118 | public static function isWindows() 119 | { 120 | return 121 | (defined('PHP_OS') && (substr_compare(PHP_OS, 'win', 0, 3, true) === 0)) || 122 | (getenv('OS') != false && substr_compare(getenv('OS'), 'windows', 0, 7, true)) 123 | ; 124 | } 125 | 126 | /** 127 | * Check if running under MS Windows Ansicon 128 | * 129 | * @return bool 130 | */ 131 | public static function isAnsicon() 132 | { 133 | return getenv('ANSICON') !== false; 134 | } 135 | 136 | /** 137 | * Check if running in a console environment (CLI) 138 | * 139 | * By default, returns value of PHP_SAPI global constant. If $isConsole is 140 | * set, and a boolean value, that value will be returned. 141 | * 142 | * @return bool 143 | */ 144 | public static function isConsole() 145 | { 146 | if (null === static::$isConsole) { 147 | static::$isConsole = (PHP_SAPI == 'cli'); 148 | } 149 | return static::$isConsole; 150 | } 151 | 152 | /** 153 | * Override the "is console environment" flag 154 | * 155 | * @param null|bool $flag 156 | */ 157 | public static function overrideIsConsole($flag) 158 | { 159 | if (null != $flag) { 160 | $flag = (bool) $flag; 161 | } 162 | static::$isConsole = $flag; 163 | } 164 | 165 | /** 166 | * Try to detect best matching adapter 167 | * @return string|null 168 | */ 169 | public static function detectBestAdapter() 170 | { 171 | // Check if we are in a console environment 172 | if (! static::isConsole()) { 173 | return; 174 | } 175 | 176 | // Check if we're on windows 177 | if (static::isWindows()) { 178 | if (static::isAnsicon()) { 179 | $className = __NAMESPACE__ . '\Adapter\WindowsAnsicon'; 180 | } else { 181 | $className = __NAMESPACE__ . '\Adapter\Windows'; 182 | } 183 | 184 | return $className; 185 | } 186 | 187 | // Default is a Posix console 188 | $className = __NAMESPACE__ . '\Adapter\Posix'; 189 | return $className; 190 | } 191 | 192 | /** 193 | * Pass-thru static call to current AdapterInterface instance. 194 | * 195 | * @param $funcName 196 | * @param $arguments 197 | * @return mixed 198 | */ 199 | public static function __callStatic($funcName, $arguments) 200 | { 201 | $instance = static::getInstance(); 202 | return call_user_func_array([$instance, $funcName], $arguments); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Adapter/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | 0) { 55 | return $width; 56 | } 57 | 58 | // Try to read console size from ANSICON env var 59 | if (preg_match('/\((\d+)x/', getenv('ANSICON'), $matches)) { 60 | $width = $matches[1]; 61 | } else { 62 | $width = AbstractAdapter::getWidth(); 63 | } 64 | 65 | return $width; 66 | } 67 | 68 | /** 69 | * Determine and return current console height. 70 | * 71 | * @return false|int 72 | */ 73 | public function getHeight() 74 | { 75 | static $height; 76 | if ($height > 0) { 77 | return $height; 78 | } 79 | 80 | // Try to read console size from ANSICON env var 81 | if (preg_match('/\(\d+x(\d+)/', getenv('ANSICON'), $matches)) { 82 | $height = $matches[1]; 83 | } else { 84 | $height = AbstractAdapter::getHeight(); 85 | } 86 | return $height; 87 | } 88 | 89 | /** 90 | * Run and cache results of mode command 91 | * 92 | * @return void 93 | */ 94 | protected function runModeCommand() 95 | { 96 | exec('mode', $output, $return); 97 | if ($return || ! count($output)) { 98 | $this->modeResult = ''; 99 | } else { 100 | $this->modeResult = trim(implode('', $output)); 101 | } 102 | } 103 | 104 | /** 105 | * Check if console is UTF-8 compatible 106 | * 107 | * @return bool 108 | */ 109 | public function isUtf8() 110 | { 111 | // Try to read code page info from "mode" command 112 | if ($this->modeResult === null) { 113 | $this->runModeCommand(); 114 | } 115 | 116 | if (preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) { 117 | return (int) $matches[1] == 65001; 118 | } 119 | 120 | return false; 121 | } 122 | 123 | /** 124 | * Return current console window title. 125 | * 126 | * @return string 127 | */ 128 | public function getTitle() 129 | { 130 | // Try to use powershell to retrieve console window title 131 | exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result); 132 | if ($result || ! $output) { 133 | return ''; 134 | } 135 | 136 | return trim($output, "\r\n"); 137 | } 138 | 139 | /** 140 | * Clear console screen 141 | */ 142 | public function clear() 143 | { 144 | echo chr(27) . '[1J' . chr(27) . '[u'; 145 | } 146 | 147 | /** 148 | * Clear line at cursor position 149 | */ 150 | public function clearLine() 151 | { 152 | echo chr(27) . '[1K'; 153 | } 154 | 155 | /** 156 | * Set Console charset to use. 157 | * 158 | * @param CharsetInterface $charset 159 | */ 160 | public function setCharset(CharsetInterface $charset) 161 | { 162 | $this->charset = $charset; 163 | } 164 | 165 | /** 166 | * Get charset currently in use by this adapter. 167 | * 168 | * @return CharsetInterface $charset 169 | */ 170 | public function getCharset() 171 | { 172 | if ($this->charset === null) { 173 | $this->charset = $this->getDefaultCharset(); 174 | } 175 | 176 | return $this->charset; 177 | } 178 | 179 | /** 180 | * @return Charset\AsciiExtended 181 | */ 182 | public function getDefaultCharset() 183 | { 184 | return new Charset\AsciiExtended(); 185 | } 186 | 187 | /** 188 | * Read a single character from the console input 189 | * 190 | * @param string|null $mask A list of allowed chars 191 | * @return string 192 | * @throws Exception\RuntimeException 193 | */ 194 | public function readChar($mask = null) 195 | { 196 | // Decide if we can use `choice` tool 197 | $useChoice = $mask !== null && preg_match('/^[a-zA-Z0-9]+$/D', $mask); 198 | 199 | if ($useChoice) { 200 | // Use Windows 98+ "choice" command, which allows for reading a 201 | // single character matching a mask, but is limited to lower ASCII 202 | // range. 203 | do { 204 | exec('choice /n /cs /c:' . $mask, $output, $return); 205 | if ($return == 255 || $return < 1 || $return > strlen($mask)) { 206 | throw new Exception\RuntimeException( 207 | '"choice" command failed to run. Are you using Windows XP or newer?' 208 | ); 209 | } 210 | 211 | // Fetch the char from mask 212 | $char = substr($mask, $return - 1, 1); 213 | } while ("" === $char || ($mask !== null && false === strstr($mask, $char))); 214 | 215 | return $char; 216 | } 217 | 218 | // Try to use PowerShell, giving it console access. Because PowersShell 219 | // interpreter can take a short while to load, we are emptying the 220 | // whole keyboard buffer and picking the last key that has been pressed 221 | // before or after PowerShell command has started. The ASCII code for 222 | // that key is then converted to a character. 223 | if ($mask === null) { 224 | exec( 225 | 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' 226 | . 'while ($Host.UI.RawUI.KeyAvailable) {$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\');}' 227 | . 'write $key.VirtualKeyCode;' 228 | . '"', 229 | $result, 230 | $return 231 | ); 232 | 233 | // Retrieve char from the result. 234 | $char = ! empty($result) ? implode('', $result) : null; 235 | 236 | if (! empty($char) && ! $return) { 237 | // We have obtained an ASCII code, convert back to a char ... 238 | $char = chr($char); 239 | 240 | // ... and return it... 241 | return $char; 242 | } 243 | } else { 244 | // Windows and DOS will return carriage-return char (ASCII 13) when 245 | // the user presses [ENTER] key, but Console Adapter user might 246 | // have provided a \n Newline (ASCII 10) in the mask, to allow 247 | // [ENTER]. We are going to replace all CR with NL to conform. 248 | $mask = strtr($mask, "\n", "\r"); 249 | 250 | // Prepare a list of ASCII codes from mask chars 251 | $asciiMask = array_map(function ($char) { 252 | return ord($char); 253 | }, str_split($mask)); 254 | $asciiMask = array_unique($asciiMask); 255 | 256 | // Char mask filtering is now handled by the PowerShell itself, 257 | // because it's a much faster method than invoking PS interpreter 258 | // after each mismatch. The command should return ASCII code of a 259 | // matching key. 260 | $result = $return = null; 261 | exec( 262 | 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' 263 | . '[int[]] $mask = '.implode(',', $asciiMask).';' 264 | . 'do {' 265 | . '$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\').VirtualKeyCode;' 266 | . '} while ( !($mask -contains $key) );' 267 | . 'write $key;' 268 | . '"', 269 | $result, 270 | $return 271 | ); 272 | 273 | $char = ! empty($result) ? trim(implode('', $result)) : null; 274 | 275 | if (! $return && $char && ($mask === null || in_array($char, $asciiMask))) { 276 | // We have obtained an ASCII code, check if it is a carriage 277 | // return and normalize it as needed 278 | if ($char == 13) { 279 | $char = 10; 280 | } 281 | 282 | // Convert to a character 283 | $char = chr($char); 284 | 285 | // ... and return it... 286 | return $char; 287 | } 288 | } 289 | 290 | // Fall back to standard input, which on Windows does not allow reading 291 | // a single character. This is a limitation of Windows streams 292 | // implementation (not PHP) and this behavior cannot be changed with a 293 | // command like "stty", known to POSIX systems. 294 | $stream = fopen('php://stdin', 'rb'); 295 | do { 296 | $char = fgetc($stream); 297 | $char = substr(trim($char), 0, 1); 298 | } while (! $char || ($mask !== null && ! stristr($mask, $char))); 299 | fclose($stream); 300 | 301 | return $char; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/Adapter/Windows.php: -------------------------------------------------------------------------------- 1 | 0) { 47 | return $width; 48 | } 49 | 50 | // Try to read console size from "mode" command 51 | if ($this->probeResult === null) { 52 | $this->runProbeCommand(); 53 | } 54 | 55 | if (count($this->probeResult) && (int) $this->probeResult[0]) { 56 | $width = (int) $this->probeResult[0]; 57 | } else { 58 | $width = parent::getWidth(); 59 | } 60 | 61 | return $width; 62 | } 63 | 64 | /** 65 | * Determine and return current console height. 66 | * 67 | * @return int 68 | */ 69 | public function getHeight() 70 | { 71 | static $height; 72 | if ($height > 0) { 73 | return $height; 74 | } 75 | 76 | // Try to read console size from "mode" command 77 | if ($this->probeResult === null) { 78 | $this->runProbeCommand(); 79 | } 80 | 81 | if (count($this->probeResult) && (int) $this->probeResult[1]) { 82 | $height = (int) $this->probeResult[1]; 83 | } else { 84 | $height = parent::getHeight(); 85 | } 86 | 87 | return $height; 88 | } 89 | 90 | /** 91 | * Probe for system capabilities and cache results 92 | * 93 | * Run a Windows Powershell command that determines parameters of console window. The command is fed through 94 | * standard input (with echo) to prevent Powershell from creating a sub-thread and hanging PHP when run through 95 | * a debugger/IDE. 96 | * 97 | * @return void 98 | */ 99 | protected function runProbeCommand() 100 | { 101 | exec( 102 | 'echo $size = $Host.ui.rawui.windowsize; write $($size.width) ' 103 | . '$($size.height) | powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command -', 104 | $output, 105 | $return 106 | ); 107 | if ($return || empty($output)) { 108 | $this->probeResult = ''; 109 | } else { 110 | $this->probeResult = $output; 111 | } 112 | } 113 | 114 | /** 115 | * Run and cache results of mode command 116 | * 117 | * @return void 118 | */ 119 | protected function runModeCommand() 120 | { 121 | exec('mode', $output, $return); 122 | if ($return || ! count($output)) { 123 | $this->modeResult = ''; 124 | } else { 125 | $this->modeResult = trim(implode('', $output)); 126 | } 127 | } 128 | 129 | /** 130 | * Check if console is UTF-8 compatible 131 | * 132 | * @return bool 133 | */ 134 | public function isUtf8() 135 | { 136 | // Try to read code page info from "mode" command 137 | if ($this->modeResult === null) { 138 | $this->runModeCommand(); 139 | } 140 | 141 | if (preg_match('/Code page\:\s+(\d+)/', $this->modeResult, $matches)) { 142 | return (int) $matches[1] == 65001; 143 | } 144 | 145 | return false; 146 | } 147 | 148 | /** 149 | * Return current console window title. 150 | * 151 | * @return string 152 | */ 153 | public function getTitle() 154 | { 155 | // Try to use powershell to retrieve console window title 156 | exec('powershell -command "write $Host.UI.RawUI.WindowTitle"', $output, $result); 157 | if ($result || ! $output) { 158 | return ''; 159 | } 160 | 161 | return trim(implode('', $output), "\r\n"); 162 | } 163 | 164 | /** 165 | * Set Console charset to use. 166 | * 167 | * @param Charset\CharsetInterface $charset 168 | */ 169 | public function setCharset(Charset\CharsetInterface $charset) 170 | { 171 | $this->charset = $charset; 172 | } 173 | 174 | /** 175 | * Get charset currently in use by this adapter. 176 | * 177 | * @return Charset\CharsetInterface $charset 178 | */ 179 | public function getCharset() 180 | { 181 | if ($this->charset === null) { 182 | $this->charset = $this->getDefaultCharset(); 183 | } 184 | 185 | return $this->charset; 186 | } 187 | 188 | /** 189 | * @return Charset\AsciiExtended 190 | */ 191 | public function getDefaultCharset() 192 | { 193 | return new Charset\AsciiExtended; 194 | } 195 | 196 | /** 197 | * Switch to utf-8 encoding 198 | * 199 | * @return void 200 | */ 201 | protected function switchToUtf8() 202 | { 203 | shell_exec('mode con cp select=65001'); 204 | } 205 | 206 | /** 207 | * Clear console screen 208 | */ 209 | public function clear() 210 | { 211 | // Attempt to clear the screen using PowerShell command 212 | exec("powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command Clear-Host", $output, $return); 213 | 214 | if ($return) { 215 | // Could not run powershell... fall back to filling the buffer with newlines 216 | echo str_repeat("\r\n", $this->getHeight()); 217 | } 218 | } 219 | 220 | /** 221 | * Clear line at cursor position 222 | */ 223 | public function clearLine() 224 | { 225 | echo "\r" . str_repeat(' ', $this->getWidth()) . "\r"; 226 | } 227 | 228 | /** 229 | * Read a single character from the console input 230 | * 231 | * @param string|null $mask A list of allowed chars 232 | * @throws Exception\RuntimeException 233 | * @return string 234 | */ 235 | public function readChar($mask = null) 236 | { 237 | // Decide if we can use `choice` tool 238 | $useChoice = $mask !== null && preg_match('/^[a-zA-Z0-9]+$/D', $mask); 239 | 240 | if ($useChoice) { 241 | // Use Windows 95+ "choice" command, which allows for reading a 242 | // single character matching a mask, but is limited to lower ASCII 243 | // range. 244 | do { 245 | exec('choice /n /cs /c:' . $mask, $output, $return); 246 | if ($return == 255 || $return < 1 || $return > strlen($mask)) { 247 | throw new Exception\RuntimeException( 248 | '"choice" command failed to run. Are you using Windows XP or newer?' 249 | ); 250 | } 251 | 252 | // Fetch the char from mask 253 | $char = substr($mask, $return - 1, 1); 254 | } while ("" === $char || ($mask !== null && false === strstr($mask, $char))); 255 | 256 | return $char; 257 | } 258 | 259 | // Try to use PowerShell, giving it console access. Because PowersShell 260 | // interpreter can take a short while to load, we are emptying the 261 | // whole keyboard buffer and picking the last key that has been pressed 262 | // before or after PowerShell command has started. The ASCII code for 263 | // that key is then converted to a character. 264 | if ($mask === null) { 265 | exec( 266 | 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' 267 | . 'while ($Host.UI.RawUI.KeyAvailable) {$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\');}' 268 | . 'write $key.VirtualKeyCode;' 269 | . '"', 270 | $result, 271 | $return 272 | ); 273 | 274 | // Retrieve char from the result. 275 | $char = ! empty($result) ? implode('', $result) : null; 276 | 277 | if (! empty($char) && ! $return) { 278 | // We have obtained an ASCII code, convert back to a char ... 279 | $char = chr($char); 280 | 281 | // ... and return it... 282 | return $char; 283 | } 284 | } else { 285 | // Windows and DOS will return carriage-return char (ASCII 13) when 286 | // the user presses [ENTER] key, but Console Adapter user might 287 | // have provided a \n Newline (ASCII 10) in the mask, to allow [ENTER]. 288 | // We are going to replace all CR with NL to conform. 289 | $mask = strtr($mask, "\n", "\r"); 290 | 291 | // Prepare a list of ASCII codes from mask chars 292 | $asciiMask = array_map(function ($char) { 293 | return ord($char); 294 | }, str_split($mask)); 295 | $asciiMask = array_unique($asciiMask); 296 | 297 | // Char mask filtering is now handled by the PowerShell itself, 298 | // because it's a much faster method than invoking PS interpreter 299 | // after each mismatch. The command should return ASCII code of a 300 | // matching key. 301 | $result = $return = null; 302 | 303 | exec( 304 | 'powershell -NonInteractive -NoProfile -NoLogo -OutputFormat Text -Command "' 305 | . '[int[]] $mask = ' . implode(',', $asciiMask) . ';' 306 | . 'do {' 307 | . '$key = $Host.UI.RawUI.ReadKey(\'NoEcho,IncludeKeyDown\').VirtualKeyCode;' 308 | . '} while ( !($mask -contains $key) );' 309 | . 'write $key;' 310 | . '"', 311 | $result, 312 | $return 313 | ); 314 | 315 | $char = ! empty($result) ? trim(implode('', $result)) : null; 316 | 317 | if (! $return && $char && ($mask === null || in_array($char, $asciiMask))) { 318 | // Normalize CR to LF 319 | if ($char == 13) { 320 | $char = 10; 321 | } 322 | 323 | // Convert to a char 324 | $char = chr($char); 325 | 326 | // ... and return it... 327 | return $char; 328 | } 329 | } 330 | 331 | // Fall back to standard input, which on Windows does not allow reading 332 | // a single character. This is a limitation of Windows streams 333 | // implementation (not PHP) and this behavior cannot be changed with a 334 | // command like "stty", known to POSIX systems. 335 | $stream = fopen('php://stdin', 'rb'); 336 | do { 337 | $char = fgetc($stream); 338 | $char = substr(trim($char), 0, 1); 339 | } while (! $char || ($mask !== null && ! stristr($mask, $char))); 340 | fclose($stream); 341 | 342 | return $char; 343 | } 344 | 345 | /** 346 | * Read a single line from the console input. 347 | * 348 | * @param int $maxLength Maximum response length 349 | * @return string 350 | */ 351 | public function readLine($maxLength = 2048) 352 | { 353 | $f = fopen('php://stdin', 'r'); 354 | $line = rtrim(fread($f, $maxLength), "\r\n"); 355 | fclose($f); 356 | 357 | return $line; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/Adapter/Posix.php: -------------------------------------------------------------------------------- 1 | [ 43 | Color::NORMAL => '22;39', 44 | Color::RESET => '22;39', 45 | 46 | Color::BLACK => '0;30', 47 | Color::RED => '0;31', 48 | Color::GREEN => '0;32', 49 | Color::YELLOW => '0;33', 50 | Color::BLUE => '0;34', 51 | Color::MAGENTA => '0;35', 52 | Color::CYAN => '0;36', 53 | Color::WHITE => '0;37', 54 | 55 | Color::GRAY => '1;30', 56 | Color::LIGHT_RED => '1;31', 57 | Color::LIGHT_GREEN => '1;32', 58 | Color::LIGHT_YELLOW => '1;33', 59 | Color::LIGHT_BLUE => '1;34', 60 | Color::LIGHT_MAGENTA => '1;35', 61 | Color::LIGHT_CYAN => '1;36', 62 | Color::LIGHT_WHITE => '1;37', 63 | ], 64 | 'bg' => [ 65 | Color::NORMAL => '0;49', 66 | Color::RESET => '0;49', 67 | 68 | Color::BLACK => '40', 69 | Color::RED => '41', 70 | Color::GREEN => '42', 71 | Color::YELLOW => '43', 72 | Color::BLUE => '44', 73 | Color::MAGENTA => '45', 74 | Color::CYAN => '46', 75 | Color::WHITE => '47', 76 | 77 | Color::GRAY => '40', 78 | Color::LIGHT_RED => '41', 79 | Color::LIGHT_GREEN => '42', 80 | Color::LIGHT_YELLOW => '43', 81 | Color::LIGHT_BLUE => '44', 82 | Color::LIGHT_MAGENTA => '45', 83 | Color::LIGHT_CYAN => '46', 84 | Color::LIGHT_WHITE => '47', 85 | ], 86 | ]; 87 | 88 | /** 89 | * Last fetched TTY mode 90 | * 91 | * @var string|null 92 | */ 93 | protected $lastTTYMode = null; 94 | 95 | /** 96 | * Write a single line of text to console and advance cursor to the next line. 97 | * 98 | * This override works around a bug in some terminals that cause the background color 99 | * to fill the next line after EOL. To remedy this, we are sending the colored string with 100 | * appropriate color reset sequences before sending EOL character. 101 | * 102 | * @link https://github.com/zendframework/zf2/issues/4167 103 | * @param string $text 104 | * @param null|int $color 105 | * @param null|int $bgColor 106 | */ 107 | public function writeLine($text = "", $color = null, $bgColor = null) 108 | { 109 | $this->write($text, $color, $bgColor); 110 | $this->write(PHP_EOL); 111 | } 112 | 113 | /** 114 | * Determine and return current console width. 115 | * 116 | * @return int 117 | */ 118 | public function getWidth() 119 | { 120 | static $width; 121 | if ($width > 0) { 122 | return $width; 123 | } 124 | 125 | /** 126 | * Try to read env variable 127 | */ 128 | if (($result = getenv('COLUMNS')) !== false) { 129 | return $width = (int) $result; 130 | } 131 | 132 | /** 133 | * Try to read console size from "tput" command 134 | */ 135 | $result = exec('tput cols', $output, $return); 136 | if (! $return && is_numeric($result)) { 137 | return $width = (int) $result; 138 | } 139 | 140 | return $width = parent::getWidth(); 141 | } 142 | 143 | /** 144 | * Determine and return current console height. 145 | * 146 | * @return false|int 147 | */ 148 | public function getHeight() 149 | { 150 | static $height; 151 | if ($height > 0) { 152 | return $height; 153 | } 154 | 155 | // Try to read env variable 156 | if (($result = getenv('LINES')) !== false) { 157 | return $height = (int) $result; 158 | } 159 | 160 | // Try to read console size from "tput" command 161 | $result = exec('tput lines', $output, $return); 162 | if (! $return && is_numeric($result)) { 163 | return $height = (int) $result; 164 | } 165 | 166 | return $height = parent::getHeight(); 167 | } 168 | 169 | /** 170 | * Run a mode command and store results 171 | * 172 | * @return void 173 | */ 174 | protected function runModeCommand() 175 | { 176 | exec('mode', $output, $return); 177 | if ($return || ! count($output)) { 178 | $this->modeResult = ''; 179 | } else { 180 | $this->modeResult = trim(implode('', $output)); 181 | } 182 | } 183 | 184 | /** 185 | * Check if console is UTF-8 compatible 186 | * 187 | * @return bool 188 | */ 189 | public function isUtf8() 190 | { 191 | // Try to retrieve it from LANG env variable 192 | if (($lang = getenv('LANG')) !== false) { 193 | return stristr($lang, 'utf-8') || stristr($lang, 'utf8'); 194 | } 195 | 196 | return false; 197 | } 198 | 199 | /** 200 | * Show console cursor 201 | */ 202 | public function showCursor() 203 | { 204 | echo "\x1b[?25h"; 205 | } 206 | 207 | /** 208 | * Hide console cursor 209 | */ 210 | public function hideCursor() 211 | { 212 | echo "\x1b[?25l"; 213 | } 214 | 215 | /** 216 | * Set cursor position 217 | * @param int $x 218 | * @param int $y 219 | */ 220 | public function setPos($x, $y) 221 | { 222 | echo "\x1b[" . $y . ';' . $x . 'f'; 223 | } 224 | 225 | /** 226 | * Prepare a string that will be rendered in color. 227 | * 228 | * @param string $string 229 | * @param int $color 230 | * @param null|int $bgColor 231 | * @throws Exception\BadMethodCallException 232 | * @return string 233 | */ 234 | public function colorize($string, $color = null, $bgColor = null) 235 | { 236 | $color = $this->getColorCode($color, 'fg'); 237 | $bgColor = $this->getColorCode($bgColor, 'bg'); 238 | return ($color !== null ? "\x1b[" . $color . 'm' : '') 239 | . ($bgColor !== null ? "\x1b[" . $bgColor . 'm' : '') 240 | . $string 241 | . "\x1b[22;39m\x1b[0;49m"; 242 | } 243 | 244 | /** 245 | * Change current drawing color. 246 | * 247 | * @param int $color 248 | * @throws Exception\BadMethodCallException 249 | */ 250 | public function setColor($color) 251 | { 252 | $color = $this->getColorCode($color, 'fg'); 253 | echo "\x1b[" . $color . 'm'; 254 | } 255 | 256 | /** 257 | * Change current drawing background color 258 | * 259 | * @param int $bgColor 260 | * @throws Exception\BadMethodCallException 261 | */ 262 | public function setBgColor($bgColor) 263 | { 264 | $bgColor = $this->getColorCode($bgColor, 'bg'); 265 | echo "\x1b[" . ($bgColor) . 'm'; 266 | } 267 | 268 | /** 269 | * Reset color to console default. 270 | */ 271 | public function resetColor() 272 | { 273 | echo "\x1b[0;49m"; // reset bg color 274 | echo "\x1b[22;39m"; // reset fg bold, bright and faint 275 | echo "\x1b[25;39m"; // reset fg blink 276 | echo "\x1b[24;39m"; // reset fg underline 277 | } 278 | 279 | /** 280 | * Set Console charset to use. 281 | * 282 | * @param Charset\CharsetInterface $charset 283 | */ 284 | public function setCharset(Charset\CharsetInterface $charset) 285 | { 286 | $this->charset = $charset; 287 | } 288 | 289 | /** 290 | * Get charset currently in use by this adapter. 291 | * 292 | * @return Charset\CharsetInterface $charset 293 | */ 294 | public function getCharset() 295 | { 296 | if ($this->charset === null) { 297 | $this->charset = $this->getDefaultCharset(); 298 | } 299 | 300 | return $this->charset; 301 | } 302 | 303 | /** 304 | * @return Charset\CharsetInterface 305 | */ 306 | public function getDefaultCharset() 307 | { 308 | if ($this->isUtf8()) { 309 | return new Charset\Utf8; 310 | } 311 | return new Charset\DECSG(); 312 | } 313 | 314 | /** 315 | * Read a single character from the console input 316 | * 317 | * @param string|null $mask A list of allowed chars 318 | * @return string 319 | */ 320 | public function readChar($mask = null) 321 | { 322 | $this->setTTYMode('-icanon -echo'); 323 | 324 | $stream = fopen('php://stdin', 'rb'); 325 | do { 326 | $char = fgetc($stream); 327 | } while (strlen($char) !== 1 || ($mask !== null && false === strstr($mask, $char))); 328 | fclose($stream); 329 | 330 | $this->restoreTTYMode(); 331 | return $char; 332 | } 333 | 334 | /** 335 | * Reset color to console default. 336 | */ 337 | public function clear() 338 | { 339 | echo "\x1b[2J"; // reset bg color 340 | $this->setPos(1, 1); // reset cursor position 341 | } 342 | 343 | /** 344 | * Restore TTY (Console) mode to previous value. 345 | * 346 | * @return void 347 | */ 348 | protected function restoreTTYMode() 349 | { 350 | if ($this->lastTTYMode === null) { 351 | return; 352 | } 353 | 354 | shell_exec('stty ' . escapeshellarg($this->lastTTYMode)); 355 | } 356 | 357 | /** 358 | * Change TTY (Console) mode 359 | * 360 | * @link http://en.wikipedia.org/wiki/Stty 361 | * @param string $mode 362 | */ 363 | protected function setTTYMode($mode) 364 | { 365 | // Store last mode 366 | $this->lastTTYMode = trim(`stty -g`); 367 | 368 | // Set new mode 369 | shell_exec('stty '.escapeshellcmd($mode)); 370 | } 371 | 372 | /** 373 | * Get the final color code and throw exception on error 374 | * 375 | * @param null|int|Xterm256 $color 376 | * @param string $type (optional) Foreground 'fg' or background 'bg'. 377 | * @throws Exception\BadMethodCallException 378 | * @return string 379 | */ 380 | protected function getColorCode($color, $type = 'fg') 381 | { 382 | if ($color instanceof Xterm256) { 383 | $r = new ReflectionClass($color); 384 | $code = $r->getStaticPropertyValue('color'); 385 | if ($type == 'fg') { 386 | $code = sprintf($code, $color::FOREGROUND); 387 | } else { 388 | $code = sprintf($code, $color::BACKGROUND); 389 | } 390 | return $code; 391 | } 392 | 393 | if ($color !== null) { 394 | if (! isset(static::$ansiColorMap[$type][$color])) { 395 | throw new Exception\BadMethodCallException(sprintf( 396 | 'Unknown color "%s". Please use one of the Zend\Console\ColorInterface constants ' 397 | . 'or use Zend\Console\Color\Xterm256::calculate', 398 | $color 399 | )); 400 | } 401 | 402 | return static::$ansiColorMap[$type][$color]; 403 | } 404 | 405 | return; 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/Adapter/AbstractAdapter.php: -------------------------------------------------------------------------------- 1 | encodeText($text); 58 | 59 | if ($color !== null || $bgColor !== null) { 60 | echo $this->colorize($text, $color, $bgColor); 61 | } else { 62 | echo $text; 63 | } 64 | } 65 | 66 | /** 67 | * Alias for write() 68 | * 69 | * @param string $text 70 | * @param null|int $color 71 | * @param null|int $bgColor 72 | */ 73 | public function writeText($text, $color = null, $bgColor = null) 74 | { 75 | return $this->write($text, $color, $bgColor); 76 | } 77 | 78 | /** 79 | * Write a single line of text to console and advance cursor to the next line. 80 | * 81 | * @param string $text 82 | * @param null|int $color 83 | * @param null|int $bgColor 84 | */ 85 | public function writeLine($text = "", $color = null, $bgColor = null) 86 | { 87 | $this->write($text . PHP_EOL, $color, $bgColor); 88 | } 89 | 90 | /** 91 | * Write a piece of text at the coordinates of $x and $y 92 | * 93 | * 94 | * @param string $text Text to write 95 | * @param int $x Console X coordinate (column) 96 | * @param int $y Console Y coordinate (row) 97 | * @param null|int $color 98 | * @param null|int $bgColor 99 | */ 100 | public function writeAt($text, $x, $y, $color = null, $bgColor = null) 101 | { 102 | $this->setPos($x, $y); 103 | $this->write($text, $color, $bgColor); 104 | } 105 | 106 | /** 107 | * Write a box at the specified coordinates. 108 | * If X or Y coordinate value is negative, it will be calculated as the distance from far right or bottom edge 109 | * of the console (respectively). 110 | * 111 | * @param int $x1 Top-left corner X coordinate (column) 112 | * @param int $y1 Top-left corner Y coordinate (row) 113 | * @param int $x2 Bottom-right corner X coordinate (column) 114 | * @param int $y2 Bottom-right corner Y coordinate (row) 115 | * @param int $lineStyle (optional) Box border style. 116 | * @param int $fillStyle (optional) Box fill style or a single character to fill it with. 117 | * @param int $color (optional) Foreground color 118 | * @param int $bgColor (optional) Background color 119 | * @param null|int $fillColor (optional) Foreground color of box fill 120 | * @param null|int $fillBgColor (optional) Background color of box fill 121 | * @throws Exception\BadMethodCallException if coordinates are invalid 122 | */ 123 | public function writeBox( 124 | $x1, 125 | $y1, 126 | $x2, 127 | $y2, 128 | $lineStyle = self::LINE_SINGLE, 129 | $fillStyle = self::FILL_NONE, 130 | $color = null, 131 | $bgColor = null, 132 | $fillColor = null, 133 | $fillBgColor = null 134 | ) { 135 | // Sanitize coordinates 136 | $x1 = (int) $x1; 137 | $y1 = (int) $y1; 138 | $x2 = (int) $x2; 139 | $y2 = (int) $y2; 140 | 141 | // Translate negative coordinates 142 | if ($x2 < 0) { 143 | $x2 = $this->getWidth() - $x2; 144 | } 145 | 146 | if ($y2 < 0) { 147 | $y2 = $this->getHeight() - $y2; 148 | } 149 | 150 | // Validate coordinates 151 | if ($x1 < 0 152 | || $y1 < 0 153 | || $x2 < $x1 154 | || $y2 < $y1 155 | ) { 156 | throw new Exception\BadMethodCallException('Supplied X,Y coordinates are invalid.'); 157 | } 158 | 159 | // Determine charset and dimensions 160 | $charset = $this->getCharset(); 161 | $width = $x2 - $x1 + 1; 162 | 163 | if ($width <= 2) { 164 | $lineStyle = static::LINE_NONE; 165 | } 166 | 167 | // Activate line drawing 168 | $this->write($charset::ACTIVATE); 169 | 170 | // Draw horizontal lines 171 | if ($lineStyle !== static::LINE_NONE) { 172 | switch ($lineStyle) { 173 | case static::LINE_SINGLE: 174 | $lineChar = $charset::LINE_SINGLE_EW; 175 | break; 176 | 177 | case static::LINE_DOUBLE: 178 | $lineChar = $charset::LINE_DOUBLE_EW; 179 | break; 180 | 181 | case static::LINE_BLOCK: 182 | default: 183 | $lineChar = $charset::LINE_BLOCK_EW; 184 | break; 185 | } 186 | 187 | $this->setPos($x1 + 1, $y1); 188 | $this->write(str_repeat($lineChar, $width - 2), $color, $bgColor); 189 | $this->setPos($x1 + 1, $y2); 190 | $this->write(str_repeat($lineChar, $width - 2), $color, $bgColor); 191 | } 192 | 193 | // Draw vertical lines and fill 194 | if (is_numeric($fillStyle) 195 | && $fillStyle !== static::FILL_NONE) { 196 | switch ($fillStyle) { 197 | case static::FILL_SHADE_LIGHT: 198 | $fillChar = $charset::SHADE_LIGHT; 199 | break; 200 | case static::FILL_SHADE_MEDIUM: 201 | $fillChar = $charset::SHADE_MEDIUM; 202 | break; 203 | case static::FILL_SHADE_DARK: 204 | $fillChar = $charset::SHADE_DARK; 205 | break; 206 | case static::FILL_BLOCK: 207 | default: 208 | $fillChar = $charset::BLOCK; 209 | break; 210 | } 211 | } elseif ($fillStyle) { 212 | $fillChar = StringUtils::getWrapper()->substr($fillStyle, 0, 1); 213 | } else { 214 | $fillChar = ' '; 215 | } 216 | 217 | if ($lineStyle === static::LINE_NONE) { 218 | for ($y = $y1; $y <= $y2; $y++) { 219 | $this->setPos($x1, $y); 220 | $this->write(str_repeat($fillChar, $width), $fillColor, $fillBgColor); 221 | } 222 | } else { 223 | switch ($lineStyle) { 224 | case static::LINE_DOUBLE: 225 | $lineChar = $charset::LINE_DOUBLE_NS; 226 | break; 227 | case static::LINE_BLOCK: 228 | $lineChar = $charset::LINE_BLOCK_NS; 229 | break; 230 | case static::LINE_SINGLE: 231 | default: 232 | $lineChar = $charset::LINE_SINGLE_NS; 233 | break; 234 | } 235 | 236 | for ($y = $y1 + 1; $y < $y2; $y++) { 237 | $this->setPos($x1, $y); 238 | $this->write($lineChar, $color, $bgColor); 239 | $this->write(str_repeat($fillChar, $width - 2), $fillColor, $fillBgColor); 240 | $this->write($lineChar, $color, $bgColor); 241 | } 242 | } 243 | 244 | // Draw corners 245 | if ($lineStyle !== static::LINE_NONE) { 246 | if ($color !== null) { 247 | $this->setColor($color); 248 | } 249 | if ($bgColor !== null) { 250 | $this->setBgColor($bgColor); 251 | } 252 | if ($lineStyle === static::LINE_SINGLE) { 253 | $this->writeAt($charset::LINE_SINGLE_NW, $x1, $y1); 254 | $this->writeAt($charset::LINE_SINGLE_NE, $x2, $y1); 255 | $this->writeAt($charset::LINE_SINGLE_SE, $x2, $y2); 256 | $this->writeAt($charset::LINE_SINGLE_SW, $x1, $y2); 257 | } elseif ($lineStyle === static::LINE_DOUBLE) { 258 | $this->writeAt($charset::LINE_DOUBLE_NW, $x1, $y1); 259 | $this->writeAt($charset::LINE_DOUBLE_NE, $x2, $y1); 260 | $this->writeAt($charset::LINE_DOUBLE_SE, $x2, $y2); 261 | $this->writeAt($charset::LINE_DOUBLE_SW, $x1, $y2); 262 | } elseif ($lineStyle === static::LINE_BLOCK) { 263 | $this->writeAt($charset::LINE_BLOCK_NW, $x1, $y1); 264 | $this->writeAt($charset::LINE_BLOCK_NE, $x2, $y1); 265 | $this->writeAt($charset::LINE_BLOCK_SE, $x2, $y2); 266 | $this->writeAt($charset::LINE_BLOCK_SW, $x1, $y2); 267 | } 268 | } 269 | 270 | // Deactivate line drawing and reset colors 271 | $this->write($charset::DEACTIVATE); 272 | $this->resetColor(); 273 | } 274 | 275 | /** 276 | * Write a block of text at the given coordinates, matching the supplied width and height. 277 | * In case a line of text does not fit desired width, it will be wrapped to the next line. 278 | * In case the whole text does not fit in desired height, it will be truncated. 279 | * 280 | * @param string $text Text to write 281 | * @param int $width Maximum block width. Negative value means distance from right edge. 282 | * @param int|null $height Maximum block height. Negative value means distance from bottom edge. 283 | * @param int $x Block X coordinate (column) 284 | * @param int $y Block Y coordinate (row) 285 | * @param null|int $color (optional) Text color 286 | * @param null|int $bgColor (optional) Text background color 287 | * @throws Exception\InvalidArgumentException 288 | */ 289 | public function writeTextBlock( 290 | $text, 291 | $width, 292 | $height = null, 293 | $x = 0, 294 | $y = 0, 295 | $color = null, 296 | $bgColor = null 297 | ) { 298 | if ($x < 0 || $y < 0) { 299 | throw new Exception\InvalidArgumentException('Supplied X,Y coordinates are invalid.'); 300 | } 301 | 302 | if ($width < 1) { 303 | throw new Exception\InvalidArgumentException('Invalid width supplied.'); 304 | } 305 | 306 | if (null !== $height && $height < 1) { 307 | throw new Exception\InvalidArgumentException('Invalid height supplied.'); 308 | } 309 | 310 | // ensure the text is not wider than the width 311 | if (strlen($text) <= $width) { 312 | // just write the line at the spec'd position 313 | $this->setPos($x, $y); 314 | $this->write($text, $color, $bgColor); 315 | return; 316 | } 317 | 318 | $text = wordwrap($text, $width, PHP_EOL, true); 319 | 320 | // convert to array of lines 321 | $lines = explode(PHP_EOL, $text); 322 | 323 | // truncate if height was specified 324 | if (null !== $height && count($lines) > $height) { 325 | $lines = array_slice($lines, 0, $height); 326 | } 327 | 328 | // write each line 329 | $curY = $y; 330 | foreach ($lines as $line) { 331 | $this->setPos($x, $curY); 332 | $this->write($line, $color, $bgColor); 333 | $curY++;//next line 334 | } 335 | } 336 | 337 | /** 338 | * Determine and return current console width. 339 | * 340 | * @return int 341 | */ 342 | public function getWidth() 343 | { 344 | return 80; 345 | } 346 | 347 | /** 348 | * Determine and return current console height. 349 | * 350 | * @return int 351 | */ 352 | public function getHeight() 353 | { 354 | return 25; 355 | } 356 | 357 | /** 358 | * Determine and return current console width and height. 359 | * 360 | * @return int[] array($width, $height) 361 | */ 362 | public function getSize() 363 | { 364 | return [ 365 | $this->getWidth(), 366 | $this->getHeight(), 367 | ]; 368 | } 369 | 370 | /** 371 | * Check if console is UTF-8 compatible 372 | * 373 | * @return bool 374 | */ 375 | public function isUtf8() 376 | { 377 | return true; 378 | } 379 | 380 | /** 381 | * Set cursor position 382 | * 383 | * @param int $x 384 | * @param int $y 385 | */ 386 | public function setPos($x, $y) 387 | { 388 | } 389 | 390 | /** 391 | * Show console cursor 392 | */ 393 | public function showCursor() 394 | { 395 | } 396 | 397 | /** 398 | * Hide console cursor 399 | */ 400 | public function hideCursor() 401 | { 402 | } 403 | 404 | /** 405 | * Return current console window title. 406 | * 407 | * @return string 408 | */ 409 | public function getTitle() 410 | { 411 | return ''; 412 | } 413 | 414 | /** 415 | * Prepare a string that will be rendered in color. 416 | * 417 | * @param string $string 418 | * @param int $color 419 | * @param null|int $bgColor 420 | * @return string 421 | */ 422 | public function colorize($string, $color = null, $bgColor = null) 423 | { 424 | return $string; 425 | } 426 | 427 | /** 428 | * Change current drawing color. 429 | * 430 | * @param int $color 431 | */ 432 | public function setColor($color) 433 | { 434 | } 435 | 436 | /** 437 | * Change current drawing background color 438 | * 439 | * @param int $color 440 | */ 441 | public function setBgColor($color) 442 | { 443 | } 444 | 445 | /** 446 | * Reset color to console default. 447 | */ 448 | public function resetColor() 449 | { 450 | } 451 | 452 | /** 453 | * Set Console charset to use. 454 | * 455 | * @param Charset\CharsetInterface $charset 456 | */ 457 | public function setCharset(Charset\CharsetInterface $charset) 458 | { 459 | $this->charset = $charset; 460 | } 461 | 462 | /** 463 | * Get charset currently in use by this adapter. 464 | * 465 | * @return Charset\CharsetInterface $charset 466 | */ 467 | public function getCharset() 468 | { 469 | if ($this->charset === null) { 470 | $this->charset = $this->getDefaultCharset(); 471 | } 472 | 473 | return $this->charset; 474 | } 475 | 476 | /** 477 | * @return Charset\Utf8 478 | */ 479 | public function getDefaultCharset() 480 | { 481 | return new Charset\Utf8; 482 | } 483 | 484 | /** 485 | * Clear console screen 486 | */ 487 | public function clear() 488 | { 489 | echo "\f"; 490 | } 491 | 492 | /** 493 | * Clear line at cursor position 494 | */ 495 | public function clearLine() 496 | { 497 | echo "\r" . str_repeat(" ", $this->getWidth()) . "\r"; 498 | } 499 | 500 | /** 501 | * Clear console screen 502 | */ 503 | public function clearScreen() 504 | { 505 | return $this->clear(); 506 | } 507 | 508 | /** 509 | * Read a single line from the console input 510 | * 511 | * @param int $maxLength Maximum response length 512 | * @return string 513 | */ 514 | public function readLine($maxLength = 2048) 515 | { 516 | $f = fopen('php://stdin', 'r'); 517 | $line = stream_get_line($f, $maxLength, PHP_EOL); 518 | fclose($f); 519 | return rtrim($line, "\n\r"); 520 | } 521 | 522 | /** 523 | * Read a single character from the console input 524 | * 525 | * @param string|null $mask A list of allowed chars 526 | * @return string 527 | */ 528 | public function readChar($mask = null) 529 | { 530 | $f = fopen('php://stdin', 'r'); 531 | do { 532 | $char = fread($f, 1); 533 | } while ("" === $char || ($mask !== null && false === strstr($mask, $char))); 534 | fclose($f); 535 | return $char; 536 | } 537 | 538 | /** 539 | * Encode a text to match console encoding 540 | * 541 | * @param string $text 542 | * @return string the encoding text 543 | */ 544 | public function encodeText($text) 545 | { 546 | if ($this->isUtf8()) { 547 | if (StringUtils::isValidUtf8($text)) { 548 | return $text; 549 | } 550 | 551 | return utf8_encode($text); 552 | } 553 | 554 | if (StringUtils::isValidUtf8($text)) { 555 | return utf8_decode($text); 556 | } 557 | 558 | return $text; 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/RouteMatcher/DefaultRouteMatcher.php: -------------------------------------------------------------------------------- 1 | defaults = $defaults; 69 | $this->constraints = $constraints; 70 | $this->aliases = $aliases; 71 | 72 | if ($filters !== null) { 73 | foreach ($filters as $name => $filter) { 74 | if (! $filter instanceof FilterInterface) { 75 | throw new Exception\InvalidArgumentException( 76 | 'Cannot use ' . gettype($filters) . ' as filter for ' . __CLASS__ 77 | ); 78 | } 79 | $this->filters[$name] = $filter; 80 | } 81 | } 82 | 83 | if ($validators !== null) { 84 | foreach ($validators as $name => $validator) { 85 | if (! $validator instanceof ValidatorInterface) { 86 | throw new Exception\InvalidArgumentException( 87 | 'Cannot use ' . gettype($validator) . ' as validator for ' . __CLASS__ 88 | ); 89 | } 90 | $this->validators[$name] = $validator; 91 | } 92 | } 93 | 94 | $this->parts = $this->parseDefinition($route); 95 | } 96 | 97 | /** 98 | * Parse a route definition. 99 | * 100 | * @param string $def 101 | * @return array 102 | * @throws Exception\InvalidArgumentException 103 | */ 104 | protected function parseDefinition($def) 105 | { 106 | $def = trim($def); 107 | $pos = 0; 108 | $length = strlen($def); 109 | $parts = []; 110 | $unnamedGroupCounter = 1; 111 | $catchAllCount = 0; 112 | 113 | while ($pos < $length) { 114 | /** 115 | * Optional value param, i.e. 116 | * [SOMETHING] 117 | */ 118 | if (preg_match('/\G\[(?P[A-Z][A-Z0-9\_\-]*?)\](?: +|$)/s', $def, $m, 0, $pos)) { 119 | $item = [ 120 | 'name' => strtolower($m['name']), 121 | 'literal' => false, 122 | 'required' => false, 123 | 'positional' => true, 124 | 'hasValue' => true, 125 | ]; 126 | } /** 127 | * Mandatory value param, i.e. 128 | * SOMETHING 129 | */ 130 | elseif (preg_match('/\G(?P[A-Z][A-Z0-9\_\-]*?)(?: +|$)/s', $def, $m, 0, $pos)) { 131 | $item = [ 132 | 'name' => strtolower($m['name']), 133 | 'literal' => false, 134 | 'required' => true, 135 | 'positional' => true, 136 | 'hasValue' => true, 137 | ]; 138 | } /** 139 | * Optional literal param, i.e. 140 | * [something] 141 | */ 142 | elseif (preg_match('/\G\[ *?(?P[a-zA-Z][a-zA-Z0-9\_\-\:]*?) *?\](?: +|$)/s', $def, $m, 0, $pos)) { 143 | $item = [ 144 | 'name' => $m['name'], 145 | 'literal' => true, 146 | 'required' => false, 147 | 'positional' => true, 148 | 'hasValue' => false, 149 | ]; 150 | } /** 151 | * Optional value param, syntax 2, i.e. 152 | * [] 153 | */ 154 | elseif (preg_match('/\G\[ *\<(?P[a-zA-Z][a-zA-Z0-9\_\-]*?)\> *\](?: +|$)/s', $def, $m, 0, $pos)) { 155 | $item = [ 156 | 'name' => $m['name'], 157 | 'literal' => false, 158 | 'required' => false, 159 | 'positional' => true, 160 | 'hasValue' => true, 161 | ]; 162 | } /** 163 | * Mandatory value param, i.e. 164 | * 165 | */ 166 | elseif (preg_match('/\G\< *(?P[a-zA-Z][a-zA-Z0-9\_\-]*?) *\>(?: +|$)/s', $def, $m, 0, $pos)) { 167 | $item = [ 168 | 'name' => $m['name'], 169 | 'literal' => false, 170 | 'required' => true, 171 | 'positional' => true, 172 | 'hasValue' => true, 173 | ]; 174 | } /** 175 | * Mandatory literal param, i.e. 176 | * something 177 | */ 178 | elseif (preg_match('/\G(?P[a-zA-Z][a-zA-Z0-9\_\-\:]*?)(?: +|$)/s', $def, $m, 0, $pos)) { 179 | $item = [ 180 | 'name' => $m['name'], 181 | 'literal' => true, 182 | 'required' => true, 183 | 'positional' => true, 184 | 'hasValue' => false, 185 | ]; 186 | } /** 187 | * Mandatory long param 188 | * --param= 189 | * --param=whatever 190 | */ 191 | elseif (preg_match( 192 | '/\G--(?P[a-zA-Z0-9][a-zA-Z0-9\_\-]+)(?P=\S*?)?(?: +|$)/s', 193 | $def, 194 | $m, 195 | 0, 196 | $pos 197 | )) { 198 | $item = [ 199 | 'name' => $m['name'], 200 | 'short' => false, 201 | 'literal' => false, 202 | 'required' => true, 203 | 'positional' => false, 204 | 'hasValue' => ! empty($m['hasValue']), 205 | ]; 206 | } /** 207 | * Optional long flag 208 | * [--param] 209 | */ 210 | elseif (preg_match( 211 | '/\G\[ *?--(?P[a-zA-Z0-9][a-zA-Z0-9\_\-]+) *?\](?: +|$)/s', 212 | $def, 213 | $m, 214 | 0, 215 | $pos 216 | )) { 217 | $item = [ 218 | 'name' => $m['name'], 219 | 'short' => false, 220 | 'literal' => false, 221 | 'required' => false, 222 | 'positional' => false, 223 | 'hasValue' => false, 224 | ]; 225 | } /** 226 | * Optional long param 227 | * [--param=] 228 | * [--param=whatever] 229 | */ 230 | elseif (preg_match( 231 | '/\G\[ *?--(?P[a-zA-Z0-9][a-zA-Z0-9\_\-]+)(?P=\S*?)? *?\](?: +|$)/s', 232 | $def, 233 | $m, 234 | 0, 235 | $pos 236 | )) { 237 | $item = [ 238 | 'name' => $m['name'], 239 | 'short' => false, 240 | 'literal' => false, 241 | 'required' => false, 242 | 'positional' => false, 243 | 'hasValue' => ! empty($m['hasValue']), 244 | ]; 245 | } /** 246 | * Mandatory short param 247 | * -a 248 | * -a=i 249 | * -a=s 250 | * -a=w 251 | */ 252 | elseif (preg_match('/\G-(?P[a-zA-Z0-9])(?:=(?P[ns]))?(?: +|$)/s', $def, $m, 0, $pos)) { 253 | $item = [ 254 | 'name' => $m['name'], 255 | 'short' => true, 256 | 'literal' => false, 257 | 'required' => true, 258 | 'positional' => false, 259 | 'hasValue' => ! empty($m['type']) ? $m['type'] : null, 260 | ]; 261 | } /** 262 | * Optional short param 263 | * [-a] 264 | * [-a=n] 265 | * [-a=s] 266 | */ 267 | elseif (preg_match( 268 | '/\G\[ *?-(?P[a-zA-Z0-9])(?:=(?P[ns]))? *?\](?: +|$)/s', 269 | $def, 270 | $m, 271 | 0, 272 | $pos 273 | )) { 274 | $item = [ 275 | 'name' => $m['name'], 276 | 'short' => true, 277 | 'literal' => false, 278 | 'required' => false, 279 | 'positional' => false, 280 | 'hasValue' => ! empty($m['type']) ? $m['type'] : null, 281 | ]; 282 | } /** 283 | * Optional literal param alternative 284 | * [ something | somethingElse | anotherOne ] 285 | * [ something | somethingElse | anotherOne ]:namedGroup 286 | */ 287 | elseif (preg_match('/ 288 | \G 289 | \[ 290 | (?P 291 | (?: 292 | \ *? 293 | (?P[a-zA-Z][a-zA-Z0-9_\-]*?) 294 | \ *? 295 | (?:\||(?=\])) 296 | \ *? 297 | )+ 298 | ) 299 | \] 300 | (?:\:(?P[a-zA-Z0-9]+))? 301 | (?:\ +|$) 302 | /sx', $def, $m, 0, $pos) 303 | ) { 304 | // extract available options 305 | $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 306 | 307 | // remove dupes 308 | $options = array_unique($options); 309 | 310 | // prepare item 311 | $item = [ 312 | 'name' => isset($m['groupName']) 313 | ? $m['groupName'] 314 | : 'unnamedGroup' . $unnamedGroupCounter++, 315 | 'literal' => true, 316 | 'required' => false, 317 | 'positional' => true, 318 | 'alternatives' => $options, 319 | 'hasValue' => false, 320 | ]; 321 | } /** 322 | * Required literal param alternative 323 | * ( something | somethingElse | anotherOne ) 324 | * ( something | somethingElse | anotherOne ):namedGroup 325 | */ 326 | elseif (preg_match('/ 327 | \G 328 | \( 329 | (?P 330 | (?: 331 | \ *? 332 | (?P[a-zA-Z][a-zA-Z0-9_\-]+) 333 | \ *? 334 | (?:\||(?=\))) 335 | \ *? 336 | )+ 337 | ) 338 | \) 339 | (?:\:(?P[a-zA-Z0-9]+))? 340 | (?:\ +|$) 341 | /sx', $def, $m, 0, $pos)) { 342 | // extract available options 343 | $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 344 | 345 | // remove dupes 346 | $options = array_unique($options); 347 | 348 | // prepare item 349 | $item = [ 350 | 'name' => isset($m['groupName']) 351 | ? $m['groupName'] 352 | : 'unnamedGroupAt' . $unnamedGroupCounter++, 353 | 'literal' => true, 354 | 'required' => true, 355 | 'positional' => true, 356 | 'alternatives' => $options, 357 | 'hasValue' => false, 358 | ]; 359 | } /** 360 | * Required long/short flag alternative 361 | * ( --something | --somethingElse | --anotherOne | -s | -a ) 362 | * ( --something | --somethingElse | --anotherOne | -s | -a ):namedGroup 363 | */ 364 | elseif (preg_match('/ 365 | \G 366 | \( 367 | (?P 368 | (?: 369 | \ *? 370 | \-+(?P[a-zA-Z0-9][a-zA-Z0-9_\-]*?) 371 | \ *? 372 | (?:\||(?=\))) 373 | \ *? 374 | )+ 375 | ) 376 | \) 377 | (?:\:(?P[a-zA-Z0-9]+))? 378 | (?:\ +|$) 379 | /sx', $def, $m, 0, $pos)) { 380 | // extract available options 381 | $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 382 | 383 | // remove dupes 384 | $options = array_unique($options); 385 | 386 | // remove prefix 387 | array_walk($options, function (&$val) { 388 | $val = ltrim($val, '-'); 389 | }); 390 | 391 | // prepare item 392 | $item = [ 393 | 'name' => isset($m['groupName']) 394 | ? $m['groupName'] 395 | : 'unnamedGroupAt' . $unnamedGroupCounter++, 396 | 'literal' => false, 397 | 'required' => true, 398 | 'positional' => false, 399 | 'alternatives' => $options, 400 | 'hasValue' => false, 401 | ]; 402 | } /** 403 | * Optional flag alternative 404 | * [ --something | --somethingElse | --anotherOne | -s | -a ] 405 | * [ --something | --somethingElse | --anotherOne | -s | -a ]:namedGroup 406 | */ 407 | elseif (preg_match('/ 408 | \G 409 | \[ 410 | (?P 411 | (?: 412 | \ *? 413 | \-+(?P[a-zA-Z0-9][a-zA-Z0-9_\-]*?) 414 | \ *? 415 | (?:\||(?=\])) 416 | \ *? 417 | )+ 418 | ) 419 | \] 420 | (?:\:(?P[a-zA-Z0-9]+))? 421 | (?:\ +|$) 422 | /sx', $def, $m, 0, $pos)) { 423 | // extract available options 424 | $options = preg_split('/ *\| */', trim($m['options']), 0, PREG_SPLIT_NO_EMPTY); 425 | 426 | // remove dupes 427 | $options = array_unique($options); 428 | 429 | // remove prefix 430 | array_walk($options, function (&$val) { 431 | $val = ltrim($val, '-'); 432 | }); 433 | 434 | // prepare item 435 | $item = [ 436 | 'name' => isset($m['groupName']) 437 | ? $m['groupName'] 438 | : 'unnamedGroupAt' . $unnamedGroupCounter++, 439 | 'literal' => false, 440 | 'required' => false, 441 | 'positional' => false, 442 | 'alternatives' => $options, 443 | 'hasValue' => false, 444 | ]; 445 | } elseif (preg_match( 446 | '/\G\[ *?\.\.\.(?P[a-zA-Z][a-zA-Z0-9\_\-\:]*?) *?\](?: +|$)/s', 447 | $def, 448 | $m, 449 | 0, 450 | $pos 451 | )) { 452 | if ($catchAllCount > 0) { 453 | throw new Exception\InvalidArgumentException( 454 | 'Cannot define more than one catchAll parameter' 455 | ); 456 | } 457 | $catchAllCount++; 458 | $item = [ 459 | 'name' => $m['name'], 460 | 'literal' => false, 461 | 'required' => false, 462 | 'catchAll' => true, 463 | 'hasValue' => true, 464 | ]; 465 | } else { 466 | throw new Exception\InvalidArgumentException( 467 | 'Cannot understand Console route at "' . substr($def, $pos) . '"' 468 | ); 469 | } 470 | 471 | if (! empty($item['positional']) && $catchAllCount > 0) { 472 | throw new Exception\InvalidArgumentException( 473 | 'Positional parameters must come before catchAlls' 474 | ); 475 | } 476 | $pos += strlen($m[0]); 477 | $parts[] = $item; 478 | } 479 | 480 | return $parts; 481 | } 482 | 483 | /** 484 | * Returns list of names representing single parameter 485 | * 486 | * @param string $name 487 | * @return string 488 | */ 489 | private function getAliases($name) 490 | { 491 | $namesToMatch = [$name]; 492 | foreach ($this->aliases as $alias => $canonical) { 493 | if ($name == $canonical) { 494 | $namesToMatch[] = $alias; 495 | } 496 | } 497 | return $namesToMatch; 498 | } 499 | 500 | /** 501 | * Returns canonical name of a parameter 502 | * 503 | * @param string $name 504 | * @return string 505 | */ 506 | private function getCanonicalName($name) 507 | { 508 | if (isset($this->aliases[$name])) { 509 | return $this->aliases[$name]; 510 | } 511 | return $name; 512 | } 513 | 514 | /** 515 | * Match parameters against route passed to constructor 516 | * 517 | * @param array $params 518 | * @return array|null 519 | */ 520 | public function match($params) 521 | { 522 | $matches = []; 523 | 524 | /* 525 | * Extract positional and named parts 526 | */ 527 | $positional = $named = []; 528 | $catchAll = null; 529 | foreach ($this->parts as &$part) { 530 | if (isset($part['positional']) && $part['positional']) { 531 | $positional[] = &$part; 532 | } elseif (isset($part['catchAll']) && $part['catchAll']) { 533 | $catchAll = &$part; 534 | $matches[$catchAll['name']] = []; 535 | } else { 536 | $named[] = &$part; 537 | } 538 | } 539 | 540 | /* 541 | * Scan for named parts inside Console params 542 | */ 543 | foreach ($named as &$part) { 544 | /* 545 | * Prepare match regex 546 | */ 547 | if (isset($part['alternatives'])) { 548 | // an alternative of flags 549 | $regex = '/^\-+(?P'; 550 | 551 | $alternativeAliases = []; 552 | foreach ($part['alternatives'] as $alternative) { 553 | $alternativeAliases[] = '(?:' . implode('|', $this->getAliases($alternative)) . ')'; 554 | } 555 | 556 | $regex .= implode('|', $alternativeAliases); 557 | 558 | if ($part['hasValue']) { 559 | $regex .= ')(?:\=(?P.*?)$)?$/'; 560 | } else { 561 | $regex .= ')$/i'; 562 | } 563 | } else { 564 | // a single named flag 565 | $name = '(?:' . implode('|', $this->getAliases($part['name'])) . ')'; 566 | 567 | if ($part['short'] === true) { 568 | // short variant 569 | if ($part['hasValue']) { 570 | $regex = '/^\-' . $name . '(?:\=(?P.*?)$)?$/i'; 571 | } else { 572 | $regex = '/^\-' . $name . '$/i'; 573 | } 574 | } elseif ($part['short'] === false) { 575 | // long variant 576 | if ($part['hasValue']) { 577 | $regex = '/^\-{2,}' . $name . '(?:\=(?P.*?)$)?$/i'; 578 | } else { 579 | $regex = '/^\-{2,}' . $name . '$/i'; 580 | } 581 | } 582 | } 583 | 584 | /* 585 | * Look for param 586 | */ 587 | $value = $param = null; 588 | for ($x = 0, $count = count($params); $x < $count; $x++) { 589 | if (preg_match($regex, $params[$x], $m)) { 590 | // found param 591 | $param = $params[$x]; 592 | 593 | // prevent further scanning of this param 594 | array_splice($params, $x, 1); 595 | 596 | if (isset($m['value'])) { 597 | $value = $m['value']; 598 | } 599 | 600 | if (isset($m['name'])) { 601 | $matchedName = $this->getCanonicalName($m['name']); 602 | } 603 | 604 | break; 605 | } 606 | } 607 | 608 | 609 | if (! $param) { 610 | /* 611 | * Drop out if that was a mandatory param 612 | */ 613 | if ($part['required']) { 614 | return; 615 | } /* 616 | * Continue to next positional param 617 | */ 618 | else { 619 | continue; 620 | } 621 | } 622 | 623 | 624 | /* 625 | * Value for flags is always boolean 626 | */ 627 | if ($param && ! $part['hasValue']) { 628 | $value = true; 629 | } 630 | 631 | /* 632 | * Try to retrieve value if it is expected 633 | */ 634 | if ((null === $value || "" === $value) && $part['hasValue']) { 635 | if ($x < count($params) + 1 && isset($params[$x])) { 636 | // retrieve value from adjacent param 637 | $value = $params[$x]; 638 | 639 | // prevent further scanning of this param 640 | array_splice($params, $x, 1); 641 | } else { 642 | // there are no more params available 643 | return; 644 | } 645 | } 646 | 647 | /* 648 | * Validate the value against constraints 649 | */ 650 | if ($part['hasValue'] && isset($this->constraints[$part['name']])) { 651 | if (! preg_match($this->constraints[$part['name']], $value)) { 652 | // constraint failed 653 | return; 654 | } 655 | } 656 | 657 | /* 658 | * Store the value 659 | */ 660 | if ($part['hasValue']) { 661 | $matches[$part['name']] = $value; 662 | } else { 663 | $matches[$part['name']] = true; 664 | } 665 | 666 | /* 667 | * If there are alternatives, fill them 668 | */ 669 | if (isset($part['alternatives'])) { 670 | if ($part['hasValue']) { 671 | foreach ($part['alternatives'] as $alt) { 672 | if ($alt === $matchedName && ! isset($matches[$alt])) { 673 | $matches[$alt] = $value; 674 | } elseif (! isset($matches[$alt])) { 675 | $matches[$alt] = null; 676 | } 677 | } 678 | } else { 679 | foreach ($part['alternatives'] as $alt) { 680 | if ($alt === $matchedName && ! isset($matches[$alt])) { 681 | $matches[$alt] = isset($this->defaults[$alt]) ? $this->defaults[$alt] : true; 682 | } elseif (! isset($matches[$alt])) { 683 | $matches[$alt] = false; 684 | } 685 | } 686 | } 687 | } 688 | } 689 | 690 | /* 691 | * Scan for left-out flags that should result in a mismatch 692 | */ 693 | foreach ($params as $param) { 694 | if (preg_match('#^\-+#', $param)) { 695 | if (null === $catchAll) { 696 | return; // there is an unrecognized flag 697 | } 698 | } 699 | } 700 | 701 | /* 702 | * Go through all positional params 703 | */ 704 | $argPos = 0; 705 | foreach ($positional as &$part) { 706 | /* 707 | * Check if param exists 708 | */ 709 | if (! isset($params[$argPos])) { 710 | if ($part['required']) { 711 | // cannot find required positional param 712 | return; 713 | } else { 714 | // stop matching 715 | break; 716 | } 717 | } 718 | 719 | $value = $params[$argPos]; 720 | 721 | /* 722 | * Check if literal param matches 723 | */ 724 | if ($part['literal']) { 725 | if ((isset($part['alternatives']) 726 | && ! in_array($value, $part['alternatives'])) 727 | || (! isset($part['alternatives']) && $value != $part['name']) 728 | ) { 729 | return; 730 | } 731 | } 732 | 733 | /* 734 | * Validate the value against constraints 735 | */ 736 | if ($part['hasValue'] && isset($this->constraints[$part['name']])) { 737 | if (! preg_match($this->constraints[$part['name']], $value)) { 738 | // constraint failed 739 | return; 740 | } 741 | } 742 | 743 | /* 744 | * Store the value 745 | */ 746 | if ($part['hasValue']) { 747 | $matches[$part['name']] = $value; 748 | } elseif (isset($part['alternatives'])) { 749 | // from all alternatives set matching parameter to TRUE and the rest to FALSE 750 | foreach ($part['alternatives'] as $alt) { 751 | if ($alt == $value) { 752 | $matches[$alt] = isset($this->defaults[$alt]) ? $this->defaults[$alt] : true; 753 | } else { 754 | $matches[$alt] = false; 755 | } 756 | } 757 | 758 | // set alternatives group value 759 | $matches[$part['name']] = $value; 760 | } elseif (! $part['required']) { 761 | // set optional parameter flag 762 | $name = $part['name']; 763 | $matches[$name] = isset($this->defaults[$name]) ? $this->defaults[$name] : true; 764 | } 765 | 766 | /* 767 | * Advance to next argument 768 | */ 769 | $argPos++; 770 | } 771 | 772 | /* 773 | * Check if we have consumed all positional parameters 774 | */ 775 | if ($argPos < count($params)) { 776 | if (null !== $catchAll) { 777 | for ($i = $argPos; $i < count($params); $i++) { 778 | $matches[$catchAll['name']][] = $params[$i]; 779 | } 780 | } else { 781 | return; // there are extraneous params that were not consumed 782 | } 783 | } 784 | 785 | /* 786 | * Any optional flags that were not entered have value false 787 | */ 788 | foreach ($this->parts as &$part) { 789 | if (! $part['required'] && ! $part['hasValue']) { 790 | if (! isset($matches[$part['name']])) { 791 | $matches[$part['name']] = false; 792 | } 793 | // unset alternatives also should be false 794 | if (isset($part['alternatives'])) { 795 | foreach ($part['alternatives'] as $alt) { 796 | if (! isset($matches[$alt])) { 797 | $matches[$alt] = false; 798 | } 799 | } 800 | } 801 | } 802 | } 803 | 804 | // run filters 805 | foreach ($matches as $name => $value) { 806 | if (isset($this->filters[$name])) { 807 | $matches[$name] = $this->filters[$name]->filter($value); 808 | } 809 | } 810 | 811 | // run validators 812 | $valid = true; 813 | foreach ($matches as $name => $value) { 814 | if (isset($this->validators[$name])) { 815 | $valid &= $this->validators[$name]->isValid($value); 816 | } 817 | } 818 | 819 | if (! $valid) { 820 | return; 821 | } 822 | 823 | return array_replace($this->defaults, $matches); 824 | } 825 | } 826 | -------------------------------------------------------------------------------- /src/Getopt.php: -------------------------------------------------------------------------------- 1 | self::MODE_ZEND, 122 | self::CONFIG_DASHDASH => true, 123 | self::CONFIG_IGNORECASE => false, 124 | self::CONFIG_PARSEALL => true, 125 | self::CONFIG_CUMULATIVE_PARAMETERS => false, 126 | self::CONFIG_CUMULATIVE_FLAGS => false, 127 | self::CONFIG_PARAMETER_SEPARATOR => null, 128 | self::CONFIG_FREEFORM_FLAGS => false, 129 | self::CONFIG_NUMERIC_FLAGS => false 130 | ]; 131 | 132 | /** 133 | * Stores the command-line arguments for the calling application. 134 | * 135 | * @var array 136 | */ 137 | protected $argv = []; 138 | 139 | /** 140 | * Stores the name of the calling application. 141 | * 142 | * @var string 143 | */ 144 | protected $progname = ''; 145 | 146 | /** 147 | * Stores the list of legal options for this application. 148 | * 149 | * @var array 150 | */ 151 | protected $rules = []; 152 | 153 | /** 154 | * Stores alternate spellings of legal options. 155 | * 156 | * @var array 157 | */ 158 | protected $ruleMap = []; 159 | 160 | /** 161 | * Stores options given by the user in the current invocation 162 | * of the application, as well as parameters given in options. 163 | * 164 | * @var array 165 | */ 166 | protected $options = []; 167 | 168 | /** 169 | * Stores the command-line arguments other than options. 170 | * 171 | * @var array 172 | */ 173 | protected $remainingArgs = []; 174 | 175 | /** 176 | * State of the options: parsed or not yet parsed? 177 | * 178 | * @var bool 179 | */ 180 | protected $parsed = false; 181 | 182 | /** 183 | * A list of callbacks to call when a particular option is present. 184 | * 185 | * @var array 186 | */ 187 | protected $optionCallbacks = []; 188 | 189 | /** 190 | * The constructor takes one to three parameters. 191 | * 192 | * The first parameter is $rules, which may be a string for 193 | * gnu-style format, or a structured array for Zend-style format. 194 | * 195 | * The second parameter is $argv, and it is optional. If not 196 | * specified, $argv is inferred from the global argv. 197 | * 198 | * The third parameter is an array of configuration parameters 199 | * to control the behavior of this instance of Getopt; it is optional. 200 | * 201 | * @param array $rules 202 | * @param array $argv 203 | * @param array $getoptConfig 204 | * @throws Exception\InvalidArgumentException 205 | */ 206 | public function __construct($rules, $argv = null, $getoptConfig = []) 207 | { 208 | if (! isset($_SERVER['argv'])) { 209 | $errorDescription = (ini_get('register_argc_argv') == false) 210 | ? "argv is not available, because ini option 'register_argc_argv' is set Off" 211 | : '$_SERVER["argv"] is not set, but Zend\Console\Getopt cannot work without this information.'; 212 | throw new Exception\InvalidArgumentException($errorDescription); 213 | } 214 | 215 | $this->progname = $_SERVER['argv'][0]; 216 | $this->setOptions($getoptConfig); 217 | $this->addRules($rules); 218 | if (! is_array($argv)) { 219 | $argv = array_slice($_SERVER['argv'], 1); 220 | } 221 | if (isset($argv)) { 222 | $this->addArguments((array) $argv); 223 | } 224 | } 225 | 226 | /** 227 | * Return the state of the option seen on the command line of the 228 | * current application invocation. This function returns true, or the 229 | * parameter to the option, if any. If the option was not given, 230 | * this function returns null. 231 | * 232 | * The magic __get method works in the context of naming the option 233 | * as a virtual member of this class. 234 | * 235 | * @param string $key 236 | * @return string 237 | */ 238 | public function __get($key) 239 | { 240 | return $this->getOption($key); 241 | } 242 | 243 | /** 244 | * Test whether a given option has been seen. 245 | * 246 | * @param string $key 247 | * @return bool 248 | */ 249 | public function __isset($key) 250 | { 251 | $this->parse(); 252 | if (isset($this->ruleMap[$key])) { 253 | $key = $this->ruleMap[$key]; 254 | return isset($this->options[$key]); 255 | } 256 | return false; 257 | } 258 | 259 | /** 260 | * Set the value for a given option. 261 | * 262 | * @param string $key 263 | * @param string $value 264 | */ 265 | public function __set($key, $value) 266 | { 267 | $this->parse(); 268 | if (isset($this->ruleMap[$key])) { 269 | $key = $this->ruleMap[$key]; 270 | $this->options[$key] = $value; 271 | } 272 | } 273 | 274 | /** 275 | * Return the current set of options and parameters seen as a string. 276 | * 277 | * @return string 278 | */ 279 | public function __toString() 280 | { 281 | return $this->toString(); 282 | } 283 | 284 | /** 285 | * Unset an option. 286 | * 287 | * @param string $key 288 | */ 289 | public function __unset($key) 290 | { 291 | $this->parse(); 292 | if (isset($this->ruleMap[$key])) { 293 | $key = $this->ruleMap[$key]; 294 | unset($this->options[$key]); 295 | } 296 | } 297 | 298 | /** 299 | * Define additional command-line arguments. 300 | * These are appended to those defined when the constructor was called. 301 | * 302 | * @param array $argv 303 | * @throws Exception\InvalidArgumentException When not given an array as parameter 304 | * @return self 305 | */ 306 | public function addArguments($argv) 307 | { 308 | if (! is_array($argv)) { 309 | throw new Exception\InvalidArgumentException("Parameter #1 to addArguments should be an array"); 310 | } 311 | $this->argv = array_merge($this->argv, $argv); 312 | $this->parsed = false; 313 | return $this; 314 | } 315 | 316 | /** 317 | * Define full set of command-line arguments. 318 | * These replace any currently defined. 319 | * 320 | * @param array $argv 321 | * @throws Exception\InvalidArgumentException When not given an array as parameter 322 | * @return self 323 | */ 324 | public function setArguments($argv) 325 | { 326 | if (! is_array($argv)) { 327 | throw new Exception\InvalidArgumentException("Parameter #1 to setArguments should be an array"); 328 | } 329 | $this->argv = $argv; 330 | $this->parsed = false; 331 | return $this; 332 | } 333 | 334 | /** 335 | * Define multiple configuration options from an associative array. 336 | * These are not program options, but properties to configure 337 | * the behavior of Zend\Console\Getopt. 338 | * 339 | * @param array $getoptConfig 340 | * @return self 341 | */ 342 | public function setOptions($getoptConfig) 343 | { 344 | if (isset($getoptConfig)) { 345 | foreach ($getoptConfig as $key => $value) { 346 | $this->setOption($key, $value); 347 | } 348 | } 349 | return $this; 350 | } 351 | 352 | /** 353 | * Define one configuration option as a key/value pair. 354 | * These are not program options, but properties to configure 355 | * the behavior of Zend\Console\Getopt. 356 | * 357 | * @param string $configKey 358 | * @param string $configValue 359 | * @return self 360 | */ 361 | public function setOption($configKey, $configValue) 362 | { 363 | if ($configKey !== null) { 364 | $this->getoptConfig[$configKey] = $configValue; 365 | } 366 | return $this; 367 | } 368 | 369 | /** 370 | * Define additional option rules. 371 | * These are appended to the rules defined when the constructor was called. 372 | * 373 | * @param array $rules 374 | * @return self 375 | */ 376 | public function addRules($rules) 377 | { 378 | $ruleMode = $this->getoptConfig['ruleMode']; 379 | switch ($this->getoptConfig['ruleMode']) { 380 | case self::MODE_ZEND: 381 | if (is_array($rules)) { 382 | $this->_addRulesModeZend($rules); 383 | break; 384 | } 385 | // intentional fallthrough 386 | case self::MODE_GNU: 387 | $this->_addRulesModeGnu($rules); 388 | break; 389 | default: 390 | /** 391 | * Call addRulesModeFoo() for ruleMode 'foo'. 392 | * The developer should subclass Getopt and 393 | * provide this method. 394 | */ 395 | $method = '_addRulesMode' . ucfirst($ruleMode); 396 | $this->$method($rules); 397 | } 398 | $this->parsed = false; 399 | return $this; 400 | } 401 | 402 | /** 403 | * Return the current set of options and parameters seen as a string. 404 | * 405 | * @return string 406 | */ 407 | public function toString() 408 | { 409 | $this->parse(); 410 | $s = []; 411 | foreach ($this->options as $flag => $value) { 412 | $s[] = $flag . '=' . ($value === true ? 'true' : $value); 413 | } 414 | return implode(' ', $s); 415 | } 416 | 417 | /** 418 | * Return the current set of options and parameters seen 419 | * as an array of canonical options and parameters. 420 | * 421 | * Clusters have been expanded, and option aliases 422 | * have been mapped to their primary option names. 423 | * 424 | * @return array 425 | */ 426 | public function toArray() 427 | { 428 | $this->parse(); 429 | $s = []; 430 | foreach ($this->options as $flag => $value) { 431 | $s[] = $flag; 432 | if ($value !== true) { 433 | $s[] = $value; 434 | } 435 | } 436 | return $s; 437 | } 438 | 439 | /** 440 | * Return the current set of options and parameters seen in Json format. 441 | * 442 | * @return string 443 | */ 444 | public function toJson() 445 | { 446 | $this->parse(); 447 | $j = []; 448 | foreach ($this->options as $flag => $value) { 449 | $j['options'][] = [ 450 | 'option' => [ 451 | 'flag' => $flag, 452 | 'parameter' => $value 453 | ] 454 | ]; 455 | } 456 | 457 | $json = \Zend\Json\Json::encode($j); 458 | return $json; 459 | } 460 | 461 | /** 462 | * Return the current set of options and parameters seen in XML format. 463 | * 464 | * @return string 465 | */ 466 | public function toXml() 467 | { 468 | $this->parse(); 469 | $doc = new \DomDocument('1.0', 'utf-8'); 470 | $optionsNode = $doc->createElement('options'); 471 | $doc->appendChild($optionsNode); 472 | foreach ($this->options as $flag => $value) { 473 | $optionNode = $doc->createElement('option'); 474 | $optionNode->setAttribute('flag', utf8_encode($flag)); 475 | if ($value !== true) { 476 | $optionNode->setAttribute('parameter', utf8_encode($value)); 477 | } 478 | $optionsNode->appendChild($optionNode); 479 | } 480 | $xml = $doc->saveXML(); 481 | return $xml; 482 | } 483 | 484 | /** 485 | * Return a list of options that have been seen in the current argv. 486 | * 487 | * @return array 488 | */ 489 | public function getOptions() 490 | { 491 | $this->parse(); 492 | return array_keys($this->options); 493 | } 494 | 495 | /** 496 | * Return the state of the option seen on the command line of the 497 | * current application invocation. 498 | * 499 | * This function returns true, or the parameter value to the option, if any. 500 | * If the option was not given, this function returns false. 501 | * 502 | * @param string $flag 503 | * @return mixed 504 | */ 505 | public function getOption($flag) 506 | { 507 | $this->parse(); 508 | if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { 509 | $flag = strtolower($flag); 510 | } 511 | if (isset($this->ruleMap[$flag])) { 512 | $flag = $this->ruleMap[$flag]; 513 | if (isset($this->options[$flag])) { 514 | return $this->options[$flag]; 515 | } 516 | } 517 | return; 518 | } 519 | 520 | /** 521 | * Return the arguments from the command-line following all options found. 522 | * 523 | * @return array 524 | */ 525 | public function getRemainingArgs() 526 | { 527 | $this->parse(); 528 | return $this->remainingArgs; 529 | } 530 | 531 | public function getArguments() 532 | { 533 | $result = $this->getRemainingArgs(); 534 | foreach ($this->getOptions() as $option) { 535 | $result[$option] = $this->getOption($option); 536 | } 537 | return $result; 538 | } 539 | 540 | /** 541 | * Return a useful option reference, formatted for display in an 542 | * error message. 543 | * 544 | * Note that this usage information is provided in most Exceptions 545 | * generated by this class. 546 | * 547 | * @return string 548 | */ 549 | public function getUsageMessage() 550 | { 551 | $usage = "Usage: {$this->progname} [ options ]\n"; 552 | $maxLen = 20; 553 | $lines = []; 554 | foreach ($this->rules as $rule) { 555 | if (isset($rule['isFreeformFlag'])) { 556 | continue; 557 | } 558 | $flags = []; 559 | if (is_array($rule['alias'])) { 560 | foreach ($rule['alias'] as $flag) { 561 | $flags[] = (strlen($flag) == 1 ? '-' : '--') . $flag; 562 | } 563 | } 564 | $linepart['name'] = implode('|', $flags); 565 | if (isset($rule['param']) && $rule['param'] != 'none') { 566 | $linepart['name'] .= ' '; 567 | switch ($rule['param']) { 568 | case 'optional': 569 | $linepart['name'] .= "[ <{$rule['paramType']}> ]"; 570 | break; 571 | case 'required': 572 | $linepart['name'] .= "<{$rule['paramType']}>"; 573 | break; 574 | } 575 | } 576 | if (strlen($linepart['name']) > $maxLen) { 577 | $maxLen = strlen($linepart['name']); 578 | } 579 | $linepart['help'] = ''; 580 | if (isset($rule['help'])) { 581 | $linepart['help'] .= $rule['help']; 582 | } 583 | $lines[] = $linepart; 584 | } 585 | foreach ($lines as $linepart) { 586 | $usage .= sprintf( 587 | "%s %s\n", 588 | str_pad($linepart['name'], $maxLen), 589 | $linepart['help'] 590 | ); 591 | } 592 | return $usage; 593 | } 594 | 595 | /** 596 | * Define aliases for options. 597 | * 598 | * The parameter $aliasMap is an associative array 599 | * mapping option name (short or long) to an alias. 600 | * 601 | * @param array $aliasMap 602 | * @throws Exception\ExceptionInterface 603 | * @return self 604 | */ 605 | public function setAliases($aliasMap) 606 | { 607 | foreach ($aliasMap as $flag => $alias) { 608 | if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { 609 | $flag = strtolower($flag); 610 | $alias = strtolower($alias); 611 | } 612 | if (! isset($this->ruleMap[$flag])) { 613 | continue; 614 | } 615 | $flag = $this->ruleMap[$flag]; 616 | if (isset($this->rules[$alias]) || isset($this->ruleMap[$alias])) { 617 | $o = (strlen($alias) == 1 ? '-' : '--') . $alias; 618 | throw new Exception\InvalidArgumentException("Option \"$o\" is being defined more than once."); 619 | } 620 | $this->rules[$flag]['alias'][] = $alias; 621 | $this->ruleMap[$alias] = $flag; 622 | } 623 | return $this; 624 | } 625 | 626 | /** 627 | * Define help messages for options. 628 | * 629 | * The parameter $helpMap is an associative array 630 | * mapping option name (short or long) to the help string. 631 | * 632 | * @param array $helpMap 633 | * @return self 634 | */ 635 | public function setHelp($helpMap) 636 | { 637 | foreach ($helpMap as $flag => $help) { 638 | if (! isset($this->ruleMap[$flag])) { 639 | continue; 640 | } 641 | $flag = $this->ruleMap[$flag]; 642 | $this->rules[$flag]['help'] = $help; 643 | } 644 | return $this; 645 | } 646 | 647 | /** 648 | * Parse command-line arguments and find both long and short 649 | * options. 650 | * 651 | * Also find option parameters, and remaining arguments after 652 | * all options have been parsed. 653 | * 654 | * @return self 655 | */ 656 | public function parse() 657 | { 658 | if ($this->parsed === true) { 659 | return $this; 660 | } 661 | 662 | $argv = $this->argv; 663 | $this->options = []; 664 | $this->remainingArgs = []; 665 | while (count($argv) > 0) { 666 | if ($argv[0] == '--') { 667 | array_shift($argv); 668 | if ($this->getoptConfig[self::CONFIG_DASHDASH]) { 669 | $this->remainingArgs = array_merge($this->remainingArgs, $argv); 670 | break; 671 | } 672 | } 673 | if (0 === strpos($argv[0], '--')) { 674 | $this->_parseLongOption($argv); 675 | } elseif (0 === strpos($argv[0], '-') && ('-' != $argv[0] || count($argv) > 1)) { 676 | $this->_parseShortOptionCluster($argv); 677 | } elseif ($this->getoptConfig[self::CONFIG_PARSEALL]) { 678 | $this->remainingArgs[] = array_shift($argv); 679 | } else { 680 | /* 681 | * We should put all other arguments in remainingArgs and stop parsing 682 | * since CONFIG_PARSEALL is false. 683 | */ 684 | $this->remainingArgs = array_merge($this->remainingArgs, $argv); 685 | break; 686 | } 687 | } 688 | $this->parsed = true; 689 | 690 | //go through parsed args and process callbacks 691 | $this->triggerCallbacks(); 692 | 693 | return $this; 694 | } 695 | 696 | /** 697 | * @param string $option The name of the property which, if present, will call the passed 698 | * callback with the value of this parameter. 699 | * @param callable $callback The callback that will be called for this option. The first 700 | * parameter will be the value of getOption($option), the second 701 | * parameter will be a reference to $this object. If the callback returns 702 | * false then an Exception\RuntimeException will be thrown indicating that 703 | * there is a parse issue with this option. 704 | * 705 | * @return self 706 | */ 707 | public function setOptionCallback($option, \Closure $callback) 708 | { 709 | $this->optionCallbacks[$option] = $callback; 710 | 711 | return $this; 712 | } 713 | 714 | /** 715 | * Triggers all the registered callbacks. 716 | */ 717 | protected function triggerCallbacks() 718 | { 719 | foreach ($this->optionCallbacks as $option => $callback) { 720 | if (null === $this->getOption($option)) { 721 | continue; 722 | } 723 | //make sure we've resolved the alias, if using one 724 | if (isset($this->ruleMap[$option]) && $option = $this->ruleMap[$option]) { 725 | if (false === $callback($this->getOption($option), $this)) { 726 | throw new Exception\RuntimeException( 727 | "The option $option is invalid. See usage.", 728 | $this->getUsageMessage() 729 | ); 730 | } 731 | } 732 | } 733 | } 734 | 735 | /** 736 | * Parse command-line arguments for a single long option. 737 | * A long option is preceded by a double '--' character. 738 | * Long options may not be clustered. 739 | * 740 | * @param mixed &$argv 741 | */ 742 | // @codingStandardsIgnoreStart 743 | protected function _parseLongOption(&$argv) 744 | { 745 | // @codingStandardsIgnoreEnd 746 | $optionWithParam = ltrim(array_shift($argv), '-'); 747 | $l = explode('=', $optionWithParam, 2); 748 | $flag = array_shift($l); 749 | $param = array_shift($l); 750 | if (isset($param)) { 751 | array_unshift($argv, $param); 752 | } 753 | $this->_parseSingleOption($flag, $argv); 754 | } 755 | 756 | /** 757 | * Parse command-line arguments for short options. 758 | * Short options are those preceded by a single '-' character. 759 | * Short options may be clustered. 760 | * 761 | * @param mixed &$argv 762 | */ 763 | // @codingStandardsIgnoreStart 764 | protected function _parseShortOptionCluster(&$argv) 765 | { 766 | // @codingStandardsIgnoreEnd 767 | $flagCluster = ltrim(array_shift($argv), '-'); 768 | foreach (str_split($flagCluster) as $flag) { 769 | $this->_parseSingleOption($flag, $argv); 770 | } 771 | } 772 | 773 | /** 774 | * Parse command-line arguments for a single option. 775 | * 776 | * @param string $flag 777 | * @param mixed $argv 778 | * @throws Exception\ExceptionInterface 779 | */ 780 | // @codingStandardsIgnoreStart 781 | protected function _parseSingleOption($flag, &$argv) 782 | { 783 | // @codingStandardsIgnoreEnd 784 | if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { 785 | $flag = strtolower($flag); 786 | } 787 | 788 | // Check if this option is numeric one 789 | if (preg_match('/^\d+$/', $flag)) { 790 | return $this->_setNumericOptionValue($flag); 791 | } 792 | 793 | if (! isset($this->ruleMap[$flag])) { 794 | // Don't throw Exception for flag-like param in case when freeform flags are allowed 795 | if (! $this->getoptConfig[self::CONFIG_FREEFORM_FLAGS]) { 796 | throw new Exception\RuntimeException( 797 | "Option \"$flag\" is not recognized.", 798 | $this->getUsageMessage() 799 | ); 800 | } 801 | 802 | // Magic methods in future will use this mark as real flag value 803 | $this->ruleMap[$flag] = $flag; 804 | $realFlag = $flag; 805 | $this->rules[$realFlag] = [ 806 | 'param' => 'optional', 807 | 'isFreeformFlag' => true 808 | ]; 809 | } else { 810 | $realFlag = $this->ruleMap[$flag]; 811 | } 812 | 813 | switch ($this->rules[$realFlag]['param']) { 814 | case 'required': 815 | if (count($argv) > 0) { 816 | $param = array_shift($argv); 817 | $this->_checkParameterType($realFlag, $param); 818 | } else { 819 | throw new Exception\RuntimeException( 820 | "Option \"$flag\" requires a parameter.", 821 | $this->getUsageMessage() 822 | ); 823 | } 824 | break; 825 | case 'optional': 826 | if (count($argv) > 0 && 0 !== strpos($argv[0], '-')) { 827 | $param = array_shift($argv); 828 | $this->_checkParameterType($realFlag, $param); 829 | } else { 830 | $param = true; 831 | } 832 | break; 833 | default: 834 | $param = true; 835 | } 836 | 837 | $this->_setSingleOptionValue($realFlag, $param); 838 | } 839 | 840 | /** 841 | * Set given value as value of numeric option 842 | * 843 | * Throw runtime exception if this action is deny by configuration 844 | * or no one numeric option handlers is defined 845 | * 846 | * @param int $value 847 | * @throws Exception\RuntimeException 848 | * @return void 849 | */ 850 | // @codingStandardsIgnoreStart 851 | protected function _setNumericOptionValue($value) 852 | { 853 | // @codingStandardsIgnoreEnd 854 | if (! $this->getoptConfig[self::CONFIG_NUMERIC_FLAGS]) { 855 | throw new Exception\RuntimeException("Using of numeric flags are deny by configuration"); 856 | } 857 | 858 | if (empty($this->getoptConfig['numericFlagsOption'])) { 859 | throw new Exception\RuntimeException("Any option for handling numeric flags are specified"); 860 | } 861 | 862 | return $this->_setSingleOptionValue($this->getoptConfig['numericFlagsOption'], $value); 863 | } 864 | 865 | /** 866 | * Add relative to options' flag value 867 | * 868 | * If options list already has current flag as key 869 | * and parser should follow cumulative params by configuration, 870 | * we should to add new param to array, not to overwrite 871 | * 872 | * @param string $flag 873 | * @param string $value 874 | */ 875 | // @codingStandardsIgnoreStart 876 | protected function _setSingleOptionValue($flag, $value) 877 | { 878 | // @codingStandardsIgnoreEnd 879 | if (true === $value && $this->getoptConfig[self::CONFIG_CUMULATIVE_FLAGS]) { 880 | // For boolean values we have to create new flag, or increase number of flags' usage count 881 | return $this->_setBooleanFlagValue($flag); 882 | } 883 | 884 | // Split multiple values, if necessary 885 | // Filter empty values from splited array 886 | $separator = $this->getoptConfig[self::CONFIG_PARAMETER_SEPARATOR]; 887 | if (is_string($value) && ! empty($separator) && is_string($separator) && substr_count($value, $separator)) { 888 | $value = array_filter(explode($separator, $value)); 889 | } 890 | 891 | if (! array_key_exists($flag, $this->options)) { 892 | $this->options[$flag] = $value; 893 | } elseif ($this->getoptConfig[self::CONFIG_CUMULATIVE_PARAMETERS]) { 894 | $this->options[$flag] = (array) $this->options[$flag]; 895 | array_push($this->options[$flag], $value); 896 | } else { 897 | $this->options[$flag] = $value; 898 | } 899 | } 900 | 901 | /** 902 | * Set TRUE value to given flag, if this option does not exist yet 903 | * In other case increase value to show count of flags' usage 904 | * 905 | * @param string $flag 906 | */ 907 | // @codingStandardsIgnoreStart 908 | protected function _setBooleanFlagValue($flag) 909 | { 910 | // @codingStandardsIgnoreEnd 911 | $this->options[$flag] = array_key_exists($flag, $this->options) 912 | ? (int) $this->options[$flag] + 1 913 | : true; 914 | } 915 | 916 | /** 917 | * Return true if the parameter is in a valid format for 918 | * the option $flag. 919 | * Throw an exception in most other cases. 920 | * 921 | * @param string $flag 922 | * @param string $param 923 | * @throws Exception\ExceptionInterface 924 | * @return bool 925 | */ 926 | // @codingStandardsIgnoreStart 927 | protected function _checkParameterType($flag, $param) 928 | { 929 | // @codingStandardsIgnoreEnd 930 | $type = 'string'; 931 | if (isset($this->rules[$flag]['paramType'])) { 932 | $type = $this->rules[$flag]['paramType']; 933 | } 934 | switch ($type) { 935 | case 'word': 936 | if (preg_match('/\W/', $param)) { 937 | throw new Exception\RuntimeException( 938 | "Option \"$flag\" requires a single-word parameter, but was given \"$param\".", 939 | $this->getUsageMessage() 940 | ); 941 | } 942 | break; 943 | case 'integer': 944 | if (preg_match('/\D/', $param)) { 945 | throw new Exception\RuntimeException( 946 | "Option \"$flag\" requires an integer parameter, but was given \"$param\".", 947 | $this->getUsageMessage() 948 | ); 949 | } 950 | break; 951 | case 'string': 952 | default: 953 | break; 954 | } 955 | return true; 956 | } 957 | 958 | /** 959 | * Define legal options using the gnu-style format. 960 | * 961 | * @param string $rules 962 | */ 963 | // @codingStandardsIgnoreStart 964 | protected function _addRulesModeGnu($rules) 965 | { 966 | // @codingStandardsIgnoreEnd 967 | $ruleArray = []; 968 | 969 | /** 970 | * Options may be single alphanumeric characters. 971 | * Options may have a ':' which indicates a required string parameter. 972 | * No long options or option aliases are supported in GNU style. 973 | */ 974 | preg_match_all('/([a-zA-Z0-9]:?)/', $rules, $ruleArray); 975 | foreach ($ruleArray[1] as $rule) { 976 | $r = []; 977 | $flag = substr($rule, 0, 1); 978 | if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { 979 | $flag = strtolower($flag); 980 | } 981 | $r['alias'][] = $flag; 982 | if (1 === strpos($rule, ':', 1)) { 983 | $r['param'] = 'required'; 984 | $r['paramType'] = 'string'; 985 | } else { 986 | $r['param'] = 'none'; 987 | } 988 | $this->rules[$flag] = $r; 989 | $this->ruleMap[$flag] = $flag; 990 | } 991 | } 992 | 993 | /** 994 | * Define legal options using the Zend-style format. 995 | * 996 | * @param array $rules 997 | * @throws Exception\ExceptionInterface 998 | */ 999 | // @codingStandardsIgnoreStart 1000 | protected function _addRulesModeZend($rules) 1001 | { 1002 | // @codingStandardsIgnoreEnd 1003 | foreach ($rules as $ruleCode => $helpMessage) { 1004 | // this may have to translate the long parm type if there 1005 | // are any complaints that =string will not work (even though that use 1006 | // case is not documented) 1007 | if (in_array(substr($ruleCode, -2, 1), ['-', '='])) { 1008 | $flagList = substr($ruleCode, 0, -2); 1009 | $delimiter = substr($ruleCode, -2, 1); 1010 | $paramType = substr($ruleCode, -1); 1011 | } else { 1012 | $flagList = $ruleCode; 1013 | $delimiter = $paramType = null; 1014 | } 1015 | if ($this->getoptConfig[self::CONFIG_IGNORECASE]) { 1016 | $flagList = strtolower($flagList); 1017 | } 1018 | $flags = explode('|', $flagList); 1019 | $rule = []; 1020 | $mainFlag = $flags[0]; 1021 | foreach ($flags as $flag) { 1022 | if (empty($flag)) { 1023 | throw new Exception\InvalidArgumentException("Blank flag not allowed in rule \"$ruleCode\"."); 1024 | } 1025 | if (strlen($flag) == 1) { 1026 | if (isset($this->ruleMap[$flag])) { 1027 | throw new Exception\InvalidArgumentException( 1028 | "Option \"-$flag\" is being defined more than once." 1029 | ); 1030 | } 1031 | $this->ruleMap[$flag] = $mainFlag; 1032 | $rule['alias'][] = $flag; 1033 | } else { 1034 | if (isset($this->rules[$flag]) || isset($this->ruleMap[$flag])) { 1035 | throw new Exception\InvalidArgumentException( 1036 | "Option \"--$flag\" is being defined more than once." 1037 | ); 1038 | } 1039 | $this->ruleMap[$flag] = $mainFlag; 1040 | $rule['alias'][] = $flag; 1041 | } 1042 | } 1043 | if (isset($delimiter)) { 1044 | switch ($delimiter) { 1045 | case self::PARAM_REQUIRED: 1046 | $rule['param'] = 'required'; 1047 | break; 1048 | case self::PARAM_OPTIONAL: 1049 | default: 1050 | $rule['param'] = 'optional'; 1051 | } 1052 | switch (substr($paramType, 0, 1)) { 1053 | case self::TYPE_WORD: 1054 | $rule['paramType'] = 'word'; 1055 | break; 1056 | case self::TYPE_INTEGER: 1057 | $rule['paramType'] = 'integer'; 1058 | break; 1059 | case self::TYPE_NUMERIC_FLAG: 1060 | $rule['paramType'] = 'numericFlag'; 1061 | $this->getoptConfig['numericFlagsOption'] = $mainFlag; 1062 | break; 1063 | case self::TYPE_STRING: 1064 | default: 1065 | $rule['paramType'] = 'string'; 1066 | } 1067 | } else { 1068 | $rule['param'] = 'none'; 1069 | } 1070 | $rule['help'] = $helpMessage; 1071 | $this->rules[$mainFlag] = $rule; 1072 | } 1073 | } 1074 | } 1075 | --------------------------------------------------------------------------------