Repository: xonsh/xonsh

Issue:
How to properly use the `man` command?
<img width="933" alt="screen shot 2016-01-21 at 6 43 14 am" src="https://cloud.githubusercontent.com/assets/2096628/12480761/48e10632-c00a-11e5-8928-695fc2a087ba.png">



Discussion:
For comparison:

``` python
In [1]: man
What manual page do you want?
```

Same from the regular prompt:

``` bash
ssilvester@0202-ssilvester ~/workspace/xonsh_kernel upgrade-to-flit $ man
IsADirectoryError: [Errno 21] Is a directory: '/usr/texbin/man'
```

For completeness:

``` bash
ssilvester@0202-ssilvester ~/workspace/xonsh_kernel upgrade-to-flit $ which man
/usr/bin/man
```

Hmm I am not seeing this issue myself:

``` console
scopatz@localhost ~ $ man ls
scopatz@localhost ~ $ man 
What manual page do you want?
```

What OS are you on?

Ubuntu 14.04, You?

(I am on OSX 10.10.5)

Just checked my Ubuntu 14.04 VM and it works fine there.

``` bash
$ $(man ls)
IsADirectoryError: [Errno 21] Is a directory: '/usr/texbin/man'
```

``` python
In [2]: subprocess.check_output(['man', 'ls'])
Out[2]: b'\nLS(1)                     BSD General Commands Manual
```

`subprocess.check_output('man ls', shell=True)` also works.

@blink1073 What's the output of `man --path` ?

``` bash
ssilvester: ~/workspace/jupyter/services (use-system-import) 6
$ man --path
/usr/texbin/man:/Users/ssilvester/anaconda/share/man:/usr/local/share/man:/usr/share/man:/opt/X11/share/man:/Library/TeX/texbin/man:/Applications/Xcode.app/Contents/Developer/usr/share/man:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/man

ssilvester: ~/workspace/jupyter/services (use-system-import) 6
$ xonsh
ssilvester@0202-ssilvester ~/workspace/jupyter/services use-system-import $ man --path
IsADirectoryError: [Errno 21] Is a directory: '/usr/texbin/man'
ssilvester@0202-ssilvester ~/workspace/jupyter/services use-system-import $
```

Can you run that in `xonsh` again with `XONSH_SHOW_TRACEBACK=True`?

``` python
ssilvester@0202-ssilvester ~/workspace/jupyter/services use-system-import $ $XONSH_SHOW_TRACEBACK=True
ssilvester@0202-ssilvester ~/workspace/jupyter/services use-system-import $ man lsTraceback (most recent call last):
  File "/Users/ssilvester/workspace/xonsh/xonsh/base_shell.py", line 143, in default
    self.execer.exec(code, mode='single', glbs=self.ctx)  # no locals
  File "/Users/ssilvester/workspace/xonsh/xonsh/execer.py", line 126, in exec
    return exec(code, glbs, locs)
  File "<xonsh-code>", line 1, in <module>
  File "/Users/ssilvester/workspace/xonsh/xonsh/built_ins.py", line 657, in subproc_uncaptured
    return run_subproc(cmds, captured=False)
  File "/Users/ssilvester/workspace/xonsh/xonsh/built_ins.py", line 560, in run_subproc
    aliased_cmd = get_script_subproc_command(n, cmd[1:])
  File "/Users/ssilvester/workspace/xonsh/xonsh/built_ins.py", line 342, in get_script_subproc_command
    elif _is_binary(fname):
  File "/Users/ssilvester/workspace/xonsh/xonsh/built_ins.py", line 302, in _is_binary
    with open(fname, 'rb') as f:
```

This fixes it, using a function from `pexpect`:

``` diff
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -7,6 +7,7 @@ special Python builtins module.
 import os
 import re
 import sys
+import stat
 import time
 import shlex
 import atexit
@@ -281,6 +282,49 @@ def iglobpath(s, ignore_case=False):
 RE_SHEBANG = re.compile(r'#![ \t]*(.+?)$')


+def _is_executable_file(path):
+    """Checks that path is an executable regular file (or a symlink to a file).
+
+    This is roughly ``os.path isfile(path) and os.access(path, os.X_OK)``, but
+    on some platforms :func:`os.access` gives us the wrong answer, so this
+    checks permission bits directly.
+    """
+    # follow symlinks,
+    fpath = os.path.realpath(path)
+
+    # return False for non-files (directories, fifo, etc.)
+    if not os.path.isfile(fpath):
+        return False
+
+    # On Solaris, etc., "If the process has appropriate privileges, an
+    # implementation may indicate success for X_OK even if none of the
+    # execute file permission bits are set."
+    #
+    # For this reason, it is necessary to explicitly check st_mode
+
+    # get file mode using os.stat, and check if `other',
+    # that is anybody, may read and execute.
+    mode = os.stat(fpath).st_mode
+    if mode & stat.S_IROTH and mode & stat.S_IXOTH:
+        return True
+
+    # get current user's group ids, and check if `group',
+    # when matching ours, may read and execute.
+    user_gids = os.getgroups() + [os.getgid()]
+    if (os.stat(fpath).st_gid in user_gids and
+            mode & stat.S_IRGRP and mode & stat.S_IXGRP):
+        return True
+
+    # finally, if file owner matches our effective userid,
+    # check if `user', may read and execute.
+    user_gids = os.getgroups() + [os.getgid()]
+    if (os.stat(fpath).st_uid == os.geteuid() and
+            mode & stat.S_IRUSR and mode & stat.S_IXUSR):
+        return True
+
+    return False
+
+
 def _get_runnable_name(fname):
     if os.path.isfile(fname) and fname != os.path.basename(fname):
         return fname
@@ -294,7 +338,9 @@ def _get_runnable_name(fname):
                     if fname == froot and ext.upper() in PATHEXT:
                         return os.path.join(d, dirfile)
             if fname in files:
-                return os.path.join(d, fname)
+                path = os.path.join(d, fname)
+                if _is_executable_file(path):
+                    return path
     return None

```

May want to use the `which` function directly: https://github.com/pexpect/pexpect/blob/master/pexpect/utils.py#L34

Thanks for the patch @blink1073, would you mind submitting a PR for this, please?

Do you want to use a variant of `which`, or just the patch above?

The patch above, please

coming right up

Thanks a ton!


Resolving patch:
diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -7,6 +7,7 @@
 import os
 import re
 import sys
+import stat
 import time
 import shlex
 import atexit
@@ -281,6 +282,49 @@ def iglobpath(s, ignore_case=False):
 RE_SHEBANG = re.compile(r'#![ \t]*(.+?)$')
 
 
+def _is_executable_file(path):
+    """Checks that path is an executable regular file (or a symlink to a file).
+
+    This is roughly ``os.path isfile(path) and os.access(path, os.X_OK)``, but
+    on some platforms :func:`os.access` gives us the wrong answer, so this
+    checks permission bits directly.
+    """
+    # follow symlinks,
+    fpath = os.path.realpath(path)
+
+    # return False for non-files (directories, fifo, etc.)
+    if not os.path.isfile(fpath):
+        return False
+
+    # On Solaris, etc., "If the process has appropriate privileges, an
+    # implementation may indicate success for X_OK even if none of the
+    # execute file permission bits are set."
+    #
+    # For this reason, it is necessary to explicitly check st_mode
+
+    # get file mode using os.stat, and check if `other',
+    # that is anybody, may read and execute.
+    mode = os.stat(fpath).st_mode
+    if mode & stat.S_IROTH and mode & stat.S_IXOTH:
+        return True
+
+    # get current user's group ids, and check if `group',
+    # when matching ours, may read and execute.
+    user_gids = os.getgroups() + [os.getgid()]
+    if (os.stat(fpath).st_gid in user_gids and
+            mode & stat.S_IRGRP and mode & stat.S_IXGRP):
+        return True
+
+    # finally, if file owner matches our effective userid,
+    # check if `user', may read and execute.
+    user_gids = os.getgroups() + [os.getgid()]
+    if (os.stat(fpath).st_uid == os.geteuid() and
+            mode & stat.S_IRUSR and mode & stat.S_IXUSR):
+        return True
+
+    return False
+
+
 def _get_runnable_name(fname):
     if os.path.isfile(fname) and fname != os.path.basename(fname):
         return fname
@@ -294,7 +338,9 @@ def _get_runnable_name(fname):
                     if fname == froot and ext.upper() in PATHEXT:
                         return os.path.join(d, dirfile)
             if fname in files:
-                return os.path.join(d, fname)
+                path = os.path.join(d, fname)
+                if _is_executable_file(path):
+                    return path
     return None
 
 


Test patch: