view cutils/util/fnmatch.py @ 304:dc1f08937621

FIX: fnmatch: handle None fnmatch filter definitions
author Franz Glasner <fzglas.hg@dom66.de>
date Wed, 05 Mar 2025 10:24:51 +0100
parents bf88323d6bf7
children 6c212e407524
line wrap: on
line source

# -*- coding: utf-8 -*-
# :-
# :Copyright: (c) 2020-2025 Franz Glasner
# :License:   BSD-3-Clause
# :-
r"""File name matching.

"""

from __future__ import print_function, absolute_import


__all__ = ["FnMatcher"]


import re

from . import glob


HELP_DESCRIPTION = """\
PATTERNs
========

  glob: case-sensitive, anchored at the begin and end
  iglob: case-insensitive variant of "glob"
  re: regular expression
  path: plain text example (rooted), can be a file or a directory or a prefix
        thereof
  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 = []
        if filter_definitions:
            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)