writing-wp-cli-scripts-that-work



writing-wp-cli-scripts-that-work

2 4


writing-wp-cli-scripts-that-work

Slides for "Writing WP-CLI Commands That Work!"

On Github stevegrunwell / writing-wp-cli-scripts-that-work

Writing WP-CLI Commands that Work!

Steve Grunwell@stevegrunwell

stevegrunwell.com/slides/wp-cli

Who am I?

  • Director of Technology @ Growella
  • Open-source contributor
  • Husband + (new-ish) father
  • Coffee roaster

Welcome to WP-CLI!

We're glad you're here!

WTF?

  • WordPress…on the Command Line!
  • Extensible tool for installing and maintaining WordPress
  • A way to talk to WordPress without the standard GUI

Why is it useful?

  • Far easier to perform bulk operations
  • Better visibility into WordPress internals
    • Cron, cache, transients, etc.
  • CLI is not subject to timeouts the way a browser would be
  • Easier filesystem operations
  • Able to script (allthethings)

When would we use that?!

  • Deploying WordPress core updates
  • Install/activate/update/delete themes + plugins
  • Troubleshooting issues with WP_Cron, transients, cache, etc.
  • Scheduled maintenance routines
  • Data migrations and transformations

#YOLO

If a script is only meant to be run once, making it available via wp-admin would be insane.

Built-in commands

Plugins and Themes

# Install Son of Clippy
$ wp plugin install son-of-clippy --activate

# Upgrade Yoast SEO
$ wp plugin update wordpress-seo

# Remove TwentyThirteen
$ wp theme delete twentythirteen

Users

# Add a new editor named Hypnotoad.
$ wp user create hypnotoad hypnotoad@example.com --role=editor

# ALL GLORY TO THE HYPNOTOAD!
$ wp user set-role hypnotoad admin

Import/Export

# Export WXR files for all post types in 5MB chunks
$ wp export --max_file_size=5

# Export only posts from 2016
$ wp export --start_date=2016-01-01 --post_type=post

# Import WXR files
$ wp import my-wxr-file.xml

Database

# Dump the database
$ wp db export my-backup.sql

# Import that database file
$ wp db import my-backup.sql

# Optimize the database
$ wp db optimize

# Repair the database
$ wp db repair

Search + Replace

# Update production URLs for staging
$ wp search-replace example.dev example.com

# Replace all instances of "foo" with "bar" *only* in wp_options
$ wp search-replace foo bar wp_options

Intelligently handles PHP serialized data!

More commands

  • Cache, transients, + cron
  • Posts + media
  • Taxonomy terms
  • Rewrites
  • Scaffolding tools

wp-cli.org/commands

What makes a Good WP-CLI Command?

Audience

WP-CLI will inherently be used by more technical users, but that doesn't mean we shouldn't make them friendly

Philosophy

wp-cli.org/docs/philosophy

I. Don't assume anything.

  • The command should be as portable as possible.
  • Don't assume any theme or plugin will be present/active.

II. Composability is always a good idea.

The output of one command should be "pipe-able"

$ wp post list | grep "hello"
> 1    Hello world!    hello-world    2014-11-11 20:46:47    publish

III. Readability trumps number of keystrokes.

Make it obvious what the user is doing!

# ¯\_(ツ)_/¯
$ wp my-command --launch

# Fffffuuuuuuuuuuuuu......
$ wp my-command --launch-nukes

IV. Stay focused.

  • WP-CLI commands should leverage WordPress functionality
  • If the command is unrelated to WordPress, why are you using WP-CLI?

Writing commands

Basic Structure

class My_Awesome_Command extends WP_CLI_Command {

    // Magic will happen in here!

}

// Tell WP-CLI that My_Awesome_Command is 'awesome'.
WP_CLI::add_command( 'awesome', 'My_Awesome_Command' );
$ wp awesome

Basic Structure

class My_Awesome_Command extends WP_CLI_Command {

    /**
     * Convert terms from taxonomy A to taxonomy B.
     *
     * ...
     */
    public function convert_terms( $args, $assoc_args ) {
        // All sorts of fancy logic.
    }

}
$ wp awesome convert_terms

Public or Private?

  • Public methods will be available to the command
  • Protected and private methods are inaccessible directly
  • Classic OOP principles apply!

DocBlock

  • Each command is prefaced with a DocBlock containing the following:
    • Short description
    • Options
    • Examples
    • DocBlock Tags
  • Enables WP-CLI to automatically generate --help docs

DocBlock: Short Description

A one-line description of what the command does.

/**
 * Convert terms from taxonomy A to taxonomy B.
 *

DocBlock: Options

Arguments accepted by the command.

/**
 * ...
 *
 * ## OPTIONS
 *
 * <origin>
 * : The original taxonomy.
 *
 * <destination>
 * : The destination taxonomy.
 *

DocBlock: Examples

How people might use your command.

/**
 * ...
 *
 * ## EXAMPLES
 *
 *   wp awesome convert-terms category post_tag
 *

DocBlock: Tags

  • @synopsis
    • Used to validate required arguments automatically
  • @subcommand
    • Set the canonical name for the command (defaults to method name)
  • @alias
    • Additional name for this command
/**
 * ...
 *
 * @synopsis   <origin> <destination>
 * @subcommand convert-terms
 * @alias      do-conversion
 */

I/O

Input: Accepting Arguments

/**
 * Demonstrate how arguments work.
 *
 * ## OPTIONS
 *
 * <required>
 * : This is a required, positional argument.
 *
 * [<optional>]
 * : This positional argument is optional.
 *
 * ...

Input: Accepting Arguments

/**
 * ...
 *
 * --option=<required>
 * : This is a required, associative argument.
 *
 * [--option2=<optional>]
 * : This associative argument is optional.
 *
 * [--flag]
 * : An optional flag.
 */
