Skip to content

Add ID range notation to commands that accept one or more IDs#593

Open
Copilot wants to merge 9 commits intomainfrom
copilot/add-ranges-to-commands
Open

Add ID range notation to commands that accept one or more IDs#593
Copilot wants to merge 9 commits intomainfrom
copilot/add-ranges-to-commands

Conversation

Copy link
Contributor

Copilot AI commented Mar 14, 2026

Commands accepting multiple IDs now support range notation, allowing bulk operations without shell expansion limits or spawning multiple WP-CLI processes.

Supported syntax

Pattern Meaning
15-35 All existing IDs from 15 to 35 inclusive
34- All existing IDs from 34 onwards
-35 All existing IDs from 1 through 35

Ranges can be mixed with individual IDs freely: wp post delete 3 7 12-24 19 34- --force

Only existing IDs within a range are processed — no warnings for gaps.

Implementation

  • src/WP_CLI/ExpandsIdRanges.php — New trait with expand_id_ranges(array $args, callable $get_ids_in_range): array. Parses range notation and delegates DB lookup to a command-specific callback. Results are deduplicated. Each range expansion emits a WP_CLI::debug() message (group: range-expansion) showing how many IDs were resolved, making large open-ended expansions visible via --debug=range-expansion.

  • CommandWithDBObject — Uses the trait, making it available to Post, Comment, User, and Site commands.

  • Term_Command — Uses the trait directly (extends WP_CLI_Command, not CommandWithDBObject). Range expansion is skipped when --by=slug is passed.

  • Per-command DB helpers — Each command gets a get_*_ids_in_range(int $start, ?int $end): array method querying its table via $wpdb->prepare() with BETWEEN/>= clauses.

Commands updated

  • post delete, post update
  • comment delete, trash, untrash, spam, unspam, approve, unapprove, update
  • user delete, update, reset-password, spam, unspam
  • term delete
  • site activate, archive, deactivate, mature, unmature, private, public, spam, unspam, unarchive

Test coverage

Behat scenarios cover range notation for:

  • wp post delete, wp post update
  • wp comment delete, wp comment trash (non-delete path)
  • wp term delete
  • wp user delete, wp user reset-password (non-delete path)
  • wp site archive (multisite path)
Original prompt

This section details on the original issue you should resolve

<issue_title>Add ranges to commands that accept one or more IDs</issue_title>
<issue_description>For commands that accept one or more IDs as positional parameters, I would like to investigate adding ranges and partial ranges as valid IDs.

A range would take the following form: <range start>-<range end>. This would mean that all existing IDs that fall into the range that starts at <range start> and ends at <range end> would be included.

Example: 15-35 would mean all IDs starting from 15 and going up to and including 35 would be processed. Only valid existing IDs within that range would be processed, so that no warnings are being thrown for missing IDs in-between.

A partial range would take one of these forms: <range start>- or -<range-end>. This would mean that either all existing IDs starting from <range start> without any upper bound or all existing IDs from 1 onwards up to and including <range end> would be included.

Ranges and individual IDs can be combined in any way, so you can use something like the following: 3 7 12-24 19 34-.

Commands that could profit from this range notation:

  • comment approve
  • comment delete
  • comment recount
  • comment spam
  • comment trash
  • comment unapprove
  • comment unspam
  • comment untrash
  • comment update
  • embed cache clear (*)
  • embed cache trigger (*)
  • media regenerate
  • post delete
  • post meta delete (*)
  • post meta update (*)
  • post update
  • site activate
  • site archive
  • site deactivate
  • site delete (*)
  • site mature
  • site private
  • site public
  • site spam
  • site unarchive
  • site unmature
  • site unspam
  • term delete
  • term meta delete (*)
  • term meta update (*)
  • term update (*)
  • user add-cap (*)
  • user add-role (*)
  • user delete
  • user meta delete (*)
  • user meta update (*)
  • user remove-cap (*)
  • user remove-role (*)
  • user reset-password
  • user set-role (*)
  • user spam
  • user unspam
  • user update
  • widget delete
  • widget move (*)
  • widget update (*)

(*) => not variadic yet</issue_description>

Comments on the Issue (you are @copilot in this section)

@schlessera @danielbachhuber, @johnbillion Would love your thoughts on the above.

