Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Git bash cannot execute shell command on login shell #219

Closed
shivapoudel opened this issue Apr 26, 2020 · 10 comments
Closed

Git bash cannot execute shell command on login shell #219

shivapoudel opened this issue Apr 26, 2020 · 10 comments
Labels

Comments

@shivapoudel
Copy link

Make sure the login shell is turned on to replicate this issue. To check use shopt login_shell and if it is turned off then use bash --login and re-check login shell status.

Not test the below install shell config accordingly.

- create:
    - ~/.wp-cli

- shell:
    - "bash -c 'curl -sL https://raw.githubusercontent.com/wp-cli/wp-cli/master/utils/wp-completion.bash -o ~/.wp-cli/wp-completion.bash'"

With a non-login shell, it's a success but with a login shell, it fails. Here is the logged error:

?[94mPath exists C:\Users\shiva/.wp-cli?[0m
?[92mAll paths have been set up?[0m
?[94mbash -c 'curl -sL https://raw.githubusercontent.com/wp-cli/wp-cli/master/utils/wp-completion.bash -o ~/.wp-cli/wp-completion.bash'?[0m
?[95mCommand [bash -c 'curl -sL https://raw.githubusercontent.com/wp-cli/wp-cli/master/utils/wp-completion.bash -o ~/.wp-cli/wp-completion.bash'] failed?[0m
?[91mSome commands were not successfully executed?[0m
?[91m
==> Some tasks were not executed successfully?[0m
@anishathalye
Copy link
Owner

anishathalye commented Apr 26, 2020

I don't currently have access to a Windows machine, so it's going to be hard for me to replicate and debug.

What happens if you use the extended form for the command, and enable stdout/stderr -- do you see a more detailed error message?

- shell:
    - command: ...
      stdout: true
      stderr: true

@shivapoudel
Copy link
Author

@anishathalye I receive this error details:

?[94mPath exists C:\Users\shiva/.wp-cli?[0m
?[92mAll paths have been set up?[0m
?[94mbash -c 'curl -sL https://raw.githubusercontent.com/wp-cli/wp-cli/master/utils/wp-completion.bash -o ~/.wp-cli/wp-completion.bash'?[0m
/c: /c: Is a directory
?[95mCommand [bash -c 'curl -sL https://raw.githubusercontent.com/wp-cli/wp-cli/master/utils/wp-completion.bash -o ~/.wp-cli/wp-completion.bash'] failed?[0m
?[91mSome commands were not successfully executed?[0m
?[91m
==> Some tasks were not executed successfully?[0m

@anishathalye
Copy link
Owner

anishathalye commented Apr 27, 2020

Huh, not exactly sure why this is happening. "/c: /c: Is a directory" is a strange error message to see. Maybe it has something to do with how the "~" is evaluated?

Does this happen if you run the command manually in a login shell in bash, with the whole bash -c ...?

@shivapoudel
Copy link
Author

Yes, it does work in the login shell with curl -L without any silent:
image

If we run without the ~/.wp-cli with curl -L without any silence. This is received:
image

@shivapoudel
Copy link
Author

shivapoudel commented Apr 27, 2020

@anishathalye Can you look in this comment? The same discussion it talked there :)

If you need any help to debug through this then let me know. I will help you with the tests in the Windows environment.

@anishathalye
Copy link
Owner

anishathalye commented Apr 30, 2020

Ok I downloaded a copy of Windows, installed Git Bash and Python, and I was able to reproduce the issue. The issue you linked is indeed related, thank you for that. I think merging #177 will fix the immediate problem, but I don't know if it'll cause collateral damage. I want to understand why the current code doesn't work.

The executable= argument was introduced in #100; the discussion there explains why we want it in certain situations. Do these situations come up when running on Windows? I am not sure. If the answer is no, then we can safely merge #177.

When run in Git Bash, Dotbot calls subprocess.call() with the executable= kwarg set to C:\Program Files\Git\usr\bin\bash.exe on my computer.

It seems like subprocess.call() with shell=True doesn't play nicely with the executable=... argument. Here's a minimal self-contained failing example:

>>> bash = 'C:\\Program Files\\Git\\usr\\bin\\bash.exe'
>>> os.path.exists(bash)
True
>>> subprocess.call('echo hi', shell=True)
hi
0
>>> subprocess.call('echo hi', shell=True, executable=bash)
/c: /c: Is a directory
126

Looking at the subprocess.py implementation and tracing it with a sys.addaudithook(), it looks like it gets to the _winapi.CreateProcess and breaks somewhere after that. I haven't had time yet to go deeper and trace execution into CreateProcess, but I wanted to write down what I've found so far. Seems like it might be a bug in Python, perhaps to do with string escaping or path parsing?

The subprocess documentation doesn't really describe the semantics of the executable= arg when combined with shell=True on Windows either. It says this for POSIX:

If shell=True, on POSIX the executable argument specifies a replacement shell for the default /bin/sh.

But it doesn't have a similar description for Windows.

@anishathalye
Copy link
Owner

anishathalye commented May 1, 2020

The relevant difference between the login shell and the non-login shell is that the login shell exports $SHELL, while the non-login shell does not (even though it has it defined). Dotbot calls subprocess.call() with executable=os.environ.get('SHELL'), so when it's not exported, it ends up doing executable=None. When executable=None, CreateProcess is called with None as the application name and the following as the command line:

 C:\\Windows\\system32\\cmd.exe /c "bash -c "echo hi""

So it runs cmd.exe with the /c ... argument, which tells it to run bash, and interpret the stuff following the -c. This is already a workaround, having to write the bash -c ... in the Dotbot install.conf.yaml. We should have just been able to write the command directly.

When executable='C:\\Program Files\\Git\\usr\\bin\\bash.exe', CreateProcess is called with C:\\Program Files\\Git\\usr\\bin\\bash.exe as the application to run, but it still has the following as the command line:

 C:\\Windows\\system32\\cmd.exe /c "bash -c "echo hi""

So we run bash, but with its argv set to ['cmd.exe, '/c', ...]. If you try running bash /c ..., you'll see the error we were originally getting:

/c: /c: Is a directory

What we really want is for subprocess.call() to understand that our shell is a POSIX-compatible one, and to respect the executable= argument but also set the argv correctly. So if we run subprocess.call('ls | wc', shell=True, executable='C:\\Program Files\\Git\\usr\\bin\\bash.exe'), we would like it to do the following:

C:\\Program Files\\Git\\usr\\bin\\bash.exe -c "ls | wc"

But instead, it does

C:\\Windows\\system32\\cmd.exe /c "ls | wc"

Bash doesn't care about its argv[0] here, and we could "fix" that by setting the COMSPEC environment variable. What is tripping it up is the /c instead of the -c. Looks like this is an issue with subprocess.py itself, and I think it's unclear how it should be fixed (how would subprocess.py know if the shell that we're calling wants /c or -c?)

On our side, we can work around it by doing something like #177 (not setting executable= on Windows), and then requiring users to do the bash -c ... thing if they want POSIX shell behavior to parse/execute their command. I don't think this will break anything, because this behavior was broken on Windows anyways.

I'm not even sure how to reliably detect Windows; e.g. does platform.system() work for Cygwin (and does this broken behavior happen on Cygwin as well?)

Update: installed Cygwin and Cygwin's Python; seems like everything works just fine there, so no workaround needed with that. platform.system() returns "CYGWIN_NT-...", so we won't confuse it in a check like platform.system() == 'Windows'

@shivapoudel
Copy link
Author

@anishathalye In windows, I tried with login shell and displays platform correctly
image

anishathalye added a commit that referenced this issue May 1, 2020
On POSIX-like systems, calling `subprocess.call()` with both
`shell=True` and `executable='...'` has the following behavior:

> If `shell=True`, on POSIX the _executable_ argument specifies a
> replacement shell for the default `/bin/sh`.

(via https://docs.python.org/3/library/subprocess.html?highlight=subprocess#popen-constructor)

This seems to have a similar behavior on Windows, but this is
problematic when a POSIX shell is substituted for cmd.exe. This is
because when `shell=True`, the shell is invoked with a '/c' argument,
which is the correct argument for cmd.exe but not for Bash, which
expects a '-c' argument instead. See here:
https://github.com/python/cpython/blob/1def7754b7a41fe57efafaf5eff24cfa15353444/Lib/subprocess.py#L1407

This is problematic when combined with Dotbot's behavior, where the
`executable` argument is set based on `$SHELL`. For example, when
running in Git Bash, the `$SHELL` environment variable is set to Bash,
so any commands run by Dotbot will fail (because it'll invoke Bash with
a '/c' argument).

This behavior of setting the `executable` argument based on `$SHELL` was
introduced in 7593d8c. This is the
desired behavior. See discussion in
#97 and
#100.

Unfortunately, this doesn't work quite right on Windows. This patch
works around the issue by avoiding setting the `executable` argument
when the platform is Windows, which is tested using
`platform.system() == 'Windows'`. This means that shell commands
executed by Dotbot on this platform will always be run using cmd.exe.
Invocations of single programs or simple commands will probably work
just fine in cmd.exe. If Bash-like behavior is desired, the user will
have to write their command as `bash -c '...'`.

This shouldn't have any implications for backwards-compatibility,
because setting the `executable` argument on Windows didn't do the right
thing anyways. Previous workarounds that users had should continue to
work with the new code.

When using Python from CYGWIN, `platform.system()` returns something
like 'CYGWIN_NT-...', so it won't be detected with the check, but this
is the correct behavior, because CYGWIN Python's `subprocess.call()` has
the POSIX-like behavior.

This patch also refactors the code to factor out the
`subprocess.call()`, which was being called in both `link.py` and
`shell.py`, so the workaround can be applied in a single place.

See the following issues/pull requests for a discussion of this bug:
- #170
- #177
- #219

An issue has also been raised in Python's issue tracker:
- https://bugs.python.org/issue40467

Thanks to @shivapoudel for originally reporting the issue, @SuJiKiNen
for debugging it and submitting a pull request, and @mohkale for
suggesting factoring out the code so that other plugins could use it.
@anishathalye
Copy link
Owner

Moving discussion to #220

@anishathalye
Copy link
Owner

Thank you for reporting this and helping with debugging. Fixed in #220.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants