Repository: conan-io/conan

Issue:
[bug] Path expansion in virtualrunenv generator adds extra colon for empty path
### Environment Details
  * Operating System+version: Debian GNU/Linux 10
  * Conan version: 1.34.0-dev
  * Python version: 3.7.0
  * Shell: GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu)

### Steps to reproduce (Include if Applicable)
~~~bash
export LD_LIBRARY_PATH=
# install a recipe with "-g virtualrunenv"
source activate.sh
echo $LD_LIBRARY_PATH
~~~
The last echo prints path with extra ":" at the end.

### Explanation

According to [Bash expansion](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html):
> **${parameter:+word}**
> If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.

Conan uses:
~~~bash
PATH="..."${PATH+:$PATH}
~~~

Using `${var+:$var}`:
~~~bash
export DEFINED_EMPTY_VAR=
echo "xxx"${DEFINED_EMPTY_VAR+:$DEFINED_EMPTY_VAR}
echo "xxx"${UNDEFINED_VAR+:$UNDEFINED_VAR}
~~~
Results
~~~
xxx:
xxx
~~~

Correct form `${var:+:$var}`:
~~~bash
export DEFINED_EMPTY_VAR=
echo "xxx"${DEFINED_EMPTY_VAR:+:$DEFINED_EMPTY_VAR}
echo "xxx"${UNDEFINED_VAR:+:$UNDEFINED_VAR}
~~~
Results
~~~
xxx
xxx
~~~


Discussion:

Resolving patch:
diff --git a/conans/client/envvars/environment.py b/conans/client/envvars/environment.py
--- a/conans/client/envvars/environment.py
+++ b/conans/client/envvars/environment.py
@@ -117,7 +117,7 @@ def _variable_placeholder(flavor, name, append_with_spaces):
     if flavor == PS1_FLAVOR:
         return "$env:%s" % name
     # flavor == sh
-    return "${%s+ $%s}" % (name, name) if append_with_spaces else "${%s+:$%s}" % (name,  name)
+    return "${%s:+ $%s}" % (name, name) if append_with_spaces else "${%s:+:$%s}" % (name,  name)
 
 
 def _format_values(flavor, variables, append_with_spaces):
@@ -143,7 +143,10 @@ def _format_values(flavor, variables, append_with_spaces):
             placeholder = _variable_placeholder(flavor, name, append_space)
             if append_space:
                 # Variables joined with spaces look like: CPPFLAGS="one two three"
-                value = " ".join(value+[placeholder])
+                if flavor == SH_FLAVOR:
+                    value = " ".join(value) + placeholder
+                else:
+                    value = " ".join(value + [placeholder])
                 value = "\"%s\"" % value if quote_elements else value
             else:
                 # Quoted variables joined with pathset may look like:


Test patch:
diff --git a/conans/test/functional/configuration/profile_test.py b/conans/test/functional/configuration/profile_test.py
--- a/conans/test/functional/configuration/profile_test.py
+++ b/conans/test/functional/configuration/profile_test.py
@@ -58,7 +58,7 @@ def test_profile_conanfile_txt(self):
         self.client.run("install . -pr envs -g virtualenv")
         content = self.client.load("environment.sh.env")
         self.assertIn(":".join(["PREPEND_VAR=\"new_path\"", "\"other_path\""]) +
-                      "${PREPEND_VAR+:$PREPEND_VAR}", content)
+                      "${PREPEND_VAR:+:$PREPEND_VAR}", content)
         if platform.system() == "Windows":
             content = self.client.load("environment.bat.env")
             self.assertIn(";".join(["PREPEND_VAR=new_path", "other_path", "%PREPEND_VAR%"]),
diff --git a/conans/test/functional/environment/apply_environment_test.py b/conans/test/functional/environment/apply_environment_test.py
--- a/conans/test/functional/environment/apply_environment_test.py
+++ b/conans/test/functional/environment/apply_environment_test.py
@@ -439,8 +439,8 @@ def package_info(self):
         if platform.system() == "Windows":
             self.assertIn('var2=value3;value2;%var2%', environment_contents)
         else:
-            self.assertIn('var2="value3":"value2"${var2+:$var2}', environment_contents)
-            self.assertIn('CPPFLAGS="OtherFlag=2 MYCPPFLAG=1 ${CPPFLAGS+ $CPPFLAGS}"',
+            self.assertIn('var2="value3":"value2"${var2:+:$var2}', environment_contents)
+            self.assertIn('CPPFLAGS="OtherFlag=2 MYCPPFLAG=1${CPPFLAGS:+ $CPPFLAGS}"',
                           environment_contents)
         self.assertIn("Another value", environment_contents)
         if platform.system() == "Windows":
@@ -675,7 +675,7 @@ def package_info(self):
 PATH=["path_from_B"]""", info)
         if platform.system() != "Windows":
             activate = client.load("environment.sh.env")
-            self.assertIn('PATH="path_from_A":"path_from_B"${PATH+:$PATH}', activate)
+            self.assertIn('PATH="path_from_A":"path_from_B"${PATH:+:$PATH}', activate)
         else:
             activate = client.load("environment.bat.env")
             self.assertIn('PATH=path_from_A;path_from_B;%PATH%', activate)
diff --git a/conans/test/functional/generators/virtualenv_test.py b/conans/test/functional/generators/virtualenv_test.py
--- a/conans/test/functional/generators/virtualenv_test.py
+++ b/conans/test/functional/generators/virtualenv_test.py
@@ -221,26 +221,24 @@ def test_list_with_spaces(self):
 
         _, environment = self._run_virtualenv(generator)
 
-        self.assertEqual(environment["CFLAGS"], "-O2 ")  # FIXME: Trailing blank
-        self.assertEqual(environment["CL"], "-MD -DNDEBUG -O2 -Ob2 ")  # FIXME: Trailing blank
+        extra_blank = " " if platform.system() == "Windows" else ""  # FIXME: Extra blank under Windows
+        self.assertEqual(environment["CFLAGS"], "-O2" + extra_blank)
+        self.assertEqual(environment["CL"], "-MD -DNDEBUG -O2 -Ob2" + extra_blank)
 
         with environment_append({"CFLAGS": "cflags", "CL": "cl"}):
             _, environment = self._run_virtualenv(generator)
-            extra_blank = " " if platform.system() != "Windows" else ""  # FIXME: Extra blank
-            self.assertEqual(environment["CFLAGS"], "-O2 {}cflags".format(extra_blank))
-            self.assertEqual(environment["CL"], "-MD -DNDEBUG -O2 -Ob2 {}cl".format(extra_blank))
+            self.assertEqual(environment["CFLAGS"], "-O2 cflags")
+            self.assertEqual(environment["CL"], "-MD -DNDEBUG -O2 -Ob2 cl")
 
     @pytest.mark.tool_conan
     def test_list_variable(self):
-        self.assertNotIn("WHATEVER", os.environ)
         self.assertIn("PATH", os.environ)
         existing_path = os.environ.get("PATH")
         # Avoid duplicates in the path
         existing_path = os.pathsep.join(OrderedDict.fromkeys(existing_path.split(os.pathsep)))
 
         generator = VirtualEnvGenerator(ConanFileMock())
-        generator.env = {"PATH": [os.path.join(self.test_folder, "bin"), r'other\path'],
-                         "WHATEVER": ["list", "other"]}
+        generator.env = {"PATH": [os.path.join(self.test_folder, "bin"), r'other\path']}
 
         _, environment = self._run_virtualenv(generator)
 
@@ -250,11 +248,38 @@ def test_list_variable(self):
             self.ori_path,
             existing_path
         ]))
+
+    @pytest.mark.tool_conan
+    def test_empty_undefined_list_variable(self):
+        self.assertNotIn("WHATEVER", os.environ)
+
+        generator = VirtualEnvGenerator(ConanFileMock())
+        generator.env = {"WHATEVER": ["list", "other"]}
+
+        _, environment = self._run_virtualenv(generator)
+
         # FIXME: extra separator in Windows
         extra_separator = os.pathsep if platform.system() == "Windows" else ""
         self.assertEqual(environment["WHATEVER"],
                          "{}{}{}{}".format("list", os.pathsep, "other", extra_separator))
 
+    @pytest.mark.tool_conan
+    @pytest.mark.skipif(platform.system() == "Windows",
+                        reason="Windows doesn't make distinction between empty and undefined environment variables")
+    def test_empty_defined_list_variable(self):
+        self.assertNotIn("WHATEVER", os.environ)
+        try:
+            os.environ["WHATEVER"] = ""
+
+            generator = VirtualEnvGenerator(ConanFileMock())
+            generator.env = {"WHATEVER": ["list", "other"]}
+
+            _, environment = self._run_virtualenv(generator)
+
+            self.assertEqual(environment["WHATEVER"], "{}{}{}".format("list", os.pathsep, "other"))
+        finally:
+            del os.environ["WHATEVER"]
+
     @pytest.mark.tool_conan
     def test_find_program(self):
         # If we add the path, we should found the env/executable instead of ori/executable
diff --git a/conans/test/functional/generators/virtualenv_windows_bash_test.py b/conans/test/functional/generators/virtualenv_windows_bash_test.py
--- a/conans/test/functional/generators/virtualenv_windows_bash_test.py
+++ b/conans/test/functional/generators/virtualenv_windows_bash_test.py
@@ -116,4 +116,4 @@ def test_git_shell(self):
         self.assertEqual(environment["WHATEVER2"], "list:existing_value")
 
         # Variable: list with spaces
-        self.assertEqual(environment["CFLAGS"], "cflags1 cflags2  existing_value")  # FIXME: extra blank
+        self.assertEqual(environment["CFLAGS"], "cflags1 cflags2 existing_value")
diff --git a/conans/test/integration/generators/virtualenv_python_test.py b/conans/test/integration/generators/virtualenv_python_test.py
--- a/conans/test/integration/generators/virtualenv_python_test.py
+++ b/conans/test/integration/generators/virtualenv_python_test.py
@@ -39,7 +39,7 @@ def package_info(self):
 
         if platform.system() != "Windows":
             contents = client.load("environment_run_python.sh.env")
-            self.assertIn('PYTHONPATH="/path/to/something"${PYTHONPATH+:$PYTHONPATH}', contents)
+            self.assertIn('PYTHONPATH="/path/to/something"${PYTHONPATH:+:$PYTHONPATH}', contents)
         else:
             contents = client.load("environment_run_python.bat.env")
             self.assertIn('PYTHONPATH=/path/to/something;%PYTHONPATH%', contents)
@@ -76,7 +76,7 @@ def package_info(self):
             if platform.system() != "Windows":
                 contents = client.load("environment_run_python.sh.env")
                 self.assertIn('PYTHONPATH="/path/to/something":"/otherpath"'
-                              '${PYTHONPATH+:$PYTHONPATH}', contents)
+                              '${PYTHONPATH:+:$PYTHONPATH}', contents)
             else:
                 contents = client.load("environment_run_python.bat.env")
                 self.assertIn('PYTHONPATH=/path/to/something;/otherpath;%PYTHONPATH%', contents)
diff --git a/conans/test/unittests/client/generators/virtualbuildenv_test.py b/conans/test/unittests/client/generators/virtualbuildenv_test.py
--- a/conans/test/unittests/client/generators/virtualbuildenv_test.py
+++ b/conans/test/unittests/client/generators/virtualbuildenv_test.py
@@ -40,11 +40,11 @@ def test_environment(self):
 
     def test_scripts(self):
         content = self.result["environment_build.sh.env"]
-        self.assertIn('CPPFLAGS="-DNDEBUG ${CPPFLAGS+ $CPPFLAGS}"', content)
-        self.assertIn('CXXFLAGS="-O3 -s --sysroot=/path/to/sysroot ${CXXFLAGS+ $CXXFLAGS}"', content)
-        self.assertIn('CFLAGS="-O3 -s --sysroot=/path/to/sysroot ${CFLAGS+ $CFLAGS}"', content)
-        self.assertIn('LDFLAGS="--sysroot=/path/to/sysroot ${LDFLAGS+ $LDFLAGS}"', content)
-        self.assertIn('LIBS="${LIBS+ $LIBS}"', content)
+        self.assertIn('CPPFLAGS="-DNDEBUG${CPPFLAGS:+ $CPPFLAGS}"', content)
+        self.assertIn('CXXFLAGS="-O3 -s --sysroot=/path/to/sysroot${CXXFLAGS:+ $CXXFLAGS}"', content)
+        self.assertIn('CFLAGS="-O3 -s --sysroot=/path/to/sysroot${CFLAGS:+ $CFLAGS}"', content)
+        self.assertIn('LDFLAGS="--sysroot=/path/to/sysroot${LDFLAGS:+ $LDFLAGS}"', content)
+        self.assertIn('LIBS="${LIBS:+ $LIBS}"', content)
 
         content = self.result["environment_build.ps1.env"]
         self.assertIn('CPPFLAGS=-DNDEBUG $env:CPPFLAGS', content)
diff --git a/conans/test/unittests/client/generators/virtualenv_python_test.py b/conans/test/unittests/client/generators/virtualenv_python_test.py
--- a/conans/test/unittests/client/generators/virtualenv_python_test.py
+++ b/conans/test/unittests/client/generators/virtualenv_python_test.py
@@ -22,6 +22,6 @@ def test_pythonpath(self):
         gen.output_path = "not-used"
         content = gen.content
 
-        self.assertIn('PYTHONPATH="1":"2":"three":"DepAPath":"DepBPath"${PYTHONPATH+:$PYTHONPATH}',
+        self.assertIn('PYTHONPATH="1":"2":"three":"DepAPath":"DepBPath"${PYTHONPATH:+:$PYTHONPATH}',
                       content["environment_run_python.sh.env"])
 
diff --git a/conans/test/unittests/client/generators/virtualenv_test.py b/conans/test/unittests/client/generators/virtualenv_test.py
--- a/conans/test/unittests/client/generators/virtualenv_test.py
+++ b/conans/test/unittests/client/generators/virtualenv_test.py
@@ -45,9 +45,9 @@ def test_variable(self):
         self.assertIn("USER_FLAG=\"user_value\"", self.result['environment.sh.env'])
 
     def test_list_variable(self):
-        self.assertIn("PATH=\"another_path\"${PATH+:$PATH}", self.result['environment.sh.env'])
-        self.assertIn("PATH2=\"p1\":\"p2\"${PATH2+:$PATH2}", self.result['environment.sh.env'])
-        self.assertIn("PATH3=\"p1\":\"p2\":\"p3\":\"p4\"${PATH3+:$PATH3}",
+        self.assertIn("PATH=\"another_path\"${PATH:+:$PATH}", self.result['environment.sh.env'])
+        self.assertIn("PATH2=\"p1\":\"p2\"${PATH2:+:$PATH2}", self.result['environment.sh.env'])
+        self.assertIn("PATH3=\"p1\":\"p2\":\"p3\":\"p4\"${PATH3:+:$PATH3}",
                       self.result['environment.sh.env'])
 
         if platform.system() == "Windows":
@@ -62,7 +62,7 @@ def test_list_variable(self):
 
     def test_list_with_spaces(self):
         self.assertIn("CL", VirtualEnvGenerator.append_with_spaces)
-        self.assertIn("CL=\"cl1 cl2 ${CL+ $CL}\"", self.result['environment.sh.env'])
+        self.assertIn("CL=\"cl1 cl2${CL:+ $CL}\"", self.result['environment.sh.env'])
         self.assertIn('CL=cl1 cl2 $env:CL', self.result["environment.ps1.env"])
 
         if platform.system() == "Windows":
diff --git a/conans/test/unittests/client/generators/virtualrunenv_test.py b/conans/test/unittests/client/generators/virtualrunenv_test.py
--- a/conans/test/unittests/client/generators/virtualrunenv_test.py
+++ b/conans/test/unittests/client/generators/virtualrunenv_test.py
@@ -41,10 +41,10 @@ def test_environment(self):
 
     def test_scripts(self):
         content = self.result[self.environment_sh_env]
-        self.assertIn('DYLD_LIBRARY_PATH="lib1":"lib2"${DYLD_LIBRARY_PATH+:$DYLD_LIBRARY_PATH}',
+        self.assertIn('DYLD_LIBRARY_PATH="lib1":"lib2"${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}',
                       content)
-        self.assertIn('LD_LIBRARY_PATH="lib1":"lib2"${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}', content)
-        self.assertIn('PATH="bin1":"bin2"${PATH+:$PATH}', content)
+        self.assertIn('LD_LIBRARY_PATH="lib1":"lib2"${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}', content)
+        self.assertIn('PATH="bin1":"bin2"${PATH:+:$PATH}', content)
 
         if platform.system() == "Windows":
             content = self.result[self.environment_bat_env]