Repository: xonsh/xonsh

Issue:
Can we enable dotglob somehow?
In bash, we can type `shopt -s dotglob` to make globbing with `*` also work on hidden dotfiles. Is there some way to do this in xonsh? xonsh doesn't recognize the `shopt` command.

My current xonfig:
```
+------------------+----------------------+
| xonsh            | 0.7.5                |
| Git SHA          | 8ab50b7e             |
| Commit Date      | Aug 16 15:26:52 2018 |
| Python           | 3.5.2                |
| PLY              | 3.9                  |
| have readline    | True                 |
| prompt toolkit   | 2.0.4                |
| shell type       | prompt_toolkit2      |
| pygments         | None                 |
| on posix         | True                 |
| on linux         | True                 |
| distro           | neon                 |
| on darwin        | False                |
| on windows       | False                |
| on cygwin        | False                |
| on msys2         | False                |
| is superuser     | False                |
| default encoding | utf-8                |
| xonsh encoding   | utf-8                |
| encoding errors  | surrogateescape      |
+------------------+----------------------+
```


Discussion:
Hey @battaglia01 -- can you elaborate a little more?  There are a couple of different ways to glob in `xonsh`

If you use regex globbing, then you can get dotfiles along with other files by using 

```
`.*`
```

or if you only want dotfiles, you can do either of

```
`\..*`
g`.*`
```
In bash, if you type `*` by default, it leaves out dotfiles. You have to deliberately type `.*` to get those. You can alter this behavior in bash by typing `shopt -s dotglob`, which makes `*` now glob dotfiles by default as well. I'm wondering if there's a way to do something similar in xonsh (and if not, this could be good to add).

Resolving patch:
diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -147,8 +147,13 @@ 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
--- 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,12 @@ 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
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -2087,45 +2087,84 @@ 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(())


Test patch:
diff --git a/tests/.somedotfile b/tests/.somedotfile
new file mode 100644
diff --git a/tests/bin/.someotherdotfile b/tests/bin/.someotherdotfile
new file mode 100644
diff --git a/tests/test_tools.py b/tests/test_tools.py
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -74,7 +74,7 @@
 )
 from xonsh.environ import Env
 
-from tools import skip_if_on_windows, skip_if_on_unix
+from tools import skip_if_on_windows, skip_if_on_unix, skip_if_py34
 
 LEXER = Lexer()
 LEXER.build()
@@ -1601,6 +1601,39 @@ def my_function():
         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_py34
+@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