Skip to content
Skip to the article
In WordPress: 10 articles
WordPress

WP-CLI cookbook

Tested WP-CLI recipes for URL changes, bulk deletion, WooCommerce bulk edits, emergency admin access, and reinstalling core or plugins.

Updated
Applies to
  • WordPress 6.x
  • WP-CLI 2.x
  • WooCommerce 9+
Tags
  • wordpress
  • wp-cli
  • woocommerce
  • maintenance
Reading time
6 min

These are the WP-CLI commands Avunu reaches for most often during migrations, cleanups, and recoveries. Run them from the WordPress root directory (where wp-config.php lives), or add --path=/var/www/example.com/htdocs to point at it.

Before You Run Anything

  • Run as the web server user, not root, so new files get the right owner: sudo -u www-data wp .... If you must run as root, WP-CLI requires --allow-root. Avoid it where you can.

  • Back up first. wp db export writes a SQL dump to the current directory.

  • Use --dry-run on commands that support it (search-replace does) and read the counts before running for real.

  • After bulk changes, run wp cache flush so the object cache doesn't serve stale data.

URLs and Search-Replace

wp search-replace is safe for serialized PHP data (widgets, options, page builder content), unlike a raw SQL REPLACE().

Switch a Site From HTTP to HTTPS

Limit the replacement to your own domain. A bare 'http://' 'https://' also rewrites links to external sites that may not support HTTPS.

wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --dry-run
wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid

Move a Site to a New Domain

For example, after copying a staging site to production:

wp search-replace 'https://staging.example.com' 'https://example.com' --skip-columns=guid --dry-run
wp search-replace 'https://staging.example.com' 'https://example.com' --skip-columns=guid
wp option get home
wp option get siteurl

Notes:

  • --skip-columns=guid leaves post GUIDs alone. They're permanent identifiers that feed readers rely on, not links.

  • Add --all-tables-with-prefix to include plugin tables that share the WordPress prefix, or --network on multisite.

  • If home and siteurl still show the old URL (for example when they're defined in wp-config.php), fix them with wp option update home 'https://example.com' or wp config set WP_HOME ....

Bulk Deletion

Warning

These commands delete content permanently. --force skips the trash. Take a backup and double-check the post type or taxonomy name first.

Delete All Posts of a Post Type

wp post list --post_type=<POST_TYPE> --post_status=any --format=ids | xargs -r wp post delete --force

--post_status=any includes drafts and private posts, which wp post list skips by default. xargs -r avoids "argument list too long" on large sets and does nothing when the list is empty.

Delete All Terms in a Taxonomy

wp term list <TAXONOMY> --field=term_id | xargs -r wp term delete <TAXONOMY>

Use the taxonomy's registered name (category, post_tag, product_cat, or a custom one like my_taxonomy).

Pause Cache Purging During Bulk Jobs

Page-cache plugins that purge on every change (LiteSpeed Cache is a common one) can make bulk deletes crawl. Deactivate the plugin for the job, then purge once at the end:

wp plugin deactivate litespeed-cache
wp term list <TAXONOMY> --field=term_id | xargs -r wp term delete <TAXONOMY>
wp plugin activate litespeed-cache
wp litespeed-purge all

WooCommerce

Use WooCommerce's CRUD functions (wc_get_products(), wc_get_orders(), $product->save()) rather than update_post_meta() or SQL. They keep the product lookup tables, caches, and HPOS order tables in sync.

Set the Price of Every Product in a Category

wp eval '
foreach ( wc_get_products( [ "category" => [ "<CATEGORY_SLUG>" ], "limit" => -1 ] ) as $product ) {
	$product->set_regular_price( "0" );
	$product->set_sale_price( "" );
	$product->save();
	WP_CLI::log( "Updated #" . $product->get_id() . " " . $product->get_name() );
}'

For variable products, the price lives on each variation, so loop over $product->get_children() and update each wc_get_product( $variation_id ) the same way.

Delete Orders Older Than a Date

This works whether the store uses HPOS or legacy post storage, and processes orders in batches so it doesn't run out of memory:

CUTOFF=<CUTOFF_DATE> wp eval '
$total = 0;
do {
	$ids = wc_get_orders( [
		"type"         => "shop_order",
		"date_created" => "<" . getenv( "CUTOFF" ),
		"limit"        => 200,
		"return"       => "ids",
	] );
	foreach ( $ids as $id ) {
		wc_get_order( $id )->delete( true );
	}
	$total += count( $ids );
	WP_CLI::log( "Deleted {$total} orders so far" );
} while ( $ids );
WP_CLI::success( "Deleted {$total} orders." );'

Replace <CUTOFF_DATE> with a date like 2023-11-01. Orders created before that date are deleted. delete( true ) deletes each order permanently instead of moving it to the trash.

Warning

Don't select order IDs from wp_posts or wp_wc_orders with SQL and delete them in a loop. With HPOS and compatibility sync, an order exists in both places, and deleting from only one leaves the two out of step. Deleting orders also removes sales history from WooCommerce Analytics.

Users

Create an Emergency Administrator

wp user create <USERNAME> <EMAIL> --role=administrator

Leaving out --user_pass makes WP-CLI generate a strong password and print it once, so the password never lands in your shell history. To reset an existing account instead, use wp user reset-password <USERNAME> --skip-email --show-password. It changes the password and prints the new one without emailing the user.

Warning

Remove or downgrade emergency accounts when you're done: wp user delete <USERNAME> --reassign=<OTHER_USER_ID>.

Integrity and Reinstalls

Check for Modified Core and Plugin Files

wp core verify-checksums
wp plugin verify-checksums --all

Any file listed as changed or unexpected is worth a look, especially after a suspected compromise. Plugins that aren't hosted on WordPress.org report that no checksums are available.

Reinstall All Plugins

This replaces plugin files with fresh copies at the same version without touching settings:

wp plugin list --status=active --field=name | xargs -r -n1 wp plugin install --force
wp plugin list --status=inactive --field=name | xargs -r -n1 wp plugin install --force

Premium plugins that aren't on WordPress.org will fail. Reinstall those from the vendor's zip. If a broken plugin stops WP-CLI from loading, add --skip-plugins --skip-themes to each wp call. For memory errors, run WP-CLI with php -d memory_limit=512M "$(command -v wp)" ....

Reinstall WordPress Core

Removing wp-admin and wp-includes first clears out any files that were injected into them, then the download restores clean copies of the same version:

VERSION="$(wp core version)"
rm -rf wp-admin wp-includes
wp core download --version="$VERSION" --skip-content --force

--skip-content leaves wp-content (themes, plugins, and uploads) alone. Root-level core files like wp-login.php are overwritten, while wp-config.php and any extra files in the web root are kept, so inspect leftover root files by hand.

Warning

Capture the version before deleting wp-includes, because wp core version reads it from there.

Roll Back to an Earlier Core Version

wp core update --version=<VERSION> --force

Downgrading core doesn't downgrade the database schema. Roll back only to undo a very recent update, and restore a database backup if the newer version ran a database upgrade the older one doesn't understand.

Database Checks

wp db commands use the credentials from wp-config.php, so you don't need a MySQL root login:

wp db check      # Run CHECK TABLE on every WordPress table.
wp db repair     # Repair corrupted tables (MyISAM, or InnoDB where supported).
wp db optimize   # Reclaim space after large deletions.

To check every database on the server rather than just this site's, run mysqlcheck --all-databases --check as a MySQL administrator.

See Database Optimization Queries for finding what's bloating the database.