From a56c5b2176165c1d8fa80b8dd8f6b9f0a7380d1a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 12:59:18 +0000 Subject: [PATCH 1/4] Document every argument `wp site list` accepts #639 handed every unrecognised argument to WP_Site_Query, which left the command accepting a good deal more than it described. Writing that set down turned up five arguments that were reachable but did not work: - 'domain__in', 'domain__not_in', 'path__in' and 'path__not_in' are read through is_array(), so a comma-separated string was skipped without a word and every site came back. - 'search_columns' reaches array_intersect(), which is fatal on a string. Splitting them into arrays before the query runs is what makes them mean anything from the command line, so they are documented alongside 'site__not_in', the network and language list filters, 'search', the meta_* filters, paging, ordering and the cache flags. WP_Site_Query's 'ID' is the same filter as this command's '--blog_id', so it is declared as an alias rather than a second entry saying the same thing. 'meta_query' and 'date_query' go the other way and are withheld with 'count': they are nested arrays with no command-line spelling, and '--registered' and '--last_updated' already cover the dates that can be expressed. Every WP_Site_Query argument is now either documented or deliberately withheld, so nothing the command accepts is left undescribed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 95 ++++++++++++++++++++++++++++++++++--- features/site.feature | 97 +++++++++++++++++++++++++++++++++++++ src/Site_Command.php | 108 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 286 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ebbc01fb4..b94433867 100644 --- a/README.md +++ b/README.md @@ -5443,7 +5443,7 @@ These fields are optionally available: Lists all sites in a multisite installation. ~~~ -wp site list [--network=] [--=] [--site__in=] [--site_user=] [--site-path=] [--blog_id=] [--site_id=] [--domain=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--field=] [--fields=] [--format=] +wp site list [--network=] [--=] [--site__in=] [--site__not_in=] [--site_user=] [--site-path=] [--path__in=] [--path__not_in=] [--blog_id=|ID] [--site_id=] [--network_id=] [--network__in=] [--network__not_in=] [--domain=] [--domain__in=] [--domain__not_in=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--lang__in=] [--lang__not_in=] [--search=] [--search_columns=] [--meta_key=] [--meta_value=] [--meta_compare=] [--meta_type=] [--number=] [--offset=] [--no_found_rows=] [--update_site_cache=] [--update_site_meta_cache=] [--orderby=] [--order=] [--field=] [--fields=] [--format=] ~~~ **OPTIONS** @@ -5453,8 +5453,7 @@ wp site list [--network=] [--=] [--site__in=] [--site_u [--=] Filter by one or more fields (see "Available Fields" section), or pass any - other argument accepted by WP_Site_Query, such as 'search', 'site__not_in', - 'number', 'offset', 'orderby' or 'order'. However, 'url' isn't an available + other argument accepted by WP_Site_Query. However, 'url' isn't an available filter, as it comes from 'home' in wp_options. Note: '--path' conflicts with the global parameter of the same name; use '--site-path' to filter by path instead. @@ -5462,22 +5461,48 @@ wp site list [--network=] [--=] [--site__in=] [--site_u [--site__in=] Only list the sites with these blog_id values (comma-separated). + [--site__not_in=] + Exclude the sites with these blog_id values (comma-separated). + [--site_user=] Only list the sites with this user. [--site-path=] Filter by path. Avoids conflict with the global `--path` parameter. - [--blog_id=] - Filter by site ID. + [--path__in=] + Only list the sites with these paths (comma-separated). + + [--path__not_in=] + Exclude the sites with these paths (comma-separated). + + [--blog_id=|ID] + Filter by site ID. `--ID` is WP_Site_Query's name for the same filter + and is accepted as an alias. [--site_id=] - Filter by the ID of the network the site belongs to. `--network` is an - alias for this, and takes precedence when both are given. + Filter by the ID of the network the site belongs to. `--network` and + `--network_id` are aliases for this; `--network` takes precedence when + more than one is given. + + [--network_id=] + Filter by the ID of the network the site belongs to. + + [--network__in=] + Only list the sites belonging to these network IDs (comma-separated). + + [--network__not_in=] + Exclude the sites belonging to these network IDs (comma-separated). [--domain=] Filter by domain. + [--domain__in=] + Only list the sites with these domains (comma-separated). + + [--domain__not_in=] + Exclude the sites with these domains (comma-separated). + [--registered=] Filter by the date the site was registered. Accepts a timestamp or a date; a value carrying no time of day matches that whole day. @@ -5504,6 +5529,62 @@ wp site list [--network=] [--=] [--site__in=] [--site_u [--lang_id=] Filter by language ID. + [--lang__in=] + Only list the sites with these language IDs (comma-separated). + + [--lang__not_in=] + Exclude the sites with these language IDs (comma-separated). + + [--search=] + Only list the sites matching this search term. Searches the domain and + path columns unless `--search_columns` narrows it. + + [--search_columns=] + Comma-separated list of columns `--search` looks in. Accepts 'domain' + and 'path'. + + [--meta_key=] + Filter by this site meta key. + + [--meta_value=] + Filter by this site meta value. Used together with `--meta_key`. + + [--meta_compare=] + Operator to test the meta value against. Accepts the operators + WP_Meta_Query supports, such as '=', '!=', 'LIKE' or 'IN'. + + [--meta_type=] + Cast the meta value to this type, such as 'NUMERIC' or 'DATE'. + + [--number=] + Limit the number of sites returned. + + [--offset=] + Number of sites to skip. Used together with `--number`. + + [--no_found_rows=] + Whether to skip counting the total rows the query matches. Accepts 1 or 0. + + [--update_site_cache=] + Whether to prime the object cache with the sites found. Accepts 1 or 0. + Turning it off saves work when listing a large network once. + + [--update_site_meta_cache=] + Whether to prime the object cache with the site meta of the sites found. + Accepts 1 or 0. + + [--orderby=] + Order the results by this field, such as 'blog_id', 'domain', 'path', + 'registered', 'last_updated' or 'site__in'. + + [--order=] + Whether to order the results ascending or descending. + --- + options: + - asc + - desc + --- + [--field=] Prints the value of a single field for each site. diff --git a/features/site.feature b/features/site.feature index bebd0e0c2..ead4caa43 100644 --- a/features/site.feature +++ b/features/site.feature @@ -1087,6 +1087,103 @@ Feature: Manage sites in a multisite installation 1 """ + Scenario: List sites using the WP_Site_Query arguments that take a list + Given a WP multisite install + + When I run `wp site create --slug=alpha --porcelain` + Then STDOUT should be a number + And save STDOUT as {ALPHA_ID} + + When I run `wp site create --slug=beta --porcelain` + Then STDOUT should be a number + And save STDOUT as {BETA_ID} + + # WP_Site_Query reads these through is_array(), so before they were split + # they were skipped without a word and every site came back. + When I run `wp site list --path__in=/alpha/,/beta/ --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + {BETA_ID} + """ + + When I run `wp site list --path__not_in=/alpha/,/beta/ --field=blog_id` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp site list --domain__in=example.com --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --domain__not_in=example.com --format=count` + Then STDOUT should be: + """ + 0 + """ + + # --search_columns reaches array_intersect(), which is fatal on a string. + When I run `wp site list --search=alpha --search_columns=path --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --search=alpha --search_columns=domain --format=count` + Then STDOUT should be: + """ + 0 + """ + + # The remaining newly documented filters. + When I run `wp site list --network__in=1 --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --network__not_in=1 --format=count` + Then STDOUT should be: + """ + 0 + """ + + When I run `wp site list --lang__in=0 --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --ID={ALPHA_ID} --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --network_id=1 --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site meta add {ALPHA_ID} colour blue` + Then STDOUT should not be empty + + When I run `wp site list --meta_key=colour --meta_value=blue --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --meta_key=colour --meta_value=red --format=count` + Then STDOUT should be: + """ + 0 + """ + Scenario: Existing site list filters keep working against WP_Site_Query Given a WP multisite install diff --git a/src/Site_Command.php b/src/Site_Command.php index 2c9834bb9..02cee2bea 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -987,8 +987,7 @@ private function get_network( $network_id ) { * * [--=] * : Filter by one or more fields (see "Available Fields" section), or pass any - * other argument accepted by WP_Site_Query, such as 'search', 'site__not_in', - * 'number', 'offset', 'orderby' or 'order'. However, 'url' isn't an available + * other argument accepted by WP_Site_Query. However, 'url' isn't an available * filter, as it comes from 'home' in wp_options. * Note: '--path' conflicts with the global parameter of the same name; use * '--site-path' to filter by path instead. @@ -996,22 +995,48 @@ private function get_network( $network_id ) { * [--site__in=] * : Only list the sites with these blog_id values (comma-separated). * + * [--site__not_in=] + * : Exclude the sites with these blog_id values (comma-separated). + * * [--site_user=] * : Only list the sites with this user. * * [--site-path=] * : Filter by path. Avoids conflict with the global `--path` parameter. * - * [--blog_id=] - * : Filter by site ID. + * [--path__in=] + * : Only list the sites with these paths (comma-separated). + * + * [--path__not_in=] + * : Exclude the sites with these paths (comma-separated). + * + * [--blog_id=|ID] + * : Filter by site ID. `--ID` is WP_Site_Query's name for the same filter + * and is accepted as an alias. * * [--site_id=] - * : Filter by the ID of the network the site belongs to. `--network` is an - * alias for this, and takes precedence when both are given. + * : Filter by the ID of the network the site belongs to. `--network` and + * `--network_id` are aliases for this; `--network` takes precedence when + * more than one is given. + * + * [--network_id=] + * : Filter by the ID of the network the site belongs to. + * + * [--network__in=] + * : Only list the sites belonging to these network IDs (comma-separated). + * + * [--network__not_in=] + * : Exclude the sites belonging to these network IDs (comma-separated). * * [--domain=] * : Filter by domain. * + * [--domain__in=] + * : Only list the sites with these domains (comma-separated). + * + * [--domain__not_in=] + * : Exclude the sites with these domains (comma-separated). + * * [--registered=] * : Filter by the date the site was registered. Accepts a timestamp or a * date; a value carrying no time of day matches that whole day. @@ -1038,6 +1063,62 @@ private function get_network( $network_id ) { * [--lang_id=] * : Filter by language ID. * + * [--lang__in=] + * : Only list the sites with these language IDs (comma-separated). + * + * [--lang__not_in=] + * : Exclude the sites with these language IDs (comma-separated). + * + * [--search=] + * : Only list the sites matching this search term. Searches the domain and + * path columns unless `--search_columns` narrows it. + * + * [--search_columns=] + * : Comma-separated list of columns `--search` looks in. Accepts 'domain' + * and 'path'. + * + * [--meta_key=] + * : Filter by this site meta key. + * + * [--meta_value=] + * : Filter by this site meta value. Used together with `--meta_key`. + * + * [--meta_compare=] + * : Operator to test the meta value against. Accepts the operators + * WP_Meta_Query supports, such as '=', '!=', 'LIKE' or 'IN'. + * + * [--meta_type=] + * : Cast the meta value to this type, such as 'NUMERIC' or 'DATE'. + * + * [--number=] + * : Limit the number of sites returned. + * + * [--offset=] + * : Number of sites to skip. Used together with `--number`. + * + * [--no_found_rows=] + * : Whether to skip counting the total rows the query matches. Accepts 1 or 0. + * + * [--update_site_cache=] + * : Whether to prime the object cache with the sites found. Accepts 1 or 0. + * Turning it off saves work when listing a large network once. + * + * [--update_site_meta_cache=] + * : Whether to prime the object cache with the site meta of the sites found. + * Accepts 1 or 0. + * + * [--orderby=] + * : Order the results by this field, such as 'blog_id', 'domain', 'path', + * 'registered', 'last_updated' or 'site__in'. + * + * [--order=] + * : Whether to order the results ascending or descending. + * --- + * options: + * - asc + * - desc + * --- + * * [--field=] * : Prints the value of a single field for each site. * @@ -1117,10 +1198,13 @@ public function list_( $args, $assoc_args ) { // // 'count' is withheld deliberately: it makes get_sites() return an integer // rather than a list, and '--format=count' is how this command spells it. + // 'meta_query' and 'date_query' are withheld because they are nested arrays + // with no command-line spelling; the meta_* and date arguments above cover + // what can be expressed here. $query_args = array_diff_key( $assoc_args, array_flip( - [ 'format', 'fields', 'field', 'count', 'blog_id', 'site_id', 'site_user', 'site-path', 'network', 'registered', 'last_updated' ] + [ 'format', 'fields', 'field', 'count', 'meta_query', 'date_query', 'blog_id', 'site_id', 'site_user', 'site-path', 'network', 'registered', 'last_updated' ] ) ); @@ -1133,6 +1217,16 @@ public function list_( $args, $assoc_args ) { $query_args['site__in'] = array_map( 'trim', explode( ',', $assoc_args['site__in'] ) ); } + // WP_Site_Query reads these through is_array() or array_intersect(), so a + // comma-separated string reaches them as a value they cannot use: the domain + // and path clauses are skipped without a word, and 'search_columns' is fatal. + // Splitting them here is what makes them mean anything from the command line. + foreach ( [ 'domain__in', 'domain__not_in', 'path__in', 'path__not_in', 'search_columns' ] as $list_arg ) { + if ( isset( $assoc_args[ $list_arg ] ) && ! is_array( $assoc_args[ $list_arg ] ) ) { + $query_args[ $list_arg ] = array_map( 'trim', explode( ',', (string) $assoc_args[ $list_arg ] ) ); + } + } + if ( isset( $assoc_args['site_id'] ) ) { $query_args['network_id'] = $assoc_args['site_id']; } From 7b1a3deaea77834a8db6c8d8f98e9b45d5434f35 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 13:13:43 +0000 Subject: [PATCH 2/4] Accept `--meta_query` and `--date_query` as JSON These two were withheld because they are nested arrays and a flat string cannot describe one. `Utils\parse_shell_arrays()` is how this package already takes such arguments - `wp comment create --comment_meta` and `wp user update --meta_input` both use it - so they can be given the same way: wp site list --meta_query='[{"key":"colour","value":"blue"}]' parse_shell_arrays() leaves a value that is not JSON alone, which would put a string where WP_Site_Query expects an array and have it ignored without a word, so that case is an error instead. The decoded value goes into the query arguments rather than back into $assoc_args, which the rest of the method reads as strings. A '--date_query' given directly is kept when '--registered' or '--last_updated' are given as well, so those narrow it the way every other filter here narrows the result rather than quietly winning. That leaves 'count' as the only argument still withheld, and it has to be: it makes get_sites() return an integer rather than a list. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 11 ++++++++- features/site.feature | 57 +++++++++++++++++++++++++++++++++++++++++++ src/Site_Command.php | 42 +++++++++++++++++++++++++++---- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b94433867..0759f296e 100644 --- a/README.md +++ b/README.md @@ -5443,7 +5443,7 @@ These fields are optionally available: Lists all sites in a multisite installation. ~~~ -wp site list [--network=] [--=] [--site__in=] [--site__not_in=] [--site_user=] [--site-path=] [--path__in=] [--path__not_in=] [--blog_id=|ID] [--site_id=] [--network_id=] [--network__in=] [--network__not_in=] [--domain=] [--domain__in=] [--domain__not_in=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--lang__in=] [--lang__not_in=] [--search=] [--search_columns=] [--meta_key=] [--meta_value=] [--meta_compare=] [--meta_type=] [--number=] [--offset=] [--no_found_rows=] [--update_site_cache=] [--update_site_meta_cache=] [--orderby=] [--order=] [--field=] [--fields=] [--format=] +wp site list [--network=] [--=] [--site__in=] [--site__not_in=] [--site_user=] [--site-path=] [--path__in=] [--path__not_in=] [--blog_id=|ID] [--site_id=] [--network_id=] [--network__in=] [--network__not_in=] [--domain=] [--domain__in=] [--domain__not_in=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--lang__in=] [--lang__not_in=] [--search=] [--search_columns=] [--meta_key=] [--meta_value=] [--meta_compare=] [--meta_type=] [--meta_query=] [--date_query=] [--number=] [--offset=] [--no_found_rows=] [--update_site_cache=] [--update_site_meta_cache=] [--orderby=] [--order=] [--field=] [--fields=] [--format=] ~~~ **OPTIONS** @@ -5556,6 +5556,15 @@ wp site list [--network=] [--=] [--site__in=] [--site__ [--meta_type=] Cast the meta value to this type, such as 'NUMERIC' or 'DATE'. + [--meta_query=] + A WP_Meta_Query clause list, as JSON, for conditions the meta_* arguments + above cannot express. + + [--date_query=] + A WP_Date_Query clause list, as JSON, for ranges `--registered` and + `--last_updated` cannot express. Giving those as well narrows this further + rather than replacing it. + [--number=] Limit the number of sites returned. diff --git a/features/site.feature b/features/site.feature index ead4caa43..00dfb470b 100644 --- a/features/site.feature +++ b/features/site.feature @@ -1184,6 +1184,63 @@ Feature: Manage sites in a multisite installation 0 """ + # --meta_query and --date_query are nested arrays, so they are given as JSON. + When I run `wp site list --meta_query='[{"key":"colour","value":"blue"}]' --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --meta_query='[{"key":"colour","compare":"NOT EXISTS"}]' --field=blog_id` + Then STDOUT should be: + """ + 1 + {BETA_ID} + """ + + When I run `wp site list --date_query='[{"column":"registered","after":"1999-01-01"}]' --format=count` + Then STDOUT should be: + """ + 3 + """ + + When I run `wp site list --date_query='[{"column":"registered","before":"1999-01-01"}]' --format=count` + Then STDOUT should be: + """ + 0 + """ + + # --registered narrows a --date_query of its own rather than replacing it. The + # date on its own has to match first, or the pair below would prove nothing. + When I run `wp site list --blog_id={ALPHA_ID} --field=registered` + Then save STDOUT as {ALPHA_REGISTERED} + + When I run `wp site list --blog_id={ALPHA_ID} --registered='{ALPHA_REGISTERED}' --format=count` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp site list --blog_id={ALPHA_ID} --date_query='[{"column":"registered","before":"1999-01-01"}]' --registered='{ALPHA_REGISTERED}' --format=count` + Then STDOUT should be: + """ + 0 + """ + + When I try `wp site list --meta_query=notjson` + Then STDERR should contain: + """ + Invalid JSON passed to --meta_query. + """ + And the return code should be 1 + + When I try `wp site list --date_query=notjson` + Then STDERR should contain: + """ + Invalid JSON passed to --date_query. + """ + And the return code should be 1 + Scenario: Existing site list filters keep working against WP_Site_Query Given a WP multisite install diff --git a/src/Site_Command.php b/src/Site_Command.php index 02cee2bea..1463c954d 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -1090,6 +1090,15 @@ private function get_network( $network_id ) { * [--meta_type=] * : Cast the meta value to this type, such as 'NUMERIC' or 'DATE'. * + * [--meta_query=] + * : A WP_Meta_Query clause list, as JSON, for conditions the meta_* arguments + * above cannot express. + * + * [--date_query=] + * : A WP_Date_Query clause list, as JSON, for ranges `--registered` and + * `--last_updated` cannot express. Giving those as well narrows this further + * rather than replacing it. + * * [--number=] * : Limit the number of sites returned. * @@ -1198,16 +1207,33 @@ public function list_( $args, $assoc_args ) { // // 'count' is withheld deliberately: it makes get_sites() return an integer // rather than a list, and '--format=count' is how this command spells it. - // 'meta_query' and 'date_query' are withheld because they are nested arrays - // with no command-line spelling; the meta_* and date arguments above cover - // what can be expressed here. $query_args = array_diff_key( $assoc_args, array_flip( - [ 'format', 'fields', 'field', 'count', 'meta_query', 'date_query', 'blog_id', 'site_id', 'site_user', 'site-path', 'network', 'registered', 'last_updated' ] + [ 'format', 'fields', 'field', 'count', 'blog_id', 'site_id', 'site_user', 'site-path', 'network', 'registered', 'last_updated' ] ) ); + // 'meta_query' and 'date_query' are nested arrays, so they are given as JSON. + // The decoded values go straight into the query arguments rather than back + // into $assoc_args, which the rest of this method reads as strings. + // + // parse_shell_arrays() leaves anything that is not JSON alone, and a string + // reaching either of these is ignored without a word. Say so instead. + $decoded_args = Utils\parse_shell_arrays( $assoc_args, [ 'meta_query', 'date_query' ] ); + + foreach ( [ 'meta_query', 'date_query' ] as $json_arg ) { + if ( ! isset( $decoded_args[ $json_arg ] ) ) { + continue; + } + + if ( ! is_array( $decoded_args[ $json_arg ] ) ) { + WP_CLI::error( "Invalid JSON passed to --{$json_arg}." ); + } + + $query_args[ $json_arg ] = $decoded_args[ $json_arg ]; + } + // Arguments this command spells differently to WP_Site_Query. if ( isset( $assoc_args['blog_id'] ) ) { $query_args['site__in'] = [ $assoc_args['blog_id'] ]; @@ -1257,8 +1283,14 @@ public function list_( $args, $assoc_args ) { } } + // A '--date_query' of its own is kept rather than replaced, so '--registered' + // and '--last_updated' narrow it the way every other filter here narrows the + // result instead of quietly winning. if ( ! empty( $date_query ) ) { - $query_args['date_query'] = $date_query; + $given = isset( $query_args['date_query'] ) && is_array( $query_args['date_query'] ) + ? $query_args['date_query'] + : []; + $query_args['date_query'] = array_merge( $given, $date_query ); } if ( isset( $assoc_args['site_user'] ) ) { From 25297bc9d4394e5406ba89bf879fd501a48f7898 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 13:30:28 +0000 Subject: [PATCH 3/4] Nest a given `--date_query` instead of appending to it A date query carries a 'relation' that governs whatever shares its list, so appending the clauses '--registered' and '--last_updated' build put them under the caller's relation as well. Given an 'OR', a site matching neither half of what was asked for came back: wp site list --date_query='{"relation":"OR", ...nonmatching...}' \ --registered= returned the site rather than nothing. Nesting the given query a level down under an outer 'AND' keeps its relation over its own clauses only. While here, '--site_id' and '--network_id' become aliases of '--network' rather than three entries describing one filter. wp-cli lets the canonical name win when several are given, which is the precedence '--network' has always had over '--site_id', so the three branches collapse to one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 16 +++++----------- features/site.feature | 9 +++++++++ src/Site_Command.php | 34 +++++++++++++++++----------------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0759f296e..efb12f229 100644 --- a/README.md +++ b/README.md @@ -5443,13 +5443,15 @@ These fields are optionally available: Lists all sites in a multisite installation. ~~~ -wp site list [--network=] [--=] [--site__in=] [--site__not_in=] [--site_user=] [--site-path=] [--path__in=] [--path__not_in=] [--blog_id=|ID] [--site_id=] [--network_id=] [--network__in=] [--network__not_in=] [--domain=] [--domain__in=] [--domain__not_in=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--lang__in=] [--lang__not_in=] [--search=] [--search_columns=] [--meta_key=] [--meta_value=] [--meta_compare=] [--meta_type=] [--meta_query=] [--date_query=] [--number=] [--offset=] [--no_found_rows=] [--update_site_cache=] [--update_site_meta_cache=] [--orderby=] [--order=] [--field=] [--fields=] [--format=] +wp site list [--network=|site_id|network_id] [--=] [--site__in=] [--site__not_in=] [--site_user=] [--site-path=] [--path__in=] [--path__not_in=] [--blog_id=|ID] [--network__in=] [--network__not_in=] [--domain=] [--domain__in=] [--domain__not_in=] [--registered=] [--last_updated=] [--public=] [--archived=] [--mature=] [--spam=] [--deleted=] [--lang_id=] [--lang__in=] [--lang__not_in=] [--search=] [--search_columns=] [--meta_key=] [--meta_value=] [--meta_compare=] [--meta_type=] [--meta_query=] [--date_query=] [--number=] [--offset=] [--no_found_rows=] [--update_site_cache=] [--update_site_meta_cache=] [--orderby=] [--order=] [--field=] [--fields=] [--format=] ~~~ **OPTIONS** - [--network=] - The network to which the sites belong. + [--network=|site_id|network_id] + The network to which the sites belong. `--site_id` is the name of the + column this filters, `--network_id` is WP_Site_Query's name for it, and + both are accepted as aliases. `--network` wins when more than one is given. [--=] Filter by one or more fields (see "Available Fields" section), or pass any @@ -5480,14 +5482,6 @@ wp site list [--network=] [--=] [--site__in=] [--site__ Filter by site ID. `--ID` is WP_Site_Query's name for the same filter and is accepted as an alias. - [--site_id=] - Filter by the ID of the network the site belongs to. `--network` and - `--network_id` are aliases for this; `--network` takes precedence when - more than one is given. - - [--network_id=] - Filter by the ID of the network the site belongs to. - [--network__in=] Only list the sites belonging to these network IDs (comma-separated). diff --git a/features/site.feature b/features/site.feature index 00dfb470b..17988bd2b 100644 --- a/features/site.feature +++ b/features/site.feature @@ -1227,6 +1227,15 @@ Feature: Manage sites in a multisite installation 0 """ + # The relation of a given --date_query governs only its own clauses. Appending + # to the same list would let an 'OR' reach the clause --registered adds and + # match a site that satisfies neither half of what was asked for. + When I run `wp site list --blog_id={ALPHA_ID} --date_query='{"relation":"OR","0":{"column":"registered","before":"1999-01-01"},"1":{"column":"registered","before":"1998-01-01"}}' --registered='{ALPHA_REGISTERED}' --format=count` + Then STDOUT should be: + """ + 0 + """ + When I try `wp site list --meta_query=notjson` Then STDERR should contain: """ diff --git a/src/Site_Command.php b/src/Site_Command.php index 1463c954d..22c8897d6 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -982,8 +982,10 @@ private function get_network( $network_id ) { * * ## OPTIONS * - * [--network=] - * : The network to which the sites belong. + * [--network=|site_id|network_id] + * : The network to which the sites belong. `--site_id` is the name of the + * column this filters, `--network_id` is WP_Site_Query's name for it, and + * both are accepted as aliases. `--network` wins when more than one is given. * * [--=] * : Filter by one or more fields (see "Available Fields" section), or pass any @@ -1014,14 +1016,6 @@ private function get_network( $network_id ) { * : Filter by site ID. `--ID` is WP_Site_Query's name for the same filter * and is accepted as an alias. * - * [--site_id=] - * : Filter by the ID of the network the site belongs to. `--network` and - * `--network_id` are aliases for this; `--network` takes precedence when - * more than one is given. - * - * [--network_id=] - * : Filter by the ID of the network the site belongs to. - * * [--network__in=] * : Only list the sites belonging to these network IDs (comma-separated). * @@ -1253,11 +1247,10 @@ public function list_( $args, $assoc_args ) { } } - if ( isset( $assoc_args['site_id'] ) ) { - $query_args['network_id'] = $assoc_args['site_id']; - } - - // '--network' has always taken precedence over '--site_id'. + // '--site_id' and '--network_id' are aliases of '--network', so they have + // already been resolved to it by the time the command runs. wp-cli lets the + // canonical name win when more than one is given, which is the precedence + // '--network' has always had over '--site_id'. if ( isset( $assoc_args['network'] ) ) { $query_args['network_id'] = $assoc_args['network']; } @@ -1286,11 +1279,18 @@ public function list_( $args, $assoc_args ) { // A '--date_query' of its own is kept rather than replaced, so '--registered' // and '--last_updated' narrow it the way every other filter here narrows the // result instead of quietly winning. + // + // It is nested a level down rather than appended to, because its 'relation' + // governs whatever shares its list: an 'OR' would reach the clauses added + // here and match a site that satisfies neither half of what was asked for. if ( ! empty( $date_query ) ) { - $given = isset( $query_args['date_query'] ) && is_array( $query_args['date_query'] ) + $given = isset( $query_args['date_query'] ) && is_array( $query_args['date_query'] ) ? $query_args['date_query'] : []; - $query_args['date_query'] = array_merge( $given, $date_query ); + + $query_args['date_query'] = empty( $given ) + ? $date_query + : array_merge( [ 'relation' => 'AND', $given ], $date_query ); } if ( isset( $assoc_args['site_user'] ) ) { From d025fb90fe9944856c8b9a69f2ff28512f324c66 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 13:36:38 +0000 Subject: [PATCH 4/4] Move the site meta filters behind @require-wp-5.1 Multisite gained the site meta table in WordPress 5.1, and WP_Site_Query gained the meta_* parameters that read it in the same release, so `wp site meta add` fails on 4.9 with "The table is not installed" and the filters have nothing to match against. They get a scenario of their own, tagged for the version that has them, and the docblock says so. The rest stays where it is: 'lang_id', 'lang__in' and 'lang__not_in' date from 4.8 and everything else here from 4.6, so only the meta arguments needed separating. Also covers '--last_updated' against an OR date query, not just '--registered', so both filters are pinned against the relation leaking. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014SSZzqMJRDTiLiDxQEPYcL --- README.md | 3 +- features/site.feature | 88 +++++++++++++++++++++++++++++-------------- src/Site_Command.php | 11 +++++- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index efb12f229..7760bf5b4 100644 --- a/README.md +++ b/README.md @@ -5538,7 +5538,8 @@ wp site list [--network=|site_id|network_id] [--=] [--site__in and 'path'. [--meta_key=] - Filter by this site meta key. + Filter by this site meta key. The site meta arguments need WordPress 5.1 + or later, which is where multisite gained the table they read. [--meta_value=] Filter by this site meta value. Used together with `--meta_key`. diff --git a/features/site.feature b/features/site.feature index 17988bd2b..da83f80f7 100644 --- a/features/site.feature +++ b/features/site.feature @@ -1169,35 +1169,7 @@ Feature: Manage sites in a multisite installation 3 """ - When I run `wp site meta add {ALPHA_ID} colour blue` - Then STDOUT should not be empty - - When I run `wp site list --meta_key=colour --meta_value=blue --field=blog_id` - Then STDOUT should be: - """ - {ALPHA_ID} - """ - - When I run `wp site list --meta_key=colour --meta_value=red --format=count` - Then STDOUT should be: - """ - 0 - """ - - # --meta_query and --date_query are nested arrays, so they are given as JSON. - When I run `wp site list --meta_query='[{"key":"colour","value":"blue"}]' --field=blog_id` - Then STDOUT should be: - """ - {ALPHA_ID} - """ - - When I run `wp site list --meta_query='[{"key":"colour","compare":"NOT EXISTS"}]' --field=blog_id` - Then STDOUT should be: - """ - 1 - {BETA_ID} - """ - + # --date_query is a nested array, so it is given as JSON. When I run `wp site list --date_query='[{"column":"registered","after":"1999-01-01"}]' --format=count` Then STDOUT should be: """ @@ -1236,6 +1208,21 @@ Feature: Manage sites in a multisite installation 0 """ + When I run `wp site list --blog_id={ALPHA_ID} --field=last_updated` + Then save STDOUT as {ALPHA_UPDATED} + + When I run `wp site list --blog_id={ALPHA_ID} --last_updated='{ALPHA_UPDATED}' --format=count` + Then STDOUT should be: + """ + 1 + """ + + When I run `wp site list --blog_id={ALPHA_ID} --date_query='{"relation":"OR","0":{"column":"last_updated","before":"1999-01-01"},"1":{"column":"last_updated","before":"1998-01-01"}}' --last_updated='{ALPHA_UPDATED}' --format=count` + Then STDOUT should be: + """ + 0 + """ + When I try `wp site list --meta_query=notjson` Then STDERR should contain: """ @@ -1250,6 +1237,49 @@ Feature: Manage sites in a multisite installation """ And the return code should be 1 + # WP_Site_Query gained the meta_* parameters, and multisite gained the site meta + # table they read, in WordPress 5.1. + @require-wp-5.1 + Scenario: Filter the site list by site meta + Given a WP multisite install + + When I run `wp site create --slug=alpha --porcelain` + Then STDOUT should be a number + And save STDOUT as {ALPHA_ID} + + When I run `wp site create --slug=beta --porcelain` + Then STDOUT should be a number + And save STDOUT as {BETA_ID} + + When I run `wp site meta add {ALPHA_ID} colour blue` + Then STDOUT should not be empty + + When I run `wp site list --meta_key=colour --meta_value=blue --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --meta_key=colour --meta_value=red --format=count` + Then STDOUT should be: + """ + 0 + """ + + # --meta_query is a nested array, so it is given as JSON. + When I run `wp site list --meta_query='[{"key":"colour","value":"blue"}]' --field=blog_id` + Then STDOUT should be: + """ + {ALPHA_ID} + """ + + When I run `wp site list --meta_query='[{"key":"colour","compare":"NOT EXISTS"}]' --field=blog_id` + Then STDOUT should be: + """ + 1 + {BETA_ID} + """ + Scenario: Existing site list filters keep working against WP_Site_Query Given a WP multisite install diff --git a/src/Site_Command.php b/src/Site_Command.php index 22c8897d6..c8b959d4e 100644 --- a/src/Site_Command.php +++ b/src/Site_Command.php @@ -1072,7 +1072,8 @@ private function get_network( $network_id ) { * and 'path'. * * [--meta_key=] - * : Filter by this site meta key. + * : Filter by this site meta key. The site meta arguments need WordPress 5.1 + * or later, which is where multisite gained the table they read. * * [--meta_value=] * : Filter by this site meta value. Used together with `--meta_key`. @@ -1290,7 +1291,13 @@ public function list_( $args, $assoc_args ) { $query_args['date_query'] = empty( $given ) ? $date_query - : array_merge( [ 'relation' => 'AND', $given ], $date_query ); + : array_merge( + [ + 'relation' => 'AND', + $given, + ], + $date_query + ); } if ( isset( $assoc_args['site_user'] ) ) {