Repository: facelessuser/wcmatch

Issue:
Possibly add an explicit exclusion pattern parameter
Wildcard Match currently has a feature that enables exclusion patterns when the appropriate flag is enabled and the pattern starts with `!`. This was a requirement for [Rummage](https://github.com/facelessuser/Rummage) and the entire reason we added the functionality. With that said, some people may want to use exclusion patterns without using `!`. The way to do this would be to have an explicit parameter that accepts exclusion patterns.

I haven't looked into how complicated this may or may not be, but it may be nice to consider an `exclude` parameter that can take a list of patterns. All patterns in `exclude` would be considered exclusion patterns and the `NEGATE` flag would not apply to them at all. As a matter of fact, users should use either `NEGATE` **or** exclude not both, but we would not restrict it, we would just ensure the `NEGATE` flag did not apply to the `exclude` patterns.


Discussion:
facelessuser: We need to slowly work through this and make sure we don't break anything.

Patch:
diff --git a/.coveragerc b/.coveragerc
index 980b3d8..e46e205 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -5,3 +5,6 @@ omit=
 [report]
 omit=
     wcmatch/pep562.py
+exclude_lines =
+    pragma: no cover
+    @overload
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 816426e..df2082d 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -4,6 +4,9 @@
 
 - **NEW**: Drop support for Python 3.6.
 - **NEW**: Switch to Hatch backend instead of Setuptools.
+- **NEW**: Add new `exclude` option to `fnmatch`, `pathlib`, and `glob` methods that allows exclusion patterns to be
+  specified directly without needing to enable `NEGATE` and prepend patterns with `!`. `exclude` accepts a separate
+  pattern or pattern list. `exclude` should not be used in conjunction with `NEGATE`. One or the other should be used.
 
 ## 8.3
 
diff --git a/docs/src/markdown/fnmatch.md b/docs/src/markdown/fnmatch.md
index 0c96382..576da39 100644
--- a/docs/src/markdown/fnmatch.md
+++ b/docs/src/markdown/fnmatch.md
@@ -54,11 +54,12 @@ be no limit.
 #### `fnmatch.fnmatch` {: #fnmatch}
 
 ```py3
-def fnmatch(filename, patterns, *, flags=0, limit=1000)
+def fnmatch(filename, patterns, *, flags=0, limit=1000, exclude=None)
 ```
 
 `fnmatch` takes a file name, a pattern (or list of patterns), and flags.  It also allows configuring the [max pattern
-limit](#multi-pattern-limits). It will return a boolean indicating whether the file name was matched by the pattern(s).
+limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or
+a list of patterns. It will return a boolean indicating whether the file name was matched by the pattern(s).
 
 ```pycon3
 >>> from wcmatch import fnmatch
@@ -74,10 +75,21 @@ When applying multiple patterns, a file matches if it matches any of the pattern
 True
 ```
 
-Exclusion patterns are allowed as well. When exclusion patterns are used in conjunction with inclusion patterns, a file
-will be considered matched if one of the inclusion patterns match **and** none of the exclusion patterns match. If an
-exclusion pattern is given without any inclusion patterns, the pattern will match nothing. Exclusion patterns are meant
-to filter other patterns, not match anything by themselves.
+Exclusions can be used by taking advantage of the `exclude` parameter. It takes a single exclude pattern or a list of
+patterns. Files that match the exclude pattern will not be matched.
+
+```pycon3
+>>> from wcmatch import fnmatch
+>>> fnmatch.fnmatch('test.py', '*', exclude='*.py')
+False
+>>> fnmatch.fnmatch('test.txt', '*', exclude='*.py')
+True
+```
+
+Inline exclusion patterns are allowed as well. When exclusion patterns are used in conjunction with inclusion patterns,
+a file will be considered matched if one of the inclusion patterns match **and** none of the exclusion patterns match.
+If an exclusion pattern is given without any inclusion patterns, the pattern will match nothing. Exclusion patterns are
+meant to filter other patterns, not match anything by themselves.
 
 ```pycon3
 >>> from wcmatch import fnmatch
@@ -107,14 +119,18 @@ True
 !!! new "New 6.0"
     `limit` was added in 6.0.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `fnmatch.filter` {: #filter}
 
 ```py3
-def filter(filenames, patterns, *, flags=0, limit=1000):
+def filter(filenames, patterns, *, flags=0, limit=1000, exclude=None):
 ```
 
 `filter` takes a list of filenames, a pattern (or list of patterns), and flags. It also allows configuring the [max 
-pattern limit](#multi-pattern-limits). It returns a list of all files that matched the pattern(s). The same logic used for
+pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a
+pattern or a list of patterns.It returns a list of all files that matched the pattern(s). The same logic used for
 [`fnmatch`](#fnmatch) is used for `filter`, albeit more efficient for processing multiple files.
 
 ```pycon3
@@ -126,16 +142,20 @@ pattern limit](#multi-pattern-limits). It returns a list of all files that match
 !!! new "New 6.0"
     `limit` was added in 6.0.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `fnmatch.translate` {: #translate}
 
 ```py3
-def translate(patterns, *, flags=0, limit=1000):
+def translate(patterns, *, flags=0, limit=1000, exclude=None):
 ```
 
 `translate` takes a file pattern (or list of patterns) and flags. It also allows configuring the [max pattern
-limit](#multi-pattern-limits). It returns two lists: one for inclusion patterns and one for exclusion patterns. The
-lists contain the regular expressions used for matching the given patterns. It should be noted that a file is considered
-matched if it matches at least one inclusion pattern and matches **none** of the exclusion patterns.
+limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or
+a list of patterns. It returns two lists: one for inclusion patterns and one for exclusion patterns. The lists contain
+the regular expressions used for matching the given patterns. It should be noted that a file is considered matched if it
+matches at least one inclusion pattern and matches **none** of the exclusion patterns.
 
 ```pycon3
 >>> from wcmatch import fnmatch
@@ -165,6 +185,9 @@ we wrap the entire group to be captured: `#!py3 '+(a)'` --> `#!py3 r'((a)+)'`.
 !!! new "New 7.1"
     Translate patterns now provide capturing groups for [`EXTMATCH`](#extmatch) groups.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `fnmatch.escape` {: #escape}
 
 ```py3
@@ -246,6 +269,12 @@ Assuming the `SPLIT` flag, this means using it in a pattern such as `inclusion|!
 If it is desired, you can force exclusion patterns, when no inclusion pattern is provided, to assume all files match
 unless the file matches the excluded pattern. This is done with the [`NEGATEALL`](#negateall) flag.
 
+`NEGATE` enables [`DOTMATCH`](#dotglob) in all exclude patterns, this cannot be disabled. This will not affect the
+inclusion patterns.
+
+If `NEGATE` is set and exclusion patterns are passed via a matching function's `exclude` parameter, `NEGATE` will be
+ignored and the `exclude` patterns will be used instead. Either `exclude` or `NEGATE` should be used, not both.
+
 #### `fnmatch.NEGATEALL, fnmatch.A` {: #negateall}
 
 `NEGATEALL` can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the
diff --git a/docs/src/markdown/glob.md b/docs/src/markdown/glob.md
index 60eeb3f..d324acf 100644
--- a/docs/src/markdown/glob.md
+++ b/docs/src/markdown/glob.md
@@ -167,12 +167,13 @@ no limit.
 #### `glob.glob` {: #glob}
 
 ```py3
-def glob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000):
+def glob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None):
 ```
 
 `glob` takes a pattern (or list of patterns), flags, and an optional root directory (string or path-like object) and/or
-directory file descriptor. It also allows configuring the [max pattern limit](#multi-pattern-limits). When executed it
-will crawl the file system returning matching files.
+directory file descriptor. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns
+can be specified via the `exclude` parameter which takes a pattern or a list of patterns.When executed it will crawl the
+file system returning matching files.
 
 !!! warning "Path-like Input Support"
     Path-like object input support is only available in Python 3.6+ as the path-like protocol was added in Python 3.6.
@@ -299,10 +300,13 @@ Additionally, you can use `dir_fd` and specify a root directory with a directory
 !!! new "New 8.2"
     `dir_fd` parameter was added in 8.2.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `glob.iglob` {: #iglob}
 
 ```py3
-def iglob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000):
+def iglob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None):
 ```
 
 `iglob` is just like [`glob`](#glob) except it returns an iterator.
@@ -322,15 +326,19 @@ def iglob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000):
 !!! new "New 8.2"
     `dir_fd` parameter was added in 8.2.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `glob.globmatch` {: #globmatch}
 
 ```py3
-def globmatch(filename, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000):
+def globmatch(filename, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None):
 ```
 
 `globmatch` takes a file name (string or path-like object), a pattern (or list of patterns), flags, and an optional root
-directory and/or file descriptor.  It also allows configuring the [max pattern limit](#multi-pattern-limits). It will
-return a boolean indicating whether the file path was matched by the pattern(s).
+directory and/or file descriptor.  It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion
+patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns. It will return a
+boolean indicating whether the file path was matched by the pattern(s).
 
 ```pycon3
 >>> from wcmatch import glob
@@ -457,16 +465,20 @@ True
 !!! new "New 8.2"
     `dir_fd` parameter was added in 8.2.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `glob.globfilter` {: #globfilter}
 
 ```py3
-def globfilter(filenames, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000):
+def globfilter(filenames, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, method=None):
 ```
 
 `globfilter` takes a list of file paths (strings or path-like objects), a pattern (or list of patterns), flags, and an
-optional root directory and/or directory file descriptor. It also allows configuring the
-[max pattern limit](#multi-pattern-limits). It returns a list of all files paths that matched the pattern(s). The same
-logic used for [`globmatch`](#globmatch) is used for `globfilter`, albeit more efficient for processing multiple files.
+optional root directory and/or directory file descriptor. It also allows configuring the 
+[max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes
+a pattern or a list of patterns.It returns a list of all files paths that matched the pattern(s). The same logic used
+for [`globmatch`](#globmatch) is used for `globfilter`, albeit more efficient for processing multiple files.
 
 !!! warning "Path-like Input Support"
     Path-like object input support is only available in Python 3.6+ as the path-like protocol was added in Python 3.6.
@@ -492,16 +504,20 @@ be matched by `GLOBSTAR`. See [`globmatch`](#globmatch) for examples.
 !!! new "New 8.2"
     `dir_fd` parameter was added in 8.2.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `glob.translate` {: #translate}
 
 ```py3
-def translate(patterns, *, flags=0, limit=1000):
+def translate(patterns, *, flags=0, limit=1000, exclude=None):
 ```
 
 `translate` takes a file pattern (or list of patterns) and flags. It also allows configuring the [max pattern
-limit](#multi-pattern-limits). It returns two lists: one for inclusion patterns and one for exclusion patterns. The
-lists contain the regular expressions used for matching the given patterns. It should be noted that a file is considered
-matched if it matches at least one inclusion pattern and matches **none** of the exclusion patterns.
+limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or
+a list of patterns. It returns two lists: one for inclusion patterns and one for exclusion patterns. The lists contain
+the regular expressions used for matching the given patterns. It should be noted that a file is considered matched if it
+matches at least one inclusion pattern and matches **none** of the exclusion patterns.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -531,6 +547,9 @@ we wrap the entire group to be captured: `#!py3 '+(a)'` --> `#!py3 r'((a)+)'`.
 !!! new "New 7.1"
     Translate patterns now provide capturing groups for [`EXTGLOB`](#extglob) groups.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `glob.escape` {: #escape}
 
 ```py3
@@ -759,6 +778,9 @@ unless the file matches the excluded pattern. This is done with the [`NEGATEALL`
 `NEGATE` enables [`DOTGLOB`](#dotglob) in all exclude patterns, this cannot be disabled. This will not affect the
 inclusion patterns.
 
+If `NEGATE` is set and exclusion patterns are passed via a matching or glob function's `exclude` parameter, `NEGATE`
+will be ignored and the `exclude` patterns will be used instead. Either `exclude` or `NEGATE` should be used, not both.
+
 #### `glob.NEGATEALL, glob.A` {: #negateall}
 
 `NEGATEALL` can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the
diff --git a/docs/src/markdown/pathlib.md b/docs/src/markdown/pathlib.md
index 92f49d8..a23c355 100644
--- a/docs/src/markdown/pathlib.md
+++ b/docs/src/markdown/pathlib.md
@@ -240,11 +240,12 @@ PosixPath('/usr/local/bin')
 #### `PurePath.match` {: #match}
 
 ```py3
-def match(self, patterns, *, flags=0, limit=1000):
+def match(self, patterns, *, flags=0, limit=1000, exclude=None):
 ```
 
 `match` takes a pattern (or list of patterns), and flags.  It also allows configuring the [max pattern
-limit](#multi-pattern-limits). It will return a boolean indicating whether the object's file path was matched by the
+limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or
+a list of patterns. It will return a boolean indicating whether the object's file path was matched by the
 pattern(s).
 
 `match` mimics Python's `pathlib` version of `match`. Python's `match` uses a right to left evaluation. Wildcard Match
@@ -279,15 +280,18 @@ True
 !!! new "New 6.0"
     `limit` was added in 6.0.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `PurePath.globmatch` {: #globmatch}
 
 ```py3
-def globmatch(self, patterns, *, flags=0, limit=1000):
+def globmatch(self, patterns, *, flags=0, limit=1000, exclude=None):
 ```
 
 `globmatch` takes a pattern (or list of patterns), and flags.  It also allows configuring the [max pattern
-limit](#multi-pattern-limits).It will return a boolean indicating whether the objects file path was matched by the
-pattern(s).
+limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or
+a list of patterns. It will return a boolean indicating whether the objects file path was matched by the pattern(s).
 
 `globmatch` is similar to [`match`](#match) except it does not use the same recursive logic that
 [`match`](#match) does. In all other respects, it behaves the same.
@@ -310,17 +314,20 @@ True
 !!! new "New 6.0"
     `limit` was added in 6.0.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `Path.glob` {: #glob}
 
 ```py3
-def glob(self, patterns, *, flags=0, limit=1000):
+def glob(self, patterns, *, flags=0, limit=1000, exclude=None):
 ```
 
 `glob` takes a pattern (or list of patterns) and flags. It also allows configuring the [max pattern
 limit](#multi-pattern-limits). It will crawl the file system, relative to the current [`Path`](#path) object,
 returning a generator of [`Path`](#path) objects. If a file/folder matches any regular, inclusion pattern, it is
-considered a match.  If a file matches *any* exclusion pattern (when enabling the [`NEGATE`](#negate) flag), then
-it will not be returned.
+considered a match.  If a file matches *any* exclusion pattern (specified via `exclude` or using negation patterns when
+enabling the [`NEGATE`](#negate) flag), then it will not be returned.
 
 This method calls our own [`iglob`](./glob.md#iglob) implementation, and as such, should behave in the same manner
 in respect to features, the one exception being that instead of returning path strings in the generator, it will return
@@ -340,17 +347,20 @@ working directory.
 !!! new "New 6.0"
     `limit` was added in 6.0.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 #### `Path.rglob` {: #rglob}
 
 ```py3
-def rglob(self, patterns, *, flags=0, path_limit=1000):
+def rglob(self, patterns, *, flags=0, path_limit=1000, exclude=None):
 ```
 
 `rglob` takes a pattern (or list of patterns) and flags. It also allows configuring the [max pattern
 limit](#multi-pattern-limits). It will crawl the file system, relative to the current [`Path`](#path) object,
 returning a generator of [`Path`](#path) objects. If a file/folder matches any regular patterns, it is considered
-a match.  If a file matches *any* exclusion pattern (when enabling the [`NEGATE`](#negate) flag), then it will be
-not be returned.
+a match.  If a file matches *any* exclusion pattern (specified via `exclude` or using negation patterns when enabling
+the [`NEGATE`](#negate) flag), then it will be not be returned.
 
 `rglob` mimics Python's [`pathlib`][pathlib] version of `rglob` in that it uses a recursive logic. What this means is
 that when you are matching a path in the form `some/path/name`, the patterns `name`, `path/name` and `some/path/name`
@@ -370,6 +380,9 @@ the same.
 !!! new "New 6.0"
     `limit` was added in 6.0.
 
+!!! new "New 8.4"
+    `exclude` parameter was added.
+
 ## Flags
 
 #### `pathlib.CASE, pathlib.C` {: #case}
@@ -401,6 +414,9 @@ unless the file matches the excluded pattern. This is done with the [`NEGATEALL`
 `NEGATE` enables [`DOTGLOB`](#dotglob) in all exclude patterns, this cannot be disabled. This will not affect the
 inclusion patterns.
 
+If `NEGATE` is set and exclusion patterns are passed via a matching or glob function's `exclude` parameter, `NEGATE`
+will be ignored and the `exclude` patterns will be used instead. Either `exclude` or `NEGATE` should be used, not both.
+
 #### `pathlib.NEGATEALL, pathlib.A` {: #negateall}
 
 `NEGATEALL` can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the
diff --git a/wcmatch/_wcparse.py b/wcmatch/_wcparse.py
index 930ed7a..297e12b 100644
--- a/wcmatch/_wcparse.py
+++ b/wcmatch/_wcparse.py
@@ -27,7 +27,7 @@ import os
 from . import util
 from . import posix
 from . _wcmatch import WcRegexp
-from typing import List, Tuple, AnyStr, Iterable, Pattern, Generic, Optional, Set, Sequence, Union, cast
+from typing import List, Tuple, AnyStr, Iterable, Pattern, Generic, Optional, Set, Sequence, Union, overload, cast
 
 UNICODE_RANGE = '\u0000-\U0010ffff'
 ASCII_RANGE = '\x00-\xff'
@@ -175,6 +175,7 @@ _ANCHOR = 0x200000000  # The pattern, if it starts with a slash, is anchored to
 _EXTMATCHBASE = 0x400000000  # Like `MATCHBASE`, but works for multiple directory levels.
 _NOABSOLUTE = 0x800000000  # Do not allow absolute patterns
 _RTL = 0x1000000000  # Match from right to left
+_NO_GLOBSTAR_CAPTURE = 0x2000000000  # Disallow `GLOBSTAR` capturing groups.
 
 FLAG_MASK = (
     CASE |
@@ -202,7 +203,8 @@ FLAG_MASK = (
     _ANCHOR |
     _EXTMATCHBASE |
     _RTL |
-    _NOABSOLUTE
+    _NOABSOLUTE |
+    _NO_GLOBSTAR_CAPTURE
 )
 CASE_FLAGS = IGNORECASE | CASE
 
@@ -299,13 +301,23 @@ class PatternLimitException(Exception):
     """Pattern limit exception."""
 
 
-def to_str_sequence(patterns: Union[str, bytes, Sequence[AnyStr]]) -> Sequence[AnyStr]:
+@overload
+def iter_patterns(patterns: Union[str, Sequence[str]]) -> Iterable[str]:
+    ...
+
+
+@overload
+def iter_patterns(patterns: Union[bytes, Sequence[bytes]]) -> Iterable[bytes]:
+    ...
+
+
+def iter_patterns(patterns: Union[AnyStr, Sequence[AnyStr]]) -> Iterable[AnyStr]:
     """Return a simple string sequence."""
 
     if isinstance(patterns, (str, bytes)):
-        return cast(Sequence[AnyStr], [patterns])
+        yield patterns
     else:
-        return patterns
+        yield from patterns
 
 
 def escape(pattern: AnyStr, unix: Optional[bool] = None, pathname: bool = True, raw: bool = False) -> AnyStr:
@@ -592,16 +604,52 @@ def is_unix_style(flags: int) -> bool:
     )
 
 
+def no_negate_flags(flags: int) -> int:
+    """No negation."""
+
+    if flags & NEGATE:
+        flags ^= NEGATE
+    if flags & NEGATEALL:
+        flags ^= NEGATEALL
+    return flags
+
+
+@overload
+def translate(
+    patterns: Union[str, Sequence[str]],
+    flags: int,
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[str, Sequence[str]]] = None
+) -> Tuple[List[str], List[str]]:
+    ...
+
+
+@overload
 def translate(
-    patterns: Sequence[AnyStr],
+    patterns: Union[bytes, Sequence[bytes]],
     flags: int,
-    limit: int = PATTERN_LIMIT
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[bytes, Sequence[bytes]]] = None
+) -> Tuple[List[bytes], List[bytes]]:
+    ...
+
+
+def translate(
+    patterns: Union[AnyStr, Sequence[AnyStr]],
+    flags: int,
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> Tuple[List[AnyStr], List[AnyStr]]:
     """Translate patterns."""
 
     positive = []  # type: List[AnyStr]
     negative = []  # type: List[AnyStr]
 
+    if exclude is not None:
+        flags = no_negate_flags(flags)
+        negative = translate(exclude, flags=flags | DOTMATCH | _NO_GLOBSTAR_CAPTURE, limit=limit)[0]
+        limit -= len(negative)
+
     flags = (flags | _TRANSLATE) & FLAG_MASK
     is_unix = is_unix_style(flags)
     seen = set()
@@ -609,7 +657,7 @@ def translate(
     try:
         current_limit = limit
         total = 0
-        for pattern in patterns:
+        for pattern in iter_patterns(patterns):
             pattern = util.norm_pattern(pattern, not is_unix, bool(flags & RAWCHARS))
             count = 0
             for count, expanded in enumerate(expand(pattern, flags, current_limit), 1):
@@ -618,7 +666,10 @@ def translate(
                     raise PatternLimitException("Pattern limit exceeded the limit of {:d}".format(limit))
                 if expanded not in seen:
                     seen.add(expanded)
-                    (negative if is_negative(expanded, flags) else positive).append(WcParse(expanded, flags).parse())
+                    if is_negative(expanded, flags):
+                        negative.append(WcParse(expanded[1:], flags | _NO_GLOBSTAR_CAPTURE | DOTMATCH).parse())
+                    else:
+                        positive.append(WcParse(expanded, flags).parse())
             if limit:
                 current_limit -= count
                 if current_limit < 1:
@@ -626,17 +677,16 @@ def translate(
     except bracex.ExpansionLimitException:
         raise PatternLimitException("Pattern limit exceeded the limit of {:d}".format(limit))
 
-    if patterns is not None and negative and not positive:
+    if negative and not positive:
         if flags & NEGATEALL:
-            default = b'**' if isinstance(patterns[0], bytes) else '**'
+            default = b'**' if isinstance(negative[0], bytes) else '**'
             positive.append(
                 WcParse(default, flags | (GLOBSTAR if flags & PATHNAME else 0)).parse()
             )
 
-    if patterns and flags & NODIR:
-        index = util.BYTES if isinstance(patterns[0], bytes) else util.UNICODE
-        exclude = cast(AnyStr, _NO_NIX_DIR[index] if is_unix else _NO_WIN_DIR[index])
-        negative.append(exclude)
+    if positive and flags & NODIR:
+        index = util.BYTES if isinstance(positive[0], bytes) else util.UNICODE
+        negative.append(cast(AnyStr, _NO_NIX_DIR[index] if is_unix else _NO_WIN_DIR[index]))
 
     return positive, negative
 
@@ -650,23 +700,49 @@ def split(pattern: AnyStr, flags: int) -> Iterable[AnyStr]:
         yield pattern
 
 
-def compile(  # noqa: A001
-    patterns: Sequence[AnyStr],
+@overload
+def compile_pattern(
+    patterns: Union[str, Sequence[str]],
     flags: int,
-    limit: int = PATTERN_LIMIT
-) -> WcRegexp[AnyStr]:
-    """Compile patterns."""
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[str, Sequence[str]]] = None
+) -> Tuple[List[Pattern[str]], List[Pattern[str]]]:
+    ...
+
+
+@overload
+def compile_pattern(
+    patterns: Union[bytes, Sequence[bytes]],
+    flags: int,
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[bytes, Sequence[bytes]]] = None
+) -> Tuple[List[Pattern[bytes]], List[Pattern[bytes]]]:
+    ...
+
+
+def compile_pattern(
+    patterns: Union[AnyStr, Sequence[AnyStr]],
+    flags: int,
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
+) -> Tuple[List[Pattern[AnyStr]], List[Pattern[AnyStr]]]:
+    """Compile the patterns."""
 
     positive = []  # type: List[Pattern[AnyStr]]
     negative = []  # type: List[Pattern[AnyStr]]
 
+    if exclude is not None:
+        flags = no_negate_flags(flags)
+        negative = compile_pattern(exclude, flags=flags | DOTMATCH | _NO_GLOBSTAR_CAPTURE, limit=limit)[0]
+        limit -= len(negative)
+
     is_unix = is_unix_style(flags)
     seen = set()
 
     try:
         current_limit = limit
         total = 0
-        for pattern in patterns:
+        for pattern in iter_patterns(patterns):
             pattern = util.norm_pattern(pattern, not is_unix, bool(flags & RAWCHARS))
             count = 0
             for count, expanded in enumerate(expand(pattern, flags, current_limit), 1):
@@ -675,7 +751,10 @@ def compile(  # noqa: A001
                     raise PatternLimitException("Pattern limit exceeded the limit of {:d}".format(limit))
                 if expanded not in seen:
                     seen.add(expanded)
-                    (negative if is_negative(expanded, flags) else positive).append(_compile(expanded, flags))
+                    if is_negative(expanded, flags):
+                        negative.append(_compile(expanded[1:], flags | _NO_GLOBSTAR_CAPTURE | DOTMATCH))
+                    else:
+                        positive.append(_compile(expanded, flags))
             if limit:
                 current_limit -= count
                 if current_limit < 1:
@@ -683,15 +762,47 @@ def compile(  # noqa: A001
     except bracex.ExpansionLimitException:
         raise PatternLimitException("Pattern limit exceeded the limit of {:d}".format(limit))
 
-    if patterns is not None and negative and not positive:
+    if negative and not positive:
         if flags & NEGATEALL:
-            default = b'**' if isinstance(patterns[0], bytes) else '**'
+            default = b'**' if isinstance(negative[0].pattern, bytes) else '**'
             positive.append(_compile(default, flags | (GLOBSTAR if flags & PATHNAME else 0)))
 
-    if patterns is not None and flags & NODIR:
-        ptype = util.BYTES if isinstance(patterns[0], bytes) else util.UNICODE
+    if positive and flags & NODIR:
+        ptype = util.BYTES if isinstance(positive[0].pattern, bytes) else util.UNICODE
         negative.append(cast(Pattern[AnyStr], RE_NO_DIR[ptype] if is_unix else RE_WIN_NO_DIR[ptype]))
 
+    return positive, negative
+
+
+@overload
+def compile(  # noqa: A001
+    patterns: Union[str, Sequence[str]],
+    flags: int,
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[str, Sequence[str]]] = None
+) -> WcRegexp[str]:
+    ...
+
+
+@overload
+def compile(  # noqa: A001
+    patterns: Union[bytes, Sequence[bytes]],
+    flags: int,
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[bytes, Sequence[bytes]]] = None
+) -> WcRegexp[bytes]:
+    ...
+
+
+def compile(  # noqa: A001
+    patterns: Union[AnyStr, Sequence[AnyStr]],
+    flags: int,
+    limit: int = PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
+) -> WcRegexp[AnyStr]:
+    """Compile patterns."""
+
+    positive, negative = compile_pattern(patterns, flags, limit, exclude)
     return WcRegexp(
         tuple(positive), tuple(negative),
         bool(flags & REALPATH), bool(flags & PATHNAME), bool(flags & FOLLOW)
@@ -847,7 +958,7 @@ class WcParse(Generic[AnyStr]):
         self.realpath = bool(flags & REALPATH) and self.pathname
         self.translate = bool(flags & _TRANSLATE)
         self.negate = bool(flags & NEGATE)
-        self.globstar_capture = self.realpath and not self.translate
+        self.globstar_capture = self.realpath and not self.translate and not bool(flags & _NO_GLOBSTAR_CAPTURE)
         self.dot = bool(flags & DOTMATCH)
         self.extend = bool(flags & EXTMATCH)
         self.matchbase = bool(flags & MATCHBASE)
@@ -1534,17 +1645,6 @@ class WcParse(Generic[AnyStr]):
         result = ['']
         prepend = ['']
 
-        self.negative = False
-
-        if is_negative(p, self.flags):
-            self.negative = True
-            p = p[1:]
-
-        if self.negative:
-            # TODO: Do we prevent `NODOTDIR` for negative patterns?
-            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:
@@ -1587,7 +1687,7 @@ class WcParse(Generic[AnyStr]):
             result = prepend + result
 
         case_flag = 'i' if not self.case_sensitive else ''
-        pattern = r'^(?s{}:{})$'.format(case_flag, ''.join(result))
+        pattern = R'^(?s{}:{})$'.format(case_flag, ''.join(result))
 
         if self.capture:
             # Strip out unnecessary regex comments
diff --git a/wcmatch/fnmatch.py b/wcmatch/fnmatch.py
index 2d813d4..8216065 100644
--- a/wcmatch/fnmatch.py
+++ b/wcmatch/fnmatch.py
@@ -21,7 +21,7 @@ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFT
 IN THE SOFTWARE.
 """
 from . import _wcparse
-from typing import Tuple, List, AnyStr, Iterable, Union, Sequence
+from typing import Tuple, List, AnyStr, Iterable, Union, Sequence, Optional
 
 __all__ = (
     "CASE", "EXTMATCH", "IGNORECASE", "RAWCHARS",
@@ -71,23 +71,25 @@ def _flag_transform(flags: int) -> int:
 
 
 def translate(
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> Tuple[List[AnyStr], List[AnyStr]]:
     """Translate `fnmatch` pattern."""
 
     flags = _flag_transform(flags)
-    return _wcparse.translate(_wcparse.to_str_sequence(patterns), flags, limit)
+    return _wcparse.translate(patterns, flags, limit, exclude=exclude)
 
 
 def fnmatch(
     filename: AnyStr,
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> bool:
     """
     Check if filename matches pattern.
@@ -97,22 +99,23 @@ def fnmatch(
     """
 
     flags = _flag_transform(flags)
-    return bool(_wcparse.compile(_wcparse.to_str_sequence(patterns), flags, limit).match(filename))
+    return bool(_wcparse.compile(patterns, flags, limit, exclude=exclude).match(filename))
 
 
 def filter(  # noqa A001
     filenames: Iterable[AnyStr],
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> List[AnyStr]:
     """Filter names using pattern."""
 
     matches = []
 
     flags = _flag_transform(flags)
-    obj = _wcparse.compile(_wcparse.to_str_sequence(patterns), flags, limit)
+    obj = _wcparse.compile(patterns, flags, limit, exclude=exclude)
 
     for filename in filenames:
         if obj.match(filename):
diff --git a/wcmatch/glob.py b/wcmatch/glob.py
index b4873b3..1f21685 100644
--- a/wcmatch/glob.py
+++ b/wcmatch/glob.py
@@ -391,15 +391,20 @@ class Glob(Generic[AnyStr]):
 
     def __init__(
         self,
-        pattern: Union[str, bytes, Sequence[AnyStr]],
+        pattern: Union[AnyStr, Sequence[AnyStr]],
         flags: int = 0,
         root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
         dir_fd: Optional[int] = None,
-        limit: int = _wcparse.PATTERN_LIMIT
+        limit: int = _wcparse.PATTERN_LIMIT,
+        exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
     ) -> None:
         """Initialize the directory walker object."""
 
-        pats = _wcparse.to_str_sequence(pattern)
+        pats = [pattern] if isinstance(pattern, (str, bytes)) else pattern
+        epats = [exclude] if isinstance(exclude, (str, bytes)) else exclude
+
+        if epats is not None:
+            flags = _wcparse.no_negate_flags(flags)
 
         self.pattern = []  # type: List[List[_GlobPart]]
         self.npatterns = []  # type: List[Pattern[AnyStr]]
@@ -424,7 +429,7 @@ class Glob(Generic[AnyStr]):
         if flags & _RTL:  # pragma: no cover
             flags ^= _RTL
         self.flags = _flag_transform(flags | REALPATH)  # type: int
-        self.negate_flags = self.flags  # type: int
+        self.negate_flags = self.flags | DOTMATCH | _wcparse._NO_GLOBSTAR_CAPTURE  # type: int
         if not self.scandotdir and not self.flags & NODOTDIR:
             self.flags |= NODOTDIR
         self.raw_chars = bool(self.flags & RAWCHARS)  # type: bool
@@ -469,19 +474,21 @@ class Glob(Generic[AnyStr]):
             )
 
         self.root_dir = temp  # type: AnyStr
+        self.current_limit = self.limit
         self._parse_patterns(pats)
+        if epats is not None:
+            self._parse_patterns(epats, force_negate=True)
 
-    def _iter_patterns(self, patterns: Sequence[AnyStr]) -> Iterator[Tuple[bool, AnyStr]]:
+    def _iter_patterns(self, patterns: Sequence[AnyStr], force_negate: bool = False) -> Iterator[Tuple[bool, AnyStr]]:
         """Iterate expanded patterns."""
 
         seen = set()
         try:
-            current_limit = self.limit
             total = 0
             for p in patterns:
                 p = util.norm_pattern(p, not self.unix, self.raw_chars)
                 count = 0
-                for count, expanded in enumerate(_wcparse.expand(p, self.flags, current_limit), 1):
+                for count, expanded in enumerate(_wcparse.expand(p, self.flags, self.current_limit), 1):
                     total += 1
                     if 0 < self.limit < total:
                         raise _wcparse.PatternLimitException(
@@ -490,26 +497,26 @@ class Glob(Generic[AnyStr]):
                     # Filter out duplicate patterns. If `NOUNIQUE` is enabled,
                     # we only want to filter on negative patterns as they are
                     # only filters.
-                    is_neg = _wcparse.is_negative(expanded, self.flags)
+                    is_neg = force_negate or _wcparse.is_negative(expanded, self.flags)
                     if not self.nounique or is_neg:
                         if expanded in seen:
                             continue
                         seen.add(expanded)
 
-                    yield is_neg, expanded
+                    yield is_neg, expanded[1:] if is_neg and not force_negate else expanded
                 if self.limit:
-                    current_limit -= count
-                    if current_limit < 1:
-                        current_limit = 1
+                    self.current_limit -= count
+                    if self.current_limit < 1:
+                        self.current_limit = 1
         except bracex.ExpansionLimitException:
             raise _wcparse.PatternLimitException(
                 "Pattern limit exceeded the limit of {:d}".format(self.limit)
             )
 
-    def _parse_patterns(self, patterns: Sequence[AnyStr]) -> None:
+    def _parse_patterns(self, patterns: Sequence[AnyStr], force_negate: bool = False) -> None:
         """Parse patterns."""
 
-        for is_neg, p in self._iter_patterns(patterns):
+        for is_neg, p in self._iter_patterns(patterns, force_negate=force_negate):
             if is_neg:
                 # 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,
@@ -523,12 +530,13 @@ class Glob(Generic[AnyStr]):
                 default = self.stars
                 self.pattern.append(_GlobSplit(default, self.flags | GLOBSTAR).split())
 
-        if self.nodir:
+        if self.nodir and not force_negate:
             self.npatterns.append(self.re_no_dir)
 
         # A single positive pattern will not find multiples of the same file
         # disable unique mode so that we won't waste time or memory computing unique returns.
         if (
+            not force_negate and
             len(self.pattern) <= 1 and
             not self.flags & NODOTDIR and
             not self.nounique and
@@ -853,54 +861,58 @@ class Glob(Generic[AnyStr]):
 
 
 def iglob(
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
     root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
     dir_fd: Optional[int] = None,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> Iterator[AnyStr]:
     """Glob."""
 
     if not isinstance(patterns, (str, bytes)) and not patterns:
         return
 
-    yield from Glob(patterns, flags, root_dir, dir_fd, limit).glob()
+    yield from Glob(patterns, flags, root_dir, dir_fd, limit, exclude).glob()
 
 
 def glob(
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
     root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
     dir_fd: Optional[int] = None,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> List[AnyStr]:
     """Glob."""
 
-    return list(iglob(patterns, flags=flags, root_dir=root_dir, dir_fd=dir_fd, limit=limit))
+    return list(iglob(patterns, flags=flags, root_dir=root_dir, dir_fd=dir_fd, limit=limit, exclude=exclude))
 
 
 def translate(
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> Tuple[List[AnyStr], List[AnyStr]]:
     """Translate glob pattern."""
 
     flags = _flag_transform(flags)
-    return _wcparse.translate(_wcparse.to_str_sequence(patterns), flags, limit)
+    return _wcparse.translate(patterns, flags, limit, exclude)
 
 
 def globmatch(
     filename: Union[AnyStr, 'os.PathLike[AnyStr]'],
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
     root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
     dir_fd: Optional[int] = None,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> bool:
     """
     Check if filename matches pattern.
@@ -909,55 +921,38 @@ def globmatch(
     but if `case_sensitive` is set, respect that instead.
     """
 
-    pats = _wcparse.to_str_sequence(patterns)
-
     # Shortcut out if we have no patterns
-    if not pats:
+    if not patterns:
         return False
 
-    ptype = type(pats[0])
     rdir = os.fspath(root_dir) if root_dir is not None else root_dir
     flags = _flag_transform(flags)
     fname = os.fspath(filename)
 
-    # Ensure types are not mismatched
-    if (rdir is not None and not isinstance(rdir, ptype)):
-        raise TypeError(
-            "Pattern type of '{}' does not match the resolved type of '{}' of the root dir".format(ptype, type(rdir))
-        )
-
-    return bool(_wcparse.compile(pats, flags, limit).match(fname, rdir, dir_fd))
+    return bool(_wcparse.compile(patterns, flags, limit, exclude).match(fname, rdir, dir_fd))
 
 
 def globfilter(
     filenames: Iterable[Union[AnyStr, 'os.PathLike[AnyStr]']],
-    patterns: Union[str, bytes, Sequence[AnyStr]],
+    patterns: Union[AnyStr, Sequence[AnyStr]],
     *,
     flags: int = 0,
     root_dir: Optional[Union[AnyStr, 'os.PathLike[AnyStr]']] = None,
     dir_fd: Optional[int] = None,
-    limit: int = _wcparse.PATTERN_LIMIT
+    limit: int = _wcparse.PATTERN_LIMIT,
+    exclude: Optional[Union[AnyStr, Sequence[AnyStr]]] = None
 ) -> List[Union[AnyStr, 'os.PathLike[AnyStr]']]:
     """Filter names using pattern."""
 
-    pats = _wcparse.to_str_sequence(patterns)
-
     # Shortcut out if we have no patterns
-    if not pats:
+    if not patterns:
         return []
 
-    ptype = type(pats[0])
     rdir = os.fspath(root_dir) if root_dir is not None else root_dir
 
-    # Ensure types are not mismatched
-    if (rdir is not None and not isinstance(rdir, ptype)):
-        raise TypeError(
-            "Pattern type of '{}' does not match the resolved type of '{}' of the root dir".format(ptype, type(rdir))
-        )
-
     matches = []  # type: List[Union[AnyStr, 'os.PathLike[AnyStr]']]
     flags = _flag_transform(flags)
-    obj = _wcparse.compile(pats, flags, limit)
+    obj = _wcparse.compile(patterns, flags, limit, exclude)
 
     for filename in filenames:
         temp = os.fspath(filename)
diff --git a/wcmatch/pathlib.py b/wcmatch/pathlib.py
index e6fde4b..05462a6 100644
--- a/wcmatch/pathlib.py
+++ b/wcmatch/pathlib.py
@@ -4,7 +4,7 @@ import os
 from . import glob
 from . import _wcparse
 from . import util
-from typing import Iterable, Any, Union, Sequence, cast
+from typing import Iterable, Any, Union, Sequence, Optional, cast
 
 __all__ = (
     "CASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH",
@@ -112,7 +112,8 @@ class PurePath(pathlib.PurePath):
         patterns: Union[str, Sequence[str]],
         *,
         flags: int = 0,
-        limit: int = _wcparse.PATTERN_LIMIT
+        limit: int = _wcparse.PATTERN_LIMIT,
+        exclude: Optional[Union[str, Sequence[str]]] = None
     ) -> bool:
         """
         Match patterns using `globmatch`, but also using the same right to left logic that the default `pathlib` uses.
@@ -124,14 +125,15 @@ class PurePath(pathlib.PurePath):
 
         """
 
-        return self.globmatch(patterns, flags=flags | _RTL, limit=limit)
+        return self.globmatch(patterns, flags=flags | _RTL, limit=limit, exclude=exclude)
 
     def globmatch(
         self,
         patterns: Union[str, Sequence[str]],
         *,
         flags: int = 0,
-        limit: int = _wcparse.PATTERN_LIMIT
+        limit: int = _wcparse.PATTERN_LIMIT,
+        exclude: Optional[Union[str, Sequence[str]]] = None
     ) -> bool:
         """
         Match patterns using `globmatch`, but without the right to left logic that the default `pathlib` uses.
@@ -144,7 +146,8 @@ class PurePath(pathlib.PurePath):
             self._translate_path(),
             patterns,
             flags=self._translate_flags(flags),
-            limit=limit
+            limit=limit,
+            exclude=exclude
         )
 
 
@@ -173,7 +176,8 @@ class Path(pathlib.Path):
         patterns: Union[str, Sequence[str]],
         *,
         flags: int = 0,
-        limit: int = _wcparse.PATTERN_LIMIT
+        limit: int = _wcparse.PATTERN_LIMIT,
+        exclude: Optional[Union[str, Sequence[str]]] = None
     ) -> Iterable['Path']:
         """
         Search the file system.
@@ -187,7 +191,7 @@ class Path(pathlib.Path):
             flags = self._translate_flags(  # type: ignore[attr-defined]
                 flags | _NOABSOLUTE
             ) | ((_PATHLIB | SCANDOTDIR) if scandotdir else _PATHLIB)
-            for filename in glob.iglob(patterns, flags=flags, root_dir=str(self), limit=limit):
+            for filename in glob.iglob(patterns, flags=flags, root_dir=str(self), limit=limit, exclude=exclude):
                 yield self.joinpath(filename)
 
     def rglob(  # type: ignore[override]
@@ -195,7 +199,8 @@ class Path(pathlib.Path):
         patterns: Union[str, Sequence[str]],
         *,
         flags: int = 0,
-        limit: int = _wcparse.PATTERN_LIMIT
+        limit: int = _wcparse.PATTERN_LIMIT,
+        exclude: Optional[Union[str, Sequence[str]]] = None
     ) -> Iterable['Path']:
         """
         Recursive glob.
@@ -207,7 +212,7 @@ class Path(pathlib.Path):
 
         """
 
-        yield from self.glob(patterns, flags=flags | _EXTMATCHBASE, limit=limit)
+        yield from self.glob(patterns, flags=flags | _EXTMATCHBASE, limit=limit, exclude=exclude)
 
 
 class PurePosixPath(PurePath):


Test patch:
diff --git a/tests/test_fnmatch.py b/tests/test_fnmatch.py
index 8535943..c52d639 100644
--- a/tests/test_fnmatch.py
+++ b/tests/test_fnmatch.py
@@ -467,6 +467,8 @@ class TestFnMatchTranslate(unittest.TestCase):
 
         self.assertTrue(fnmatch.fnmatch('name', '!test', flags=fnmatch.N | fnmatch.A))
         self.assertTrue(fnmatch.fnmatch(b'name', b'!test', flags=fnmatch.N | fnmatch.A))
+        self.assertFalse(fnmatch.fnmatch('test', '!test', flags=fnmatch.N | fnmatch.A))
+        self.assertFalse(fnmatch.fnmatch(b'test', b'!test', flags=fnmatch.N | fnmatch.A))
 
     def test_default_translate(self):
         """Test default with exclusion in translation."""
@@ -475,6 +477,57 @@ class TestFnMatchTranslate(unittest.TestCase):
         self.assertTrue(len(fnmatch.translate(b'!test', flags=fnmatch.N | fnmatch.A)[0]) == 1)
 
 
+class TestExcludes(unittest.TestCase):
+    """Test expansion limits."""
+
+    def test_translate_exclude(self):
+        """Test exclusion in translation."""
+
+        results = fnmatch.translate('*', exclude='test')
+        self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1)
+        results = fnmatch.translate(b'*', exclude=b'test')
+        self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1)
+
+    def test_translate_exclude_mix(self):
+        """
+        Test translate exclude mix.
+
+        If both are given, flags are ignored.
+        """
+
+        results = fnmatch.translate(['*', '!test'], exclude=b'test', flags=fnmatch.N | fnmatch.A)
+        self.assertTrue(len(results[0]) == 2 and len(results[1]) == 1)
+
+    def test_exclude(self):
+        """Test exclude parameter."""
+
+        self.assertTrue(fnmatch.fnmatch('name', '*', exclude='test'))
+        self.assertTrue(fnmatch.fnmatch(b'name', b'*', exclude=b'test'))
+        self.assertFalse(fnmatch.fnmatch('test', '*', exclude='test'))
+        self.assertFalse(fnmatch.fnmatch(b'test', b'*', exclude=b'test'))
+
+    def test_exclude_mix(self):
+        """
+        Test exclusion flags mixed with exclusion parameter.
+
+        If both are given, flags are ignored.
+        """
+
+        self.assertTrue(fnmatch.fnmatch('name', '*', exclude='test', flags=fnmatch.N | fnmatch.A))
+        self.assertTrue(fnmatch.fnmatch(b'name', b'*', exclude=b'test', flags=fnmatch.N | fnmatch.A))
+        self.assertFalse(fnmatch.fnmatch('test', '*', exclude='test', flags=fnmatch.N | fnmatch.A))
+        self.assertFalse(fnmatch.fnmatch(b'test', b'*', exclude=b'test', flags=fnmatch.N | fnmatch.A))
+
+        self.assertTrue(fnmatch.fnmatch('name', ['*', '!name'], exclude='test', flags=fnmatch.N | fnmatch.A))
+        self.assertFalse(fnmatch.fnmatch('test', ['*', '!name'], exclude='test', flags=fnmatch.N | fnmatch.A))
+        self.assertTrue(fnmatch.fnmatch('!name', ['*', '!name'], exclude='test', flags=fnmatch.N | fnmatch.A))
+
+    def test_filter(self):
+        """Test exclusion with filter."""
+
+        self.assertEqual(fnmatch.filter(['name', 'test'], '*', exclude='test'), ['name'])
+
+
 class TestIsMagic(unittest.TestCase):
     """Test "is magic" logic."""
 
diff --git a/tests/test_glob.py b/tests/test_glob.py
index 73d40cc..9d626be 100644
--- a/tests/test_glob.py
+++ b/tests/test_glob.py
@@ -1608,6 +1608,16 @@ class TestGlobPaths(unittest.TestCase):
         )
 
 
+class TestExcludes(unittest.TestCase):
+    """Test expansion limits."""
+
+    def test_translate_exclude(self):
+        """Test excludes."""
+
+        results = glob.glob('**/*.md', exclude='**/README.md', flags=glob.GLOBSTAR)
+        self.assertTrue('README.md' not in results)
+
+
 @unittest.skipUnless(os.path.expanduser('~') != '~', "Requires expand user functionality")
 class TestTilde(unittest.TestCase):
     """Test tilde cases."""
diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py
index 99165fc..4dd6a74 100644
--- a/tests/test_globmatch.py
+++ b/tests/test_globmatch.py
@@ -1887,3 +1887,54 @@ class TestExpansionLimit(unittest.TestCase):
 
         with self.assertRaises(_wcparse.PatternLimitException):
             glob.translate('{1..11}', flags=glob.BRACE, limit=10)
+
+
+class TestExcludes(unittest.TestCase):
+    """Test expansion limits."""
+
+    def test_translate_exclude(self):
+        """Test exclusion in translation."""
+
+        results = glob.translate('*/somepath', exclude='test/somepath')
+        self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1)
+        results = glob.translate(b'*/somepath', exclude=b'test/somepath')
+        self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1)
+
+    def test_translate_exclude_mix(self):
+        """
+        Test translate exclude mix.
+
+        If both are given, flags are ignored.
+        """
+
+        results = glob.translate(['*/somepath', '!test/somepath'], exclude=b'test/somepath', flags=glob.N | glob.A)
+        self.assertTrue(len(results[0]) == 2 and len(results[1]) == 1)
+
+    def test_exclude(self):
+        """Test exclude parameter."""
+
+        self.assertTrue(glob.globmatch('path/name', '*/*', exclude='*/test'))
+        self.assertTrue(glob.globmatch(b'path/name', b'*/*', exclude=b'*/test'))
+        self.assertFalse(glob.globmatch('path/test', '*/*', exclude='*/test'))
+        self.assertFalse(glob.globmatch(b'path/test', b'*/*', exclude=b'*/test'))
+
+    def test_exclude_mix(self):
+        """
+        Test exclusion flags mixed with exclusion parameter.
+
+        If both are given, flags are ignored.
+        """
+
+        self.assertTrue(glob.globmatch('path/name', '*/*', exclude='*/test', flags=glob.N | glob.A))
+        self.assertTrue(glob.globmatch(b'path/name', b'*/*', exclude=b'*/test', flags=glob.N | glob.A))
+        self.assertFalse(glob.globmatch('path/test', '*/*', exclude='*/test', flags=glob.N | glob.A))
+        self.assertFalse(glob.globmatch(b'path/test', b'*/*', exclude=b'*/test', flags=glob.N | glob.A))
+
+        self.assertTrue(glob.globmatch('path/name', ['*/*', '!path/name'], exclude='*/test', flags=glob.N | glob.A))
+        self.assertFalse(glob.globmatch('path/test', ['*/*', '!path/name'], exclude='*/test', flags=glob.N | glob.A))
+        self.assertTrue(glob.globmatch('!path/name', ['*/*', '!path/name'], exclude='*/test', flags=glob.N | glob.A))
+
+    def test_filter(self):
+        """Test exclusion with filter."""
+
+        self.assertEqual(glob.globfilter(['path/name', 'path/test'], '*/*', exclude='path/test'), ['path/name'])
diff --git a/tests/test_pathlib.py b/tests/test_pathlib.py
index 277f7d1..5508056 100644
--- a/tests/test_pathlib.py
+++ b/tests/test_pathlib.py
@@ -372,3 +372,13 @@ class TestExpansionLimit(unittest.TestCase):
 
         with self.assertRaises(_wcparse.PatternLimitException):
             list(pathlib.Path('.').rglob('{1..11}', flags=pathlib.BRACE, limit=10))
+
+
+class TestExcludes(unittest.TestCase):
+    """Test expansion limits."""
+
+    def test_exclude(self):
+        """Test exclude parameter."""
+
+        self.assertTrue(pathlib.PurePath('path/name').globmatch('*/*', exclude='*/test'))
+        self.assertFalse(pathlib.PurePath('path/test').globmatch('*/*', exclude='*/test'))