WP-CLI From A to Z

Ryan Kanner (@codeprokid)

slides.com/codeprokid/wp-cli-az

What is wp-cli?

What is WP-CLI?

  • It's WordPress... on the command line
  • Open source project backed by WordPress.org, and collaborated on in Github.
  • Allows you to talk to WordPress without going through the browser.
  • Has an easy to use API for extending it with your own commands.
  • Available from wp-cli.org

Why should I use it?

  • ...Because it's awesome.
  • Allows you to automate monotonous tasks.
  • Gives you an easy way to do bulk operations like deleting posts, or migrating data.
  • Provides an easy way to touch some hard-to-control WordPress internals (transients, rewrite rules)
  • It's consistent and repeatable.

When should I use it?

  • Update/Install: Core/Themes/Plugins
  • Search & Replace your DB
  • Automating deployments or maintenance
  • Troubleshoot issues with some WordPress internals (cron, cache, transients)
  • Long running batch processes
  • Data migrations
  • Scaffolding themes & plugins

Getting going

Download the .phar file

$ curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar

Check if it's working

$ php wp-cli.phar --info

Make it executable, and put it in your PATH

$ chmod +x wp-cli.phar
$ sudo mv wp-cli.phar /usr/local/bin/wp

That's it!

$ wp --info
PHP binary:    /usr/bin/php5
PHP version:    5.5.9-1ubuntu4.14
php.ini used:   /etc/php5/cli/php.ini
WP-CLI root dir:        /home/wp-cli/.wp-cli
WP-CLI packages dir:    /home/wp-cli/.wp-cli/packages/
WP-CLI global config:   /home/wp-cli/.wp-cli/config.yml
WP-CLI project config:
WP-CLI version: 1.1.0

Basic usage

install & Update all the things

# Download a fresh copy of WordPress
wp core download

# Update Core
wp core update

# Update a plugin
wp plugin update jetpack

# Install a theme
wp theme install twentyseventeen --activate



CRUD Data

# Create a term
wp term create category Rabbits --parent=animals

# Get information on a user
wp user get 10

# Update an option
wp option update my_favorite_pet Rabbits

# Delete post ID 123
wp post delete 123 --force=true

Managing UI-less Features

# Regenerate all thumbnails
wp media regenerate

# Delete expired transients
wp transient delete --expired

# Delete all transients
wp transient delete --all

# List of scheduled cron events
wp cron event list

# Run all cron events in queue
wp cron event run --due-now

# Flush rewrite rules
wp rewrite flush

# List of rewrites
wp rewrite list --format=csv

# Flush the cache
wp cache flush

Scaffold Plugins!

Global Parameters

--path=<path>
//Path to the WordPress files.

--ssh=[<user>@]<host>[:<port>][<path>]
//Perform operation against a remote server over SSH.

--http=<http>
//Perform operation against a remote WordPress install over HTTP.

--url=<url>
//Pretend request came from given URL. In multisite, this 
//argument is how the target site is specified.

--skip-plugins[=<plugin>]
//Skip loading all or some plugins. Note: mu-plugins are still loaded.

--skip-themes[=<theme>]
//Skip loading all or some themes.

--skip-packages
//Skip loading all installed packages.

--require=<path>
//Load PHP file before running the command (may be used more than once).

--[no-]color
//Whether to colorize the output.

--quiet
//Suppress informational messages.

And More!

Advanced Usage

Combining commands

// Delete all pages
$ wp post delete $(wp post list --post_type='page' --format=ids)

// Delete all posts in the trash
$ wp post delete $(wp post list --post_status=trash --format=ids)
// Update an option for all sites in a network
$ wp site list --field=url | xargs -n1 -I {} sh -c 'wp --url={} option update my_option my_value'
// Import tags from a CSV
#Usage ./import_tags <file-name>

while IFS=, read line
do
    eval wp term create post_tag "\""$line"\""
done < $1

Generate Dummy Data for testing

# Generate 100 dummy posts
wp post generate --format=ids --count=100 | xargs -n1 -I % wp term meta add % generated_data true

# Generate 10 dummy categories
wp term generate category --format=ids --count=10 | xargs -n1 -I % wp term meta add % generated_data true

# Generate 100 dummy users
wp user generate --format=ids --count=100 | xargs -n1 -I % wp term meta add % generated_data true

# Clean It Up
wp term list category --field=term_id --meta_key=generated_data | xargs wp term delete category
wp user list --field=user_id --meta_key=generated_data | xargs wp user delete
wp post list --format=ids --meta_key=generated_data | xargs wp post delete 

Bash shortcuts

# Add this to your ~/.bashrc

syn_prod_db() {
    wp search-replace $1 $2 --export=db.sql
    wp @production db export backup.sql
    wp @production db import db.sql
    wp @production db optimize
}

# Usage
sync_prod_db mysite.dev mysite.com

Using wp-cli.yml files

  • Set configuration defaults for all of your projects, or on a project by project basis
  • Create alias's for all of your environments
  • Uses hierarchical inheritance
    • ~/.wp-cli/config.yml Global System settings
    • wp-cli.yml Working directory, or upwards
    • wp-cli.local.yml Project specific

Parameter & command defaults

# Global parameter defaults
path: wp-core
url: http://example.com
user: admin
color: false
disabled_commands:
  - db drop
  - plugin install
require:
  - path-to/command.php

# Subcommand defaults (e.g. `wp core config`)
core config:
    dbuser: root
    dbpass: 
    extra-php: |
        define( 'WP_DEBUG', true );
        define( 'WP_POST_REVISIONS', 50 );

Aliases

// Project level
@staging:
    ssh: wpcli@staging.wp-cli.org
    user: wpcli
    path: /srv/www/staging.wp-cli.org
@production:
    ssh: wpcli@wp-cli.org:2222
    user: wpcli
    path: /srv/www/wp-cli.org

// Global locally
@site1
    path: /projects/project_1_root
@site2
    path: /projects/project_2_root
@site3
    path: /projects/project_3_root
@site4
    path: /projects/project_4_root

Export & Import data

# Search & Replace Local DB & export to .sql file
wp search-replace mysite.dev mysite.com --export=db.sql

# Take a backup of the old DB
wp @production db export backup.sql

# Import the new DB
wp @production db import db.sql

# Optimize the DB after import
wp @production db optimize

Extending

The package index

  • Like the plugin repository, but for WP-CLI commands
  • Official repository is here -> http://wp-cli.org/package-index/
  • You can install a package with wp package install <PACKAGE_NAME>
  • You can also create your own packages. Get started by installing the scaffold package... package.

Anatomy of a command

// Class to contain our command
Class My_Command extends WP_CLI {

    // Any public method within the class becomes a command
    public function say_hello( $args, $assoc_args ) {
        
        $name = ( isset( $args[0] ) ) : $args[0] : 'Anonymous';
        
        WP_CLI::success( 'Hello ' . $name );

    }

}
// Registers all commands under the `sample` sub-command
WP_CLI::add_command( 'sample', 'My_Command' );
# How to run this command:
wp sample say_hello Ryan
Success: Hello Ryan

