How to Create Custom WP-CLI Commands
Recently I’ve been improving my Simple WordPress Crossposting plugin with some custom WP-CLI commands and got a thought in my mind – “why not create a tutorial about that?”
So here it is, below I am going to show you different examples of custom WP-CLI commands with subcommands, arguments and flags.
Creating a Simple WP-CLI Command
Let’s start it simple, so our first custom WP-CLI command is just going to print “Hello world!” or something. The whole process is just about using the WP_CLI::add_command() method.
add_action( 'cli_init', function() {
WP_CLI::add_command( 'mishatest', function( $args ) {
// print a text line
WP_CLI::log( 'Hello world, how is it going?' );
} );
} );Don’t forget to use WP_CLI inside the cli_init action hook, otherwise get ready to get a “Fatal error: Uncaught Error: Class ‘WP_CLI’ not found” message or you can also avoid it by using if ( defined( 'WP_CLI' ) && WP_CLI ) { condition like this:
if( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'mishatest', function( $args ) {
// print a text line
WP_CLI::log( 'Hello world, how is it going?' );
} );
}Or:
if( defined( 'WP_CLI' ) && WP_CLI ) {
require_once __DIR__ . '/inc/class-misha-test-command.php';
}As a result we get this:

And now let’s make the things a little bit more interesting.
Output commands (error and success messages)
In the example above we used WP_CLI::log() method to display the text, but there are more available methods:
WP_CLI::success(),WP_CLI::error(),WP_CLI::warning().
These methods just add an appropriate colored text string before the initial message like “Success:” and “Error:” (WP_CLI::error() also exits the script, for other commands you will need to additionally use WP_CLI::halt(200)). Example of messages:

Below we’re also going to use subcommands and arguments to display different kind of messages with the same WP-CLI command.
Class-based Commands
The same command can be written down as a PHP class.
class Misha_Test_Command {
public function __invoke( $args ) {
WP_CLI::log( 'Hello world, how is it going?' );
}
}
add_action( 'cli_init', function() {
WP_CLI::add_command( 'mishatest', 'Misha_Test_Command' );
// or:
// $instance = new Misha_Test_Command();
// WP_CLI::add_command( 'mishatest', $instance );
} );We are using the __invoke() method here in order to do something when the initial command is being run. Or we can create other public methods and they will automatically become subcommands.
Creating subcommands
As I’ve already mentioned before, in order to create a subcommand, we just need to add another public method to the PHP class of our command. In the example below we also have a protected method get_message() and when you try to run wp mishatest get_message you will get an error “Error: ‘get_message’ is not a registered subcommand of ‘mishatest’.
Let’s take a look:
class Misha_Test_Command {
protected function get_message() {
return 'Hello world, how is it going?';
}
// wp mishatest success
public function success() {
WP_CLI::success( $this->get_message() );
}
// wp mishatest error
public function error() {
WP_CLI::error( $this->get_message() );
}
// wp mishatest warning
public function warning() {
WP_CLI::warning( $this->get_message() );
}
// wp mishatest log
public function log() {
WP_CLI::log( $this->get_message() );
}
}
WP_CLI::add_command( 'mishatest', 'Misha_Test_Command' );You don’t forget about cli_init hook or defined( 'WP_CLI' ) && WP_CLI condition here, right?
And the result in my MacOS terminal:

Command Arguments
Now let’s try to do the same kind of things not with subcommands but with arguments.
Positional arguments
Positional arguments are just what we type after the command. For example if we take a look at the core WordPress WP-CLI command, for example wp plugin install hello, we can see here that “hello” is the positional argument which provides the name of the plugin we’re going to install.
Returning back to our simple example we can use a positional argument to provide a message type we’re going to print, for example wp mishatest success.
WP_CLI::add_command( 'mishatest', function( $args ) {
// define the message type
$message_type = ! empty( $args[0] ) ? $args[0] : 'log';
switch( $message_type ) {
case 'success' : {
WP_CLI::success( 'Hello world, how is it going?' );
break;
}
case 'error' : {
WP_CLI::error( 'Hello world, how is it going?' );
break;
}
case 'warning' : {
WP_CLI::warning( 'Hello world, how is it going?' );
break;
}
case 'log' : {
WP_CLI::log( 'Hello world, how is it going?' );
break;
}
}
} );Here we go:

Flags (associative arguments)
And now let’s change our command a little bit, so it is going to look that way: wp mishatest --message_type=success.
WP_CLI::add_command( 'mishatest', function( $args, $flags ) {
// merge the actual flag values with the default values
$flags = wp_parse_args(
$flags,
array(
'message_type' => 'log',
)
);
switch( $flags[ 'message_type' ] ) {
case 'success' : {
WP_CLI::success( 'Hello world, how is it going?' );
break;
}
case 'error' : {
WP_CLI::error( 'Hello world, how is it going?' );
break;
}
case 'warning' : {
WP_CLI::warning( 'Hello world, how is it going?' );
break;
}
case 'log' : {
WP_CLI::log( 'Hello world, how is it going?' );
break;
}
}
} );Examples of commands:

Commands with progress bar
Displaying text messages is super-fun, but let’s do something more meaningful. For example let’s generate posts with the help of an external API (it can be AI by the way).
class Misha_Dino_Command {
// this method can not be used as a subcommand because it is a protected method
protected function get_title() {
// right now we're getting a random dino name via API
$res = wp_remote_get(
add_query_arg(
array(
'format' => 'text',
'paragraphs' => 1,
'words' => 1,
),
'https://dinoipsum.com/api/'
)
);
if( 'OK' === wp_remote_retrieve_response_message( $res ) ) {
// return a randomly generated dino name
return wp_remote_retrieve_body( $res );
} else {
// return an error and exit the script
WP_CLI::error( 'Oops... something is wrong with the dino name!' );
}
}
// generate posts
public function generate( $args, $flags ) {
$flags = wp_parse_args( $flags, array( 'count' => 5 ) );
$progress = \WP_CLI\Utils\make_progress_bar( 'Generating Posts', $flags[ 'count' ] );
for( $i = 0; $i < $flags[ 'count' ]; $i++ ) {
wp_insert_post( array(
'post_title' => $this->get_title(),
'post_status' => 'publish',
'post_type' => 'post',
) );
$progress->tick();
}
$progress->finish();
WP_CLI::success( $flags[ 'count' ] . ' dinosaurus generated!' );
}
}
add_action( 'cli_init', function() {
WP_CLI::add_command( 'mishatest', 'Misha_Dino_Command' );
} );Running the command:

Generated posts in WordPress admin:

Help documentation for a command
The last but not the least, let’s provide a help documentation to our command (to be more exact – subcommand) which will be available just by typing --help flag at the end.
There are actually some standards to that, you can find them in the official WordPress documentation here.
/**
* Generates dinosaurus (posts).
*
* ## OPTIONS
*
* [--count=<count>]
* : How many posts to generate in a single run.
* ---
* default: 5
* ---
*
* ## EXAMPLES
*
* # Generate 50 posts.
* $ wp mishatest generate --count=50
* Success: 50 dinosaurus generated!
*/
public function generate( $args, $flags ) {
What else would you like to learn about WP-CLI? Let me know in the comments.
Misha Rudrastyh
Hey guys and welcome to my website. For more than 15 years I've been doing my best to share with you some superb WordPress guides and tips for free.
Need some developer help? Contact me
Very nice!