Repository: conan-io/conan

Issue:
Error when trying to read the `conanfile.txt`
In our Jenkins-system, jenkins created a projectdirectory, that contains an "@". After we generated the conanfile.txt, we cannot run conan, cause it doesn't find the "@ folder=True" at the end of the line.

The conanfile.txt parser, splits at the first "@" and when we habe an "@" in our directory name, the parsing fails.

Example:
/home/jenkins/workspace/conan_test@2/conanfile.txt
```
[generators]
cmake

[requires]
OpenSSL/1.1.1@conan/stable

[options]
OpenSSL:shared=True
OpenSSL:no_zlib=True

[imports]
"., LICENSE -> /home/jenkins/workspace/conan_test@2/install/licenses @ folder=True"
lib, * -> /home/jenkins/workspace/conan_test@2/g/install/lib
bin, * -> /home/jenkins/workspace/conan_test@2/install/bin
include, * -> /home/jenkins/workspace/conan_test@2/install/include
```

Run conan install:
`conan install . -g cmake -s build_type=Debug -s os=Linux -s arch=x86_64 -s compiler=gcc -s compiler.version=7 -s compiler.libcxx=libstdc++11 --build=missing`

Error:
ERROR: Invalid imports line: "., LICENSE -> /home/jenkins/workspace/conan_test@2/install/licenses @ folder=True"
EX: OpenCV/lib, * -> ./lib
Invalid imports. Unknown argument 2install/licenses @ folder


Code with error:
/conans/client/loader_txt.py:
https://github.com/conan-io/conan/blob/ab15630bf0675eb2b787b4aaad8f79a53b3580f6/conans/client/loader_txt.py#L75

Possible Solution:
Split it at the last "@" if possible or ignore things, that are inside `"` or `'`.



Discussion:
Thanks for reporting.
It should be an `rsplit` and matching the right part with a regular expression of: `param=value` values separated or not with spaces.
How bad is this blocking to you?

Resolving patch:
diff --git a/conans/client/loader_txt.py b/conans/client/loader_txt.py
--- a/conans/client/loader_txt.py
+++ b/conans/client/loader_txt.py
@@ -8,7 +8,8 @@ class ConanFileTextLoader(object):
     def __init__(self, input_text):
         # Prefer composition over inheritance, the __getattr__ was breaking things
         self._config_parser = ConfigParser(input_text,  ["requires", "generators", "options",
-                                                         "imports", "build_requires"], parse_lines=True)
+                                                         "imports", "build_requires"],
+                                           parse_lines=True)
 
     @property
     def requirements(self):
@@ -33,8 +34,13 @@ def _import_parameters(self):
         def _parse_args(param_string):
             root_package, ignore_case, folder, excludes, keep_path = None, False, False, None, True
             params = param_string.split(",")
-            params = [p.split("=") for p in params if p]
-            for (var, value) in params:
+            params = [p.strip() for p in params if p.strip()]
+            for param in params:
+                try:
+                    var, value = param.split("=")
+                except ValueError:
+                    raise ConanException("Wrong imports argument '%s'. "
+                                         "Need a 'arg=value' pair." % param)
                 var = var.strip()
                 value = value.strip()
                 if var == "root_package":
@@ -52,11 +58,15 @@ def _parse_args(param_string):
             return root_package, ignore_case, folder, excludes, keep_path
 
         def _parse_import(line):
-            pair = line.split("->")
-            source = pair[0].strip().split(',', 1)
-            dest = pair[1].strip()
-            src, pattern = source[0].strip(), source[1].strip()
-            return pattern, dest, src
+            try:
+                pair = line.split("->", 1)
+                source = pair[0].strip().split(',', 1)
+                dest = pair[1].strip()
+                src, pattern = source[0].strip(), source[1].strip()
+                return pattern, dest, src
+            except Exception:
+                raise ConanException("Wrong imports line: %s\n"
+                                     "Use syntax: path, pattern -> local-folder" % line)
 
         ret = []
         local_install_text = self._config_parser.imports
@@ -72,7 +82,7 @@ def _parse_import(line):
                 raise ConanException("%s\n%s" % (invalid_line_msg,
                                                  "Import's paths can't begin with '/' or '..'"))
             try:
-                tokens = line.split("@", 1)
+                tokens = line.rsplit("@", 1)
                 if len(tokens) > 1:
                     line = tokens[0]
                     params = tokens[1]
