From 4a1ff6041f652b464a012f66c53b287c54a64627 Mon Sep 17 00:00:00 2001 From: Name Date: Sun, 28 Jul 2024 22:21:10 -0700 Subject: [PATCH 1/3] Add invert-find operation mode Add new operation mode "invert-find", which finding files without any of the specified tags. This feature is implemented by creating a metadata query for files not containing any of the given tags and emitting the results accordingly. --- Tag/Tag.h | 1 + Tag/Tag.m | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/Tag/Tag.h b/Tag/Tag.h index 05227c9..f6e9d37 100644 --- a/Tag/Tag.h +++ b/Tag/Tag.h @@ -36,6 +36,7 @@ typedef NS_ENUM(int, OperationMode) { OperationModeRemove = 'r', OperationModeMatch = 'm', OperationModeFind = 'f', + OperationModeInvertFind = 'F', OperationModeUsage = 'u', OperationModeList = 'l', }; diff --git a/Tag/Tag.m b/Tag/Tag.m index 686e630..55f09b7 100644 --- a/Tag/Tag.m +++ b/Tag/Tag.m @@ -98,6 +98,9 @@ - (OutputFlags)outputFlagsForMode:(OperationMode)mode case OperationModeFind: result = OutputFlagsName; break; + case OperationModeInvertFind: + result = OutputFlagsName; + break; case OperationModeList: result = OutputFlagsName | OutputFlagsTags; break; @@ -124,6 +127,7 @@ - (void)parseCommandLineArgv:(char * const *)argv argc:(int)argc { "remove", required_argument, 0, OperationModeRemove }, { "match", required_argument, 0, OperationModeMatch }, { "find", required_argument, 0, OperationModeFind }, + { "invert-find",required_argument, 0, OperationModeInvertFind }, { "usage", optional_argument, 0, OperationModeUsage }, { "list", no_argument, 0, OperationModeList }, @@ -182,7 +186,7 @@ - (void)parseCommandLineArgv:(char * const *)argv argc:(int)argc // Parse Options int option_char; int option_index; - while ((option_char = getopt_long(argc, argv, "s:a:r:m:f:u::lAeRdnNtTgGcp0hv", options, &option_index)) != -1) + while ((option_char = getopt_long(argc, argv, "s:a:r:m:f:F:u::lAeRdnNtTgGcp0hv", options, &option_index)) != -1) { switch (option_char) { @@ -191,6 +195,7 @@ - (void)parseCommandLineArgv:(char * const *)argv argc:(int)argc case OperationModeRemove: case OperationModeMatch: case OperationModeFind: + case OperationModeInvertFind: case OperationModeUsage: case OperationModeList: { @@ -503,6 +508,10 @@ - (void)performOperation [self doFind]; break; + case OperationModeInvertFind: + [self doInvertFind]; + break; + case OperationModeUsage: [self doUsage]; break; @@ -855,6 +864,10 @@ - (void)doFind [self findGutsWithUsage:NO]; } +- (void)doInvertFind +{ + [self findFilesWithoutTagWithUsage:NO]; +} - (void)doUsage { @@ -898,6 +911,53 @@ - (void)findGutsWithUsage:(BOOL)usageMode } +- (void)findFilesWithoutTagWithUsage:(BOOL)usageMode +{ + // Start a metadata search for files not containing any of the given tags + NSMetadataQuery* metadataQuery = [self performMetadataSearchForTags:nil usageMode:usageMode]; + + // Emit the results of the query, either for tags or for usage + if (usageMode) + { + // Print the statistics, ignoring the general query results + NSDictionary* valueLists = [metadataQuery valueLists]; + NSArray* tagTuples = valueLists[kMDItemUserTags]; + for (NSMetadataQueryAttributeValueTuple* tuple in tagTuples) + { + NSString* tag = (tuple.value == [NSNull null]) ? @"" : tuple.value; + Printf(@"%ld\t%@\n", (long)tuple.count, [self displayStringForTag:tag]); + } + } + else + { + // Print the query results + [metadataQuery enumerateResultsUsingBlock:^(NSMetadataItem* theResult, NSUInteger idx, BOOL * _Nonnull stop) { + @autoreleasepool { + NSString* path = [theResult valueForAttribute:(NSString *)kMDItemPath]; + if (path) + { + NSURL* URL = [NSURL fileURLWithPath:path]; + NSArray* tagArray = [theResult valueForAttribute:kMDItemUserTags]; + + // Check if the file does not contain any of the tags + BOOL containsTag = NO; + for (NSString* tag in self.tags) { + if ([tagArray containsObject:tag]) { + containsTag = YES; + break; + } + } + + if (!containsTag) { + [self emitURL:URL tags:nil]; + } + } + } + }]; + } +} + + - (NSPredicate*)formQueryPredicateForTags:(NSSet*)tagSet { BOOL matchAny = [self wildcardInTagSet:tagSet]; From b52950c7497d46b5f4d0fe13531c0fdc2e9ec663 Mon Sep 17 00:00:00 2001 From: Name Date: Sun, 28 Jul 2024 22:09:11 -0700 Subject: [PATCH 2/3] Document invert-find operation mode Incorporate invert-find operation help to man page, README --- README.md | 23 ++++++++++++++--------- Tag/Tag.m | 15 ++++++++------- Tag/tag.1 | 3 +++ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 90707c1..3564abb 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,14 @@ Usage tag - A tool for manipulating and querying file tags. usage: - tag -a | --add ... Add tags to file - tag -r | --remove ... Remove tags from file - tag -s | --set ... Set tags on file - tag -m | --match ... Display files with matching tags - tag -f | --find ... Find all files with tags, limited to paths if present - tag -l | --list ... List the tags on file - tag -u | --usage ... Display tags used, with usage counts + tag -a | --add ... Add tags to file + tag -r | --remove ... Remove tags from file + tag -s | --set ... Set tags on file + tag -m | --match ... Display files with matching tags + tag -f | --find ... Find all files with tags, limited to paths if present + tag -F | --invert-find ... Find all files with tags, limited to paths if present + tag -l | --list ... List the tags on file + tag -u | --usage ... Display tags used, with usage counts is a comma-separated list of tag names; use * to match/find any tag. additional options: -v | --version Display version @@ -154,7 +155,11 @@ You may also supply one or more paths in which to search. tag --find tagname /path/to/here tag --find tagname --home /path/to/here ./there - + +### Find all files on the filesystem without specified tags + +The *invert-find* operation searches across your filesystem for all files that *do not* contain the specified tags. This uses the same filesystem metadata database that Spotlight uses, so it is fast. + ### Display tag usage The *usage* operation follows the syntax and operation of *find*, but instead of displaying the files found, it lists the tags found, prefixed with the usage count of each. In contrast to *find*, the tag operand to *usage* is optional: it defaults to '*'. @@ -223,7 +228,7 @@ Advanced Usage * Wherever a "file" is expected, a list of files may be used instead. These are provided as separate parameters. * Note that directories can be tagged as well, so directories may be specified instead of files. * The --all, --enter, and --recursive options apply to --add, --remove, --set, --match, and --list, and control whether hidden files are processed and whether directories are entered and/or processed recursively. If a directory is supplied, but neither of --enter or --recursive, then the operation will apply to the directory itself, rather than to its contents. -* The operation selector --add, --remove, --set, --match, --list, --find, or --usage may be abbreviated as -a, -r, -s, -m, -l, -f, or -u respectively. All of the options have a short version, in fact. See see the synopsis above, or output from help. +* The operation selector --add, --remove, --set, --match, --list, --find, --invert-find, or --usage may be abbreviated as -a, -r, -s, -m, -l, -f, -F, or -u respectively. All of the options have a short version, in fact. See see the synopsis above, or output from help. * If no operation selector is given, the operation will default to *list*. * A *list* operation will default to the current directory if no directory is given. * For compatibility with Finder, tags are compared in a case-insensitive manner. diff --git a/Tag/Tag.m b/Tag/Tag.m index 55f09b7..5f43efb 100644 --- a/Tag/Tag.m +++ b/Tag/Tag.m @@ -371,13 +371,14 @@ - (void)displayHelp { Printf(@"%@ - %@", [self programName], @"A tool for manipulating and querying file tags.\n" " usage:\n" - " tag -a | --add ... Add tags to file\n" - " tag -r | --remove ... Remove tags from file\n" - " tag -s | --set ... Set tags on file\n" - " tag -m | --match ... Display files with matching tags\n" - " tag -f | --find ... Find all files with tags (-A, -e, -R ignored)\n" - " tag -u | --usage ... Display tags used, with usage counts\n" - " tag -l | --list ... List the tags on file\n" + " tag -a | --add ... Add tags to file\n" + " tag -r | --remove ... Remove tags from file\n" + " tag -s | --set ... Set tags on file\n" + " tag -m | --match ... Display files with matching tags\n" + " tag -f | --find ... Find all files with tags (-A, -e, -R ignored)\n" + " tag -F | --invert-find ... Find all files without tags (-A, -e, -R ignored)\n" + " tag -u | --usage ... Display tags used, with usage counts\n" + " tag -l | --list ... List the tags on file\n" " is a comma-separated list of tag names; use * to match/find any tag.\n" " additional options:\n" " -v | --version Display version\n" diff --git a/Tag/tag.1 b/Tag/tag.1 index 7239566..902d698 100644 --- a/Tag/tag.1 +++ b/Tag/tag.1 @@ -23,6 +23,9 @@ Display files with matching tags .BR \-f ", " \-\-find\ \fItags\ \fIpath\fR Find all files with tags (-A, -e, -R ignored) .TP +.BR \-F ", " \-\-invert-find\ \fItags\ \fIpath\fR +Find all files without tags (-A, -e, -R ignored) +.TP .BR \-u ", " \-\-usage\ \fItags\ \fIpath\fR Display tag usage .TP From 30b3d9f9d0cc5334fc6a59df2859b4dc239d0f59 Mon Sep 17 00:00:00 2001 From: Name Date: Sun, 28 Jul 2024 21:53:25 -0700 Subject: [PATCH 3/3] Update man page date --- Tag/tag.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tag/tag.1 b/Tag/tag.1 index 902d698..6094368 100644 --- a/Tag/tag.1 +++ b/Tag/tag.1 @@ -1,4 +1,4 @@ -.TH "TAG" "1" "Jul 2019" "Tag" "tag" +.TH "TAG" "28" "Jul 2024" "Tag" "tag" . .SH "NAME" \fBtag\fR \- A tool for manipulating and querying file tags.