You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
7.2 KiB
299 lines
7.2 KiB
<?php
|
|
|
|
namespace EE_CLI\Dispatcher;
|
|
|
|
/**
|
|
* A leaf node in the command tree.
|
|
*
|
|
* @package EE_CLI
|
|
*/
|
|
class Subcommand extends CompositeCommand {
|
|
|
|
private $alias;
|
|
|
|
private $when_invoked;
|
|
|
|
function __construct( $parent, $name, $docparser, $when_invoked ) {
|
|
parent::__construct( $parent, $name, $docparser );
|
|
|
|
$this->when_invoked = $when_invoked;
|
|
|
|
$this->alias = $docparser->get_tag( 'alias' );
|
|
|
|
$this->synopsis = $docparser->get_synopsis();
|
|
if ( !$this->synopsis && $this->longdesc ) {
|
|
$this->synopsis = self::extract_synopsis( $this->longdesc );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the synopsis from PHPdoc string.
|
|
*
|
|
* @param string $longdesc Command docs via PHPdoc
|
|
* @return string
|
|
*/
|
|
private static function extract_synopsis( $longdesc ) {
|
|
preg_match_all( '/(.+?)[\r\n]+:/', $longdesc, $matches );
|
|
return implode( ' ', $matches[1] );
|
|
}
|
|
|
|
/**
|
|
* Subcommands can't have subcommands because they
|
|
* represent code to be executed.
|
|
*
|
|
* @return bool
|
|
*/
|
|
function can_have_subcommands() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the synopsis string for this subcommand.
|
|
* A synopsis defines what runtime arguments are
|
|
* expected, useful to humans and argument validation.
|
|
*
|
|
* @return string
|
|
*/
|
|
function get_synopsis() {
|
|
return $this->synopsis;
|
|
}
|
|
|
|
/**
|
|
* If an alias is set, grant access to it.
|
|
* Aliases permit subcommands to be instantiated
|
|
* with a secondary identity.
|
|
*
|
|
* @return string
|
|
*/
|
|
function get_alias() {
|
|
return $this->alias;
|
|
}
|
|
|
|
/**
|
|
* Print the usage details to the end user.
|
|
*
|
|
* @param string $prefix
|
|
*/
|
|
function show_usage( $prefix = 'usage: ' ) {
|
|
\EE_CLI::line( $this->get_usage( $prefix ) );
|
|
}
|
|
|
|
/**
|
|
* Get the usage of the subcommand as a formatted string.
|
|
*
|
|
* @param string $prefix
|
|
* @return string
|
|
*/
|
|
function get_usage( $prefix ) {
|
|
return sprintf( "%s%s %s",
|
|
$prefix,
|
|
implode( ' ', get_path( $this ) ),
|
|
$this->get_synopsis()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Wrapper for CLI Tools' prompt() method.
|
|
*
|
|
* @param string $question
|
|
* @param string $default
|
|
* @return string|false
|
|
*/
|
|
private function prompt( $question, $default ) {
|
|
|
|
try {
|
|
$response = \cli\prompt( $question, $default );
|
|
} catch( \Exception $e ) {
|
|
\EE_CLI::line();
|
|
return false;
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Interactively prompt the user for input
|
|
* based on defined synopsis and passed arguments.
|
|
*
|
|
* @param array $args
|
|
* @param array $assoc_args
|
|
* @return array
|
|
*/
|
|
private function prompt_args( $args, $assoc_args ) {
|
|
|
|
$synopsis = $this->get_synopsis();
|
|
|
|
if ( ! $synopsis )
|
|
return array( $args, $assoc_args );
|
|
|
|
$spec = array_filter( \EE_CLI\SynopsisParser::parse( $synopsis ), function( $spec_arg ) {
|
|
return in_array( $spec_arg['type'], array( 'generic', 'positional', 'assoc', 'flag' ) );
|
|
});
|
|
|
|
$spec = array_values( $spec );
|
|
|
|
// 'positional' arguments are positional (aka zero-indexed)
|
|
// so $args needs to be reset before prompting for new arguments
|
|
$args = array();
|
|
foreach( $spec as $key => $spec_arg ) {
|
|
|
|
$current_prompt = ( $key + 1 ) . '/' . count( $spec ) . ' ';
|
|
$default = ( $spec_arg['optional'] ) ? '' : false;
|
|
|
|
// 'generic' permits arbitrary key=value (e.g. [--<field>=<value>] )
|
|
if ( 'generic' == $spec_arg['type'] ) {
|
|
|
|
list( $key_token, $value_token ) = explode( '=', $spec_arg['token'] );
|
|
|
|
$repeat = false;
|
|
do {
|
|
if ( ! $repeat )
|
|
$key_prompt = $current_prompt . $key_token;
|
|
else
|
|
$key_prompt = str_repeat( " ", strlen( $current_prompt ) ) . $key_token;
|
|
|
|
$key = $this->prompt( $key_prompt, $default );
|
|
if ( false === $key )
|
|
return array( $args, $assoc_args );
|
|
|
|
if ( $key ) {
|
|
$key_prompt_count = strlen( $key_prompt ) - strlen( $value_token ) - 1;
|
|
$value_prompt = str_repeat( " ", $key_prompt_count ) . '=' . $value_token;
|
|
|
|
$value = $this->prompt( $value_prompt, $default );
|
|
if ( false === $value )
|
|
return array( $args, $assoc_args );
|
|
|
|
$assoc_args[$key] = $value;
|
|
|
|
$repeat = true;
|
|
$required = false;
|
|
} else {
|
|
$repeat = false;
|
|
}
|
|
|
|
} while( $required || $repeat );
|
|
|
|
} else {
|
|
|
|
$prompt = $current_prompt . $spec_arg['token'];
|
|
if ( 'flag' == $spec_arg['type'] )
|
|
$prompt .= ' (Y/n)';
|
|
|
|
$response = $this->prompt( $prompt, $default );
|
|
if ( false === $response )
|
|
return array( $args, $assoc_args );
|
|
|
|
if ( $response ) {
|
|
switch ( $spec_arg['type'] ) {
|
|
case 'positional':
|
|
if ( $spec_arg['repeating'] )
|
|
$response = explode( ' ', $response );
|
|
else
|
|
$response = array( $response );
|
|
$args = array_merge( $args, $response );
|
|
break;
|
|
case 'assoc':
|
|
$assoc_args[$spec_arg['name']] = $response;
|
|
break;
|
|
case 'flag':
|
|
if ( 'Y' == $response )
|
|
$assoc_args[$spec_arg['name']] = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return array( $args, $assoc_args );
|
|
}
|
|
|
|
/**
|
|
* Validate the supplied arguments to the command.
|
|
* Throws warnings or errors if arguments are missing
|
|
* or invalid.
|
|
*
|
|
* @param array $args
|
|
* @param array $assoc_args
|
|
* @param array $extra_args
|
|
* @return array list of invalid $assoc_args keys to unset
|
|
*/
|
|
private function validate_args( $args, $assoc_args, $extra_args ) {
|
|
$synopsis = $this->get_synopsis();
|
|
if ( !$synopsis )
|
|
return array();
|
|
|
|
$validator = new \EE_CLI\SynopsisValidator( $synopsis );
|
|
|
|
$cmd_path = implode( ' ', get_path( $this ) );
|
|
foreach ( $validator->get_unknown() as $token ) {
|
|
\EE_CLI::warning( sprintf(
|
|
"The `%s` command has an invalid synopsis part: %s",
|
|
$cmd_path, $token
|
|
) );
|
|
}
|
|
|
|
if ( !$validator->enough_positionals( $args ) ) {
|
|
$this->show_usage();
|
|
exit(1);
|
|
}
|
|
|
|
$unknown_positionals = $validator->unknown_positionals( $args );
|
|
if ( !empty( $unknown_positionals ) ) {
|
|
\EE_CLI::error( 'Too many positional arguments: ' .
|
|
implode( ' ', $unknown_positionals ) );
|
|
}
|
|
|
|
list( $errors, $to_unset ) = $validator->validate_assoc(
|
|
array_merge( \EE_CLI::get_config(), $extra_args, $assoc_args )
|
|
);
|
|
|
|
if ( $this->name != 'help' ) {
|
|
foreach ( $validator->unknown_assoc( $assoc_args ) as $key ) {
|
|
$errors['fatal'][] = "unknown --$key parameter";
|
|
}
|
|
}
|
|
|
|
if ( !empty( $errors['fatal'] ) ) {
|
|
$out = 'Parameter errors:';
|
|
foreach ( $errors['fatal'] as $key => $error ) {
|
|
$out .= "\n {$error}";
|
|
if ( $desc = $this->docparser->get_param_desc( $key ) ) {
|
|
$out .= " ({$desc})";
|
|
}
|
|
}
|
|
|
|
\EE_CLI::error( $out );
|
|
}
|
|
|
|
array_map( '\\EE_CLI::warning', $errors['warning'] );
|
|
|
|
return $to_unset;
|
|
}
|
|
|
|
/**
|
|
* Invoke the subcommand with the supplied arguments.
|
|
* Given a --prompt argument, interactively request input
|
|
* from the end user.
|
|
*
|
|
* @param array $args
|
|
* @param array $assoc_args
|
|
*/
|
|
public function invoke( $args, $assoc_args, $extra_args ) {
|
|
if ( \EE_CLI::get_config( 'prompt' ) )
|
|
list( $args, $assoc_args ) = $this->prompt_args( $args, $assoc_args );
|
|
|
|
$to_unset = $this->validate_args( $args, $assoc_args, $extra_args );
|
|
|
|
foreach ( $to_unset as $key ) {
|
|
unset( $assoc_args[ $key ] );
|
|
}
|
|
|
|
$path = get_path( $this->get_parent() );
|
|
\EE_CLI::do_hook( 'before_invoke:' . implode( ' ', array_slice( $path, 1 ) ) );
|
|
|
|
call_user_func( $this->when_invoked, $args, array_merge( $extra_args, $assoc_args ) );
|
|
|
|
\EE_CLI::do_hook( 'after_invoke:' . implode( ' ', array_slice( $path, 1 ) ) );
|
|
}
|
|
}
|
|
|