惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
aimingoo的专栏
aimingoo的专栏
Vercel News
Vercel News
U
Unit 42
L
LangChain Blog
J
Java Code Geeks
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
F
Fortinet All Blogs
小众软件
小众软件
I
InfoQ
P
Proofpoint News Feed
D
DataBreaches.Net
Martin Fowler
Martin Fowler
H
Help Net Security
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
B
Blog RSS Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
WP-CLI: Advanced Techniques for Real-World WordPress Deve...
Kushang Tail · 2026-05-13 · via DEV Community

In Part 1, we got WP-CLI installed and covered the essential commands. Now it's time to go deeper — custom commands, automation pipelines, remote management, and safe deployment patterns.


📌 Table of Contents

  1. Advanced Custom PHP Commands
  2. WP-CLI in CI/CD Pipelines
  3. Managing Remote WordPress Sites
  4. The --dry-run Safety Net
  5. Final Thoughts

Advanced Custom PHP Commands

What Makes a WP-CLI Command "Advanced"?

In Part 1, we wrote a simple shell script that chained built-in commands. That's great for setup tasks — but what if you need logic that WP-CLI doesn't have out of the box?

That's where WP_CLI::add_command() comes in. It lets you register your own commands in PHP, with full access to the WordPress codebase, custom arguments, flags, progress bars, formatted output, and error handling.

The anatomy of a custom command looks like this:

WP_CLI::add_command( 'namespace action', callable, $args );

Enter fullscreen mode Exit fullscreen mode

  • namespace — your command group (e.g. cleanup, mysite, tools)
  • action — what it does (e.g. posts, users, cache)
  • callable — a function or class method that runs the logic
  • $args — optional: description, synopsis, usage examples

💡 Always wrap custom commands in if ( defined( 'WP_CLI' ) && WP_CLI ) — this ensures the code only runs in a CLI context and never on a web request.


Example 1 — Simple: Bulk Delete Posts by Status

A clean, practical command to delete posts by any status (draft, pending, trash). Useful after a content migration or cleanup sprint.

<?php
// Usage: wp cleanup posts --status=draft
// File: mu-plugins/cli-cleanup.php

if ( defined( 'WP_CLI' ) && WP_CLI ) {

    WP_CLI::add_command( 'cleanup posts', function( $args, $assoc ) {

        $status = $assoc['status'] ?? 'draft';
        $allowed = [ 'draft', 'pending', 'trash', 'private' ];

        if ( ! in_array( $status, $allowed ) ) {
            WP_CLI::error( "Invalid status. Allowed: " . implode( ', ', $allowed ) );
        }

        $posts = get_posts([
            'post_status' => $status,
            'numberposts' => -1,
            'fields'      => 'ids',
        ]);

        if ( empty( $posts ) ) {
            WP_CLI::warning( "No posts found with status: {$status}" );
            return;
        }

        foreach ( $posts as $id ) {
            wp_delete_post( $id, true );
            WP_CLI::line( "Deleted post #$id" );
        }

        WP_CLI::success( count( $posts ) . " posts permanently deleted." );
    });

}

Enter fullscreen mode Exit fullscreen mode

Run it:

wp cleanup posts --status=draft
wp cleanup posts --status=trash

Enter fullscreen mode Exit fullscreen mode

What it handles:

  • Validates the status before running
  • Warns if nothing is found instead of silently exiting
  • Gives per-item feedback + a final success count

Example 2 — Advanced: User Audit & Export Command

A more powerful command that audits all users, filters by role and last login, exports a CSV report, and optionally deactivates inactive accounts — with --dry-run support baked in.

<?php
// Usage: wp audit users --role=subscriber --days=90 --export --dry-run
// File: mu-plugins/cli-audit.php

if ( defined( 'WP_CLI' ) && WP_CLI ) {

    WP_CLI::add_command( 'audit users', function( $args, $assoc ) {

        $role    = $assoc['role']   ?? 'subscriber';
        $days    = (int) ( $assoc['days']   ?? 90 );
        $dry_run = isset( $assoc['dry-run'] );
        $export  = isset( $assoc['export'] );
        $cutoff  = strtotime( "-{$days} days" );

        WP_CLI::log( "Auditing '{$role}' users inactive for {$days}+ days..." );
        if ( $dry_run ) WP_CLI::log( "[DRY RUN] No changes will be made." );

        $users = get_users([ 'role' => $role, 'number' => -1 ]);

        if ( empty( $users ) ) {
            WP_CLI::warning( "No users found with role: {$role}" );
            return;
        }

        $flagged = [];
        $progress = \WP_CLI\Utils\make_progress_bar(
            'Scanning users', count( $users )
        );

        foreach ( $users as $user ) {
            $last_login = (int) get_user_meta( $user->ID, 'last_login', true );

            // Flag if never logged in OR inactive beyond cutoff
            if ( ! $last_login || $last_login < $cutoff ) {
                $flagged[] = [
                    'ID'         => $user->ID,
                    'login'      => $user->user_login,
                    'email'      => $user->user_email,
                    'registered' => $user->user_registered,
                    'last_login' => $last_login
                        ? date( 'Y-m-d', $last_login )
                        : 'Never',
                ];

                if ( ! $dry_run ) {
                    // Deactivate: remove role instead of deleting
                    $user_obj = new WP_User( $user->ID );
                    $user_obj->remove_role( $role );
                    $user_obj->add_role( 'inactive' );
                }
            }
            $progress->tick();
        }

        $progress->finish();

        if ( empty( $flagged ) ) {
            WP_CLI::success( "All users are active. Nothing to flag." );
            return;
        }

        // Display results as a table
        WP_CLI\Utils\format_items( 'table', $flagged,
            [ 'ID', 'login', 'email', 'registered', 'last_login' ]
        );

        // Export to CSV if requested
        if ( $export ) {
            $file = 'inactive-users-' . date('Y-m-d') . '.csv';
            $fp   = fopen( $file, 'w' );
            fputcsv( $fp, array_keys( $flagged[0] ) );
            foreach ( $flagged as $row ) fputcsv( $fp, $row );
            fclose( $fp );
            WP_CLI::success( "Exported to {$file}" );
        }

        $action = $dry_run ? "would be deactivated" : "deactivated";
        WP_CLI::success( count( $flagged ) . " users {$action}." );
    });

}

Enter fullscreen mode Exit fullscreen mode

Run it:

# Preview only — safe, no changes
wp audit users --role=subscriber --days=90 --dry-run

# Run it for real + export a CSV report
wp audit users --role=subscriber --days=90 --export

# Target a different role and timeframe
wp audit users --role=editor --days=180

Enter fullscreen mode Exit fullscreen mode

What makes this advanced:

  • Progress bar for large user sets
  • --dry-run flag built-in — preview before committing
  • Formatted table output using WP-CLI's own utils
  • CSV export with a timestamped filename
  • Degrades gracefully: warns, doesn't crash

WP-CLI in CI/CD Pipelines

The Basic Idea

CI/CD pipelines (GitHub Actions, GitLab CI, Bitbucket Pipelines, etc.) automate what happens after you push code. WP-CLI fits naturally into the deploy step — after files are transferred, you run WP-CLI commands to bring the environment in sync.

Common Pipeline Tasks with WP-CLI

Task Command
Run DB migrations wp core update-db
Flush rewrite rules wp rewrite flush
Clear object cache wp cache flush
Activate/deactivate plugins wp plugin activate <slug>
Sync options across environments wp option update <key> <value>
Verify WordPress is healthy wp core verify-checksums

Example — GitHub Actions Deploy Step

# .github/workflows/deploy.yml

- name: Deploy to Staging
  run: |
    ssh user@staging.example.com << 'EOF'
      cd /var/www/html

      # Pull latest code
      git pull origin main

      # Run WP-CLI post-deploy tasks
      wp core update-db
      wp rewrite flush
      wp cache flush
      wp plugin activate my-custom-plugin

      echo "✅ Deploy complete"
    EOF

Enter fullscreen mode Exit fullscreen mode

Use Case: Environment Sync on Deploy

When deploying from local → staging → production, option values often differ (API keys, URLs, feature flags). Instead of manually editing the DB:

# Set the correct API base URL for this environment
wp option update api_base_url 'https://staging.example.com/api'

# Enable maintenance mode plugin only on production
wp plugin activate wp-maintenance-mode --url=production.example.com

Enter fullscreen mode Exit fullscreen mode

💡 Keep it simple in pipelines. Only run idempotent commands — things that are safe to re-run if the pipeline retries. flush, update-db, and option update are all safe. Avoid commands that delete data.


