Skip to content

Commit 93c4f06

Browse files
committed
feat: add more cli flag help method
1 parent 02af7b6 commit 93c4f06

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed

Diff for: src/Helper/FlagHelper.php

+262
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,271 @@
99

1010
namespace Toolkit\Cli\Helper;
1111

12+
use function array_map;
13+
use function explode;
14+
use function implode;
15+
use function is_numeric;
16+
use function ltrim;
17+
use function strlen;
18+
1219
/**
1320
* class FlagHelper
1421
*/
1522
class FlagHelper extends CliHelper
1623
{
24+
/**
25+
* @param array $names
26+
*
27+
* @return string
28+
*/
29+
public static function buildOptHelpName(array $names): string
30+
{
31+
$nodes = array_map(static function (string $name) {
32+
return (strlen($name) > 1 ? '--' : '-') . $name;
33+
}, $names);
34+
35+
return implode(', ', $nodes);
36+
}
37+
38+
/**
39+
* check input is valid option value
40+
*
41+
* @param mixed $val
42+
*
43+
* @return bool
44+
*/
45+
public static function isOptionValue(mixed $val): bool
46+
{
47+
if ($val === false) {
48+
return false;
49+
}
50+
51+
// if is: '', 0 || is not option name
52+
if (!$val || $val[0] !== '-') {
53+
return true;
54+
}
55+
56+
// ensure is option value.
57+
if (!str_contains($val, '=')) {
58+
return true;
59+
}
60+
61+
// is string value, but contains '='
62+
[$name,] = explode('=', $val, 2);
63+
64+
// named argument OR invalid: 'some = string'
65+
return false === self::isValidName($name);
66+
}
67+
68+
/**
69+
* check and get option name
70+
*
71+
* valid:
72+
* `-a`
73+
* `-b=value`
74+
* `--long`
75+
* `--long=value1`
76+
*
77+
* invalid:
78+
* - empty string
79+
* - no prefix '-' (is argument)
80+
* - invalid option name as argument. eg: '-9' '--34' '- '
81+
*
82+
* @param string $val
83+
*
84+
* @return string
85+
*/
86+
public static function filterOptionName(string $val): string
87+
{
88+
// is not an option.
89+
if ('' === $val || $val[0] !== '-') {
90+
return '';
91+
}
92+
93+
$name = ltrim($val, '- ');
94+
if (is_numeric($name)) {
95+
return '';
96+
}
97+
98+
return $name;
99+
}
100+
101+
/**
102+
* Parses $GLOBALS['argv'] for parameters and assigns them to an array.
103+
*
104+
* **NOTICE**: this is a very simple implements, recommend use package: toolkit/pflag
105+
*
106+
* eg:
107+
*
108+
* ```bash
109+
* php cli.php run name=john city=chengdu -s=test --page=23 -d -rf --debug --task=off -y=false -D -e dev -v vvv
110+
* ```
111+
*
112+
* Usage:
113+
*
114+
* ```php
115+
* $argv = $_SERVER['argv'];
116+
* // notice: must shift first element.
117+
* $script = \array_shift($argv);
118+
*
119+
* [$args, $sOpts, $lOpts] = FlagHelper::parseArgv($argv);
120+
* ```
121+
*
122+
* Supports opts style:
123+
*
124+
* ```bash
125+
* -e
126+
* -e <value>
127+
* -e=<value>
128+
* --long-opt
129+
* --long-opt <value>
130+
* --long-opt=<value>
131+
* ```
132+
*
133+
* Supports args style:
134+
*
135+
* ```bash
136+
* <value>
137+
* arg=<value>
138+
* ```
139+
*
140+
* @link http://php.net/manual/zh/function.getopt.php#83414
141+
*
142+
* @param array $params
143+
* @param array{boolOpts:array, arrayOpts:array, mergeOpts: bool} $config
144+
*
145+
* @return array returns like `[args, short-opts, long-opts]`; If 'mergeOpts' is True, will return `[args, opts]`
146+
*/
147+
public static function parseArgv(array $params, array $config = []): array
148+
{
149+
if (!$params) {
150+
return [[], [], []];
151+
}
152+
153+
$config = array_merge([
154+
// List of parameters without values(bool option keys)
155+
'boolOpts' => [], // ['debug', 'h']
156+
// Whether merge short-opts and long-opts
157+
'mergeOpts' => false,
158+
// Only want parsed options.
159+
// if not empty, will ignore no matched
160+
'wantParsedOpts' => [],
161+
// List of option allow array values.
162+
'arrayOpts' => [], // ['names', 'status']
163+
// Special short style
164+
// posix: -abc will expand: -a -b -c
165+
// unix: -abc will expand: -a=bc
166+
'shortStyle' => 'posix',
167+
], $config);
168+
169+
$args = $sOpts = $lOpts = [];
170+
// config
171+
$boolOpts = array_flip((array)$config['boolOpts']);
172+
$arrayOpts = array_flip((array)$config['arrayOpts']);
173+
174+
$optParseEnd = false;
175+
while (false !== ($p = current($params))) {
176+
next($params);
177+
178+
// option parse end, collect remaining arguments.
179+
if ($optParseEnd) {
180+
self::collectArgs($args, $p);
181+
continue;
182+
}
183+
184+
// check is an option name.
185+
if ($pn = self::filterOptionName($p)) {
186+
$value = true;
187+
$isLong = false;
188+
$option = substr($p, 1);
189+
190+
// long-opt: (--<opt>)
191+
if (str_starts_with($option, '-')) {
192+
$option = substr($option, 1);
193+
$isLong = true;
194+
195+
// long-opt: value specified inline (--<opt>=<value>)
196+
if (str_contains($option, '=')) {
197+
[$option, $value] = explode('=', $option, 2);
198+
}
199+
200+
// short-opt: value specified inline (-<opt>=<value>)
201+
} elseif (isset($option[1]) && $option[1] === '=') {
202+
[$option, $value] = explode('=', $option, 2);
203+
}
204+
205+
// check if next parameter is a descriptor or a value
206+
$nxt = current($params);
207+
208+
// next elem is value. fix: allow empty string ''
209+
if ($value === true && !isset($boolOpts[$option]) && self::isOptionValue($nxt)) {
210+
// list(,$val) = each($params);
211+
$value = $nxt;
212+
next($params);
213+
214+
// short-opt: bool opts. like -e -abc
215+
} elseif (!$isLong && $value === true) {
216+
foreach (str_split($option) as $char) {
217+
$sOpts[$char] = true;
218+
}
219+
continue;
220+
}
221+
222+
$value = self::filterBool($value);
223+
$isArray = isset($arrayOpts[$option]);
224+
225+
if ($isLong) {
226+
if ($isArray) {
227+
$lOpts[$option][] = $value;
228+
} else {
229+
$lOpts[$option] = $value;
230+
}
231+
} elseif ($isArray) { // short
232+
$sOpts[$option][] = $value;
233+
} else { // short
234+
$sOpts[$option] = $value;
235+
}
236+
237+
continue;
238+
}
239+
240+
// stop parse options:
241+
// - found '--' will stop parse options
242+
if ($p === '--') {
243+
$optParseEnd = true;
244+
continue;
245+
}
246+
247+
// parse arguments:
248+
// - param doesn't belong to any option, define it is args
249+
self::collectArgs($args, $p);
250+
}
251+
252+
if ($config['mergeOpts']) {
253+
return [$args, array_merge($sOpts, $lOpts)];
254+
}
255+
256+
return [$args, $sOpts, $lOpts];
257+
}
258+
259+
/**
260+
* @param array $args
261+
* @param string $p
262+
*/
263+
private static function collectArgs(array &$args, string $p): void
264+
{
265+
// value specified inline (<arg>=<value>)
266+
if (str_contains($p, '=')) {
267+
[$name, $value] = explode('=', $p, 2);
268+
269+
if (self::isValidName($name)) {
270+
$args[$name] = self::filterBool($value);
271+
} else {
272+
$args[] = $p;
273+
}
274+
} else {
275+
$args[] = $p;
276+
}
277+
}
278+
17279
}

0 commit comments

Comments
 (0)