Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ Usage

tag - A tool for manipulating and querying file tags.
usage:
tag -a | --add <tags> <path>... Add tags to file
tag -r | --remove <tags> <path>... Remove tags from file
tag -s | --set <tags> <path>... Set tags on file
tag -m | --match <tags> <path>... Display files with matching tags
tag -f | --find <tags> <path>... Find all files with tags, limited to paths if present
tag -l | --list <path>... List the tags on file
tag -u | --usage <tags> <path>... Display tags used, with usage counts
tag -a | --add <tags> <path>... Add tags to file
tag -r | --remove <tags> <path>... Remove tags from file
tag -s | --set <tags> <path>... Set tags on file
tag -m | --match <tags> <path>... Display files with matching tags
tag -f | --find <tags> <path>... Find all files with tags, limited to paths if present
tag -F | --invert-find <tags> <path>... Find all files with tags, limited to paths if present
tag -l | --list <path>... List the tags on file
tag -u | --usage <tags> <path>... Display tags used, with usage counts
<tags> is a comma-separated list of tag names; use * to match/find any tag.
additional options:
-v | --version Display version
Expand Down Expand Up @@ -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 '*'.
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions Tag/Tag.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ typedef NS_ENUM(int, OperationMode) {
OperationModeRemove = 'r',
OperationModeMatch = 'm',
OperationModeFind = 'f',
OperationModeInvertFind = 'F',
OperationModeUsage = 'u',
OperationModeList = 'l',
};
Expand Down
77 changes: 69 additions & 8 deletions Tag/Tag.m
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ - (OutputFlags)outputFlagsForMode:(OperationMode)mode
case OperationModeFind:
result = OutputFlagsName;
break;
case OperationModeInvertFind:
result = OutputFlagsName;
break;
case OperationModeList:
result = OutputFlagsName | OutputFlagsTags;
break;
Expand All @@ -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 },
Expand Down Expand Up @@ -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)
{
Expand All @@ -191,6 +195,7 @@ - (void)parseCommandLineArgv:(char * const *)argv argc:(int)argc
case OperationModeRemove:
case OperationModeMatch:
case OperationModeFind:
case OperationModeInvertFind:
case OperationModeUsage:
case OperationModeList:
{
Expand Down Expand Up @@ -366,13 +371,14 @@ - (void)displayHelp
{
Printf(@"%@ - %@", [self programName], @"A tool for manipulating and querying file tags.\n"
" usage:\n"
" tag -a | --add <tags> <path>... Add tags to file\n"
" tag -r | --remove <tags> <path>... Remove tags from file\n"
" tag -s | --set <tags> <path>... Set tags on file\n"
" tag -m | --match <tags> <path>... Display files with matching tags\n"
" tag -f | --find <tags> <path>... Find all files with tags (-A, -e, -R ignored)\n"
" tag -u | --usage <tags> <path>... Display tags used, with usage counts\n"
" tag -l | --list <path>... List the tags on file\n"
" tag -a | --add <tags> <path>... Add tags to file\n"
" tag -r | --remove <tags> <path>... Remove tags from file\n"
" tag -s | --set <tags> <path>... Set tags on file\n"
" tag -m | --match <tags> <path>... Display files with matching tags\n"
" tag -f | --find <tags> <path>... Find all files with tags (-A, -e, -R ignored)\n"
" tag -F | --invert-find <tags> <path>... Find all files without tags (-A, -e, -R ignored)\n"
" tag -u | --usage <tags> <path>... Display tags used, with usage counts\n"
" tag -l | --list <path>... List the tags on file\n"
" <tags> is a comma-separated list of tag names; use * to match/find any tag.\n"
" additional options:\n"
" -v | --version Display version\n"
Expand Down Expand Up @@ -503,6 +509,10 @@ - (void)performOperation
[self doFind];
break;

case OperationModeInvertFind:
[self doInvertFind];
break;

case OperationModeUsage:
[self doUsage];
break;
Expand Down Expand Up @@ -855,6 +865,10 @@ - (void)doFind
[self findGutsWithUsage:NO];
}

- (void)doInvertFind
{
[self findFilesWithoutTagWithUsage:NO];
}

- (void)doUsage
{
Expand Down Expand Up @@ -898,6 +912,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]) ? @"<no_tag>" : 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];
Expand Down
5 changes: 4 additions & 1 deletion Tag/tag.1
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down