changeset 300:1fc117f5f9a1

treesum: Implement --include/--exclude commandline parsing for file name inclusion and exclusion. BUGS: - Real filename matching not yet implemented - Pattern description only very rudimentary
author Franz Glasner <fzglas.hg@dom66.de>
date Tue, 04 Mar 2025 16:30:10 +0100
parents bcbc68d8aa12
children d246b631b85a
files cutils/treesum.py cutils/util/fnmatch.py
diffstat 2 files changed, 92 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/cutils/treesum.py	Tue Mar 04 11:26:22 2025 +0100
+++ b/cutils/treesum.py	Tue Mar 04 16:30:10 2025 +0100
@@ -30,6 +30,7 @@
 from . import util
 from .util import cm
 from .util import digest
+from .util import fnmatch
 from .util import walk
 from .util.crc32 import crc32
 
@@ -65,6 +66,11 @@
             help="Put given comment COMMENT into the output as \"COMMENT\". "
                  "Can be given more than once.")
         gp.add_argument(
+            "--exclude", "-X", action=PatternMatchAction, kind="exclude",
+            dest="fnmatch_filters", metavar="PATTERN",
+            help="""Exclude names matching the given PATTERN.
+For help on PATTERN use \"help patterns\".""")
+        gp.add_argument(
             "--follow-directory-symlinks", "-l", action=SymlinkAction,
             const="follow-directory-symlinks",
             default=FollowSymlinkConfig(False, False, True),
@@ -99,6 +105,11 @@
 (--physical, --logical, -p, --no-follow-directory-symlinks,
 --no-follow-file-symlinks, et al.).""")
         gp.add_argument(
+            "--include", "-I", action=PatternMatchAction, kind="include",
+            dest="fnmatch_filters", metavar="PATTERN",
+            help="""Include names matching the given PATTERN.
+For help on PATTERN use \"help patterns\".""")
+        gp.add_argument(
             "--logical", "-L", action=SymlinkAction, dest="follow_symlinks",
             const=FollowSymlinkConfig(True, True, True),
             help="""Follow symbolic links everywhere: on command line
@@ -248,6 +259,12 @@
         description="Show this help message or a subcommand's help and exit.")
     hparser.add_argument("help_command", nargs='?', metavar="COMMAND")
 
+    patparser = subparsers.add_parser(
+        "patterns",
+        help="Show the help for PATTERNs and exit",
+        description=fnmatch.HELP_DESCRIPTION,
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+
     vparser = subparsers.add_parser(
         "version",
         help="Show the program's version number and exit",
@@ -274,6 +291,8 @@
                 vparser.print_help()
             elif opts.help_command == "help":
                 hparser.print_help()
+            elif opts.help_command == "patterns":
+                patparser.print_help()
             else:
                 parser.print_help()
         return 0
@@ -289,7 +308,7 @@
     )
     logging.captureWarnings(True)
 
-    return treesum(opts)
+    return treesum(opts, patparser=patparser)
 
 
 FollowSymlinkConfig = collections.namedtuple("FollowSymlinkConfig",
@@ -356,11 +375,39 @@
         setattr(namespace, self.dest, curval)
 
 
+class PatternMatchAction(argparse.Action):
+
+    def __init__(self, *args, **kwargs):
+        if "nargs" in kwargs:
+            raise argparse.ArgumentError(None, "`nargs' not allowed")
+        if "type" in kwargs:
+            raise argparse.ArgumentError(None, "`type' not allowed")
+        kwargs["nargs"] = 1
+
+        self.__kind = kwargs.pop("kind", None)
+        if self.__kind is None:
+            raise argparse.ArgumentError(None, "`kind' is required")
+        if self.__kind not in ("exclude", "include"):
+            raise argparse.ArgumentError(
+                None, "`kind' must be one of `include' or `exclude'")
+
+        super(PatternMatchAction, self).__init__(*args, **kwargs)
+
+    def __call__(self, parser, namespace, values, option_string=None):
+        items = getattr(namespace, self.dest, None)
+        if items is None:
+            items = []
+            setattr(namespace, self.dest, items)
+        for v in values:
+            items.append((self.__kind, v))
+
+
 def gen_generate_opts(directories=[],
                       algorithm=util.default_algotag(),
                       append_output=False,
                       base64=False,
                       comment=[],
+                      fnmatch_filters=[],
                       follow_symlinks=FollowSymlinkConfig(False, False, False),
                       full_mode=False,
                       generator="normal",
@@ -375,6 +422,19 @@
                       utf8=False):
     if not isinstance(follow_symlinks, FollowSymlinkConfig):
         raise TypeError("`follow_symlinks' must be a FollowSymlinkConfig")
+    if not isinstance(fnmatch_filters, (list, tuple, type(None))):
+        raise TypeError("`fnmatch_filters' must be a sequence (list, tuple)")
+    if fnmatch_filters:
+        for f in fnmatch_filters:
+            if not isinstance(f, (tuple, list)):
+                raise TypeError(
+                    "items in `fnmatch_filters' must be tuples or lists")
+            if f[0] not in ("exclude", "include"):
+                raise ValueError(
+                    "every kind of every item in `fnmatch_filters' must be"
+                    " \"include\" or \"exclude\""
+                )
+
     # Not following symlinks to files is not yet supported: reset to True
 #    if not follow_symlinks.file:
 #        follow_symlinks = follow_symlinks._make([follow_symlinks.command_line,
@@ -387,6 +447,7 @@
         append_output=append_output,
         base64=base64,
         comment=comment,
+        fnmatch_filters=fnmatch_filters,
         follow_symlinks=follow_symlinks,
         generator=generator,
         logical=logical,
@@ -409,12 +470,14 @@
     return opts
 
 
-def treesum(opts):
+def treesum(opts, patparser=None):
     # XXX TBD: opts.check and opts.checklist (as in shasum.py)
     if opts.subcommand in ("generate", "gen"):
         return generate_treesum(opts)
     elif opts.subcommand == "info":
         return print_treesum_digestfile_infos(opts)
+    elif opts.subcommand == "patterns":
+        patparser.print_help()
     else:
         raise RuntimeError(
             "command `{}' not yet handled".format(opts.subcommand))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cutils/util/fnmatch.py	Tue Mar 04 16:30:10 2025 +0100
@@ -0,0 +1,27 @@
+# -*- 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__ = []
+
+
+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
+  filepath: exactly a single file, relative to the root of the tree
+
+"""