Repository: conan-io/conan

Issue:
wide-open version range on command-line now misinterpreted as path
In the provisioning scripts to create jenkins slaves, I have several commands of the style

`conan install name/[*]@user/channel --profile foo` which are intended to pre-download the latest available version of a package to be baked in to the VM disk image, so that it will not have to be re-extracted by each job (the compute resources are launched ephemerally).

For a few of these packages, there is also some action in the deploy() method (e.g. registering with the CMake package registry to bootstrap find_package(cmake-conan).

I just updated these provisioning scripts to conan 1.19, and such commands seem to no longer work; they are mis-interpreted as file paths, and now fail with 

> ERROR: Conanfile not found at ERROR: Conanfile not found at $PWD/name/[*]@user/channel

Instead of the expected 

> ERROR: Version range '*' from requirement 'package/[*]@user/channel' required by 'virtual' could not be resolved in local cache

(or of course success, if I had used a real name/user/channel instead of bogus placeholders).  But the error that this is being treated as a file paths rather than a references is visible even with no real package. However, this is not happening with version ranges in general, just '*': otherwise-equivalent forms like `conan install package/[x]@user/channel --profile foo` or `conan install package/[>=0]@user/channel --profile foo` seem to work fine.

searching among the releases on pypi, this seems to have broken between conan 1.17.2 and 1.18.0, so I suspect some change to parsing with the changes that made user/channel optional.

It's not a big deal to just change these scripts to use `[x]` instead of `[*]`, but it seems likely to have been unintentional (if it was on purpose, maybe https://docs.conan.io/en/latest/versioning/version_ranges.html should warn against the use of '*' in semver ranges?).

I see the same behavior on both windows 10 (python 3.7.4) and linux (ubuntu bionic, python 3.6.8).


Discussion:

Resolving patch:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -1204,7 +1204,7 @@ def search(self, *args):
             raise ConanException("'--table' argument cannot be used together with '--json'")
 
         # Searching foo/bar is considered a pattern (FIXME: 2.0) so use strict mode to disambiguate
-        is_reference = check_valid_ref(args.pattern_or_reference, strict_mode=True)
+        is_reference = check_valid_ref(args.pattern_or_reference)
 
         if is_reference:
             ref = ConanFileReference.loads(args.pattern_or_reference)
diff --git a/conans/client/remover.py b/conans/client/remover.py
--- a/conans/client/remover.py
+++ b/conans/client/remover.py
@@ -143,7 +143,7 @@ def remove(self, pattern, remote_name, src=None, build_ids=None, package_ids_fil
         if remote_name and (build_ids is not None or src):
             raise ConanException("Remotes don't have 'build' or 'src' folder, just packages")
 
-        is_reference = check_valid_ref(pattern, strict_mode=True)
+        is_reference = check_valid_ref(pattern)
         input_ref = ConanFileReference.loads(pattern) if is_reference else None
 
         if not input_ref and packages_query is not None:
diff --git a/conans/model/ref.py b/conans/model/ref.py
--- a/conans/model/ref.py
+++ b/conans/model/ref.py
@@ -69,15 +69,21 @@ def get_reference_fields(arg_reference, user_channel_input=False):
 
 def check_valid_ref(reference, strict_mode=True):
     """
+    :param reference: string to be analyzed if it is a reference or not
     :param strict_mode: Only if the reference contains the "@" is valid, used to disambiguate"""
     try:
-
         if not reference:
             return False
-        if strict_mode and "@" not in reference:
-            return False
-        if strict_mode and "*" in reference:
-            return False
+        if strict_mode:
+            if "@" not in reference:
+                return False
+            if "*" in reference:
+                ref = ConanFileReference.loads(reference, validate=True)
+                if "*" in ref.name or "*" in ref.user or "*" in ref.channel:
+                    return False
+                if str(ref.version).startswith("["):  # It is a version range
+                    return True
+                return False
         ConanFileReference.loads(reference, validate=True)
         return True
     except ConanException:


Test patch:
diff --git a/conans/test/functional/command/install_test.py b/conans/test/functional/command/install_test.py
--- a/conans/test/functional/command/install_test.py
+++ b/conans/test/functional/command/install_test.py
@@ -661,3 +661,13 @@ def install_skip_disabled_remote_test(self):
         client.run("remote disable default")
         client.run("install Pkg/0.1@lasote/testing", assert_error=False)
         self.assertNotIn("Trying with 'default'...", client.out)
+
+    def install_version_range_reference_test(self):
+        # https://github.com/conan-io/conan/issues/5905
+        client = TestClient()
+        client.save({"conanfile.py": GenConanfile()})
+        client.run("create . pkg/0.1@user/channel")
+        client.run("install pkg/[*]@user/channel")
+        self.assertIn("pkg/0.1@user/channel from local cache - Cache", client.out)
+        client.run("install pkg/[0.*]@user/channel")
+        self.assertIn("pkg/0.1@user/channel from local cache - Cache", client.out)
diff --git a/conans/test/unittests/model/ref_test.py b/conans/test/unittests/model/ref_test.py
--- a/conans/test/unittests/model/ref_test.py
+++ b/conans/test/unittests/model/ref_test.py
@@ -148,6 +148,24 @@ class CheckValidRefTest(unittest.TestCase):
     def test_string(self):
         self.assertTrue(check_valid_ref("package/1.0@user/channel"))
         self.assertTrue(check_valid_ref("package/1.0@user/channel"))
+        self.assertTrue(check_valid_ref("package/[*]@user/channel"))
+        self.assertTrue(check_valid_ref("package/[>1.0]@user/channel"))
+        self.assertTrue(check_valid_ref("package/[1.*]@user/channel"))
+
+        # Patterns are invalid
+        self.assertFalse(check_valid_ref("package/*@user/channel"))
+        self.assertFalse(check_valid_ref("package/1.0@user/*"))
+        self.assertFalse(check_valid_ref("package/1.0@user/chan*"))
+        self.assertFalse(check_valid_ref("package/[>1.0]@user/chan*"))
+        self.assertFalse(check_valid_ref("*/1.0@user/channel"))
+        self.assertFalse(check_valid_ref("package*/1.0@user/channel"))
+
+        # * pattern is valid in non stric_mode
+        self.assertTrue(check_valid_ref("package/*@user/channel", strict_mode=False))
+        self.assertTrue(check_valid_ref("package/*@user/*", strict_mode=False))
+
+        # But other patterns are not valid in non stric_mode
+        self.assertFalse(check_valid_ref("package/1.0@user/chan*", strict_mode=False))
 
     def test_incomplete_refs(self):
         self.assertTrue(check_valid_ref("package/1.0", strict_mode=False))