├── .gitignore ├── src ├── Exceptions │ ├── AbstractFlagException.php │ ├── InvalidFlagParamException.php │ ├── InvalidFlagTypeException.php │ └── MissingFlagParamException.php └── Flags.php ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── phpunit.xml.dist ├── example └── example.php ├── composer.json ├── LICENSE.md ├── .mddoc.xml ├── README.md └── test └── FlagsTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | .idea/ 4 | clover.xml 5 | phpunit.xml 6 | -------------------------------------------------------------------------------- /src/Exceptions/AbstractFlagException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | test 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/example.php: -------------------------------------------------------------------------------- 1 | bool('foo', false, 'Enable the foo'); 8 | $bar = & $flags->uint('bar', 10, 'Number of bars'); 9 | $baz = & $flags->string('baz', 'default', 'What to name the baz'); 10 | $verbose = & $flags->short('v', 'verbosity'); 11 | 12 | /** 13 | * No Default value, making qux is *required* 14 | */ 15 | $qux = & $flags->bool('qux'); 16 | 17 | try { 18 | $flags->parse(); 19 | } catch( Exception $e ) { 20 | die($e->getMessage() . PHP_EOL . $flags->getDefaults() . PHP_EOL); 21 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donatj/flags", 3 | "type": "library", 4 | "description": "GNU-style PHP command line argument parser", 5 | "keywords": [ 6 | "cli", 7 | "arguments", 8 | "options", 9 | "flags", 10 | "gnu-style" 11 | ], 12 | "require-dev": { 13 | "donatj/drop": "*", 14 | "phpunit/phpunit": "~7|~9" 15 | }, 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Jesse Donat", 20 | "email": "donatj@gmail.com", 21 | "homepage": "https://donatstudios.com", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": ">=7.1.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "donatj\\": "src/" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - pull_request 3 | - push 4 | 5 | name: CI 6 | 7 | jobs: 8 | run: 9 | name: Tests 10 | 11 | strategy: 12 | matrix: 13 | operating-system: [ubuntu-latest] 14 | php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] 15 | 16 | runs-on: ${{ matrix.operating-system }} 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v6 21 | 22 | - name: Install PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | extensions: sockets, json, curl 27 | 28 | - name: Install dependencies with composer 29 | run: composer install 30 | 31 | - name: Run tests 32 | run: ./vendor/bin/phpunit 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) 2013 Jesse G. Donat 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /.mddoc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | Here is a simple example script: 36 | 37 | 38 | ]]> 45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flags 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/donatj/flags/version)](https://packagist.org/packages/donatj/flags) 4 | [![Total Downloads](https://poser.pugx.org/donatj/flags/downloads)](https://packagist.org/packages/donatj/flags) 5 | [![License](https://poser.pugx.org/donatj/flags/license)](https://packagist.org/packages/donatj/flags) 6 | [![ci.yml](https://github.com/donatj/Flags/actions/workflows/ci.yml/badge.svg)](https://github.com/donatj/Flags/actions/workflows/ci.yml) 7 | 8 | 9 | Flags is an argument parser inspired by the Go-lang [Flag](http://golang.org/pkg/flag/#Parsed) package, taking its methodology but attaching a **GNU-style** flag parser. 10 | 11 | --- 12 | 13 | Flags supports the following style of parameters: 14 | 15 | Long-Flags 16 | `--key=value` / `--key value` 17 | 18 | Short-Flags 19 | `-v` 20 | 21 | GNU Style Multi-Short-Flags 22 | `-Xasd` 23 | 24 | Multiple of the Same Short-Flag 25 | `-vvv` 26 | 27 | As well as the ` -- ` operator for absolute separation of arguments from options. 28 | 29 | ## Requirements 30 | 31 | - **php**: >=7.1.0 32 | 33 | ## Installing 34 | 35 | Install the latest version with: 36 | 37 | ```bash 38 | composer require 'donatj/flags' 39 | ``` 40 | 41 | ## Example 42 | 43 | Here is a simple example script: 44 | 45 | ```php 46 | bool('foo', false, 'Enable the foo'); 53 | $bar = & $flags->uint('bar', 10, 'Number of bars'); 54 | $baz = & $flags->string('baz', 'default', 'What to name the baz'); 55 | $verbose = & $flags->short('v', 'verbosity'); 56 | 57 | /** 58 | * No Default value, making qux is *required* 59 | */ 60 | $qux = & $flags->bool('qux'); 61 | 62 | try { 63 | $flags->parse(); 64 | } catch( Exception $e ) { 65 | die($e->getMessage() . PHP_EOL . $flags->getDefaults() . PHP_EOL); 66 | } 67 | ``` 68 | 69 | The by-reference `= &` allows the value to be updated from the *default* to the argument value once the `parse()` method has been triggered. This is inspired by the Go Flag packages use of pointers 70 | 71 | ``` 72 | bash-3.2$ php example/example.php 73 | Expected option --qux missing. 74 | -v verbosity 75 | --foo Enable the foo 76 | --bar [uint] Number of bars 77 | --baz [string] What to name the baz 78 | --qux 79 | 80 | ``` 81 | 82 | ## Documentation 83 | 84 | ### Class: donatj\Flags 85 | 86 | #### Method: Flags->__construct 87 | 88 | ```php 89 | function __construct([ ?array $args = null [, $skipFirstArgument = true]]) 90 | ``` 91 | 92 | Flags constructor. 93 | 94 | ##### Parameters: 95 | 96 | - ***array*** | ***null*** `$args` - The arguments to parse, defaults to $_SERVER['argv'] 97 | - ***bool*** `$skipFirstArgument` - Setting to false causes the first argument to be parsed as an parameter 98 | rather than the command. 99 | 100 | --- 101 | 102 | #### Method: Flags->arg 103 | 104 | ```php 105 | function arg($index) 106 | ``` 107 | 108 | Returns the n'th command-line argument. `arg(0)` is the first remaining argument after flags have been processed. 109 | 110 | ##### Parameters: 111 | 112 | - ***int*** `$index` 113 | 114 | ##### Returns: 115 | 116 | - ***string*** 117 | 118 | --- 119 | 120 | #### Method: Flags->args 121 | 122 | ```php 123 | function args() 124 | ``` 125 | 126 | Returns the non-flag command-line arguments. 127 | 128 | ##### Returns: 129 | 130 | - ***string[]*** - Array of argument strings 131 | 132 | --- 133 | 134 | #### Method: Flags->shorts 135 | 136 | ```php 137 | function shorts() 138 | ``` 139 | 140 | Returns an array of short-flag call-counts indexed by character 141 | 142 | `-v` would set the 'v' index to 1, whereas `-vvv` will set the 'v' index to 3 143 | 144 | ##### Returns: 145 | 146 | - ***array*** 147 | 148 | --- 149 | 150 | #### Method: Flags->longs 151 | 152 | ```php 153 | function longs() 154 | ``` 155 | 156 | Returns an array of long-flag values indexed by flag name 157 | 158 | ##### Returns: 159 | 160 | - ***array*** 161 | 162 | --- 163 | 164 | #### Method: Flags->short 165 | 166 | ```php 167 | function short($letter [, $usage = '']) 168 | ``` 169 | 170 | Defines a short-flag of specified name, and usage string. 171 | 172 | The return value is a reference to an integer variable that stores the number of times the short-flag was called. 173 | 174 | This means the value of the reference for v would be the following. 175 | 176 | -v => 1 177 | -vvv => 3 178 | 179 | ##### Parameters: 180 | 181 | - ***string*** `$letter` - The character of the short-flag to define 182 | - ***string*** `$usage` - The usage description 183 | 184 | ##### Returns: 185 | 186 | - ***int*** 187 | 188 | --- 189 | 190 | #### Method: Flags->bool 191 | 192 | ```php 193 | function bool($name [, $value = null [, $usage = '']]) 194 | ``` 195 | 196 | Defines a bool long-flag of specified name, default value, and usage string. 197 | 198 | The return value is a reference to a variable that stores the value of the flag. 199 | 200 | ##### Examples 201 | 202 | ##### Truth-y 203 | 204 | --mybool=[true|t|1] 205 | --mybool [true|t|1] 206 | --mybool 207 | 208 | ##### False-y 209 | 210 | --mybool=[false|f|0] 211 | --mybool [false|f|0] 212 | [not calling --mybool and having the default false] 213 | 214 | ##### Parameters: 215 | 216 | - ***string*** `$name` - The name of the long-flag to define 217 | - ***mixed*** `$value` - The default value - usually false for bool - which if null marks the flag required 218 | - ***string*** `$usage` - The usage description 219 | 220 | ##### Returns: 221 | 222 | - ***mixed*** - A reference to the flags value 223 | 224 | --- 225 | 226 | #### Method: Flags->float 227 | 228 | ```php 229 | function float($name [, $value = null [, $usage = '']]) 230 | ``` 231 | 232 | Defines a float long-flag of specified name, default value, and usage string. 233 | 234 | The return value is a reference to a variable that stores the value of the flag. 235 | 236 | ##### Examples 237 | 238 | --myfloat=1.1 239 | --myfloat 1.1 240 | 241 | ##### Parameters: 242 | 243 | - ***string*** `$name` - The name of the long-flag to define 244 | - ***mixed*** `$value` - The default value which if null marks the flag required 245 | - ***string*** `$usage` - The usage description 246 | 247 | ##### Returns: 248 | 249 | - ***mixed*** - A reference to the flags value 250 | 251 | --- 252 | 253 | #### Method: Flags->int 254 | 255 | ```php 256 | function int($name [, $value = null [, $usage = '']]) 257 | ``` 258 | 259 | Defines an integer long-flag of specified name, default value, and usage string. 260 | 261 | The return value is a reference to a variable that stores the value of the flag. 262 | 263 | Note: Float values trigger an error, rather than casting. 264 | 265 | ##### Examples 266 | 267 | --myinteger=1 268 | --myinteger 1 269 | 270 | ##### Parameters: 271 | 272 | - ***string*** `$name` - The name of the long-flag to define 273 | - ***mixed*** `$value` - The default value which if null marks the flag required 274 | - ***string*** `$usage` - The usage description 275 | 276 | ##### Returns: 277 | 278 | - ***mixed*** - A reference to the flags value 279 | 280 | --- 281 | 282 | #### Method: Flags->uint 283 | 284 | ```php 285 | function uint($name [, $value = null [, $usage = '']]) 286 | ``` 287 | 288 | Defines a unsigned integer long-flag of specified name, default value, and usage string. 289 | 290 | The return value is a reference to a variable that stores the value of the flag. 291 | 292 | Note: Negative values trigger an error, rather than casting. 293 | 294 | ##### Examples 295 | 296 | --myinteger=1 297 | --myinteger 1 298 | 299 | ##### Parameters: 300 | 301 | - ***string*** `$name` - The name of the long-flag to define 302 | - ***mixed*** `$value` - The default value which if null marks the flag required 303 | - ***string*** `$usage` - The usage description 304 | 305 | ##### Returns: 306 | 307 | - ***mixed*** - A reference to the flags value 308 | 309 | --- 310 | 311 | #### Method: Flags->string 312 | 313 | ```php 314 | function string($name [, $value = null [, $usage = '']]) 315 | ``` 316 | 317 | Defines a string long-flag of specified name, default value, and usage string. 318 | 319 | The return value is a reference to a variable that stores the value of the flag. 320 | 321 | Examples 322 | 323 | --mystring=vermouth 324 | --mystring="blind jazz singers" 325 | --mystring vermouth 326 | --mystring "blind jazz singers" 327 | 328 | ##### Parameters: 329 | 330 | - ***string*** `$name` - The name of the long-flag to define 331 | - ***mixed*** `$value` - The default value which if null marks the flag required 332 | - ***string*** `$usage` - The usage description 333 | 334 | ##### Returns: 335 | 336 | - ***mixed*** - A reference to the flags value 337 | 338 | --- 339 | 340 | #### Method: Flags->getDefaults 341 | 342 | ```php 343 | function getDefaults() 344 | ``` 345 | 346 | Returns the default values of all defined command-line flags as a formatted string. 347 | 348 | ##### Example 349 | 350 | ``` 351 | -v Output in verbose mode 352 | --testsuite [string] Which test suite to run. 353 | --bootstrap [string] A "bootstrap" PHP file that is run before the specs. 354 | --help Display this help message. 355 | --version Display this applications version. 356 | ``` 357 | 358 | ##### Returns: 359 | 360 | - ***string*** 361 | 362 | --- 363 | 364 | #### Method: Flags->parse 365 | 366 | ```php 367 | function parse([ ?array $args = null [, $ignoreExceptions = false [, $skipFirstArgument = null]]]) 368 | ``` 369 | 370 | Parses flag definitions from the argument list, which should include the command name. 371 | 372 | Must be called after all flags are defined and before flags are accessed by the program. 373 | 374 | Will throw exceptions on Missing Require Flags, Unknown Flags or Incorrect Flag Types 375 | 376 | ##### Parameters: 377 | 378 | - ***array*** | ***null*** `$args` - The arguments to parse. Defaults to arguments defined in the constructor. 379 | - ***bool*** `$ignoreExceptions` - Setting to true causes parsing to continue even after an exception has been 380 | thrown. 381 | - ***bool*** `$skipFirstArgument` - Option to parse the first argument as an parameter rather than the command. 382 | Defaults to constructor value 383 | 384 | **Throws**: `\donatj\Exceptions\MissingFlagParamException` 385 | 386 | **Throws**: `\donatj\Exceptions\InvalidFlagParamException` 387 | 388 | **Throws**: `\donatj\Exceptions\InvalidFlagTypeException` 389 | 390 | --- 391 | 392 | #### Method: Flags->parsed 393 | 394 | ```php 395 | function parsed() 396 | ``` 397 | 398 | Returns true if the command-line flags have been parsed. 399 | 400 | ##### Returns: 401 | 402 | - ***bool*** -------------------------------------------------------------------------------- /src/Flags.php: -------------------------------------------------------------------------------- 1 | args = $args; 57 | $this->skipFirstArgument = $skipFirstArgument; 58 | } 59 | 60 | /** 61 | * Returns the n'th command-line argument. `arg(0)` is the first remaining argument after flags have been processed. 62 | * 63 | * @param int $index 64 | * @return string 65 | */ 66 | public function arg( $index ) { 67 | return isset($this->arguments[$index]) ? $this->arguments[$index] : null; 68 | } 69 | 70 | /** 71 | * Returns the non-flag command-line arguments. 72 | * 73 | * @return string[] Array of argument strings 74 | */ 75 | public function args() { 76 | return $this->arguments; 77 | } 78 | 79 | /** 80 | * Returns an array of short-flag call-counts indexed by character 81 | * 82 | * `-v` would set the 'v' index to 1, whereas `-vvv` will set the 'v' index to 3 83 | * 84 | * @return array 85 | */ 86 | public function shorts() { 87 | $out = []; 88 | foreach( $this->definedShortFlags as $key => $data ) { 89 | $out[$key] = $data[self::DEF_VALUE]; 90 | } 91 | 92 | return $out; 93 | } 94 | 95 | /** 96 | * Returns an array of long-flag values indexed by flag name 97 | * 98 | * @return array 99 | */ 100 | public function longs() { 101 | $out = []; 102 | foreach( $this->definedFlags as $key => $data ) { 103 | $out[$key] = $data[self::DEF_VALUE]; 104 | } 105 | 106 | return $out; 107 | } 108 | 109 | /** 110 | * Defines a short-flag of specified name, and usage string. 111 | * The return value is a reference to an integer variable that stores the number of times the short-flag was called. 112 | * 113 | * This means the value of the reference for v would be the following. 114 | * 115 | * -v => 1 116 | * -vvv => 3 117 | * 118 | * @param string $letter The character of the short-flag to define 119 | * @param string $usage The usage description 120 | * @return int 121 | */ 122 | public function &short( $letter, $usage = '' ) { 123 | $this->definedShortFlags[$letter[0]] = [ 124 | self::DEF_VALUE => 0, 125 | self::DEF_USAGE => $usage, 126 | ]; 127 | 128 | return $this->definedShortFlags[$letter[0]]['value']; 129 | } 130 | 131 | /** 132 | * Defines a bool long-flag of specified name, default value, and usage string. 133 | * The return value is a reference to a variable that stores the value of the flag. 134 | * 135 | * Examples: 136 | * 137 | * Truth-y: 138 | * 139 | * --mybool=[true|t|1] 140 | * --mybool [true|t|1] 141 | * --mybool 142 | * 143 | * False-y: 144 | * 145 | * --mybool=[false|f|0] 146 | * --mybool [false|f|0] 147 | * [not calling --mybool and having the default false] 148 | * 149 | * @param string $name The name of the long-flag to define 150 | * @param mixed $value The default value - usually false for bool - which if null marks the flag required 151 | * @param string $usage The usage description 152 | * @return mixed A reference to the flags value 153 | */ 154 | public function &bool( $name, $value = null, $usage = '' ) { 155 | return $this->storeFlag(self::TYPE_BOOL, $name, $value, $usage); 156 | } 157 | 158 | /** 159 | * Defines a float long-flag of specified name, default value, and usage string. 160 | * The return value is a reference to a variable that stores the value of the flag. 161 | * 162 | * Examples: 163 | * 164 | * --myfloat=1.1 165 | * --myfloat 1.1 166 | * 167 | * @param string $name The name of the long-flag to define 168 | * @param mixed $value The default value which if null marks the flag required 169 | * @param string $usage The usage description 170 | * @return mixed A reference to the flags value 171 | */ 172 | public function &float( $name, $value = null, $usage = '' ) { 173 | return $this->storeFlag(self::TYPE_FLOAT, $name, $value, $usage); 174 | } 175 | 176 | /** 177 | * Defines an integer long-flag of specified name, default value, and usage string. 178 | * The return value is a reference to a variable that stores the value of the flag. 179 | * 180 | * Note: Float values trigger an error, rather than casting. 181 | * 182 | * Examples: 183 | * 184 | * --myinteger=1 185 | * --myinteger 1 186 | * 187 | * @param string $name The name of the long-flag to define 188 | * @param mixed $value The default value which if null marks the flag required 189 | * @param string $usage The usage description 190 | * @return mixed A reference to the flags value 191 | */ 192 | public function &int( $name, $value = null, $usage = '' ) { 193 | return $this->storeFlag(self::TYPE_INT, $name, $value, $usage); 194 | } 195 | 196 | /** 197 | * Defines a unsigned integer long-flag of specified name, default value, and usage string. 198 | * The return value is a reference to a variable that stores the value of the flag. 199 | * 200 | * Note: Negative values trigger an error, rather than casting. 201 | * 202 | * Examples: 203 | * 204 | * --myinteger=1 205 | * --myinteger 1 206 | * 207 | * @param string $name The name of the long-flag to define 208 | * @param mixed $value The default value which if null marks the flag required 209 | * @param string $usage The usage description 210 | * @return mixed A reference to the flags value 211 | */ 212 | public function &uint( $name, $value = null, $usage = '' ) { 213 | return $this->storeFlag(self::TYPE_UINT, $name, $value, $usage); 214 | } 215 | 216 | /** 217 | * Defines a string long-flag of specified name, default value, and usage string. 218 | * The return value is a reference to a variable that stores the value of the flag. 219 | * 220 | * Examples 221 | * 222 | * --mystring=vermouth 223 | * --mystring="blind jazz singers" 224 | * --mystring vermouth 225 | * --mystring "blind jazz singers" 226 | * 227 | * @param string $name The name of the long-flag to define 228 | * @param mixed $value The default value which if null marks the flag required 229 | * @param string $usage The usage description 230 | * @return mixed A reference to the flags value 231 | */ 232 | public function &string( $name, $value = null, $usage = '' ) { 233 | return $this->storeFlag(self::TYPE_STRING, $name, $value, $usage); 234 | } 235 | 236 | /** 237 | * @param string $type 238 | * @param string $name 239 | * @param mixed $value 240 | * @param string $usage 241 | * @return mixed 242 | */ 243 | private function &storeFlag( $type, $name, $value, $usage ) { 244 | 245 | $this->definedFlags[$name] = [ 246 | self::DEF_TYPE => $type, 247 | self::DEF_USAGE => $usage, 248 | self::DEF_REQUIRED => $value === null, 249 | self::DEF_VALUE => $value, 250 | ]; 251 | 252 | return $this->definedFlags[$name][self::DEF_VALUE]; 253 | } 254 | 255 | /** 256 | * Returns the default values of all defined command-line flags as a formatted string. 257 | * 258 | * Example: 259 | * 260 | * ``` 261 | * -v Output in verbose mode 262 | * --testsuite [string] Which test suite to run. 263 | * --bootstrap [string] A "bootstrap" PHP file that is run before the specs. 264 | * --help Display this help message. 265 | * --version Display this applications version. 266 | * ``` 267 | * 268 | * @return string 269 | */ 270 | public function getDefaults() { 271 | 272 | $output = ''; 273 | $final = []; 274 | $max = 0; 275 | 276 | foreach( $this->definedShortFlags as $char => $data ) { 277 | $final["-{$char}"] = $data[self::DEF_USAGE]; 278 | } 279 | 280 | foreach( $this->definedFlags as $flag => $data ) { 281 | $key = "--{$flag}"; 282 | $final[$key] = ($data[self::DEF_REQUIRED] ? 283 | "<{$data[self::DEF_TYPE]}> " : 284 | ($data[self::DEF_TYPE] == self::TYPE_BOOL ? 285 | '' : 286 | "[{$data[self::DEF_TYPE]}] " 287 | )) . $data[self::DEF_USAGE]; 288 | $max = max($max, strlen($key)); 289 | } 290 | 291 | foreach( $final as $flag => $usage ) { 292 | $output .= sprintf('%' . ($max + 5) . 's', $flag) . " {$usage}" . PHP_EOL; 293 | } 294 | 295 | return $output; 296 | } 297 | 298 | /** 299 | * Parses flag definitions from the argument list, which should include the command name. 300 | * Must be called after all flags are defined and before flags are accessed by the program. 301 | * 302 | * Will throw exceptions on Missing Require Flags, Unknown Flags or Incorrect Flag Types 303 | * 304 | * @param array|null $args The arguments to parse. Defaults to arguments defined in the constructor. 305 | * @param bool $ignoreExceptions Setting to true causes parsing to continue even after an exception has been 306 | * thrown. 307 | * @param bool $skipFirstArgument Option to parse the first argument as an parameter rather than the command. 308 | * Defaults to constructor value 309 | * @throws Exceptions\MissingFlagParamException 310 | * @throws Exceptions\InvalidFlagParamException 311 | * @throws Exceptions\InvalidFlagTypeException 312 | */ 313 | public function parse( ?array $args = null, $ignoreExceptions = false, $skipFirstArgument = null ) { 314 | if( $args === null ) { 315 | $args = $this->args; 316 | } 317 | 318 | if( $skipFirstArgument === null ) { 319 | $skipFirstArgument = $this->skipFirstArgument; 320 | } 321 | 322 | if( $skipFirstArgument ) { 323 | array_shift($args); 324 | } 325 | 326 | [ $longParams, $shortParams, $this->arguments ] = $this->splitArguments($args, $this->definedFlags); 327 | 328 | foreach( $longParams as $name => $value ) { 329 | if( !isset($this->definedFlags[$name]) ) { 330 | if( !$ignoreExceptions ) { 331 | throw new InvalidFlagParamException('Unknown option: --' . $name); 332 | } 333 | } else { 334 | $defined_flag =& $this->definedFlags[$name]; 335 | 336 | if( $this->validateType($defined_flag[self::DEF_TYPE], $value) ) { 337 | $defined_flag[self::DEF_VALUE] = $value; 338 | $defined_flag[self::DEF_PARSED] = true; 339 | } else { 340 | if( !$ignoreExceptions ) { 341 | throw new InvalidFlagTypeException('Option --' . $name . ' expected type: "' . $defined_flag[self::DEF_TYPE] . '"'); 342 | } 343 | } 344 | } 345 | } 346 | 347 | foreach( $shortParams as $char => $value ) { 348 | if( !isset($this->definedShortFlags[$char]) ) { 349 | if( !$ignoreExceptions ) { 350 | throw new InvalidFlagParamException('Unknown option: -' . $char); 351 | } 352 | } else { 353 | $this->definedShortFlags[$char][self::DEF_VALUE] = $value; 354 | } 355 | } 356 | 357 | foreach( $this->definedFlags as $name => $data ) { 358 | if( ($data[self::DEF_VALUE] === null) && !$ignoreExceptions ) { 359 | throw new MissingFlagParamException('Expected option --' . $name . ' missing.'); 360 | } 361 | } 362 | 363 | $this->parsed = true; 364 | } 365 | 366 | /** 367 | * Returns true if the command-line flags have been parsed. 368 | * 369 | * @return bool 370 | */ 371 | public function parsed() { 372 | return $this->parsed; 373 | } 374 | 375 | /** 376 | * @param string $type 377 | * @param mixed $value 378 | * @return bool 379 | */ 380 | private function validateType( $type, &$value ) : bool { 381 | $validate = [ 382 | self::TYPE_BOOL => function ( &$val ) { 383 | $val = strtolower((string)$val); 384 | if( $val == '0' || $val == 'f' || $val == 'false' ) { 385 | $val = false; 386 | 387 | return true; 388 | } 389 | if( $val == '1' || $val == 't' || $val == 'true' ) { 390 | $val = true; 391 | 392 | return true; 393 | } 394 | 395 | return false; 396 | }, 397 | self::TYPE_UINT => function ( &$val ) { 398 | if( abs((float)$val) == (int)$val ) { 399 | $val = (int)$val; 400 | 401 | return true; 402 | } 403 | 404 | return false; 405 | }, 406 | self::TYPE_INT => function ( &$val ) { 407 | if( is_numeric($val) && floatval($val) == intval($val) ) { 408 | $val = intval($val); 409 | 410 | return true; 411 | } 412 | 413 | return false; 414 | }, 415 | self::TYPE_FLOAT => function ( &$val ) { 416 | if( is_numeric($val) ) { 417 | $val = floatval($val); 418 | 419 | return true; 420 | } 421 | 422 | return false; 423 | }, 424 | self::TYPE_STRING => function ( &$val ) { 425 | if( $val !== true ) { 426 | $val = (string)$val; 427 | 428 | return true; 429 | } 430 | 431 | return false; 432 | }, 433 | ]; 434 | 435 | $test = $validate[$type]; 436 | 437 | return $test($value); 438 | } 439 | 440 | /** 441 | * @param array $args 442 | * @param array $definedFlags 443 | * @return array 444 | */ 445 | protected function splitArguments( array $args, array $definedFlags ) : array { 446 | $longParams = []; 447 | $shortParams = []; 448 | $arguments = []; 449 | 450 | $forceValue = false; 451 | $getValue = false; 452 | $startArgs = false; 453 | foreach( $args as $arg ) { 454 | if( isset($arg[0]) && $arg[0] == '-' && !$startArgs && !$forceValue && $arg !== '-' ) { 455 | $cleanArg = ltrim($arg, '- '); 456 | 457 | if( $getValue ) { 458 | $longParams[$getValue] = true; 459 | } 460 | 461 | $getValue = false; 462 | 463 | if( $arg === '--' ) { 464 | $startArgs = true; 465 | } elseif( isset($arg[1]) && $arg[1] === '-' ) { 466 | $split = explode('=', $arg, 2); 467 | 468 | if( count($split) > 1 ) { 469 | $longParams[ltrim(reset($split), '- ')] = end($split); 470 | } else { 471 | $getValue = $cleanArg; 472 | 473 | if( isset($definedFlags[$cleanArg]) && $definedFlags[$cleanArg][self::DEF_TYPE] != self::TYPE_BOOL ) { 474 | $forceValue = true; 475 | } 476 | } 477 | } else { 478 | $split = str_split($cleanArg); 479 | foreach( $split as $char ) { 480 | $shortParams[$char] = isset($shortParams[$char]) ? $shortParams[$char] + 1 : 1; 481 | } 482 | } 483 | } elseif( ($getValue !== false && !$startArgs) || $forceValue ) { 484 | $longParams[$getValue] = $arg; 485 | $getValue = false; 486 | $forceValue = false; 487 | } else { 488 | $arguments[] = $arg; 489 | } 490 | } 491 | 492 | if( $getValue ) { 493 | $longParams[$getValue] = true; 494 | 495 | return [ $longParams, $shortParams, $arguments ]; 496 | } 497 | 498 | return [ $longParams, $shortParams, $arguments ]; 499 | } 500 | 501 | } 502 | -------------------------------------------------------------------------------- /test/FlagsTest.php: -------------------------------------------------------------------------------- 1 | bool('bool'); 16 | 17 | $flags->parse(explode(' ', 'test.php --bool')); 18 | $this->assertTrue($bool); 19 | 20 | $options = [ 0 => [ 'true', 't', '1' ], 1 => [ 'false', 'f', '0' ] ]; 21 | 22 | foreach( [ '=', ' ' ] as $sep ) { 23 | foreach( $options as $bool => $values ) { 24 | foreach( $values as $value ) { 25 | $flags->parse(explode(' ', 'test.php --bool' . $sep . $value)); 26 | $this->assertSame($bool, (bool)$bool); 27 | } 28 | } 29 | } 30 | 31 | $flags->parse(explode(' ', 'test.php --bool -- argument')); 32 | $this->assertTrue($bool); 33 | } 34 | 35 | public function testBoolException() : void { 36 | $this->expectException(InvalidFlagTypeException::class); 37 | 38 | $flags = new Flags(); 39 | $flags->bool('bool'); 40 | $flags->parse(explode(' ', 'test.php --bool=10')); 41 | } 42 | 43 | public function testBoolException2() : void { 44 | $this->expectException(InvalidFlagTypeException::class); 45 | 46 | $flags = new Flags(); 47 | $flags->bool('bool'); 48 | $flags->parse(explode(' ', 'test.php --bool string')); 49 | } 50 | 51 | public function testFloat() : void { 52 | $flags = new Flags(); 53 | $uint = &$flags->float('float'); 54 | 55 | $values = [ 4, 4.2, '4.', '-.4', '4.0', .4, -4, 0, 1000, '-012' ]; 56 | 57 | foreach( [ '=', ' ' ] as $sep ) { 58 | foreach( $values as $value ) { 59 | $flags->parse(explode(' ', 'test.php --float' . $sep . $value)); 60 | $this->assertSame($uint, floatval($value)); 61 | } 62 | } 63 | } 64 | 65 | public function testFloatException() : void { 66 | $this->expectException(InvalidFlagTypeException::class); 67 | 68 | $flags = new Flags(); 69 | $flags->float('float'); 70 | $flags->parse(explode(' ', 'test.php --float=spiders')); 71 | } 72 | 73 | public function testFloatException2() : void { 74 | $this->expectException(InvalidFlagTypeException::class); 75 | 76 | $flags = new Flags(); 77 | $flags->float('float'); 78 | $flags->parse(explode(' ', 'test.php --float')); 79 | } 80 | 81 | public function testInt() : void { 82 | $flags = new Flags(); 83 | $int = &$flags->int('int'); 84 | 85 | $values = [ 4, '4.', '4.0', -4, 0, 1000, '-012' ]; 86 | 87 | foreach( [ '=', ' ' ] as $sep ) { 88 | foreach( $values as $value ) { 89 | $flags->parse(explode(' ', 'test.php --int' . $sep . $value)); 90 | $this->assertSame($int, intval($value)); 91 | } 92 | } 93 | } 94 | 95 | public function testIntException() : void { 96 | $this->expectException(InvalidFlagTypeException::class); 97 | 98 | $flags = new Flags(); 99 | $flags->int('int'); 100 | $flags->parse(explode(' ', 'test.php --int=spiders')); 101 | } 102 | 103 | public function testIntException2() : void { 104 | $this->expectException(InvalidFlagTypeException::class); 105 | 106 | $flags = new Flags(); 107 | $flags->int('int'); 108 | $flags->parse(explode(' ', 'test.php --int=1.1')); 109 | } 110 | 111 | public function testIntException3() : void { 112 | $this->expectException(InvalidFlagTypeException::class); 113 | 114 | $flags = new Flags(); 115 | $flags->int('int'); 116 | $flags->parse(explode(' ', 'test.php --int')); 117 | } 118 | 119 | public function testUint() : void { 120 | $flags = new Flags(); 121 | $bool = &$flags->uint('uint'); 122 | 123 | $values = [ 4, '4.', '4.0', 0, 1000, 12 ]; 124 | 125 | foreach( [ '=', ' ' ] as $sep ) { 126 | foreach( $values as $value ) { 127 | $flags->parse(explode(' ', 'test.php --uint' . $sep . $value)); 128 | $this->assertSame($bool, abs(intval($value))); 129 | } 130 | } 131 | } 132 | 133 | public function testUintException() : void { 134 | $this->expectException(InvalidFlagTypeException::class); 135 | 136 | $flags = new Flags(); 137 | $flags->uint('uint'); 138 | $flags->parse(explode(' ', 'test.php --uint=-2')); 139 | } 140 | 141 | public function testUintException2() : void { 142 | $this->expectException(InvalidFlagTypeException::class); 143 | 144 | $flags = new Flags(); 145 | $flags->uint('uint'); 146 | $flags->parse(explode(' ', 'test.php --uint=2.2')); 147 | } 148 | 149 | public function testString() : void { 150 | $flags = new Flags(); 151 | $string = &$flags->string('string'); 152 | $values = [ 4, '4.', '4.0', 0, 1000, 12, "what", "funky fresh", "hot=dog" ]; 153 | 154 | foreach( $values as $value ) { 155 | $flags->parse([ 'test.php', '--string=' . $value ]); 156 | $this->assertSame($string, strval($value)); 157 | 158 | $flags->parse([ 'test.php', '--string', $value ]); 159 | $this->assertSame($string, strval($value)); 160 | } 161 | } 162 | 163 | public function testStringException() : void { 164 | $this->expectException(InvalidFlagTypeException::class); 165 | 166 | $flags = new Flags(); 167 | $flags->string('string'); 168 | $flags->parse(explode(' ', 'test.php --string')); 169 | } 170 | 171 | public static function parseProvider() : array { 172 | return [ 173 | [ 'test.php --sponges=false --help -v argument1 --pie 59 argument2 --what=14 -vv --int1 7 --int2=-4 --last -- --argument_that_looks_like_a_param', true ], 174 | [ '--sponges=false --help -v argument1 --pie 59 argument2 --what=14 -vv --int1 7 --int2=-4 --last -- --argument_that_looks_like_a_param', false ], 175 | ]; 176 | } 177 | 178 | /** 179 | * @dataProvider parseProvider 180 | */ 181 | public function testParse( string $arguments, bool $skipFirst ) : void { 182 | $argParts = explode(' ', $arguments); 183 | 184 | foreach( [ 0, 1, 2 ] as $useConstructor ) { 185 | if( $useConstructor === 0 ) { 186 | $flags = new Flags($argParts, $skipFirst); 187 | } elseif( $useConstructor === 1 ) { 188 | $flags = new Flags(); 189 | } elseif( $useConstructor === 2 ) { 190 | $flags = new Flags([ '--this=is', 'trash', 'data' ], !$skipFirst); 191 | } 192 | 193 | $sponges = &$flags->bool('sponges'); 194 | $what = &$flags->uint('what'); 195 | $int1 = &$flags->int('int1'); 196 | $int2 = &$flags->int('int2'); 197 | $pie = &$flags->string('pie'); 198 | $cat = &$flags->string('cat', 'Maine Coon'); 199 | $help = &$flags->bool('help', false); 200 | $last = &$flags->bool('last', false); 201 | $verbose = &$flags->short('v'); 202 | $all = &$flags->short('a'); 203 | 204 | if( $useConstructor !== 0 ) { 205 | $flags->parse($argParts, false, $skipFirst); 206 | } else { 207 | $flags->parse(); 208 | } 209 | 210 | $longs = $flags->longs(); 211 | $this->assertFalse($sponges); 212 | $this->assertFalse($longs['sponges']); 213 | $this->assertSame($what, 14); 214 | $this->assertSame($longs['what'], 14); 215 | $this->assertSame($int1, 7); 216 | $this->assertSame($longs['int1'], 7); 217 | $this->assertSame($int2, -4); 218 | $this->assertSame($longs['int2'], -4); 219 | $this->assertSame($pie, '59'); 220 | $this->assertSame($longs['pie'], '59'); 221 | $this->assertSame($cat, 'Maine Coon'); 222 | $this->assertSame($longs['cat'], 'Maine Coon'); 223 | $this->assertTrue($help); 224 | $this->assertTrue($longs['help']); 225 | $this->assertTrue($last); 226 | $this->assertTrue($longs['last']); 227 | 228 | $shorts = $flags->shorts(); 229 | $this->assertSame($verbose, 3); 230 | $this->assertSame($shorts['v'], 3); 231 | $this->assertSame($all, 0); 232 | $this->assertSame($shorts['a'], 0); 233 | 234 | $this->assertEquals([ 235 | 0 => 'argument1', 236 | 1 => 'argument2', 237 | 2 => '--argument_that_looks_like_a_param', 238 | ], $flags->args()); 239 | 240 | $this->assertSame($flags->arg(0), 'argument1'); 241 | $this->assertSame($flags->arg(1), 'argument2'); 242 | $this->assertSame($flags->arg(2), '--argument_that_looks_like_a_param'); 243 | $this->assertNull($flags->arg(3)); 244 | } 245 | } 246 | 247 | public function testParse2() { 248 | $flags = new Flags(); 249 | $capx = &$flags->short('X'); 250 | $lowerx = &$flags->short('x'); 251 | $a = &$flags->short('a'); 252 | $s = &$flags->short('s'); 253 | $d = &$flags->short('d'); 254 | $qm = &$flags->short('?'); 255 | 256 | $flags->parse(explode(' ', 'main.php -Xassd?')); 257 | 258 | $this->assertSame($capx, 1); 259 | $this->assertSame($lowerx, 0); 260 | $this->assertSame($a, 1); 261 | $this->assertSame($s, 2); 262 | $this->assertSame($d, 1); 263 | $this->assertSame($qm, 1); 264 | 265 | $this->assertEquals([], $flags->longs()); 266 | 267 | $this->assertEquals([ 'X' => 1, 'x' => 0, 'a' => 1, 's' => 2, 'd' => 1, '?' => 1 ], $flags->shorts()); 268 | 269 | # ==== 270 | 271 | $_SERVER['argv'] = [ 'test.php', '--a', '7' ]; 272 | $flags = new Flags(); 273 | $a = &$flags->int('a'); 274 | $flags->parse(); 275 | 276 | $this->assertSame($a, 7); 277 | } 278 | 279 | public function testParsed() : void { 280 | $flags = new Flags(); 281 | $this->assertFalse($flags->parsed()); 282 | 283 | try { 284 | $flags->parse(explode(' ', 'test.php --failtoparse=true')); 285 | $this->fail('An exception should have been thrown.'); 286 | } catch( \Exception $e ) { 287 | //This is expected to fail. We're simply making sure it didn't parse. 288 | } 289 | 290 | $this->assertSame(false, $flags->parsed()); 291 | 292 | $flags->parse(explode(' ', 'test.php a b c')); 293 | $this->assertSame(true, $flags->parsed()); 294 | } 295 | 296 | public function testParseExceptionInvalidFlagParamException() : void { 297 | $this->expectException(InvalidFlagParamException::class); 298 | 299 | $flags = new Flags(); 300 | $flags->parse(explode(' ', 'test.php --fake=what')); 301 | } 302 | 303 | public function testNotParseExceptionInvalidFlagParamException() : void { 304 | $this->expectNotToPerformAssertions(); 305 | 306 | $flags = new Flags(); 307 | $flags->parse(explode(' ', 'test.php --fake=what'), true); 308 | } 309 | 310 | public function testParseExceptionInvalidFlagParamException2() : void { 311 | $this->expectException(InvalidFlagParamException::class); 312 | 313 | $flags = new Flags(); 314 | $flags->parse(explode(' ', 'test.php --fake what')); 315 | } 316 | 317 | public function testNotParseExceptionInvalidFlagParamException2() : void { 318 | $this->expectNotToPerformAssertions(); 319 | 320 | $flags = new Flags(); 321 | $flags->parse(explode(' ', 'test.php --fake what'), true); 322 | } 323 | 324 | public function testParseExceptionInvalidFlagParamException3() : void { 325 | $this->expectException(InvalidFlagParamException::class); 326 | 327 | $flags = new Flags(); 328 | $flags->parse(explode(' ', 'test.php --fake')); 329 | } 330 | 331 | public function testNotParseExceptionInvalidFlagParamException3() : void { 332 | $this->expectNotToPerformAssertions(); 333 | 334 | $flags = new Flags(); 335 | $flags->parse(explode(' ', 'test.php --fake'), true); 336 | } 337 | 338 | public function testParseExceptionInvalidFlagParamException4() : void { 339 | $this->expectException(InvalidFlagParamException::class); 340 | 341 | $flags = new Flags(); 342 | $flags->parse(explode(' ', 'test.php -fake')); 343 | } 344 | 345 | public function testNotParseExceptionInvalidFlagParamException4() : void { 346 | $this->expectNotToPerformAssertions(); 347 | 348 | $flags = new Flags(); 349 | $flags->parse(explode(' ', 'test.php -fake'), true); 350 | } 351 | 352 | public function testParseExceptionInvalidFlagParamException5() : void { 353 | $this->expectException(InvalidFlagParamException::class); 354 | 355 | $flags = new Flags(); 356 | $flags->parse(explode(' ', 'test.php -v')); 357 | } 358 | 359 | public function testNotParseExceptionInvalidFlagParamException5() : void { 360 | $this->expectNotToPerformAssertions(); 361 | 362 | $flags = new Flags(); 363 | $flags->parse(explode(' ', 'test.php -v'), true); 364 | } 365 | 366 | public function testParseExceptionMissingFlagParamException() : void { 367 | $this->expectException(MissingFlagParamException::class); 368 | 369 | $flags = new Flags(); 370 | $flags->string('blah'); 371 | $flags->parse(explode(' ', 'test.php')); 372 | } 373 | 374 | public function testNotParseExceptionMissingFlagParamException() : void { 375 | $this->expectNotToPerformAssertions(); 376 | 377 | $flags = new Flags(); 378 | $flags->string('blah'); 379 | $flags->parse(explode(' ', 'test.php'), true); 380 | } 381 | 382 | public function testParseExceptionMissingFlagParamException2() : void { 383 | $this->expectException(MissingFlagParamException::class); 384 | 385 | $flags = new Flags(); 386 | $flags->bool('foo'); 387 | $flags->bool('bar'); 388 | $flags->parse(explode(' ', 'test.php --foo')); 389 | } 390 | 391 | public function testNotParseExceptionMissingFlagParamException2() : void { 392 | $this->expectNotToPerformAssertions(); 393 | 394 | $flags = new Flags(); 395 | $flags->bool('foo'); 396 | $flags->bool('bar'); 397 | $flags->parse(explode(' ', 'test.php --foo'), true); 398 | } 399 | 400 | public function testGetDefaults() : void { 401 | 402 | $flags = new Flags(); 403 | 404 | $longs = [ 405 | [ 'bool', 'foo', false, 'Enable the foos' ], 406 | [ 'string', 'bar', 'string', 'Baz text to display' ], 407 | [ 'int', 'baz', -10, 'How many bazs' ], 408 | [ 'uint', 'quux', 10, 'How many quuxi' ], 409 | [ 'float', 'thud', 10.1, 'How many thuds' ], 410 | 411 | [ 'bool', 'xfoo', null, 'Enable the foos' ], 412 | [ 'string', 'xbar', null, 'Baz text to display' ], 413 | [ 'int', 'xbaz', null, 'How many bazs' ], 414 | [ 'uint', 'xquux', null, 'How many quuxi' ], 415 | [ 'float', 'xthud', null, 'How many thuds' ], 416 | ]; 417 | 418 | foreach( $longs as $data ) { 419 | $flags->{$data[0]}($data[1], $data[2], $data[3]); 420 | } 421 | 422 | $shorts = [ 423 | [ 'v', 'verbosity, more v\'s = more verbose' ], 424 | [ 'a', 'verbosity, more v\'s = more verbose' ], 425 | ]; 426 | 427 | foreach( $shorts as $data ) { 428 | $flags->short($data[0], $data[1]); 429 | } 430 | 431 | $longCount = 0; 432 | $shortCount = 0; 433 | 434 | preg_match_all('/^\s*(?P-)?-(?P[a-z]+)\s+(?:(?P\[[a-z]+\])|(?P<[a-z]+>))?\s+(?P.*)/m', $flags->getDefaults(), $result, PREG_PATTERN_ORDER); 435 | for( $i = 0; $i < count($result[0]); $i += 1 ) { 436 | if( $result['long'][$i] === '-' ) { 437 | $longCount++; 438 | 439 | foreach( $longs as $long ) { 440 | if( $long[1] == $result['key'][$i] ) { 441 | if( $long[2] !== null ) { 442 | $this->assertTrue(isset($result['optional'][$i][0]) || $long[0] == 'bool'); 443 | } else { 444 | $this->assertTrue(isset($result['required'][$i][0]) || $long[0] == 'bool'); 445 | } 446 | 447 | $this->assertEquals($long[3], $result['msg'][$i]); 448 | break; 449 | } 450 | } 451 | } else { 452 | $shortCount++; 453 | 454 | foreach( $shorts as $short ) { 455 | if( $short[0] == $result['key'][$i] ) { 456 | $this->assertEquals($short[1], $result['msg'][$i]); 457 | 458 | break; 459 | } 460 | } 461 | } 462 | } 463 | 464 | $this->assertEquals(count($longs), $longCount); 465 | $this->assertEquals(count($shorts), $shortCount); 466 | } 467 | 468 | /** 469 | * Test that an empty string e.g. `foo.php --foo ""` does not explode 470 | */ 471 | public function testEmptyStringValueRegression() : void { 472 | $flags = new Flags(); 473 | 474 | $foo =& $flags->string('foo', 'test'); 475 | $flags->parse([ 'souplex', '--foo', '' ]); 476 | 477 | $this->assertSame($foo, ''); 478 | } 479 | 480 | public function testHandleArgumentSingleDash() : void { 481 | $flags = new Flags(); 482 | 483 | $flags->string('foo', 'test'); 484 | $flags->parse([ 'fooplex', '-', 'sass' ]); 485 | 486 | $this->assertSame($flags->args(), [ '-', 'sass' ]); 487 | } 488 | 489 | } 490 | --------------------------------------------------------------------------------