@@ -10,17 +10,24 @@ final class CLIParser {
1010
1111 /**
1212 * @var string[]
13+ * @readonly
1314 * @psalm-var list<string>
1415 */
1516 private array $ args ;
17+ /**
18+ * @readonly
19+ */
20+ private string $ optionsString ;
1621 /**
1722 * @psalm-var null|list<string>|array<string, array{
1823 * filter: int,
1924 * flags?: int,
2025 * options?: array{
2126 * default?: scalar,
2227 * ...
23- * }
28+ * },
29+ * value_label?: string,
30+ * description?: string
2431 * }>
2532 */
2633 private ?array $ allowedOptions = null ;
@@ -52,10 +59,11 @@ final class CLIParser {
5259 /**
5360 * @param string[] $args e.g. $_SERVER['argv']
5461 * @psalm-param list<string> $args
62+ * @param string $optionsString will be printed inside usage documentation right after the script name and should contain e.g. `--in=<file> [--out=<file>]`
5563 */
56- public function __construct (array $ args ) {
57- array_shift ($ args );
64+ public function __construct (array $ args , string $ optionsString = '' ) {
5865 $ this ->args = $ args ;
66+ $ this ->optionsString = $ optionsString ;
5967 }
6068
6169 /**
@@ -68,7 +76,9 @@ public function __construct(array $args) {
6876 * options?: array{
6977 * default?: scalar,
7078 * ...
71- * }
79+ * },
80+ * value_label?: string,
81+ * description?: string
7282 * }> $allowedOptions
7383 * @see https://www.php.net/manual/en/function.filter-var-array.php
7484 */
@@ -109,6 +119,61 @@ public function setStrictMode(bool $strictMode): void {
109119 $ this ->strictMode = $ strictMode ;
110120 }
111121
122+ public function printUsage (): void {
123+ $ usage = "\e[33mUsage: \e[0m " .PHP_EOL ;
124+ $ script = $ _SERVER ['argv ' ][0 ] ?? 'script.php ' ;
125+ $ usage .= 'php ' .$ script .' ' .$ this ->optionsString .PHP_EOL .PHP_EOL ;
126+ if (!empty ($ this ->allowedOptions )){
127+ $ usage .= "\e[33mOptions: \e[0m " .PHP_EOL ;
128+ $ isList = array_is_list ($ this ->allowedOptions );
129+ $ options = [];
130+ foreach ($ this ->allowedOptions as $ key => $ option ){
131+ if ($ isList ){
132+ /**
133+ * @var string $option
134+ * @var string|false $flag
135+ */
136+ $ flag = array_search ($ option , $ this ->allowedFlags ?? [], true );
137+ $ options [] = [
138+ 'name ' => $ option ,
139+ 'flag ' => $ flag ,
140+ 'value ' => '' ,
141+ 'description ' => '' ,
142+ 'default ' => null
143+ ];
144+ } else {
145+ /**
146+ * @var string $key
147+ * @var string|false $flag
148+ */
149+ $ flag = array_search ($ key , $ this ->allowedFlags ?? [], true );
150+ $ options [] = [
151+ 'name ' => $ key ,
152+ 'flag ' => $ flag ,
153+ 'value ' => $ option ['value_label ' ] ?? '' ,
154+ 'description ' => $ option ['description ' ] ?? '' ,
155+ 'default ' => $ option ['options ' ]['default ' ] ?? null
156+ ];
157+ }
158+ }
159+ $ maxLength = 0 ;
160+ foreach ($ options as $ key => $ opt ){
161+ $ len = strlen (($ opt ['flag ' ] === false ? '' : '- ' .$ opt ['flag ' ].', ' ).'-- ' .$ opt ['name ' ].(empty ($ opt ['value ' ]) ? '' : '=< ' .$ opt ['value ' ].'> ' ));
162+ $ options [$ key ]['length ' ] = $ len ;
163+ if ($ maxLength < $ len ){
164+ $ maxLength = $ len ;
165+ }
166+ }
167+ foreach ($ options as $ option ){
168+ $ padding = str_pad (' ' , $ maxLength + 2 - $ option ['length ' ]);
169+ $ usage .= " \e[32m " .($ option ['flag ' ] === false ? '' : '- ' .$ option ['flag ' ].', ' ).'-- ' .$ option ['name ' ]."\e[0m " .(empty ($ option ['value ' ]) ? '' : "=< \e[34m " .$ option ['value ' ]."\e[0m> " ).
170+ $ padding .$ option ['description ' ].($ option ['default ' ] === null ? '' : " DEFAULT: \e[34m " .strval ($ option ['default ' ])."\e[0m " ).PHP_EOL ;
171+ }
172+ $ usage .= PHP_EOL ;
173+ }
174+ echo $ usage ;
175+ }
176+
112177 private function validateOption (string $ option , ?string $ value ): bool {
113178 if ($ this ->allowedOptions === null || (array_is_list ($ this ->allowedOptions ) && in_array ($ option , $ this ->allowedOptions ))){
114179 $ this ->options [$ option ] = $ value ?? true ;
@@ -171,7 +236,9 @@ public function parse(): bool {
171236 }
172237
173238 $ endofoptions = false ;
174- while (($ arg = array_shift ($ this ->args )) !== null ){
239+ $ args = $ this ->args ;
240+ array_shift ($ args );
241+ while (($ arg = array_shift ($ args )) !== null ){
175242 if ($ endofoptions ){ // if we have reached end of options, we cast all remaining argvs as arguments
176243 $ this ->arguments [] = $ arg ;
177244
@@ -186,12 +253,12 @@ public function parse(): bool {
186253 if ($ equalPos !== false ){ // is it the syntax '--option=value'?
187254 $ value = mb_substr ($ option , $ equalPos + 1 );
188255 $ option = mb_substr ($ option , 0 , $ equalPos );
189- } else if (($ this -> args [0 ][0 ] ?? '- ' ) !== '- ' ){ // is the option not followed by another option/flag but by arguments
190- while (($ this -> args [0 ][0 ] ?? '- ' ) !== '- ' ){
256+ } else if (($ args [0 ][0 ] ?? '- ' ) !== '- ' ){ // is the option not followed by another option/flag but by arguments
257+ while (($ args [0 ][0 ] ?? '- ' ) !== '- ' ){
191258 /**
192259 * @psalm-suppress PossiblyNullOperand
193260 */
194- $ value .= array_shift ($ this -> args ).' ' ;
261+ $ value .= array_shift ($ args ).' ' ;
195262 }
196263 $ value = rtrim ($ value , ' ' );
197264 }
@@ -222,13 +289,13 @@ public function parse(): bool {
222289 $ value = null ;
223290 if ($ chr === '= ' ){ // is it the syntax '-f=value'?
224291 $ value = mb_substr ($ arg , $ i + 1 );
225- } else if (($ this -> args [0 ][0 ] ?? '- ' ) !== '- ' ){ // is the flag not followed by another option/flag but by arguments
292+ } else if (($ args [0 ][0 ] ?? '- ' ) !== '- ' ){ // is the flag not followed by another option/flag but by arguments
226293 $ value = '' ;
227- while (($ this -> args [0 ][0 ] ?? '- ' ) !== '- ' ){
294+ while (($ args [0 ][0 ] ?? '- ' ) !== '- ' ){
228295 /**
229296 * @psalm-suppress PossiblyNullOperand
230297 */
231- $ value .= array_shift ($ this -> args ).' ' ;
298+ $ value .= array_shift ($ args ).' ' ;
232299 }
233300 $ value = rtrim ($ value , ' ' );
234301 }
0 commit comments