Use TOCData methods to process new headings
[mediawiki.git] / maintenance / includes / MaintenanceParameters.php
blobdc42689e1551b9979e5c1df6e7bc4c3ce4f5a1fa
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.
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.
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
18 * @file
21 namespace MediaWiki\Maintenance;
23 use UnexpectedValueException;
25 /**
26 * Command line parameter handling for maintenance scripts.
28 * @since 1.39
29 * @ingroup Maintenance
31 class MaintenanceParameters {
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}>
38 private $mOptDefs = [];
40 /** @var array<string,string> Mapping short options to long ones */
41 private $mShortOptionMap = [];
43 /** @var array<int,array> Desired/allowed args */
44 private $mArgDefs = [];
46 /** @var array<string,int> Map of arg names to offsets */
47 private $mArgOffsets = [];
49 /** @var bool Allow arbitrary options to be passed, or only specified ones? */
50 private $mAllowUnregisteredOptions = false;
52 /** @var string|null Name of the script currently running */
53 private $mName = null;
55 /** @var string|null A description of the script, children should change this via addDescription() */
56 private $mDescription = null;
58 /** @var array<string,string> This is the list of options that were actually passed */
59 private $mOptions = [];
61 /** @var array<int,string> This is the list of arguments that were actually passed */
62 private $mArgs = [];
64 /** @var array<string,array> maps group names to lists of option names */
65 private $mOptionGroups = [];
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.
72 * @var array
74 private $optionsSequence = [];
76 /** @var string[] */
77 private $errors = [];
79 /** @var string */
80 private $usagePrefix = 'php';
82 /**
83 * Returns a reference to a member field.
84 * This is a backwards compatibility hack, it should be removed as soon as possible!
86 * @param string $fieldName
88 * @return mixed A reference to a member field
89 * @internal For use by the Maintenance class, for backwards compatibility support.
91 public function &getFieldReference( string $fieldName ) {
92 return $this->$fieldName;
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.
100 * @param string $groupName
101 * @param array $paramNames
103 public function assignGroup( string $groupName, array $paramNames ) {
104 $this->mOptionGroups[ $groupName ] = array_merge(
105 $this->mOptionGroups[ $groupName ] ?? [],
106 $paramNames
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
116 public function supportsOption( string $name ) {
117 return isset( $this->mOptDefs[$name] );
121 * Add a option to the script. Will be displayed on --help
122 * with the associated description
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?
131 public function addOption( string $name, string $description, bool $required = false,
132 bool $withArg = false, $shortName = false, bool $multiOccurrence = false
134 $this->mOptDefs[$name] = [
135 'desc' => $description,
136 'require' => $required,
137 'withArg' => $withArg,
138 'shortName' => $shortName,
139 'multiOccurrence' => $multiOccurrence
142 if ( $shortName !== false ) {
143 $this->mShortOptionMap[$shortName] = $name;
148 * Checks to see if a particular option was set.
150 * @param string $name The name of the option
151 * @return bool
153 public function hasOption( string $name ): bool {
154 return isset( $this->mOptions[$name] );
158 * Get the value of an option, or return the default.
160 * If the option was defined to support multiple occurrences,
161 * this will return an array.
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
168 public function getOption( string $name, $default = null ) {
169 if ( $this->hasOption( $name ) ) {
170 return $this->mOptions[$name];
171 } else {
172 return $default;
177 * Define a positional argument. getArg() can later be used to get the value given
178 * for the argument, by index or by name.
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
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" );
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']}"
200 if ( $prevArg['multi'] ) {
201 throw new UnexpectedValueException(
202 "Argument {$arg} cannot follow multi-value argument {$prevArg['name']}"
207 $this->mArgDefs[] = [
208 'name' => $arg,
209 'desc' => $description,
210 'require' => $required,
211 'multi' => $multi,
214 $ofs = count( $this->mArgDefs ) - 1;
215 $this->mArgOffsets[$arg] = $ofs;
216 return $ofs;
220 * Remove an option. Useful for removing options that won't be used in your script.
221 * @param string $name The option to remove.
223 public function deleteOption( string $name ) {
224 unset( $this->mOptDefs[$name] );
225 unset( $this->mOptions[$name] );
227 foreach ( $this->optionsSequence as $i => [ $opt, ] ) {
228 if ( $opt === $name ) {
229 unset( $this->optionsSequence[$i] );
230 break;
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?
240 public function setAllowUnregisteredOptions( bool $allow ) {
241 $this->mAllowUnregisteredOptions = $allow;
245 * Set a short description of what the script does.
246 * @param string $text
248 public function setDescription( string $text ) {
249 $this->mDescription = $text;
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
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];
264 return isset( $this->mArgs[$argId] );
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
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];
281 return $this->mArgs[$argId] ?? $default;
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[]
290 public function getArgs( $offset = 0 ): array {
291 if ( is_string( $offset ) && isset( $this->mArgOffsets[$offset] ) ) {
292 $offset = $this->mArgOffsets[$offset];
295 return array_slice( $this->mArgs, $offset );
299 * Get the name of an argument at the given index.
301 * @param int $argIndex The integer value (from zero) for the arg
303 * @return ?string the name of the argument, or null if no name is defined for that argument
305 public function getArgName( int $argIndex ): ?string {
306 return $this->mArgDefs[ $argIndex ]['name'] ?? null;
310 * Programmatically set the value of the given option.
311 * Useful for setting up child scripts, see runChild().
313 * @param string $name
314 * @param mixed|null $value
316 public function setOption( string $name, $value ): void {
317 $this->mOptions[$name] = $value;
321 * Programmatically set the value of the given argument.
322 * Useful for setting up child scripts, see runChild().
324 * @param string|int $argId
325 * @param string $value
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];
332 $this->mArgs[$argId] = $value;
336 * Clear all parameter values.
337 * Note that all parameter definitions remain intact.
339 public function clear() {
340 $this->mOptions = [];
341 $this->mArgs = [];
342 $this->optionsSequence = [];
343 $this->errors = [];
347 * Merge options declarations from $other into this instance.
349 * @param MaintenanceParameters $other
351 public function mergeOptions( MaintenanceParameters $other ) {
352 $this->mOptDefs = $other->mOptDefs + $this->mOptDefs;
353 $this->mShortOptionMap = $other->mShortOptionMap + $this->mShortOptionMap;
355 $this->mOptionGroups = array_merge_recursive( $this->mOptionGroups, $other->mOptionGroups );
357 $this->clear();
361 * Load params and arguments from a given array
362 * of command-line arguments
364 * @param array $argv The argument array.
365 * @param int $skip Skip that many elements at the beginning of $argv.
367 public function loadWithArgv( array $argv, int $skip = 0 ) {
368 $this->clear();
370 $options = [];
371 $args = [];
372 $this->optionsSequence = [];
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 );
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 );
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!" );
397 $this->setOptionValue( $options, $option, $param );
398 } else {
399 $bits = explode( '=', $option, 2 );
400 $this->setOptionValue( $options, $bits[0], $bits[1] ?? 1 );
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];
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!" );
419 $this->setOptionValue( $options, $option, $param );
420 } else {
421 $this->setOptionValue( $options, $option, 1 );
424 } else {
425 $args[] = $arg;
429 $this->mOptions = $options;
430 $this->mArgs = $args;
434 * Helper function used solely by loadWithArgv
435 * to prevent code duplication
437 * This sets the param in the options array based on
438 * whether or not it can be specified multiple times.
440 * @param array &$options
441 * @param string $option
442 * @param mixed $value
444 private function setOptionValue( array &$options, string $option, $value ) {
445 $this->optionsSequence[] = [ $option, $value ];
447 if ( isset( $this->mOptDefs[$option] ) ) {
448 $multi = $this->mOptDefs[$option]['multiOccurrence'];
449 } else {
450 $multi = false;
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" );
464 private function error( string $msg ) {
465 $this->errors[] = $msg;
469 * Get any errors encountered while processing parameters.
471 * @return string[]
473 public function getErrors(): array {
474 return $this->errors;
478 * Whether any errors have been recorded so far.
480 * @return bool
482 public function hasErrors(): bool {
483 return (bool)$this->errors;
487 * Set the script name, for use in the help message
489 * @param string $name
491 public function setName( string $name ) {
492 $this->mName = $name;
496 * Get the script name, as shown in the help message
498 * @return string
500 public function getName(): string {
501 return $this->mName;
505 * Force option and argument values.
507 * @internal
509 * @param array $opts
510 * @param array $args
512 public function setOptionsAndArgs( array $opts, array $args ) {
513 $this->mOptions = $opts;
514 $this->mArgs = $args;
516 $this->optionsSequence = [];
517 foreach ( $opts as $name => $value ) {
518 $array = (array)$value;
520 foreach ( $array as $v ) {
521 $this->optionsSequence[] = [ $name, $v ];
527 * Run some validation checks on the params, etc.
529 * Error details can be obtained via getErrors().
531 * @return bool
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;
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;
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;
559 return $valid;
563 * Get help text.
565 * @return string
567 public function getHelp(): string {
568 $screenWidth = 80; // TODO: Calculate this!
569 $tab = " ";
570 $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
572 ksort( $this->mOptDefs );
574 $output = [];
576 // Description ...
577 if ( $this->mDescription ) {
578 $output[] = "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n";
580 $output[] = "\nUsage: {$this->usagePrefix} " . basename( $this->mName );
582 // ... append options ...
583 if ( $this->mOptDefs ) {
584 $output[] = ' [OPTION]...';
586 foreach ( $this->mOptDefs as $name => $opt ) {
587 if ( $opt['require'] ) {
588 $output[] = " --$name";
590 if ( $opt['withArg'] ) {
591 $vname = strtoupper( $name );
592 $output[] = " <$vname>";
598 // ... and append arguments.
599 if ( $this->mArgDefs ) {
600 $args = '';
601 foreach ( $this->mArgDefs as $arg ) {
602 $argRepr = $this->getArgRepresentation( $arg );
604 $args .= ' ';
605 $args .= $argRepr;
607 $output[] = $args;
609 $output[] = "\n\n";
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
620 $params = array_diff_key( $params, array_flip( $groupOptions ) );
623 $output[] = $this->formatHelpItems(
624 $params,
625 'Script specific options',
626 $descWidth, $tab
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";
642 $output[] = "\n";
645 return implode( '', $output );
648 private function formatHelpItems( array $items, $heading, $descWidth, $tab ) {
649 if ( $items === [] ) {
650 return '';
653 $output = [];
655 $output[] = "$heading:\n";
657 foreach ( $items as $name => $info ) {
658 if ( $info['shortName'] !== false ) {
659 $name .= ' (-' . $info['shortName'] . ')';
661 if ( $info['withArg'] ) {
662 $vname = strtoupper( $name );
663 $name .= " <$vname>";
666 $output[] =
667 wordwrap(
668 "$tab--$name: " . strtr( $info['desc'], [ "\n" => "\n$tab$tab" ] ),
669 $descWidth,
670 "\n$tab$tab"
671 ) . "\n";
674 $output[] = "\n";
676 return implode( '', $output );
680 * Returns the names of defined options
681 * @return string[]
683 public function getOptionNames(): array {
684 return array_keys( $this->mOptDefs );
688 * Returns any option values
689 * @return array
691 public function getOptions(): array {
692 return $this->mOptions;
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 ]
700 public function getOptionsSequence(): array {
701 return $this->optionsSequence;
705 * @param string $usagePrefix
707 public function setUsagePrefix( string $usagePrefix ) {
708 $this->usagePrefix = $usagePrefix;
712 * @param array $argInfo
714 * @return string
716 private function getArgRepresentation( array $argInfo ): string {
717 if ( $argInfo['require'] ) {
718 $rep = '<' . $argInfo['name'] . '>';
719 } else {
720 $rep = '[' . $argInfo['name'] . ']';
723 if ( $argInfo['multi'] ) {
724 $rep .= '...';
727 return $rep;