Repository: Cog-Creators/Red-DiscordBot

Issue:
[Core] Add a possible way to remove multiple paths with `[p]removepath`.
### Type of feature request

Command

### Description of the feature you're suggesting

Currently you can only remove one path with `[p]removepath`, it would be great if you could remove multiple ones at once instead of having to redo the command multiple times, Let me add a small example here, if someone has 10 paths and wants to remove 5 of them, They might want them all at once. So a possible way would be great there.

### Anything else?

No.


Discussion:
Whilst I'm not exactly opposed to this idea, I don't think removing paths is a frequent practice. If there are a few paths that wish to be removed, it wouldn't cause harm to re-use the command until all the necessary paths have been removed. Considering the use of the command to this extent isn't common, it questions whether the change would actually be necessary. I guess it wouldn't hurt to implement a fitting change though.
@Flame442 Can you assign me to this issue? I'll take it :)


Resolving patch:
diff --git a/redbot/core/cog_manager.py b/redbot/core/cog_manager.py
--- a/redbot/core/cog_manager.py
+++ b/redbot/core/cog_manager.py
@@ -4,9 +4,10 @@
 from importlib import import_module, invalidate_caches
 from importlib.machinery import ModuleSpec
 from pathlib import Path
-from typing import Union, List, Optional
+from typing import TYPE_CHECKING, Union, List, Optional
 
 import redbot.cogs
+from redbot.core.commands import BadArgument
 from redbot.core.utils import deduplicate_iterables
 import discord
 
@@ -15,11 +16,26 @@
 from .i18n import Translator, cog_i18n
 from .data_manager import cog_data_path
 
-from .utils.chat_formatting import box, pagify
+from .utils.chat_formatting import box, pagify, humanize_list, inline
 
 __all__ = ["CogManager"]
 
 
+# Duplicate of redbot.cogs.cleanup.converters.positive_int
+if TYPE_CHECKING:
+    positive_int = int
+else:
+
+    def positive_int(arg: str) -> int:
+        try:
+            ret = int(arg)
+        except ValueError:
+            raise BadArgument(_("{arg} is not an integer.").format(arg=inline(arg)))
+        if ret <= 0:
+            raise BadArgument(_("{arg} is not a positive integer.").format(arg=inline(arg)))
+        return ret
+
+
 class NoSuchCog(ImportError):
     """Thrown when a cog is missing.
 
@@ -358,39 +374,53 @@ async def addpath(self, ctx: commands.Context, *, path: Path):
         else:
             await ctx.send(_("Path successfully added."))
 
-    @commands.command()
+    @commands.command(require_var_positional=True)
     @checks.is_owner()
-    async def removepath(self, ctx: commands.Context, path_number: int):
+    async def removepath(self, ctx: commands.Context, *path_numbers: positive_int):
         """
-        Removes a path from the available cog paths given the `path_number` from `[p]paths`.
+        Removes one or more paths from the available cog paths given the `path_numbers` from `[p]paths`.
         """
-        path_number -= 1
-        if path_number < 0:
-            await ctx.send(_("Path numbers must be positive."))
-            return
+        valid: List[Path] = []
+        invalid: List[int] = []
 
         cog_paths = await ctx.bot._cog_mgr.user_defined_paths()
-        try:
-            to_remove = cog_paths.pop(path_number)
-        except IndexError:
-            await ctx.send(_("That is an invalid path number."))
-            return
+        # dict.fromkeys removes duplicates while preserving the order
+        for path_number in dict.fromkeys(sorted(path_numbers)):
+            idx = path_number - 1
+            try:
+                to_remove = cog_paths[idx]
+            except IndexError:
+                invalid.append(path_number)
+            else:
+                await ctx.bot._cog_mgr.remove_path(to_remove)
+                valid.append(to_remove)
+
+        parts = []
+        if valid:
+            parts.append(
+                _("The following paths were removed: {paths}").format(
+                    paths=humanize_list([inline(str(path)) for path in valid])
+                )
+            )
+        if invalid:
+            parts.append(
+                _("The following path numbers did not exist: {path_numbers}").format(
+                    path_numbers=humanize_list([inline(str(path)) for path in invalid])
+                )
+            )
 
-        await ctx.bot._cog_mgr.remove_path(to_remove)
-        await ctx.send(_("Path successfully removed."))
+        for page in pagify("\n\n".join(parts), ["\n", " "]):
+            await ctx.send(page)
 
     @commands.command()
     @checks.is_owner()
-    async def reorderpath(self, ctx: commands.Context, from_: int, to: int):
+    async def reorderpath(self, ctx: commands.Context, from_: positive_int, to: positive_int):
         """
         Reorders paths internally to allow discovery of different cogs.
         """
         # Doing this because in the paths command they're 1 indexed
         from_ -= 1
         to -= 1
-        if from_ < 0 or to < 0:
-            await ctx.send(_("Path numbers must be positive."))
-            return
 
         all_paths = await ctx.bot._cog_mgr.user_defined_paths()
         try:


Test patch: