Repository: xonsh/xonsh

Issue:
windows: shebang (#!) scripts don't work
On Windows, running shebang scripts from xonsh generally doesn't work.  My preliminary investigation shows that _is_runnable_name() may be too restrictive and preventing the shebang-checking code from ever executing.  On Linux this is no big deal since Linux intrinsically can run shebang lines.  On Windows, this needs to be fixed.

I think a good answer here is for _is_runnable_name() to do an effective 'which' command to see if anything on the PATH may be runnable.  

For increased compatibility with msysGit, cygwin, and other unix-compatibility schemes on Windows, I think '#!' handling on Windows should additionally allow unix-style pathnames.  A simple way to do this is to check for a '#!' path starting with '/' and just take the basename of the path, relying on PATH lookup to find the right thing to execute.  Many '#!' scripts use '/usr/bin/env' anyway to perform a similar lookup of the correct file on the PATH.

Does this seem like a reasonable direction to go?



Discussion:
> I think a good answer here is for _is_runnable_name() to do an effective 'which' command to see if anything on the PATH may be runnable. 

When I wrote `_is_runnable_name`, it was only intended for running when invoking an executable by specifying its full name.  So I think it is important not to lose that, but extending it to also check whether the argument is an executable in the PATH seems like it should be fine.

> A simple way to do this is to check for a '#!' path starting with '/' and just take the basename of the path, relying on PATH lookup to find the right thing to execute. 
> Does this seem like a reasonable direction to go?

I'm no Windows expert, but I think that sounds pretty reasonable (though of course we don't want to do that on *nix systems).

Do you want to tackle these, or should I?  I might have some time this afternoon.

@wrywerytwreywery it'll be a couple days until I could get back to this, so you're welcome to take it.  I think it's the highest priority issue on the Windows port.


Resolving patch:
diff --git a/xonsh/base_shell.py b/xonsh/base_shell.py
--- a/xonsh/base_shell.py
+++ b/xonsh/base_shell.py
@@ -1,4 +1,5 @@
 """The base class for xonsh shell"""
+import os
 import sys
 import builtins
 import traceback
diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -204,8 +204,13 @@ def iglobpath(s):
 RE_SHEBANG = re.compile(r'#![ \t]*(.+?)$')
 
 
-def _is_runnable_name(fname):
-    return os.path.isfile(fname) and fname != os.path.basename(fname)
+def _get_runnable_name(fname):
+    if os.path.isfile(fname) and fname != os.path.basename(fname):
+        return fname
+    for d in builtins.__xonsh_env__['PATH']:
+        if os.path.isdir(d) and fname in os.listdir(d):
+            return os.path.join(d, fname)
+    return None
 
 
 def _is_binary(fname, limit=80):
@@ -221,6 +226,18 @@ def _is_binary(fname, limit=80):
     return False
 
 
+def _un_shebang(x):
+    if x == '/usr/bin/env':
+        return []
+    elif any(x.startswith(i) for i in ['/usr/bin', '/usr/local/bin', '/bin']):
+        x = os.path.basename(x)
+    elif x.endswith('python') or x.endswith('python.exe'):
+        x = 'python'
+    if x == 'xonsh':
+        return ['python', '-m', 'xonsh.main']
+    return [x]
+
+
 def get_script_subproc_command(fname, args):
     """
     Given the name of a script outside the path, returns a list representing
@@ -250,6 +267,12 @@ def get_script_subproc_command(fname, args):
         else:
             interp = ['xonsh']
 
+    if ON_WINDOWS:
+        o = []
+        for i in interp:
+            o.extend(_un_shebang(i))
+        interp = o
+
     return interp + [fname] + args
 
 
@@ -301,9 +324,10 @@ def run_subproc(cmds, captured=True):
         stdout = last_stdout if cmd is last_cmd else PIPE
         uninew = cmd is last_cmd
         alias = builtins.aliases.get(cmd[0], None)
-        if _is_runnable_name(cmd[0]):
+        n = _get_runnable_name(cmd[0])
+        if n is not None:
             try:
-                aliased_cmd = get_script_subproc_command(cmd[0], cmd[1:])
+                aliased_cmd = get_script_subproc_command(n, cmd[1:])
             except PermissionError:
                 e = 'xonsh: subprocess mode: permission denied: {0}'
                 raise XonshError(e.format(cmd[0]))


Test patch: