Permalink
Cannot retrieve contributors at this time
1473 lines (1313 sloc)
42.4 KB
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use cli\Colors; | |
use Mustangostang\Spyc; | |
use WP_CLI\Configurator; | |
use WP_CLI\Dispatcher; | |
use WP_CLI\Dispatcher\CommandAddition; | |
use WP_CLI\Dispatcher\CommandFactory; | |
use WP_CLI\Dispatcher\CommandNamespace; | |
use WP_CLI\Dispatcher\CompositeCommand; | |
use WP_CLI\Dispatcher\RootCommand; | |
use WP_CLI\DocParser; | |
use WP_CLI\ExitException; | |
use WP_CLI\FileCache; | |
use WP_CLI\Loggers\Execution; | |
use WP_CLI\Process; | |
use WP_CLI\ProcessRun; | |
use WP_CLI\Runner; | |
use WP_CLI\SynopsisParser; | |
use WP_CLI\Utils; | |
use WP_CLI\WpHttpCacheManager; | |
/** | |
* Various utilities for WP-CLI commands. | |
*/ | |
class WP_CLI { | |
private static $logger; | |
private static $hooks = []; | |
private static $hooks_passed = []; | |
private static $capture_exit = false; | |
private static $deferred_additions = []; | |
/** | |
* Set the logger instance. | |
* | |
* @param object $logger Logger instance to set. | |
*/ | |
public static function set_logger( $logger ) { | |
self::$logger = $logger; | |
} | |
/** | |
* Get the logger instance. | |
* | |
* @return object $logger Logger instance. | |
*/ | |
public static function get_logger() { | |
return self::$logger; | |
} | |
/** | |
* Get the Configurator instance | |
* | |
* @return Configurator | |
*/ | |
public static function get_configurator() { | |
static $configurator; | |
if ( ! $configurator ) { | |
$configurator = new Configurator( WP_CLI_ROOT . '/php/config-spec.php' ); | |
} | |
return $configurator; | |
} | |
public static function get_root_command() { | |
static $root; | |
if ( ! $root ) { | |
$root = new RootCommand(); | |
} | |
return $root; | |
} | |
public static function get_runner() { | |
static $runner; | |
if ( ! $runner ) { | |
$runner = new Runner(); | |
} | |
return $runner; | |
} | |
/** | |
* @return FileCache | |
*/ | |
public static function get_cache() { | |
static $cache; | |
if ( ! $cache ) { | |
$home = Utils\get_home_dir(); | |
$dir = getenv( 'WP_CLI_CACHE_DIR' ) ? : "$home/.wp-cli/cache"; | |
$ttl = getenv( 'WP_CLI_CACHE_EXPIRY' ) ? : 15552000; | |
$max_size = getenv( 'WP_CLI_CACHE_MAX_SIZE' ) ? : 314572800; | |
// 6 months, 300mb | |
$cache = new FileCache( $dir, $ttl, $max_size ); | |
// Clean older files on shutdown with 1/50 probability. | |
// phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- no crypto and WP not loaded. | |
if ( 0 === mt_rand( 0, 50 ) ) { | |
register_shutdown_function( | |
function () use ( $cache ) { | |
$cache->clean(); | |
} | |
); | |
} | |
} | |
return $cache; | |
} | |
/** | |
* Set the context in which WP-CLI should be run | |
*/ | |
public static function set_url( $url ) { | |
self::debug( 'Set URL: ' . $url, 'bootstrap' ); | |
$url_parts = Utils\parse_url( $url ); | |
self::set_url_params( $url_parts ); | |
} | |
private static function set_url_params( $url_parts ) { | |
$f = function( $key ) use ( $url_parts ) { | |
return Utils\get_flag_value( $url_parts, $key, '' ); | |
}; | |
if ( isset( $url_parts['host'] ) ) { | |
if ( isset( $url_parts['scheme'] ) && 'https' === strtolower( $url_parts['scheme'] ) ) { | |
$_SERVER['HTTPS'] = 'on'; | |
} | |
$_SERVER['HTTP_HOST'] = $url_parts['host']; | |
if ( isset( $url_parts['port'] ) ) { | |
$_SERVER['HTTP_HOST'] .= ':' . $url_parts['port']; | |
} | |
$_SERVER['SERVER_NAME'] = $url_parts['host']; | |
} | |
$_SERVER['REQUEST_URI'] = $f( 'path' ) . ( isset( $url_parts['query'] ) ? '?' . $url_parts['query'] : '' ); | |
$_SERVER['SERVER_PORT'] = Utils\get_flag_value( $url_parts, 'port', '80' ); | |
$_SERVER['QUERY_STRING'] = $f( 'query' ); | |
} | |
/** | |
* @return WpHttpCacheManager | |
*/ | |
public static function get_http_cache_manager() { | |
static $http_cacher; | |
if ( ! $http_cacher ) { | |
$http_cacher = new WpHttpCacheManager( self::get_cache() ); | |
} | |
return $http_cacher; | |
} | |
/** | |
* Colorize a string for output. | |
* | |
* Yes, you can change the color of command line text too. For instance, | |
* here's how `WP_CLI::success()` colorizes "Success: " | |
* | |
* ``` | |
* WP_CLI::colorize( "%GSuccess:%n " ) | |
* ``` | |
* | |
* Uses `\cli\Colors::colorize()` to transform color tokens to display | |
* settings. Choose from the following tokens (and note 'reset'): | |
* | |
* * %y => ['color' => 'yellow'], | |
* * %g => ['color' => 'green'], | |
* * %b => ['color' => 'blue'], | |
* * %r => ['color' => 'red'], | |
* * %p => ['color' => 'magenta'], | |
* * %m => ['color' => 'magenta'], | |
* * %c => ['color' => 'cyan'], | |
* * %w => ['color' => 'grey'], | |
* * %k => ['color' => 'black'], | |
* * %n => ['color' => 'reset'], | |
* * %Y => ['color' => 'yellow', 'style' => 'bright'], | |
* * %G => ['color' => 'green', 'style' => 'bright'], | |
* * %B => ['color' => 'blue', 'style' => 'bright'], | |
* * %R => ['color' => 'red', 'style' => 'bright'], | |
* * %P => ['color' => 'magenta', 'style' => 'bright'], | |
* * %M => ['color' => 'magenta', 'style' => 'bright'], | |
* * %C => ['color' => 'cyan', 'style' => 'bright'], | |
* * %W => ['color' => 'grey', 'style' => 'bright'], | |
* * %K => ['color' => 'black', 'style' => 'bright'], | |
* * %N => ['color' => 'reset', 'style' => 'bright'], | |
* * %3 => ['background' => 'yellow'], | |
* * %2 => ['background' => 'green'], | |
* * %4 => ['background' => 'blue'], | |
* * %1 => ['background' => 'red'], | |
* * %5 => ['background' => 'magenta'], | |
* * %6 => ['background' => 'cyan'], | |
* * %7 => ['background' => 'grey'], | |
* * %0 => ['background' => 'black'], | |
* * %F => ['style' => 'blink'], | |
* * %U => ['style' => 'underline'], | |
* * %8 => ['style' => 'inverse'], | |
* * %9 => ['style' => 'bright'], | |
* * %_ => ['style' => 'bright'] | |
* | |
* @access public | |
* @category Output | |
* | |
* @param string $string String to colorize for output, with color tokens. | |
* @return string Colorized string. | |
*/ | |
public static function colorize( $string ) { | |
return Colors::colorize( $string, self::get_runner()->in_color() ); | |
} | |
/** | |
* Schedule a callback to be executed at a certain point. | |
* | |
* Hooks conceptually are very similar to WordPress actions. WP-CLI hooks | |
* are typically called before WordPress is loaded. | |
* | |
* WP-CLI hooks include: | |
* | |
* * `before_add_command:<command>` - Before the command is added. | |
* * `after_add_command:<command>` - After the command was added. | |
* * `before_invoke:<command>` - Just before a command is invoked. | |
* * `after_invoke:<command>` - Just after a command is invoked. | |
* * `find_command_to_run_pre` - Just before WP-CLI finds the command to run. | |
* * `before_registering_contexts` (1) - Before the contexts are registered. | |
* * `before_wp_load` - Just before the WP load process begins. | |
* * `before_wp_config_load` - After wp-config.php has been located. | |
* * `after_wp_config_load` - After wp-config.php has been loaded into scope. | |
* * `after_wp_load` - Just after the WP load process has completed. | |
* * `before_run_command` (3) - Just before the command is executed. | |
* | |
* The parentheses behind the hook name denote the number of arguments | |
* being passed into the hook. For such hooks, the callback should return | |
* the first argument again, making them work like a WP filter. | |
* | |
* WP-CLI commands can create their own hooks with `WP_CLI::do_hook()`. | |
* | |
* If additional arguments are passed through the `WP_CLI::do_hook()` call, | |
* these will be passed on to the callback provided by `WP_CLI::add_hook()`. | |
* | |
* ``` | |
* # `wp network meta` confirms command is executing in multisite context. | |
* WP_CLI::add_command( 'network meta', 'Network_Meta_Command', array( | |
* 'before_invoke' => function () { | |
* if ( !is_multisite() ) { | |
* WP_CLI::error( 'This is not a multisite installation.' ); | |
* } | |
* } | |
* ) ); | |
* ``` | |
* | |
* @access public | |
* @category Registration | |
* | |
* @param string $when Identifier for the hook. | |
* @param mixed $callback Callback to execute when hook is called. | |
* @return null | |
*/ | |
public static function add_hook( $when, $callback ) { | |
if ( array_key_exists( $when, self::$hooks_passed ) ) { | |
self::debug( | |
sprintf( | |
'Immediately invoking on passed hook "%s": %s', | |
$when, | |
Utils\describe_callable( $callback ) | |
), | |
'hooks' | |
); | |
call_user_func_array( $callback, (array) self::$hooks_passed[ $when ] ); | |
} | |
self::$hooks[ $when ][] = $callback; | |
} | |
/** | |
* Execute callbacks registered to a given hook. | |
* | |
* See `WP_CLI::add_hook()` for details on WP-CLI's internal hook system. | |
* Commands can provide and call their own hooks. | |
* | |
* @access public | |
* @category Registration | |
* | |
* @param string $when Identifier for the hook. | |
* @param mixed ...$args Optional. Arguments that will be passed onto the | |
* callback provided by `WP_CLI::add_hook()`. | |
* @return null|mixed Returns the first optional argument if optional | |
* arguments were passed, otherwise returns null. | |
*/ | |
public static function do_hook( $when, ...$args ) { | |
self::$hooks_passed[ $when ] = $args; | |
$has_args = count( $args ) > 0; | |
if ( ! isset( self::$hooks[ $when ] ) ) { | |
if ( $has_args ) { | |
return $args[0]; | |
} | |
return null; | |
} | |
self::debug( | |
sprintf( | |
'Processing hook "%s" with %d callbacks', | |
$when, | |
count( self::$hooks[ $when ] ) | |
), | |
'hooks' | |
); | |
foreach ( self::$hooks[ $when ] as $callback ) { | |
self::debug( | |
sprintf( | |
'On hook "%s": %s', | |
$when, | |
Utils\describe_callable( $callback ) | |
), | |
'hooks' | |
); | |
if ( $has_args ) { | |
$return_value = $callback( ...$args ); | |
if ( isset( $return_value ) ) { | |
$args[0] = $return_value; | |
} | |
} else { | |
$callback(); | |
} | |
} | |
if ( $has_args ) { | |
return $args[0]; | |
} | |
return null; | |
} | |
/** | |
* Add a callback to a WordPress action or filter. | |
* | |
* `add_action()` without needing access to `add_action()`. If WordPress is | |
* already loaded though, you should use `add_action()` (and `add_filter()`) | |
* instead. | |
* | |
* @access public | |
* @category Registration | |
* | |
* @param string $tag Named WordPress action or filter. | |
* @param mixed $function_to_add Callable to execute when the action or filter is evaluated. | |
* @param integer $priority Priority to add the callback as. | |
* @param integer $accepted_args Number of arguments to pass to callback. | |
* @return true | |
*/ | |
public static function add_wp_hook( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { | |
global $wp_filter, $merged_filters; | |
if ( function_exists( 'add_filter' ) ) { | |
add_filter( $tag, $function_to_add, $priority, $accepted_args ); | |
} else { | |
$idx = self::wp_hook_build_unique_id( $tag, $function_to_add, $priority ); | |
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- This is intentional & the purpose of this function. | |
$wp_filter[ $tag ][ $priority ][ $idx ] = [ | |
'function' => $function_to_add, | |
'accepted_args' => $accepted_args, | |
]; | |
unset( $merged_filters[ $tag ] ); | |
} | |
return true; | |
} | |
/** | |
* Build Unique ID for storage and retrieval. | |
* | |
* Essentially _wp_filter_build_unique_id() without needing access to _wp_filter_build_unique_id() | |
*/ | |
private static function wp_hook_build_unique_id( $tag, $function, $priority ) { | |
global $wp_filter; | |
static $filter_id_count = 0; | |
if ( is_string( $function ) ) { | |
return $function; | |
} | |
if ( is_object( $function ) ) { | |
// Closures are currently implemented as objects | |
$function = [ $function, '' ]; | |
} else { | |
$function = (array) $function; | |
} | |
if ( is_object( $function[0] ) ) { | |
// Object Class Calling | |
if ( function_exists( 'spl_object_hash' ) ) { | |
return spl_object_hash( $function[0] ) . $function[1]; | |
} | |
$obj_idx = get_class( $function[0] ) . $function[1]; | |
if ( ! isset( $function[0]->wp_filter_id ) ) { | |
if ( false === $priority ) { | |
return false; | |
} | |
$obj_idx .= isset( $wp_filter[ $tag ][ $priority ] ) ? count( (array) $wp_filter[ $tag ][ $priority ] ) : $filter_id_count; | |
$function[0]->wp_filter_id = $filter_id_count; | |
++$filter_id_count; | |
} else { | |
$obj_idx .= $function[0]->wp_filter_id; | |
} | |
return $obj_idx; | |
} | |
if ( is_string( $function[0] ) ) { | |
// Static Calling | |
return $function[0] . '::' . $function[1]; | |
} | |
} | |
/** | |
* Register a command to WP-CLI. | |
* | |
* WP-CLI supports using any callable class, function, or closure as a | |
* command. `WP_CLI::add_command()` is used for both internal and | |
* third-party command registration. | |
* | |
* Command arguments are parsed from PHPDoc by default, but also can be | |
* supplied as an optional third argument during registration. | |
* | |
* ``` | |
* # Register a custom 'foo' command to output a supplied positional param. | |
* # | |
* # $ wp foo bar --append=qux | |
* # Success: bar qux | |
* | |
* /** | |
* * My awesome closure command | |
* * | |
* * <message> | |
* * : An awesome message to display | |
* * | |
* * --append=<message> | |
* * : An awesome message to append to the original message. | |
* * | |
* * @when before_wp_load | |
* *\/ | |
* $foo = function( $args, $assoc_args ) { | |
* WP_CLI::success( $args[0] . ' ' . $assoc_args['append'] ); | |
* }; | |
* WP_CLI::add_command( 'foo', $foo ); | |
* ``` | |
* | |
* @access public | |
* @category Registration | |
* | |
* @param string $name Name for the command (e.g. "post list" or "site empty"). | |
* @param callable $callable Command implementation as a class, function or closure. | |
* @param array $args { | |
* Optional. An associative array with additional registration parameters. | |
* | |
* @type callable $before_invoke Callback to execute before invoking the command. | |
* @type callable $after_invoke Callback to execute after invoking the command. | |
* @type string $shortdesc Short description (80 char or less) for the command. | |
* @type string $longdesc Description of arbitrary length for examples, etc. | |
* @type string $synopsis The synopsis for the command (string or array). | |
* @type string $when Execute callback on a named WP-CLI hook (e.g. before_wp_load). | |
* @type bool $is_deferred Whether the command addition had already been deferred. | |
* } | |
* @return bool True on success, false if deferred, hard error if registration failed. | |
*/ | |
public static function add_command( $name, $callable, $args = [] ) { | |
// Bail immediately if the WP-CLI executable has not been run. | |
if ( ! defined( 'WP_CLI' ) ) { | |
return false; | |
} | |
$valid = false; | |
if ( is_callable( $callable ) ) { | |
$valid = true; | |
} elseif ( is_string( $callable ) && class_exists( (string) $callable ) ) { | |
$valid = true; | |
} elseif ( is_object( $callable ) ) { | |
$valid = true; | |
} elseif ( Utils\is_valid_class_and_method_pair( $callable ) ) { | |
$valid = true; | |
} | |
if ( ! $valid ) { | |
if ( is_array( $callable ) ) { | |
$callable[0] = is_object( $callable[0] ) ? get_class( $callable[0] ) : $callable[0]; | |
$callable = [ $callable[0], $callable[1] ]; | |
} | |
self::error( sprintf( 'Callable %s does not exist, and cannot be registered as `wp %s`.', json_encode( $callable ), $name ) ); | |
} | |
$addition = new CommandAddition(); | |
self::do_hook( "before_add_command:{$name}", $addition ); | |
if ( $addition->was_aborted() ) { | |
self::warning( "Aborting the addition of the command '{$name}' with reason: {$addition->get_reason()}." ); | |
return false; | |
} | |
foreach ( [ 'before_invoke', 'after_invoke' ] as $when ) { | |
if ( isset( $args[ $when ] ) ) { | |
self::add_hook( "{$when}:{$name}", $args[ $when ] ); | |
} | |
} | |
$path = preg_split( '/\s+/', $name ); | |
$leaf_name = array_pop( $path ); | |
$command = self::get_root_command(); | |
while ( ! empty( $path ) ) { | |
$subcommand_name = $path[0]; | |
$parent = implode( ' ', $path ); | |
$subcommand = $command->find_subcommand( $path ); | |
// Parent not found. Defer addition or create an empty container as | |
// needed. | |
if ( ! $subcommand ) { | |
if ( isset( $args['is_deferred'] ) && $args['is_deferred'] ) { | |
$subcommand = new CompositeCommand( | |
$command, | |
$subcommand_name, | |
new DocParser( '' ) | |
); | |
self::debug( | |
"Adding empty container for deferred command: {$name}", | |
'commands' | |
); | |
$command->add_subcommand( $subcommand_name, $subcommand ); | |
} else { | |
self::debug( "Deferring command: {$name}", 'commands' ); | |
self::defer_command_addition( | |
$name, | |
$parent, | |
$callable, | |
$args | |
); | |
return false; | |
} | |
} | |
$command = $subcommand; | |
} | |
$leaf_command = CommandFactory::create( $leaf_name, $callable, $command ); | |
// Only add a command namespace if the command itself does not exist yet. | |
if ( $leaf_command instanceof CommandNamespace | |
&& array_key_exists( $leaf_name, $command->get_subcommands() ) ) { | |
return false; | |
} | |
// Reattach commands attached to namespace to real command. | |
$subcommand_name = (array) $leaf_name; | |
$existing_command = $command->find_subcommand( $subcommand_name ); | |
if ( $existing_command instanceof CompositeCommand && $existing_command->can_have_subcommands() ) { | |
if ( $leaf_command instanceof CommandNamespace || ! $leaf_command->can_have_subcommands() ) { | |
$command_to_keep = $existing_command; | |
} else { | |
$command_to_keep = $leaf_command; | |
} | |
self::merge_sub_commands( $command_to_keep, $existing_command, $leaf_command ); | |
} | |
/** @var Dispatcher\Subcommand|Dispatcher\CompositeCommand|Dispatcher\CommandNamespace $leaf_command */ | |
if ( ! $command->can_have_subcommands() ) { | |
throw new Exception( | |
sprintf( | |
"'%s' can't have subcommands.", | |
implode( ' ', Dispatcher\get_path( $command ) ) | |
) | |
); | |
} | |
if ( isset( $args['shortdesc'] ) ) { | |
$leaf_command->set_shortdesc( $args['shortdesc'] ); | |
} | |
if ( isset( $args['longdesc'] ) ) { | |
$leaf_command->set_longdesc( $args['longdesc'] ); | |
} | |
if ( isset( $args['synopsis'] ) ) { | |
if ( is_string( $args['synopsis'] ) ) { | |
$leaf_command->set_synopsis( $args['synopsis'] ); | |
} elseif ( is_array( $args['synopsis'] ) ) { | |
$synopsis = SynopsisParser::render( $args['synopsis'] ); | |
$leaf_command->set_synopsis( $synopsis ); | |
$long_desc = ''; | |
$bits = explode( ' ', $synopsis ); | |
foreach ( $args['synopsis'] as $key => $arg ) { | |
$long_desc .= $bits[ $key ] . "\n"; | |
if ( ! empty( $arg['description'] ) ) { | |
$long_desc .= ': ' . $arg['description'] . "\n"; | |
} | |
$yamlify = []; | |
foreach ( [ 'default', 'options' ] as $key ) { | |
if ( isset( $arg[ $key ] ) ) { | |
$yamlify[ $key ] = $arg[ $key ]; | |
} | |
} | |
if ( ! empty( $yamlify ) ) { | |
$long_desc .= Spyc::YAMLDump( $yamlify ); | |
$long_desc .= '---' . "\n"; | |
} | |
$long_desc .= "\n"; | |
} | |
if ( ! empty( $long_desc ) ) { | |
$long_desc = rtrim( $long_desc, "\r\n" ); | |
$long_desc = '## OPTIONS' . "\n\n" . $long_desc; | |
if ( ! empty( $args['longdesc'] ) ) { | |
$long_desc .= "\n\n" . ltrim( $args['longdesc'], "\r\n" ); | |
} | |
$leaf_command->set_longdesc( $long_desc ); | |
} | |
} | |
} | |
if ( isset( $args['when'] ) ) { | |
self::get_runner()->register_early_invoke( $args['when'], $leaf_command ); | |
} | |
if ( ! empty( $parent ) ) { | |
$sub_command = trim( str_replace( $parent, '', $name ) ); | |
self::debug( "Adding command: {$sub_command} in {$parent} Namespace", 'commands' ); | |
} else { | |
self::debug( "Adding command: {$name}", 'commands' ); | |
} | |
$command->add_subcommand( $leaf_name, $leaf_command ); | |
self::do_hook( "after_add_command:{$name}" ); | |
return true; | |
} | |
/** | |
* Merge the sub-commands of two commands into a single command to keep. | |
* | |
* @param CompositeCommand $command_to_keep Command to merge the sub commands into. This is typically one of the | |
* two others. | |
* @param CompositeCommand $old_command Command that was already registered. | |
* @param CompositeCommand $new_command New command that is being added. | |
*/ | |
private static function merge_sub_commands( | |
CompositeCommand $command_to_keep, | |
CompositeCommand $old_command, | |
CompositeCommand $new_command | |
) { | |
foreach ( $old_command->get_subcommands() as $subname => $subcommand ) { | |
$command_to_keep->add_subcommand( $subname, $subcommand, false ); | |
} | |
foreach ( $new_command->get_subcommands() as $subname => $subcommand ) { | |
$command_to_keep->add_subcommand( $subname, $subcommand, true ); | |
} | |
} | |
/** | |
* Defer command addition for a sub-command if the parent command is not yet | |
* registered. | |
* | |
* @param string $name Name for the sub-command. | |
* @param string $parent Name for the parent command. | |
* @param string $callable Command implementation as a class, function or closure. | |
* @param array $args Optional. See `WP_CLI::add_command()` for details. | |
*/ | |
private static function defer_command_addition( $name, $parent, $callable, $args = [] ) { | |
$args['is_deferred'] = true; | |
self::$deferred_additions[ $name ] = [ | |
'parent' => $parent, | |
'callable' => $callable, | |
'args' => $args, | |
]; | |
self::add_hook( | |
"after_add_command:$parent", | |
function () use ( $name ) { | |
$deferred_additions = WP_CLI::get_deferred_additions(); | |
if ( ! array_key_exists( $name, $deferred_additions ) ) { | |
return; | |
} | |
$callable = $deferred_additions[ $name ]['callable']; | |
$args = $deferred_additions[ $name ]['args']; | |
WP_CLI::remove_deferred_addition( $name ); | |
WP_CLI::add_command( $name, $callable, $args ); | |
} | |
); | |
} | |
/** | |
* Get the list of outstanding deferred command additions. | |
* | |
* @return array Array of outstanding command additions. | |
*/ | |
public static function get_deferred_additions() { | |
return self::$deferred_additions; | |
} | |
/** | |
* Remove a command addition from the list of outstanding deferred additions. | |
*/ | |
public static function remove_deferred_addition( $name ) { | |
if ( ! array_key_exists( $name, self::$deferred_additions ) ) { | |
self::warning( "Trying to remove a non-existent command addition '{$name}'." ); | |
} | |
unset( self::$deferred_additions[ $name ] ); | |
} | |
/** | |
* Display informational message without prefix, and ignore `--quiet`. | |
* | |
* Message is written to STDOUT. `WP_CLI::log()` is typically recommended; | |
* `WP_CLI::line()` is included for historical compat. | |
* | |
* @access public | |
* @category Output | |
* | |
* @param string $message Message to display to the end user. | |
* @return null | |
*/ | |
public static function line( $message = '' ) { | |
echo $message . "\n"; | |
} | |
/** | |
* Display informational message without prefix. | |
* | |
* Message is written to STDOUT, or discarded when `--quiet` flag is supplied. | |
* | |
* ``` | |
* # `wp cli update` lets user know of each step in the update process. | |
* WP_CLI::log( sprintf( 'Downloading from %s...', $download_url ) ); | |
* ``` | |
* | |
* @access public | |
* @category Output | |
* | |
* @param string $message Message to write to STDOUT. | |
*/ | |
public static function log( $message ) { | |
if ( null === self::$logger ) { | |
return; | |
} | |
self::$logger->info( $message ); | |
} | |
/** | |
* Display success message prefixed with "Success: ". | |
* | |
* Success message is written to STDOUT. | |
* | |
* Typically recommended to inform user of successful script conclusion. | |
* | |
* ``` | |
* # wp rewrite flush expects 'rewrite_rules' option to be set after flush. | |
* flush_rewrite_rules( \WP_CLI\Utils\get_flag_value( $assoc_args, 'hard' ) ); | |
* if ( ! get_option( 'rewrite_rules' ) ) { | |
* WP_CLI::warning( "Rewrite rules are empty." ); | |
* } else { | |
* WP_CLI::success( 'Rewrite rules flushed.' ); | |
* } | |
* ``` | |
* | |
* @access public | |
* @category Output | |
* | |
* @param string $message Message to write to STDOUT. | |
* @return null | |
*/ | |
public static function success( $message ) { | |
if ( null === self::$logger ) { | |
return; | |
} | |
self::$logger->success( $message ); | |
} | |
/** | |
* Display debug message prefixed with "Debug: " when `--debug` is used. | |
* | |
* Debug message is written to STDERR, and includes script execution time. | |
* | |
* Helpful for optionally showing greater detail when needed. Used throughout | |
* WP-CLI bootstrap process for easier debugging and profiling. | |
* | |
* ``` | |
* # Called in `WP_CLI\Runner::set_wp_root()`. | |
* private static function set_wp_root( $path ) { | |
* define( 'ABSPATH', Utils\trailingslashit( $path ) ); | |
* WP_CLI::debug( 'ABSPATH defined: ' . ABSPATH ); | |
* $_SERVER['DOCUMENT_ROOT'] = realpath( $path ); | |
* } | |
* | |
* # Debug details only appear when `--debug` is used. | |
* # $ wp --debug | |
* # [...] | |
* # Debug: ABSPATH defined: /srv/www/wordpress-develop.dev/src/ (0.225s) | |
* ``` | |
* | |
* @access public | |
* @category Output | |
* | |
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR. | |
* @param string|bool $group Organize debug message to a specific group. | |
* Use `false` to not group the message. | |
* @return null | |
*/ | |
public static function debug( $message, $group = false ) { | |
static $storage = []; | |
if ( ! self::$logger ) { | |
$storage[] = [ $message, $group ]; | |
return; | |
} | |
if ( ! empty( $storage ) && self::$logger ) { | |
foreach ( $storage as $entry ) { | |
list( $stored_message, $stored_group ) = $entry; | |
self::$logger->debug( self::error_to_string( $stored_message ), $stored_group ); | |
} | |
$storage = []; | |
} | |
self::$logger->debug( self::error_to_string( $message ), $group ); | |
} | |
/** | |
* Display warning message prefixed with "Warning: ". | |
* | |
* Warning message is written to STDERR. | |
* | |
* Use instead of `WP_CLI::debug()` when script execution should be permitted | |
* to continue. | |
* | |
* ``` | |
* # `wp plugin activate` skips activation when plugin is network active. | |
* $status = $this->get_status( $plugin->file ); | |
* // Network-active is the highest level of activation status | |
* if ( 'active-network' === $status ) { | |
* WP_CLI::warning( "Plugin '{$plugin->name}' is already network active." ); | |
* continue; | |
* } | |
* ``` | |
* | |
* @access public | |
* @category Output | |
* | |
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR. | |
* @return null | |
*/ | |
public static function warning( $message ) { | |
if ( null === self::$logger ) { | |
return; | |
} | |
self::$logger->warning( self::error_to_string( $message ) ); | |
} | |
/** | |
* Display error message prefixed with "Error: " and exit script. | |
* | |
* Error message is written to STDERR. Defaults to halting script execution | |
* with return code 1. | |
* | |
* Use `WP_CLI::warning()` instead when script execution should be permitted | |
* to continue. | |
* | |
* ``` | |
* # `wp cache flush` considers flush failure to be a fatal error. | |
* if ( false === wp_cache_flush() ) { | |
* WP_CLI::error( 'The object cache could not be flushed.' ); | |
* } | |
* ``` | |
* | |
* @access public | |
* @category Output | |
* | |
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR. | |
* @param boolean|integer $exit True defaults to exit(1). | |
* @return null | |
*/ | |
public static function error( $message, $exit = true ) { | |
if ( null !== self::$logger && ! isset( self::get_runner()->assoc_args['completions'] ) ) { | |
self::$logger->error( self::error_to_string( $message ) ); | |
} | |
$return_code = false; | |
if ( true === $exit ) { | |
$return_code = 1; | |
} elseif ( is_int( $exit ) && $exit >= 1 ) { | |
$return_code = $exit; | |
} | |
if ( $return_code ) { | |
if ( self::$capture_exit ) { | |
throw new ExitException( null, $return_code ); | |
} | |
exit( $return_code ); | |
} | |
} | |
/** | |
* Halt script execution with a specific return code. | |
* | |
* Permits script execution to be overloaded by `WP_CLI::runcommand()` | |
* | |
* @access public | |
* @category Output | |
* | |
* @param integer $return_code | |
* @return never | |
*/ | |
public static function halt( $return_code ) { | |
if ( self::$capture_exit ) { | |
throw new ExitException( null, $return_code ); | |
} | |
exit( $return_code ); | |
} | |
/** | |
* Display a multi-line error message in a red box. Doesn't exit script. | |
* | |
* Error message is written to STDERR. | |
* | |
* @access public | |
* @category Output | |
* | |
* @param array $message_lines Multi-line error message to be displayed. | |
*/ | |
public static function error_multi_line( $message_lines ) { | |
if ( null === self::$logger ) { | |
return; | |
} | |
if ( ! isset( self::get_runner()->assoc_args['completions'] ) && is_array( $message_lines ) ) { | |
self::$logger->error_multi_line( array_map( [ __CLASS__, 'error_to_string' ], $message_lines ) ); | |
} | |
} | |
/** | |
* Ask for confirmation before running a destructive operation. | |
* | |
* If 'y' is provided to the question, the script execution continues. If | |
* 'n' or any other response is provided to the question, script exits. | |
* | |
* ``` | |
* # `wp db drop` asks for confirmation before dropping the database. | |
* | |
* WP_CLI::confirm( "Are you sure you want to drop the database?", $assoc_args ); | |
* ``` | |
* | |
* @access public | |
* @category Input | |
* | |
* @param string $question Question to display before the prompt. | |
* @param array $assoc_args Skips prompt if 'yes' is provided. | |
*/ | |
public static function confirm( $question, $assoc_args = [] ) { | |
if ( ! Utils\get_flag_value( $assoc_args, 'yes' ) ) { | |
fwrite( STDOUT, $question . ' [y/n] ' ); | |
$answer = strtolower( trim( fgets( STDIN ) ) ); | |
if ( 'y' !== $answer ) { | |
exit; | |
} | |
} | |
} | |
/** | |
* Read value from a positional argument or from STDIN. | |
* | |
* @param array $args The list of positional arguments. | |
* @param int $index At which position to check for the value. | |
* | |
* @return string | |
*/ | |
public static function get_value_from_arg_or_stdin( $args, $index ) { | |
if ( isset( $args[ $index ] ) ) { | |
$raw_value = $args[ $index ]; | |
} else { | |
// We don't use file_get_contents() here because it doesn't handle | |
// Ctrl-D properly, when typing in the value interactively. | |
$raw_value = ''; | |
while ( false !== ( $line = fgets( STDIN ) ) ) { | |
$raw_value .= $line; | |
} | |
} | |
return $raw_value; | |
} | |
/** | |
* Read a value, from various formats. | |
* | |
* @access public | |
* @category Input | |
* | |
* @param mixed $raw_value | |
* @param array $assoc_args | |
*/ | |
public static function read_value( $raw_value, $assoc_args = [] ) { | |
if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) { | |
$value = json_decode( $raw_value, true ); | |
if ( null === $value ) { | |
self::error( sprintf( 'Invalid JSON: %s', $raw_value ) ); | |
} | |
} else { | |
$value = $raw_value; | |
} | |
return $value; | |
} | |
/** | |
* Display a value, in various formats | |
* | |
* @param mixed $value Value to display. | |
* @param array $assoc_args Arguments passed to the command, determining format. | |
*/ | |
public static function print_value( $value, $assoc_args = [] ) { | |
if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) { | |
$value = json_encode( $value ); | |
} elseif ( Utils\get_flag_value( $assoc_args, 'format' ) === 'yaml' ) { | |
$value = Spyc::YAMLDump( $value, 2, 0 ); | |
} elseif ( is_array( $value ) || is_object( $value ) ) { | |
$value = var_export( $value, true ); | |
} | |
echo $value . "\n"; | |
} | |
/** | |
* Convert a WP_Error or Exception into a string | |
* | |
* @param string|WP_Error|Exception|Throwable $errors | |
* @throws InvalidArgumentException | |
* | |
* @return string | |
*/ | |
public static function error_to_string( $errors ) { | |
if ( is_string( $errors ) ) { | |
return $errors; | |
} | |
// Only json_encode() the data when it needs it | |
$render_data = function( $data ) { | |
if ( is_array( $data ) || is_object( $data ) ) { | |
return json_encode( $data ); | |
} | |
return '"' . $data . '"'; | |
}; | |
if ( $errors instanceof WP_Error ) { | |
foreach ( $errors->get_error_messages() as $message ) { | |
if ( $errors->get_error_data() ) { | |
return $message . ' ' . $render_data( $errors->get_error_data() ); | |
} | |
return $message; | |
} | |
} | |
// PHP 7+: internal and user exceptions must implement Throwable interface. | |
// PHP 5: internal and user exceptions must extend Exception class. | |
if ( interface_exists( 'Throwable' ) && ( $errors instanceof Throwable ) || ( $errors instanceof Exception ) ) { | |
return get_class( $errors ) . ': ' . $errors->getMessage(); | |
} | |
throw new InvalidArgumentException( | |
sprintf( | |
"Unsupported argument type passed to WP_CLI::error_to_string(): '%s'", | |
gettype( $errors ) | |
) | |
); | |
} | |
/** | |
* Launch an arbitrary external process that takes over I/O. | |
* | |
* ``` | |
* # `wp core download` falls back to the `tar` binary when PharData isn't available | |
* if ( ! class_exists( 'PharData' ) ) { | |
* $cmd = "tar xz --strip-components=1 --directory=%s -f $tarball"; | |
* WP_CLI::launch( Utils\esc_cmd( $cmd, $dest ) ); | |
* return; | |
* } | |
* ``` | |
* | |
* @access public | |
* @category Execution | |
* | |
* @param string $command External process to launch. | |
* @param boolean $exit_on_error Whether to exit if the command returns an elevated return code. | |
* @param boolean $return_detailed Whether to return an exit status (default) or detailed execution results. | |
* @return int|ProcessRun The command exit status, or a ProcessRun object for full details. | |
*/ | |
public static function launch( $command, $exit_on_error = true, $return_detailed = false ) { | |
Utils\check_proc_available( 'launch' ); | |
$proc = Process::create( $command ); | |
$results = $proc->run(); | |
if ( -1 === $results->return_code ) { | |
self::warning( "Spawned process returned exit code {$results->return_code}, which could be caused by a custom compiled version of PHP that uses the --enable-sigchild option." ); | |
} | |
if ( $results->return_code && $exit_on_error ) { | |
exit( $results->return_code ); | |
} | |
if ( $return_detailed ) { | |
return $results; | |
} | |
return $results->return_code; | |
} | |
/** | |
* Run a WP-CLI command in a new process reusing the current runtime arguments. | |
* | |
* Use `WP_CLI::runcommand()` instead, which is easier to use and works better. | |
* | |
* Note: While this command does persist a limited set of runtime arguments, | |
* it *does not* persist environment variables. Practically speaking, WP-CLI | |
* packages won't be loaded when using WP_CLI::launch_self() because the | |
* launched process doesn't have access to the current process $HOME. | |
* | |
* @access public | |
* @category Execution | |
* | |
* @param string $command WP-CLI command to call. | |
* @param array $args Positional arguments to include when calling the command. | |
* @param array $assoc_args Associative arguments to include when calling the command. | |
* @param bool $exit_on_error Whether to exit if the command returns an elevated return code. | |
* @param bool $return_detailed Whether to return an exit status (default) or detailed execution results. | |
* @param array $runtime_args Override one or more global args (path,url,user,allow-root) | |
* @return int|ProcessRun The command exit status, or a ProcessRun instance | |
*/ | |
public static function launch_self( $command, $args = [], $assoc_args = [], $exit_on_error = true, $return_detailed = false, $runtime_args = [] ) { | |
$reused_runtime_args = [ | |
'path', | |
'url', | |
'user', | |
'allow-root', | |
]; | |
foreach ( $reused_runtime_args as $key ) { | |
if ( isset( $runtime_args[ $key ] ) ) { | |
$assoc_args[ $key ] = $runtime_args[ $key ]; | |
continue; | |
} | |
$value = self::get_runner()->config[ $key ]; | |
if ( $value ) { | |
$assoc_args[ $key ] = $value; | |
} | |
} | |
$php_bin = escapeshellarg( Utils\get_php_binary() ); | |
$script_path = $GLOBALS['argv'][0]; | |
if ( getenv( 'WP_CLI_CONFIG_PATH' ) ) { | |
$config_path = getenv( 'WP_CLI_CONFIG_PATH' ); | |
} else { | |
$config_path = Utils\get_home_dir() . '/.wp-cli/config.yml'; | |
} | |
$config_path = escapeshellarg( $config_path ); | |
$args = implode( ' ', array_map( 'escapeshellarg', $args ) ); | |
$assoc_args = Utils\assoc_args_to_str( $assoc_args ); | |
$full_command = "WP_CLI_CONFIG_PATH={$config_path} {$php_bin} {$script_path} {$command} {$args} {$assoc_args}"; | |
return self::launch( $full_command, $exit_on_error, $return_detailed ); | |
} | |
/** | |
* Get the path to the PHP binary used when executing WP-CLI. | |
* | |
* Environment values permit specific binaries to be indicated. | |
* | |
* Note: moved to Utils, left for BC. | |
* | |
* @access public | |
* @category System | |
* | |
* @return string | |
*/ | |
public static function get_php_binary() { | |
return Utils\get_php_binary(); | |
} | |
/** | |
* Confirm that a global configuration parameter does exist. | |
* | |
* @access public | |
* @category Input | |
* | |
* @param string $key Config parameter key to check. | |
* | |
* @return bool | |
*/ | |
public static function has_config( $key ) { | |
return array_key_exists( $key, self::get_runner()->config ); | |
} | |
/** | |
* Get values of global configuration parameters. | |
* | |
* Provides access to `--path=<path>`, `--url=<url>`, and other values of | |
* the [global configuration parameters](https://wp-cli.org/config/). | |
* | |
* ``` | |
* WP_CLI::log( 'The --url=<url> value is: ' . WP_CLI::get_config( 'url' ) ); | |
* ``` | |
* | |
* @access public | |
* @category Input | |
* | |
* @param string $key Get value for a specific global configuration parameter. | |
* @return mixed | |
*/ | |
public static function get_config( $key = null ) { | |
if ( null === $key ) { | |
return self::get_runner()->config; | |
} | |
if ( ! isset( self::get_runner()->config[ $key ] ) ) { | |
self::warning( "Unknown config option '$key'." ); | |
return null; | |
} | |
return self::get_runner()->config[ $key ]; | |
} | |
/** | |
* Run a WP-CLI command. | |
* | |
* Launches a new child process to run a specified WP-CLI command. | |
* Optionally: | |
* | |
* * Run the command in an existing process. | |
* * Prevent halting script execution on error. | |
* * Capture and return STDOUT, or full details about command execution. | |
* * Parse JSON output if the command rendered it. | |
* | |
* ``` | |
* $options = array( | |
* 'return' => true, // Return 'STDOUT'; use 'all' for full object. | |
* 'parse' => 'json', // Parse captured STDOUT to JSON array. | |
* 'launch' => false, // Reuse the current process. | |
* 'exit_error' => true, // Halt script execution on error. | |
* ); | |
* $plugins = WP_CLI::runcommand( 'plugin list --format=json', $options ); | |
* ``` | |
* | |
* @access public | |
* @category Execution | |
* | |
* @param string $command WP-CLI command to run, including arguments. | |
* @param array $options Configuration options for command execution. | |
* @return mixed | |
*/ | |
public static function runcommand( $command, $options = [] ) { | |
$defaults = [ | |
'launch' => true, // Launch a new process, or reuse the existing. | |
'exit_error' => true, // Exit on error by default. | |
'return' => false, // Capture and return output, or render in realtime. | |
'parse' => false, // Parse returned output as a particular format. | |
]; | |
$options = array_merge( $defaults, $options ); | |
$launch = $options['launch']; | |
$exit_error = $options['exit_error']; | |
$return = $options['return']; | |
$parse = $options['parse']; | |
$retval = null; | |
if ( $launch ) { | |
Utils\check_proc_available( 'launch option' ); | |
$descriptors = [ | |
0 => STDIN, | |
1 => STDOUT, | |
2 => STDERR, | |
]; | |
if ( $return ) { | |
$descriptors = [ | |
0 => STDIN, | |
1 => [ 'pipe', 'w' ], | |
2 => [ 'pipe', 'w' ], | |
]; | |
} | |
$php_bin = escapeshellarg( Utils\get_php_binary() ); | |
$script_path = $GLOBALS['argv'][0]; | |
// Persist runtime arguments unless they've been specified otherwise. | |
$configurator = self::get_configurator(); | |
$argv = array_slice( $GLOBALS['argv'], 1 ); | |
list( $ignore1, $ignore2, $runtime_config ) = $configurator->parse_args( $argv ); | |
foreach ( $runtime_config as $k => $v ) { | |
if ( preg_match( "|^--{$k}=?$|", $command ) ) { | |
unset( $runtime_config[ $k ] ); | |
} | |
} | |
$runtime_config = Utils\assoc_args_to_str( $runtime_config ); | |
$runcommand = "{$php_bin} {$script_path} {$runtime_config} {$command}"; | |
$pipes = []; | |
$proc = Utils\proc_open_compat( $runcommand, $descriptors, $pipes, getcwd() ); | |
if ( $return ) { | |
$stdout = stream_get_contents( $pipes[1] ); | |
fclose( $pipes[1] ); | |
$stderr = stream_get_contents( $pipes[2] ); | |
fclose( $pipes[2] ); | |
} | |
$return_code = proc_close( $proc ); | |
if ( -1 === $return_code ) { | |
self::warning( 'Spawned process returned exit code -1, which could be caused by a custom compiled version of PHP that uses the --enable-sigchild option.' ); | |
} elseif ( $return_code && $exit_error ) { | |
exit( $return_code ); | |
} | |
if ( true === $return || 'stdout' === $return ) { | |
$retval = trim( $stdout ); | |
} elseif ( 'stderr' === $return ) { | |
$retval = trim( $stderr ); | |
} elseif ( 'return_code' === $return ) { | |
$retval = $return_code; | |
} elseif ( 'all' === $return ) { | |
$retval = (object) [ | |
'stdout' => trim( $stdout ), | |
'stderr' => trim( $stderr ), | |
'return_code' => $return_code, | |
]; | |
} | |
} else { | |
$configurator = self::get_configurator(); | |
$argv = Utils\parse_str_to_argv( $command ); | |
list( $args, $assoc_args, $runtime_config ) = $configurator->parse_args( $argv ); | |
if ( $return ) { | |
$existing_logger = self::$logger; | |
self::$logger = new Execution(); | |
self::$logger->ob_start(); | |
} | |
if ( ! $exit_error ) { | |
self::$capture_exit = true; | |
} | |
try { | |
self::get_runner()->run_command( | |
$args, | |
$assoc_args, | |
[ | |
'back_compat_conversions' => true, | |
] | |
); | |
$return_code = 0; | |
} catch ( ExitException $e ) { | |
$return_code = $e->getCode(); | |
} | |
if ( $return ) { | |
$execution_logger = self::$logger; | |
$execution_logger->ob_end(); | |
self::$logger = $existing_logger; | |
$stdout = $execution_logger->stdout; | |
$stderr = $execution_logger->stderr; | |
if ( true === $return || 'stdout' === $return ) { | |
$retval = trim( $stdout ); | |
} elseif ( 'stderr' === $return ) { | |
$retval = trim( $stderr ); | |
} elseif ( 'return_code' === $return ) { | |
$retval = $return_code; | |
} elseif ( 'all' === $return ) { | |
$retval = (object) [ | |
'stdout' => trim( $stdout ), | |
'stderr' => trim( $stderr ), | |
'return_code' => $return_code, | |
]; | |
} | |
} | |
if ( ! $exit_error ) { | |
self::$capture_exit = false; | |
} | |
} | |
if ( ( true === $return || 'stdout' === $return ) | |
&& 'json' === $parse ) { | |
$retval = json_decode( $retval, true ); | |
} | |
return $retval; | |
} | |
/** | |
* Run a given command within the current process using the same global | |
* parameters. | |
* | |
* Use `WP_CLI::runcommand()` instead, which is easier to use and works better. | |
* | |
* To run a command using a new process with the same global parameters, | |
* use WP_CLI::launch_self(). To run a command using a new process with | |
* different global parameters, use WP_CLI::launch(). | |
* | |
* ``` | |
* ob_start(); | |
* WP_CLI::run_command( array( 'cli', 'cmd-dump' ) ); | |
* $ret = ob_get_clean(); | |
* ``` | |
* | |
* @access public | |
* @category Execution | |
* | |
* @param array $args Positional arguments including command name. | |
* @param array $assoc_args | |
*/ | |
public static function run_command( $args, $assoc_args = [] ) { | |
self::get_runner()->run_command( $args, $assoc_args ); | |
} | |
// DEPRECATED STUFF | |
public static function add_man_dir() { | |
trigger_error( 'WP_CLI::add_man_dir() is deprecated. Add docs inline.', E_USER_WARNING ); | |
} | |
// back-compat | |
public static function out( $str ) { | |
fwrite( STDOUT, $str ); | |
} | |
// back-compat | |
// phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Deprecated method. | |
public static function addCommand( $name, $class ) { | |
trigger_error( | |
sprintf( | |
'wp %s: %s is deprecated. use WP_CLI::add_command() instead.', | |
$name, | |
__FUNCTION__ | |
), | |
E_USER_WARNING | |
); | |
self::add_command( $name, $class ); | |
} | |
} |