Repository: xonsh/xonsh

Issue:
proposal: allow directories in `BASH_COMPLETIONS`
there are a lot of bash-completion files provided by packages on my system (Manjaro / Arch) in a directory. it would be handy to just add that directory's path to `BASH_COMPLETIONS` to effectively include them all.

also, shouldn't the configuration-wizard propose default values depending on the detected OS?

i can propably submit patches for both, but rather not very soon.



Discussion:
hey @funkyfuture -- i'll take a look at doing that.  for the moment, you can use a workaround like this in your `xonshrc`

``` console
$BASH_COMPLETIONS[0]='/usr/share/bash-completion/bash_completion'

completions = ['pacman', 'arch', 'git', 'systemctl']

for i in completions:
    comp = '/usr/share/bash-completion/completions/{}'.format(i)
    if not os.path.isfile(comp):
        continue

    if not comp in $BASH_COMPLETIONS:
        $BASH_COMPLETIONS.append(comp)
```

Hi @funkyfuture - to echo @gforsyth, thanks for the suggestion.

> also, shouldn't the configuration-wizard propose default values depending on the detected OS?

It already does this if the Env defaults are OS dependent. For example, see `BASH_COMPLETIONS` https://github.com/scopatz/xonsh/blob/master/xonsh/environ.py#L157

@gforsyth i hope you didn't work on it. i just finished it.

No worries, I got distracted.
On May 14, 2016 12:20 PM, "Frank Sachsenheim" notifications@github.com
wrote:

> @gforsyth https://github.com/gforsyth i hope you didn't work on it. i
> just finished it.
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly or view it on GitHub
> https://github.com/scopatz/xonsh/issues/848#issuecomment-219229110


Resolving patch:
diff --git a/xonsh/completer.py b/xonsh/completer.py
--- a/xonsh/completer.py
+++ b/xonsh/completer.py
@@ -5,6 +5,7 @@
 import ast
 import sys
 import shlex
+from pathlib import Path
 import pickle
 import inspect
 import builtins
@@ -40,7 +41,7 @@ def _path_from_partial_string(inp, pos=None):
             else:
                 return None
         string = partial[startix:endix]
-    end = re.sub(RE_STRING_START,'',quote)
+    end = re.sub(RE_STRING_START, '', quote)
     _string = string
     if not _string.endswith(end):
         _string = _string + end
@@ -84,6 +85,7 @@ def _path_from_partial_string(inp, pos=None):
 
 WS = set(' \t\r\n')
 
+
 def startswithlow(x, start, startlow=None):
     """True if x starts with a string or its lowercase version. The lowercase
     version may be optionally be provided.
@@ -120,6 +122,7 @@ def _normpath(p):
 
     return p
 
+
 class Completer(object):
     """This provides a list of optional completions for the xonsh shell."""
 
@@ -268,8 +271,9 @@ def _filter_repeats(self, comps):
         return {max(i, key=len) for i in reps.values()}
 
     def find_and_complete(self, line, idx, ctx=None):
-        """Finds the completions given only the full code line and a current cursor
-        position. This represents an easier alternative to the complete() method.
+        """Finds the completions given only the full code line and a current
+        cursor position. This represents an easier alternative to the
+        complete() method.
 
         Parameters
         ----------
@@ -308,8 +312,8 @@ def _add_env(self, paths, prefix):
             csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
             startswither = startswithnorm if csc else startswithlow
             key = prefix[1:]
-            keylow = key.lower()
-            paths.update({'$' + k for k in builtins.__xonsh_env__ if startswither(k, key, keylow)})
+            paths.update({'$' + k for k in builtins.__xonsh_env__
+                         if startswither(k, key, key.lower())})
 
     def _add_dots(self, paths, prefix):
         if prefix in {'', '.'}:
@@ -353,7 +357,6 @@ def _quote_to_use(self, x):
         else:
             return single
 
-
     def _quote_paths(self, paths, start, end):
         out = set()
         space = ' '
@@ -386,7 +389,6 @@ def _quote_paths(self, paths, start, end):
             out.add(start + s + end)
         return out
 
-
     def path_complete(self, prefix, start, end, cdpath=False):
         """Completes based on a path name."""
         tilde = '~'
@@ -425,38 +427,40 @@ def bash_complete(self, prefix, line, begidx, endidx):
         else:
             prefix = shlex.quote(prefix)
 
-        script = BASH_COMPLETE_SCRIPT.format(filename=fnme,
-                    line=' '.join(shlex.quote(p) for p in splt),
-                    comp_line=shlex.quote(line), n=n, func=func, cmd=cmd,
-                    end=endidx + 1, prefix=prefix, prev=shlex.quote(prev))
+        script = BASH_COMPLETE_SCRIPT.format(
+            filename=fnme, line=' '.join(shlex.quote(p) for p in splt),
+            comp_line=shlex.quote(line), n=n, func=func, cmd=cmd,
+            end=endidx + 1, prefix=prefix, prev=shlex.quote(prev))
         try:
-            out = subprocess.check_output(['bash'], input=script,
-                    universal_newlines=True, stderr=subprocess.PIPE,
-                    env=builtins.__xonsh_env__.detype())
+            out = subprocess.check_output(
+                ['bash'], input=script, universal_newlines=True,
+                stderr=subprocess.PIPE, env=builtins.__xonsh_env__.detype())
         except subprocess.CalledProcessError:
             out = ''
 
         rtn = set(out.splitlines())
         return rtn
 
-    def _source_completions(self):
-        srcs = []
-        for f in builtins.__xonsh_env__.get('BASH_COMPLETIONS'):
-            if os.path.isfile(f):
-                # We need to "Unixify" Windows paths for Bash to understand
-                if ON_WINDOWS:
-                    f = RE_WIN_DRIVE.sub(lambda m: '/{0}/'.format(m.group(1).lower()), f).replace('\\', '/')
-                srcs.append('source ' + f)
-        return srcs
+    @staticmethod
+    def _collect_completions_sources():
+        sources = []
+        paths = (Path(x) for x in
+                 builtins.__xonsh_env__.get('BASH_COMPLETIONS'))
+        for path in paths:
+            if path.is_file():
+                sources.append('source ' + str(path))
+            elif path.is_dir():
+                for _file in (x for x in path.glob('*') if x.is_file()):
+                    sources.append('source ' + str(_file))
+        return sources
 
     def _load_bash_complete_funcs(self):
         self.bash_complete_funcs = bcf = {}
-        inp = self._source_completions()
-        if len(inp) == 0:
+        inp = self._collect_completions_sources()
+        if not inp:
             return
         inp.append('complete -p\n')
-        out = subprocess.check_output(['bash'], input='\n'.join(inp),
-                env=builtins.__xonsh_env__.detype(), universal_newlines=True)
+        out = self._source_completions(inp)
         for line in out.splitlines():
             head, _, cmd = line.rpartition(' ')
             if len(cmd) == 0 or cmd == 'cd':
@@ -467,8 +471,8 @@ def _load_bash_complete_funcs(self):
             bcf[cmd] = m.group(1)
 
     def _load_bash_complete_files(self):
-        inp = self._source_completions()
-        if len(inp) == 0:
+        inp = self._collect_completions_sources()
+        if not inp:
             self.bash_complete_files = {}
             return
         if self.bash_complete_funcs:
@@ -476,8 +480,7 @@ def _load_bash_complete_files(self):
             bash_funcs = set(self.bash_complete_funcs.values())
             inp.append('declare -F ' + ' '.join([f for f in bash_funcs]))
             inp.append('shopt -u extdebug\n')
-        out = subprocess.check_output(['bash'], input='\n'.join(inp),
-                env=builtins.__xonsh_env__.detype(), universal_newlines=True)
+        out = self._source_completions(inp)
         func_files = {}
         for line in out.splitlines():
             parts = line.split()
@@ -488,6 +491,11 @@ def _load_bash_complete_files(self):
             if func in func_files
         }
 
+    def _source_completions(self, source):
+        return subprocess.check_output(
+            ['bash'], input='\n'.join(source), universal_newlines=True,
+            env=builtins.__xonsh_env__.detype(), stderr=subprocess.DEVNULL)
+
     def attr_complete(self, prefix, ctx):
         """Complete attributes of an object."""
         attrs = set()
@@ -513,10 +521,11 @@ def attr_complete(self, prefix, ctx):
         opts = []
         for i in _opts:
             try:
-                v = eval('{0}.{1}'.format(expr, i), _ctx)
-                opts.append(i)
+                eval('{0}.{1}'.format(expr, i), _ctx)
             except:  # pylint:disable=bare-except
                 continue
+            else:
+                opts.append(i)
         if len(attr) == 0:
             opts = [o for o in opts if not o.startswith('_')]
         else:
@@ -558,11 +567,12 @@ def option_complete(self, prefix, cmd):
         startswither = startswithnorm if csc else startswithlow
         if cmd not in self._options.keys():
             try:
-                manpage = subprocess.Popen(["man", cmd],
-                            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
+                manpage = subprocess.Popen(
+                    ["man", cmd], stdout=subprocess.PIPE,
+                    stderr=subprocess.DEVNULL)
                 # This is a trick to get rid of reverse line feeds
-                text = subprocess.check_output(["col", "-b"],
-                        stdin=manpage.stdout)
+                text = subprocess.check_output(
+                    ["col", "-b"], stdin=manpage.stdout)
                 text = text.decode('utf-8')
                 scraped_text = ' '.join(SCRAPE_RE.findall(text))
                 matches = INNER_OPTIONS_RE.findall(scraped_text)


Test patch: