Repository: xonsh/xonsh

Issue:
Surprising `mpirun -np 1 which python`
I'm trying to understand how to use xonsh. This is particularly disturbing and looks like a bug!

```
pierre@kth ~ $ python --version                                                 
Python 3.8.3
pierre@kth ~ $ mpirun -np 1 python --version                                    
Python 2.7.18rc1
pierre@kth ~ $ which python                                                     
/home/pierre/.pyenv/shims/python
pierre@kth ~ $ mpirun -np 1 which python                                        
/usr/bin/python
pierre@kth ~ $ echo $PATH                                                       
/home/pierre/miniconda3/condabin/app:/home/pierre/opt/julia-1.0.2/bin/:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin/:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
pierre@kth ~ $ mpirun -np 1 echo $PATH                                          
/home/pierre/miniconda3/condabin/app:/home/pierre/opt/julia-1.0.2/bin/:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin/:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
```

## xonfig

(xonsh from master)

<details>

```
$ xonfig
+------------------+-----------------+
| xonsh            | 0.9.18          |
| Python           | 3.8.3           |
| PLY              | 3.11            |
| have readline    | True            |
| prompt toolkit   | 3.0.5           |
| shell type       | prompt_toolkit  |
| pygments         | 2.6.1           |
| on posix         | True            |
| on linux         | True            |
| distro           | ubuntu          |
| on darwin        | False           |
| on windows       | False           |
| on cygwin        | False           |
| on msys2         | False           |
| is superuser     | False           |
| default encoding | utf-8           |
| xonsh encoding   | utf-8           |
| encoding errors  | surrogateescape |
+------------------+-----------------+
```

</details>


Discussion:
Of course,
```
pierre@kth ~ $ bash
pierre@kth:~$ mpirun -np 1 which python
/home/pierre/.pyenv/shims/python
```
Hi @paugier, I believe `mpirun -np 1 echo $PATH` does not really do what you think it does. The `$PATH` variable gets expanded **before** `mpirun` is called.
You probably wanted to run `mpirun -np 1 echo r"$PATH"`.
Hmm, thinking about it, `/bin/echo` probably cannot expand variables either, so you would probably want to run `mpirun -np 1 python -c "import os; print(os.environ['PATH'])"` in order to find out what the `PATH` value really is within `mpirun`.
Of course! Thank you @laloch 
```
pierre@gre ~ $ echo $PATH                                                       
/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
pierre@gre ~ $ mpirun -np 1 python -c "import os; print(os.environ['PATH'])"    
/usr/bin:/usr/bin:/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
```
So PATH is modified by mpirun when launched from xonsh (and not when launched from bash).

Or maybe Xonsh fails to prepare the environment for the child process properly...
Is there a difference between:
```
$ echo $PATH
$ python -c "import os; print(os.environ['PATH'])"
$ mpirun -np 1 python -c "import os; print(os.environ['PATH'])"
```
?
```
$ echo $PATH                                                       
/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
$ python -c "import os; print(os.environ['PATH'])"                 
/home/pierre/.pyenv/versions/3.8.2/bin:/home/pierre/.pyenv/libexec:/home/pierre/.pyenv/plugins/python-build/bin:/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
$ mpirun -np 1 python -c "import os; print(os.environ['PATH'])"    
/usr/bin:/usr/bin:/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
```
For the second command (path from Python), it seems that pyenv adds some paths.

I see. So it seems Xonsh is not guilty, right?
I don't know who's guilty but it is simpler without pyenv and there is still something strange:
```
$ echo $PATH                                                                                                  
/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
$ /usr/bin/python3 -c "import os; print(os.environ['PATH'])"                                                  
/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
$ mpirun -np 1 /usr/bin/python3 -c "import os; print(os.environ['PATH'])"                                     
/usr/bin:/usr/bin:/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
$ mpirun -x PATH -np 1 /usr/bin/python3 -c "import os; print(os.environ['PATH'])"                             
/usr/bin:/home/pierre/.cargo/bin:/data0/opt/miniconda3/condabin/app:/data0/opt/miniconda3/condabin:/home/pierre/.pyenv/shims:/home/pierre/.pyenv/bin:/home/pierre/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
```
Fascinating :)
So `mpirun` insists on prepending the PATH with at least one "/usr/bin". Funny.
What about `mpirun --path $PATH ...`?
`--path` does not help. And it also works with a much simpler `PATH`:

