Repository: facelessuser/wcmatch

Issue:
Negative patterns should probably not treat dots or symlinks special
To be completely effective, negate patterns should probably not treat symlinks or dots special. It makes it difficult to negate a range like this:

positive: `./*/*.py` -> negative `**/test/*`

It is similarly difficult with symlinks. Since negate is always applied to patterns that were already matched with dot and symlinks rules, and we are just filtering the results, negate patterns should probably match dots, special directories, and symlinks with `*`, `[]`, `**`, `?`, etc.

Discussion:

Patch:
diff --git a/docs/src/markdown/changelog.md b/docs/src/markdown/changelog.md
index 6d6bdc7..462326d 100644
--- a/docs/src/markdown/changelog.md
+++ b/docs/src/markdown/changelog.md
@@ -1,10 +1,14 @@
 # Changelog
 
-## 3.1.0
+## 4.0.0
 
 - **NEW**: Deprecated `WcMatch` class methods `kill` and `reset`. `WcMatch` should be broken with a simple `break` statement instead.
 - **NEW**: Add a new flag `MARK` to force `glob` to return directories with a trailing slash.
 - **NEW**: Add `MATCHBASE` that causes `glob` and `WcMatch`, when the pattern has no slashes in it, to seek for any file anywhere in the tree with a matching basename.
+- **NEW**: Exclusion patterns (enabled with `NEGATE`) now always enable `DOTALL` in the exclusion patterns. They also will match symlinks in `**` patterns. Only non `NEGATE` patterns that are paired with a `NEGATE` pattern are subject to symlinks and dot rules. Exclusion patterns themselves allow dots and symlinks to make filtering easier.
+- **NEW**: Exclusion patterns no longer provide a default regular pattern if one is not applied. Exclusion patterns are
+meant to filter the results of normal patterns. You can either use the `SPLIT` flag and provide a pattern with your default ('default_pattern|!exclusion'), or feed in a list of multiple patterns instead of a single string (`['default_pattern', '!exclusion']`). If you really need the old behavior, you can use the `NEGDEFAULT` flag which will provide a default of `**` which is subject to the `GLOBSTAR` flag. `NEGDEFAULT` will raise a deprecation warning and will be removed in the future.
+- **FIX**: An empty pattern in `glob` should not match slashes.
 
 ## 3.0.2
 
@@ -40,7 +44,7 @@
 ## 2.1.0
 
 - **NEW**: Deprecate `version` and `version_info` in favor of the more standard `__version__` and `__version_info__`.
-- **FIX**: Fix issue where negated patterns would trigger before end of path.
+- **FIX**: Fix issue where exclusion patterns would trigger before end of path.
 - **FIX**: Fix `GLOBSTAR` regular expression pattern issues.
 
 ## 2.0.3
diff --git a/docs/src/markdown/fnmatch.md b/docs/src/markdown/fnmatch.md
index c87b139..6d49a18 100644
--- a/docs/src/markdown/fnmatch.md
+++ b/docs/src/markdown/fnmatch.md
@@ -19,7 +19,7 @@ Pattern           | Meaning
 `[!seq]`          | Matches any character not in seq.
 `[[:alnum:]]`     | POSIX style character classes inside sequences. The `C` locale is used for byte strings and Unicode properties for Unicode strings. See [POSIX Character Classes](#posix-character-classes) for more info.
 `\`               | Escapes characters. If applied to a meta character, it will be treated as a normal character.
-`!`               | Inverse pattern (with configuration, can use `-` instead of `!`).
+`!`               | Exclusion pattern (with configuration, can use `-` instead of `!`).
 `?(pattern_list)` | The pattern matches if zero or one occurrences of any of the patterns in the `pattern_list` match the input string.
 `*(pattern_list)` | The pattern matches if zero or more occurrences of any of the patterns in the `pattern_list` match the input string.
 `+(pattern_list)` | The pattern matches if one or more occurrences of any of the patterns in the `pattern_list` match the input string.
@@ -57,17 +57,17 @@ When applying multiple patterns, a file matches if it matches any of the pattern
 True
 ```
 
-Inverse patterns are allowed as well.
+Exclusion patterns are allowed as well.
 
 ```pycon3
 >>> from wcmatch import fnmatch
->>> fnmatch.fnmatch('test.py', r'!*.py', flags=fnmatch.NEGATE)
+>>> fnmatch.fnmatch('test.py', r'*|!*.py', flags=fnmatch.NEGATE | fnamtch.SPLIT)
 False
->>> fnmatch.fnmatch('test.txt', r'!*.py', flags=fnmatch.NEGATE)
+>>> fnmatch.fnmatch('test.txt', r'*|!*.py', flags=fnmatch.NEGATE | fnamtch.SPLIT)
 True
 ```
 
-When inverse patterns are used in conjunction with other patterns, a file will be considered matched if one of the positive patterns match **and** none of the inverse patterns match. If only inverse patterns are applied, the file must not match any of the patterns.
+When exclusion patterns are used in conjunction with other patterns, a file will be considered matched if one of the regular patterns match **and** none of the exclusion patterns match. If an exclusion pattern is given without any regular patterns, the pattern will match nothing. Exclusion patterns are meant to filter other patterns, not match anything by themselves.
 
 ```pycon3
 >>> from wcmatch import fnmatch
@@ -115,14 +115,14 @@ def fnsplit(pattern, *, flags=0):
 def translate(patterns, *, flags=0):
 ```
 
-`translate` takes a file pattern (or list of patterns) and returns two lists: one for positive patterns and one for inverse patterns. The lists contain the regular expressions used for matching the given patterns.
+`translate` takes a file pattern (or list of patterns) and returns two lists: one for normal patterns and one for exclusion patterns. The lists contain the regular expressions used for matching the given patterns. All patterns are constructed in such a way that a matched pattern is what is desired, regardless of whether the pattern is an exclusion pattern or regular pattern. It should be noted that a file is considered matched if it matches at least one regular pattern and all of the exclusion patterns.
 
 ```pycon3
 >>> from wcmatch import translate
 >>> fnmatch.translate(r'*.{a,{b,c}}', flags=fnmatch.BRACE)
 (['^(?s:(?=.).*?\\.a)$', '^(?s:(?=.).*?\\.b)$', '^(?s:(?=.).*?\\.c)$'], [])
->>> fnmatch.translate(r'!*.{a,{b,c}}', flags=fnmatch.BRACE | fnmatch.NEGATE)
-([], ['^(?!(?s:(?=.).*?\\.a)).*?$', '^(?!(?s:(?=.).*?\\.b)).*?$', '^(?!(?s:(?=.).*?\\.c)).*?$'])
+>>> fnmatch.translate(r'**|!*.{a,{b,c}}', flags=fnmatch.BRACE | fnmatch.NEGATE | fnmatch.SPLIT)
+(['^(?s:(?=.)(?![.]).*?)$'], ['^(?!(?s:(?=.).*?\\.a)$).*?$', '^(?!(?s:(?=.).*?\\.b)$).*?$', '^(?!(?s:(?=.).*?\\.c)$).*?$'])
 ```
 
 ## Flags
@@ -141,11 +141,17 @@ def translate(patterns, *, flags=0):
 
 #### `fnmatch.NEGATE, fnmatch.N` {: #fnmatchnegate}
 
-`NEGATE` causes patterns that start with `!` to be treated as inverse matches. A pattern of `!*.py` would match any file but Python files. If used with [`EXTMATCH`](#fnmatchextmatch), patterns like `!(inverse|pattern)` will be mistakenly parsed as an inverse pattern instead of an inverse `extmatch` group.  See [`MINUSNEGATE`](#fnmatchminusnegate) for an alternative syntax that plays nice with `EXTMATCH`.
+`NEGATE` causes patterns that start with `!` to be treated as exclusion matches. A pattern of `!*.py` would match any file but Python files. If used with [`EXTMATCH`](#fnmatchextmatch), patterns like `!(inverse|pattern)` will be mistakenly parsed as an exclusion pattern instead of an inverse `extmatch` group.  See [`MINUSNEGATE`](#fnmatchminusnegate) for an alternative syntax that plays nice with `EXTMATCH`.
+
+!!! warning "Change 4.0"
+    In 4.0, `NEGATE` now requires a non-exclusion pattern to be paired with it or it will match nothing. You can either
+    use [`SPLIT`](#fnmatchSPLIT), or feed in a list of multiple patterns instead of a single string. If you really
+    need the old behavior, you can use the `NEGDEFAULT` flag which will provide a default of `**` which is subject to
+    the `GLOBSTAR` flag. `NEGDEFAULT` will raise a deprecation warning and will be removed in the future.
 
 #### `fnmatch.MINUSNEGATE, fnmatch.M` {: #fnmatchminusnegate}
 
-When `MINUSNEGATE` is used with [`NEGATE`](#fnmatchnegate), negate patterns are recognized by a pattern starting with `-` instead of `!`. This plays nice with the [`EXTMATCH`](#fnmatchextmatch) option.
+When `MINUSNEGATE` is used with [`NEGATE`](#fnmatchnegate), exclusion patterns are recognized by a pattern starting with `-` instead of `!`. This plays nice with the [`EXTMATCH`](#fnmatchextmatch) option.
 
 #### `fnmatch.DOTMATCH, fnmatch.D` {: #fnmatchdotmatch}
 
diff --git a/docs/src/markdown/glob.md b/docs/src/markdown/glob.md
index dd55190..c2a2963 100644
--- a/docs/src/markdown/glob.md
+++ b/docs/src/markdown/glob.md
@@ -20,7 +20,7 @@ Pattern           | Meaning
 `[!seq]`          | Matches any character not in seq.
 `[[:alnum:]]`     | POSIX style character classes inside sequences.  The `C` locale is used for byte strings and Unicode properties for Unicode strings. See [POSIX Character Classes](#posix-character-classes) for more info.
 `\`               | Escapes characters. If applied to a meta character, it will be treated as a normal character.
-`!`               | Inverse pattern (with configuration, can use `-` instead of `!`).
+`!`               | Exclusion pattern (with configuration, can use `-` instead of `!`).
 `?(pattern_list)` | The pattern matches if zero or one occurrences of any of the patterns in the `pattern_list` match the input string.
 `*(pattern_list)` | The pattern matches if zero or more occurrences of any of the patterns in the `pattern_list` match the input string.
 `+(pattern_list)` | The pattern matches if one or more occurrences of any of the patterns in the `pattern_list` match the input string.
@@ -70,7 +70,7 @@ Pattern           | Meaning
 def glob(patterns, *, flags=0):
 ```
 
-`glob` takes a pattern (or list of patterns) and will crawl the file system returning matching files. If a file/folder matches any positive patterns, it is considered a match.  If it matches *any* inverse pattern (when enabling the [`NEGATE`](#globnegate) flag), then it will be not be returned.
+`glob` takes a pattern (or list of patterns) and will crawl the file system returning matching files. If a file/folder matches any regular patterns, it is considered a match.  If it matches *any* exclusion pattern (when enabling the [`NEGATE`](#globnegate) flag), then it will be not be returned.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -130,7 +130,7 @@ When applying multiple patterns, a file path matches if it matches any of the pa
 True
 ```
 
-Inverse patterns are allowed as well.
+Exclusion patterns are allowed as well.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -140,7 +140,7 @@ False
 True
 ```
 
-When inverse patterns are used in conjunction with other patterns, a path will be considered matched if one of the positive patterns match **and** none of the inverse patterns match. If only inverse patterns are applied, the path must not match any of the patterns.
+When exclusion patterns are used in conjunction with other patterns, a path will be considered matched if one of the positive patterns match **and** none of the exclusion patterns match. If an exclusion pattern is given without any regular patterns, the pattern will match nothing. Exclusion patterns are meant to filter other patterns, not match anything by themselves.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -217,14 +217,14 @@ def globsplit(pattern, *, flags=0):
 def translate(patterns, *, flags=0):
 ```
 
-`translate` takes a glob pattern (or list of patterns) and returns two lists: one for positive patterns and one for inverse patterns. The lists contain the regular expressions used for matching the given patterns. Patterns should be used by matching a file against the patterns in the positive list and the negative list. A file is considered matching if it it matches at least one positive pattern and does not match any negative patterns.
+`translate` takes a file pattern (or list of patterns) and returns two lists: one for normal patterns and one for exclusion patterns. The lists contain the regular expressions used for matching the given patterns. All patterns are constructed in such a way that a matched pattern is what is desired, regardless of whether the pattern is an exclusion pattern or regular pattern. It should be noted that a file is considered matched if it matches at least one regular pattern and all of the exclusion patterns.
 
 ```pycon3
 >>> from wcmatch import glob
 >>> glob.translate(r'**/*.{py,txt}')
 (['^(?s:(?:(?!(?:\\/|^)\\.).)*?(?:^|$|\\/)+(?=.)(?!(?:\\.{1,2})(?:$|\\/))(?:(?!\\.)[^\\/]*?)?\\.py[\\/]*?)$', '^(?s:(?:(?!(?:\\/|^)\\.).)*?(?:^|$|\\/)+(?=.)(?!(?:\\.{1,2})(?:$|\\/))(?:(?!\\.)[^\\/]*?)?\\.txt[\\/]*?)$'], [])
->>> glob.translate(r'!**/*.{py,txt}', flags=glob.NEGATE)
-([], ['^(?!(?s:(?:(?!(?:\\/|^)\\.).)*?(?:^|$|\\/)+(?=.)(?!(?:\\.{1,2})(?:$|\\/))(?:(?!\\.)[^\\/]*?)?\\.py[\\/]*?)).*?$', '^(?!(?s:(?:(?!(?:\\/|^)\\.).)*?(?:^|$|\\/)+(?=.)(?!(?:\\.{1,2})(?:$|\\/))(?:(?!\\.)[^\\/]*?)?\\.txt[\\/]*?)).*?$'])
+>>> glob.translate(r'**|!**/*.{py,txt}', flags=glob.NEGATE | glob.SPLIT)
+(['^(?s:(?=.)(?!(?:\\.{1,2})(?:$|\\/))(?:(?!\\.)[^\\/]*?)?[\\/]*?)$'], ['^(?!(?s:(?=.)(?!(?:\\.{1,2})(?:$|\\/))[^\\/]*?\\/+(?=.)(?!(?:\\.{1,2})(?:$|\\/))[^\\/]*?\\.\\{py\\,txt\\}[\\/]*?)$).*?$'])
 ```
 
 #### `glob.escape`
@@ -283,11 +283,17 @@ On Windows, `FORCECASE` will also force paths to be treated like Linux/Unix path
 
 #### `glob.NEGATE, glob.N` {: #globnegate}
 
-`NEGATE` causes patterns that start with `!` to be treated as inverse matches. A pattern of `!*.py` would match any file but Python files. If used with the extended glob feature, patterns like `!(inverse|pattern)` will be mistakenly parsed as an inverse pattern instead of as an inverse extended glob group.  See [`MINUSNEGATE`](#globminusgate) for an alternative syntax that plays nice with extended glob.
+`NEGATE` causes patterns that start with `!` to be treated as exclusion patterns. A pattern of `!*.py` would match any file but Python files. If used with the extended glob feature, patterns like `!(inverse|pattern)` will be mistakenly parsed as an exclusion pattern instead of as an inverse extended glob group.  See [`MINUSNEGATE`](#globminusgate) for an alternative syntax that plays nice with extended glob.
+
+!!! warning "Changes 4.0"
+    In 4.0, `NEGATE` now requires a non-exclusion pattern to be paired with it or it will match nothing. You can either
+    use [`SPLIT`](#fnmatchSPLIT), or feed in a list of multiple patterns instead of a single string. If you really
+    need the old behavior, you can use the `NEGDEFAULT` flag which will provide a default of `**` which is subject to
+    the `GLOBSTAR` flag. `NEGDEFAULT` will raise a deprecation warning and will be removed in the future.
 
 #### `glob.MINUSNEGATE, glob.M` {: #globminusnegate}
 
-When `MINUSNEGATE` is used with [`NEGATE`](#globnegate), negate patterns are recognized by a pattern starting with `-` instead of `!`. This plays nice with the extended glob feature which already uses `!` in patterns such as `!(...)`.
+When `MINUSNEGATE` is used with [`NEGATE`](#globnegate), exclusion patterns are recognized by a pattern starting with `-` instead of `!`. This plays nice with the extended glob feature which already uses `!` in patterns such as `!(...)`.
 
 #### `glob.GLOBSTAR, glob.G` {: #globglobstar}
 
diff --git a/wcmatch/__meta__.py b/wcmatch/__meta__.py
index f716932..edec59f 100644
--- a/wcmatch/__meta__.py
+++ b/wcmatch/__meta__.py
@@ -186,5 +186,5 @@ def parse_version(ver, pre=False):
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(3, 0, 2, "final")
+__version_info__ = Version(4, 0, 0, ".dev")
 __version__ = __version_info__._get_canonical()
diff --git a/wcmatch/_wcparse.py b/wcmatch/_wcparse.py
index f494451..a44ad3e 100644
--- a/wcmatch/_wcparse.py
+++ b/wcmatch/_wcparse.py
@@ -70,6 +70,8 @@ MATCHBASE = 0x2000
 _FORCEWIN = 0x100000000  # Forces Windows behavior (used to not assume Unix/Linux because of `FORCECASE` on Windows).
 _TRANSLATE = 0x200000000  # Lets us know we are performing a translation, and we just want the regex.
 _ANCHOR = 0x400000000  # The pattern, if it starts with a slash, is anchored to the working directory; strip the slash.
+_NO_TRANSLATE = 0x800000000  # Don't return translation pattern in positive form, but in the faster negative form.
+NEGDEFAULT = 0x1000000000  # Provide a default for exclude patterns
 
 FLAG_MASK = (
     FORCECASE |
@@ -85,9 +87,11 @@ FLAG_MASK = (
     REALPATH |
     FOLLOW |
     MATCHBASE |
+    NEGDEFAULT |
     _FORCEWIN |
     _TRANSLATE |
-    _ANCHOR
+    _ANCHOR |
+    _NO_TRANSLATE
 )
 CASE_FLAGS = FORCECASE | IGNORECASE
 
@@ -214,16 +218,21 @@ def translate(patterns, flags):
     if isinstance(patterns, (str, bytes)):
         patterns = [patterns]
 
-    flags |= _TRANSLATE
+    flags = (flags | _TRANSLATE) & FLAG_MASK
 
     for pattern in patterns:
         for expanded in expand_braces(pattern, flags):
             (negative if is_negative(expanded, flags) else positive).append(
-                WcParse(expanded, flags & FLAG_MASK).parse()
+                WcParse(expanded, flags).parse()
             )
 
-    if patterns and flags & REALPATH and negative and not positive:
-        positive.append(_compile(b'**' if isinstance(patterns[0], bytes) else '**', flags))
+    if patterns and negative and not positive:
+        if flags & NEGDEFAULT:
+            util.warn_deprecated('Automatic defaults for exclusion patterns is deprecated')
+            default = '**'
+            if isinstance(patterns[0], bytes):
+                default = os.fsencode(default)
+            positive.append(WcParse(default, flags).parse())
 
     return positive, negative
 
@@ -252,8 +261,13 @@ def compile(patterns, flags):  # noqa A001
         for expanded in expand_braces(pattern, flags):
             (negative if is_negative(expanded, flags) else positive).append(_compile(expanded, flags))
 
-    if patterns and flags & REALPATH and negative and not positive:
-        positive.append(_compile(b'**' if isinstance(patterns[0], bytes) else '**', flags))
+    if patterns and negative and not positive:
+        if flags & NEGDEFAULT:
+            util.warn_deprecated('Automatic defaults for exclusion patterns is deprecated')
+            default = '**'
+            if isinstance(patterns[0], bytes):
+                default = os.fsencode(default)
+            positive.append(_compile(default, flags))
 
     return WcRegexp(tuple(positive), tuple(negative), flags & REALPATH, flags & PATHNAME, flags & FOLLOW)
 
@@ -644,7 +658,10 @@ class WcParse(object):
         self.raw_chars = bool(flags & RAWCHARS)
         self.globstar = self.pathname and bool(flags & GLOBSTAR)
         self.realpath = bool(flags & REALPATH) and self.pathname
-        self.globstar_capture = self.realpath and not bool(flags & _TRANSLATE)
+        self.translate = bool(flags & _TRANSLATE)
+        self.globstar_capture = self.realpath and not self.translate
+        if flags & _NO_TRANSLATE:
+            self.translate = False
         self.dot = bool(flags & DOTMATCH)
         self.extend = bool(flags & EXTMATCH)
         self.matchbase = bool(flags & MATCHBASE)
@@ -1248,15 +1265,19 @@ class WcParse(object):
 
         result = ['']
         matchbase = ['']
-        negative = False
+        self.negative = False
 
         p = util.norm_pattern(self.pattern, not self.unix, self.raw_chars)
 
         p = p.decode('latin-1') if self.is_bytes else p
         if is_negative(p, self.flags):
-            negative = True
+            self.negative = True
             p = p[1:]
 
+        if self.negative:
+            self.globstar_capture = False
+            self.dot = True
+
         if self.anchor:
             p, number = (RE_ANCHOR if not self.win_drive_detect else RE_WIN_ANCHOR).subn('', p)
             if number:
@@ -1268,19 +1289,20 @@ class WcParse(object):
             self.root('**', matchbase)
             self.globstar = globstar
 
-        self.root(p, result)
+        if p:
+            self.root(p, result)
 
-        if self.matchbase:
+        if p and self.matchbase:
             result = matchbase + result
 
         case_flag = 'i' if not self.case_sensitive else ''
         if util.PY36:
             pattern = (
-                r'^(?!(?s%s:%s)$).*?$' if negative and not self.globstar_capture else r'^(?s%s:%s)$'
+                r'^(?!(?s%s:%s)$).*?$' if self.negative and self.translate else r'^(?s%s:%s)$'
             ) % (case_flag, ''.join(result))
         else:
             pattern = (
-                r'(?s%s)^(?!(?:%s)$).*?$' if negative and not self.globstar_capture else r'(?s%s)^(?:%s)$'
+                r'(?s%s)^(?!(?:%s)$).*?$' if self.negative and self.translate else r'(?s%s)^(?:%s)$'
             ) % (case_flag, ''.join(result))
 
         if self.is_bytes:
@@ -1363,7 +1385,7 @@ def _match_real(filename, include, exclude, follow, symlinks):
         matched = True
         if exclude:
             for pattern in exclude:
-                if _fs_match(pattern, filename, is_dir, sep, follow, symlinks):
+                if _fs_match(pattern, filename, is_dir, sep, True, symlinks):
                     matched = False
                     break
     return matched
@@ -1397,14 +1419,11 @@ def _match_pattern(filename, include, exclude, real, path, follow):
             matched = True
             break
 
-    if not include and exclude:
-        matched = True
-
     if matched:
         matched = True
         if exclude:
             for pattern in exclude:
-                if not pattern.fullmatch(filename):
+                if pattern.fullmatch(filename):
                     matched = False
                     break
     return matched
diff --git a/wcmatch/fnmatch.py b/wcmatch/fnmatch.py
index 224f110..f070df4 100644
--- a/wcmatch/fnmatch.py
+++ b/wcmatch/fnmatch.py
@@ -25,8 +25,9 @@ from . import _wcparse
 
 __all__ = (
     "EXTMATCH", "FORCECASE", "IGNORECASE", "RAWCHARS",
-    "NEGATE", "MINUSNEGATE", "DOTMATCH", "BRACE",
-    "F", "I", "R", "N", "M", "D", "E",
+    "NEGATE", "MINUSNEGATE", "DOTMATCH", "BRACE", "SPLIT",
+    "NEGDEFAULT",
+    "F", "I", "R", "N", "M", "D", "E", "S",
     "translate", "fnmatch", "filter", "fnsplit"
 )
 
@@ -39,6 +40,7 @@ D = DOTMATCH = _wcparse.DOTMATCH
 E = EXTMATCH = _wcparse.EXTMATCH
 B = BRACE = _wcparse.BRACE
 S = SPLIT = _wcparse.SPLIT
+NEGDEFAULT = _wcparse.NEGDEFAULT
 
 FLAG_MASK = (
     FORCECASE |
@@ -49,7 +51,8 @@ FLAG_MASK = (
     DOTMATCH |
     EXTMATCH |
     BRACE |
-    SPLIT
+    SPLIT |
+    NEGDEFAULT
 )
 
 
diff --git a/wcmatch/glob.py b/wcmatch/glob.py
index 46c9c32..6603ce6 100644
--- a/wcmatch/glob.py
+++ b/wcmatch/glob.py
@@ -23,13 +23,14 @@ IN THE SOFTWARE.
 import os
 import sys
 import functools
+import re
 from . import _wcparse
 from . import util
 
 __all__ = (
     "FORCECASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH",
     "EXTGLOB", "EXTMATCH", "GLOBSTAR", "NEGATE", "MINUSNEGATE", "BRACE",
-    "REALPATH", "FOLLOW", "MATCHBASE",
+    "REALPATH", "FOLLOW", "MATCHBASE", "MARK", "NEGDEFAULT",
     "F", "I", "R", "D", "E", "G", "N", "M", "P", "L", "X", 'K',
     "iglob", "glob", "globsplit", "globmatch", "globfilter", "escape"
 )
@@ -53,6 +54,7 @@ L = FOLLOW = _wcparse.FOLLOW
 S = SPLIT = _wcparse.SPLIT
 X = MATCHBASE = _wcparse.MATCHBASE
 K = MARK = 0x10000
+NEGDEFAULT = _wcparse.NEGDEFAULT
 
 FLAG_MASK = (
     FORCECASE |
@@ -67,7 +69,8 @@ FLAG_MASK = (
     REALPATH |
     FOLLOW |
     SPLIT |
-    MATCHBASE
+    MATCHBASE |
+    NEGDEFAULT
 )
 
 
@@ -92,6 +95,9 @@ class Glob(object):
         self.mark = bool(flags & MARK)
         if self.mark:
             flags ^= MARK
+        self.neg_default = bool(flags & NEGDEFAULT)
+        if self.neg_default:
+            flags ^= NEGDEFAULT
         self.flags = _flag_transform(flags | _wcparse.REALPATH) ^ _wcparse.REALPATH
         self.follow_links = bool(flags & FOLLOW)
         self.dot = bool(flags & DOTMATCH)
@@ -114,22 +120,25 @@ class Glob(object):
         """Parse patterns."""
 
         self.pattern = []
-        self.npatterns = None
-        npattern = []
+        self.npatterns = []
+        nflags = self.flags | _wcparse.REALPATH | _wcparse._NO_TRANSLATE
         for p in pattern:
             if _wcparse.is_negative(p, self.flags):
                 # Treat the inverse pattern as a normal pattern if it matches, we will exclude.
                 # This is faster as compiled patterns usually compare the include patterns first,
                 # and then the exclude, but glob will already know it wants to include the file.
-                npattern.append(p[1:])
+                self.npatterns.append(re.compile(_wcparse.translate(p, flags=nflags)[1][0]))
             else:
                 self.pattern.extend(
                     [_wcparse.WcPathSplit(x, self.flags).split() for x in _wcparse.expand_braces(p, self.flags)]
                 )
-        if npattern:
-            self.npatterns = _wcparse.compile(npattern, self.flags ^ (_wcparse.NEGATE | _wcparse.REALPATH))
-        if not self.pattern and self.npatterns is not None:
-            self.pattern.append(_wcparse.WcPathSplit((b'**' if self.is_bytes else '**'), self.flags).split())
+        if not self.pattern and self.npatterns:
+            if self.neg_default:
+                util.warn_deprecated('Automatic defaults for exclusion patterns is deprecated')
+                default = '**'
+                if self.is_bytes:
+                    default = os.fsencode(default)
+                self.pattern.append(_wcparse.WcPathSplit(default, self.flags).split())
 
     def _is_hidden(self, name):
         """Check if is file hidden."""
@@ -146,17 +155,24 @@ class Glob(object):
 
         return name in (b'..', '..')
 
-    def _match_excluded(self, filename, patterns):
-        """Call match real directly to skip unnecessary `exists` check."""
+    def _match_excluded(self, filename, is_dir):
+        """Check if file should be excluded."""
 
-        return _wcparse._match_real(
-            filename, patterns._include, patterns._exclude, patterns._follow, self.symlinks
-        )
+        if is_dir and not filename.endswith(self.sep):
+            filename += self.sep
 
-    def _is_excluded(self, path, dir_only):
+        matched = False
+        for pattern in self.npatterns:
+            if pattern.fullmatch(filename):
+                matched = True
+                break
+
+        return matched
+
+    def _is_excluded(self, path, is_dir):
         """Check if file is excluded."""
 
-        return self.npatterns and self._match_excluded(path, self.npatterns)
+        return self.npatterns and self._match_excluded(path, is_dir)
 
     def _match_literal(self, a, b=None):
         """Match two names."""
@@ -393,21 +409,21 @@ class Glob(object):
                                 if rest:
                                     this = rest.pop(0)
                                     for match, is_dir in self._glob(curdir, this, rest):
-                                        if not self._is_excluded(match, dir_only):
+                                        if not self._is_excluded(match, is_dir):
                                             yield self.format_path(match, is_dir, dir_only)
-                                elif not self._is_excluded(curdir, dir_only):
+                                elif not self._is_excluded(curdir, is_dir):
                                     yield self.format_path(curdir, is_dir, dir_only)
                     else:
                         # Return the file(s) and finish.
                         for match, is_dir in results:
-                            if os.path.lexists(match) and not self._is_excluded(match, dir_only):
+                            if os.path.lexists(match) and not self._is_excluded(match, is_dir):
                                 yield self.format_path(match, is_dir, dir_only)
                 else:
                     # Path starts with a magic pattern, let's get globbing
                     rest = pattern[:]
                     this = rest.pop(0)
                     for match, is_dir in self._glob(curdir if not curdir == self.current else self.empty, this, rest):
-                        if not self._is_excluded(match, dir_only):
+                        if not self._is_excluded(match, is_dir):
                             yield self.format_path(match, is_dir, dir_only)
 
 
diff --git a/wcmatch/util.py b/wcmatch/util.py
index ae5454a..c2ff135 100644
--- a/wcmatch/util.py
+++ b/wcmatch/util.py
@@ -261,3 +261,13 @@ def deprecated(message, stacklevel=2):  # pragma: no cover
             return func(*args, **kwargs)
         return _func
     return _decorator
+
+
+def warn_deprecated(message, stacklevel=2):  # pragma: no cover
+    """Warn deprecated."""
+
+    warnings.warn(
+        message,
+        category=DeprecationWarning,
+        stacklevel=stacklevel
+    )
diff --git a/wcmatch/wcmatch.py b/wcmatch/wcmatch.py
index 60b5f73..eb58e43 100644
--- a/wcmatch/wcmatch.py
+++ b/wcmatch/wcmatch.py
@@ -28,7 +28,8 @@ from . import util
 __all__ = (
     "FORCECASE", "IGNORECASE", "RAWCHARS", "FILEPATHNAME", "DIRPATHNAME",
     "EXTMATCH", "GLOBSTAR", "BRACE", "MINUSNEGATE", "SYMLINKS", "HIDDEN", "RECURSIVE",
-    "F", "I", "R", "P", "E", "G", "M", "DP", "FP", "SL", "HD", "RV",
+    "MATCHBASE", "NEGDEFAULT",
+    "F", "I", "R", "P", "E", "G", "M", "DP", "FP", "SL", "HD", "RV", "X",
     "WcMatch"
 )
 
@@ -40,6 +41,7 @@ G = GLOBSTAR = _wcparse.GLOBSTAR
 B = BRACE = _wcparse.BRACE
 M = MINUSNEGATE = _wcparse.MINUSNEGATE
 X = MATCHBASE = _wcparse.MATCHBASE
+NEGDEFAULT = _wcparse.NEGDEFAULT
 
 # Control `PATHNAME` individually for folder exclude and files
 DP = DIRPATHNAME = 0x10000
@@ -64,7 +66,8 @@ FLAG_MASK = (
     SYMLINKS |
     HIDDEN |
     RECURSIVE |
-    MATCHBASE
+    MATCHBASE |
+    NEGDEFAULT
 )
 
 


Test patch:
diff --git a/tests/test_fnmatch.py b/tests/test_fnmatch.py
index 1c38511..0019436 100644
--- a/tests/test_fnmatch.py
+++ b/tests/test_fnmatch.py
@@ -238,7 +238,12 @@ class TestFnMatchFilter:
 
         # Issue #24
         ['*.bar', ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"], ["foo.bar", "foo.cfg.bar"], 0],
-        ['!*.bar', ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"], ["goo.cfg", "foo.bar.cfg"], fnmatch.N]
+        [
+            '*|!*.bar',
+            ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"],
+            ["goo.cfg", "foo.bar.cfg"],
+            fnmatch.N | fnmatch.S
+        ]
     ]
 
     @classmethod
@@ -486,3 +491,47 @@ class TestDeprecated(unittest.TestCase):
             self.assertTrue(len(w) == 1)
             self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
             self.assertTrue(patterns, ['test', 'test'])
+
+    def test_default_compile(self):
+        """Test deprecated default."""
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            self.assertTrue(fnmatch.fnmatch('name', '!test', flags=fnmatch.N | fnmatch.NEGDEFAULT))
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
+
+    def test_default_compile_bytes(self):
+        """Test deprecated default bytes."""
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            self.assertTrue(fnmatch.fnmatch(b'name', b'!test', flags=fnmatch.N | fnmatch.NEGDEFAULT))
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
+
+    def test_default_translate(self):
+        """Test deprecated default."""
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            fnmatch.translate('!test', flags=fnmatch.N | fnmatch.NEGDEFAULT)
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
+
+    def test_default_translate_bytes(self):
+        """Test deprecated default bytes."""
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            fnmatch.translate(b'!test', flags=fnmatch.N | fnmatch.NEGDEFAULT)
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
diff --git a/tests/test_glob.py b/tests/test_glob.py
index 4325de8..310a104 100644
--- a/tests/test_glob.py
+++ b/tests/test_glob.py
@@ -383,6 +383,7 @@ class Testglob(_TestGlob):
         [('[.a]',), [('a',)]],
         [('*.',), []],
 
+        Options(default_negate='**'),
         # Glob inverse
         [
             ('a*', '**'),
@@ -400,7 +401,6 @@ class Testglob(_TestGlob):
             ('**', 'EF'),
             [
             ] if not can_symlink() else [
-                ('sym3', 'EF')
             ],
             glob.N | glob.L
         ],
@@ -786,15 +786,11 @@ class Testglob(_TestGlob):
             ('**', 'EF'),
             [('a', 'bcd', 'EF'), ('EF',)] if not can_symlink() else [('a', 'bcd', 'EF'), ('EF',), ('sym3', 'EF')]
         ],
-        Options(just_negative=True),
+        Options(just_negative=True, default_negate='**'),
         [
             ('a*', '**'),
             [
-                ('EF',), ('ZZZ',)
             ] if not can_symlink() else [
-                ('EF',), ('ZZZ',),
-                ('sym1',), ('sym3',), ('sym2',), ('sym3', 'efg'), ('sym3', 'efg', 'ha'),
-                ('sym3', 'EF')
             ],
             glob.N
         ],
@@ -1052,3 +1048,25 @@ class TestDeprecated(unittest.TestCase):
             self.assertTrue(len(w) == 1)
             self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
             self.assertTrue(patterns, ['test', 'test'])
+
+    def test_default(self):
+        """Test deprecated default."""
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            glob.glob('!name', flags=glob.N | glob.NEGDEFAULT)
+            self.assertTrue(len(w) != 0)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
+
+    def test_default_bytes(self):
+        """Test deprecated default bytes."""
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            glob.glob(b'!name', flags=glob.N | glob.NEGDEFAULT)
+            self.assertTrue(len(w) != 0)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py
index acef01e..f66344c 100644
--- a/tests/test_globmatch.py
+++ b/tests/test_globmatch.py
@@ -7,6 +7,105 @@ import os
 import wcmatch.glob as glob
 import wcmatch._wcparse as _wcparse
 import wcmatch.util as util
+import shutil
+
+# Below is general helper stuff that Python uses in `unittests`.  As these
+# not meant for users, and could change without notice, include them
+# ourselves so we aren't surprised later.
+TESTFN = '@test'
+
+# Disambiguate `TESTFN` for parallel testing, while letting it remain a valid
+# module name.
+TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid())
+
+
+def create_empty_file(filename):
+    """Create an empty file. If the file already exists, truncate it."""
+
+    fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
+    os.close(fd)
+
+
+_can_symlink = None
+
+
+def can_symlink():
+    """Check if we can symlink."""
+
+    global _can_symlink
+    if _can_symlink is not None:
+        return _can_symlink
+    symlink_path = TESTFN + "can_symlink"
+    try:
+        os.symlink(TESTFN, symlink_path)
+        can = True
+    except (OSError, NotImplementedError, AttributeError):
+        can = False
+    else:
+        os.remove(symlink_path)
+    _can_symlink = can
+    return can
+
+
+def skip_unless_symlink(test):
+    """Skip decorator for tests that require functional symlink."""
+
+    ok = can_symlink()
+    msg = "Requires functional symlink implementation"
+    return test if ok else unittest.skip(msg)(test)
+
+
+class _TestGlobmatch(unittest.TestCase):
+    """Test the `WcMatch` class."""
+
+    def mktemp(self, *parts):
+        """Make temp directory."""
+
+        filename = self.norm(*parts)
+        base, file = os.path.split(filename)
+        if not os.path.exists(base):
+            retry = 3
+            while retry:
+                try:
+                    os.makedirs(base)
+                    retry = 0
+                except Exception:
+                    retry -= 1
+        create_empty_file(filename)
+
+    def force_err(self):
+        """Force an error."""
+
+        raise TypeError
+
+    def norm(self, *parts):
+        """Normalizes file path (in relation to temp directory)."""
+        tempdir = os.fsencode(self.tempdir) if isinstance(parts[0], bytes) else self.tempdir
+        return os.path.join(tempdir, *parts)
+
+    def norm_list(self, files):
+        """Normalize file list."""
+
+        return sorted([self.norm(os.path.normpath(x)) for x in files])
+
+    def setUp(self):
+        """Setup."""
+
+        self.tempdir = TESTFN + "_dir"
+        self.default_flags = glob.G | glob.P
+
+    def tearDown(self):
+        """Cleanup."""
+
+        retry = 3
+        while retry:
+            try:
+                shutil.rmtree(self.tempdir)
+                while os.path.exists(self.tempdir):
+                    pass
+                retry = 0
+            except Exception:
+                retry -= 1
 
 
 class GlobFiles():
@@ -295,7 +394,7 @@ class TestGlobFilter:
         GlobFiles(['d', 'e', '!ab', '!abc', 'a!b', '\\!a']),
 
         # anything that is NOT a* matches.
-        ['!a*', ['\\!a', 'd', 'e', '!ab', '!abc']],
+        ['**|!a*', ['\\!a', 'd', 'e', '!ab', '!abc'], glob.S],
 
         # anything that IS !a* matches.
         ['!a*', ['!ab', '!abc'], glob.N],
@@ -305,7 +404,7 @@ class TestGlobFilter:
         # ['!!a*', ['a!b']],
 
         # anything that is NOT !a* matches
-        ['!\\!a*', ['a!b', 'd', 'e', '\\!a']],
+        ['**|!\\!a*', ['a!b', 'd', 'e', '\\!a'], glob.S],
 
         # negation nestled within a pattern
         GlobFiles(
@@ -441,7 +540,7 @@ class TestGlobFilter:
             ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"]
         ),
         ['*.bar', ["foo.bar", "foo.cfg.bar"]],
-        ['!*.bar', ["goo.cfg", "foo.bar.cfg"]]
+        ['*|!*.bar', ["goo.cfg", "foo.bar.cfg"], glob.S]
     ]
 
     @classmethod
@@ -835,7 +934,7 @@ class TestGlobMatchSpecial(unittest.TestCase):
             )
         )
 
-    def test_glob_translate_real_has_positive_default(self):
+    def test_glob_translate_real_has_no_positive_default(self):
         """Test that `REALPATH` translations provide a default positive pattern."""
 
         pos, neg = glob.translate('!this', flags=self.flags)
@@ -843,7 +942,7 @@ class TestGlobMatchSpecial(unittest.TestCase):
         self.assertTrue(len(neg) == 1)
 
         pos, neg = glob.translate('!this', flags=self.flags | glob.REALPATH)
-        self.assertTrue(len(pos) == 1)
+        self.assertTrue(len(pos) == 0)
         self.assertTrue(len(neg) == 1)
 
     def test_glob_match_real(self):
@@ -879,9 +978,23 @@ class TestGlobMatchSpecial(unittest.TestCase):
 
         # Let's find something predictable for this cross platform test.
         user_dir = os.path.expanduser('~')
-        glob_user = glob.escape(user_dir)
-        self.assertFalse(glob.globmatch(user_dir, '**', flags=self.flags | glob.REALPATH))
-        self.assertTrue(glob.globmatch(user_dir, glob_user + '/**', flags=self.flags | glob.REALPATH))
+        if user_dir != '~':
+            glob_user = glob.escape(user_dir)
+            self.assertFalse(glob.globmatch(user_dir, '**', flags=self.flags | glob.REALPATH))
+            self.assertTrue(glob.globmatch(user_dir, glob_user + '/**', flags=self.flags | glob.REALPATH))
+
+    def test_glob_integrity_bytes(self):
+        """Test glob integrity to exercises the bytes portion of the code."""
+
+        self.assertTrue(
+            all(
+                [
+                    glob.globmatch(
+                        x, b'!**/*.md', flags=self.flags | glob.SPLIT
+                    ) for x in glob.glob(b'!**/*.md', flags=self.flags | glob.SPLIT)
+                ]
+            )
+        )
 
     def test_glob_integrity(self):
         """`globmatch` must match what glob globs."""
@@ -1116,3 +1229,48 @@ class TestGlobMatchSpecial(unittest.TestCase):
 
         if util.platform() == "windows":
             self.assertTrue(glob.globmatch('DOCS', '**/docs/**', flags=self.flags | glob.REALPATH | glob.FORCECASE))
+
+
+@skip_unless_symlink
+class TestGlobmatchSymlink(_TestGlobmatch):
+    """Test symlinks."""
+
+    def mksymlink(self, original, link):
+        """Make symlink."""
+
+        if not os.path.lexists(link):
+            os.symlink(original, link)
+
+    def setUp(self):
+        """Setup."""
+
+        self.tempdir = TESTFN + "_dir"
+        self.mktemp('.hidden', 'a.txt')
+        self.mktemp('.hidden', 'b.file')
+        self.mktemp('.hidden_file')
+        self.mktemp('a.txt')
+        self.mktemp('b.file')
+        self.mktemp('c.txt.bak')
+        self.can_symlink = can_symlink()
+        if self.can_symlink:
+            self.mksymlink('.hidden', self.norm('sym1'))
+            self.mksymlink(os.path.join('.hidden', 'a.txt'), self.norm('sym2'))
+
+        self.default_flags = glob.G | glob.P | glob.B
+
+    def test_globmatch_symlink(self):
+        """Test `globmatch` with symlinks."""
+
+        self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/*.txt}', flags=self.default_flags))
+        self.assertTrue(glob.globmatch(self.tempdir + '/a.txt', '**/*.txt', flags=self.default_flags))
+
+    def test_globmatch_follow_symlink(self):
+        """Test `globmatch` with symlinks that we follow."""
+
+        self.assertTrue(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/*.txt', flags=self.default_flags | glob.L))
+        self.assertTrue(glob.globmatch(self.tempdir + '/a.txt', '**/*.txt', flags=self.default_flags | glob.L))
+
+    def test_globmatch_trigger_symlink_cache(self):
+        """Use a pattern that exercises the symlink cache."""
+
+        self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/{*.txt,*.t*}', flags=self.default_flags))
diff --git a/tests/test_wcmatch.py b/tests/test_wcmatch.py
index 851ce7a..1ea5a54 100644
--- a/tests/test_wcmatch.py
+++ b/tests/test_wcmatch.py
@@ -90,13 +90,6 @@ class _TestWcmatch(unittest.TestCase):
         """Setup."""
 
         self.tempdir = TESTFN + "_dir"
-        self.mktemp('.hidden', 'a.txt')
-        self.mktemp('.hidden', 'b.file')
-        self.mktemp('.hidden_file')
-        self.mktemp('a.txt')
-        self.mktemp('b.file')
-        self.mktemp('c.txt.bak')
-
         self.default_flags = wcmatch.R | wcmatch.I | wcmatch.M | wcmatch.SL
         self.errors = []
         self.skipped = 0