WP-CLI

Drive WordPress from the shell: core, plugin and theme commands, database work, wp eval, and registering your own commands.

SyntaxWP_CLI::add_command( string $name, callable|string $callable, array $args = array() )

WP-CLI boots WordPress without a web request and then hands you its API from the shell. Anything you can do in the admin you can script, run over SSH, put in a deploy step, or repeat across a hundred sites in a loop — and it works when the admin is broken, which is often exactly when you need it.

The command families

  • wp core download|update|update-db|version — core files and then the database schema, in that order.
  • wp plugin and wp theme — install, activate, update --all, list --status=active. wp plugin install akismet --activate is the shorthand for an install followed by a separate wp plugin activate akismet.
  • wp db export|import|query|cli — a wrapper around mysqldump and the MySQL client that reads credentials from wp-config.php, so you never retype them. wp db export with no filename writes {dbname}-{Y-m-d}-{hash}.sql.
  • wp search-replace — the URL rewrite that understands serialized data; see the deployment lesson.
  • wp eval 'php', plus wp option, wp post, wp user, wp cron for one-off surgery.

Global flags and your own commands

--path= selects the install, --url= selects the site on multisite, and --format=json|csv|count shapes output for scripts.

WP_CLI::add_command( $name, $callable ) registers a command. The callback receives ( $args, $assoc_args ) — positional arguments and --key=value arguments — and WP-CLI parses the synopsis and help text out of the docblock above it.

Example

Example · php
<?php
/**
 * Plugin Name: Acme CLI
 */

// WP-CLI defines the WP_CLI constant. Guard so this file is inert on web requests.
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
    return;
}

class Acme_Report_Command {

    /**
     * Rebuilds the cached sales report for one month.
     *
     * ## OPTIONS
     *
     * <month>
     * : Month to rebuild, as YYYY-MM.
     *
     * [--force]
     * : Rebuild even when a cached report already exists.
     *
     * [--format=<format>]
     * : Render output in a particular format.
     * ---
     * default: table
     * options:
     *   - table
     *   - json
     * ---
     *
     * ## EXAMPLES
     *
     *     wp acme report rebuild 2026-08 --force
     */
    public function rebuild( $args, $assoc_args ) {
        list( $month ) = $args;                 // positional arguments, in order
        $force = isset( $assoc_args['force'] ); // flags are simply present or absent

        if ( ! preg_match( '/^\d{4}-\d{2}$/', $month ) ) {
            // error() prints to STDERR and exits with a non-zero status,
            // which is what makes a deploy script stop.
            WP_CLI::error( "Not a YYYY-MM month: {$month}" );
        }

        WP_CLI::log( "Rebuilding {$month}..." ); // ordinary output, hidden by --quiet
        $rows = acme_rebuild_report( $month, $force );

        WP_CLI::success( sprintf( '%d rows written.', count( $rows ) ) );
    }
}

// Public methods of the class become subcommands: wp acme report rebuild
WP_CLI::add_command( 'acme report', 'Acme_Report_Command' );

When to use it

  • A deploy script must update core, run the database migration and clear caches without a browser.
  • The admin is white-screening and you need to deactivate a plugin to get back in.
  • You have to change one option value on forty sites in a network.
  • A colleague needs a database dump of production with the credentials already configured.

More examples

A safe update run

The order matters: core update-db must follow core update, and a database export must precede both. Use this as the body of a deploy script — every command returns a non-zero exit status on failure, so `set -e` stops the run before the next destructive step.

Example · bash
# Back up first — credentials are read from wp-config.php, so no flags needed.
wp db export backups/pre-deploy.sql

# Core files, then the database schema. Skipping update-db leaves the site
# showing the "database update required" screen.
wp core update
wp core update-db

wp plugin update --all
wp theme update --all

# Inspect state without opening the admin.
wp plugin list --status=active --field=name
wp option get siteurl
wp cron event list --due-now

# wp core version reports what is actually on disk, not what the admin claims.
wp core version

wp eval for one-off work

wp eval is the escape hatch for work that has no command: clearing one transient, checking a value, running a backfill. The globals caveat catches everyone once — $wpdb is null inside eval unless you declare it, because the code is executed inside a WP-CLI method rather than at file scope.

Example · bash
# The code runs inside a method, so globals must be declared explicitly.
wp eval 'global $wpdb; WP_CLI::log( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts}" ) );'

# Anything WordPress can do is available, because WordPress is fully loaded.
wp eval 'WP_CLI::log( wp_get_environment_type() );'
wp eval 'delete_transient( "acme_feed" ); WP_CLI::success( "cleared" );'

# For anything longer than one line, put it in a file and run that instead.
wp eval-file scripts/backfill.php

# --skip-plugins / --skip-themes load core only: the fastest way to find out
# whether a plugin is the thing breaking the site.
wp --skip-plugins --skip-themes option get home

Looping over a multisite network

WP-CLI is the only practical way to audit or change a large network — the network admin has no bulk view of per-site plugin state. Use this pattern before a plugin removal, to find which sites actually have it active.

Example · bash
# --url= picks the site; the command otherwise behaves exactly as on a single site.
for url in $(wp site list --field=url); do
  echo "== $url"
  wp --url="$url" plugin list --status=active --field=name
done

# Network-wide operations do not need --url.
wp plugin update --all
wp site list --fields=blog_id,url,registered --format=csv

Discussion

  • Be the first to comment on this lesson.