```
$ $PATH = "/usr/bin:/sbin:/bin"                                                                               
$ /usr/bin/python3 -c "import os; print(os.environ['PATH'])"                                                  
/usr/bin:/sbin:/bin
$ mpirun -np 1 /usr/bin/python3 -c "import os; print(os.environ['PATH'])"                                     
/usr/bin:/usr/bin:/usr/bin:/sbin:/bin
$ mpirun --path "$PATH" -np 1 /usr/bin/python3 -c "import os; print(os.environ['PATH'])"                      
/usr/bin:/usr/bin:/usr/bin:/sbin:/bin
$ mpirun -x PATH -np 1 /usr/bin/python3 -c "import os; print(os.environ['PATH'])"                             
/usr/bin:/usr/bin:/sbin:/bin
```
I'm seeing this odd thing in Fedora as well.

```
$ echo $PATH                                                                                                  
/usr/lib64/openmpi/bin:/home/jonas/.local/share/cargo/bin:/home/jonas/.local/bin:/home/jonas/bin:/home/jonas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
$ python3 -c "import os; print(os.environ['PATH'])"                                                           
/usr/lib64/openmpi/bin:/home/jonas/.local/share/cargo/bin:/home/jonas/.local/bin:/home/jonas/bin:/home/jonas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
$ bash -c """mpirun -n 1 python3 -c "import os; print(os.environ['PATH'])" """                                
/usr/lib64/openmpi/bin:/home/jonas/.local/share/cargo/bin:/home/jonas/.local/bin:/home/jonas/bin:/home/jonas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
$ mpirun -n 1 bash -c r"echo $PATH"                                                                           
/usr/lib64/openmpi/bin:/usr/lib64/openmpi/bin:/usr/lib64/openmpi/bin:/home/jonas/.local/share/cargo/bin:/home/jonas/.local/bin:/home/jonas/bin:/home/jonas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
$ mpirun -n 1 python3 -c "import os; print(os.environ['PATH'])"                                               
/usr/lib64/openmpi/bin:/usr/lib64/openmpi/bin:/usr/lib64/openmpi/bin:/home/jonas/.local/share/cargo/bin:/home/jonas/.local/bin:/home/jonas/bin:/home/jonas/.cargo/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
```

In my case it prepends PATH with /usr/lib64/openmpi/bin *twice*. 
Alas, I don't see how we at Xonsh could help you. I think you guys should file a bug report at Open MPI issue tracker.
Isn't it a xonsh issue that OpenMPI doesn't feel the same when it is called from xonsh than when it is called from bash or fish? The shell should not influence the result of a program, no?
```
$ cat print_path.sh                                                
#!/usr/bin/env bash
echo $PATH
$ $PATH = "/usr/bin:/sbin:/bin"                                    
$ ./print_path.sh                                                  
/usr/bin:/sbin:/bin
$ mpirun -np 1 ./print_path.sh                                     
/usr/bin:/usr/bin:/usr/bin:/sbin:/bin
$ bash                                                             
$ export PATH="/usr/bin:/sbin:/bin"
$ mpirun -np 1 ./print_path.sh
/usr/bin:/sbin:/bin
$ fish
Welcome to fish, the friendly interactive shell
~> export PATH="/usr/bin:/sbin:/bin"
~> mpirun -np 1 ./print_path.sh
/usr/bin:/sbin:/bin
```


> The shell should not influence the result of a program, no?

Yes, of course. Unless, of course, the "program" does actively something to break its own environment in dependence on the parent process, process group, controlling terminal, moon phase or other arbitrary condition we cannot assume.
Yes, I see what you mean. And it seems to be the case!

A simple workaround, use `env` before `mpirun`
```
$ mpirun -np 1 ./print_path.sh                                     
/usr/bin:/usr/bin:/usr/bin:/sbin:/bin
$ env mpirun -np 1 ./print_path.sh                                 
/usr/bin:/sbin:/bin
```
Thank you for your time @laloch 
@laloch 

Under the hood,
`mpirun -np 1 ./print_path.sh`
will result in (in `bash`)

`execve ("/usr/bin/mpirun", ["mpirun", "-np", "1", "./print_path.sh"], [...])`

but in `xonsh`

`execve ("/usr/bin/mpirun", ["/usr/bin/mpirun", "-np", "1", "./print_path.sh"], [...])`

Open MPI will then "do its thing" (e.g. alter `$PATH`) depending on whether `argv[0]` is `mpirun` or `/usr/bin/mpirun` (and no other esoteric conditions :-))

Bottom line, `xonsh` does not pass to `execve()` in `argv[0]` the command as provided by the user (e.g. `mpirun`), but its full path (e.g. `/usr/bin/mpirun`). Is this a bug or a feature? I think the former, but this is clearly not my call.
@ggouaillardet, thanks for investigating the issue.
Looking at the `execve` documentation I'd say you are right - It's a bug ... in `bash`. But if this misuse of `execve` arguments is a common practice, we can adjust. Unless it's a feature of Python libraries.
FWIW, `man execve` on OSX says

> The argument argv is a pointer to a null-terminated array of character pointers to null-terminated character strings.  [...] by custom, the first element should be the name of the executed program (for example, the last component of path).

My understanding is that the "last component" of `/usr/bin/mpirun` is `mpirun`, so not only `bash` is correct but it also honors the custom.
Interesting. Linux (man 2 execve) documentation says:
> By convention, the first of these strings (i.e., argv[0]) should contain the filename associated with the file being executed.

"filename" for me is a name used to **uniquely** idetify a file in a file system.
To me, `filename` is very ambiguous and does not suggest "name of the file" (e.g. `mpirun`) or the full path (and btw, with the use of symbolic links, several paths can point to the very same inode (`mpirun` here)).
So @ggouaillardet, you basically say that the following commands would lead to execution in different environments, right? That sounds really weird.
```
$ which mpirun
/usr/bin/mpirun
$ cd /usr
$ mpirun
$ bin/mpirun
$ /usr/bin/mpirun
```
under bash and with a default install, `mpirun` and `/usr/bin/mpirun` will indeed have the app executed with a different `$PATH`.
`mpirun` and `bin/mpirun` will lead to the same environment w.r.t. `$PATH` 
The Linux documentation uses both `pathname` (first argument of `execve`) and `filename` (content of `argv[0]`).

I also think it is weird to modify PATH depending on how the program is called. But I guess it is useful in some cases for OpenMPI.

Anyway, in terms of how the programs should be called, shouldn't xonsh have to follow bash even if bash is wrong?
> Anyway, in terms of how the programs should be called, shouldn't xonsh have to follow bash even if bash is wrong?

Yes, definitely. Regardless of what I think about the issue, Xonsh should definitely behave consistently with sh/bash. PR incoming.

Resolving patch:
diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -206,8 +206,9 @@ def _un_shebang(x):
 
 def get_script_subproc_command(fname, args):
     """Given the name of a script outside the path, returns a list representing
-    an appropriate subprocess command to execute the script.  Raises
-    PermissionError if the script is not executable.
+    an appropriate subprocess command to execute the script or None if
+    the argument is not readable or not a script. Raises PermissionError
+    if the script is not executable.
     """
     # make sure file is executable
     if not os.access(fname, os.X_OK):
@@ -217,10 +218,10 @@ def get_script_subproc_command(fname, args):
         # execute permissions but not read/write permissions. This enables
         # things with the SUID set to be run. Needs to come before _is_binary()
         # is called, because that function tries to read the file.
-        return [fname] + args
+        return None
     elif _is_binary(fname):
         # if the file is a binary, we should call it directly
-        return [fname] + args
+        return None
     if ON_WINDOWS:
         # Windows can execute various filetypes directly
         # as given in PATHEXT
@@ -730,7 +731,9 @@ def resolve_executable_commands(self):
         if self.binary_loc is None:
             return
         try:
-            self.cmd = get_script_subproc_command(self.binary_loc, self.cmd[1:])
+            scriptcmd = get_script_subproc_command(self.binary_loc, self.cmd[1:])
+            if scriptcmd is not None:
+                self.cmd = scriptcmd
         except PermissionError:
             e = "xonsh: subprocess mode: permission denied: {0}"
             raise XonshError(e.format(self.cmd[0]))


Test patch:
diff --git a/tests/bin/checkargv0.xsh b/tests/bin/checkargv0.xsh
new file mode 100755
--- /dev/null
+++ b/tests/bin/checkargv0.xsh
@@ -0,0 +1,25 @@
+#!/usr/bin/env xonsh
+res = True
+script = """import os
+path = "/proc/{}/cmdline".format(os.getpid())
+with open(path, "r") as f:
+    lines = f.read().split("\x00")
+    print(lines[0])"""
+
+def check(python):
+    argv0 = $(@(python) -c @(script)).rstrip()
+
+    if argv0 == python:
+        return True
+
+    print("Unexpected argv[0]: {} != {}".format(argv0, python))
+    return False
+
+
+python = "python"
+res &= check(python)
+
+python = $(which -s python)
+res &= check(python)
+if res:
+    print("OK")
diff --git a/tests/test_integrations.py b/tests/test_integrations.py
--- a/tests/test_integrations.py
+++ b/tests/test_integrations.py
@@ -10,7 +10,9 @@
 from xonsh.lib.os import indir
 
 from tools import (
+    skip_if_on_msys,
     skip_if_on_windows,
+    skip_if_on_darwin,
     ON_WINDOWS,
     ON_DARWIN,
     ON_TRAVIS,
@@ -681,3 +683,10 @@ def _echo(args):
 def test_single_command_return_code(cmd, exp_rtn):
     _, _, rtn = run_xonsh(cmd, single_command=True)
     assert rtn == exp_rtn
+
+
+@skip_if_on_msys
+@skip_if_on_windows
+@skip_if_on_darwin
+def test_argv0():
+    check_run_xonsh("checkargv0.xsh", None, "OK\n")