├── .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 |
31 |
34 |
35 | Here is a simple example script:
36 |
37 |
38 | ]]>
45 |
46 |
47 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flags
2 |
3 | [](https://packagist.org/packages/donatj/flags)
4 | [](https://packagist.org/packages/donatj/flags)
5 | [](https://packagist.org/packages/donatj/flags)
6 | [](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 |
--------------------------------------------------------------------------------