2015-03-01 22:59:44 -06:00
|
|
|
|
"""A (tab-)completer for xonsh."""
|
|
|
|
|
from __future__ import print_function, unicode_literals
|
|
|
|
|
import os
|
2015-03-02 23:01:43 -06:00
|
|
|
|
import re
|
2015-03-01 22:59:44 -06:00
|
|
|
|
import builtins
|
2015-05-25 12:51:07 +02:00
|
|
|
|
import pickle
|
2015-03-01 22:59:44 -06:00
|
|
|
|
import subprocess
|
2015-05-05 16:03:01 +02:00
|
|
|
|
import sys
|
2015-03-01 22:59:44 -06:00
|
|
|
|
|
2015-03-15 17:30:45 -05:00
|
|
|
|
from xonsh.built_ins import iglobpath
|
2015-04-06 08:14:53 -04:00
|
|
|
|
from xonsh.tools import subexpr_from_unbalanced
|
2015-03-15 17:30:45 -05:00
|
|
|
|
|
2015-03-26 01:33:08 -05:00
|
|
|
|
RE_DASHF = re.compile(r'-F\s+(\w+)')
|
2015-04-05 16:31:50 -05:00
|
|
|
|
RE_ATTR = re.compile(r'(\S+(\..+)*)\.(\w*)$')
|
2015-03-02 23:01:43 -06:00
|
|
|
|
|
2015-04-02 19:07:31 -04:00
|
|
|
|
XONSH_TOKENS = {
|
|
|
|
|
'and ', 'as ', 'assert ', 'break', 'class ', 'continue', 'def ', 'del ',
|
|
|
|
|
'elif ', 'else', 'except ', 'finally:', 'for ', 'from ', 'global ',
|
|
|
|
|
'import ', 'if ', 'in ', 'is ', 'lambda ', 'nonlocal ', 'not ', 'or ',
|
|
|
|
|
'pass', 'raise ', 'return ', 'try:', 'while ', 'with ', 'yield ', '+', '-',
|
|
|
|
|
'/', '//', '%', '**', '|', '&', '~', '^', '>>', '<<', '<', '<=', '>', '>=',
|
|
|
|
|
'==', '!=', '->', '=', '+=', '-=', '*=', '/=', '%=', '**=', '>>=', '<<=',
|
|
|
|
|
'&=', '^=', '|=', '//=', ',', ';', ':', '?', '??', '$(', '${', '$[', '..',
|
|
|
|
|
'...'
|
|
|
|
|
}
|
2015-03-01 22:59:44 -06:00
|
|
|
|
|
2015-03-03 00:52:33 -06:00
|
|
|
|
BASH_COMPLETE_SCRIPT = """source {filename}
|
|
|
|
|
COMP_WORDS=({line})
|
|
|
|
|
COMP_LINE="{line}"
|
|
|
|
|
COMP_POINT=${{#COMP_LINE}}
|
|
|
|
|
COMP_COUNT={end}
|
|
|
|
|
COMP_CWORD={n}
|
|
|
|
|
{func} {cmd} {prefix} {prev}
|
|
|
|
|
for ((i=0;i<${{#COMPREPLY[*]}};i++)) do echo ${{COMPREPLY[i]}}; done
|
|
|
|
|
"""
|
|
|
|
|
|
2015-08-03 20:46:41 -04:00
|
|
|
|
|
2015-07-30 19:40:19 -05:00
|
|
|
|
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.
|
|
|
|
|
"""
|
|
|
|
|
if startlow is None:
|
|
|
|
|
startlow = start.lower()
|
|
|
|
|
return x.startswith(start) or x.lower().startswith(startlow)
|
|
|
|
|
|
2015-04-02 19:07:31 -04:00
|
|
|
|
|
2015-08-02 16:14:56 -05:00
|
|
|
|
def startswithnorm(x, start, startlow=None):
|
2015-08-03 20:46:41 -04:00
|
|
|
|
"""True if x starts with a string s. Ignores its lowercase version, but
|
2015-08-02 16:14:56 -05:00
|
|
|
|
matches the API of startswithlow().
|
|
|
|
|
"""
|
|
|
|
|
return x.startswith(start)
|
|
|
|
|
|
|
|
|
|
|
2015-07-31 17:23:35 +02:00
|
|
|
|
def _normpath(p):
|
|
|
|
|
# Prevent normpath() from removing initial ‘./’
|
2015-07-31 18:25:28 +02:00
|
|
|
|
here = os.curdir + os.sep
|
|
|
|
|
if p.startswith(here):
|
|
|
|
|
return os.path.join(os.curdir, os.path.normpath(p[len(here):]))
|
2015-07-31 17:23:35 +02:00
|
|
|
|
return os.path.normpath(p)
|
|
|
|
|
|
|
|
|
|
|
2015-03-01 22:59:44 -06:00
|
|
|
|
class Completer(object):
|
|
|
|
|
"""This provides a list of optional completions for the xonsh shell."""
|
|
|
|
|
|
2015-03-02 23:01:43 -06:00
|
|
|
|
def __init__(self):
|
2015-04-06 08:14:53 -04:00
|
|
|
|
# initialize command cache
|
|
|
|
|
self._path_checksum = None
|
|
|
|
|
self._alias_checksum = None
|
|
|
|
|
self._path_mtime = -1
|
2015-04-06 08:18:23 -04:00
|
|
|
|
self._cmds_cache = frozenset()
|
2015-05-25 12:51:07 +02:00
|
|
|
|
self._man_completer = ManCompleter()
|
2015-03-02 23:01:43 -06:00
|
|
|
|
try:
|
|
|
|
|
# FIXME this could be threaded for faster startup times
|
|
|
|
|
self._load_bash_complete_funcs()
|
|
|
|
|
# or we could make this lazy
|
|
|
|
|
self._load_bash_complete_files()
|
|
|
|
|
self.have_bash = True
|
2015-07-08 14:17:58 +02:00
|
|
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
2015-03-02 23:01:43 -06:00
|
|
|
|
self.have_bash = False
|
|
|
|
|
|
2015-03-01 22:59:44 -06:00
|
|
|
|
def complete(self, prefix, line, begidx, endidx, ctx=None):
|
|
|
|
|
"""Complete the string s, given a possible execution context.
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
----------
|
|
|
|
|
prefix : str
|
|
|
|
|
The string to match
|
|
|
|
|
line : str
|
|
|
|
|
The line that prefix appears on.
|
|
|
|
|
begidx : int
|
|
|
|
|
The index in line that prefix starts on.
|
|
|
|
|
endidx : int
|
|
|
|
|
The index in line that prefix ends on.
|
|
|
|
|
ctx : Iterable of str (ie dict, set, etc), optional
|
|
|
|
|
Names in the current execution context.
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
-------
|
|
|
|
|
rtn : list of str
|
|
|
|
|
Possible completions of prefix, sorted alphabetically.
|
|
|
|
|
"""
|
2015-03-02 19:18:16 -06:00
|
|
|
|
space = ' ' # intern some strings for faster appending
|
2015-03-02 18:36:07 -06:00
|
|
|
|
slash = '/'
|
2015-04-05 15:42:24 -05:00
|
|
|
|
dot = '.'
|
2015-04-03 18:25:44 -04:00
|
|
|
|
ctx = ctx or {}
|
2015-07-30 19:40:19 -05:00
|
|
|
|
prefixlow = prefix.lower()
|
2015-04-03 18:25:44 -04:00
|
|
|
|
cmd = line.split(' ', 1)[0]
|
2015-08-02 16:14:56 -05:00
|
|
|
|
env = builtins.__xonsh_env__
|
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
|
|
|
|
startswither = startswithnorm if csc else startswithlow
|
2015-03-03 00:52:33 -06:00
|
|
|
|
if begidx == 0:
|
2015-04-03 18:25:44 -04:00
|
|
|
|
# the first thing we're typing; could be python or subprocess, so
|
|
|
|
|
# anything goes.
|
2015-04-06 08:14:53 -04:00
|
|
|
|
rtn = self.cmd_complete(prefix)
|
2015-04-03 18:25:44 -04:00
|
|
|
|
elif cmd in self.bash_complete_funcs:
|
2015-03-08 21:19:16 -05:00
|
|
|
|
rtn = set()
|
|
|
|
|
for s in self.bash_complete(prefix, line, begidx, endidx):
|
|
|
|
|
if os.path.isdir(s.rstrip()):
|
|
|
|
|
s = s.rstrip() + slash
|
|
|
|
|
rtn.add(s)
|
2015-03-15 17:55:42 -05:00
|
|
|
|
if len(rtn) == 0:
|
|
|
|
|
rtn = self.path_complete(prefix)
|
2015-03-08 21:19:16 -05:00
|
|
|
|
return sorted(rtn)
|
2015-04-06 08:14:53 -04:00
|
|
|
|
elif prefix.startswith('${') or prefix.startswith('@('):
|
|
|
|
|
# python mode explicitly
|
|
|
|
|
rtn = set()
|
2015-05-19 18:07:04 +02:00
|
|
|
|
elif prefix.startswith('-'):
|
2015-05-25 12:51:07 +02:00
|
|
|
|
return sorted(self._man_completer.option_complete(prefix, cmd))
|
2015-04-06 08:14:53 -04:00
|
|
|
|
elif cmd not in ctx:
|
2015-05-05 16:03:01 +02:00
|
|
|
|
if cmd == 'import' and begidx == len('import '):
|
|
|
|
|
# completing module to import
|
|
|
|
|
return sorted(self.module_complete(prefix))
|
2015-04-06 08:14:53 -04:00
|
|
|
|
if cmd in self._all_commands():
|
|
|
|
|
# subproc mode; do path completions
|
2015-07-28 17:42:46 +02:00
|
|
|
|
return sorted(self.path_complete(prefix, cdpath=True))
|
2015-04-06 08:14:53 -04:00
|
|
|
|
else:
|
|
|
|
|
# if we're here, could be anything
|
|
|
|
|
rtn = set()
|
2015-03-03 00:52:33 -06:00
|
|
|
|
else:
|
2015-04-05 20:43:25 -04:00
|
|
|
|
# if we're here, we're not a command, but could be anything else
|
2015-03-03 00:52:33 -06:00
|
|
|
|
rtn = set()
|
2015-08-02 16:14:56 -05:00
|
|
|
|
rtn |= {s for s in XONSH_TOKENS if startswither(s, prefix, prefixlow)}
|
2015-03-01 22:59:44 -06:00
|
|
|
|
if ctx is not None:
|
2015-04-05 15:42:24 -05:00
|
|
|
|
if dot in prefix:
|
|
|
|
|
rtn |= self.attr_complete(prefix, ctx)
|
|
|
|
|
else:
|
2015-08-02 16:14:56 -05:00
|
|
|
|
rtn |= {s for s in ctx if startswither(s, prefix, prefixlow)}
|
|
|
|
|
rtn |= {s for s in dir(builtins) if startswither(s, prefix, prefixlow)}
|
2015-07-30 19:40:19 -05:00
|
|
|
|
rtn |= {s + space for s in builtins.aliases
|
2015-08-02 16:14:56 -05:00
|
|
|
|
if startswither(s, prefix, prefixlow)}
|
2015-04-03 18:25:44 -04:00
|
|
|
|
rtn |= self.path_complete(prefix)
|
|
|
|
|
return sorted(rtn)
|
|
|
|
|
|
|
|
|
|
def _add_env(self, paths, prefix):
|
2015-03-01 22:59:44 -06:00
|
|
|
|
if prefix.startswith('$'):
|
2015-04-02 19:07:31 -04:00
|
|
|
|
env = builtins.__xonsh_env__
|
2015-08-02 16:14:56 -05:00
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
|
|
|
|
startswither = startswithnorm if csc else startswithlow
|
2015-03-01 22:59:44 -06:00
|
|
|
|
key = prefix[1:]
|
2015-07-30 19:40:19 -05:00
|
|
|
|
keylow = key.lower()
|
2015-08-02 16:14:56 -05:00
|
|
|
|
paths.update({'$' + k for k in env if startswither(k, key, keylow)})
|
2015-04-03 18:25:44 -04:00
|
|
|
|
|
|
|
|
|
def _add_dots(self, paths, prefix):
|
|
|
|
|
if prefix in {'', '.'}:
|
|
|
|
|
paths.update({'./', '../'})
|
|
|
|
|
if prefix == '..':
|
|
|
|
|
paths.add('../')
|
2015-03-02 23:01:43 -06:00
|
|
|
|
|
2015-05-28 02:48:11 +02:00
|
|
|
|
def _add_cdpaths(self, paths, prefix):
|
|
|
|
|
"""Completes current prefix using CDPATH"""
|
|
|
|
|
env = builtins.__xonsh_env__
|
2015-08-02 16:14:56 -05:00
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
2015-05-28 02:48:11 +02:00
|
|
|
|
for cdp in env.get("CDPATH", []):
|
2015-08-03 20:46:41 -04:00
|
|
|
|
test_glob = os.path.join(cdp, prefix) + '*'
|
|
|
|
|
for s in iglobpath(test_glob, ignore_case=(not csc)):
|
2015-07-28 17:42:46 +02:00
|
|
|
|
if os.path.isdir(s):
|
|
|
|
|
paths.add(os.path.basename(s))
|
2015-05-28 02:48:11 +02:00
|
|
|
|
|
2015-04-06 08:14:53 -04:00
|
|
|
|
def cmd_complete(self, cmd):
|
2015-03-03 00:52:33 -06:00
|
|
|
|
"""Completes a command name based on what is on the $PATH"""
|
|
|
|
|
space = ' '
|
2015-07-30 19:40:19 -05:00
|
|
|
|
cmdlow = cmd.lower()
|
2015-08-02 16:14:56 -05:00
|
|
|
|
env = builtins.__xonsh_env__
|
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
|
|
|
|
startswither = startswithnorm if csc else startswithlow
|
2015-08-03 20:46:41 -04:00
|
|
|
|
return {s + space
|
|
|
|
|
for s in self._all_commands()
|
|
|
|
|
if startswither(s, cmd, cmdlow)}
|
2015-03-02 23:01:43 -06:00
|
|
|
|
|
2015-05-05 16:03:01 +02:00
|
|
|
|
def module_complete(self, prefix):
|
2015-05-19 18:07:04 +02:00
|
|
|
|
"""Completes a name of a module to import."""
|
2015-07-30 19:40:19 -05:00
|
|
|
|
prefixlow = prefix.lower()
|
2015-05-05 16:03:01 +02:00
|
|
|
|
modules = set(sys.modules.keys())
|
2015-08-02 16:14:56 -05:00
|
|
|
|
env = builtins.__xonsh_env__
|
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
|
|
|
|
startswither = startswithnorm if csc else startswithlow
|
|
|
|
|
return {s for s in modules if startswither(s, prefix, prefixlow)}
|
2015-05-05 16:03:01 +02:00
|
|
|
|
|
2015-07-28 17:42:46 +02:00
|
|
|
|
def path_complete(self, prefix, cdpath=False):
|
2015-03-15 17:30:45 -05:00
|
|
|
|
"""Completes based on a path name."""
|
|
|
|
|
space = ' ' # intern some strings for faster appending
|
|
|
|
|
slash = '/'
|
2015-03-15 17:48:15 -05:00
|
|
|
|
tilde = '~'
|
2015-03-16 21:13:53 -04:00
|
|
|
|
paths = set()
|
2015-08-02 16:14:56 -05:00
|
|
|
|
env = builtins.__xonsh_env__
|
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
2015-03-16 21:13:53 -04:00
|
|
|
|
if prefix.startswith("'") or prefix.startswith('"'):
|
|
|
|
|
prefix = prefix[1:]
|
2015-08-02 16:14:56 -05:00
|
|
|
|
for s in iglobpath(prefix + '*', ignore_case=(not csc)):
|
2015-03-16 21:13:53 -04:00
|
|
|
|
if space in s:
|
|
|
|
|
s = repr(s + (slash if os.path.isdir(s) else ''))
|
|
|
|
|
else:
|
|
|
|
|
s = s + (slash if os.path.isdir(s) else space)
|
|
|
|
|
paths.add(s)
|
2015-03-15 17:48:15 -05:00
|
|
|
|
if tilde in prefix:
|
|
|
|
|
home = os.path.expanduser(tilde)
|
|
|
|
|
paths = {s.replace(home, tilde) for s in paths}
|
2015-04-03 18:25:44 -04:00
|
|
|
|
self._add_env(paths, prefix)
|
|
|
|
|
self._add_dots(paths, prefix)
|
2015-07-28 17:42:46 +02:00
|
|
|
|
if cdpath:
|
|
|
|
|
self._add_cdpaths(paths, prefix)
|
2015-07-31 17:23:35 +02:00
|
|
|
|
return {_normpath(s) for s in paths}
|
2015-03-15 17:30:45 -05:00
|
|
|
|
|
2015-03-03 00:52:33 -06:00
|
|
|
|
def bash_complete(self, prefix, line, begidx, endidx):
|
|
|
|
|
"""Attempts BASH completion."""
|
|
|
|
|
splt = line.split()
|
|
|
|
|
cmd = splt[0]
|
|
|
|
|
func = self.bash_complete_funcs.get(cmd, None)
|
|
|
|
|
fnme = self.bash_complete_files.get(cmd, None)
|
|
|
|
|
if func is None or fnme is None:
|
|
|
|
|
return set()
|
2015-04-02 20:00:58 -05:00
|
|
|
|
idx = n = 0
|
2015-03-03 00:52:33 -06:00
|
|
|
|
for n, tok in enumerate(splt):
|
|
|
|
|
if tok == prefix:
|
|
|
|
|
idx = line.find(prefix, idx)
|
|
|
|
|
if idx >= begidx:
|
|
|
|
|
break
|
|
|
|
|
prev = tok
|
|
|
|
|
if len(prefix) == 0:
|
|
|
|
|
prefix = '""'
|
|
|
|
|
n += 1
|
2015-04-02 19:07:31 -04:00
|
|
|
|
script = BASH_COMPLETE_SCRIPT.format(filename=fnme,
|
|
|
|
|
line=line,
|
|
|
|
|
n=n,
|
|
|
|
|
func=func,
|
|
|
|
|
cmd=cmd,
|
|
|
|
|
end=endidx + 1,
|
|
|
|
|
prefix=prefix,
|
|
|
|
|
prev=prev)
|
|
|
|
|
out = subprocess.check_output(['bash'],
|
|
|
|
|
input=script,
|
|
|
|
|
universal_newlines=True,
|
2015-03-13 19:32:31 -05:00
|
|
|
|
stderr=subprocess.PIPE)
|
2015-03-03 00:52:33 -06:00
|
|
|
|
space = ' '
|
2015-04-02 19:07:31 -04:00
|
|
|
|
rtn = {s + space if s[-1:].isalnum() else s for s in out.splitlines()}
|
2015-03-03 00:52:33 -06:00
|
|
|
|
return rtn
|
|
|
|
|
|
2015-03-09 19:42:50 -05:00
|
|
|
|
def _source_completions(self):
|
|
|
|
|
srcs = []
|
|
|
|
|
for f in builtins.__xonsh_env__.get('BASH_COMPLETIONS', ()):
|
|
|
|
|
if os.path.isfile(f):
|
|
|
|
|
srcs.append('source ' + f)
|
|
|
|
|
return srcs
|
|
|
|
|
|
2015-03-02 23:01:43 -06:00
|
|
|
|
def _load_bash_complete_funcs(self):
|
|
|
|
|
self.bash_complete_funcs = bcf = {}
|
2015-04-02 20:00:58 -05:00
|
|
|
|
inp = self._source_completions()
|
|
|
|
|
if len(inp) == 0:
|
2015-03-09 19:42:50 -05:00
|
|
|
|
return
|
2015-04-02 20:00:58 -05:00
|
|
|
|
inp.append('complete -p\n')
|
2015-04-02 21:03:31 -05:00
|
|
|
|
out = subprocess.check_output(['bash'], input='\n'.join(inp),
|
2015-03-09 19:42:50 -05:00
|
|
|
|
universal_newlines=True)
|
2015-03-02 23:01:43 -06:00
|
|
|
|
for line in out.splitlines():
|
|
|
|
|
head, cmd = line.rsplit(' ', 1)
|
2015-03-07 16:36:31 -06:00
|
|
|
|
if len(cmd) == 0 or cmd == 'cd':
|
2015-03-02 23:01:43 -06:00
|
|
|
|
continue
|
|
|
|
|
m = RE_DASHF.search(head)
|
|
|
|
|
if m is None:
|
|
|
|
|
continue
|
|
|
|
|
bcf[cmd] = m.group(1)
|
|
|
|
|
|
|
|
|
|
def _load_bash_complete_files(self):
|
2015-04-02 20:00:58 -05:00
|
|
|
|
inp = self._source_completions()
|
|
|
|
|
if len(inp) == 0:
|
2015-03-09 19:42:50 -05:00
|
|
|
|
self.bash_complete_files = {}
|
|
|
|
|
return
|
2015-07-12 12:44:26 +02:00
|
|
|
|
if self.bash_complete_funcs:
|
|
|
|
|
inp.append('shopt -s extdebug')
|
2015-08-03 20:46:41 -04:00
|
|
|
|
bash_funcs = set(self.bash_complete_funcs.values())
|
|
|
|
|
inp.append('declare -F ' + ' '.join([f for f in bash_funcs]))
|
2015-07-12 12:44:26 +02:00
|
|
|
|
inp.append('shopt -u extdebug\n')
|
2015-04-02 20:00:58 -05:00
|
|
|
|
out = subprocess.check_output(['bash'], input='\n'.join(inp),
|
2015-03-02 23:01:43 -06:00
|
|
|
|
universal_newlines=True)
|
|
|
|
|
func_files = {}
|
|
|
|
|
for line in out.splitlines():
|
|
|
|
|
parts = line.split()
|
|
|
|
|
func_files[parts[0]] = parts[-1]
|
2015-04-02 19:07:31 -04:00
|
|
|
|
self.bash_complete_files = {
|
|
|
|
|
cmd: func_files[func]
|
|
|
|
|
for cmd, func in self.bash_complete_funcs.items()
|
|
|
|
|
if func in func_files
|
|
|
|
|
}
|
2015-04-05 16:31:50 -05:00
|
|
|
|
|
|
|
|
|
def attr_complete(self, prefix, ctx):
|
|
|
|
|
"""Complete attributes of an object."""
|
|
|
|
|
attrs = set()
|
|
|
|
|
m = RE_ATTR.match(prefix)
|
|
|
|
|
if m is None:
|
|
|
|
|
return attrs
|
|
|
|
|
expr, attr = m.group(1, 3)
|
|
|
|
|
expr = subexpr_from_unbalanced(expr, '(', ')')
|
|
|
|
|
expr = subexpr_from_unbalanced(expr, '[', ']')
|
|
|
|
|
expr = subexpr_from_unbalanced(expr, '{', '}')
|
|
|
|
|
try:
|
|
|
|
|
val = builtins.evalx(expr, glbs=ctx)
|
|
|
|
|
except: # pylint:disable=bare-except
|
|
|
|
|
try:
|
|
|
|
|
val = builtins.evalx(expr, glbs=builtins.__dict__)
|
|
|
|
|
except: # pylint:disable=bare-except
|
|
|
|
|
return attrs # anything could have gone wrong!
|
|
|
|
|
opts = dir(val)
|
|
|
|
|
if len(attr) == 0:
|
|
|
|
|
opts = [o for o in opts if not o.startswith('_')]
|
|
|
|
|
else:
|
2015-08-02 16:14:56 -05:00
|
|
|
|
env = builtins.__xonsh_env__
|
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
|
|
|
|
startswither = startswithnorm if csc else startswithlow
|
2015-07-30 19:40:19 -05:00
|
|
|
|
attrlow = attr.lower()
|
2015-08-02 16:14:56 -05:00
|
|
|
|
opts = [o for o in opts if startswither(oattr, attrlow)]
|
2015-04-05 16:31:50 -05:00
|
|
|
|
prelen = len(prefix)
|
|
|
|
|
for opt in opts:
|
|
|
|
|
a = getattr(val, opt)
|
|
|
|
|
rpl = opt + '(' if callable(a) else opt
|
|
|
|
|
# note that prefix[:prelen-len(attr)] != prefix[:-len(attr)]
|
|
|
|
|
# when len(attr) == 0.
|
2015-07-30 09:39:29 +02:00
|
|
|
|
comp = prefix[:prelen - len(attr)] + rpl
|
2015-04-05 16:31:50 -05:00
|
|
|
|
attrs.add(comp)
|
|
|
|
|
return attrs
|
2015-04-06 08:14:53 -04:00
|
|
|
|
|
|
|
|
|
def _all_commands(self):
|
2015-04-06 21:20:58 -04:00
|
|
|
|
path = builtins.__xonsh_env__.get('PATH', [])
|
2015-04-06 08:31:23 -04:00
|
|
|
|
# did PATH change?
|
2015-04-06 08:14:53 -04:00
|
|
|
|
path_hash = hash(tuple(path))
|
|
|
|
|
cache_valid = path_hash == self._path_checksum
|
|
|
|
|
self._path_checksum = path_hash
|
2015-04-06 08:31:23 -04:00
|
|
|
|
# did aliases change?
|
2015-04-06 21:20:58 -04:00
|
|
|
|
al_hash = hash(tuple(sorted(builtins.aliases.keys())))
|
2015-04-06 08:14:53 -04:00
|
|
|
|
self._alias_checksum = al_hash
|
|
|
|
|
cache_valid = cache_valid and al_hash == self._alias_checksum
|
2015-04-06 08:26:20 -04:00
|
|
|
|
pm = self._path_mtime
|
2015-04-06 08:31:23 -04:00
|
|
|
|
# did the contents of any directory in PATH change?
|
|
|
|
|
for d in filter(os.path.isdir, path):
|
|
|
|
|
m = os.stat(d).st_mtime
|
|
|
|
|
if m > pm:
|
|
|
|
|
pm = m
|
|
|
|
|
cache_valid = False
|
2015-04-06 08:26:20 -04:00
|
|
|
|
self._path_mtime = pm
|
2015-04-06 08:14:53 -04:00
|
|
|
|
if cache_valid:
|
|
|
|
|
return self._cmds_cache
|
|
|
|
|
allcmds = set()
|
2015-04-06 21:20:58 -04:00
|
|
|
|
for d in filter(os.path.isdir, path):
|
|
|
|
|
allcmds |= set(os.listdir(d))
|
2015-04-06 08:14:53 -04:00
|
|
|
|
allcmds |= set(builtins.aliases.keys())
|
|
|
|
|
self._cmds_cache = frozenset(allcmds)
|
|
|
|
|
return self._cmds_cache
|
2015-05-25 12:51:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OPTIONS_PATH = os.path.expanduser('~') + "/.xonsh_man_completions"
|
|
|
|
|
SCRAPE_RE = re.compile(r'^(?:\s*(?:-\w|--[a-z0-9-]+)[\s,])+', re.M)
|
|
|
|
|
INNER_OPTIONS_RE = re.compile(r'-\w|--[a-z0-9-]+')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ManCompleter(object):
|
|
|
|
|
"""Helper class that loads completions derived from man pages."""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self._load_cached_options()
|
|
|
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
|
self._save_cached_options()
|
|
|
|
|
|
|
|
|
|
def option_complete(self, prefix, cmd):
|
|
|
|
|
"""Completes an option name, basing on content of man page."""
|
2015-08-02 16:14:56 -05:00
|
|
|
|
env = builtins.__xonsh_env__
|
|
|
|
|
csc = env.get('CASE_SENSITIVE_COMPLETIONS', True)
|
|
|
|
|
startswither = startswithnorm if csc else startswithlow
|
2015-05-25 12:51:07 +02:00
|
|
|
|
if cmd not in self._options.keys():
|
2015-06-17 23:23:15 +02:00
|
|
|
|
try:
|
2015-08-03 20:46:41 -04:00
|
|
|
|
manpage = subprocess.Popen(["man", cmd],
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.DEVNULL)
|
2015-06-17 23:23:15 +02:00
|
|
|
|
# This is a trick to get rid of reverse line feeds
|
2015-08-03 20:46:41 -04:00
|
|
|
|
text = subprocess.check_output(["col", "-b"],
|
|
|
|
|
stdin=manpage.stdout)
|
|
|
|
|
text = text.decode('utf-8')
|
2015-06-17 23:23:15 +02:00
|
|
|
|
scraped_text = ' '.join(SCRAPE_RE.findall(text))
|
|
|
|
|
matches = INNER_OPTIONS_RE.findall(scraped_text)
|
|
|
|
|
self._options[cmd] = matches
|
|
|
|
|
except:
|
|
|
|
|
return set()
|
2015-07-30 19:40:19 -05:00
|
|
|
|
prefixlow = prefix.lower()
|
2015-08-03 20:46:41 -04:00
|
|
|
|
return {s for s in self._options[cmd]
|
|
|
|
|
if startswither(s, prefix, prefixlow)}
|
2015-05-25 12:51:07 +02:00
|
|
|
|
|
|
|
|
|
def _load_cached_options(self):
|
|
|
|
|
"""Load options from file at startup."""
|
|
|
|
|
try:
|
|
|
|
|
with open(OPTIONS_PATH, 'rb') as f:
|
|
|
|
|
self._options = pickle.load(f)
|
|
|
|
|
except:
|
|
|
|
|
self._options = {}
|
|
|
|
|
|
|
|
|
|
def _save_cached_options(self):
|
|
|
|
|
"""Save completions to file."""
|
|
|
|
|
with open(OPTIONS_PATH, 'wb') as f:
|
|
|
|
|
pickle.dump(self._options, f)
|