Repository: facelessuser/wcmatch

Issue:
Add ability to change the current working directory when globbing
This would add a parameter, maybe `cwd`, `start`, or something similar. The parameter would allow a user to glob from a different location than they are currently in. We could probably extend `globmatch` with `REALPATH` to evaluate the location as well.

Discussion:
facelessuser: At the very least, we still need tests that verify path-like inputs into globfilter and globmatch before we can merge this.
facelessuser: Wondering if we need to rethink pathlib handling in `globmatch` and `globfilter`. We can force Windows or Unix/Linux path handling, but what if we give it Windows flavored path on and force Unix/Linux behavior? Do we need to worry about this? Or do we just normalize it and return the results as a string?

We need to think about this a bit more.
facelessuser: Apparently we were altering the returned value in filters by normalizing the paths. I'm not sure why we were doing this as we really shouldn't alter what we are given. Or better put, we can later it internally to do the comparison, but we should return the items in the list as they were given.

This became evident when we were feeding in path-like objects and getting out strings instead. This is not what we should be doing. This has been fixed moving forward.
facelessuser: Thinking about this more, I don't think we are going to worry about checking if `FORCE_WIN` and `FORCE_UNIX` are being enabled with `PosixPurePath` and `WindowsPurePath`. At least when it comes to the general interface of `glob.globmatch` and `glob.globfilter`. We will leave it up to the user to feed in the proper path objects, and if they don't, the path-like object will be treated in a generic manner.

Our custom pathlib objects will remain unchanged though. This means if you called `WindowsPurePath.globmatch` it will only function with Windows logic; you cannot force it to use Unix/Linux logic. But if you pass any path-like object through `glob.globmatch` (even our own pathlib object) you could force Unix/Linux behavior as `glob.globmatch` will not check any kind of mapping of Windows specific or Unix specific path-like objects, not even our own.

This is no different than how builtin functions handle path-like objects. These are both called on Windows. Notice it doesn't reject a Posix PurePath. As long as there is nothing in the path that `listdir` can't understand, then it will be handled without issue.

```py
>>> [x for x in os.listdir(pathlib.PureWindowsPath('docs/src'))]
['dictionary', 'markdown']
>>> [x for x in os.listdir(pathlib.PurePosixPath('docs/src'))]
['dictionary', 'markdown']
```

Granted, you can only instantiate  PurePath objects on a different OS, so we never had to worry about Path objects which access the file system.

facelessuser: @gir-bot lgtm
facelessuser: Small note though. Python 3.5 does not support the path-like interface, therefore wcmatch on Python 3.5 will also not support the path-like interface.
facelessuser: @gir-bot lgtm

Patch:
diff --git a/docs/src/markdown/changelog.md b/docs/src/markdown/changelog.md
index 1bc1ea0..de5b1b7 100644
--- a/docs/src/markdown/changelog.md
+++ b/docs/src/markdown/changelog.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## 5.1.0
+
+- **NEW**: Add new parameter to `glob` related functions (except in `pathlib`) called `root_dir` that allows a user to
+  specify a different working directory with either a string or path-like object. Path-like inputs are only supported on
+  Python 3.6+.
+- **NEW**: Support path-like objects for `globmatch` and `globfilter` path inputs. Path-like inputs are only supported
+  on Python 3.6+.
+- **FIX**: Filter functions should not alter the slashes of files it filters. Filtered strings and paths should be
+  returned unaltered.
+
 ## 5.0.3
 
 - **FIX**: Rework `glob` relative path handling so internally it is indistinguishable from when it is given no relative
diff --git a/docs/src/markdown/fnmatch.md b/docs/src/markdown/fnmatch.md
index 2844a39..1a68a69 100644
--- a/docs/src/markdown/fnmatch.md
+++ b/docs/src/markdown/fnmatch.md
@@ -139,7 +139,7 @@ patterns.
 
 `CASE` forces case sensitivity. `CASE` has higher priority than [`IGNORECASE`](#fnmatchignorecase).
 
-!!! new "New 4.3.0"
+!!! new "New 4.3"
     `CASE` is new in 4.3.0.
 
 #### `fnmatch.IGNORECASE, fnmatch.I` {: #fnmatchignorecase}
@@ -228,7 +228,7 @@ normalized. This is great if you need to match Windows specific names on a Linux
 
 If `FORCEWIN` is used along side [`FORCEUNIX`](#fnmatchforceunix), both will be ignored.
 
-!!! new "New 4.2.0"
+!!! new "New 4.2"
     `FORCEWIN` is new in 4.2.0.
 
 #### `fnmatch.FORCEUNIX, fnmatch.U` {: #fnmatchforceunix}
@@ -241,7 +241,7 @@ to use case insensitivity.
 
 If `FORCEUNIX` is used along side [`FORCEWIN`](#fnmatchforcewin), both will be ignored.
 
-!!! new "New 4.2.0"
+!!! new "New 4.2"
     `FORCEUNIX` is new in 4.2.0.
 
 --8<--
diff --git a/docs/src/markdown/glob.md b/docs/src/markdown/glob.md
index cf7d7d2..8a6ff9f 100644
--- a/docs/src/markdown/glob.md
+++ b/docs/src/markdown/glob.md
@@ -97,10 +97,14 @@ Pattern           | Meaning
 #### `glob.glob`
 
 ```py3
-def glob(patterns, *, flags=0):
+def glob(patterns, *, flags=0, root_dir=None):
 ```
 
-`glob` takes a pattern (or list of patterns) and will crawl the file system returning matching files.
+`glob` takes a pattern (or list of patterns) and will crawl the file system returning matching files, flags, and an
+optional root directory (string or path-like object).
+
+!!! 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.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -166,10 +170,25 @@ $ echo {*,README}.md
 LICENSE.md README.md README.md
 ```
 
+By default, `glob` uses the current working directory to evaluate relative patterns. Normally you'd have to use
+`#!py3 os.chdir('/new/path')` to evaluate patterns relative to a different path. By setting `root_dir` parameter you can
+change the root path without using `os.chdir`.
+
+```pycon3
+>>> from wcmatch import glob
+>>> glob.glob('*')
+['appveyor.yml', 'docs', 'LICENSE.md', 'MANIFEST.in', 'mkdocs.yml', 'README.md', 'requirements', 'setup.cfg', 'setup.py', 'tests', 'tox.ini', 'wcmatch']
+>>> glob.glob('*', root_dir='docs/src')
+['dictionary', 'markdown']
+```
+
+!!! new "New 5.1"
+    `root_dir` was added in 5.1.0.
+
 #### `glob.iglob`
 
 ```py3
-def iglob(patterns, *, flags=0):
+def iglob(patterns, *, flags=0, root_dir=None):
 ```
 
 `iglob` is just like [`glob`](#globglob) except it returns an iterator.
@@ -180,14 +199,20 @@ def iglob(patterns, *, flags=0):
 ['docs/src/markdown/_snippets/abbr.md', 'docs/src/markdown/_snippets/links.md', 'docs/src/markdown/_snippets/refs.md', 'docs/src/markdown/changelog.md', 'docs/src/markdown/fnmatch.md', 'docs/src/markdown/glob.md', 'docs/src/markdown/index.md', 'docs/src/markdown/installation.md', 'docs/src/markdown/license.md', 'README.md']
 ```
 
+!!! new "New 5.1"
+    `root_dir` was added in 5.1.0.
+
 #### `glob.globmatch`
 
 ```py3
-def globmatch(filename, patterns, *, flags=0):
+def globmatch(filename, patterns, *, flags=0, root_dir=None):
 ```
 
-`globmatch` takes a file name, a pattern (or list of patterns), and flags.  It will return a boolean indicating whether
-the file path was matched by the pattern(s).
+`globmatch` takes a file name (string or path-like object), a pattern (or list of patterns), flags, and an optional root
+directory.  It will return a boolean indicating whether the file path was matched by the pattern(s).
+
+!!! 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.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -274,15 +299,33 @@ False
 True
 ```
 
+If you are using [`REALPATH`](#globrealpath) and want to evaluate the paths relative to a different directory, you can
+set the `root_dir` parameter.
+
+```pycon3
+>>> from wcmatch import glob
+>>> glob.globmatch('markdown', 'markdown', flags=glob.REALPATH)
+False
+>>> glob.globmatch('markdown', 'markdown', flags=glob.REALPATH, root_dir='docs/src')
+True
+```
+
+!!! new "New 5.1"
+    - `root_dir` was added in 5.1.0.
+    - path-like object support for file path inputs was added in 5.1.0
+
 #### `glob.globfilter`
 
 ```py3
-def globfilter(filenames, patterns, *, flags=0):
+def globfilter(filenames, patterns, *, flags=0, root_dir=None):
 ```
 
-`globfilter` takes a list of file paths, a pattern (or list of patterns), and flags. It returns a list of all files
-paths that matched the pattern(s). The same logic used for [`globmatch`](#globglobmatch) is used for `globfilter`,
-albeit more efficient for processing multiple files.
+`globfilter` takes a list of file paths (strings or path-like objects), a pattern (or list of patterns), and flags. It
+returns a list of all files paths that matched the pattern(s). The same logic used for [`globmatch`](#globglobmatch) 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.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -295,6 +338,10 @@ associated. But you can enable the [`REALPATH`](#globrealpath) flag and `globfil
 context such as: whether the file exists, whether it is a directory or not, or whether it has symlinks that should not
 be matched by `GLOBSTAR`. See [`globmatch`](#globglobmatch) for examples.
 
+!!! new "New 5.1"
+    - `root_dir` was added in 5.1.0.
+    - path-like object support for file path inputs was added in 5.1.0
+
 #### `glob.translate`
 
 ```py3
@@ -393,7 +440,7 @@ escaping will be used.
 On Windows, drive letters (`C:`) and UNC host/share (`//host/share`) portions of a path will still be treated case
 insensitively, but the rest of the path will have case sensitive logic applied.
 
-!!! new "New 4.3.0"
+!!! new "New 4.3"
     `CASE` is new in 4.3.0.
 
 #### `glob.IGNORECASE, glob.I` {: #globignorecase}
@@ -479,7 +526,7 @@ logic so that the path must meet the following in order to match:
 Since `REALPATH` causes the file system to be referenced when matching a path, flags such as
 [`FORCEUNIX`](#globforceunix) and [`FORCEWIN`](#globforcewin) are not allowed with this flag and will be ignored.
 
-!!! new "NEW 3.0"
+!!! new "New 3.0"
     `REALPATH` was added in 3.0.
 
 #### `glob.DOTGLOB, glob.D` {: #globdotglob}
@@ -589,7 +636,7 @@ or [`iglob`](#globiglob). It also will not work when using the [`REALPATH`](#glo
 
 If `FORCEWIN` is used along side [`FORCEUNIX`](#globforceunix), both will be ignored.
 
-!!! new "New 4.2.0"
+!!! new "New 4.2"
     `FORCEWIN` is new in 4.2.0.
 
 #### `glob.FORCEUNIX, glob.U` {: #globforceunix}
@@ -605,7 +652,7 @@ use case insensitivity.
 
 If `FORCEUNIX` is used along side [`FORCEWIN`](#globforcewin), both will be ignored.
 
-!!! new "New 4.2.0"
+!!! new "New 4.2"
     `FORCEUNIX` is new in 4.2.0.
 
 --8<--
diff --git a/docs/src/markdown/pathlib.md b/docs/src/markdown/pathlib.md
index dda93c5..4a1b19d 100644
--- a/docs/src/markdown/pathlib.md
+++ b/docs/src/markdown/pathlib.md
@@ -249,6 +249,10 @@ This method calls our own [`iglob`](./glob.md#globiglob) implementation, and as
 in respect to features, the one exception being that instead of returning path strings in the generator, it will return
 [`Path`](#pathlibpath) objects.
 
+The one difference between this `glob` and the [`iglob`](./glob.md#globiglob) API is that this function does not accept
+the `root_dir` parameter. All searches are relative to the object's path, which is evaluated relative to the current
+working directory.
+
 ```pycon3
 >>> from wcmatch import pathlib
 >>> p = pathlib.Path('docs/src')
diff --git a/docs/src/markdown/wcmatch.md b/docs/src/markdown/wcmatch.md
index 8343960..0ada6a4 100644
--- a/docs/src/markdown/wcmatch.md
+++ b/docs/src/markdown/wcmatch.md
@@ -173,7 +173,7 @@ Checks if an abort has been issued.
 True
 ```
 
-!!! new "New 4.1.0"
+!!! new "New 4.1"
     `is_aborted` was added in 4.1.0.
 
 #### `WcMatch.get_skipped`
@@ -286,7 +286,7 @@ is called on every new [`match`](#wcmatchmatch) call.
 
 `RECURSIVE` forces a recursive search that will crawl all subdirectories.
 
-!!! new "New 3.0.0"
+!!! new "New 3.0"
     Added in 3.0 and must be used instead of the old `recursive` parameter which has also been removed as of 3.0.
 
 #### `wcmatch.HIDDEN, wcmatch.HD` {: #wcmatchhidden}
@@ -294,7 +294,7 @@ is called on every new [`match`](#wcmatchmatch) call.
 `HIDDEN` enables the crawling of hidden directories and will return hidden files if the wildcard pattern matches. This
 enables not just dot files, but system hidden files as well.
 
-!!! new "New 3.0.0"
+!!! new "New 3.0"
     Added in 3.0 and must be used instead of the old `show_hidden` parameter which has also been removed as of 3.0.
 
 #### `wcmatch.SYMLINK, wcmatch.SL` {: #wcmatchsymlink}
@@ -302,14 +302,14 @@ enables not just dot files, but system hidden files as well.
 `SYMLINK` enables the crawling of symlink directories. By default, symlink directories are ignored during the file
 crawl.
 
-!!! new "New 3.0.0"
+!!! new "New 3.0"
     Added in 3.0. Additionally, symlinks are now ignored by default moving forward if `SYMLINK` is not enabled.
 
 #### `wcmatch.CASE, wcmatch.C` {: #wcmatchcase}
 
 `CASE` forces case sensitivity. `CASE` has higher priority than [`IGNORECASE`](#wcmatchignorecase).
 
-!!! new "New 4.3.0"
+!!! new "New 4.3"
     `CASE` is new in 4.3.0.
 
 #### `wcmatch.IGNORECASE, wcmatch.I` {: #wcmatchignorecase}
diff --git a/wcmatch/__init__.py b/wcmatch/__init__.py
index eaaeebd..e0bb9f3 100644
--- a/wcmatch/__init__.py
+++ b/wcmatch/__init__.py
@@ -4,7 +4,7 @@ Wild Card Match.
 A custom implementation of `fnmatch` and `glob`.
 
 Licensed under MIT
-Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+Copyright (c) 2018 - 2019 Isaac Muse <isaacmuse@gmail.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 documentation files (the "Software"), to deal in the Software without restriction, including without limitation
diff --git a/wcmatch/_wcparse.py b/wcmatch/_wcparse.py
index 6ab1ae0..daa5215 100644
--- a/wcmatch/_wcparse.py
+++ b/wcmatch/_wcparse.py
@@ -4,7 +4,7 @@ Wild Card Match.
 A custom implementation of `fnmatch`.
 
 Licensed under MIT
-Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+Copyright (c) 2018 - 2019 Isaac Muse <isaacmuse@gmail.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 documentation files (the "Software"), to deal in the Software without restriction, including without limitation
@@ -1415,7 +1415,7 @@ class WcParse(object):
         return pattern
 
 
-def _fs_match(pattern, filename, is_dir, sep, follow, symlinks):
+def _fs_match(pattern, filename, is_dir, sep, follow, symlinks, root):
     """
     Match path against the pattern.
 
@@ -1450,7 +1450,7 @@ def _fs_match(pattern, filename, is_dir, sep, follow, symlinks):
                     at_end = m.end(i) == end
                     parts = star.strip(sep).split(sep)
                     if base is None:
-                        base = filename[:m.start(i)]
+                        base = os.path.join(root, filename[:m.start(i)])
                     for part in parts:
                         base = os.path.join(base, part)
                         if is_dir or i != last or not at_end:
@@ -1468,7 +1468,7 @@ def _fs_match(pattern, filename, is_dir, sep, follow, symlinks):
     return matched
 
 
-def _match_real(filename, include, exclude, follow, symlinks):
+def _match_real(filename, include, exclude, follow, symlinks, root):
     """Match real filename includes and excludes."""
 
     sep = '\\' if util.platform() == "windows" else '/'
@@ -1477,7 +1477,7 @@ def _match_real(filename, include, exclude, follow, symlinks):
 
     is_dir = filename.endswith(sep)
     try:
-        is_file_dir = os.path.isdir(filename)
+        is_file_dir = os.path.isdir(os.path.join(root, filename))
     except OSError:  # pragma: no cover
         is_file_dir = False
 
@@ -1487,7 +1487,7 @@ def _match_real(filename, include, exclude, follow, symlinks):
 
     matched = False
     for pattern in include:
-        if _fs_match(pattern, filename, is_dir, sep, follow, symlinks):
+        if _fs_match(pattern, filename, is_dir, sep, follow, symlinks, root):
             matched = True
             break
 
@@ -1495,34 +1495,36 @@ def _match_real(filename, include, exclude, follow, symlinks):
         matched = True
         if exclude:
             for pattern in exclude:
-                if _fs_match(pattern, filename, is_dir, sep, True, symlinks):
+                if _fs_match(pattern, filename, is_dir, sep, True, symlinks, root):
                     matched = False
                     break
     return matched
 
 
-def _match_pattern(filename, include, exclude, real, path, follow):
+def _match_pattern(filename, include, exclude, real, path, follow, root_dir=None):
     """Match includes and excludes."""
 
     if real:
+
         symlinks = {}
         if isinstance(filename, bytes):
-            curdir = os.fsencode(os.curdir)
+            root = root_dir if root_dir else b'.'
             ptype = BYTES
         else:
-            curdir = os.curdir
+            root = root_dir if root_dir else '.'
             ptype = UNICODE
+
         mount = RE_WIN_MOUNT[ptype] if util.platform() == "windows" else RE_MOUNT[ptype]
 
         if not mount.match(filename):
-            exists = os.path.lexists(os.path.join(curdir, filename))
+            exists = os.path.lexists(os.path.join(root, filename))
         else:
             exists = os.path.lexists(filename)
 
         if not exists:
             return False
         if path:
-            return _match_real(filename, include, exclude, follow, symlinks)
+            return _match_real(filename, include, exclude, follow, symlinks, root)
 
     matched = False
     for pattern in include:
@@ -1595,10 +1597,18 @@ class WcRegexp(util.Immutable):
             self._follow != other._follow
         )
 
-    def match(self, filename):
+    def match(self, filename, root_dir=None):
         """Match filename."""
 
-        return _match_pattern(filename, self._include, self._exclude, self._real, self._path, self._follow)
+        return _match_pattern(
+            filename,
+            self._include,
+            self._exclude,
+            self._real,
+            self._path,
+            self._follow,
+            root_dir=root_dir
+        )
 
 
 def _pickle(p):
diff --git a/wcmatch/fnmatch.py b/wcmatch/fnmatch.py
index 1eb882b..8a79183 100644
--- a/wcmatch/fnmatch.py
+++ b/wcmatch/fnmatch.py
@@ -4,7 +4,7 @@ Wild Card Match.
 A custom implementation of `fnmatch`.
 
 Licensed under MIT
-Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+Copyright (c) 2018 - 2019 Isaac Muse <isaacmuse@gmail.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 documentation files (the "Software"), to deal in the Software without restriction, including without limitation
@@ -100,8 +100,6 @@ def filter(filenames, patterns, *, flags=0):  # noqa A001
     obj = _wcparse.compile(_wcparse.split(patterns, flags), flags)
 
     for filename in filenames:
-        if not unix:
-            filename = _wcparse.norm_slash(filename, flags)
-        if obj.match(filename):
+        if obj.match(_wcparse.norm_slash(filename, flags) if not unix else filename):
             matches.append(filename)
     return matches
diff --git a/wcmatch/glob.py b/wcmatch/glob.py
index 3a091ea..9694fc8 100644
--- a/wcmatch/glob.py
+++ b/wcmatch/glob.py
@@ -4,7 +4,7 @@ Wild Card Match.
 A custom implementation of `glob`.
 
 Licensed under MIT
-Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+Copyright (c) 2018 - 2019 Isaac Muse <isaacmuse@gmail.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 documentation files (the "Software"), to deal in the Software without restriction, including without limitation
@@ -107,12 +107,12 @@ def _flag_transform(flags):
 class Glob(object):
     """Glob patterns."""
 
-    def __init__(self, pattern, flags=0, curdir=None):
+    def __init__(self, pattern, flags=0, root_dir=None):
         """Initialize the directory walker object."""
 
         self.is_bytes = isinstance(pattern[0], bytes)
         self.current = b'.' if self.is_bytes else '.'
-        self.curdir = curdir if curdir is not None else self.current
+        self.root_dir = util.fscodec(root_dir, self.is_bytes) if root_dir is not None else self.current
         self.mark = bool(flags & MARK)
         if self.mark:
             flags ^= MARK
@@ -225,13 +225,23 @@ class Glob(object):
             matcher = target.match
         return matcher
 
+    def prepend_base(self, path):
+        """Join path to base if pattern is not absolute."""
+
+        if self.is_abs_pattern:
+            return path
+        else:
+            return os.path.join(self.root_dir, path)
+
     def _iter(self, curdir, dir_only, deep):
         """Iterate the directory."""
 
         if not curdir:
-            scandir = self.curdir
+            scandir = self.root_dir
+        elif self.is_abs_pattern:
+            scandir = curdir
         else:
-            scandir = os.path.join(self.curdir, curdir)
+            scandir = os.path.join(self.root_dir, curdir)
 
         # Python will never return . or .., so fake it.
         for special in self.specials:
@@ -384,7 +394,7 @@ class Glob(object):
                 else:
                     yield path, is_dir
 
-    def _get_starting_paths(self, curdir, dir_only, base):
+    def _get_starting_paths(self, curdir, dir_only):
         """
         Get the starting location.
 
@@ -397,7 +407,7 @@ class Glob(object):
         results = [(curdir, True)]
 
         if not self._is_parent(curdir) and not self._is_this(curdir):
-            fullpath = os.path.abspath(os.path.join(base, curdir))
+            fullpath = os.path.abspath(self.prepend_base(curdir))
             basename = os.path.basename(fullpath)
             dirname = os.path.dirname(fullpath)
             if basename:
@@ -417,28 +427,27 @@ class Glob(object):
         """Starts off the glob iterator."""
 
         curdir = self.current
-        base = self.curdir
 
         for pattern in self.pattern:
             # If the pattern ends with `/` we return the files ending with `/`.
             dir_only = pattern[-1].dir_only if pattern else False
+            self.is_abs_pattern = pattern[0].is_drive if pattern else False
 
             if pattern:
                 if not pattern[0].is_magic:
                     # Path starts with normal plain text
                     # Lets verify the case of the starting directory (if possible)
                     this = pattern[0]
-
                     curdir = this[0]
 
                     # Abort if we cannot find the drive, or if current directory is empty
-                    if not curdir or (this.is_drive and not os.path.lexists(os.path.join(base, curdir))):
+                    if not curdir or (self.is_abs_pattern and not os.path.lexists(self.prepend_base(curdir))):
                         continue
 
                     # Make sure case matches, but running case insensitive
                     # on a case sensitive file system may return more than
                     # one starting location.
-                    results = [(curdir, True)] if this.is_drive else self._get_starting_paths(curdir, dir_only, base)
+                    results = [(curdir, True)] if self.is_abs_pattern else self._get_starting_paths(curdir, dir_only)
                     if not results:
                         continue
 
@@ -457,7 +466,7 @@ class Glob(object):
                     else:
                         # Return the file(s) and finish.
                         for match, is_dir in results:
-                            if os.path.lexists(os.path.join(base, match)) and not self._is_excluded(match, is_dir):
+                            if os.path.lexists(self.prepend_base(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
@@ -468,16 +477,16 @@ class Glob(object):
                             yield self.format_path(match, is_dir, dir_only)
 
 
-def iglob(patterns, *, flags=0):
+def iglob(patterns, *, flags=0, root_dir=None):
     """Glob."""
 
-    yield from Glob(util.to_tuple(patterns), flags).glob()
+    yield from Glob(util.to_tuple(patterns), flags, root_dir).glob()
 
 
-def glob(patterns, *, flags=0):
+def glob(patterns, *, flags=0, root_dir=None):
     """Glob."""
 
-    return list(iglob(util.to_tuple(patterns), flags=flags))
+    return list(iglob(util.to_tuple(patterns), flags=flags, root_dir=root_dir))
 
 
 def translate(patterns, *, flags=0):
@@ -487,7 +496,7 @@ def translate(patterns, *, flags=0):
     return _wcparse.translate(_wcparse.split(patterns, flags), flags)
 
 
-def globmatch(filename, patterns, *, flags=0):
+def globmatch(filename, patterns, *, flags=0, root_dir=None):
     """
     Check if filename matches pattern.
 
@@ -495,25 +504,34 @@ def globmatch(filename, patterns, *, flags=0):
     but if `case_sensitive` is set, respect that instead.
     """
 
+    is_bytes = isinstance(patterns[0], bytes) if not isinstance(patterns, (bytes, str)) else isinstance(patterns, bytes)
+    if root_dir is not None:
+        root_dir = util.fscodec(root_dir, is_bytes)
+
     flags = _flag_transform(flags)
+    filename = util.fscodec(filename, is_bytes)
     if not _wcparse.is_unix_style(flags):
         filename = _wcparse.norm_slash(filename, flags)
-    return _wcparse.compile(_wcparse.split(patterns, flags), flags).match(filename)
+    return _wcparse.compile(_wcparse.split(patterns, flags), flags).match(filename, root_dir=root_dir)
 
 
-def globfilter(filenames, patterns, *, flags=0):
+def globfilter(filenames, patterns, *, flags=0, root_dir=None):
     """Filter names using pattern."""
 
-    matches = []
+    is_bytes = isinstance(patterns[0], bytes) if not isinstance(patterns, (bytes, str)) else isinstance(patterns, bytes)
+    if root_dir is not None:
+        root_dir = util.fscodec(root_dir, is_bytes)
 
+    matches = []
     flags = _flag_transform(flags)
     unix = _wcparse.is_unix_style(flags)
     obj = _wcparse.compile(_wcparse.split(patterns, flags), flags)
 
     for filename in filenames:
+        temp = util.fscodec(filename, is_bytes)
         if not unix:
-            filename = _wcparse.norm_slash(filename, flags)
-        if obj.match(filename):
+            temp = _wcparse.norm_slash(temp, flags)
+        if obj.match(temp, root_dir):
             matches.append(filename)
     return matches
 
diff --git a/wcmatch/pathlib.py b/wcmatch/pathlib.py
index 0b82e0f..0e0adac 100644
--- a/wcmatch/pathlib.py
+++ b/wcmatch/pathlib.py
@@ -3,7 +3,6 @@ import pathlib
 import os
 from . import glob
 from . import _wcparse
-from . import util
 
 __all__ = (
     "CASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH",
@@ -76,7 +75,7 @@ class Path(pathlib.Path):
 
         if self.is_dir():
             flags = self._translate_flags(flags | _wcparse._NOABSOLUTE)
-            for filename in glob.Glob(util.to_tuple(patterns), flags, curdir=str(self)).glob():
+            for filename in glob.iglob(patterns, flags=flags, root_dir=str(self)):
                 yield self.joinpath(filename)
 
     def rglob(self, patterns, *, flags=0):
diff --git a/wcmatch/util.py b/wcmatch/util.py
index c24e34c..e2c15a0 100644
--- a/wcmatch/util.py
+++ b/wcmatch/util.py
@@ -253,3 +253,15 @@ def warn_deprecated(message, stacklevel=2):  # pragma: no cover
         category=DeprecationWarning,
         stacklevel=stacklevel
     )
+
+
+def fscodec(path, encode=False):
+    """
+    Provide common interface when using to translate path-like files.
+
+    Python 3.5 does not support `os.PathLike` interfaces, so we only return strings and bytes.
+    """
+
+    if not isinstance(path, (str, bytes)):
+        path = os.fsencode(path) if encode else os.fsdecode(path)
+    return path
diff --git a/wcmatch/wcmatch.py b/wcmatch/wcmatch.py
index 9e40d97..9ebc6c8 100644
--- a/wcmatch/wcmatch.py
+++ b/wcmatch/wcmatch.py
@@ -4,7 +4,7 @@ Wild Card Match.
 A module for performing wild card matches.
 
 Licensed under MIT
-Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+Copyright (c) 2018 - 2019 Isaac Muse <isaacmuse@gmail.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 documentation files (the "Software"), to deal in the Software without restriction, including without limitation


Test patch:
diff --git a/tests/test_fnmatch.py b/tests/test_fnmatch.py
index fca1dbb..b3a46a7 100644
--- a/tests/test_fnmatch.py
+++ b/tests/test_fnmatch.py
@@ -239,13 +239,13 @@ class TestFnMatchFilter:
         [
             'usr/*',
             ['usr/bin', 'usr', 'usr\\lib'],
-            (['usr\\bin', 'usr\\lib'] if not util.is_case_sensitive() else ['usr/bin']),
+            (['usr/bin', 'usr\\lib'] if not util.is_case_sensitive() else ['usr/bin']),
             0
         ],
         [
             r'usr\\*',
             ['usr/bin', 'usr', 'usr\\lib'],
-            (['usr\\bin', 'usr\\lib'] if not util.is_case_sensitive() else ['usr\\lib']),
+            (['usr/bin', 'usr\\lib'] if not util.is_case_sensitive() else ['usr\\lib']),
             0
         ],
         [r'te\st[ma]', ['testm', 'test\\3', 'testa'], ['testm', 'testa'], fnmatch.I],
diff --git a/tests/test_glob.py b/tests/test_glob.py
index 6b4b210..5c0fe4e 100644
--- a/tests/test_glob.py
+++ b/tests/test_glob.py
@@ -11,6 +11,7 @@ made difference in implementation.
 """
 import contextlib
 from wcmatch import glob
+from wcmatch import pathlib
 from wcmatch import util
 import re
 import types
@@ -248,6 +249,9 @@ class _TestGlob:
             cls.assert_equal({type(r) for r in res}, {str})
         cls.assert_count_equal(glob.iglob(p, **kwargs), res)
 
+        if 'root_dir' in kwargs and kwargs['root_dir'] is not None:
+            kwargs['root_dir'] = os.fsencode(kwargs['root_dir'])
+
         bres = [os.fsencode(x) for x in res]
         cls.assert_count_equal(glob.glob(os.fsencode(p), **kwargs), bres)
         cls.assert_count_equal(glob.iglob(os.fsencode(p), **kwargs), bres)
@@ -283,6 +287,9 @@ class _TestGlob:
             cls.assert_equal({type(r) for r in res}, {str})
         cls.assert_count_equal(glob.iglob(p, **kwargs), res)
 
+        if 'root_dir' in kwargs and kwargs['root_dir'] is not None:
+            kwargs['root_dir'] = os.fsencode(kwargs['root_dir'])
+
         bres = [os.fsencode(x) for x in res]
         cls.assert_count_equal(glob.glob([os.fsencode(x) for x in p], **kwargs), bres)
         cls.assert_count_equal(glob.iglob([os.fsencode(x) for x in p], **kwargs), bres)
@@ -345,8 +352,10 @@ class _TestGlob:
         print("EXPECTED: ", results)
 
         if cls.cwd_temp:
-            with change_cwd(cls.tempdir):
-                res = cls.nglob(*pattern, flags=flags) if negative else cls.glob(*pattern, flags=flags)
+            if negative:
+                res = cls.nglob(*pattern, flags=flags, root_dir=cls.tempdir)
+            else:
+                res = cls.glob(*pattern, flags=flags, root_dir=cls.tempdir)
         else:
             res = cls.nglob(*pattern, flags=flags) if negative else cls.glob(*pattern, flags=flags)
         if results is not None:
@@ -854,16 +863,14 @@ class Testglob(_TestGlob):
     def test_negateall(self):
         """Negate applied to all files."""
 
-        with change_cwd(self.tempdir):
-            for file in glob.glob('!**/', flags=glob.N | glob.NEGATEALL | glob.G):
-                self.assertFalse(os.path.isdir(file))
+        for file in glob.glob('!**/', flags=glob.N | glob.NEGATEALL | glob.G, root_dir=self.tempdir):
+            self.assertFalse(os.path.isdir(file))
 
     def test_negateall_bytes(self):
         """Negate applied to all files."""
 
-        with change_cwd(self.tempdir):
-            for file in glob.glob(b'!**/', flags=glob.N | glob.NEGATEALL | glob.G):
-                self.assertFalse(os.path.isdir(file))
+        for file in glob.glob(b'!**/', flags=glob.N | glob.NEGATEALL | glob.G, root_dir=os.fsencode(self.tempdir)):
+            self.assertFalse(os.path.isdir(file))
 
 
 class TestGlobMarked(Testglob):
@@ -895,11 +902,28 @@ class TestCWD(_TestGlob):
             os.symlink(os.path.join('a', 'bcd'), cls.norm('sym3'))
 
     def test_cwd(self):
-        """Test glob cases."""
+        """Test root level glob on current working directory."""
 
         with change_cwd(self.tempdir):
             self.assert_equal(glob.glob('EF'), ['EF'])
 
+    def test_cwd_root_dir(self):
+        """Test root level glob when we switch directory via `root_dir`."""
+
+        self.assert_equal(glob.glob('EF', root_dir=self.tempdir), ['EF'])
+
+    @pytest.mark.skipif(not util.PY36, reason="path-like interface not supported on Python < 3.6")
+    def test_cwd_root_dir_pathlike(self):
+        """Test root level glob when we switch directory via `root_dir` with a path-like object."""
+
+        self.assert_equal(glob.glob('EF', root_dir=pathlib.Path(self.tempdir)), ['EF'])
+
+    @pytest.mark.skipif(not util.PY36, reason="path-like interface not supported on Python < 3.6")
+    def test_cwd_root_dir_pathlike_bytes(self):
+        """Test root level glob when we switch directory via `root_dir` with a path-like object."""
+
+        self.assert_equal(glob.glob(b'EF', root_dir=pathlib.Path(self.tempdir)), [b'EF'])
+
 
 class TestGlobCornerCase(_TestGlob):
     """
diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py
index 7556afd..261379b 100644
--- a/tests/test_globmatch.py
+++ b/tests/test_globmatch.py
@@ -686,7 +686,7 @@ class TestGlobFilter:
                 flags=flags
             )
         )
-        source = sorted(cls.norm_files(case[1], flags))
+        source = sorted(case[1])
         print("TEST: ", result, '<==>', source, '\n')
         cls.assert_equal(result, source)
 
@@ -1346,6 +1346,68 @@ class TestGlobMatchSpecial(unittest.TestCase):
             self.assertFalse(glob.globmatch('docs/', '**/DOCS/**', flags=self.flags | glob.FORCEWIN | glob.FORCEUNIX))
             self.assertTrue(glob.globmatch('docs/', '**/docs/**', flags=self.flags | glob.FORCEWIN | glob.FORCEUNIX))
 
+    def test_root_dir(self):
+        """Test root directory with `globmatch`."""
+
+        self.assertFalse(glob.globmatch('markdown', 'markdown', flags=glob.REALPATH))
+        self.assertTrue(glob.globmatch('markdown', 'markdown', flags=glob.REALPATH, root_dir='docs/src'))
+
+    @pytest.mark.skipif(not util.PY36, reason="path-like interface not supported on Python < 3.6")
+    def test_match_root_dir_pathlib(self):
+        """Test root directory with `globmatch` using `pathlib`."""
+
+        from wcmatch import pathlib
+
+        self.assertFalse(glob.globmatch(pathlib.Path('markdown'), 'markdown', flags=glob.REALPATH))
+        self.assertTrue(
+            glob.globmatch(pathlib.Path('markdown'), 'markdown', flags=glob.REALPATH, root_dir=pathlib.Path('docs/src'))
+        )
+
+    @pytest.mark.skipif(not util.PY36, reason="path-like interface not supported on Python < 3.6")
+    def test_match_root_dir_pathlib_bytes(self):
+        """Test root directory with `globmatch` using `pathlib`."""
+
+        from wcmatch import pathlib
+
+        self.assertFalse(glob.globmatch(pathlib.Path('markdown'), b'markdown', flags=glob.REALPATH))
+        self.assertTrue(
+            glob.globmatch(
+                pathlib.Path('markdown'),
+                b'markdown', flags=glob.REALPATH,
+                root_dir=pathlib.Path('docs/src')
+            )
+        )
+
+    @pytest.mark.skipif(not util.PY36, reason="path-like interface not supported on Python < 3.6")
+    def test_filter_root_dir_pathlib(self):
+        """Test root directory with `globfilter`."""
+
+        from wcmatch import pathlib
+
+        results = glob.globfilter(
+            [pathlib.Path('markdown')],
+            'markdown',
+            flags=glob.REALPATH,
+            root_dir=pathlib.Path('docs/src')
+        )
+
+        self.assertTrue(all([isinstance(result, pathlib.Path) for result in results]))
+
+    @pytest.mark.skipif(not util.PY36, reason="path-like interface not supported on Python < 3.6")
+    def test_filter_root_dir_pathlib_bytes(self):
+        """Test root directory with `globfilter`."""
+
+        from wcmatch import pathlib
+
+        results = glob.globfilter(
+            [pathlib.Path('markdown')],
+            b'markdown',
+            flags=glob.REALPATH,
+            root_dir=pathlib.Path('docs/src')
+        )
+
+        self.assertTrue(all([isinstance(result, pathlib.Path) for result in results]))
+
 
 @skip_unless_symlink
 class TestGlobmatchSymlink(_TestGlobmatch):