rewrite bash completer

This commit is contained in:
BlahGeek 2016-08-23 22:00:27 +08:00
parent 10093a9f86
commit 188fee34dd
5 changed files with 39 additions and 168 deletions

View file

@ -1,13 +1,13 @@
**Added:** None
**Changed:** None
**Changed:**
* New implementation of bash completer with better performance and compatibility.
**Deprecated:** None
**Removed:** None
**Fixed:**
* Source `bash_completion` at first on bash completion
**Fixed:** None
**Security:** None

View file

@ -3,14 +3,9 @@
import builtins
import collections.abc as abc
import xonsh.completers.bash as compbash
class Completer(object):
"""This provides a list of optional completions for the xonsh shell."""
def __init__(self):
compbash.update_bash_completion()
def complete(self, prefix, line, begidx, endidx, ctx=None):
"""Complete the string, given a possible execution context.

View file

@ -1,101 +1,38 @@
import os
import re
import shlex
import pickle
import hashlib
import pathlib
import builtins
import subprocess
import xonsh.lazyasd as xl
import xonsh.platform as xp
from xonsh.completers.path import _quote_paths
RE_DASHF = xl.LazyObject(lambda: re.compile(r'-F\s+(\w+)'),
globals(), 'RE_DASHF')
INITED = False
BASH_COMPLETE_HASH = None
BASH_COMPLETE_FUNCS = {}
BASH_COMPLETE_FILES = {}
CACHED_HASH = None
CACHED_FUNCS = None
CACHED_FILES = None
BASH_COMPLETE_SCRIPT = """
BASH_COMPLETION_DIR="nonexist"
BASH_COMPLETION_COMPAT_DIR="nonexist"
{completions_sources}
source "{filename}"
BASH_COMPLETE_SCRIPT = r'''
{sources}
if (complete -p "{cmd}" 2> /dev/null || echo _minimal) | grep --quiet -e "_minimal"
then
declare -f _completion_loader > /dev/null && _completion_loader "{cmd}"
fi
_func=$(complete -p {cmd} | grep -o -e '-F \w\+' | cut -d ' ' -f 2)
COMP_WORDS=({line})
COMP_LINE={comp_line}
COMP_POINT=${{#COMP_LINE}}
COMP_COUNT={end}
COMP_CWORD={n}
{func} {cmd} {prefix} {prev}
$_func {cmd} {prefix} {prev}
for ((i=0;i<${{#COMPREPLY[*]}};i++)) do echo ${{COMPREPLY[i]}}; done
"""
def update_bash_completion():
global BASH_COMPLETE_FUNCS, BASH_COMPLETE_FILES, BASH_COMPLETE_HASH
global CACHED_FUNCS, CACHED_FILES, CACHED_HASH, INITED
completers = builtins.__xonsh_env__.get('BASH_COMPLETIONS', ())
BASH_COMPLETE_HASH = hashlib.md5(repr(completers).encode()).hexdigest()
datadir = builtins.__xonsh_env__['XONSH_DATA_DIR']
cachefname = os.path.join(datadir, 'bash_completion_cache')
if not INITED:
if os.path.isfile(cachefname):
# load from cache
with open(cachefname, 'rb') as cache:
CACHED_HASH, CACHED_FUNCS, CACHED_FILES = pickle.load(cache)
BASH_COMPLETE_HASH = CACHED_HASH
BASH_COMPLETE_FUNCS = CACHED_FUNCS
BASH_COMPLETE_FILES = CACHED_FILES
else:
# create initial cache
_load_bash_complete_funcs()
_load_bash_complete_files()
CACHED_HASH = BASH_COMPLETE_HASH
CACHED_FUNCS = BASH_COMPLETE_FUNCS
CACHED_FILES = BASH_COMPLETE_FILES
with open(cachefname, 'wb') as cache:
val = (CACHED_HASH, CACHED_FUNCS, CACHED_FILES)
pickle.dump(val, cache)
INITED = True
invalid = ((not os.path.isfile(cachefname)) or
BASH_COMPLETE_HASH != CACHED_HASH or
_completions_time() > os.stat(cachefname).st_mtime)
if invalid:
# update the cache
_load_bash_complete_funcs()
_load_bash_complete_files()
CACHED_HASH = BASH_COMPLETE_HASH
CACHED_FUNCS = BASH_COMPLETE_FUNCS
CACHED_FILES = BASH_COMPLETE_FILES
with open(cachefname, 'wb') as cache:
val = (CACHED_HASH, BASH_COMPLETE_FUNCS, BASH_COMPLETE_FILES)
pickle.dump(val, cache)
'''
def complete_from_bash(prefix, line, begidx, endidx, ctx):
"""Completes based on results from BASH completion."""
update_bash_completion()
sources = _collect_completions_sources()
if not sources:
return set()
splt = line.split()
cmd = splt[0]
func = BASH_COMPLETE_FUNCS.get(cmd, None)
fnme = BASH_COMPLETE_FILES.get(cmd, None)
if func is None or fnme is None:
return set()
idx = n = 0
prev = ''
for n, tok in enumerate(splt):
if tok == prefix:
idx = line.find(prefix, idx)
@ -109,10 +46,14 @@ def complete_from_bash(prefix, line, begidx, endidx, ctx):
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,
sources='\n'.join(sources), line=' '.join(shlex.quote(p) for p in splt),
comp_line=shlex.quote(line), n=n, cmd=cmd,
end=endidx + 1, prefix=prefix, prev=shlex.quote(prev),
completions_sources='\n'.join(_collect_completions_sources()))
)
with open('/tmp/tmp.log', 'w') as f:
f.write(script)
try:
out = subprocess.check_output(
[xp.bash_command()], input=script, universal_newlines=True,
@ -124,59 +65,6 @@ def complete_from_bash(prefix, line, begidx, endidx, ctx):
return rtn
def _load_bash_complete_funcs():
global BASH_COMPLETE_FUNCS
BASH_COMPLETE_FUNCS = bcf = {}
inp = _collect_completions_sources()
if not inp:
return
inp.append('complete -p\n')
out = _source_completions(inp)
for line in out.splitlines():
head, _, cmd = line.rpartition(' ')
if len(cmd) == 0 or cmd == 'cd':
continue
m = RE_DASHF.search(head)
if m is None:
continue
bcf[cmd] = m.group(1)
def _load_bash_complete_files():
global BASH_COMPLETE_FILES
inp = _collect_completions_sources()
if not inp:
BASH_COMPLETE_FILES = {}
return
if BASH_COMPLETE_FUNCS:
inp.append('shopt -s extdebug')
bash_funcs = set(BASH_COMPLETE_FUNCS.values())
inp.append('declare -F ' + ' '.join([f for f in bash_funcs]))
inp.append('shopt -u extdebug\n')
out = _source_completions(inp)
func_files = {}
for line in out.splitlines():
parts = line.split()
if xp.ON_WINDOWS:
parts = [parts[0], ' '.join(parts[2:])]
func_files[parts[0]] = parts[-1]
BASH_COMPLETE_FILES = {
cmd: func_files[func]
for cmd, func in BASH_COMPLETE_FUNCS.items()
if func in func_files
}
def _source_completions(source):
try:
return subprocess.check_output(
[xp.bash_command()], input='\n'.join(source),
universal_newlines=True, env=builtins.__xonsh_env__.detype(),
stderr=subprocess.DEVNULL)
except FileNotFoundError:
return ''
def _collect_completions_sources():
sources = []
completers = builtins.__xonsh_env__.get('BASH_COMPLETIONS', ())
@ -188,9 +76,3 @@ def _collect_completions_sources():
for _file in (x for x in path.glob('*') if x.is_file()):
sources.append('source "{}"'.format(_file.as_posix()))
return sources
def _completions_time():
compfiles = builtins.__xonsh_env__.get('BASH_COMPLETIONS', ())
compfiles = [os.stat(x).st_mtime for x in compfiles if os.path.exists(x)]
return max(compfiles) if compfiles else 0

View file

@ -355,19 +355,20 @@ def DEFAULT_DOCS():
'shell.\n\nPressing the right arrow key inserts the currently '
'displayed suggestion. Only usable with $SHELL_TYPE=prompt_toolkit.'),
'BASH_COMPLETIONS': VarDocs(
'This is a list (or tuple) of strings that specifies where the BASH '
'completion files may be found. The default values are platform '
'This is a list (or tuple) of strings that specifies where the '
'`bash_completion` script may be found. For better performance, '
'base-completion v2.x is recommended since it lazy-loads individual '
'completion scripts. Paths or directories of individual completion '
'scripts (like `.../completes/ssh`) do not need to be included here. '
'The default values are platform '
'dependent, but sane. To specify an alternate list, do so in the run '
'control file.', default=(
"Normally this is:\n\n"
" ('/etc/bash_completion',\n"
" '/usr/share/bash-completion/completions/git')\n\n"
" ('/etc/bash_completion', )\n\n"
"But, on Mac it is:\n\n"
" ('/usr/local/etc/bash_completion',\n"
" '/opt/local/etc/profile.d/bash_completion.sh')\n\n"
" ('/usr/local/etc/bash_completion', )\n\n"
"And on Arch Linux it is:\n\n"
" ('/usr/share/bash-completion/bash_completion',\n"
" '/usr/share/bash-completion/completions/git')\n\n"
" ('/usr/share/bash-completion/bash_completion', )\n\n"
"Other OS-specific defaults may be added in the future.")),
'CASE_SENSITIVE_COMPLETIONS': VarDocs(
'Sets whether completions should be case sensitive or case '

View file

@ -271,22 +271,15 @@ def BASH_COMPLETIONS_DEFAULT():
"""
if ON_LINUX or ON_CYGWIN:
if linux_distro() == 'arch':
bcd = (
'/usr/share/bash-completion/bash_completion',
'/usr/share/bash-completion/completions')
bcd = ('/usr/share/bash-completion/bash_completion', )
else:
bcd = ('/usr/share/bash-completion',
'/usr/share/bash-completion/completions')
bcd = ('/usr/share/bash-completion', )
elif ON_DARWIN:
bcd = ('/usr/local/etc/bash_completion',
'/opt/local/etc/profile.d/bash_completion.sh')
bcd = ('/usr/local/share/bash-completion/bash_completion', # v2.x
'/usr/local/etc/bash_completion') # v1.x
elif ON_WINDOWS and git_for_windows_path():
bcd = (os.path.join(git_for_windows_path(),
'usr\\share\\bash-completion'),
os.path.join(git_for_windows_path(),
'usr\\share\\bash-completion\\completions'),
os.path.join(git_for_windows_path(),
'mingw64\\share\\git\\completion\\git-completion.bash'))
'usr\\share\\bash-completion'), )
else:
bcd = ()
return bcd