public function my_command( $args, $assoc_args ) {
    // ...

Input: Accepting Arguments

public function my_command( $args, $assoc_args ) {
    if ( isset( $assoc_args['option2'] ) ) {
        // The user passed some value to --option2.
    }
}

Input: Confirm

# Confirm an action.
WP_CLI::confirm( 'Are you sure you want to do this?' );
> Are you sure you want to do this? [y/n]

Input: Prompts

Request additional information from the user.

$email = cli\prompt( 'Email address', 'user@example.com' );
Email address [user@example.com]:

Input: Menus

Present the user with a list of options.

$options = array(
    'foo' => 'Foo',
    'bar' => 'Bar',
    'baz' => 'Baz',
);

$selected = cli\menu( $options, 'foo', 'What to do?' );
  1. Foo
  2. Bar
  3. Baz

What to do? [Foo]:

Output: The Simple Stuff

# Simply output text.
WP_CLI::log( 'A normal message' );

# Prepend the text with a colored "Warning:".
WP_CLI::warn( '¯\_(ツ)_/¯' );

# Prepend the message with "Error:" and return.
WP_CLI::error( '(╯°□°)╯︵ ┻━┻' );

# Prepend the text with a colored "Success:"!
WP_CLI::success( 'GREAT SUCCESS!' );

Output: Tables

$table = new cli\Table;
$table->setHeaders( array(
    'id'    => 'ID',
    'title' => 'Title'
) );

foreach ( $posts as $post ) {
    $table->addRow( array(
        'id'    => $post->ID,
        'title' => $post->post_title,
    ) );
}

$table->display();

Output: Tables

# $ wp my-plugin get-posts

+----+----------------------+
| ID | Title                |
+----+----------------------+
| 1  | Hello World!         |
+----+----------------------+
| 2  | Goodbye Cruel World! |
+----+----------------------+
# $ wp my-plugin get-posts > posts.csv

ID  Title
1   Hello World!
2   Goodbye Cruel World!

Output: format_items()

Shortcut for table construction, also works for YAML, JSON, and more:

$headers = array(
    'id'    => 'ID',
    'title' => 'Title'
);
$rows    = array();

foreach ( $posts as $post ) {
    $rows[] = array(
        'id'    => $post->ID,
        'title' => $post->post_title,
    );
}

WP_CLI\Utils\format_items( 'table', $rows, $headers );

Output: Progress Indicators

Show the user that progress is being made.

$progress = WP_CLI\Utils\make_progress_bar( 'Making progress', 10 );

for ( $i = 0; $i < 10; $i++ ) {
    sleep( 1 ); // Do something real here, please.
    $progress->tick();
}
Making progress  100%[=================================] 0:10 / 0:10

Error log

Don't forget the PHP error log!

error_log( 'Something went wrong!' );

More good ideas

Dry run

When performing destructive options, consider a --dry-run flag

$dry_run = isset( $assoc_args['dry-run'] );

if ( ! $dry_run ) {
    do_something_destructive();
}
WP_CLI::log( 'Something destructive happened' );

Hear me roar (or not)!

Great scripts will include options for --verbose, --quiet, or both.

// Define the value once.
$verbose = isset( $assoc_args['verbose'] );

// Simple conditional around output.
if ( $verbose ) {
    WP_CLI::log( 'Thank you for reading me!' );
}

Always remember your audience and build your command around their needs.

WP-CLI in Plugins

WP-CLI commands make a great addition to most plugins!

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

WordPress Plugin API

The actions and filters you use on your site are still active, so use them!

// Trigger some action in my theme
do_action( 'my_theme_some_action' );

// Remove filters you won't be needing.
remove_filter( 'the_content', 'wpautop' );

wp_parse_args()

Don't forget useful WordPress functions like wp_parse_args() for setting defaults!

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

Performance Tips

Caching

  • Try to cache whenever possible
  • Be very careful about what persists in the object cache!

WP_IMPORTING

  • Little-known constant that tells WordPress we're moving a lot of data around.
  • Prevent some post-import pings and extraneous checks
if ( ! defined( 'WP_IMPORTING' ) ) {
    define( 'WP_IMPORTING', true );
}

SAVEQUERIES

Useful constant for debugging, major bottleneck when running large scripts!

Activated by default in VIP Quickstart!

Garbage Collection

When a variable, instance, etc. is no longer being used, PHP will try to clean up the memory in a process known as garbage collection

unset( $instance_var );
$my_global_var = null;

Normally handled at end of request, but CLI commands can run far longer!

stop_the_insanity()

protected function stop_the_insanity() {
    global $wpdb, $wp_object_cache;

    $wpdb->queries = array();

    if ( is_object( $wp_object_cache ) ) {
        $wp_object_cache->group_ops = array();
        $wp_object_cache->stats = array();
        $wp_object_cache->memcache_debug = array();
        $wp_object_cache->cache = array();

        if ( method_exists( $wp_object_cache, '__remoteset' ) ) {
            $wp_object_cache->__remoteset(); // important
        }
    }
}

More Tips & Tricks

Bash completion

  • Not installed out of the box
  • Available via wp-cli.org

Multisite

When working with WordPress Multisite, know what site you're working on!

# Runs on the default site.
$ wp cache empty

# Runs on a specific site.
$ wp cache empty --url=http://subsite.example.com

Useful Globals

--user Set the current WordPress user --debug Show all PHP errors --prompt Prompt the user to enter values for all arguments

Don't you die on me!

Remember: calling die() or exit will kill the entire script, so use with care!

If in doubt, return early!

Sub-commands

# Launch an external process.
WP_CLI::launch( WP_CLI\Utils\esc_cmd( $command ) );

# Call another WP-CLI command.
WP_CLI::launch_self( $command, $args, $assoc_args );

Thank you!

Steve Grunwellstevegrunwell.comgrowella.com

stevegrunwell.com/slides/wp-cli

Writing WP-CLI Commands that Work! Steve Grunwell @stevegrunwell stevegrunwell.com/slides/wp-cli