Repository: facelessuser/wcmatch

Issue:
Odd behavior with wcmatch.glob.glob()?
Hello there.  I was very pleased to see a python module that attempted to codify the extended glob pattern matching that one gets with the Bash command line. Great stuff!

However, in making use of it, I found a weird scenario. 

Consider this piece of code run under Python 3.6:

```py
glob.globfilter(["goo.cfg", "foo.bar", "foo.bar.cfg", 'foo.cfg.bar'], '!*.bar', flags=glob.EXTGLOB|glob.NEGATE)
```

It unexpectedly returns:

```
['goo.cfg']
```

That doesn't make sense to me. I would have expected:

```
['goo.cfg', 'foo.bar.cfg']
```

I meant, the following code behaves the way I would expect when I remove the negation:

```py
glob.globfilter(["goo.cfg", "foo.bar", "foo.bar.cfg", 'foo.cfg.bar'], '*.bar', flags=glob.EXTGLOB|glob.NEGATE)
```

```
['foo.bar', 'foo.cfg.bar']
```

foo.bar.cfg doesn't appear and rightly so in the positive case so why is it disappearing in the negative one.

Could there be a syntactic misunderstanding that I am not picking up from the docs?

Regards,
IMc


Discussion:
facelessuser: Two tests are broken, so we need to figure out why and either fix the underlying issue, or fix the test if it was wrong.
facelessuser: Ugh, we still have some Windows issues.

Patch:
diff --git a/docs/src/markdown/changelog.md b/docs/src/markdown/changelog.md
index e990e91..9fa3a10 100644
--- a/docs/src/markdown/changelog.md
+++ b/docs/src/markdown/changelog.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 2.0.4
+
+- **FIX**: Fix issue where negated patterns would trigger before end of path.
+- **FIX**: Fix `GLOBSTAR` regular expression pattern issues.
+
 ## 2.0.3
 
 - **FIX**: In `glob`, properly handle files in the current working directory when give a literal pattern that matches it.
diff --git a/wcmatch/_wcparse.py b/wcmatch/_wcparse.py
index e188415..827c216 100644
--- a/wcmatch/_wcparse.py
+++ b/wcmatch/_wcparse.py
@@ -107,6 +107,7 @@ _EOP = r'$'
 _GLOBSTAR_DIV = r'(?:^|$|%s)+'
 # Lookahead to see there is one character.
 _NEED_CHAR = r'(?=.)'
+_NEED_SEP = r'(?=%s)'
 # Group that matches one or none
 _QMARK_GROUP = r'(?:%s)?'
 # Group that matches Zero or more
@@ -914,7 +915,7 @@ class WcParse(object):
                     current[-1] = value
                 else:
                     # Replace the last path separator
-                    current[-1] = _NEED_CHAR
+                    current[-1] = _NEED_SEP % self.get_path_sep()
                     current.append(value)
                 self.consume_path_sep(i)
                 current.append(sep)
@@ -1095,7 +1096,12 @@ class WcParse(object):
             m = RE_WIN_PATH.match(pattern)
             if m:
                 drive = m.group(0).replace('\\\\', '\\')
+                if drive.endswith('\\'):
+                    slash = True
+                drive = drive[:-1]
                 current.append(re.escape(drive))
+                if slash:
+                    current.append(self.get_path_sep() + _ONE_OR_MORE)
                 i.advance(m.end(0))
                 self.consume_path_sep(i)
 
@@ -1161,9 +1167,9 @@ class WcParse(object):
 
         case_flag = 'i' if not self.case_sensitive else ''
         if util.PY36:
-            pattern = (r'^(?!(?s%s:%s)).*?$' if negative else r'^(?s%s:%s)$') % (case_flag, ''.join(result))
+            pattern = (r'^(?!(?s%s:%s)$).*?$' if negative else r'^(?s%s:%s)$') % (case_flag, ''.join(result))
         else:
-            pattern = (r'(?s%s)^(?!(?:%s)).*?$' if negative else r'(?s%s)^(?:%s)$') % (case_flag, ''.join(result))
+            pattern = (r'(?s%s)^(?!(?:%s)$).*?$' if negative else r'(?s%s)^(?:%s)$') % (case_flag, ''.join(result))
 
         if self.is_bytes:
             pattern = pattern.encode('latin-1')
diff --git a/wcmatch/file_hidden.py b/wcmatch/file_hidden.py
index d89200b..087c304 100644
--- a/wcmatch/file_hidden.py
+++ b/wcmatch/file_hidden.py
@@ -87,7 +87,7 @@ if util.platform() == "osx" and _OSX_FOUNDATION_METHOD == _OSX_FOUNDATION_NOT_LO
         cf.CFBooleanGetValue.restype = ctypes.c_int
 
         # This one is a static `CFStringRef`.
-        kCFURLIsHiddenKey = ctypes.c_void_p.in_dll(cf, 'kCFURLIsHiddenKey')
+        kCFURLIsHiddenKey = ctypes.c_void_p.in_dll(cf, 'kCFURLIsHiddenKey')  # noqa: N816
 
         @contextlib.contextmanager
         def cfreleasing(objects):


Test patch:
diff --git a/tests/test_fnmatch.py b/tests/test_fnmatch.py
index 0737a70..5fa09a5 100644
--- a/tests/test_fnmatch.py
+++ b/tests/test_fnmatch.py
@@ -233,7 +233,11 @@ class TestFnMatchFilter:
             0
         ],
         [r'te\st[ma]', ['testm', 'test\\3', 'testa'], ['testm', 'testa'], fnmatch.I],
-        [r'te\st[ma]', ['testm', 'test\\3', 'testa'], ['testm', 'testa'], fnmatch.F]
+        [r'te\st[ma]', ['testm', 'test\\3', 'testa'], ['testm', 'testa'], fnmatch.F],
+
+        # 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]
     ]
 
     @classmethod
@@ -297,10 +301,10 @@ class TestFnMatchTranslate(unittest.TestCase):
         p1, p2 = self.split_translate('*test[a-z]?|*test2[a-z]?|!test[!a-z]|!test[!-|a-z]', flags | fnmatch.N)
         if util.PY36:
             self.assertEqual(p1, [r'^(?s:(?=.).*?test[a-z].)$', r'^(?s:(?=.).*?test2[a-z].)$'])
-            self.assertEqual(p2, [r'^(?!(?s:test[^a-z])).*?$', r'^(?!(?s:test[^\-\|a-z])).*?$'])
+            self.assertEqual(p2, [r'^(?!(?s:test[^a-z])$).*?$', r'^(?!(?s:test[^\-\|a-z])$).*?$'])
         else:
             self.assertEqual(p1, [r'(?s)^(?:(?=.).*?test[a-z].)$', r'(?s)^(?:(?=.).*?test2[a-z].)$'])
-            self.assertEqual(p2, [r'(?s)^(?!(?:test[^a-z])).*?$', r'(?s)^(?!(?:test[^\-\|a-z])).*?$'])
+            self.assertEqual(p2, [r'(?s)^(?!(?:test[^a-z])$).*?$', r'(?s)^(?!(?:test[^\-\|a-z])$).*?$'])
 
         p1, p2 = self.split_translate('test[]][!][][]', flags | fnmatch.F)
         if util.PY36:
@@ -332,10 +336,10 @@ class TestFnMatchTranslate(unittest.TestCase):
         p1, p2 = self.split_translate('-|-test|-', flags=flags | fnmatch.N | fnmatch.M)
         if util.PY36:
             self.assertEqual(p1, [])
-            self.assertEqual(p2, [r'^(?!(?s:)).*?$', r'^(?!(?s:test)).*?$', r'^(?!(?s:)).*?$'])
+            self.assertEqual(p2, [r'^(?!(?s:)$).*?$', r'^(?!(?s:test)$).*?$', r'^(?!(?s:)$).*?$'])
         else:
             self.assertEqual(p1, [])
-            self.assertEqual(p2, [r'(?s)^(?!(?:)).*?$', r'(?s)^(?!(?:test)).*?$', r'(?s)^(?!(?:)).*?$'])
+            self.assertEqual(p2, [r'(?s)^(?!(?:)$).*?$', r'(?s)^(?!(?:test)$).*?$', r'(?s)^(?!(?:)$).*?$'])
 
         p1, p2 = self.split_translate('test[^chars]', flags)
         if util.PY36:
diff --git a/tests/test_glob.py b/tests/test_glob.py
index 3baed7f..e353738 100644
--- a/tests/test_glob.py
+++ b/tests/test_glob.py
@@ -366,12 +366,12 @@ class Testglob(_TestGlob):
 
         # Glob inverse
         [
-            ('a*',),
+            ('a*', '**'),
             [
                 ('EF',), ('ZZZ',), ('',)
             ] if not can_symlink() else [
                 ('EF',), ('ZZZ',), ('',), ('sym1',), ('sym3',), ('sym2',),
-                ('sym3', 'efg'), ('sym3', 'efg', 'ha'), ('sym3', 'EF')
+                ('a',), ('aaa', ), ('aab', ), ('sym3', 'efg'), ('sym3', 'efg', 'ha'), ('sym3', 'EF')
             ],
             glob.N
         ],
@@ -714,12 +714,13 @@ class Testglob(_TestGlob):
         ],
         Options(just_negative=True),
         [
-            ('a*',),
+            ('a*', '**'),
             [
                 ('EF',), ('ZZZ',)
             ] if not can_symlink() else [
                 ('EF',), ('ZZZ',),
-                ('sym1',), ('sym3',), ('sym2',), ('sym3', 'efg'), ('sym3', 'efg', 'ha'), ('sym3', 'EF')
+                ('a',), ('aaa', ), ('aab', ), ('sym1',), ('sym3',), ('sym2',), ('sym3', 'efg'), ('sym3', 'efg', 'ha'),
+                ('sym3', 'EF')
             ],
             glob.N
         ],
diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py
index 2aa8761..87c9a0b 100644
--- a/tests/test_globmatch.py
+++ b/tests/test_globmatch.py
@@ -394,7 +394,14 @@ class TestGlobFilter:
         ['@(test/test)', [], glob.F],
         [r'@(test\/test)', [], glob.F],
         ['test[/]test', [], glob.F],
-        [r'test[\/]test', [], glob.F]
+        [r'test[\/]test', [], glob.F],
+
+        # Issue #24
+        GlobFiles(
+            ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"]
+        ),
+        ['*.bar', ["foo.bar", "foo.cfg.bar"]],
+        ['!*.bar', ["goo.cfg", "foo.bar.cfg"]]
     ]
 
     @classmethod