So, you’re looking to supercharge your WordPress workflow with some custom WP-CLI commands. That’s a smart move! Instead of clicking around in the admin or writing custom PHP scripts for repetitive tasks, you can build command-line tools that do exactly what you need. And the best part? WP-CLI is incredibly flexible, letting you create commands with subcommands, handle arguments, and even format your output nicely. Let’s dive into how to actually do it.
Before we get too deep, let’s set the stage. You’ll need a WordPress installation and WP-CLI itself. If you don’t have WP-CLI set up, that’s your very first step. You can find detailed instructions on the official WP-CLI website. Once that’s done, you’re ready to start building.
Where Your Custom Commands Live
WP-CLI looks for custom commands in a few specific places. The most common and practical spot for your own work is within your WordPress installation itself.
The wp-cli.yml Configuration File
You can tell WP-CLI where to find your custom command files by creating a wp-cli.yml file in the root of your WordPress installation (the same directory as wp-config.php). This file is a YAML file, which is pretty straightforward.
Here’s a basic wp-cli.yml to get you started:
“`yaml
path: .
core: abs/path/to/wordpress/install # Replace with your actual WordPress path
require:
- ../my-wp-cli-commands/my-commands.php
“`
The require directive is the key here. It tells WP-CLI to load the specified PHP file (in this case, my-commands.php) which will contain your custom command definitions. You can specify multiple files or even entire directories to require.
Alternative: Global wp-cli.yml
You can also have a global wp-cli.yml file located in your home directory. This is useful if you have commands you want to use across many different WordPress installations. The priority given to the local wp-cli.yml means it will override global settings for that specific site.
The Anatomy of a WP-CLI Command Class
Every custom WP-CLI command is essentially a PHP class that extends WP_CLI_Command. This class will house your command’s logic, its subcommands, and how it handles arguments and parameters.
Basic Command Structure
Let’s look at a simple example of a command class. This one will be named My_Custom_Command and will offer a basic hello command.
“`php
if ( ! class_exists( ‘WP_CLI’ ) ) {
return;
}
/**
- A simple example of a custom WP-CLI command.
*/
class My_Custom_Command extends WP_CLI_Command {
/**
- Greets the user.
*
- ## OPTIONS
*
- –name=
- : The name of the person to greet.
*
- ## EXAMPLES
*
- wp my-custom hello –name=World
*
- @param array $args Positional arguments.
- @param array $assoc_args Associative arguments.
*/
public function hello( $args, $assoc_args ) {
$name = \WP_CLI\Utils\get_flag_value( $assoc_args, ‘name’, ‘World’ );
\WP_CLI::success( “Hello, {$name}!” );
}
}
// Register the command with WP-CLI.
\WP_CLI::add_command( ‘my-custom’, ‘My_Custom_Command’ );
“`
Save this code in a file like my-commands.php (as referenced in our wp-cli.yml) and place it within your custom command directory (e.g., ../my-wp-cli-commands/). Now, if you run wp my-custom hello, you should see “Hello, World!”. If you run wp my-custom hello --name=Alice, you’ll see “Hello, Alice!”.
If you’re looking to deepen your understanding of creating custom WP-CLI commands, you might find it helpful to explore related resources that offer insights into best practices and advanced techniques. One such article is available at this link, where you can discover additional tips and examples that can enhance your command-writing skills in WordPress.
Crafting Subcommands: Building Command Hierarchies
Most useful commands aren’t just single actions. They often have related sub-actions. WP-CLI makes this easy with subcommands.
How Subcommands Work
When you define a public method within your WP_CLI_Command class, WP-CLI automatically registers it as a subcommand. The method name becomes the subcommand name.
Defining Subcommands as Methods
In the My_Custom_Command class above, the hello() method is a subcommand. If you had another public method like goodbye(), it would become a subcommand named goodbye.
“`php
// … (previous code) …
class My_Custom_Command extends WP_CLI_Command {
// … (hello method) …
/**
- Says goodbye to the user.
*
- ## OPTIONS
*
- –farewell=
- : The farewell message.
*
- ## EXAMPLES
*
- wp my-custom goodbye –farewell=See you later!
*
- @param array $args Positional arguments.
- @param array $assoc_args Associative arguments.
*/
public function goodbye( $args, $assoc_args ) {
$farewell = \WP_CLI\Utils\get_flag_value( $assoc_args, ‘farewell’, ‘Goodbye!’ );
\WP_CLI::success( $farewell );
}
}
\WP_CLI::add_command( ‘my-custom’, ‘My_Custom_Command’ );
“`
Now you can run wp my-custom goodbye and wp my-custom goodbye --farewell=So long!.
Nested Subcommands: Deeper Hierarchies
For more complex command structures, you can even create nested subcommands. This involves having a command class that defines subcommands, and one of those subcommands is itself a command class that defines further subcommands. This can get a bit verbose quickly, so use it judiciously.
Let’s imagine a scenario where we want to manage user roles. We could have a wp user-roles command that then has subcommands like list, add, remove, and assign.
“`php
if ( ! class_exists( ‘WP_CLI’ ) ) {
return;
}
/**
- Manages user roles.
*/
class My_User_Roles_Command extends WP_CLI_Command {
/**
- Lists all available user roles.
*
- @param array $args Positional arguments.
- @param array $assoc_args Associative arguments.
*/
public function list_( $args, $assoc_args ) { // Trailing underscore to avoid conflict with PHP keyword ‘list’
$roles = wp_roles()->roles;
if ( empty( $roles ) ) {
\WP_CLI::warning( ‘No roles found.’ );
return;
}
foreach ( $roles as $role_slug => $role_data ) {
\WP_CLI::line( “{$role_slug}: {$role_data[‘name’]}” );
}
}
/**
- Adds a new user role.
*
- ## OPTIONS
*
- –slug=
- : The unique slug for the new role.
*
- –name=
- : The display name for the new role.
*
- ## EXAMPLES
*
- wp my-user-roles add –slug=super_editor –name=”Super Editor”
*
- @param array $args Positional arguments.
- @param array $assoc_args Associative arguments.
*/
public function add( $args, $assoc_args ) {
$slug = \WP_CLI\Utils\get_flag_value( $assoc_args, ‘slug’ );
$name = \WP_CLI\Utils\get_flag_value( $assoc_args, ‘name’ );
if ( ! $slug || ! $name ) {
\WP_CLI::error( ‘Both –slug and –name are required.’ );
return;
}
if ( add_role( $slug, $name ) ) {
\WP_CLI::success( “Role ‘{$name}’ ({$slug}) added successfully.” );
} else {
\WP_CLI::error( “Failed to add role ‘{$name}’ ({$slug}). It might already exist.” );
}
}
}
\WP_CLI::add_command( ‘my-user-roles’, ‘My_User_Roles_Command’ );
“`
Now you can run:
wp my-user-roles list
wp my-user-roles add --slug=content_writer --name="Content Writer"
This structure keeps your commands organized and makes them more intuitive to use.
Handling Arguments: Getting Data Into Your Commands
Commands often need to operate on specific data. That’s where arguments and parameters come in. WP-CLI provides robust ways to define and handle them.
Positional Arguments
These are the arguments you pass to your command without a name, in a specific order. Think of them like array elements.
Defining Positional Arguments
Positional arguments are captured in the $args array that’s passed to your command method. The order matters.
Consider a command that takes a post ID and then a status:
“`php
// … (previous code) …
class My_Content_Command extends WP_CLI_Command {
/**
- Updates the status of a post.
*
- @param array $args Positional arguments. Expected: [post_id], [new_status].
- @param array $assoc_args Associative arguments.
*/
public function update_status( $args, $assoc_args ) {
if ( count( $args ) < 2 ) {
\WP_CLI::error( ‘Usage: wp my-content update-status
return;
}
$post_id = (int) $args[0];
$new_status = sanitize_text_field( $args[1] );
$post = get_post( $post_id );
if ( ! $post ) {
\WP_CLI::error( “Post with ID {$post_id} not found.” );
return;
}
// Basic status validation (can be expanded)
$valid_statuses = array( ‘publish’, ‘pending’, ‘draft’, ‘private’, ‘trash’ );
if ( ! in_array( $new_status, $valid_statuses ) ) {
\WP_CLI::error( “Invalid status: {$new_status}. Allowed statuses are: ” . implode( ‘, ‘, $valid_statuses ) );
return;
}
$updated = wp_update_post( array( ‘ID’ => $post_id, ‘post_status’ => $new_status ) );
if ( $updated && ! is_wp_error( $updated ) ) {
\WP_CLI::success( “Post {$post_id} status updated to ‘{$new_status}’.” );
} else {
\WP_CLI::error( “Failed to update post {$post_id} status.” );
if ( is_wp_error( $updated ) ) {
\WP_CLI::error( $updated->get_error_message() );
}
}
}
}
\WP_CLI::add_command( ‘my-content’, ‘My_Content_Command’ );
“`
You would run this like: wp my-content update-status 123 draft.
Associative Arguments (Optional Parameters / Flags)
These are named parameters, often preceded by --. They are incredibly useful for providing optional settings or values to your commands. These are captured in the $assoc_args array.
Defining Associative Arguments with Docblocks
The power of associative arguments comes from how you document them in your method’s docblock. WP-CLI parses this documentation to understand what arguments your command expects and how to use them.
The standard format is followed by OPTIONS, then each option on a new line.
“`
OPTIONS
–option-name=
–no-option A boolean flag (no value needed).
–optional-option[=
“`
Let’s revisit our hello command to demonstrate.
“`php
// … (previous code) …
class My_Custom_Command extends WP_CLI_Command {
/**
- Greets the user.
*
- ## OPTIONS
*
- –name=
- : The name of the person to greet.
*
- –formal
- : Use a formal greeting.
*
- ## EXAMPLES
*
- wp my-custom hello –name=Alice
- wp my-custom hello –name=Bob –formal
- wp my-custom hello
*
- @param array $args Positional arguments.
- @param array $assoc_args Associative arguments.
*/
public function hello( $args, $assoc_args ) {
// Get the name, defaulting to ‘World’ if not provided.
$name = \WP_CLI\Utils\get_flag_value( $assoc_args, ‘name’, ‘World’ );
// Check if the –formal flag is present.
$is_formal = isset( $assoc_args[‘formal’] );
if ( $is_formal ) {
\WP_CLI::success( “Greetings, {$name}!” );
} else {
\WP_CLI::success( “Hello, {$name}!” );
}
}
}
\WP_CLI::add_command( ‘my-custom’, ‘My_Custom_Command’ );
“`
Now you can run:
wp my-custom hello --name=Alice
wp my-custom hello --name=Bob --formal
wp my-custom hello
Boolean Flags
When you have an option that is just a presence or absence, like --force or --verbose, you define it without . Example: --force.
In your PHP method, you’ll check for its existence in $assoc_args.
“`php
// … inside your command method …
if ( isset( $assoc_args[‘force’] ) ) {
// Do something forcefully
\WP_CLI::line( ‘Running in force mode.’ );
}
“`
Optional Flags with Values
If an option might or might not have a value, use [=. Example: --output-file[=.
“`php
// … inside your command method …
$output_file = \WP_CLI\Utils\get_flag_value( $assoc_args, ‘output-file’ );
if ( $output_file ) {
// Write to the specified file
\WP_CLI::line( “Output will be written to: {$output_file}” );
}
“`
Retrieving Arguments Safely
WP-CLI provides utility functions to make retrieving arguments easier and safer, especially for handling defaults and type casting.
\WP_CLI\Utils\get_flag_value()
This is your go-to function for associative arguments. It takes the $assoc_args array, the flag name, and an optional default value.
$value = \WP_CLI\Utils\get_flag_value( $assoc_args, 'your-flag-name', 'default_value' );
\WP_CLI\Utils\parse_ssh()
If your command needs to accept SSH connection parameters, this utility can help parse them.
\WP_CLI\Utils\is_file_or_directory()
Useful for validating that a provided path argument is indeed a file or directory.
If you’re looking to enhance your WordPress development skills, understanding how to write a custom WP-CLI command with subcommands, arguments, and output formatting can be incredibly beneficial. For a deeper dive into related topics, you might find it useful to explore how to manage server migrations effectively. A great resource on this subject is an article about migrating from one CyberPanel to another, which provides insights that can complement your WP-CLI knowledge. You can check it out here.
Output Formatting: Making Your Commands Readable
Raw output is okay sometimes, but for many tasks, clear, formatted output is crucial. WP-CLI offers several ways to achieve this.
Basic Output Utilities
WP-CLI provides simple functions for different types of output.
\WP_CLI::line()
Prints a simple line of text.
\WP_CLI::success()
Prints a success message, often with a green checkmark.
\WP_CLI::error()
Prints an error message, typically in red, and often exits the script.
\WP_CLI::warning()
Prints a warning message, usually in yellow.
\WP_CLI::log()
For general logging messages.
\WP_CLI::debug()
For detailed debugging information, which can be toggled with the --debug flag.
Let’s incorporate these into our My_Content_Command example.
“`php
// … (previous code) …
class My_Content_Command extends WP_CLI_Command {
/**
- Updates the status of a post.
*
- @param array $args Positional arguments. Expected: [post_id], [new_status].
- @param array $assoc_args Associative arguments.
*/
public function update_status( $args, $assoc_args ) {
if ( count( $args ) < 2 ) {
\WP_CLI::error( ‘Usage: wp my-content update-status
return;
}
$post_id = (int) $args[0];
$new_status = sanitize_text_field( $args[1] );
$post = get_post( $post_id );
if ( ! $post ) {
\WP_CLI::error( “Post with ID {$post_id} not found.” );
return;
}
$valid_statuses = array( ‘publish’, ‘pending’, ‘draft’, ‘private’, ‘trash’ );
if ( ! in_array( $new_status, $valid_statuses ) ) {
\WP_CLI::error( “Invalid status: {$new_status}. Allowed statuses are: ” . implode( ‘, ‘, $valid_statuses ) );
return;
}
\WP_CLI::log( “Attempting to update post {$post_id} to status ‘{$new_status}’…” );
$updated = wp_update_post( array( ‘ID’ => $post_id, ‘post_status’ => $new_status ) );
if ( $updated && ! is_wp_error( $updated ) ) {
\WP_CLI::success( “Post {$post_id} status updated successfully to ‘{$new_status}’.” );
} else {
\WP_CLI::error( “Failed to update post {$post_id} status.” );
if ( is_wp_error( $updated ) ) {
\WP_CLI\log( “WP Error: ” . $updated->get_error_message() ); // Using log for potentially non-critical errors
}
}
}
}
\WP_CLI::add_command( ‘my-content’, ‘My_Content_Command’ );
“`
Table Formatting
For displaying lists of data where columns make sense, WP-CLI has a built-in table renderer.
Using \WP_CLI\Table
You typically prepare an array of arrays, where each inner array is a row and the keys of the first row define the column headers.
“`php
// … (previous code) …
class My_User_Command extends WP_CLI_Command {
/**
- Lists users with their ID, login, and roles.
*
- ## OPTIONS
*
- –role=
- : Filter users by a specific role.
*
- ## EXAMPLES
*
- wp my-user list
- wp my-user list –role=editor
*
- @param array $args Positional arguments.
- @param array $assoc_args Associative arguments.
*/
public function list_( $args, $assoc_args ) { // Trailing underscore for ‘list’
$role_filter = isset( $assoc_args[‘role’] ) ? sanitize_text_field( $assoc_args[‘role’] ) : null;
$users_query = new WP_User_Query( array( ‘role’ => $role_filter ) );
$users = $users_query->get_results();
if ( empty( $users ) ) {
if ( $role_filter ) {
\WP_CLI::warning( “No users found with the role ‘{$role_filter}’.” );
} else {
\WP_CLI::warning( “No users found in the system.” );
}
return;
}
$table_data = array();
foreach ( $users as $user ) {
$table_data[] = array(
‘ID’ => $user->ID,
‘Login’ => $user->user_login,
‘Roles’ => implode( ‘, ‘, $user->roles ),
);
}
$table = new \WP_CLI\Table();
$table->set_headers( array( ‘ID’, ‘Login’, ‘Roles’ ) );
$table->set_rows( $table_data );
$table->display();
}
}
\WP_CLI::add_command( ‘my-user’, ‘My_User_Command’ );
“`
Now, wp my-user list will display your users in a clean table, and wp my-user list --role=editor will filter it.
JSON Output
For programmatic consumption, JSON is often the best format. WP-CLI has a dedicated command for this.
Using \WP_CLI::format_json()
You can simply pass your data to this function, and it will output valid JSON.
“`php
// … (previous code) …
class My_Data_Command extends WP_CLI_Command {
/**
- Exports site options as JSON.
*
- ## OPTIONS
*
- –format=
- : The output format. Can be ‘json’ or ‘table’. Defaults to ‘json’.
*
- ## EXAMPLES
*
- wp my-data export-options
- wp my-data export-options –format=table
*
- @param array $args Positional arguments.
- @param array $assoc_args Associative arguments.
*/
public function export_options( $args, $assoc_args ) {
$options = get_option( ‘site_options’, array() ); // Example: getting site options
// In a real scenario, you might fetch specific options or all options.
// For this example, let’s just use a few dummy options.
$data_to_export = array(
‘siteurl’ => get_option( ‘siteurl’ ),
‘home’ => get_option( ‘home’ ),
‘my_custom_setting’ => get_option( ‘my_custom_setting’, ‘not set’ ),
);
$format = \WP_CLI\Utils\get_flag_value( $assoc_args, ‘format’, ‘json’ );
if ( ‘json’ === $format ) {
\WP_CLI::format_json( $data_to_export );
} elseif ( ‘table’ === $format ) {
$table_data = array();
foreach ( $data_to_export as $key => $value ) {
$table_data[] = array( ‘Option’ => $key, ‘Value’ => $value );
}
$table = new \WP_CLI\Table();
$table->set_headers( array( ‘Option’, ‘Value’ ) );
$table->set_rows( $table_data );
$table->display();
} else {
\WP_CLI::error( “Invalid format ‘{$format}’. Supported formats are ‘json’ and ‘table’.” );
}
}
}
\WP_CLI::add_command( ‘my-data’, ‘My_Data_Command’ );
“`
Running wp my-data export-options will output JSON. wp my-data export-options --format=table will show it in a table.
If you’re looking to enhance your WordPress development skills, you might find it helpful to explore a related article that delves into the intricacies of creating custom WP-CLI commands. This resource provides valuable insights on how to effectively manage subcommands, arguments, and output formatting, making your command-line interactions more efficient. For a deeper understanding, check out this informative piece on WordPress command-line tools that can complement your learning experience.
Error Handling and User Feedback
No one likes cryptic errors. Good error handling and clear feedback are essential for user-friendly commands.
Robust Error Handling
Always anticipate what could go wrong.
Validation
Validate all inputs, whether they come from positional arguments, associative arguments, or external sources. Use PHP’s built-in validation functions or custom logic.
is_wp_error() Checks
When interacting with WordPress core functions that can return WP_Error objects (like wp_update_post, wp_insert_user), always check is_wp_error() and handle it gracefully.
Exiting Gracefully
Use \WP_CLI::error() to signal a failure. This will typically exit the script with a non-zero status code, which is standard for command-line tools to indicate an error occurred.
Providing Context to Errors
Don’t just say “Error.” Explain what happened and, if possible, how to fix it.
Informative Error Messages
When calling \WP_CLI::error(), include details about the failure.
“`php
// Instead of:
// \WP_CLI::error( ‘Failed.’ );
// Do this:
$result = do_something_that_might_fail();
if ( is_wp_error( $result ) ) {
\WP_CLI::error( “Operation failed: ” . $result->get_error_message() );
}
“`
User Feedback Beyond Success/Error
Sometimes, you want to let the user know what’s happening without it being a success or an error.
Using \WP_CLI::log() and \WP_CLI::line()
These are perfect for progress updates, confirmation messages, or simply providing informational output.
“`php
\WP_CLI::log( “Processing item {$i} of {$total}…” );
\WP_CLI::line( “Task completed.” );
“`
The --debug Flag
For really detailed debugging output, make sure your command supports the --debug flag. WP-CLI automatically handles this for you if you use \WP_CLI::debug().
“`php
\WP_CLI::debug( “Internal variable value: ” . print_r( $variable, true ) );
“`
Best Practices and Advanced Tips
As you build more complex commands, keep these practices in mind.
Keep Commands Focused
Each command should ideally do one thing and do it well. This makes them easier to understand, test, and reuse. If a command starts to feel like it’s doing too much, consider splitting it into multiple commands or subcommands.
Use WordPress APIs
Leverage WordPress core functions (get_posts, update_post_meta, wp_users_list, etc.) whenever possible. This ensures your commands behave consistently with the rest of WordPress and benefit from its internal logic and security measures.
Test Your Commands
Just like any code, your WP-CLI commands need testing.
Manual Testing
Run your commands in various scenarios: with different arguments, edge cases, and error conditions.
Unit/Integration Testing
For more robust solutions, consider writing PHPUnit tests for your custom commands. WP-CLI provides tools and guidance for this.
Documentation is Key
Your docblocks are not just for WP-CLI; they are your command’s documentation. Make them clear, concise, and accurate. Include:
- A brief description of what the command does.
- Detailed explanations of options and arguments.
- Realistic examples of how to use the command.
WP-CLI itself uses these docblocks to generate help text when you run wp help my-command.
Namespace Your Commands
To avoid conflicts with other plugins or themes that might be creating their own WP-CLI commands, it’s good practice to namespace your commands. For example, instead of just my-command, use myplugin-command or mytheme-command. This makes it clear where the command originates.
Consider Performance
If your commands will be running on large sites or in automated scripts, be mindful of performance. Avoid unnecessary database queries, looping through large datasets inefficiently, and consider providing options to limit the scope of an operation (e.g., --limit=, --post-type=).
Security
Always sanitize and validate any user-provided input. Treat all external input as potentially untrusted, even if you’re the one running the command. Use WordPress’s built-in sanitization functions.
By following these guidelines, you can build powerful, reliable, and user-friendly custom WP-CLI commands that will significantly enhance your WordPress development and management experience. Happy commanding!