From a44409329087b817c59f53798db8df644fe7b2d6 Mon Sep 17 00:00:00 2001 From: Anthony Scopatz Date: Wed, 12 Sep 2018 18:12:02 -0400 Subject: [PATCH] dotfile matching --- news/dotglob.rst | 17 +++++++++++++++ tests/.somedotfile | 0 tests/bin/.someotherdotfile | 0 tests/test_tools.py | 32 +++++++++++++++++++++++++++++ xonsh/built_ins.py | 4 +++- xonsh/environ.py | 6 ++++++ xonsh/tools.py | 41 ++++++++++++++++++++++++++++++++----- 7 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 news/dotglob.rst create mode 100644 tests/.somedotfile create mode 100644 tests/bin/.someotherdotfile diff --git a/news/dotglob.rst b/news/dotglob.rst new file mode 100644 index 000000000..e014db19b --- /dev/null +++ b/news/dotglob.rst @@ -0,0 +1,17 @@ +**Added:** + +* New ``$DOTGLOB`` environment variable enables globs to match + "hidden" files which start with a literal ``.``. Set this + variable to ``True`` to get this matching behavior. + Cooresponding API changes have been made to + ``xonsh.tools.globpath()`` and ``xonsh.tools.iglobpath()`` + +**Changed:** None + +**Deprecated:** None + +**Removed:** None + +**Fixed:** None + +**Security:** None diff --git a/tests/.somedotfile b/tests/.somedotfile new file mode 100644 index 000000000..e69de29bb diff --git a/tests/bin/.someotherdotfile b/tests/bin/.someotherdotfile new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tools.py b/tests/test_tools.py index bf8c676eb..1379cfa7e 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1601,6 +1601,38 @@ def test_deprecated_past_expiry_raises_assertion_error(expired_version): my_function() +@skip_if_on_windows +def test_iglobpath_no_dotfiles(xonsh_builtins): + d = os.path.dirname(__file__) + g = d + '/*' + files = list(iglobpath(g, include_dotfiles=False)) + assert d + '/.somedotfile' not in files + + +@skip_if_on_windows +def test_iglobpath_dotfiles(xonsh_builtins): + d = os.path.dirname(__file__) + g = d + '/*' + files = list(iglobpath(g, include_dotfiles=True)) + assert d + '/.somedotfile' in files + + +@skip_if_on_windows +def test_iglobpath_no_dotfiles_recursive(xonsh_builtins): + d = os.path.dirname(__file__) + g = d + '/**' + files = list(iglobpath(g, include_dotfiles=False)) + assert d + '/bin/.someotherdotfile' not in files + + +@skip_if_on_windows +def test_iglobpath_dotfiles_recursive(xonsh_builtins): + d = os.path.dirname(__file__) + g = d + '/**' + files = list(iglobpath(g, include_dotfiles=True)) + assert d + '/bin/.someotherdotfile' in files + + def test_iglobpath_empty_str(monkeypatch, xonsh_builtins): # makes sure that iglobpath works, even when os.scandir() and os.listdir() # fail to return valid results, like an empty filename diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py index 4be8434f8..43a4f7e45 100644 --- a/xonsh/built_ins.py +++ b/xonsh/built_ins.py @@ -147,8 +147,10 @@ def regexsearch(s): def globsearch(s): csc = builtins.__xonsh_env__.get("CASE_SENSITIVE_COMPLETIONS") glob_sorted = builtins.__xonsh_env__.get("GLOB_SORTED") + dotglob = builtins.__xonsh_env__.get("DOTGLOB") return globpath( - s, ignore_case=(not csc), return_empty=True, sort_result=glob_sorted + s, ignore_case=(not csc), return_empty=True, sort_result=glob_sorted, + include_dotfiles=dotglob ) diff --git a/xonsh/environ.py b/xonsh/environ.py index e2ce7194d..4af85a8b5 100644 --- a/xonsh/environ.py +++ b/xonsh/environ.py @@ -177,6 +177,7 @@ def DEFAULT_ENSURERS(): "COMPLETIONS_MENU_ROWS": (is_int, int, str), "COMPLETION_QUERY_LIMIT": (is_int, int, str), "DIRSTACK_SIZE": (is_int, int, str), + "DOTGLOB": (is_bool, to_bool, bool_to_str), "DYNAMIC_CWD_WIDTH": ( is_dynamic_cwd_width, to_dynamic_cwd_tuple, @@ -356,6 +357,7 @@ def DEFAULT_VALUES(): "COMPLETIONS_MENU_ROWS": 5, "COMPLETION_QUERY_LIMIT": 100, "DIRSTACK_SIZE": 20, + "DOTGLOB": False, "DYNAMIC_CWD_WIDTH": (float("inf"), "c"), "DYNAMIC_CWD_ELISION_CHAR": "", "EXPAND_ENV_VARS": True, @@ -551,6 +553,10 @@ def DEFAULT_DOCS(): "for confirmation." ), "DIRSTACK_SIZE": VarDocs("Maximum size of the directory stack."), + "DOTGLOB": VarDocs('Globbing files with "*" or "**" will also match ' + "dotfiles, or those 'hidden' files whose names " + "begin with a literal '.'. Such files are filtered " + "out by default."), "DYNAMIC_CWD_WIDTH": VarDocs( "Maximum length in number of characters " "or as a percentage for the ``cwd`` prompt variable. For example, " diff --git a/xonsh/tools.py b/xonsh/tools.py index beae3304c..f2228f456 100644 --- a/xonsh/tools.py +++ b/xonsh/tools.py @@ -2087,45 +2087,76 @@ def expand_case_matching(s): return "".join(t) -def globpath(s, ignore_case=False, return_empty=False, sort_result=None): +def globpath(s, ignore_case=False, return_empty=False, sort_result=None, + include_dotfiles=None): """Simple wrapper around glob that also expands home and env vars.""" - o, s = _iglobpath(s, ignore_case=ignore_case, sort_result=sort_result) + o, s = _iglobpath(s, ignore_case=ignore_case, sort_result=sort_result, + include_dotfiles=include_dotfiles) o = list(o) no_match = [] if return_empty else [s] return o if len(o) != 0 else no_match -def _iglobpath(s, ignore_case=False, sort_result=None): +def _dotglobstr(s): + modified = False + dotted_s = s + if '/*' in dotted_s: + dotted_s = dotted_s.replace('/*', '/.*') + dotted_s = dotted_s.replace('/.**/.*', '/**/.*') + modified = True + if dotted_s.startswith('*') and not dotted_s.startswith('**'): + dotted_s = '.' + dotted_s + modified = True + return dotted_s, modified + + +def _iglobpath(s, ignore_case=False, sort_result=None, include_dotfiles=None): s = builtins.__xonsh_expand_path__(s) if sort_result is None: sort_result = builtins.__xonsh_env__.get("GLOB_SORTED") + if include_dotfiles is None: + include_dotfiles = builtins.__xonsh_env__.get("DOTGLOB") if ignore_case: s = expand_case_matching(s) if sys.version_info > (3, 5): if "**" in s and "**/*" not in s: s = s.replace("**", "**/*") + if include_dotfiles: + dotted_s, dotmodified = _dotglobstr(s) # `recursive` is only a 3.5+ kwarg. if sort_result: paths = glob.glob(s, recursive=True) + if include_dotfiles and dotmodified: + paths.extend(glob.iglob(dotted_s, recursive=True)) paths.sort() paths = iter(paths) else: paths = glob.iglob(s, recursive=True) + if include_dotfiles and dotmodified: + paths = itertools.chain(glob.iglob(dotted_s, recursive=True), + paths) return paths, s else: + if include_dotfiles: + dotted_s, dotmodified = _dotglobstr(s) if sort_result: paths = glob.glob(s) + if include_dotfiles and dotmodified: + paths.extend(glob.iglob(dotted_s)) paths.sort() paths = iter(paths) else: paths = glob.iglob(s) + if include_dotfiles and dotmodified: + paths = itertools.chain(glob.iglob(dotted_s), paths) return paths, s -def iglobpath(s, ignore_case=False, sort_result=None): +def iglobpath(s, ignore_case=False, sort_result=None, include_dotfiles=None): """Simple wrapper around iglob that also expands home and env vars.""" try: - return _iglobpath(s, ignore_case=ignore_case, sort_result=sort_result)[0] + return _iglobpath(s, ignore_case=ignore_case, sort_result=sort_result, + include_dotfiles=include_dotfiles)[0] except IndexError: # something went wrong in the actual iglob() call return iter(())