mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 08:24:40 +01:00
rewrite bash completer
This commit is contained in:
parent
10093a9f86
commit
188fee34dd
5 changed files with 39 additions and 168 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 '
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue