Source code for xonsh.completers.path

import ast
import glob
import os
import re

import xonsh.lib.lazyasd as xl
import xonsh.platform as xp
import xonsh.tools as xt
from xonsh.built_ins import XSH
from xonsh.completers.tools import RichCompletion, contextual_completer
from xonsh.parsers.completion_context import CommandContext


@xl.lazyobject
def PATTERN_NEED_QUOTES():
    pattern = r'\s`\$\{\}\[\]\,\*\(\)"\'\?&#'
    if xp.ON_WINDOWS:
        pattern += "%"
    pattern = "[" + pattern + "]" + r"|\band\b|\bor\b"
    return re.compile(pattern)


[docs] def cd_in_command(line): """Returns True if "cd" is a token in the line, False otherwise.""" lexer = XSH.execer.parser.lexer lexer.reset() lexer.input(line) have_cd = False for tok in lexer: if tok.type == "NAME" and tok.value == "cd": have_cd = True break return have_cd
def _get_normalized_pstring_quote(s): for pre, norm_pre in (("p", "p"), ("pr", "pr"), ("rp", "pr"), ("fp", "pf")): for q in ('"', "'"): if s.startswith(f"{pre}{q}"): return norm_pre, q return (None, None) def _path_from_partial_string(inp, pos=None): if pos is None: pos = len(inp) partial = inp[:pos] startix, endix, quote = xt.check_for_partial_string(partial) _post = "" if startix is None: return None elif endix is None: string = partial[startix:] else: if endix != pos: _test = partial[endix:pos] if not any(i == " " for i in _test): _post = _test else: return None string = partial[startix:endix] # If 'pr'/'rp', treat as raw string, otherwise strip leading 'p' pstring_pre = _get_normalized_pstring_quote(quote)[0] if pstring_pre == "pr": string = f"r{string[2:]}" elif pstring_pre == "p": string = string[1:] end = xt.RE_STRING_START.sub("", quote) _string = string if not _string.endswith(end): _string = _string + end try: val = ast.literal_eval(_string) except (SyntaxError, ValueError): return None if isinstance(val, bytes): env = XSH.env val = val.decode( encoding=env.get("XONSH_ENCODING"), errors=env.get("XONSH_ENCODING_ERRORS") ) return string + _post, val + _post, quote, end def _normpath(p): """ Wraps os.normpath() to avoid removing './' at the beginning and '/' at the end. On windows it does the same with backslashes """ initial_dotslash = p.startswith(os.curdir + os.sep) initial_dotslash |= xp.ON_WINDOWS and p.startswith(os.curdir + os.altsep) p = p.rstrip() trailing_slash = p.endswith(os.sep) trailing_slash |= xp.ON_WINDOWS and p.endswith(os.altsep) p = os.path.normpath(p) if initial_dotslash and p != ".": p = os.path.join(os.curdir, p) if trailing_slash: p = os.path.join(p, "") if xp.ON_WINDOWS and XSH.env.get("FORCE_POSIX_PATHS"): p = p.replace(os.sep, os.altsep) return p def _startswithlow(x, start, startlow=None): if startlow is None: startlow = start.lower() return x.startswith(start) or x.lower().startswith(startlow) def _startswithnorm(x, start, startlow=None): return x.startswith(start) def _dots(prefix): complete_dots = XSH.env.get("COMPLETE_DOTS", "matching").lower() if complete_dots == "never": return () slash = xt.get_sep() if slash == "\\": slash = "" prefixes = {"."} if complete_dots == "always": prefixes.add("") if prefix in prefixes: return ("." + slash, ".." + slash) elif prefix == "..": return (".." + slash,) else: return () def _add_cdpaths(paths, prefix): """Completes current prefix using CDPATH""" env = XSH.env csc = env.get("CASE_SENSITIVE_COMPLETIONS") glob_sorted = env.get("GLOB_SORTED") for cdp in env.get("CDPATH"): test_glob = os.path.join(cdp, prefix) + "*" for s in xt.iglobpath( test_glob, ignore_case=(not csc), sort_result=glob_sorted ): if os.path.isdir(s): paths.add(os.path.relpath(s, cdp)) def _quote_to_use(x): single = "'" double = '"' if single in x and double not in x: return double else: return single def _is_directory_in_cdpath(path): env = XSH.env for cdp in env.get("CDPATH"): if os.path.isdir(os.path.join(cdp, path)): return True return False def _quote_paths(paths, start, end, append_end=True, cdpath=False): expand_path = XSH.expand_path out = set() space = " " backslash = "\\" double_backslash = "\\\\" slash = xt.get_sep() orig_start = start orig_end = end # quote on all or none, to make readline completes to max prefix need_quotes = any( re.search(PATTERN_NEED_QUOTES, x) or (backslash in x and slash != backslash) for x in paths ) for s in paths: start = orig_start end = orig_end if start == "" and need_quotes: start = end = _quote_to_use(s) expanded = expand_path(s) if os.path.isdir(expanded) or (cdpath and _is_directory_in_cdpath(expanded)): _tail = slash elif end == "": _tail = space else: _tail = "" if start != "" and "r" not in start and backslash in s: start = f"r{start}" s = s + _tail if end != "": if "r" not in start.lower(): s = s.replace(backslash, double_backslash) if end in s: s = s.replace(end, "".join(f"\\{i}" for i in end)) s = start + s + end if append_end else start + s out.add(s) return out, need_quotes def _joinpath(path): # convert our tuple representation back into a string representing a path if path is None: return "" elif len(path) == 0: return "" elif path == ("",): return xt.get_sep() elif path[0] == "": return xt.get_sep() + _normpath(os.path.join(*path)) else: return _normpath(os.path.join(*path)) def _splitpath(path): # convert a path into an intermediate tuple representation # if this tuple starts with '', it means that the path was an absolute path path = _normpath(path) if path.startswith(xt.get_sep()): pre = ("",) else: pre = () return pre + _splitpath_helper(path, ()) def _splitpath_helper(path, sofar=()): folder, path = os.path.split(path) if path: sofar = sofar + (path,) if not folder or folder == xt.get_sep(): return sofar[::-1] elif xp.ON_WINDOWS and not path: return os.path.splitdrive(folder)[:1] + sofar[::-1] elif xp.ON_WINDOWS and os.path.splitdrive(path)[0]: return sofar[::-1] return _splitpath_helper(folder, sofar)
[docs] def subsequence_match(ref, typed, csc): """ Detects whether typed is a subsequence of ref. Returns ``True`` if the characters in ``typed`` appear (in order) in ``ref``, regardless of exactly where in ``ref`` they occur. If ``csc`` is ``False``, ignore the case of ``ref`` and ``typed``. Used in "subsequence" path completion (e.g., ``~/u/ro`` expands to ``~/lou/carcohl``) """ if csc: return _subsequence_match_iter(ref, typed) else: return _subsequence_match_iter(ref.lower(), typed.lower())
def _subsequence_match_iter(ref, typed): if len(typed) == 0: return True elif len(ref) == 0: return False elif ref[0] == typed[0]: return _subsequence_match_iter(ref[1:], typed[1:]) else: return _subsequence_match_iter(ref[1:], typed) def _expand_one(sofar, nextone, csc): out = set() glob_sorted = XSH.env.get("GLOB_SORTED") for i in sofar: _glob = os.path.join(_joinpath(i), "*") if i is not None else "*" for j in xt.iglobpath(_glob, sort_result=glob_sorted): j = os.path.basename(j) if subsequence_match(j, nextone, csc): out.add((i or ()) + (j,)) return out def _complete_path_raw(prefix, line, start, end, ctx, cdpath=True, filtfunc=None): # string stuff for automatic quoting path_str_start = "" path_str_end = "" append_end = True p = _path_from_partial_string(line, end) lprefix = len(prefix) if p is not None: lprefix = len(p[0]) # Compensate for 'p' if p-string variant pstring_pre = _get_normalized_pstring_quote(p[2])[0] if pstring_pre in ("pr", "p"): lprefix += 1 prefix = p[1] path_str_start = p[2] path_str_end = p[3] if len(line) >= end + 1 and line[end] == path_str_end: append_end = False tilde = "~" paths = set() env = XSH.env csc = env.get("CASE_SENSITIVE_COMPLETIONS") glob_sorted = env.get("GLOB_SORTED") prefix = glob.escape(prefix) for s in xt.iglobpath(prefix + "*", ignore_case=(not csc), sort_result=glob_sorted): paths.add(s) if len(paths) == 0 and env.get("SUBSEQUENCE_PATH_COMPLETION"): # this block implements 'subsequence' matching, similar to fish and zsh. # matches are based on subsequences, not substrings. # e.g., ~/u/ro completes to ~/lou/carcolh # see above functions for details. p = _splitpath(os.path.expanduser(prefix)) p_len = len(p) if p_len != 0: relative_char = ["", ".", ".."] if p[0] in relative_char: i = 0 while i < p_len and p[i] in relative_char: i += 1 basedir = p[:i] p = p[i:] else: basedir = None matches_so_far = {basedir} for i in p: matches_so_far = _expand_one(matches_so_far, i, csc) paths |= {_joinpath(i) for i in matches_so_far} if len(paths) == 0 and env.get("FUZZY_PATH_COMPLETION"): threshold = env.get("SUGGEST_THRESHOLD") for s in xt.iglobpath( os.path.dirname(prefix) + "*", ignore_case=(not csc), sort_result=glob_sorted, ): if xt.levenshtein(prefix, s, threshold) < threshold: paths.add(s) if cdpath and cd_in_command(line): _add_cdpaths(paths, prefix) paths = set(filter(filtfunc, paths)) if tilde in prefix: home = os.path.expanduser(tilde) paths = {s.replace(home, tilde) for s in paths} paths, _ = _quote_paths( {_normpath(s) for s in paths}, path_str_start, path_str_end, append_end, cdpath ) paths.update(filter(filtfunc, _dots(prefix))) return paths, lprefix
[docs] @contextual_completer def complete_path(context): """Completes path names.""" if context.command: return contextual_complete_path(context.command) elif context.python: line = context.python.prefix # simple prefix _complete_path_raw will handle gracefully: prefix = line.rsplit(" ", 1)[-1] return _complete_path_raw(prefix, line, len(line) - len(prefix), len(line), {}) return set(), 0
[docs] def contextual_complete_path(command: CommandContext, cdpath=True, filtfunc=None): # ``_complete_path_raw`` may add opening quotes: prefix = command.raw_prefix completions, lprefix = _complete_path_raw( prefix, prefix, 0, len(prefix), ctx={}, cdpath=cdpath, filtfunc=filtfunc, ) # ``_complete_path_raw`` may have added closing quotes: rich_completions = { RichCompletion(comp, append_closing_quote=False) for comp in completions } return rich_completions, lprefix
[docs] def complete_dir(command: CommandContext): return contextual_complete_path(command, filtfunc=os.path.isdir)