Repository: xonsh/xonsh

Issue:
prompt: error: on field 'curr_branch' on running xonsh
Hey, I wanted to try out xonsh and installed it using homebrew, but I see this error every time I get into a `xonsh` shell. It looks like something related to getting the current git branch. 

```
~  xonsh
prompt: error: on field 'curr_branch'
xonsh: To log full traceback to a file set: $XONSH_TRACEBACK_LOGFILE = <filename>
Traceback (most recent call last):
  File "/usr/local/Cellar/xonsh/0.5.8/libexec/lib/python3.6/site-packages/xonsh/prompt/__amalgam__.py", line 671, in _get_field_value
    value = field_value() if callable(field_value) else field_value
  File "/usr/local/Cellar/xonsh/0.5.8/libexec/lib/python3.6/site-packages/xonsh/prompt/__amalgam__.py", line 486, in current_branch
    has_git = bool(cmds.locate_binary('git', ignore_alias=True))
  File "/usr/local/Cellar/xonsh/0.5.8/libexec/lib/python3.6/site-packages/xonsh/__amalgam__.py", line 7790, in locate_binary
    _ = self.all_commands
  File "/usr/local/Cellar/xonsh/0.5.8/libexec/lib/python3.6/site-packages/xonsh/__amalgam__.py", line 7733, in all_commands
    for cmd in executables_in(path):
  File "/usr/local/Cellar/xonsh/0.5.8/libexec/lib/python3.6/site-packages/xonsh/__amalgam__.py", line 5881, in executables_in
    yield from func(path)
  File "/usr/local/Cellar/xonsh/0.5.8/libexec/lib/python3.6/site-packages/xonsh/__amalgam__.py", line 5849, in _executables_in_posix
    yield from _yield_accessible_unix_file_names(path)
  File "/usr/local/Cellar/xonsh/0.5.8/libexec/lib/python3.6/site-packages/xonsh/__amalgam__.py", line 5829, in _yield_accessible_unix_file_names
    for file_ in scandir(path):
NotADirectoryError: [Errno 20] Not a directory: '/usr/local/bin/jamf'
```

Here are my `xonfig` and `.xonshrc`:

```
r@GOHG ~ (ERROR:curr_branch) $ xonfig
+------------------+-----------------+
| xonsh            | 0.5.8           |
| Python           | 3.6.0           |
| PLY              | 3.9             |
| have readline    | True            |
| prompt toolkit   | None            |
| shell type       | readline        |
| pygments         | None            |
| on posix         | True            |
| on linux         | False           |
| on darwin        | True            |
| on windows       | False           |
| on cygwin        | False           |
| is superuser     | False           |
| default encoding | utf-8           |
| xonsh encoding   | utf-8           |
| encoding errors  | surrogateescape |
+------------------+-----------------+
```

```
$ cat ~/.xonshrc
$XONSH_SHOW_TRACEBACK = True
```

Let me know if you need any other information and/or need help fixing this. Thanks!


Discussion:
Hey @rajathagasthya - thanks for reporting! 
Can you post your `$PATH`?
@gforsyth Sure.

```
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/bin/jamf:/usr/local/sbin
```

I see `/usr/local/bin/jamf` here, but I've no idea how it ended up there. Could that be the problem? 

Thanks!
I think that is the problem -- if you remove that I think it will stop the error. That said, we should handle this more gracefully, anyway. 

Is `jamf` an executable? I wonder if whatever installer created that has a bug.
Yeah, that's an executable (I'm guessing it's installed by my employer on my work laptop). I just removed it and it indeed stopped the error. `xonsh` works fine now.

Sorry for the false alarm, but it would be great if it errors out gracefully in such situations. 
Glad it's working now! And yes, we should definitely handle that error better.
I'd love to fix this and contribute if this a beginner level work, with some hand holding :)
Hey @rajathagasthya, awesome! We're always psyched to have new people on board. 

The relevant code is here: https://github.com/xonsh/xonsh/blob/master/xonsh/tools.py#L139

`EnvPath` is the class-type of our `$PATH` object. 
We want to filter out things that aren't directories at initialization and also when items are set or inserted.
You could use `os.path.isdir` or `os.path.isfile` to check that. 

It's interesting that this doesn't seem to affect Python 3.5 (or at least, I can't get it to throw an error by adding a file to my `$PATH`) -- probably worth adding a few tests, too! 
Feel free to ask questions here, or open a WIP pull-request and ask there!
@gforsyth Thank you, sir!
@gforsyth @laerus I'm looking into this issue. That's what I understand about it:

1. All(?) environment paths except `BASH_COMPLETIONS` are a list of directories.

2. xonsh crashes if a non directory is in the `$PATH`

The possible fixes is to let `EnvPath` remain a wrapper around `list` and handle the case of non-directories where the path is being searched for an executable. This looks a sensible approach because there are other path variables too which can contain files(`CLASSPATH`, `MODULEPATH`).

Should I go forward with it? Is there anything I should beware of while I'm doing it? I'm new to xonsh and don't want to screw anythings else up.

Resolving patch:
diff --git a/xonsh/commands_cache.py b/xonsh/commands_cache.py
--- a/xonsh/commands_cache.py
+++ b/xonsh/commands_cache.py
@@ -70,12 +70,21 @@ def get_possible_names(name):
         else:
             return [name]
 
+    @staticmethod
+    def remove_dups(p):
+        ret = list()
+        for e in p:
+            if e not in ret:
+                ret.append(e)
+        return ret
+
     @property
     def all_commands(self):
         paths = builtins.__xonsh_env__.get('PATH', [])
-        pathset = frozenset(x for x in paths if os.path.isdir(x))
+        paths = CommandsCache.remove_dups(paths)
+        path_immut = tuple(x for x in paths if os.path.isdir(x))
         # did PATH change?
-        path_hash = hash(pathset)
+        path_hash = hash(path_immut)
         cache_valid = path_hash == self._path_checksum
         self._path_checksum = path_hash
         # did aliases change?
@@ -85,7 +94,7 @@ def all_commands(self):
         self._alias_checksum = al_hash
         # did the contents of any directory in PATH change?
         max_mtime = 0
-        for path in pathset:
+        for path in path_immut:
             mtime = os.stat(path).st_mtime
             if mtime > max_mtime:
                 max_mtime = mtime
@@ -94,7 +103,7 @@ def all_commands(self):
         if cache_valid:
             return self._cmds_cache
         allcmds = {}
-        for path in reversed(paths):
+        for path in reversed(path_immut):
             # iterate backwards so that entries at the front of PATH overwrite
             # entries at the back.
             for cmd in executables_in(path):


Test patch: