Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.59% covered (success)
95.59%
217 / 227
82.35% covered (warning)
82.35%
28 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
MaintenanceParameters
95.59% covered (success)
95.59%
217 / 227
82.35% covered (warning)
82.35%
28 / 34
98
0.00% covered (danger)
0.00%
0 / 1
 getFieldReference
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 assignGroup
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 supportsOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addOption
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 hasOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOption
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addArg
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
6.09
 deleteOption
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 setAllowUnregisteredOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasArg
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getArg
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getArgs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getArgName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setArg
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 clear
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 mergeOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 loadWithArgv
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
16
 setOptionValue
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setOptionsAndArgs
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 validate
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
10
 getHelp
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
11
 formatHelpItems
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 getOptionNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOptionsSequence
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUsagePrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArgRepresentation
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Maintenance;
22
23use UnexpectedValueException;
24
25/**
26 * Command line parameter handling for maintenance scripts.
27 *
28 * @since 1.39
29 * @ingroup Maintenance
30 */
31class MaintenanceParameters {
32
33    /**
34     * Array of desired/allowed params
35     * @var array<string,array>
36     * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string|bool,multiOccurrence:bool}>
37     */
38    private $mOptDefs = [];
39
40    /** @var array<string,string> Mapping short options to long ones */
41    private $mShortOptionMap = [];
42
43    /** @var array<int,array> Desired/allowed args */
44    private $mArgDefs = [];
45
46    /** @var array<string,int> Map of arg names to offsets */
47    private $mArgOffsets = [];
48
49    /** @var bool Allow arbitrary options to be passed, or only specified ones? */
50    private $mAllowUnregisteredOptions = false;
51
52    /** @var string|null Name of the script currently running */
53    private $mName = null;
54
55    /** @var string|null A description of the script, children should change this via addDescription() */
56    private $mDescription = null;
57
58    /** @var array<string,string> This is the list of options that were actually passed */
59    private $mOptions = [];
60
61    /** @var array<int,string> This is the list of arguments that were actually passed */
62    private $mArgs = [];
63
64    /** @var array<string,array> maps group names to lists of option names */
65    private $mOptionGroups = [];
66
67    /**
68     * Used to read the options in the order they were passed.
69     * This is an array of arrays where
70     * 0 => the option name and 1 => option value.
71     *
72     * @var array
73     */
74    private $optionsSequence = [];
75
76    /** @var string[] */
77    private $errors = [];
78
79    /** @var string */
80    private $usagePrefix = 'php maintenance/run.php';
81
82    /**
83     * Returns a reference to a member field.
84     * This is a backwards compatibility hack, it should be removed as soon as possible!
85     *
86     * @param string $fieldName
87     *
88     * @return mixed A reference to a member field
89     * @internal For use by the Maintenance class, for backwards compatibility support.
90     */
91    public function &getFieldReference( string $fieldName ) {
92        return $this->$fieldName;
93    }
94
95    /**
96     * Assigns a list of options to the given group.
97     * The given options will be shown as part of the given group
98     * in the help message.
99     *
100     * @param string $groupName
101     * @param array $paramNames
102     */
103    public function assignGroup( string $groupName, array $paramNames ) {
104        $this->mOptionGroups[ $groupName ] = array_merge(
105            $this->mOptionGroups[ $groupName ] ?? [],
106            $paramNames
107        );
108    }
109
110    /**
111     * Checks to see if a particular option in supported. Normally this means it
112     * has been registered by the script via addOption.
113     * @param string $name The name of the option<string,string>
114     * @return bool true if the option exists, false otherwise
115     */
116    public function supportsOption( string $name ) {
117        return isset( $this->mOptDefs[$name] );
118    }
119
120    /**
121     * Add a option to the script. Will be displayed on --help
122     * with the associated description
123     *
124     * @param string $name The name of the param (help, version, etc)
125     * @param string $description The description of the param to show on --help
126     * @param bool $required Is the param required?
127     * @param bool $withArg Is an argument required with this option?
128     * @param string|bool $shortName Character to use as short name
129     * @param bool $multiOccurrence Can this option be passed multiple times?
130     */
131    public function addOption( string $name, string $description, bool $required = false,
132        bool $withArg = false, $shortName = false, bool $multiOccurrence = false
133    ) {
134        $this->mOptDefs[$name] = [
135            'desc' => $description,
136            'require' => $required,
137            'withArg' => $withArg,
138            'shortName' => $shortName,
139            'multiOccurrence' => $multiOccurrence
140        ];
141
142        if ( $shortName !== false ) {
143            $this->mShortOptionMap[$shortName] = $name;
144        }
145    }
146
147    /**
148     * Checks to see if a particular option was set.
149     *
150     * @param string $name The name of the option
151     * @return bool
152     */
153    public function hasOption( string $name ): bool {
154        return isset( $this->mOptions[$name] );
155    }
156
157    /**
158     * Get the value of an option, or return the default.
159     *
160     * If the option was defined to support multiple occurrences,
161     * this will return an array.
162     *
163     * @param string $name The name of the param
164     * @param mixed|null $default Anything you want, default null
165     * @return mixed
166     * @return-taint none
167     */
168    public function getOption( string $name, $default = null ) {
169        if ( $this->hasOption( $name ) ) {
170            return $this->mOptions[$name];
171        } else {
172            return $default;
173        }
174    }
175
176    /**
177     * Define a positional argument. getArg() can later be used to get the value given
178     * for the argument, by index or by name.
179     *
180     * @param string $arg Name of the arg, like 'start'
181     * @param string $description Short description of the arg
182     * @param bool $required Is this required?
183     * @param bool $multi Does it allow multiple values? (Last arg only)
184     * @return int the offset of the argument
185     */
186    public function addArg( string $arg, string $description, bool $required = true, bool $multi = false ): int {
187        if ( isset( $this->mArgOffsets[$arg] ) ) {
188            throw new UnexpectedValueException( "Argument already defined: $arg" );
189        }
190
191        $argCount = count( $this->mArgDefs );
192        if ( $argCount ) {
193            $prevArg = $this->mArgDefs[ $argCount - 1 ];
194            if ( !$prevArg['require'] && $required ) {
195                throw new UnexpectedValueException(
196                    "Required argument {$arg} cannot follow an optional argument {$prevArg['name']}"
197                );
198            }
199
200            if ( $prevArg['multi'] ) {
201                throw new UnexpectedValueException(
202                    "Argument {$arg} cannot follow multi-value argument {$prevArg['name']}"
203                );
204            }
205        }
206
207        $this->mArgDefs[] = [
208            'name' => $arg,
209            'desc' => $description,
210            'require' => $required,
211            'multi' => $multi,
212        ];
213
214        $ofs = count( $this->mArgDefs ) - 1;
215        $this->mArgOffsets[$arg] = $ofs;
216        return $ofs;
217    }
218
219    /**
220     * Remove an option. Useful for removing options that won't be used in your script.
221     * @param string $name The option to remove.
222     */
223    public function deleteOption( string $name ) {
224        unset( $this->mOptDefs[$name] );
225        unset( $this->mOptions[$name] );
226
227        foreach ( $this->optionsSequence as $i => [ $opt, ] ) {
228            if ( $opt === $name ) {
229                unset( $this->optionsSequence[$i] );
230                break;
231            }
232        }
233    }
234
235    /**
236     * Sets whether to allow unknown options to be passed to the script.
237     * By default, unknown options cause an error.
238     * @param bool $allow Should we allow?
239     */
240    public function setAllowUnregisteredOptions( bool $allow ) {
241        $this->mAllowUnregisteredOptions = $allow;
242    }
243
244    /**
245     * Set a short description of what the script does.
246     * @param string $text
247     */
248    public function setDescription( string $text ) {
249        $this->mDescription = $text;
250    }
251
252    /**
253     * Was a value for the given argument provided?
254     * @param int|string $argId The index (from zero) of the argument, or
255     *                   the name declared for the argument by addArg().
256     * @return bool
257     */
258    public function hasArg( $argId ): bool {
259        // arg lookup by name
260        if ( is_string( $argId ) && isset( $this->mArgOffsets[$argId] ) ) {
261            $argId = $this->mArgOffsets[$argId];
262        }
263
264        return isset( $this->mArgs[$argId] );
265    }
266
267    /**
268     * Get an argument.
269     * @param int|string $argId The index (from zero) of the argument, or
270     *                   the name declared for the argument by addArg().
271     * @param string|null $default The default if it doesn't exist
272     * @return string|null
273     * @return-taint none
274     */
275    public function getArg( $argId, ?string $default = null ): ?string {
276        // arg lookup by name
277        if ( is_string( $argId ) && isset( $this->mArgOffsets[$argId] ) ) {
278            $argId = $this->mArgOffsets[$argId];
279        }
280
281        return $this->mArgs[$argId] ?? $default;
282    }
283
284    /**
285     * Get arguments.
286     * @param int|string $offset The index (from zero) of the first argument, or
287     *                   the name declared for the argument by addArg().
288     * @return string[]
289     */
290    public function getArgs( $offset = 0 ): array {
291        if ( is_string( $offset ) && isset( $this->mArgOffsets[$offset] ) ) {
292            $offset = $this->mArgOffsets[$offset];
293        }
294
295        return array_slice( $this->mArgs, $offset );
296    }
297
298    /**
299     * Get the name of an argument at the given index.
300     *
301     * @param int $argIndex The integer value (from zero) for the arg
302     *
303     * @return string|null The name of the argument, or null if the argument does not exist.
304     */
305    public function getArgName( int $argIndex ): ?string {
306        return $this->mArgDefs[ $argIndex ]['name'] ?? null;
307    }
308
309    /**
310     * Programmatically set the value of the given option.
311     * Useful for setting up child scripts, see runChild().
312     *
313     * @param string $name
314     * @param mixed|null $value
315     */
316    public function setOption( string $name, $value ): void {
317        $this->mOptions[$name] = $value;
318    }
319
320    /**
321     * Programmatically set the value of the given argument.
322     * Useful for setting up child scripts, see runChild().
323     *
324     * @param string|int $argId
325     * @param string $value
326     */
327    public function setArg( $argId, $value ): void {
328        // arg lookup by name
329        if ( is_string( $argId ) && isset( $this->mArgOffsets[$argId] ) ) {
330            $argId = $this->mArgOffsets[$argId];
331        }
332        $this->mArgs[$argId] = $value;
333    }
334
335    /**
336     * Clear all parameter values.
337     * Note that all parameter definitions remain intact.
338     */
339    public function clear() {
340        $this->mOptions = [];
341        $this->mArgs = [];
342        $this->optionsSequence = [];
343        $this->errors = [];
344    }
345
346    /**
347     * Merge options declarations from $other into this instance.
348     *
349     * @param MaintenanceParameters $other
350     */
351    public function mergeOptions( MaintenanceParameters $other ) {
352        $this->mOptDefs = $other->mOptDefs + $this->mOptDefs;
353        $this->mShortOptionMap = $other->mShortOptionMap + $this->mShortOptionMap;
354
355        $this->mOptionGroups = array_merge_recursive( $this->mOptionGroups, $other->mOptionGroups );
356
357        $this->clear();
358    }
359
360    /**
361     * Load params and arguments from a given array
362     * of command-line arguments
363     *
364     * @param array $argv The argument array.
365     * @param int $skip Skip that many elements at the beginning of $argv.
366     */
367    public function loadWithArgv( array $argv, int $skip = 0 ) {
368        $this->clear();
369
370        $options = [];
371        $args = [];
372        $this->optionsSequence = [];
373
374        // Ignore a number of arguments at the beginning of the array.
375        // Typically used to ignore the script name at index 0.
376        $argv = array_slice( $argv, $skip );
377
378        # Parse arguments
379        for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
380            if ( $arg == '--' ) {
381                # End of options, remainder should be considered arguments
382                $arg = next( $argv );
383                while ( $arg !== false ) {
384                    $args[] = $arg;
385                    $arg = next( $argv );
386                }
387                break;
388            } elseif ( substr( $arg, 0, 2 ) == '--' ) {
389                # Long options
390                $option = substr( $arg, 2 );
391                if ( isset( $this->mOptDefs[$option] ) && $this->mOptDefs[$option]['withArg'] ) {
392                    $param = next( $argv );
393                    if ( $param === false ) {
394                        $this->error( "Option --$option needs a value after it!" );
395                    }
396
397                    $this->setOptionValue( $options, $option, $param );
398                } else {
399                    $bits = explode( '=', $option, 2 );
400                    $this->setOptionValue( $options, $bits[0], $bits[1] ?? 1 );
401                }
402            } elseif ( $arg == '-' ) {
403                # Lonely "-", often used to indicate stdin or stdout.
404                $args[] = $arg;
405            } elseif ( substr( $arg, 0, 1 ) == '-' ) {
406                # Short options
407                $argLength = strlen( $arg );
408                for ( $p = 1; $p < $argLength; $p++ ) {
409                    $givenShort = $arg[$p];
410                    $option = $givenShort;
411                    if ( !isset( $this->mOptDefs[$givenShort] ) && isset( $this->mShortOptionMap[$givenShort] ) ) {
412                        $option = $this->mShortOptionMap[$givenShort];
413                    }
414
415                    if ( isset( $this->mOptDefs[$option]['withArg'] ) && $this->mOptDefs[$option]['withArg'] ) {
416                        $param = next( $argv );
417                        if ( $param === false ) {
418                            $this->error( "Option -$givenShort needs a value after it!" );
419                        }
420                        $this->setOptionValue( $options, $option, $param );
421                    } else {
422                        $this->setOptionValue( $options, $option, 1 );
423                    }
424                }
425            } else {
426                $args[] = $arg;
427            }
428        }
429
430        $this->mOptions = $options;
431        $this->mArgs = $args;
432    }
433
434    /**
435     * Helper function used solely by loadWithArgv
436     * to prevent code duplication
437     *
438     * This sets the param in the options array based on
439     * whether or not it can be specified multiple times.
440     *
441     * @param array &$options
442     * @param string $option
443     * @param mixed $value
444     */
445    private function setOptionValue( array &$options, string $option, $value ) {
446        $this->optionsSequence[] = [ $option, $value ];
447
448        if ( isset( $this->mOptDefs[$option] ) ) {
449            $multi = $this->mOptDefs[$option]['multiOccurrence'];
450        } else {
451            $multi = false;
452        }
453        $exists = array_key_exists( $option, $options );
454        if ( $multi && $exists ) {
455            $options[$option][] = $value;
456        } elseif ( $multi ) {
457            $options[$option] = [ $value ];
458        } elseif ( !$exists ) {
459            $options[$option] = $value;
460        } else {
461            $this->error( "Option --$option given twice" );
462        }
463    }
464
465    private function error( string $msg ) {
466        $this->errors[] = $msg;
467    }
468
469    /**
470     * Get any errors encountered while processing parameters.
471     *
472     * @return string[]
473     */
474    public function getErrors(): array {
475        return $this->errors;
476    }
477
478    /**
479     * Whether any errors have been recorded so far.
480     *
481     * @return bool
482     */
483    public function hasErrors(): bool {
484        return (bool)$this->errors;
485    }
486
487    /**
488     * Set the script name, for use in the help message
489     *
490     * @param string $name
491     */
492    public function setName( string $name ) {
493        $this->mName = $name;
494    }
495
496    /**
497     * Get the script name, as shown in the help message
498     *
499     * @return string
500     */
501    public function getName(): string {
502        return $this->mName;
503    }
504
505    /**
506     * Force option and argument values.
507     *
508     * @internal
509     *
510     * @param array $opts
511     * @param array $args
512     */
513    public function setOptionsAndArgs( array $opts, array $args ) {
514        $this->mOptions = $opts;
515        $this->mArgs = $args;
516
517        $this->optionsSequence = [];
518        foreach ( $opts as $name => $value ) {
519            $array = (array)$value;
520
521            foreach ( $array as $v ) {
522                $this->optionsSequence[] = [ $name, $v ];
523            }
524        }
525    }
526
527    /**
528     * Run some validation checks on the params, etc.
529     *
530     * Error details can be obtained via getErrors().
531     *
532     * @return bool
533     */
534    public function validate() {
535        $valid = true;
536        # Check to make sure we've got all the required options
537        foreach ( $this->mOptDefs as $opt => $info ) {
538            if ( $info['require'] && !$this->hasOption( $opt ) ) {
539                $this->error( "Option --$opt is required!" );
540                $valid = false;
541            }
542        }
543        # Check arg list too
544        foreach ( $this->mArgDefs as $k => $info ) {
545            if ( $info['require'] && !$this->hasArg( $k ) ) {
546                $this->error( 'Argument <' . $info['name'] . '> is required!' );
547                $valid = false;
548            }
549        }
550        if ( !$this->mAllowUnregisteredOptions ) {
551            # Check for unexpected options
552            foreach ( $this->mOptions as $opt => $val ) {
553                if ( !$this->supportsOption( $opt ) ) {
554                    $this->error( "Unexpected option --$opt!" );
555                    $valid = false;
556                }
557            }
558        }
559
560        return $valid;
561    }
562
563    /**
564     * Get help text.
565     *
566     * @return string
567     */
568    public function getHelp(): string {
569        $screenWidth = 80; // TODO: Calculate this!
570        $tab = "    ";
571        $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
572
573        ksort( $this->mOptDefs );
574
575        $output = [];
576
577        // Description ...
578        if ( $this->mDescription ) {
579            $output[] = "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n";
580        }
581        $output[] = "\nUsage: {$this->usagePrefix} " . basename( $this->mName );
582
583        // ... append options ...
584        if ( $this->mOptDefs ) {
585            $output[] = ' [OPTION]...';
586
587            foreach ( $this->mOptDefs as $name => $opt ) {
588                if ( $opt['require'] ) {
589                    $output[] = " --$name";
590
591                    if ( $opt['withArg'] ) {
592                        $vname = strtoupper( $name );
593                        $output[] = " <$vname>";
594                    }
595                }
596            }
597        }
598
599        // ... and append arguments.
600        if ( $this->mArgDefs ) {
601            $args = '';
602            foreach ( $this->mArgDefs as $arg ) {
603                $argRepr = $this->getArgRepresentation( $arg );
604
605                $args .= ' ';
606                $args .= $argRepr;
607            }
608            $output[] = $args;
609        }
610        $output[] = "\n\n";
611
612        // Go through the declared groups and output the options for each group separately.
613        // Maintain the remaining options in $params.
614        $params = $this->mOptDefs;
615        foreach ( $this->mOptionGroups as $groupName => $groupOptions ) {
616            $output[] = $this->formatHelpItems(
617                array_intersect_key( $params, array_flip( $groupOptions ) ),
618                $groupName,
619                $descWidth, $tab
620            );
621            $params = array_diff_key( $params, array_flip( $groupOptions ) );
622        }
623
624        $output[] = $this->formatHelpItems(
625            $params,
626            'Script specific options',
627            $descWidth, $tab
628        );
629
630        // Print arguments
631        if ( count( $this->mArgDefs ) > 0 ) {
632            $output[] = "Arguments:\n";
633            // Arguments description
634            foreach ( $this->mArgDefs as $info ) {
635                $argRepr = $this->getArgRepresentation( $info );
636                $output[] =
637                    wordwrap(
638                        "$tab$argRepr" . $info['desc'],
639                        $descWidth,
640                        "\n$tab$tab"
641                    ) . "\n";
642            }
643            $output[] = "\n";
644        }
645
646        return implode( '', $output );
647    }
648
649    private function formatHelpItems( array $items, $heading, $descWidth, $tab ) {
650        if ( $items === [] ) {
651            return '';
652        }
653
654        $output = [];
655
656        $output[] = "$heading:\n";
657
658        foreach ( $items as $name => $info ) {
659            $out = $name;
660
661            if ( $info['shortName'] !== false ) {
662                $out .= ' (-' . $info['shortName'] . ')';
663            }
664
665            if ( $info['withArg'] ) {
666                $vname = strtoupper( $name );
667                $out .= " <$vname>";
668            }
669
670            $output[] =
671                wordwrap(
672                    "$tab--$out" . strtr( $info['desc'], [ "\n" => "\n$tab$tab" ] ),
673                    $descWidth,
674                    "\n$tab$tab"
675                ) . "\n";
676        }
677
678        $output[] = "\n";
679
680        return implode( '', $output );
681    }
682
683    /**
684     * Returns the names of defined options
685     * @return string[]
686     */
687    public function getOptionNames(): array {
688        return array_keys( $this->mOptDefs );
689    }
690
691    /**
692     * Returns any option values
693     * @return array
694     */
695    public function getOptions(): array {
696        return $this->mOptions;
697    }
698
699    /**
700     * Returns option values as an ordered sequence.
701     * Useful for option chaining (Ex. dumpBackup.php).
702     * @return array[] a list of pairs of like [ $option, $value ]
703     */
704    public function getOptionsSequence(): array {
705        return $this->optionsSequence;
706    }
707
708    /**
709     * @param string $usagePrefix
710     */
711    public function setUsagePrefix( string $usagePrefix ) {
712        $this->usagePrefix = $usagePrefix;
713    }
714
715    /**
716     * @param array $argInfo
717     *
718     * @return string
719     */
720    private function getArgRepresentation( array $argInfo ): string {
721        if ( $argInfo['require'] ) {
722            $rep = '<' . $argInfo['name'] . '>';
723        } else {
724            $rep = '[' . $argInfo['name'] . ']';
725        }
726
727        if ( $argInfo['multi'] ) {
728            $rep .= '...';
729        }
730
731        return $rep;
732    }
733
734}