Prerequisites
If you want to follow these examples in your local environment, you’ll need a theme with PHP template files, a hybrid theme, and, of course, WP-CLI installed. I’m using Twenty Twenty-One as the last default hybrid theme.
HINT: quickly download and activate Twenty Twenty-One theme
wp theme install --activate twentytwentyone
A problem/use case
When you start writing PHP code in a WordPress theme or plugin, learning the Loop is one of the most important things. It is the base of every template file. Similarly, to perform many highly useful actions with WP-CLI, you need to learn and understand loops in CLI tools.
Examples
List all the posts
This snippet will give different results depending on the template. You’ll get a list of posts in all categories on archive templates, as many as you set in reading options; you’ll get only one on singular templates. Different archives will have different lists. The most simple loop, in the most generic template, the index.php
, will just list all posts (limited by the number set in options) in descending order, ordered by published date.
HINT: quickly generate 100 posts
wp post generate
if ( have_posts() ) :
while ( have_posts() ) : the_post();
// Display post content
endwhile;
endif;
WP-CLI:
wp post list
If we want to go beyond these default templates and queries, which we often do, we must create a custom query. That means modifying arguments for the WP_Query
object.
List all posts in category
For example, we want to see all the posts from the Terry Pratchett
category. On the category.php
template, we would use the first PHP example. But maybe we want to list all these posts on a single.php
template as a part of the “Also in this category” section.
HINT: quickly create a new category
wp term create category "Terry Pratchett" --description="The best reporter on the Discworld."
The loop from the first example will give the current post in this template, so we need to create another loop with a custom query to get all the other posts from the same category.
HINT: quickly move all posts from Uncategorized category to Terry Pratchett
for x in $(wp post list --cat=1 --field=ID) ; do wp post term add $x category terry-pratchett && wp post term remove $x category uncategorized ; done
// We want to get category ID dynamically.
$category = get_the_category( get_the_ID() );
$args = array(
'post_type' => 'post',
'posts_per_page' => 1000, // Never use -1, it's very bad for performance.
'cat' => $category[0]->term_id, // Assuming the post has only one category.
);
$in_this_category = new WP_Query( $args );
if ( $in_this_category->have_posts() ) :
while ( $in_this_category->have_posts() ) : $in_this_category->the_post();
// Display post content
endwhile;
endif;
// Always reset post data after the custom loop to avoid unexpected results on the rest of the page.
wp_reset_postdata();
HINT: quickly find category term ID
wp term list category --fields=term_id,slug
WP-CLI:
wp post list --cat=2
List all posts in the category but exclude the current post
With the previous example, we’re listing all the posts in the category, including the current one. That’s usually not desired, and, more often than not, you’ll want to exclude it. You might be tempted to use post__not_in
argument, like this:
// Get current post ID.
$current_post = get_the_ID();
$args = array(
'post_type' => 'post',
'posts_per_page' => 1000, // Never use -1, it's very bad for performance.
'cat' => $category[0]->term_id, // Assuming the post has only one category.
'post__not_in' => $current_post, // Also very bad for performance.
);
Even though this example fits very well with my point, which we will come to later, I can not, in my good conscience, leave it here as an example of how to exclude the current post. What you really want to do in this situation is to get all posts with the query and skip the current post later on inside the loop.
// Get current post ID.
$current_post = get_the_ID();
// We want to get category ID dynamically.
$category = get_the_category( $current_post );
$args = array(
'post_type' => 'post',
'posts_per_page' => 1000, // Never use -1, it's very bad for performance.
'cat' => $category[0]->term_id, // Assuming the post has only one category.
);
$in_this_category = new WP_Query( $args );
if ( $in_this_category->have_posts() ) :
while ( $in_this_category->have_posts() ) : $in_this_category->the_post();
if ( get_the_ID() === $current_post ) {
continue;
}
// Display post content
endwhile;
endif;
// Always reset post data after the custom loop to avoid unexpected results on the rest of the page.
wp_reset_postdata();
For the sake of the example, let’s say the current post ID is 82.
WP-CLI:
wp post list --cat=2 --post__not_in=82
The thing to note here are parameter names. WP-CLI uses the same parameter names as WP_Query: cat
and post__not_in
. The only difference is that WP_Query needs them in an array, while WP-CLI prefixes them with --
.
Return only post IDs
Let’s say you want to list only post titles as links. You don’t need the whole post object being returned by the query. You’ll only need post IDs and can get them with the fields parameter. You can switch to foreach a loop as well.
// Get current post ID.
$current_post = get_the_ID();
// We want to get category ID dynamically.
$category = get_the_category( $current_post );
$args = array(
'post_type' => 'post',
'posts_per_page' => 1000, // Never use -1, it's very bad for performance.
'cat' => $category[0]->term_id, // Assuming the post has only one category.
'fields' => 'ids',
);
$in_this_category = new WP_Query( $args );
foreach ( $in_this_category->posts as $id ) {
if ( $id === $current_post ) {
continue;
}
$permalink = get_the_permalink( $id );
$title = get_the_title( $id );
echo '<a href="' , esc_url( $permalink ) , '">' , esc_html( $title ) , '</a>';
}
// Always reset post data after the custom loop to avoid unexpected results on the rest of the page.
wp_reset_postdata();
WP-CLI:
wp post list --cat=2 --post__not_in=82 --fields=ID
This is the only place the WP-CLI parameter has a different name than the WP_Query one.
You may have never thought about what the query returned to you because it was never on your way – you would use only what you needed. Might as well get the whole post object because you never know what the future brings.
However, by default, you’ll get post ID, title, post name, published data, and status fields in WP-CLI. If you’re using the loop to perform some action other than listing all posts, chances are you don’t need all those parameters.
+-----+--------------+-------------+---------------------+-------------+
| ID | post_title | post_name | post_date | post_status |
+-----+--------------+-------------+---------------------+-------------+
| 105 | Post 101 | post-101 | 2024-01-13 12:02:38 | publish |
| 85 | Post 81 | post-81 | 2024-01-13 12:02:38 | publish |
+-----+--------------+-------------+---------------------+-------------+
Because of this, you can really fine-tune what fields you’ll get back from the WP-CLI query. WP_Query has only a few options for the fields
argument: all
, ids
, and id=>parent
; while WP-CLI can use ALL query arguments.
I’ll repeat this: WP-CLI can use any query argument for returning values.
For example, return post title, published and modified date for all posts and order descending by modified date:
wp post list --fields=post_title,post_date,post_modified --order=DESC --orderby=post_modified
Go ahead, try it.
Conclusion
WP-CLI queries are template-agnostic and treat every query as a custom one. Listing posts in WP-CLI has a different purpose than loops in PHP files. Other than informational value, just listing posts is not very useful in the terminal. But performing and understanding loops in WP-CLI is crucial for doing other actions that would take much time in PHP (or dashboard) and only one command in WP-CLI (e.g. deleting tens of thousands of posts).
Helpful commands
Download and activate Twenty Twenty-One theme
wp theme install --activate twentytwentyone
Generate 100 posts
wp post generate
Create a new category
wp term create category "Terry Pratchett" --description="The best reporter on the Discworld."
Find category terms IDs
wp term list category --fields=term_id,slug
Move all posts from Uncategorized category to Terry Pratchett
for x in $(wp post list --cat=1 --field=ID) ; do wp post term add $x category terry-pratchett && wp post term remove $x category uncategorized ; done