From 37bcf2a6f4bf0d937b64e92c3f3a75f7c132181c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 10:30:00 +0000 Subject: [PATCH 1/5] Document the category and tag filters `wp post list` accepts `--cat`, `--tag` and `--category_name` are WP_Query arguments that already worked here and nothing wrote down. Checked with values shaped the way the command line delivers them, since none of this needs code to work: cat=2 -> 1 post cat=2,3 -> 2 posts, comma matches any cat=-2 -> 2 posts, a negative ID excludes tag=alpha-tag -> 1 post tag=alpha-tag,beta-tag -> 2 posts, comma matches any tag=alpha-tag+beta-tag -> 0 posts, plus requires all 'feed' is left alone deliberately. It is a query var WordPress fills in for RSS and the like, it filters nothing when passed to WP_Query, and writing it down would advertise a no-op. 'tag_id' is left alone for a different reason. It filters, but documenting a name puts it in the candidate set wp-cli/wp-cli#6392 matches typos against, and 'tag_id' sits within two edits of both 'page_id' and 'tag__in'. It would trade the two false positives this commit removes for two others. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 13 ++++++++++++- features/post.feature | 35 +++++++++++++++++++++++++++++++++++ src/Post_Command.php | 11 +++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8aa6c4f16..a1862e9bf 100644 --- a/README.md +++ b/README.md @@ -3150,7 +3150,7 @@ wp post get [--field=] [--fields=] [--format=] Gets a list of posts. ~~~ -wp post list [--=] [--p=|ID] [--title=|post_title] [--name=<slug>|post_name] [--author=<author>|post_author] [--author_name=<author_name>] [--post_type=<post_type>] [--post_status=<post_status>] [--post_parent=<post_parent>] [--post_mime_type=<post_mime_type>] [--menu_order=<menu_order>] [--comment_status=<comment_status>] [--ping_status=<ping_status>] [--comment_count=<comment_count>] [--s=<string>] [--year=<year>] [--monthnum=<monthnum>] [--day=<day>] [--m=<yearmonth>] [--w=<week>] [--field=<field>] [--fields=<fields>] [--format=<format>] +wp post list [--<field>=<value>] [--p=<id>|ID] [--title=<title>|post_title] [--name=<slug>|post_name] [--author=<author>|post_author] [--author_name=<author_name>] [--cat=<cat>] [--category_name=<category_name>] [--tag=<tag>] [--post_type=<post_type>] [--post_status=<post_status>] [--post_parent=<post_parent>] [--post_mime_type=<post_mime_type>] [--menu_order=<menu_order>] [--comment_status=<comment_status>] [--ping_status=<ping_status>] [--comment_count=<comment_count>] [--s=<string>] [--year=<year>] [--monthnum=<monthnum>] [--day=<day>] [--m=<yearmonth>] [--w=<week>] [--field=<field>] [--fields=<fields>] [--format=<format>] ~~~ Display posts based on all arguments supported by [WP_Query()](https://developer.wordpress.org/reference/classes/wp_query/). @@ -3186,6 +3186,17 @@ Only shows post types marked as post by default. [--author_name=<author_name>] Filter by the 'user_nicename' of the post's author. + [--cat=<cat>] + Filter by category ID. Accepts a comma-separated list to match any of + them, and a negative ID excludes that category instead. + + [--category_name=<category_name>] + Filter by category slug. + + [--tag=<tag>] + Filter by tag slug. Accepts a comma-separated list to match any of them, + or a '+'-separated list to require all of them. + [--post_type=<post_type>] Filter by post type. Defaults to 'post'. Accepts a comma-separated list, or 'any' for every type registered without 'exclude_from_search'. diff --git a/features/post.feature b/features/post.feature index 7d7571e1b..676e79dd5 100644 --- a/features/post.feature +++ b/features/post.feature @@ -688,3 +688,38 @@ Feature: Manage WordPress posts """ {DOOMED_ID} """ + + Scenario: Filtering by category and tag + When I run `wp term create category 'Alpha Cat' --porcelain` + Then save STDOUT as {CAT_ID} + + When I run `wp term create post_tag 'Alpha Tag' --slug=alpha-tag --porcelain` + Then save STDOUT as {TAG_ID} + + When I run `wp post create --post_title='Tagged' --post_status=publish --post_category={CAT_ID} --tags_input=alpha-tag --porcelain` + Then STDOUT should be a number + + When I run `wp post list --cat={CAT_ID} --format=count` + Then STDOUT should be: + """ + 1 + """ + + # A negative ID excludes that category rather than selecting it. + When I run `wp post list --cat=-{CAT_ID} --field=post_title` + Then STDOUT should not contain: + """ + Tagged + """ + + When I run `wp post list --tag=alpha-tag --format=count` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp post list --category_name=alpha-cat --format=count` + Then STDOUT should be: + """ + 1 + """ diff --git a/src/Post_Command.php b/src/Post_Command.php index ab3a26f79..89cf61633 100644 --- a/src/Post_Command.php +++ b/src/Post_Command.php @@ -644,6 +644,17 @@ protected function delete_callback( $post_id, $assoc_args ) { * [--author_name=<author_name>] * : Filter by the 'user_nicename' of the post's author. * + * [--cat=<cat>] + * : Filter by category ID. Accepts a comma-separated list to match any of + * them, and a negative ID excludes that category instead. + * + * [--category_name=<category_name>] + * : Filter by category slug. + * + * [--tag=<tag>] + * : Filter by tag slug. Accepts a comma-separated list to match any of them, + * or a '+'-separated list to require all of them. + * * [--post_type=<post_type>] * : Filter by post type. Defaults to 'post'. Accepts a comma-separated list, * or 'any' for every type registered without 'exclude_from_search'. From 81f5b3e190b17a9020a52f5ddfa50896d8f16a26 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 18 Aug 2026 10:47:16 +0000 Subject: [PATCH 2/5] Document the meta, time, password and ordering arguments too Sweeping the rest of WP_Query's query vars turned up more that work here and nothing writes down. Each was tried with a value shaped the way the command line delivers it, and only the ones that changed the result are documented: - meta_key and meta_value, which is how you filter on a custom field - hour, minute and second, which finish the date family that already had year, monthnum, day, m and w - has_password and post_password - orderby and order Left alone for cause: 'sentence', 'perm', 'preview', 'error', 'tb', 'paged', 'embed' and 'sticky' change nothing when passed, and 'page_id' filters but sits within two edits of 'paged' and 'tag_id', so documenting it would add two false positives to wp-cli/wp-cli#6392 the way 'tag_id' would. All three of hour, minute and second together is deliberately not asserted. That path compares a DATE_FORMAT() string rather than separate HOUR() and MINUTE() clauses, and the SQLite integration plugin does not emulate it the way MySQL does: the same query matches on MariaDB and matches nothing on SQLite. Two units at a time behaves the same on both and is covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 33 ++++++++++++++++++++- features/post.feature | 69 +++++++++++++++++++++++++++++++++++++++++++ src/Post_Command.php | 31 +++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a1862e9bf..ef446fa11 100644 --- a/README.md +++ b/README.md @@ -3150,7 +3150,7 @@ wp post get <id> [--field=<field>] [--fields=<fields>] [--format=<format>] Gets a list of posts. ~~~ -wp post list [--<field>=<value>] [--p=<id>|ID] [--title=<title>|post_title] [--name=<slug>|post_name] [--author=<author>|post_author] [--author_name=<author_name>] [--cat=<cat>] [--category_name=<category_name>] [--tag=<tag>] [--post_type=<post_type>] [--post_status=<post_status>] [--post_parent=<post_parent>] [--post_mime_type=<post_mime_type>] [--menu_order=<menu_order>] [--comment_status=<comment_status>] [--ping_status=<ping_status>] [--comment_count=<comment_count>] [--s=<string>] [--year=<year>] [--monthnum=<monthnum>] [--day=<day>] [--m=<yearmonth>] [--w=<week>] [--field=<field>] [--fields=<fields>] [--format=<format>] +wp post list [--<field>=<value>] [--p=<id>|ID] [--title=<title>|post_title] [--name=<slug>|post_name] [--author=<author>|post_author] [--author_name=<author_name>] [--cat=<cat>] [--category_name=<category_name>] [--tag=<tag>] [--post_type=<post_type>] [--post_status=<post_status>] [--post_parent=<post_parent>] [--post_mime_type=<post_mime_type>] [--menu_order=<menu_order>] [--comment_status=<comment_status>] [--ping_status=<ping_status>] [--comment_count=<comment_count>] [--s=<string>] [--year=<year>] [--monthnum=<monthnum>] [--day=<day>] [--hour=<hour>] [--minute=<minute>] [--second=<second>] [--m=<yearmonth>] [--w=<week>] [--meta_key=<meta_key>] [--meta_value=<meta_value>] [--has_password=<has_password>] [--post_password=<post_password>] [--orderby=<orderby>] [--order=<order>] [--field=<field>] [--fields=<fields>] [--format=<format>] ~~~ Display posts based on all arguments supported by [WP_Query()](https://developer.wordpress.org/reference/classes/wp_query/). @@ -3236,12 +3236,43 @@ Only shows post types marked as post by default. [--day=<day>] Filter by day of the month, 1 to 31. + [--hour=<hour>] + Filter by hour, 0 to 23. + + [--minute=<minute>] + Filter by minute, 0 to 59. + + [--second=<second>] + Filter by second, 0 to 59. + [--m=<yearmonth>] Filter by year and month together, e.g. 202401. [--w=<week>] Filter by week of the year, 0 to 53. + [--meta_key=<meta_key>] + Filter by posts having this meta key. Pair it with `--meta_value` to + filter on the value as well. + + [--meta_value=<meta_value>] + Filter by this meta value. Needs `--meta_key` to say which key it + belongs to. + + [--has_password=<has_password>] + Filter by whether the post has a password. Accepts 1 or 0. + + [--post_password=<post_password>] + Filter by the post's password, matched in full. + + [--orderby=<orderby>] + Order the results by this field. Accepts what WP_Query accepts, e.g. + 'date', 'title', 'ID', 'menu_order', 'rand', or 'meta_value' alongside + `--meta_key`. + + [--order=<order>] + Direction to order by. Accepts 'ASC' or 'DESC'. + [--field=<field>] Prints the value of a single field for each post. diff --git a/features/post.feature b/features/post.feature index 676e79dd5..0c8b40f53 100644 --- a/features/post.feature +++ b/features/post.feature @@ -723,3 +723,72 @@ Feature: Manage WordPress posts """ 1 """ + + Scenario: Filtering by meta, time of day, password and order + When I run `wp post create --post_title='Timed' --post_status=publish --post_date='2020-03-04 05:06:07' --porcelain` + Then STDOUT should be a number + And save STDOUT as {TIMED_ID} + + When I run `wp post meta add {TIMED_ID} color blue` + Then STDOUT should not be empty + + When I run `wp post create --post_title='Locked' --post_status=publish --post_password=secret --porcelain` + Then STDOUT should be a number + + # Time of day, alongside the year/month/day filters already documented. + # Two units at once exercises the combined path; all three of hour, minute + # and second together is left out on purpose, because that path compares a + # DATE_FORMAT() string and the SQLite integration plugin does not emulate + # it the way MySQL does, so it matches nothing there. + When I run `wp post list --hour=5 --minute=6 --field=post_title` + Then STDOUT should be: + """ + Timed + """ + + When I run `wp post list --second=7 --field=post_title` + Then STDOUT should be: + """ + Timed + """ + + # A meta key on its own, then narrowed by its value. + When I run `wp post list --meta_key=color --field=post_title` + Then STDOUT should be: + """ + Timed + """ + + When I run `wp post list --meta_key=color --meta_value=red --format=count` + Then STDOUT should be: + """ + 0 + """ + + When I run `wp post list --has_password=1 --field=post_title` + Then STDOUT should be: + """ + Locked + """ + + When I run `wp post list --post_password=secret --field=post_title` + Then STDOUT should be: + """ + Locked + """ + + When I run `wp post list --orderby=title --order=ASC --field=post_title` + Then STDOUT should be: + """ + Hello world! + Locked + Timed + """ + + When I run `wp post list --orderby=title --order=DESC --field=post_title` + Then STDOUT should be: + """ + Timed + Locked + Hello world! + """ diff --git a/src/Post_Command.php b/src/Post_Command.php index 89cf61633..b225b7b5a 100644 --- a/src/Post_Command.php +++ b/src/Post_Command.php @@ -694,12 +694,43 @@ protected function delete_callback( $post_id, $assoc_args ) { * [--day=<day>] * : Filter by day of the month, 1 to 31. * + * [--hour=<hour>] + * : Filter by hour, 0 to 23. + * + * [--minute=<minute>] + * : Filter by minute, 0 to 59. + * + * [--second=<second>] + * : Filter by second, 0 to 59. + * * [--m=<yearmonth>] * : Filter by year and month together, e.g. 202401. * * [--w=<week>] * : Filter by week of the year, 0 to 53. * + * [--meta_key=<meta_key>] + * : Filter by posts having this meta key. Pair it with `--meta_value` to + * filter on the value as well. + * + * [--meta_value=<meta_value>] + * : Filter by this meta value. Needs `--meta_key` to say which key it + * belongs to. + * + * [--has_password=<has_password>] + * : Filter by whether the post has a password. Accepts 1 or 0. + * + * [--post_password=<post_password>] + * : Filter by the post's password, matched in full. + * + * [--orderby=<orderby>] + * : Order the results by this field. Accepts what WP_Query accepts, e.g. + * 'date', 'title', 'ID', 'menu_order', 'rand', or 'meta_value' alongside + * `--meta_key`. + * + * [--order=<order>] + * : Direction to order by. Accepts 'ASC' or 'DESC'. + * * [--field=<field>] * : Prints the value of a single field for each post. * From 3f93cad7d20ebfa28b715607d4eddef8dc62cc14 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 18 Aug 2026 11:40:56 +0000 Subject: [PATCH 3/5] Take the argument list from WP_Query::parse_query Guessing at which arguments are worth writing down produced two that should not have been: 'has_password' and 'post_password' filter, but neither is a WP_Query argument. They are not in the docblock on WP_Query::parse_query, which is the list WP_Query actually documents, and they are gone again. Working from that docblock instead turned up the reverse problem, a lot of arguments left out for no reason. The list ones - post__in, author__in, category__in, tag__in and their relatives - were skipped on the belief that a comma-separated value would reach WP_Query as a string and quietly match its first entry alone. That was wrong: process_csv_arguments_to_arrays() already splits every argument whose name contains '__'. They work: wp post list --post__in=4,5 --format=count -> 2 wp post list --post_name__in=one,two -> 2 posts date_query, meta_query and tax_query were likewise already handled - list_() names all three for JSON decoding - and likewise undocumented. Sixty-three arguments are documented now, every one of them in the parse_query docblock. What is left out of that docblock is either internal bookkeeping, where a CLI user has no reason to reach ('cache_results', 'no_found_rows', 'suppress_filters', the cache-priming flags), or means something other than it appears to for a list ('page' paginates within a single post, 'comments_per_page' and 'posts_per_archive_page' are not about this query). Documenting a family by halves turned out to be worse than documenting all of it, measured against the near-match check in wp-cli/wp-cli#6392: documented set false positives before this commit 2 + the list arguments only 3 'tag_id' now looks like 'tag__in' + those and the id arguments 3 'paged' now looks like 'page_id' + those and pagination 2 everything here 2 Each partial step stranded a name whose neighbours had just been documented. The two that survive are 'feed' and 'tb', which are not filters at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 108 +++++++++++++++++++++++++++++++++++++++--- features/post.feature | 86 ++++++++++++++++++++++++++++----- src/Post_Command.php | 106 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 276 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ef446fa11..bb1dfb2f7 100644 --- a/README.md +++ b/README.md @@ -3150,7 +3150,7 @@ wp post get <id> [--field=<field>] [--fields=<fields>] [--format=<format>] Gets a list of posts. ~~~ -wp post list [--<field>=<value>] [--p=<id>|ID] [--title=<title>|post_title] [--name=<slug>|post_name] [--author=<author>|post_author] [--author_name=<author_name>] [--cat=<cat>] [--category_name=<category_name>] [--tag=<tag>] [--post_type=<post_type>] [--post_status=<post_status>] [--post_parent=<post_parent>] [--post_mime_type=<post_mime_type>] [--menu_order=<menu_order>] [--comment_status=<comment_status>] [--ping_status=<ping_status>] [--comment_count=<comment_count>] [--s=<string>] [--year=<year>] [--monthnum=<monthnum>] [--day=<day>] [--hour=<hour>] [--minute=<minute>] [--second=<second>] [--m=<yearmonth>] [--w=<week>] [--meta_key=<meta_key>] [--meta_value=<meta_value>] [--has_password=<has_password>] [--post_password=<post_password>] [--orderby=<orderby>] [--order=<order>] [--field=<field>] [--fields=<fields>] [--format=<format>] +wp post list [--<field>=<value>] [--p=<id>|ID] [--title=<title>|post_title] [--name=<slug>|post_name] [--author=<author>|post_author] [--author_name=<author_name>] [--cat=<cat>] [--category_name=<category_name>] [--tag=<tag>] [--post_type=<post_type>] [--post_status=<post_status>] [--post_parent=<post_parent>] [--post_mime_type=<post_mime_type>] [--menu_order=<menu_order>] [--comment_status=<comment_status>] [--ping_status=<ping_status>] [--comment_count=<comment_count>] [--s=<string>] [--year=<year>] [--monthnum=<monthnum>] [--day=<day>] [--hour=<hour>] [--minute=<minute>] [--second=<second>] [--m=<yearmonth>] [--w=<week>] [--meta_key=<meta_key>] [--meta_value=<meta_value>] [--orderby=<orderby>] [--order=<order>] [--post__in=<ids>] [--post__not_in=<ids>] [--post_name__in=<slugs>] [--post_parent__in=<ids>] [--post_parent__not_in=<ids>] [--author__in=<ids>] [--author__not_in=<ids>] [--category__in=<ids>] [--category__and=<ids>] [--category__not_in=<ids>] [--tag_id=<tag_id>] [--tag__in=<ids>] [--tag__and=<ids>] [--tag__not_in=<ids>] [--tag_slug__in=<slugs>] [--tag_slug__and=<slugs>] [--page_id=<page_id>] [--pagename=<pagename>] [--attachment_id=<attachment_id>] [--date_query=<json>] [--meta_query=<json>] [--tax_query=<json>] [--meta_compare=<meta_compare>] [--sentence=<sentence>] [--exact=<exact>] [--search_columns=<columns>] [--perm=<perm>] [--posts_per_page=<number>] [--paged=<paged>] [--offset=<offset>] [--nopaging=<nopaging>] [--field=<field>] [--fields=<fields>] [--format=<format>] ~~~ Display posts based on all arguments supported by [WP_Query()](https://developer.wordpress.org/reference/classes/wp_query/). @@ -3259,12 +3259,6 @@ Only shows post types marked as post by default. Filter by this meta value. Needs `--meta_key` to say which key it belongs to. - [--has_password=<has_password>] - Filter by whether the post has a password. Accepts 1 or 0. - - [--post_password=<post_password>] - Filter by the post's password, matched in full. - [--orderby=<orderby>] Order the results by this field. Accepts what WP_Query accepts, e.g. 'date', 'title', 'ID', 'menu_order', 'rand', or 'meta_value' alongside @@ -3273,6 +3267,106 @@ Only shows post types marked as post by default. [--order=<order>] Direction to order by. Accepts 'ASC' or 'DESC'. + [--post__in=<ids>] + Only list the posts with these IDs (comma-separated). + + [--post__not_in=<ids>] + Exclude the posts with these IDs (comma-separated). + + [--post_name__in=<slugs>] + Only list the posts with these slugs (comma-separated). Unlike `--name` + this is not a single-post query, so it reaches drafts as well. + + [--post_parent__in=<ids>] + Only list the posts whose parent is one of these IDs (comma-separated). + + [--post_parent__not_in=<ids>] + Exclude the posts whose parent is one of these IDs (comma-separated). + + [--author__in=<ids>] + Only list the posts by these author IDs (comma-separated). + + [--author__not_in=<ids>] + Exclude the posts by these author IDs (comma-separated). + + [--category__in=<ids>] + Only list the posts in these category IDs (comma-separated). + + [--category__and=<ids>] + Only list the posts in all of these category IDs (comma-separated). + + [--category__not_in=<ids>] + Exclude the posts in these category IDs (comma-separated). + + [--tag_id=<tag_id>] + Filter by tag ID. + + [--tag__in=<ids>] + Only list the posts with these tag IDs (comma-separated). + + [--tag__and=<ids>] + Only list the posts with all of these tag IDs (comma-separated). + + [--tag__not_in=<ids>] + Exclude the posts with these tag IDs (comma-separated). + + [--tag_slug__in=<slugs>] + Only list the posts with these tag slugs (comma-separated). + + [--tag_slug__and=<slugs>] + Only list the posts with all of these tag slugs (comma-separated). + + [--page_id=<page_id>] + Filter by page ID. Needs `--post_type=page` to match anything. + + [--pagename=<pagename>] + Filter by page slug. Needs `--post_type=page` to match anything. + + [--attachment_id=<attachment_id>] + Filter by attachment ID. Needs `--post_type=attachment` to match + anything. + + [--date_query=<json>] + Filter by a date query, given as JSON. See WP_Date_Query. + + [--meta_query=<json>] + Filter by a meta query, given as JSON. See WP_Meta_Query. + + [--tax_query=<json>] + Filter by a taxonomy query, given as JSON. See WP_Tax_Query. + + [--meta_compare=<meta_compare>] + Operator to test `--meta_value` with, e.g. '=', '!=', '>' or 'LIKE'. + + [--sentence=<sentence>] + Match `--s` as one phrase rather than as separate words. Accepts 1 or 0. + + [--exact=<exact>] + Match `--s` against the whole column rather than part of it. Accepts 1 + or 0. + + [--search_columns=<columns>] + Comma-separated list of columns `--s` looks in. Accepts 'post_title', + 'post_excerpt' and 'post_content'. + + [--perm=<perm>] + Filter by what the current user may do with the post. Accepts + 'readable' or 'editable'; pair it with the global `--user` argument, + since WP-CLI is no user by default. + + [--posts_per_page=<number>] + How many posts to return. Defaults to -1, meaning every post. + + [--paged=<paged>] + Which page of results to return, counted in `--posts_per_page` steps. + + [--offset=<offset>] + How many posts to skip. Needs `--posts_per_page` set to something other + than -1, which otherwise takes precedence. + + [--nopaging=<nopaging>] + Return every post, ignoring `--posts_per_page`. Accepts 1 or 0. + [--field=<field>] Prints the value of a single field for each post. diff --git a/features/post.feature b/features/post.feature index 0c8b40f53..4dcecfaf3 100644 --- a/features/post.feature +++ b/features/post.feature @@ -724,7 +724,7 @@ Feature: Manage WordPress posts 1 """ - Scenario: Filtering by meta, time of day, password and order + Scenario: Filtering by meta, time of day and order When I run `wp post create --post_title='Timed' --post_status=publish --post_date='2020-03-04 05:06:07' --porcelain` Then STDOUT should be a number And save STDOUT as {TIMED_ID} @@ -765,30 +765,94 @@ Feature: Manage WordPress posts 0 """ - When I run `wp post list --has_password=1 --field=post_title` + When I run `wp post list --orderby=title --order=ASC --field=post_title` Then STDOUT should be: """ + Hello world! Locked + Timed """ - When I run `wp post list --post_password=secret --field=post_title` + When I run `wp post list --orderby=title --order=DESC --field=post_title` Then STDOUT should be: """ + Timed Locked + Hello world! """ - When I run `wp post list --orderby=title --order=ASC --field=post_title` + Scenario: Filtering by lists, JSON queries and search columns + When I run `wp post create --post_title='Zebra Title' --post_content='nothing' --post_status=publish --porcelain` + Then save STDOUT as {TITLED} + + When I run `wp post create --post_title='Plain' --post_name=plain --post_content='zebra in body' --post_status=publish --porcelain` + Then save STDOUT as {BODIED} + + When I run `wp post meta add {TITLED} rank 5` + Then STDOUT should not be empty + + # Arguments carrying '__' are split on commas before they reach WP_Query, + # which is what makes them usable from the command line at all. + When I run `wp post list --post__in={TITLED},{BODIED} --format=count` Then STDOUT should be: """ - Hello world! - Locked - Timed + 2 """ - When I run `wp post list --orderby=title --order=DESC --field=post_title` + When I run `wp post list --post__not_in={TITLED},{BODIED} --format=count` Then STDOUT should be: """ - Timed - Locked - Hello world! + 1 + """ + + # Unlike --name, this reaches a draft, because it is not a single-post query. + When I run `wp post list --post_name__in=plain --field=post_title` + Then STDOUT should be: + """ + Plain + """ + + # --s searches every column; --search_columns narrows it. + When I run `wp post list --s=zebra --format=count` + Then STDOUT should be: + """ + 2 + """ + + When I run `wp post list --s=zebra --search_columns=post_title --field=post_title` + Then STDOUT should be: + """ + Zebra Title + """ + + When I run `wp post list --s=zebra --search_columns=post_title,post_content --format=count` + Then STDOUT should be: + """ + 2 + """ + + # The nested queries are given as JSON, which this command decodes. + When I run `wp post list --meta_query='[{"key":"rank","value":"5"}]' --field=post_title` + Then STDOUT should be: + """ + Zebra Title + """ + + When I run `wp post list --meta_key=rank --meta_value=4 --meta_compare=">" --field=post_title` + Then STDOUT should be: + """ + Zebra Title + """ + + # --offset only means anything once --posts_per_page is bounded. + When I run `wp post list --posts_per_page=10 --offset=1 --format=count` + Then STDOUT should be: + """ + 2 + """ + + When I run `wp post list --posts_per_page=1 --nopaging=1 --format=count` + Then STDOUT should be: + """ + 3 """ diff --git a/src/Post_Command.php b/src/Post_Command.php index b225b7b5a..11cb05f7a 100644 --- a/src/Post_Command.php +++ b/src/Post_Command.php @@ -717,12 +717,6 @@ protected function delete_callback( $post_id, $assoc_args ) { * : Filter by this meta value. Needs `--meta_key` to say which key it * belongs to. * - * [--has_password=<has_password>] - * : Filter by whether the post has a password. Accepts 1 or 0. - * - * [--post_password=<post_password>] - * : Filter by the post's password, matched in full. - * * [--orderby=<orderby>] * : Order the results by this field. Accepts what WP_Query accepts, e.g. * 'date', 'title', 'ID', 'menu_order', 'rand', or 'meta_value' alongside @@ -731,6 +725,106 @@ protected function delete_callback( $post_id, $assoc_args ) { * [--order=<order>] * : Direction to order by. Accepts 'ASC' or 'DESC'. * + * [--post__in=<ids>] + * : Only list the posts with these IDs (comma-separated). + * + * [--post__not_in=<ids>] + * : Exclude the posts with these IDs (comma-separated). + * + * [--post_name__in=<slugs>] + * : Only list the posts with these slugs (comma-separated). Unlike `--name` + * this is not a single-post query, so it reaches drafts as well. + * + * [--post_parent__in=<ids>] + * : Only list the posts whose parent is one of these IDs (comma-separated). + * + * [--post_parent__not_in=<ids>] + * : Exclude the posts whose parent is one of these IDs (comma-separated). + * + * [--author__in=<ids>] + * : Only list the posts by these author IDs (comma-separated). + * + * [--author__not_in=<ids>] + * : Exclude the posts by these author IDs (comma-separated). + * + * [--category__in=<ids>] + * : Only list the posts in these category IDs (comma-separated). + * + * [--category__and=<ids>] + * : Only list the posts in all of these category IDs (comma-separated). + * + * [--category__not_in=<ids>] + * : Exclude the posts in these category IDs (comma-separated). + * + * [--tag_id=<tag_id>] + * : Filter by tag ID. + * + * [--tag__in=<ids>] + * : Only list the posts with these tag IDs (comma-separated). + * + * [--tag__and=<ids>] + * : Only list the posts with all of these tag IDs (comma-separated). + * + * [--tag__not_in=<ids>] + * : Exclude the posts with these tag IDs (comma-separated). + * + * [--tag_slug__in=<slugs>] + * : Only list the posts with these tag slugs (comma-separated). + * + * [--tag_slug__and=<slugs>] + * : Only list the posts with all of these tag slugs (comma-separated). + * + * [--page_id=<page_id>] + * : Filter by page ID. Needs `--post_type=page` to match anything. + * + * [--pagename=<pagename>] + * : Filter by page slug. Needs `--post_type=page` to match anything. + * + * [--attachment_id=<attachment_id>] + * : Filter by attachment ID. Needs `--post_type=attachment` to match + * anything. + * + * [--date_query=<json>] + * : Filter by a date query, given as JSON. See WP_Date_Query. + * + * [--meta_query=<json>] + * : Filter by a meta query, given as JSON. See WP_Meta_Query. + * + * [--tax_query=<json>] + * : Filter by a taxonomy query, given as JSON. See WP_Tax_Query. + * + * [--meta_compare=<meta_compare>] + * : Operator to test `--meta_value` with, e.g. '=', '!=', '>' or 'LIKE'. + * + * [--sentence=<sentence>] + * : Match `--s` as one phrase rather than as separate words. Accepts 1 or 0. + * + * [--exact=<exact>] + * : Match `--s` against the whole column rather than part of it. Accepts 1 + * or 0. + * + * [--search_columns=<columns>] + * : Comma-separated list of columns `--s` looks in. Accepts 'post_title', + * 'post_excerpt' and 'post_content'. + * + * [--perm=<perm>] + * : Filter by what the current user may do with the post. Accepts + * 'readable' or 'editable'; pair it with the global `--user` argument, + * since WP-CLI is no user by default. + * + * [--posts_per_page=<number>] + * : How many posts to return. Defaults to -1, meaning every post. + * + * [--paged=<paged>] + * : Which page of results to return, counted in `--posts_per_page` steps. + * + * [--offset=<offset>] + * : How many posts to skip. Needs `--posts_per_page` set to something other + * than -1, which otherwise takes precedence. + * + * [--nopaging=<nopaging>] + * : Return every post, ignoring `--posts_per_page`. Accepts 1 or 0. + * * [--field=<field>] * : Prints the value of a single field for each post. * From b4d5562fdf5b42a61f389055343816ef5c31c3a6 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 18 Aug 2026 12:13:59 +0000 Subject: [PATCH 4/5] Say which WordPress version each argument needs The parse_query docblock records when arguments arrived, and four of the ones documented here arrived after the oldest WordPress this command is tested against: title, post_name__in 4.4.0 comment_count 4.9.0 search_columns 6.2.0 Only 'search_columns' lands past the floor, and its scenario was failing on 4.9 for that reason: the argument is not recognised there, so '--s' searches every column and the narrowing the scenario asserts does not happen. It is its own scenario now, tagged @require-wp-6.2, rather than sitting inside a scenario that has to run everywhere. The two 4.4 arguments get @require-wp-4.4 on the scenarios that exercise them, which changes nothing on a current install but matches how the rest of this suite guards itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 10 +++++---- features/post.feature | 50 ++++++++++++++++++++++++++++++++----------- src/Post_Command.php | 10 +++++---- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index bb1dfb2f7..c4dc72301 100644 --- a/README.md +++ b/README.md @@ -3169,7 +3169,8 @@ Only shows post types marked as post by default. [--title=<title>|post_title] Filter by post title, matched in full. `--post_title` is the name of the - column this filters and is accepted as an alias. + column this filters and is accepted as an alias. Needs WordPress 4.4 or + later. [--name=<slug>|post_name] Filter by post slug. `--post_name` is the name of the column this filters @@ -3222,7 +3223,7 @@ Only shows post types marked as post by default. Filter by ping status. Accepts 'open' or 'closed'. [--comment_count=<comment_count>] - Filter by number of comments. + Filter by number of comments. Needs WordPress 4.9 or later. [--s=<string>] Only list the posts matching this search term. @@ -3275,7 +3276,8 @@ Only shows post types marked as post by default. [--post_name__in=<slugs>] Only list the posts with these slugs (comma-separated). Unlike `--name` - this is not a single-post query, so it reaches drafts as well. + this is not a single-post query, so it reaches drafts as well. Needs + WordPress 4.4 or later. [--post_parent__in=<ids>] Only list the posts whose parent is one of these IDs (comma-separated). @@ -3347,7 +3349,7 @@ Only shows post types marked as post by default. [--search_columns=<columns>] Comma-separated list of columns `--s` looks in. Accepts 'post_title', - 'post_excerpt' and 'post_content'. + 'post_excerpt' and 'post_content'. Needs WordPress 6.2 or later. [--perm=<perm>] Filter by what the current user may do with the post. Accepts diff --git a/features/post.feature b/features/post.feature index 4dcecfaf3..38c7ee7c4 100644 --- a/features/post.feature +++ b/features/post.feature @@ -592,6 +592,7 @@ Feature: Manage WordPress posts {"block_version":1} """ + @require-wp-4.4 Scenario: Filtering by the wp_posts column names When I run `wp post create --post_title='Alpha' --post_status=publish --porcelain` Then STDOUT should be a number @@ -781,7 +782,8 @@ Feature: Manage WordPress posts Hello world! """ - Scenario: Filtering by lists, JSON queries and search columns + @require-wp-4.4 + Scenario: Filtering by lists and JSON queries When I run `wp post create --post_title='Zebra Title' --post_content='nothing' --post_status=publish --porcelain` Then save STDOUT as {TITLED} @@ -812,47 +814,69 @@ Feature: Manage WordPress posts Plain """ - # --s searches every column; --search_columns narrows it. When I run `wp post list --s=zebra --format=count` Then STDOUT should be: """ 2 """ - When I run `wp post list --s=zebra --search_columns=post_title --field=post_title` + # The nested queries are given as JSON, which this command decodes. + When I run `wp post list --meta_query='[{"key":"rank","value":"5"}]' --field=post_title` Then STDOUT should be: """ Zebra Title """ - When I run `wp post list --s=zebra --search_columns=post_title,post_content --format=count` + When I run `wp post list --meta_key=rank --meta_value=4 --meta_compare=">" --field=post_title` + Then STDOUT should be: + """ + Zebra Title + """ + + # --offset only means anything once --posts_per_page is bounded. + When I run `wp post list --posts_per_page=10 --offset=1 --format=count` Then STDOUT should be: """ 2 """ - # The nested queries are given as JSON, which this command decodes. - When I run `wp post list --meta_query='[{"key":"rank","value":"5"}]' --field=post_title` + When I run `wp post list --posts_per_page=1 --nopaging=1 --format=count` Then STDOUT should be: """ - Zebra Title + 3 """ - When I run `wp post list --meta_key=rank --meta_value=4 --meta_compare=">" --field=post_title` + # 'search_columns' is a WP_Query argument as of WordPress 6.2. Before that it + # is not recognised, so '--s' searches every column and the narrowing here + # would not happen. + @require-wp-6.2 + Scenario: Narrowing a search to particular columns + When I run `wp post create --post_title='Zebra Title' --post_content='nothing' --post_status=publish --porcelain` + Then STDOUT should be a number + + When I run `wp post create --post_title='Plain' --post_content='zebra in body' --post_status=publish --porcelain` + Then STDOUT should be a number + + When I run `wp post list --s=zebra --format=count` + Then STDOUT should be: + """ + 2 + """ + + When I run `wp post list --s=zebra --search_columns=post_title --field=post_title` Then STDOUT should be: """ Zebra Title """ - # --offset only means anything once --posts_per_page is bounded. - When I run `wp post list --posts_per_page=10 --offset=1 --format=count` + When I run `wp post list --s=zebra --search_columns=post_content --field=post_title` Then STDOUT should be: """ - 2 + Plain """ - When I run `wp post list --posts_per_page=1 --nopaging=1 --format=count` + When I run `wp post list --s=zebra --search_columns=post_title,post_content --format=count` Then STDOUT should be: """ - 3 + 2 """ diff --git a/src/Post_Command.php b/src/Post_Command.php index 11cb05f7a..c349ae445 100644 --- a/src/Post_Command.php +++ b/src/Post_Command.php @@ -627,7 +627,8 @@ protected function delete_callback( $post_id, $assoc_args ) { * * [--title=<title>|post_title] * : Filter by post title, matched in full. `--post_title` is the name of the - * column this filters and is accepted as an alias. + * column this filters and is accepted as an alias. Needs WordPress 4.4 or + * later. * * [--name=<slug>|post_name] * : Filter by post slug. `--post_name` is the name of the column this filters @@ -680,7 +681,7 @@ protected function delete_callback( $post_id, $assoc_args ) { * : Filter by ping status. Accepts 'open' or 'closed'. * * [--comment_count=<comment_count>] - * : Filter by number of comments. + * : Filter by number of comments. Needs WordPress 4.9 or later. * * [--s=<string>] * : Only list the posts matching this search term. @@ -733,7 +734,8 @@ protected function delete_callback( $post_id, $assoc_args ) { * * [--post_name__in=<slugs>] * : Only list the posts with these slugs (comma-separated). Unlike `--name` - * this is not a single-post query, so it reaches drafts as well. + * this is not a single-post query, so it reaches drafts as well. Needs + * WordPress 4.4 or later. * * [--post_parent__in=<ids>] * : Only list the posts whose parent is one of these IDs (comma-separated). @@ -805,7 +807,7 @@ protected function delete_callback( $post_id, $assoc_args ) { * * [--search_columns=<columns>] * : Comma-separated list of columns `--s` looks in. Accepts 'post_title', - * 'post_excerpt' and 'post_content'. + * 'post_excerpt' and 'post_content'. Needs WordPress 6.2 or later. * * [--perm=<perm>] * : Filter by what the current user may do with the post. Accepts From ea04b0b1e14ea715184e60131e51b0e07e1a0dd8 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 18 Aug 2026 12:26:41 +0000 Subject: [PATCH 5/5] Note only the version that is actually out of reach WordPress 4.9 is the floor here, so saying that 'title' and 'post_name__in' need 4.4 tells a reader nothing they can act on, and 'comment_count' arrived in 4.9 itself. Those three notes are gone, along with the @require-wp-4.4 tags that came with them - the one already on the meta keys scenario is older than this branch and stays. 'search_columns' is the only one past the floor, so it keeps its note and its @require-wp-6.2 scenario. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 8 +++----- features/post.feature | 2 -- src/Post_Command.php | 8 +++----- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c4dc72301..17168698d 100644 --- a/README.md +++ b/README.md @@ -3169,8 +3169,7 @@ Only shows post types marked as post by default. [--title=<title>|post_title] Filter by post title, matched in full. `--post_title` is the name of the - column this filters and is accepted as an alias. Needs WordPress 4.4 or - later. + column this filters and is accepted as an alias. [--name=<slug>|post_name] Filter by post slug. `--post_name` is the name of the column this filters @@ -3223,7 +3222,7 @@ Only shows post types marked as post by default. Filter by ping status. Accepts 'open' or 'closed'. [--comment_count=<comment_count>] - Filter by number of comments. Needs WordPress 4.9 or later. + Filter by number of comments. [--s=<string>] Only list the posts matching this search term. @@ -3276,8 +3275,7 @@ Only shows post types marked as post by default. [--post_name__in=<slugs>] Only list the posts with these slugs (comma-separated). Unlike `--name` - this is not a single-post query, so it reaches drafts as well. Needs - WordPress 4.4 or later. + this is not a single-post query, so it reaches drafts as well. [--post_parent__in=<ids>] Only list the posts whose parent is one of these IDs (comma-separated). diff --git a/features/post.feature b/features/post.feature index 38c7ee7c4..1cf5f9014 100644 --- a/features/post.feature +++ b/features/post.feature @@ -592,7 +592,6 @@ Feature: Manage WordPress posts {"block_version":1} """ - @require-wp-4.4 Scenario: Filtering by the wp_posts column names When I run `wp post create --post_title='Alpha' --post_status=publish --porcelain` Then STDOUT should be a number @@ -782,7 +781,6 @@ Feature: Manage WordPress posts Hello world! """ - @require-wp-4.4 Scenario: Filtering by lists and JSON queries When I run `wp post create --post_title='Zebra Title' --post_content='nothing' --post_status=publish --porcelain` Then save STDOUT as {TITLED} diff --git a/src/Post_Command.php b/src/Post_Command.php index c349ae445..1880f24b5 100644 --- a/src/Post_Command.php +++ b/src/Post_Command.php @@ -627,8 +627,7 @@ protected function delete_callback( $post_id, $assoc_args ) { * * [--title=<title>|post_title] * : Filter by post title, matched in full. `--post_title` is the name of the - * column this filters and is accepted as an alias. Needs WordPress 4.4 or - * later. + * column this filters and is accepted as an alias. * * [--name=<slug>|post_name] * : Filter by post slug. `--post_name` is the name of the column this filters @@ -681,7 +680,7 @@ protected function delete_callback( $post_id, $assoc_args ) { * : Filter by ping status. Accepts 'open' or 'closed'. * * [--comment_count=<comment_count>] - * : Filter by number of comments. Needs WordPress 4.9 or later. + * : Filter by number of comments. * * [--s=<string>] * : Only list the posts matching this search term. @@ -734,8 +733,7 @@ protected function delete_callback( $post_id, $assoc_args ) { * * [--post_name__in=<slugs>] * : Only list the posts with these slugs (comma-separated). Unlike `--name` - * this is not a single-post query, so it reaches drafts as well. Needs - * WordPress 4.4 or later. + * this is not a single-post query, so it reaches drafts as well. * * [--post_parent__in=<ids>] * : Only list the posts whose parent is one of these IDs (comma-separated).