From cb462fc3129f880534dbeafedeb34c8b377b403e Mon Sep 17 00:00:00 2001 From: Jonathan Neidel Date: Fri, 20 Feb 2026 14:11:53 +0100 Subject: [PATCH 1/2] Fix completions for _ and : as separators --- filetags/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/filetags/__init__.py b/filetags/__init__.py index ceec5b9..52d119f 100755 --- a/filetags/__init__.py +++ b/filetags/__init__.py @@ -335,8 +335,12 @@ class SimpleCompleter(object): def __init__(self, options): self.options = sorted(options) - # removing '-' as a delimiter character in order to be able to use '-tagname' for removing: - readline.set_completer_delims(readline.get_completer_delims().replace('-', '')) + # remove delimiter characters to improve completion + # remove '-' in order to be able to complete '-tagname' for removing + # remove '_' & ':' to be able to complete 'key_' into 'key_value{1,2,3}' + readline.set_completer_delims( + readline.get_completer_delims().replace('-', '').replace('_', '').replace(':', '') + ) return From 1c84f2cd4b781137c358d3d38c77768f70f60f29 Mon Sep 17 00:00:00 2001 From: Jonathan Neidel Date: Fri, 27 Feb 2026 23:24:59 +0100 Subject: [PATCH 2/2] Add validation for problematic characters --- README.org | 2 ++ filetags/__init__.py | 75 +++++++++++++++++++++++++++++++++++++++----- tests/unit_tests.py | 5 ++- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/README.org b/README.org index 1cb1a85..5f5cdf2 100644 --- a/README.org +++ b/README.org @@ -194,6 +194,8 @@ options: List all file-tags which are found in file names but are not part of .filetags --tag-gardening This is for getting an overview on tags that might require to be renamed (typos, singular/plural, ...). See also http://www.webology.org/2008/v5n3/a58.html --force-cv Only allow tags that are part of the controlled vocabulary (.filetags) + --allow-problematic-characters + Allow problematic characters (":") in user-entered tags -v, --verbose Enable verbose mode -q, --quiet Enable quiet mode --version Display version and exit diff --git a/filetags/__init__.py b/filetags/__init__.py index 52d119f..42cda71 100755 --- a/filetags/__init__.py +++ b/filetags/__init__.py @@ -73,6 +73,7 @@ def safe_import(library): # unused: INVOCATION_TIME = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()) FILENAME_TAG_SEPARATOR = ' -- ' BETWEEN_TAG_SEPARATOR = ' ' +PROBLEMATIC_TAG_CHARACTERS = [':'] CONTROLLED_VOCABULARY_FILENAME = ".filetags" HINT_FOR_BEING_IN_VOCABULARY_TEMPLATE = ' *' TAGFILTER_DIRECTORY = os.path.join(os.path.expanduser("~"), ".filetags_tagfilter") @@ -291,6 +292,11 @@ def safe_import(library): dest="force_cv", action="store_true", help="Only allow tags that are part of the controlled vocabulary (.filetags)") +parser.add_argument("--allow-problematic-characters", + dest="allow_problematic_characters", action="store_true", + help="Allow problematic characters (" + ", ".join('"' + char + '"' for char in PROBLEMATIC_TAG_CHARACTERS) + + ") in user-entered tags") + parser.add_argument("-v", "--verbose", dest="verbose", action="store_true", help="Enable verbose mode") @@ -418,6 +424,7 @@ def __init__(self, root, vocabulary, upto9_tags_for_shortcuts, tags_for_visual, self.entered_tags = "" self.cancelled = False self.force_cv_enabled = options.force_cv and not options.remove and not options.tagfilter and not options.tagtrees + self.allow_problematic_characters = options.allow_problematic_characters low_contrast_fg_color = self.get_soft_foreground(root, 0.6) ## better than hard-coded gray values that interfere with default color schema # Label for instructions @@ -626,13 +633,24 @@ def submit_tags(self): self.entered_tags = self.entry.get().strip() self.cancelled = False + tags_for_validation = extract_tags_from_argument(self.entered_tags) - invalid_tags = force_cv_validator(self.force_cv_enabled, tags_for_validation, self.vocabulary) - if invalid_tags: - error_msg = "Invalid tags: " + BETWEEN_TAG_SEPARATOR.join(invalid_tags) - similar_msg = build_similar_to_invalid_tags_message(invalid_tags, self.vocabulary) + cv_invalid_tags = force_cv_validator(self.force_cv_enabled, tags_for_validation, self.vocabulary) + character_invalid_tags = problematic_characters_validator(self.allow_problematic_characters, tags_for_validation) + if cv_invalid_tags or character_invalid_tags: + combined_invalid_tags = [] + if cv_invalid_tags: + combined_invalid_tags.extend(cv_invalid_tags) + if character_invalid_tags: + combined_invalid_tags.extend(character_invalid_tags) + combined_invalid_tags = list(dict.fromkeys(combined_invalid_tags)) + + error_msg = "Invalid tags: " + BETWEEN_TAG_SEPARATOR.join(combined_invalid_tags) + similar_msg = build_similar_to_invalid_tags_message(combined_invalid_tags, self.vocabulary) if similar_msg: error_msg += "\n" + similar_msg + if character_invalid_tags: + error_msg += "\n" + build_problematic_characters_message() self.error_label.config(text=error_msg) return @@ -1673,6 +1691,30 @@ def force_cv_validator(force_cv_enabled, tags_for_validation, vocabulary): return invalid_tags return None +def problematic_characters_validator(allow_problematic_characters, tags_for_validation): + if allow_problematic_characters or not tags_for_validation: + return None + + invalid_tags = [] + for tag in tags_for_validation: + if tag.startswith('-'): + continue + for char in PROBLEMATIC_TAG_CHARACTERS: + if char in tag: + invalid_tags.append(tag) + break + + if invalid_tags: + return list(dict.fromkeys(invalid_tags)) + return None + +def build_problematic_characters_message(): + if len(PROBLEMATIC_TAG_CHARACTERS) == 1: + characters = '"' + PROBLEMATIC_TAG_CHARACTERS[0] + '"' + return "Character " + characters + " can be problematic and is prohibited." + characters = ", ".join('"' + char + '"' for char in PROBLEMATIC_TAG_CHARACTERS) + return "Characters " + characters + " can be problematic and are prohibited." + def build_similar_to_invalid_tags_message(invalid_tags, vocabulary): suggestions = [] for tag in invalid_tags: @@ -2451,10 +2493,21 @@ def ask_for_tags(vocabulary, controlled_vocabulary, upto9_tags_for_shortcuts, ta sys.stdout.flush() sys.exit(0) - validation_error = force_cv_validator(force_cv_enabled, tags_from_userinput, controlled_vocabulary) - if validation_error: + cv_validation_error = force_cv_validator(force_cv_enabled, tags_from_userinput, controlled_vocabulary) + character_validation_error = problematic_characters_validator(options.allow_problematic_characters, tags_from_userinput) + + if cv_validation_error or character_validation_error: if not gui: - previous_error = validation_error + combined_errors = [] + if cv_validation_error: + combined_errors.extend(cv_validation_error) + if character_validation_error: + combined_errors.extend(character_validation_error) + combined_errors = list(dict.fromkeys(combined_errors)) + + if character_validation_error and combined_errors: + combined_errors[-1] = combined_errors[-1] + ". " + build_problematic_characters_message() + previous_error = combined_errors continue previous_error = None @@ -3217,6 +3270,14 @@ def main(): logging.info(similar_msg) sys.exit(22) + if options.tags: + invalid_tags = problematic_characters_validator(options.allow_problematic_characters, tags_from_userinput) + if invalid_tags: + logging.error(colorama.Fore.RED + "Invalid tags: " + + colorama.Style.RESET_ALL + BETWEEN_TAG_SEPARATOR.join(invalid_tags)) + logging.error(build_problematic_characters_message()) + sys.exit(23) + if options.remove: logging.info("removing tags \"%s\" ..." % str(BETWEEN_TAG_SEPARATOR.join(tags_from_userinput))) elif options.tagfilter: diff --git a/tests/unit_tests.py b/tests/unit_tests.py index 53bf140..b426e02 100755 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -185,7 +185,6 @@ def test_get_upto_nine_keys_of_dict_with_highest_value(self): myresult) def test_get_common_tags_from_files(self): - self.assertEqual(filetags.get_common_tags_from_files(['file1.txt']), []) self.assertEqual(filetags.get_common_tags_from_files(['file1 -- foo.txt']), ['foo']) self.assertSetEqual(set(filetags.get_common_tags_from_files(['file1 -- foo bar.txt'])), set(['foo', 'bar'])) @@ -214,6 +213,10 @@ def test_get_invalid_tags_for_vocabulary(self): self.assertEqual(filetags.get_invalid_tags_for_vocabulary(['-foo', '-qux'], vocabulary), ['-qux']) self.assertEqual(filetags.get_invalid_tags_for_vocabulary(['aa', 'aa', 'bb', 'aa'], ['bb']), ['aa']) + def test_problematic_characters_validator(self): + tags = ['foo', 'bar:baz'] + self.assertEqual(filetags.problematic_characters_validator(False, tags), ['bar:baz']) + def test_build_similar_to_invalid_tags_message(self): self.assertIsNone(filetags.build_similar_to_invalid_tags_message(['xxx'], ['foo', 'bar'])) self.assertEqual(filetags.build_similar_to_invalid_tags_message(['Simpson'], ['Simson', 'simpson']),