I know that we can already use xargs for doing similar things, but I do think this makes it much easier for people to execute for complex operations, and lets you more easily fine-tune the IDs you're targeting.</comment_new>
<comment_new>@danielbachhuber
I'm 👎 on this for a few reasons:

  1. It violates our philosophy of composability.
  2. It would be new syntax for the user to learn.
  3. It's valuable for a WP-CLI user to learn to use xargs and other shell tools in conjunction with WP-CLI.</comment_new>
    <comment_new>@schlessera
    Responding to @danielbachhuber's cons:
  1. It violates our philosophy of composability.

Composability is still available, but extended so that you can also compose ranges. So, you can do something like this:

wp post delete $(command to figure out start)-$(command to figure out end)

I don't see this as breaking that principle, it is a mere extension to make the syntax more expressive.

  1. It would be new syntax for the user to learn.

Yes, I agree. I don't think this would be a big issue, because a) the syntax is actually far from complex and b) it is purely optional, you can still just use one regular ID without ever noticing something was changed.

  1. It's valuable for a WP-CLI user to learn to use xargs and other shell tools in conjunction with WP-CLI.

I agree with that, but I don't think skipping a valuable performance and expressiveness improvement for that sake is a good move or a service to users. For a lot of users, this syntax change might actually mean they will never even have a need for xargs and don't need to deal with its much more complex syntax.

Responding to @johnbillion's questions:

Does this syntax provide real value? What use cases does this improve? Some example use cases would be good.

The original use case that triggered this idea is wp-cli/media-command#82

Can it be made performant internally? More so than piping in IDs with xargs?

It will actually be more performant than the xargs right out of the box, because the IDs can be retrieved with 1 query to add them to the loop. So, a range like 1-300 will entail 1 query and 1 loading of WordPress only.</comment_new>
<comment_new>@danielbachhuber

Composability is still available, but extended so that you can also compose ranges. So, you can do something like this:

I ...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@github-actions github-actions bot added command:comment-approve Related to 'comment approve' command command:comment-delete Related to 'comment delete' command command:comment-recount Related to 'comment recount' command command:comment-spam Related to 'comment spam' command command:comment-trash Related to 'comment trash' command command:comment-unapprove Related to 'comment unapprove' command command:comment-unspam Related to 'comment unspam' command command:comment-untrash Related to 'comment untrash' command command:comment-update Related to 'comment update' command command:post-delete Related to 'post delete' command labels Mar 14, 2026
Copilot AI and others added 2 commits March 14, 2026 21:53
… commands

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
… in reset_password

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Copilot AI changed the title [WIP] Add ranges to commands that accept one or more IDs Add ID range notation to commands that accept one or more IDs Mar 14, 2026
Copilot AI requested a review from swissspidy March 14, 2026 21:57
@swissspidy

This comment was marked as resolved.

@codecov
Copy link

codecov bot commented Mar 14, 2026

Codecov Report

❌ Patch coverage is 72.80702% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Term_Command.php 67.85% 9 Missing ⚠️
src/WP_CLI/ExpandsIdRanges.php 60.86% 9 Missing ⚠️
src/User_Command.php 84.00% 4 Missing ⚠️
src/Comment_Command.php 82.35% 3 Missing ⚠️
src/Post_Command.php 72.72% 3 Missing ⚠️
src/Site_Command.php 70.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

… to protected

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>

This comment was marked as resolved.

@swissspidy

This comment was marked as resolved.

… and term feature files

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>

This comment was marked as resolved.

@swissspidy swissspidy marked this pull request as ready for review March 15, 2026 09:21
@swissspidy swissspidy requested a review from a team as a code owner March 15, 2026 09:21
Copilot AI review requested due to automatic review settings March 15, 2026 09:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for ID range notation (e.g. 15-35, 34-, -35) to multiple WP-CLI entity commands so users can target large sets of objects without shell expansion limits or running multiple CLI processes.

Changes:

  • Introduces a reusable WP_CLI\ExpandsIdRanges trait to parse range arguments and delegate ID lookups to command-specific callbacks.
  • Wires range expansion into Post, Comment, User, Site (via CommandWithDBObject) and Term (directly) commands, with per-command SQL helpers to fetch IDs in a range.
  • Adds Behat feature coverage for range usage in post, comment, term, and user workflows (partial coverage).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/WP_CLI/ExpandsIdRanges.php Adds generic range parsing + deduplication for positional args.
src/WP_CLI/CommandWithDBObject.php Mixes the trait into the base DB-object command class.
src/Post_Command.php Expands ranges for post update / post delete and adds a DB helper.
src/Comment_Command.php Expands ranges across several comment subcommands and adds a DB helper.
src/User_Command.php Expands ranges for multiple user subcommands and adds a DB helper.
src/Term_Command.php Uses trait directly for term delete (skips when --by=slug).
src/Site_Command.php Expands ranges for site status subcommands via get_sites_ids() and adds a DB helper.
phpcs.xml.dist Excludes the new namespaced trait file from the global-prefix namespace sniff.
features/*.feature Adds new scenarios validating range expansion for a subset of updated commands.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

swissspidy and others added 2 commits March 15, 2026 14:17
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@swissspidy

This comment was marked as resolved.

…and feature tests for site, comment, user range operations

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>

This comment was marked as resolved.

@swissspidy
Copy link
Member

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for performing bulk operations using ID ranges, which will significantly improve user experience for many commands. The implementation using a reusable trait is well-designed. My review focuses on improving code clarity and consistency by reducing duplication in the new database helper methods. I've also identified a critical issue in User_Command where several commands could misinterpret user logins as ID ranges, potentially leading to unintended actions. Addressing this will make the new feature more robust and reliable.

Comment on lines +290 to +307
foreach ( $args as $arg ) {
if ( is_string( $arg ) && preg_match( '/^\d+-\d+$/', $arg ) ) {
$user_by_login = get_user_by( 'login', $arg );
$user_by_email = get_user_by( 'email', $arg );

if ( $user_by_login || $user_by_email ) {
// Treat as login/email, do not expand as an ID range.
$expanded_args[] = $arg;
} else {
$range_expanded = self::expand_id_ranges( [ $arg ], [ $this, 'get_user_ids_in_range' ] );
foreach ( $range_expanded as $expanded_arg ) {
$expanded_args[] = $expanded_arg;
}
}
} else {
$expanded_args[] = $arg;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The special handling for user logins that look like ID ranges is a great idea. However, the current implementation only checks for full ranges (e.g., 12-24) and misses partial ranges (e.g., 12- or -24). This could lead to a user login like 12- being incorrectly treated as an ID range. The check should be expanded to cover all range formats supported by ExpandsIdRanges. Additionally, the loop for merging expanded IDs can be simplified using array_merge.

foreach ( $args as $arg ) {
	if ( is_string( $arg ) && preg_match( '/^(\d+-\d*|-\d+)$/', $arg ) && '-' !== $arg ) {
		$user_by_login = get_user_by( 'login', $arg );
		$user_by_email = get_user_by( 'email', $arg );

		if ( $user_by_login || $user_by_email ) {
			// Treat as login/email, do not expand as an ID range.
			$expanded_args[] = $arg;
		} else {
			$expanded_args = array_merge( $expanded_args, self::expand_id_ranges( [ $arg ], [ $this, 'get_user_ids_in_range' ] ) );
		}
	} else {
		$expanded_args[] = $arg;
	}
}

* @param array $assoc_args Associative arguments.
*/
public function update( $args, $assoc_args ) {
$args = self::expand_id_ranges( $args, [ $this, 'get_user_ids_in_range' ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The update command can accept user logins, emails, or IDs. Similar to the delete command, there's a risk of misinterpreting a user login that looks like an ID range (e.g., '12-24'). You should add the same protective logic here as in the delete method to check if a user exists with that login/email before attempting to expand it as a range. This also applies to reset-password, spam, and unspam commands.

	$expanded_args = [];
	foreach ( $args as $arg ) {
		if ( is_string( $arg ) && preg_match( '/^(\d+-\d*|-\d+)$/', $arg ) && '-' !== $arg ) {
			$user_by_login = get_user_by( 'login', $arg );
			$user_by_email = get_user_by( 'email', $arg );

			if ( $user_by_login || $user_by_email ) {
				// Treat as login/email, do not expand as an ID range.
				$expanded_args[] = $arg;
			} else {
				$expanded_args = array_merge( $expanded_args, self::expand_id_ranges( [ $arg ], [ $this, 'get_user_ids_in_range' ] ) );
			}
		} else {
			$expanded_args[] = $arg;
		}
	}
	$args = $expanded_args;

* @subcommand reset-password
*/
public function reset_password( $args, $assoc_args ) {
$args = self::expand_id_ranges( $args, [ $this, 'get_user_ids_in_range' ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The reset-password command can accept user logins. Similar to the delete command, there's a risk of misinterpreting a user login that looks like an ID range (e.g., '12-24'). You should add protective logic here to check if a user exists with that login/email before attempting to expand it as a range.

		$expanded_args = [];
		foreach ( $args as $arg ) {
			if ( is_string( $arg ) && preg_match( '/^(\d+-\d*|-\d+)$/', $arg ) && '-' !== $arg ) {
				$user_by_login = get_user_by( 'login', $arg );
				$user_by_email = get_user_by( 'email', $arg );

				if ( $user_by_login || $user_by_email ) {
					// Treat as login/email, do not expand as an ID range.
					$expanded_args[] = $arg;
				} else {
					$expanded_args = array_merge( $expanded_args, self::expand_id_ranges( [ $arg ], [ $this, 'get_user_ids_in_range' ] ) );
				}
			} else {
				$expanded_args[] = $arg;
			}
		}
		$args          = $expanded_args;

* Success: Spammed 1 of 1 users.
*/
public function spam( $args ) {
$args = self::expand_id_ranges( $args, [ $this, 'get_user_ids_in_range' ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The spam command can accept user logins. Similar to the delete command, there's a risk of misinterpreting a user login that looks like an ID range (e.g., '12-24'). You should add protective logic here to check if a user exists with that login/email before attempting to expand it as a range.

		$expanded_args = [];
		foreach ( $args as $arg ) {
			if ( is_string( $arg ) && preg_match( '/^(\d+-\d*|-\d+)$/', $arg ) && '-' !== $arg ) {
				$user_by_login = get_user_by( 'login', $arg );
				$user_by_email = get_user_by( 'email', $arg );

				if ( $user_by_login || $user_by_email ) {
					// Treat as login/email, do not expand as an ID range.
					$expanded_args[] = $arg;
				} else {
					$expanded_args = array_merge( $expanded_args, self::expand_id_ranges( [ $arg ], [ $this, 'get_user_ids_in_range' ] ) );
				}
			} else {
				$expanded_args[] = $arg;
			}
		}
		$args = $expanded_args;

* Success: Unspamed 1 of 1 users.
*/
public function unspam( $args ) {
$args = self::expand_id_ranges( $args, [ $this, 'get_user_ids_in_range' ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The unspam command can accept user logins. Similar to the delete command, there's a risk of misinterpreting a user login that looks like an ID range (e.g., '12-24'). You should add protective logic here to check if a user exists with that login/email before attempting to expand it as a range.

		$expanded_args = [];
		foreach ( $args as $arg ) {
			if ( is_string( $arg ) && preg_match( '/^(\d+-\d*|-\d+)$/', $arg ) && '-' !== $arg ) {
				$user_by_login = get_user_by( 'login', $arg );
				$user_by_email = get_user_by( 'email', $arg );

				if ( $user_by_login || $user_by_email ) {
					// Treat as login/email, do not expand as an ID range.
					$expanded_args[] = $arg;
				} else {
					$expanded_args = array_merge( $expanded_args, self::expand_id_ranges( [ $arg ], [ $this, 'get_user_ids_in_range' ] ) );
				}
			} else {
				$expanded_args[] = $arg;
			}
		}
		$args = $expanded_args;

$start = (int) $matches[1];
$end = (int) $matches[2];
if ( $start > $end ) {
// Normalize reversed ranges like "35-15" to "15-35".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a small typo in this comment. It says 15-15 but it should probably be 15-35 to match the example 35-15.

				// Normalize reversed ranges like "35-15" to "15-35".

Comment on lines +805 to +819
protected function get_comment_ids_in_range( int $start, ?int $end ): array {
global $wpdb;

if ( null === $end ) {
return array_map(
'intval',
$wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM {$wpdb->comments} WHERE comment_ID >= %d ORDER BY comment_ID ASC", $start ) )
);
}

return array_map(
'intval',
$wpdb->get_col( $wpdb->prepare( "SELECT comment_ID FROM {$wpdb->comments} WHERE comment_ID BETWEEN %d AND %d ORDER BY comment_ID ASC", $start, $end ) )
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method can be made more concise and DRY by building the query dynamically instead of having two separate return paths.

	protected function get_comment_ids_in_range( int $start, ?int $end ): array {
		global $wpdb;

		$query  = "SELECT comment_ID FROM {$wpdb->comments} WHERE ";
		$params = [ $start ];

		if ( null === $end ) {
			$query .= 'comment_ID >= %d';
		} else {
			$query   .= 'comment_ID BETWEEN %d AND %d';
			$params[] = $end;
		}

		$query .= ' ORDER BY comment_ID ASC';

		return array_map(
			'intval',
			$wpdb->get_col( $wpdb->prepare( $query, ...$params ) )
		);
	}

Comment on lines +1254 to +1268
protected function get_post_ids_in_range( int $start, ?int $end ): array {
global $wpdb;

if ( null === $end ) {
return array_map(
'intval',
$wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID >= %d ORDER BY ID ASC", $start ) )
);
}

return array_map(
'intval',
$wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID BETWEEN %d AND %d ORDER BY ID ASC", $start, $end ) )
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method can be made more concise and DRY by building the query dynamically instead of having two separate return paths.

	protected function get_post_ids_in_range( int $start, ?int $end ): array {
		global $wpdb;

		$query  = "SELECT ID FROM {$wpdb->posts} WHERE ";
		$params = [ $start ];

		if ( null === $end ) {
			$query .= 'ID >= %d';
		} else {
			$query   .= 'ID BETWEEN %d AND %d';
			$params[] = $end;
		}

		$query .= ' ORDER BY ID ASC';

		return array_map(
			'intval',
			$wpdb->get_col( $wpdb->prepare( $query, ...$params ) )
		);
	}

Comment on lines +1493 to +1507
protected function get_site_ids_in_range( int $start, ?int $end ): array {
global $wpdb;

if ( null === $end ) {
return array_map(
'intval',
$wpdb->get_col( $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} WHERE blog_id >= %d ORDER BY blog_id ASC", $start ) )
);
}

return array_map(
'intval',
$wpdb->get_col( $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} WHERE blog_id BETWEEN %d AND %d ORDER BY blog_id ASC", $start, $end ) )
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method can be made more concise and DRY by building the query dynamically instead of having two separate return paths.

	protected function get_site_ids_in_range( int $start, ?int $end ): array {
		global $wpdb;

		$query  = "SELECT blog_id FROM {$wpdb->blogs} WHERE ";
		$params = [ $start ];

		if ( null === $end ) {
			$query .= 'blog_id >= %d';
		} else {
			$query   .= 'blog_id BETWEEN %d AND %d';
			$params[] = $end;
		}

		$query .= ' ORDER BY blog_id ASC';

		return array_map(
			'intval',
			$wpdb->get_col( $wpdb->prepare( $query, ...$params ) )
		);
	}

Comment on lines +921 to +948
protected function get_term_ids_in_range( string $taxonomy, int $start, ?int $end ): array {
global $wpdb;

if ( null === $end ) {
return array_map(
'intval',
$wpdb->get_col(
$wpdb->prepare(
"SELECT t.term_id FROM {$wpdb->terms} t JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE t.term_id >= %d AND tt.taxonomy = %s ORDER BY t.term_id ASC",
$start,
$taxonomy
)
)
);
}

return array_map(
'intval',
$wpdb->get_col(
$wpdb->prepare(
"SELECT t.term_id FROM {$wpdb->terms} t JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE t.term_id BETWEEN %d AND %d AND tt.taxonomy = %s ORDER BY t.term_id ASC",
$start,
$end,
$taxonomy
)
)
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method can be made more concise and DRY by building the query dynamically instead of having two separate return paths.

	protected function get_term_ids_in_range( string $taxonomy, int $start, ?int $end ): array {
		global $wpdb;

		$query  = "SELECT t.term_id FROM {$wpdb->terms} t JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE ";
		$params = [ $start ];

		if ( null === $end ) {
			$query .= 't.term_id >= %d';
		} else {
			$query   .= 't.term_id BETWEEN %d AND %d';
			$params[] = $end;
		}

		$query   .= ' AND tt.taxonomy = %s ORDER BY t.term_id ASC';
		$params[] = $taxonomy;

		return array_map(
			'intval',
			$wpdb->get_col( $wpdb->prepare( $query, ...$params ) )
		);
	}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

command:comment-approve Related to 'comment approve' command command:comment-delete Related to 'comment delete' command command:comment-recount Related to 'comment recount' command command:comment-spam Related to 'comment spam' command command:comment-trash Related to 'comment trash' command command:comment-unapprove Related to 'comment unapprove' command command:comment-unspam Related to 'comment unspam' command command:comment-untrash Related to 'comment untrash' command command:comment-update Related to 'comment update' command command:post-delete Related to 'post delete' command

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ranges to commands that accept one or more IDs

3 participants