Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.56% covered (success)
95.56%
215 / 225
82.35% covered (warning)
82.35%
28 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
MaintenanceParameters
95.56% covered (success)
95.56%
215 / 225
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%
38 / 38
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%
18 / 18
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     * Per 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 the name of the argument, or null if no name is defined for that argument
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                    $option = $arg[$p];
410                    if ( !isset( $this->mOptDefs[$option] ) && isset( $this->mShortOptionMap[$option] ) ) {
411                        $option = $this->mShortOptionMap[$option];
412                    }
413
414                    if ( isset( $this->mOptDefs[$option]['withArg'] ) && $this->mOptDefs[$option]['withArg'] ) {
415                        $param = next( $argv );
416                        if ( $param === false ) {
417                            $this->error( "Option --$option needs a value after it!" );
418                        }
419                        $this->setOptionValue( $options, $option, $param );
420                    } else {
421                        $this->setOptionValue( $options, $option, 1 );
422                    }
423                }
424            } else {
425                $args[] = $arg;
426            }
427        }
428
429        $this->mOptions = $options;
430        $this->mArgs = $args;
431    }
432
433    /**
434     * Helper function used solely by loadWithArgv
435     * to prevent code duplication
436     *
437     * This sets the param in the options array based on
438     * whether or not it can be specified multiple times.
439     *
440     * @param array &$options
441     * @param string $option
442     * @param mixed $value
443     */
444    private function setOptionValue( array &$options, string $option, $value ) {
445        $this->optionsSequence[] = [ $option, $value ];
446
447        if ( isset( $this->mOptDefs[$option] ) ) {
448            $multi = $this->mOptDefs[$option]['multiOccurrence'];
449        } else {
450            $multi = false;
451        }
452        $exists = array_key_exists( $option, $options );
453        if ( $multi && $exists ) {
454            $options[$option][] = $value;
455        } elseif ( $multi ) {
456            $options[$option] = [ $value ];
457        } elseif ( !$exists ) {
458            $options[$option] = $value;
459        } else {
460            $this->error( "Option --$option given twice" );
461        }
462    }
463
464    private function error( string $msg ) {
465        $this->errors[] = $msg;
466    }
467
468    /**
469     * Get any errors encountered while processing parameters.
470     *
471     * @return string[]
472     */
473    public function getErrors(): array {
474        return $this->errors;
475    }
476
477    /**
478     * Whether any errors have been recorded so far.
479     *
480     * @return bool
481     */
482    public function hasErrors(): bool {
483        return (bool)$this->errors;
484    }
485
486    /**
487     * Set the script name, for use in the help message
488     *
489     * @param string $name
490     */
491    public function setName( string $name ) {
492        $this->mName = $name;
493    }
494
495    /**
496     * Get the script name, as shown in the help message
497     *
498     * @return string
499     */
500    public function getName(): string {
501        return $this->mName;
502    }
503
504    /**
505     * Force option and argument values.
506     *
507     * @internal
508     *
509     * @param array $opts
510     * @param array $args
511     */
512    public function setOptionsAndArgs( array $opts, array $args ) {
513        $this->mOptions = $opts;
514        $this->mArgs = $args;
515
516        $this->optionsSequence = [];
517        foreach ( $opts as $name => $value ) {
518            $array = (array)$value;
519
520            foreach ( $array as $v ) {
521                $this->optionsSequence[] = [ $name, $v ];
522            }
523        }
524    }
525
526    /**
527     * Run some validation checks on the params, etc.
528     *
529     * Error details can be obtained via getErrors().
530     *
531     * @return bool
532     */
533    public function validate() {
534        $valid = true;
535        # Check to make sure we've got all the required options
536        foreach ( $this->mOptDefs as $opt => $info ) {
537            if ( $info['require'] && !$this->hasOption( $opt ) ) {
538                $this->error( "Option --$opt is required!" );
539                $valid = false;
540            }
541        }
542        # Check arg list too
543        foreach ( $this->mArgDefs as $k => $info ) {
544            if ( $info['require'] && !$this->hasArg( $k ) ) {
545                $this->error( 'Argument <' . $info['name'] . '> is required!' );
546                $valid = false;
547            }
548        }
549        if ( !$this->mAllowUnregisteredOptions ) {
550            # Check for unexpected options
551            foreach ( $this->mOptions as $opt => $val ) {
552                if ( !$this->supportsOption( $opt ) ) {
553                    $this->error( "Unexpected option --$opt!" );
554                    $valid = false;
555                }
556            }
557        }
558
559        return $valid;
560    }
561
562    /**
563     * Get help text.
564     *
565     * @return string
566     */
567    public function getHelp(): string {
568        $screenWidth = 80; // TODO: Calculate this!
569        $tab = "    ";
570        $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
571
572        ksort( $this->mOptDefs );
573
574        $output = [];
575
576        // Description ...
577        if ( $this->mDescription ) {
578            $output[] = "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n";
579        }
580        $output[] = "\nUsage: {$this->usagePrefix} " . basename( $this->mName );
581
582        // ... append options ...
583        if ( $this->mOptDefs ) {
584            $output[] = ' [OPTION]...';
585
586            foreach ( $this->mOptDefs as $name => $opt ) {
587                if ( $opt['require'] ) {
588                    $output[] = " --$name";
589
590                    if ( $opt['withArg'] ) {
591                        $vname = strtoupper( $name );
592                        $output[] = " <$vname>";
593                    }
594                }
595            }
596        }
597
598        // ... and append arguments.
599        if ( $this->mArgDefs ) {
600            $args = '';
601            foreach ( $this->mArgDefs as $arg ) {
602                $argRepr = $this->getArgRepresentation( $arg );
603
604                $args .= ' ';
605                $args .= $argRepr;
606            }
607            $output[] = $args;
608        }
609        $output[] = "\n\n";
610
611        // Go through the declared groups and output the options for each group separately.
612        // Maintain the remaining options in $params.
613        $params = $this->mOptDefs;
614        foreach ( $this->mOptionGroups as $groupName => $groupOptions ) {
615            $output[] = $this->formatHelpItems(
616                array_intersect_key( $params, array_flip( $groupOptions ) ),
617                $groupName,
618                $descWidth, $tab
619            );
620            $params = array_diff_key( $params, array_flip( $groupOptions ) );
621        }
622
623        $output[] = $this->formatHelpItems(
624            $params,
625            'Script specific options',
626            $descWidth, $tab
627        );
628
629        // Print arguments
630        if ( count( $this->mArgDefs ) > 0 ) {
631            $output[] = "Arguments:\n";
632            // Arguments description
633            foreach ( $this->mArgDefs as $info ) {
634                $argRepr = $this->getArgRepresentation( $info );
635                $output[] =
636                    wordwrap(
637                        "$tab$argRepr" . $info['desc'],
638                        $descWidth,
639                        "\n$tab$tab"
640                    ) . "\n";
641            }
642            $output[] = "\n";
643        }
644
645        return implode( '', $output );
646    }
647
648    private function formatHelpItems( array $items, $heading, $descWidth, $tab ) {
649        if ( $items === [] ) {
650            return '';
651        }
652
653        $output = [];
654
655        $output[] = "$heading:\n";
656
657        foreach ( $items as $name => $info ) {
658            if ( $info['shortName'] !== false ) {
659                $name .= ' (-' . $info['shortName'] . ')';
660            }
661            if ( $info['withArg'] ) {
662                $vname = strtoupper( $name );
663                $name .= " <$vname>";
664            }
665
666            $output[] =
667                wordwrap(
668                    "$tab--$name" . strtr( $info['desc'], [ "\n" => "\n$tab$tab" ] ),
669                    $descWidth,
670                    "\n$tab$tab"
671                ) . "\n";
672        }
673
674        $output[] = "\n";
675
676        return implode( '', $output );
677    }
678
679    /**
680     * Returns the names of defined options
681     * @return string[]
682     */
683    public function getOptionNames(): array {
684        return array_keys( $this->mOptDefs );
685    }
686
687    /**
688     * Returns any option values
689     * @return array
690     */
691    public function getOptions(): array {
692        return $this->mOptions;
693    }
694
695    /**
696     * Returns option values as an ordered sequence.
697     * Useful for option chaining (Ex. dumpBackup.php).
698     * @return array[] a list of pairs of like [ $option, $value ]
699     */
700    public function getOptionsSequence(): array {
701        return $this->optionsSequence;
702    }
703
704    /**
705     * @param string $usagePrefix
706     */
707    public function setUsagePrefix( string $usagePrefix ) {
708        $this->usagePrefix = $usagePrefix;
709    }
710
711    /**
712     * @param array $argInfo
713     *
714     * @return string
715     */
716    private function getArgRepresentation( array $argInfo ): string {
717        if ( $argInfo['require'] ) {
718            $rep = '<' . $argInfo['name'] . '>';
719        } else {
720            $rep = '[' . $argInfo['name'] . ']';
721        }
722
723        if ( $argInfo['multi'] ) {
724            $rep .= '...';
725        }
726
727        return $rep;
728    }
729
730}