Repository: conan-io/conan

Issue:
Conan not escaping quotes in bash bath with self.run(...,win_bash=True)
Using conan 1.3.0 on windows 10 with Git for Windows 2.17.0 installed to the default location "C:/Program Files/Git/bin/bash.exe", the conan build step fails. Output is below:
```
$ conan build . --source-folder=tmp/source --install-folder=tmp/install --build-folder=tmp/build
Project: Running build()
Project: run_in_windows_bash: "C:/Program Files/Git/bin/bash" --login -c ^"cd \^"/c/dev/external-libraries/conan/libusb/tmp/build\^" ^&^& ls ^"
'C:/Program' is not recognized as an internal or external command,
operable program or batch file.
ERROR: libusb/1.0.0@PROJECT: Error in build() method, line 21
        self.run("ls", win_bash=True)
        ConanException: Error 1 while executing "C:/Program Files/Git/bin/bash" --login -c ^"cd \^"/c/dev/external-libraries/conan/libusb/tmp/build\^" ^&^& ls ^"
```

My current conanfile.py follows:
```
from conans import ConanFile, AutoToolsBuildEnvironment, tools

class LibusbConan(ConanFile):
    name = "libusb"
    version = "1.0.0"
    license = "LGPL 2.1"
    url = "git@github.com:leapmotion/libusb.git"
    description = "<Description of Libusb here>"
    settings = "os", "compiler", "build_type", "arch"
    options = {"shared": [True, False]}
    default_options = "shared=True"
    generators = "cmake"

    def source(self):
        self.run("git clone git@github.com:leapmotion/libusb.git")
        self.run("cd libusb && git checkout fix-soname-0")

    def build(self):
        autotools = AutoToolsBuildEnvironment(self, win_bash=True)
        self.run("ls", win_bash=True)
        autotools.configure(args=["--disable-udev"])
        autotools.make()

    def package(self):
        self.copy("*.h", dst="include", src="hello")
        self.copy("*hello.lib", dst="lib", keep_path=False)
        self.copy("*.dll", dst="bin", keep_path=False)
        self.copy("*.so", dst="lib", keep_path=False)
        self.copy("*.dylib", dst="lib", keep_path=False)
        self.copy("*.a", dst="lib", keep_path=False)

    def package_info(self):
        self.cpp_info.libs = ["hello"]

```

To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
I have checked this, and it seems it is not a problem of the quotes, but due to the command not being ran with ``shell=True``. It is because the underlying call is ``os.system``:
- ``run_in_windows_bash`` calls ``conanfile.run(win_bash=False)``
- ``conanfile.run()`` calls ``ConanRunner`` which ends in a ``_simple_os_call``=> ``os.system``. 
- Replacing with a Popen with shell=True seems to be fixed

This doesn't seem a regression, but a pre-existing bug. Need to revise this with @lasote to see how to fix it. 

Thanks very much for your detailed report and reproducible case!

