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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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 -i | --invert <tags> <path>... Invert 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
Expand Down Expand Up @@ -61,6 +62,13 @@ The *set* operation replaces all tags on the specified files with one or more ne
tag --set tagname file
tag --set tagname1,tagname2,... file1 file2...

### Invert tags on a file

The *invert* operation sets tags which are not present and remove tags which are present on the specified files.

tag --invert tagname file
tag --invert tagname1,tagname2,... file1 file2...

### Show files matching tags

The *match* operation prints the file names that match the specified tags. Matched files must have at least *all* of the tags specified. Note that *match* matches only against the files that are provided as parameters (and those that it encounters if you use the --enter or --recursive options). To search for tagged files across your filesystem, see the *find* operation.
Expand Down Expand Up @@ -223,7 +231,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, --invert, --match, --list, --find, or --usage may be abbreviated as -a, -r, -s, -i, -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.
* 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 @@ -34,6 +34,7 @@ typedef NS_ENUM(int, OperationMode) {
OperationModeSet = 's',
OperationModeAdd = 'a',
OperationModeRemove = 'r',
OperationModeInvert = 'i',
OperationModeMatch = 'm',
OperationModeFind = 'f',
OperationModeUsage = 'u',
Expand Down
52 changes: 51 additions & 1 deletion Tag/Tag.m
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ - (void)parseCommandLineArgv:(char * const *)argv argc:(int)argc
{ "set", required_argument, 0, OperationModeSet },
{ "add", required_argument, 0, OperationModeAdd },
{ "remove", required_argument, 0, OperationModeRemove },
{ "invert", required_argument, 0, OperationModeInvert },
{ "match", required_argument, 0, OperationModeMatch },
{ "find", required_argument, 0, OperationModeFind },
{ "usage", optional_argument, 0, OperationModeUsage },
Expand Down Expand Up @@ -182,13 +183,14 @@ - (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:i:m:f:u::lAeRdnNtTgGcp0hv", options, &option_index)) != -1)
{
switch (option_char)
{
case OperationModeSet:
case OperationModeAdd:
case OperationModeRemove:
case OperationModeInvert:
case OperationModeMatch:
case OperationModeFind:
case OperationModeUsage:
Expand Down Expand Up @@ -369,6 +371,7 @@ - (void)displayHelp
" 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 -i | --invert <tags> <path>... Invert 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"
Expand Down Expand Up @@ -495,6 +498,10 @@ - (void)performOperation
[self doRemove];
break;

case OperationModeInvert:
[self doInvert];
break;

case OperationModeMatch:
[self doMatch];
break;
Expand Down Expand Up @@ -806,6 +813,49 @@ - (void)doRemove
}


- (void)doInvert
{
// If there are no tags to invert, we're done
if (![self.tags count])
return;

// Only perform invert on specified URLs
// (we don't implicitly enumerate the current directory)
if ([self.URLs count] == 0)
return;

// Enumerate the provided URLs, adding tags to each
// --all, --enter, and --recursive apply
[self enumerateURLsWithBlock:^(NSURL *URL) {
NSError* error;

// Get the existing tags
NSArray* existingTags;
if (![URL getResourceValue:&existingTags forKey:NSURLTagNamesKey error:&error])
[self reportFatalError:error onURL:URL];

NSMutableSet* curTags = [self tagSetFromTagArray:existingTags];

// Calculate tags to be removed
NSMutableSet* tagsToRemove = [curTags mutableCopy];
[tagsToRemove intersectSet:self.tags];

// Calculate tags to be added
NSMutableSet* tagsToAdd = [self.tags mutableCopy];
[tagsToAdd minusSet:tagsToRemove];

// Remove tags to be removed
[curTags minusSet:tagsToRemove];
// Add tags to be added
[curTags unionSet:tagsToAdd];

// Set all the new tags onto the item
if (![URL setResourceValue:[self tagArrayFromTagSet:curTags] forKey:NSURLTagNamesKey error:&error])
[self reportFatalError:error onURL:URL];
}];
}


- (void)doMatch
{
BOOL matchAny = [self wildcardInTagSet:self.tags];
Expand Down