diff --git a/src/rpft/cli.py b/src/rpft/cli.py index 66fffef..d389afa 100644 --- a/src/rpft/cli.py +++ b/src/rpft/cli.py @@ -72,8 +72,11 @@ def create_parser(): help=( "Tags to filter the content index sheet. A sequence of lists, with each " "list starting with an integer (tag position) followed by tags to include " - "for this position. Example: 1 foo bar 2 baz means: only include rows if " - "tags:1 is empty, foo or bar, and tags:2 is empty or baz" + "or exclude for this position. To exclude, precede by '!'.\n" + "Example: 1 foo bar 2 baz means: only include rows if " + "tags:1 is empty, foo or bar, and tags:2 is empty or baz\n" + "Example: 1 foo bar ! 2 !baz means: only include rows if " + "tags:1 is foo or bar (but not empty), and tags:2 is not baz" ), ) return parser diff --git a/src/rpft/parsers/creation/tagmatcher.py b/src/rpft/parsers/creation/tagmatcher.py index 7e4d8bd..6110ffa 100644 --- a/src/rpft/parsers/creation/tagmatcher.py +++ b/src/rpft/parsers/creation/tagmatcher.py @@ -3,7 +3,8 @@ class TagMatcher: def __init__(self, params=[]): - self.tag_patterns = collections.defaultdict(list) + self.include_patterns = collections.defaultdict(list) + self.exclude_patterns = collections.defaultdict(list) if not params: return current_index = None @@ -20,11 +21,18 @@ def __init__(self, params=[]): "Tags parameter must start with a " "number indicating the tag position." ) - self.tag_patterns[current_index].append(param) + if param[0] == "!": + self.exclude_patterns[current_index].append(param[1:]) + else: + self.include_patterns[current_index].append(param) + # Empty string is accepted by default + self.include_patterns[current_index].append("") def matches(self, tags): matches = True for i, tag in enumerate(tags): - if tag and i in self.tag_patterns and tag not in self.tag_patterns[i]: + if i in self.include_patterns and tag not in self.include_patterns[i]: + matches = False + if i in self.exclude_patterns and tag in self.exclude_patterns[i]: matches = False return matches diff --git a/tests/test_tagmatcher.py b/tests/test_tagmatcher.py index 4e09ef3..04f2378 100644 --- a/tests/test_tagmatcher.py +++ b/tests/test_tagmatcher.py @@ -38,3 +38,33 @@ def test_two_tags(self): self.assertFalse(tm.matches(["something"])) self.assertFalse(tm.matches(["something", "foo"])) self.assertFalse(tm.matches(["something", "baz"])) + + def test_exclude_tag(self): + tm = TagMatcher(["1", "!foo"]) + self.assertTrue(tm.matches([])) + self.assertTrue(tm.matches([""])) + self.assertTrue(tm.matches(["bar"])) + self.assertFalse(tm.matches(["foo"])) + + def test_excludetwo_tag(self): + tm = TagMatcher(["1", "!foo", "!bar"]) + self.assertTrue(tm.matches([])) + self.assertTrue(tm.matches([""])) + self.assertTrue(tm.matches(["baz"])) + self.assertFalse(tm.matches(["bar"])) + self.assertFalse(tm.matches(["foo"])) + + def test_excludeemptyonly_tag(self): + tm = TagMatcher(["1", "!"]) + self.assertTrue(tm.matches([])) + self.assertTrue(tm.matches(["bar"])) + self.assertTrue(tm.matches(["foo"])) + self.assertFalse(tm.matches([""])) + + def test_excludeempty_tag(self): + tm = TagMatcher(["1", "!", "foo", "bar"]) + self.assertTrue(tm.matches([])) + self.assertTrue(tm.matches(["bar"])) + self.assertTrue(tm.matches(["foo"])) + self.assertFalse(tm.matches(["baz"])) + self.assertFalse(tm.matches([""]))