the option and 1 => parameter value. * * @var array */ public $orderedOptions = []; /** * Adds default args (--help, --quiet right now) */ public function __construct() { $this->addDefaultParams(); } /** * Set the description text. * @param string $text The text of the description */ public function addDescription( string $text ): void { $this->description = $text; } /** * Get the script's name * @return string */ public function getName(): string { return $this->self; } /** * Checks to see if a particular option is supported. Normally this means * it has been registered by the script via addOption. * @param string $name The name of the option * @return bool true if the option exists, false otherwise */ public function supportsOption( string $name ): bool { return isset( $this->params[$name] ); } /** * Add a parameter to the script. Will be displayed on --help * with the associated description * * @param string $name The name of the param (help, version, etc) * @param string $description The description of the param to show on --help * @param bool $required Is the param required? * @param bool $withArg Is an argument required with this option? * @param string|bool $shortName Character to use as short name * @param bool $multiOccurrence Can this option be passed multiple times? */ public function addOption( string $name, string $description, bool $required = false, bool $withArg = false, bool $shortName = false, bool $multiOccurrence = false ): void { $this->params[$name] = [ 'desc' => $description, 'require' => $required, 'withArg' => $withArg, 'shortName' => $shortName, 'multiOccurrence' => $multiOccurrence, ]; if ( $shortName !== false ) { $this->shortParamsMap[$shortName] = $name; } } /** * Checks to see if a particular option exists. * @param string $name The name of the option * @return bool */ public function hasOption( string $name ): bool { return isset( $this->options[$name] ); } /** * Get an option, or return the default. * * If the option was added to support multiple occurrences, * this will return an array. * * @param string $name The name of the param * @param mixed|null $default Anything you want, default null * @return mixed */ public function getOption( string $name, $default = null ) { if ( $this->hasOption( $name ) ) { return $this->options[$name]; } else { // Set it so we don't have to provide the default again $this->options[$name] = $default; return $this->options[$name]; } } /** * Add some args that are needed * @param string $arg Name of the arg, like 'start' * @param string $description Short description of the arg * @param bool $required Is this required? */ public function addArg( string $arg, string $description, bool $required = true ) { $this->argList[] = [ 'name' => $arg, 'desc' => $description, 'require' => $required ]; } /** * Remove an option. Useful for removing options that won't be used in your script. * @param string $name The option to remove. */ public function deleteOption( string $name ): void { unset( $this->params[$name] ); } /** * Sets whether to allow unregistered options, which are options passed to * a script that do not match an expected parameter. * @param bool $allow Should we allow? */ public function setAllowUnregisteredOptions( bool $allow ): void { $this->allowUnregisteredOptions = $allow; } /** * Does a given argument exist? * @param int $argId The integer value (from zero) for the arg * @return bool */ public function hasArg( int $argId = 0 ): bool { return isset( $this->args[$argId] ); } /** * Get an argument. * @param int $argId The integer value (from zero) for the arg * @param mixed|null $default The default if it doesn't exist * @return mixed */ public function getArg( int $argId = 0, $default = null ) { return $this->hasArg( $argId ) ? $this->args[$argId] : $default; } /** * @return bool */ public function isQuiet(): bool { return $this->quiet; } /** * Add the default parameters to the scripts */ public function addDefaultParams(): void { # Generic (non script dependent) options: $this->addOption( 'help', 'Display this help message', false, false, 'h' ); $this->addOption( 'quiet', 'Whether to suppress non-error output', false, false, 'q' ); # Save generic options to display them separately in help $this->genericParameters = $this->params; # Save additional script dependent options to display # them separately in help $this->dependentParameters = array_diff_key( $this->params, $this->genericParameters ); } /** * Clear all params and arguments. */ public function clearParamsAndArgs(): void { $this->options = []; $this->args = []; } /** * Load params and arguments from a given array * of command-line arguments * * @param array $argv */ public function loadWithArgv( array $argv ): void { $options = []; $args = []; $this->orderedOptions = []; # Parse arguments for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) { if ( $arg == '--' ) { # End of options, remainder should be considered arguments $arg = next( $argv ); while ( $arg !== false ) { $args[] = $arg; $arg = next( $argv ); } break; } elseif ( substr( $arg, 0, 2 ) == '--' ) { # Long options $option = substr( $arg, 2 ); if ( isset( $this->params[$option] ) && $this->params[$option]['withArg'] ) { $param = next( $argv ); if ( $param === false ) { $this->error( "\nERROR: $option parameter needs a value after it\n" ); $this->maybeHelp( true ); } $this->setParam( $options, $option, $param ); } else { $bits = explode( '=', $option, 2 ); $this->setParam( $options, $bits[0], $bits[1] ?? 1 ); } } elseif ( $arg == '-' ) { # Lonely "-", often used to indicate stdin or stdout. $args[] = $arg; } elseif ( substr( $arg, 0, 1 ) == '-' ) { # Short options $argLength = strlen( $arg ); for ( $p = 1; $p < $argLength; $p++ ) { $option = $arg[$p]; if ( !isset( $this->params[$option] ) && isset( $this->shortParamsMap[$option] ) ) { $option = $this->shortParamsMap[$option]; } if ( isset( $this->params[$option]['withArg'] ) && $this->params[$option]['withArg'] ) { $param = next( $argv ); if ( $param === false ) { $this->error( "\nERROR: $option parameter needs a value after it\n" ); $this->maybeHelp( true ); } $this->setParam( $options, $option, $param ); } else { $this->setParam( $options, $option, 1 ); } } } else { $args[] = $arg; } } $this->options = $options; $this->args = $args; } /** * Helper function used solely by loadParamsAndArgs * to prevent code duplication * * This sets the param in the options array based on * whether or not it can be specified multiple times. * * @param array &$options * @param string $option * @param mixed $value */ private function setParam( array &$options, string $option, $value ): void { $this->orderedOptions[] = [ $option, $value ]; if ( isset( $this->params[$option] ) ) { $multi = $this->params[$option]['multiOccurrence']; } else { $multi = false; } $exists = array_key_exists( $option, $options ); if ( $multi && $exists ) { $options[$option][] = $value; } elseif ( $multi ) { $options[$option] = [ $value ]; } elseif ( !$exists ) { $options[$option] = $value; } else { $this->error( "\nERROR: $option parameter given twice\n" ); $this->maybeHelp( true ); } } /** * Process command line arguments * $options becomes an array with keys set to the option names * $args becomes a zero-based array containing the non-option arguments * * @param string|null $self The name of the script, if any * @param array|null $opts An array of options, in form of key=>value * @param array|null $args An array of command line arguments */ public function loadParamsAndArgs( string $self = null, array $opts = null, array $args = null ): void { # If we were given opts or args, set those and return early if ( $self ) { $this->self = $self; } if ( $opts ) { $this->options = $opts; } if ( $args ) { $this->args = $args; } global $argv; $this->self = $argv[0]; $this->loadWithArgv( array_slice( $argv, 1 ) ); } /** * Run some validation checks on the params, etc */ public function validateParamsAndArgs(): void { $die = false; # Check to make sure we've got all the required options foreach ( $this->params as $opt => $info ) { if ( $info['require'] && !$this->hasOption( $opt ) ) { $this->error( "Param $opt required!" ); $die = true; } } # Check arg list too foreach ( $this->argList as $k => $info ) { if ( $info['require'] && !$this->hasArg( $k ) ) { $this->error( 'Argument <' . $info['name'] . '> required!' ); $die = true; } } if ( !$this->allowUnregisteredOptions ) { # Check for unexpected options foreach ( $this->options as $opt => $val ) { if ( !$this->supportsOption( $opt ) ) { $this->error( "Unexpected option $opt!" ); $die = true; } } } if ( $die ) { $this->maybeHelp( true ); } } /** * Maybe show the help. * @param bool $force Whether to force the help to show, default false */ public function maybeHelp( bool $force = false ): void { if ( !$force && !$this->hasOption( 'help' ) ) { return; } $screenWidth = 80; // TODO: Calculate this! $tab = " "; $descWidth = $screenWidth - ( 2 * strlen( $tab ) ); ksort( $this->params ); // Description ... if ( $this->description ) { print "\n" . wordwrap( $this->description, $screenWidth ) . "\n"; } $output = "\nUsage: php " . basename( $this->self ); // ... append parameters ... if ( $this->params ) { $output .= " [--" . implode( "|--", array_keys( $this->params ) ) . "]"; } // ... and append arguments. if ( $this->argList ) { $output .= ' '; foreach ( $this->argList as $k => $arg ) { if ( $arg['require'] ) { $output .= '<' . $arg['name'] . '>'; } else { $output .= '[' . $arg['name'] . ']'; } if ( $k < count( $this->argList ) - 1 ) { $output .= ' '; } } } print "$output\n\n"; # TODO abstract some repetitive code below // Generic parameters foreach ( $this->genericParameters as $par => $info ) { if ( $info['shortName'] !== false ) { $par .= " (-{$info['shortName']})"; } $this->output( wordwrap( "$tab--$par: " . $info['desc'], $descWidth, "\n$tab$tab" ) . "\n" ); } print "\n"; $scriptDependantParams = $this->dependentParameters; if ( count( $scriptDependantParams ) > 0 ) { print "Script dependent parameters:\n"; // Parameters description foreach ( $scriptDependantParams as $par => $info ) { if ( $info['shortName'] !== false ) { $par .= " (-{$info['shortName']})"; } $this->output( wordwrap( "$tab--$par: " . $info['desc'], $descWidth, "\n$tab$tab" ) . "\n" ); } print "\n"; } // Script specific parameters not defined on construction by // addDefaultParams() $scriptSpecificParams = array_diff_key( # all script parameters: $this->params, # remove the default parameters: $this->genericParameters, $this->dependentParameters ); if ( count( $scriptSpecificParams ) > 0 ) { print "Script specific parameters:\n"; // Parameters description foreach ( $scriptSpecificParams as $par => $info ) { if ( $info['shortName'] !== false ) { $par .= " (-{$info['shortName']})"; } $this->output( wordwrap( "$tab--$par: " . $info['desc'], $descWidth, "\n$tab$tab" ) . "\n" ); } print "\n"; } // Print arguments if ( count( $this->argList ) > 0 ) { print "Arguments:\n"; // Arguments description foreach ( $this->argList as $info ) { $openChar = $info['require'] ? '<' : '['; $closeChar = $info['require'] ? '>' : ']'; $this->output( wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " . $info['desc'], $descWidth, "\n$tab$tab" ) . "\n" ); } print "\n"; } die( 1 ); } /** * Override to redirect opts processor error messages. * * @param string $err */ protected function error( string $err ): void { error_log( $err ); } /** * Override to redirect opts processor output. * * @param string $out */ protected function output( string $out ): void { print $out; } }