diff cutils/util/fnmatch.py @ 302:bf88323d6bf7

treesum: Implement --exclude/--include. - Filtering - Document in output - Handle in the "info" command
author Franz Glasner <fzglas.hg@dom66.de>
date Wed, 05 Mar 2025 10:07:44 +0100
parents 1fc117f5f9a1
children dc1f08937621
line wrap: on
line diff
--- a/cutils/util/fnmatch.py	Wed Mar 05 10:06:38 2025 +0100
+++ b/cutils/util/fnmatch.py	Wed Mar 05 10:07:44 2025 +0100
@@ -10,7 +10,12 @@
 from __future__ import print_function, absolute_import
 
 
-__all__ = []
+__all__ = ["FnMatcher"]
+
+
+import re
+
+from . import glob
 
 
 HELP_DESCRIPTION = """\
@@ -22,6 +27,109 @@
   re: regular expression
   path: plain text example (rooted), can be a file or a directory or a prefix
         thereof
-  filepath: exactly a single file, relative to the root of the tree
+  fullpath: exactly a single full path (file or directory), relative to the
+            root of the tree
 
 """
+
+
+def glob_factory(pattern):
+
+    cpat = re.compile(
+        # automatically anchored
+        "\\A{}\\Z".format(glob.glob_to_regexp(pattern)),
+        re.DOTALL)
+
+    def _glob_matcher(s):
+        return cpat.search(s) is not None
+
+    return _glob_matcher
+
+
+def iglob_factory(pattern):
+
+    cpat = re.compile(
+        # automatically anchored
+        "\\A{}\\Z".format(glob.glob_to_regexp(pattern)),
+        re.DOTALL | re.IGNORECASE)
+
+    def _iglob_matcher(s):
+        return cpat.search(s) is not None
+
+    return _iglob_matcher
+
+
+def re_factory(pattern):
+
+    cpat = re.compile(pattern, re.DOTALL)
+
+    def _re_matcher(s):
+        return cpat.search(s) is not None
+
+    return _re_matcher
+
+
+def path_factory(pattern):
+
+    def _path_matcher(s):
+        return s.startswith(pattern)
+
+    return _path_matcher
+
+
+def fullpath_factory(pattern):
+
+    def _fullpath_matcher(s):
+        return s == pattern
+
+    return _fullpath_matcher
+
+
+class FnMatcher(object):
+
+    _registry = {
+        "glob": glob_factory,
+        "iglob": iglob_factory,
+        "re": re_factory,
+        "path": path_factory,
+        "fullpath": fullpath_factory,
+    }
+
+    VISIT_DEFAULT = True    # Overall default value for visiting
+
+    def __init__(self, matchers):
+        super(FnMatcher, self).__init__()
+        self._matchers = matchers
+
+    @classmethod
+    def build_from_commandline_patterns(klass, filter_definitions):
+        matchers = []
+        for action, kpattern in filter_definitions:
+            kind, sep, pattern = kpattern.partition(':')
+            if not sep:
+                # use the default
+                kind = "glob"
+                pattern = kpattern
+            factory = klass._registry.get(kind, None)
+            if not factory:
+                raise RuntimeError("unknown pattern kind: {}".format(kind))
+            matchers.append((action, kind, factory(pattern), pattern))
+
+        return klass(matchers)
+
+    def shall_visit(self, fn):
+        visit = self.VISIT_DEFAULT
+        for action, kind, matcher, orig_pattern in self._matchers:
+            res = matcher(fn)
+            if res:
+                if action == "include":
+                    visit = True
+                elif action == "exclude":
+                    visit = False
+                else:
+                    raise RuntimeError("unknown action: {}".format(action))
+        return visit
+
+    def definitions(self):
+        for action, kind, matcher, orig_pattern in self._matchers:
+            yield (action, kind, orig_pattern)