Keeping commands reusable & accepting input

  • In order to make commands more reusable, they should accept input arguments.
  • 2 ways to accept them, normal args & flags.
  • Allows scripts to work for multiple environments (ex. ID's being different), and allows for consistent repeatability

Fun with flags

// Class to contain our command
Class My_Command extends WP_CLI {

    // Any public method within the class becomes a command
    public function say_hello( $args, $assoc_args ) {
        
        $name = ( isset( $args[0] ) ) : $args[0] : 'Anonymous';

        $excited = WP_CLI\Utils\get_flag_value( $assoc_args, 'excited', false );

        $message = 'Hello ' . $name;

        $message .= ( 'true' === $excited ) ? '!' : '';
        
        WP_CLI::success( $message );

    }

}
// Registers all commands under the `sample` sub-command
WP_CLI::add_command( 'sample', 'My_Command' );

# Running the following:
wp sample say_hello Ryan --excited=true

# Will return
Success: Hello Ryan!

Controlling commands with doc blocks

/**
* Command for saying hello to people
* 
* # OPTIONS
*
* <name>
* : Who we should say hello to
*
* [--excited]
* : Whether or not to append an exclamation mark to the message
* ---
* default: false
* options:
*  - false
*  - true
* ---
* 
* @subcommand say-hello
* @when before_wp_load
*/
public function say_hello( $args, $assoc_args ){ /* code */ };

Run WordPress code

class Term_Mover extends WP_CLI {

    public function move_terms( $args, $assoc_args ) {

        $delete_old_term = WP_CLI\Utils\get_flag_value( $assoc_args, 'delete-old-term', false );
        // Get all the terms from the old taxonomy
        $old_terms = get_terms( array( 'taxonomy' => $args[0] ) );
        // Make sure there are terms to move
        if ( empty( $old_terms ) || is_wp_error( $old_terms ) ) {
            WP_CLI::error( 'Could not find any terms to move' );
        }

        foreach ( $old_terms as $term ) {

             $term_id = wp_insert_term( $term->name, $args[1] );
             
             if ( false !== $term_id && ! is_wp_error( $term_id ) ) {

                WP_CLI::success( 'Successfully inserted term id: ' . $term_id );

                if ( 'true' === $delete_old_term ) {
                    wp_delete_term( $term->term_id, $args[0] );
                }

             } else {
                WP_CLI::error( 'Could not insert new term' );
             }
        }
    }
}
WP_CLI::add_command( 'sample', 'Term_Mover' );

Include commands in a theme or plugin

// Only load our CLI command when loaded via WP_CLI.
if ( defined( 'WP_CLI' ) && WP_CLI ) {
    require_once dirname( __FILE__ ) . '/my-cli-class.php';
}

Tips & Tricks

writing your own

  • All args passed to the command actually get passed as strings, so if you are expecting an integer, typeset it to be one.
  • Including a `dry-run` flag for commands manipulating data, so you don't accidentally nuke your DB.
  • Even though you are on the command line, you should still consider timeouts as a possibility, try to do things in batches for large datasets.

Adding a command to an existing subcommand

You can actually add a new command to an existing sub-command. See below:

// Class to contain our command
Class My_Command extends WP_CLI {

    // Any public method within the class becomes a command
    public function say_hello( $args, $assoc_args ) {
        
        $name = ( isset( $args[0] ) ) : $args[0] : 'Anonymous';
        
        WP_CLI::success( 'Hello ' . $name );

    }

}
// Registers all commands under the `plugin` sub-command
WP_CLI::add_command( 'plugin', 'My_Command' );

# Usage
wp plugin say_hello Ryan

Use WP_parse_Args for setting defaults

$assoc_args = wp_parse_args( $assoc_args, array(
    'foo' => true, 
    'bar' => array(),
);

Progress bars are sweet

$count = 100;

$progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count );

for ( $i = 0; $i < $count; $i++ ) {
    // Your code
    $progress->tick();
}

$progress->finish();

Progress bars are sweet

$count = 100;

$progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count );

for ( $i = 0; $i < $count; $i++ ) {
    // Your code
    $progress->tick();
}

$progress->finish();

Use other WP-CLI commands within your command

// Run an existing CLI command within another command
$plugins = WP_CLI::runcommand( 'plugin list --format=json', array(
    'return' => true, // Return `STDOUT` from command
    'parse' => 'json', // Convert the `STDOUT` to a json obj
    'launch' => false, // re-use same process so we can capture result
    'exit_error' => true, // Halt the entire script on error
) ); 

Save the output of a command

You can easily save the output of any command that gives you a STDOUT with the > operator

# Save all of the terms in the post_tag taxonomy to a CSV
wp term list post_tag --field=name --format=csv > mytags.csv

DEMO!

WP-CLI From A to Z

By Ryan Kanner

WP-CLI From A to Z

In this presentation, I will take you from installing wp-cli to writing your own commands.

  • 3,056