Managing Remote WordPress Sites

WP-CLI isn't limited to the machine you're sitting at. With SSH aliases, you can run commands against any remote WordPress site as if you were logged into the server.

Setting Up SSH Aliases

Add this to your wp-cli.yml file in your project root (or ~/.wp-cli/config.yml for global use):

# wp-cli.yml
@staging:
  ssh: user@staging.example.com/var/www/html

@production:
  ssh: user@production.example.com/var/www/html

Enter fullscreen mode Exit fullscreen mode

Now prefix any command with the alias:

# Run a command on staging
wp @staging plugin list

# Flush cache on production
wp @production cache flush

# Search-replace on staging DB
wp @staging search-replace 'http://' 'https://' --all-tables

Enter fullscreen mode Exit fullscreen mode

Managing Multiple Sites at Once

# Update all plugins on both environments
wp @staging plugin update --all
wp @production plugin update --all

# Check core version across all environments
wp @staging core version
wp @production core version

Enter fullscreen mode Exit fullscreen mode

⚠️ Always run on staging first. Confirm the output is what you expect — then run on production.

Useful Flags for Remote Work

Flag Purpose
--ssh=user@host/path One-off remote command without an alias
--skip-plugins Run without loading plugins (useful when a plugin is broken)
--skip-themes Same as above for themes
--quiet Suppress output — good for cron jobs
--url=<domain> Target a specific site in a multisite network

The --dry-run Safety Net

What Is --dry-run?

--dry-run is a flag supported by several WP-CLI commands that lets you preview exactly what will happen — without actually doing it.

Think of it as reading the recipe out loud before you start cooking. You catch mistakes before they cost you.

# See what would be replaced — nothing changes in the DB
wp search-replace 'http://oldsite.com' 'https://newsite.com' --all-tables --dry-run

Enter fullscreen mode Exit fullscreen mode

The output shows every table and row that would be affected, with a count — but the database is untouched.


Why --dry-run Is Your Safest Habit

When you're working with WP-CLI, there is no undo button. A wrong search-replace or an accidental delete is permanent unless you have a backup. --dry-run gives you a confirmation step before anything irreversible happens.

Here's why it matters in practice:

  • Catches scope creep — you might expect 10 rows to change, but dry-run shows 1,400. That's a signal to narrow your query.
  • Validates your syntax — a typo in your command fails safely instead of corrupting data.
  • Builds confidence — especially useful when running commands on production for the first time.
  • Documents what will happen — share the dry-run output with a teammate for a quick review before committing.

🔒 Best practice: Make --dry-run the first run, always. Only move to the real command after the preview looks exactly right.


Real-World Use Cases

Use Case 1 — Safe Site Migration

Before doing a full URL swap after moving a site:

# Step 1: Preview — how many rows will change?
wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
  --all-tables \
  --dry-run

# Output shows: 47 replacements across 6 tables
# Looks right — now run for real:

# Step 2: Commit
wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
  --all-tables

Enter fullscreen mode Exit fullscreen mode


Use Case 2 — Bulk Post Deletion Preview

Before deleting old draft posts:

# Step 1: Dry run — list what would be deleted
wp post list --post_status=draft --format=count

# Shows: 83 posts

# Step 2: Delete only if count looks right
wp post delete $(wp post list --post_status=draft --format=ids) \
  --force

Enter fullscreen mode Exit fullscreen mode


Use Case 3 — Plugin Update Safety Check

Before mass-updating on production:

# Check what has updates available (non-destructive by nature)
wp plugin update --all --dry-run

# Review the list — then update for real
wp plugin update --all

Enter fullscreen mode Exit fullscreen mode


Final Thoughts

WP-CLI's true value isn't any single command — it's the compounding effect of everything working together. Custom commands give you project-specific tools. CI/CD integration makes deployments repeatable. SSH aliases let you manage any site from one terminal. And --dry-run means you can move fast without breaking things.

If Part 1 was about getting started, Part 2 is about building trust in your own workflow. The developers who get the most out of WP-CLI are the ones who start scripting their own patterns — and this is exactly how you do it.


🔗 Missed Part 1? Start here → — installation, must-know commands, and your first shell script.


Found this useful? Drop a ❤️ and share it with someone who's still clicking through wp-admin.