@@ -80,7 +90,8 @@ def _parse_import(line):
                     params = ""
                 root_package, ignore_case, folder, excludes, keep_path = _parse_args(params)
                 pattern, dest, src = _parse_import(line)
-                ret.append((pattern, dest, src, root_package, folder, ignore_case, excludes, keep_path))
+                ret.append((pattern, dest, src, root_package, folder, ignore_case, excludes,
+                            keep_path))
             except Exception as e:
                 raise ConanException("%s\n%s" % (invalid_line_msg, str(e)))
         return ret


Test patch:
diff --git a/conans/test/functional/conanfile/imports_tests.py b/conans/test/functional/conanfile/imports_tests.py
--- a/conans/test/functional/conanfile/imports_tests.py
+++ b/conans/test/functional/conanfile/imports_tests.py
@@ -97,6 +97,46 @@ def imports_folders_txt_test(self):
         self.assertEqual(load(os.path.join(client.current_folder, "licenses/LibC/license.txt")),
                          "LicenseC")
 
+    def imports_wrong_args_txt_test(self):
+        client = TestClient()
+        conanfile = """
+[imports]
+., license* -> lic@myfolder @ something
+"""
+        client.save({"conanfile.txt": conanfile})
+        client.run("install .", assert_error=True)
+        self.assertIn("Wrong imports argument 'something'. Need a 'arg=value' pair.", client.out)
+
+    def imports_wrong_line_txt_test(self):
+        client = TestClient()
+        conanfile = """
+[imports]
+., license*
+"""
+        client.save({"conanfile.txt": conanfile})
+        client.run("install .", assert_error=True)
+        self.assertIn("Wrong imports line: ., license*", client.out)
+        self.assertIn("Use syntax: path, pattern -> local-folder", client.out)
+
+    def imports_folders_extrachars_txt_test(self):
+        # https://github.com/conan-io/conan/issues/4524
+        client = self._set_up()
+
+        conanfile = """[requires]
+LibC/0.1@lasote/testing
+[imports]
+., license* -> lic@myfolder @ folder=True, ignore_case=True, excludes=*.md # comment
+., *.md -> lic@myfolder @ # This is mandatory, otherwise it will fail
+"""
+        client.save({"conanfile.txt": conanfile}, clean_first=True)
+        client.run("install . --build=missing")
+        self.assertEqual(load(os.path.join(client.current_folder, "lic@myfolder/LibA/LICENSE.txt")),
+                         "LicenseA")
+        self.assertEqual(load(os.path.join(client.current_folder, "lic@myfolder/LICENSE.md")),
+                         "LicenseB")
+        self.assertEqual(load(os.path.join(client.current_folder, "lic@myfolder/LibC/license.txt")),
+                         "LicenseC")
+
     def conanfile_txt_multi_excludes_test(self):
         # https://github.com/conan-io/conan/issues/2293
         client = TestClient()
@@ -138,14 +178,14 @@ def imports(self):
         client.save({"conanfile.py": testconanfile}, clean_first=True)
         client.run("install . --build=missing")
         self.assertTrue(os.path.exists(os.path.join(client.current_folder,
-                                                     "licenses/path/to/license.txt")))
+                                                    "licenses/path/to/license.txt")))
 
         # keep_path = False AND conanfile.py
         testconanfile = testconanfile.replace("keep_path=True", "keep_path=False")
         client.save({"conanfile.py": testconanfile}, clean_first=True)
         client.run("install . --build=missing")
         self.assertTrue(os.path.exists(os.path.join(client.current_folder,
-                                                     "licenses/license.txt")))
+                                                    "licenses/license.txt")))
 
         # keep_path = True AND conanfile.txt
         testconanfile = conanfile_txt % "LibC/0.1@lasote/testing"
@@ -156,7 +196,7 @@ def imports(self):
         client.save({"conanfile.txt": testconanfile}, clean_first=True)
         client.run("install . --build=missing")
         self.assertTrue(os.path.exists(os.path.join(client.current_folder,
-                                                     "licenses/path/to/license.txt")))
+                                                    "licenses/path/to/license.txt")))
 
         # keep_path = False AND conanfile.txt
         testconanfile = testconanfile.replace("keep_path=True", "keep_path=False")