Repository: conan-io/conan
Pull request: Remove packages when version is asterisk
When removing a pattern like _hello/*@conan/testing_, Conan doesn't remove all patterns because all fields (name, version, channel, user) are validated by ConanFileReference. However, when using a pattern like _hello*/*@conan/testing_, the ConanFileReference won't match the name and will search for recipes.

Changelog: Fix: Remove packages when version is asterisk (#5297)
Docs: Omit

fixes #5297 

- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. 

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>


Problem statement:
[bug] conan remove does not find recipe
Hi there,
I tried to delete every version of a recipe in a certain user/channel but Conan stopped working with the error that it could not find the recipe:
```
conan remove cygwin_installer/*@user/channel
ERROR: Recipe not found: 'cygwin_installer/*@user/channel'
```
But when I modify the recipe name and append an asterix it works:
```
conan remove cygwin_installer*/*@user/channel
Are you sure you want to delete from 'cygwin_installer/2.9.0@user/channel' (yes/no):
```

Is this intentional?

Hints from issue discussion:

Commit message: #5297 Validate channel when removing

Signed-off-by: Uilian Ries <uilianries@gmail.com>

Patch to review:
diff --git a/conans/client/remover.py b/conans/client/remover.py
index de6ca8675a3..353bbcb8393 100644
--- a/conans/client/remover.py
+++ b/conans/client/remover.py
@@ -168,7 +168,15 @@ def remove(self, pattern, remote_name, src=None, build_ids=None, package_ids_fil
             else:
                 refs = self._remote_manager.search_recipes(remote, pattern)
         else:
-            if input_ref:
+            if not input_ref or (input_ref and
+                                 input_ref.version == "*" or
+                                 input_ref.channel == "*" or
+                                 input_ref.user == "*"):
+                refs = search_recipes(self._cache, pattern)
+                if not refs:
+                    self._user_io.out.warn("No package recipe matches '%s'" % str(pattern))
+                    return
+            else:
                 refs = []
                 if self._cache.installed_as_editable(input_ref):
                     raise ConanException(self._message_removing_editable(input_ref))
@@ -176,11 +184,6 @@ def remove(self, pattern, remote_name, src=None, build_ids=None, package_ids_fil
                     raise RecipeNotFoundException(input_ref,
                                                   print_rev=self._cache.config.revisions_enabled)
                 refs.append(input_ref)
-            else:
-                refs = search_recipes(self._cache, pattern)
-                if not refs:
-                    self._user_io.out.warn("No package recipe matches '%s'" % str(pattern))
-                    return
 
         if input_ref and not input_ref.revision:
             # Ignore revisions for deleting if the input was not with a revision
diff --git a/conans/test/functional/command/remove_test.py b/conans/test/functional/command/remove_test.py
index c62452f40da..7d1b7f988f8 100644
--- a/conans/test/functional/command/remove_test.py
+++ b/conans/test/functional/command/remove_test.py
@@ -284,6 +284,42 @@ def basic_packages_test(self):
                              os.listdir(os.path.join(self.client.storage_folder,
                                                      "Hello/2.4.11/myuser/testing")))
 
+    def _validate_remove_all_hello_packages(self):
+        self.assert_folders(local_folders={"H1": None, "H2": None, "B": [1, 2], "O": [1, 2]},
+                            remote_folders={"H1": [1, 2], "H2": [1, 2], "B": [1, 2], "O": [1, 2]},
+                            build_folders={"H1": None, "H2": None, "B": [1, 2], "O": [1, 2]},
+                            src_folders={"H1": False, "H2": False, "B": True, "O": True})
+        folders = os.listdir(self.client.storage_folder)
+        six.assertCountEqual(self, ["Other", "Bye"], folders)
+
+    def test_remove_any_package_version(self):
+        self.client.run("remove Hello/*@myuser/testing -f")
+        self._validate_remove_all_hello_packages()
+
+    def test_remove_any_package_version_channel(self):
+        self.client.run("remove Hello/*@*/testing -f")
+        self._validate_remove_all_hello_packages()
+
+    def test_remove_any_package_version_channel(self):
+        self.client.run("remove Hello/*@*/* -f")
+        self._validate_remove_all_hello_packages()
+
+    def _validate_remove_hello_1_4_10(self):
+        self.assert_folders(local_folders={"H1": None, "H2": [1, 2], "B": [1, 2], "O": [1, 2]},
+                            remote_folders={"H1": [1, 2], "H2": [1, 2], "B": [1, 2], "O": [1, 2]},
+                            build_folders={"H1": None, "H2": [1, 2], "B": [1, 2], "O": [1, 2]},
+                            src_folders={"H1": False, "H2": True, "B": True, "O": True})
+        folders = os.listdir(self.client.storage_folder)
+        six.assertCountEqual(self, ["Hello", "Other", "Bye"], folders)
+
+    def test_remove_any_package_channel(self):
+        self.client.run("remove Hello/1.4.10@*/testing -f")
+        self._validate_remove_hello_1_4_10()
+
+    def test_remove_any_package_channel(self):
+        self.client.run("remove Hello/1.4.10@myuser/* -f")
+        self._validate_remove_hello_1_4_10()
+
     def builds_test(self):
         mocked_user_io = UserIO(out=TestBufferConanOutput())
         mocked_user_io.request_boolean = Mock(return_value=True)


Code review comments:
diff_hunk: @@ -168,19 +168,22 @@ def remove(self, pattern, remote_name, src=None, build_ids=None, package_ids_fil
             else:
                 refs = self._remote_manager.search_recipes(remote, pattern)
         else:
-            if input_ref:
+            if not input_ref or (input_ref and
+                                 input_ref.version == "*" or
+                                 input_ref.channel == "*" or
+                                 input_ref.user == "*"):
path: conans/client/remover.py
text: @user1:
We can make it a little bit easier 😉 

```python

from conans.model.ref import check_valid_ref

if check_valid_ref(input_ref, allow_pattern=False):
    # Do the refs.append(input_ref)
else:
    # Do the search

```

@author:
much better! thanks!