Resolving patch:
diff --git a/conans/client/runner.py b/conans/client/runner.py
--- a/conans/client/runner.py
+++ b/conans/client/runner.py
@@ -15,7 +15,7 @@ def __init__(self, print_commands_to_output=False, generate_run_log_file=False,
         self._generate_run_log_file = generate_run_log_file
         self._log_run_to_output = log_run_to_output
 
-    def __call__(self, command, output, log_filepath=None, cwd=None):
+    def __call__(self, command, output, log_filepath=None, cwd=None, subprocess=False):
         """
         @param command: Command to execute
         @param output: Instead of print to sys.stdout print to that stream. Could be None
@@ -38,7 +38,7 @@ def __call__(self, command, output, log_filepath=None, cwd=None):
             stream_output.write(call_message)
 
         # No output has to be redirected to logs or buffer or omitted
-        if output is True and not log_filepath and self._log_run_to_output:
+        if output is True and not log_filepath and self._log_run_to_output and not subprocess:
             return self._simple_os_call(command, cwd)
         elif log_filepath:
             if stream_output:
diff --git a/conans/client/tools/win.py b/conans/client/tools/win.py
--- a/conans/client/tools/win.py
+++ b/conans/client/tools/win.py
@@ -443,4 +443,5 @@ def run_in_windows_bash(conanfile, bashcmd, cwd=None, subsystem=None, msys_mingw
         bash_path = '"%s"' % bash_path if " " in bash_path else bash_path
         wincmd = '%s --login -c %s' % (bash_path, escape_windows_cmd(to_run))
         conanfile.output.info('run_in_windows_bash: %s' % wincmd)
-        return conanfile.run(wincmd, win_bash=False)
+        # https://github.com/conan-io/conan/issues/2839 (subprocess=True)
+        return conanfile._runner(wincmd, win_bash=False, subprocess=True)
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -257,8 +257,9 @@ def package_info(self):
 
     def run(self, command, output=True, cwd=None, win_bash=False, subsystem=None, msys_mingw=True):
         if not win_bash:
-            retcode = self._runner(command, output, os.path.abspath(RUN_LOG_NAME),  cwd)
+            retcode = self._runner(command, output, os.path.abspath(RUN_LOG_NAME), cwd)
         else:
+            # FIXME: run in windows bash is not using output
             retcode = tools.run_in_windows_bash(self, bashcmd=command, cwd=cwd, subsystem=subsystem,
                                                 msys_mingw=msys_mingw)
 


Test patch:
diff --git a/conans/test/util/tools_test.py b/conans/test/util/tools_test.py
--- a/conans/test/util/tools_test.py
+++ b/conans/test/util/tools_test.py
@@ -31,7 +31,7 @@ def __init__(self, return_ok=True):
         self.command_called = None
         self.return_ok = return_ok
 
-    def __call__(self, command, output, win_bash=False, subsystem=None): # @UnusedVariable
+    def __call__(self, command, output, win_bash=False, subsystem=None):  # @UnusedVariable
         self.command_called = command
         self.win_bash = win_bash
         self.subsystem = subsystem
@@ -267,26 +267,24 @@ def system_package_tool_test(self):
             spt.update()
             self.assertEquals(runner.command_called, "sudo zypper --non-interactive ref")
 
-            
             os_info.linux_distro = "redhat"
             spt = SystemPackageTool(runner=runner, os_info=os_info)
             spt.install("a_package", force=False)
             self.assertEquals(runner.command_called, "rpm -q a_package")
             spt.install("a_package", force=True)
             self.assertEquals(runner.command_called, "sudo yum install -y a_package")
-            
+
             os_info.linux_distro = "debian"
             spt = SystemPackageTool(runner=runner, os_info=os_info)
             with self.assertRaises(ConanException):
                 runner.return_ok = False
                 spt.install("a_package")
                 self.assertEquals(runner.command_called, "sudo apt-get install -y --no-install-recommends a_package")
-                
+
             runner.return_ok = True
             spt.install("a_package", force=False)
             self.assertEquals(runner.command_called, "dpkg -s a_package")
 
-            
             os_info.is_macos = True
             os_info.is_linux = False
             os_info.is_windows = False
@@ -318,7 +316,8 @@ def system_package_tool_test(self):
                 spt.install("a_package", force=True)
                 self.assertEquals(runner.command_called, "choco install --yes a_package")
                 spt.install("a_package", force=False)
-                self.assertEquals(runner.command_called, 'choco search --local-only --exact a_package | findstr /c:"1 packages installed."')
+                self.assertEquals(runner.command_called,
+                                  'choco search --local-only --exact a_package | findstr /c:"1 packages installed."')
 
         with tools.environment_append({"CONAN_SYSREQUIRES_SUDO": "False"}):
 
@@ -384,7 +383,8 @@ def system_package_tool_test(self):
                 spt.install("a_package", force=True)
                 self.assertEquals(runner.command_called, "choco install --yes a_package")
                 spt.install("a_package", force=False)
-                self.assertEquals(runner.command_called, 'choco search --local-only --exact a_package | findstr /c:"1 packages installed."')
+                self.assertEquals(runner.command_called,
+                                  'choco search --local-only --exact a_package | findstr /c:"1 packages installed."')
 
     def system_package_tool_try_multiple_test(self):
         class RunnerMultipleMock(object):
@@ -425,7 +425,7 @@ def __init__(self, expected=None):
                 self.calls = 0
                 self.expected = expected
 
-            def __call__(self, command, *args, **kwargs):
+            def __call__(self, command, *args, **kwargs):  # @UnusedVariable
                 self.calls += 1
                 return 0 if command in self.expected else 1
 
@@ -617,33 +617,35 @@ def run_in_bash_test(self):
 
         class MockConanfile(object):
             def __init__(self):
-                self.command = ""
-                self.output = namedtuple("output", "info")(lambda x: None)
+
+                self.output = namedtuple("output", "info")(lambda x: None)  # @UnusedVariable
                 self.env = {"PATH": "/path/to/somewhere"}
 
-            def run(self, command, win_bash=False):
-                self.command = command
+                class MyRun(object):
+                    def __call__(self, command, win_bash=False, subprocess=False):  # @UnusedVariable
+                        self.command = command
+                self._runner = MyRun()
 
         conanfile = MockConanfile()
         tools.run_in_windows_bash(conanfile, "a_command.bat", subsystem="cygwin")
-        self.assertIn("bash", conanfile.command)
-        self.assertIn("--login -c", conanfile.command)
-        self.assertIn("^&^& a_command.bat ^", conanfile.command)
+        self.assertIn("bash", conanfile._runner.command)
+        self.assertIn("--login -c", conanfile._runner.command)
+        self.assertIn("^&^& a_command.bat ^", conanfile._runner.command)
 
         with tools.environment_append({"CONAN_BASH_PATH": "path\\to\\mybash.exe"}):
             tools.run_in_windows_bash(conanfile, "a_command.bat", subsystem="cygwin")
-            self.assertIn('path\\to\\mybash.exe --login -c', conanfile.command)
+            self.assertIn('path\\to\\mybash.exe --login -c', conanfile._runner.command)
 
         with tools.environment_append({"CONAN_BASH_PATH": "path with spaces\\to\\mybash.exe"}):
             tools.run_in_windows_bash(conanfile, "a_command.bat", subsystem="cygwin")
-            self.assertIn('"path with spaces\\to\\mybash.exe" --login -c', conanfile.command)
+            self.assertIn('"path with spaces\\to\\mybash.exe" --login -c', conanfile._runner.command)
 
         # try to append more env vars
         conanfile = MockConanfile()
         tools.run_in_windows_bash(conanfile, "a_command.bat", subsystem="cygwin", env={"PATH": "/other/path",
                                                                                        "MYVAR": "34"})
         self.assertIn('^&^& PATH=\\^"/cygdrive/other/path:/cygdrive/path/to/somewhere:$PATH\\^" '
-                      '^&^& MYVAR=34 ^&^& a_command.bat ^', conanfile.command)
+                      '^&^& MYVAR=34 ^&^& a_command.bat ^', conanfile._runner.command)
 
     def download_retries_test(self):
         out = TestBufferConanOutput()