From 50ba047b85c9896da74610daabbcbe2b27d9a366 Mon Sep 17 00:00:00 2001 From: James Berry Date: Fri, 1 Nov 2013 09:53:01 -0700 Subject: [PATCH] Compare tags in a case-insensitive fashion for compatibility with Finder. Closes #2. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We try to retain tag case for existing tags on a file. But with this fix we won’t add a second tag on a file that differs only in case, and we’ll match for files with tags that differ in only in case from the match expression. --- Makefile | 2 +- Tag.xcodeproj/project.pbxproj | 6 ++ Tag/Tag.h | 2 +- Tag/Tag.m | 120 +++++++++++++++++++++------------- Tag/TagName.h | 44 +++++++++++++ Tag/TagName.m | 67 +++++++++++++++++++ 6 files changed, 193 insertions(+), 48 deletions(-) create mode 100644 Tag/TagName.h create mode 100644 Tag/TagName.m diff --git a/Makefile b/Makefile index 4331247..2abdf14 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ INSTALL = /usr/bin/install BINDIR = ${prefix}/bin MANDIR = ${prefix}/man -SRCS = Tag/main.m Tag/Tag.m +SRCS = Tag/main.m Tag/Tag.m Tag/TagName.m LIBS = -framework Foundation \ -framework CoreServices diff --git a/Tag.xcodeproj/project.pbxproj b/Tag.xcodeproj/project.pbxproj index 415e29f..1cb0ae7 100644 --- a/Tag.xcodeproj/project.pbxproj +++ b/Tag.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 901F1415181B16A00007C852 /* tag.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = 901F1414181B16A00007C852 /* tag.1 */; }; 901F141D181B25F10007C852 /* Tag.m in Sources */ = {isa = PBXBuildFile; fileRef = 901F141C181B25F10007C852 /* Tag.m */; }; 901F141F181B469E0007C852 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 901F141E181B469E0007C852 /* CoreServices.framework */; }; + 90A6AB6E1823FE7B002AE709 /* TagName.m in Sources */ = {isa = PBXBuildFile; fileRef = 90A6AB6D1823FE7B002AE709 /* TagName.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -39,6 +40,8 @@ 901F1423181B71AF0007C852 /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; 901F1425181B90930007C852 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 901F1426181B90930007C852 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.md; sourceTree = ""; }; + 90A6AB6C1823FE7B002AE709 /* TagName.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TagName.h; sourceTree = ""; }; + 90A6AB6D1823FE7B002AE709 /* TagName.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TagName.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -90,6 +93,8 @@ 901F1414181B16A00007C852 /* tag.1 */, 901F141B181B25F10007C852 /* Tag.h */, 901F141C181B25F10007C852 /* Tag.m */, + 90A6AB6C1823FE7B002AE709 /* TagName.h */, + 90A6AB6D1823FE7B002AE709 /* TagName.m */, 901F1412181B16A00007C852 /* Supporting Files */, ); path = Tag; @@ -154,6 +159,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 90A6AB6E1823FE7B002AE709 /* TagName.m in Sources */, 901F141D181B25F10007C852 /* Tag.m in Sources */, 901F1411181B16A00007C852 /* main.m in Sources */, ); diff --git a/Tag/Tag.h b/Tag/Tag.h index e05f5a4..cbf92fb 100644 --- a/Tag/Tag.h +++ b/Tag/Tag.h @@ -56,7 +56,7 @@ typedef NS_ENUM(int, SearchScope) { @property (assign, nonatomic) OutputFlags outputFlags; @property (assign, nonatomic) SearchScope searchScope; -@property (copy, nonatomic) NSArray* tags; +@property (copy, nonatomic) NSSet* tags; @property (copy, nonatomic) NSArray* URLs; - (void)parseCommandLineArgv:(char * const *)argv argc:(int)argc; diff --git a/Tag/Tag.m b/Tag/Tag.m index 3d5142e..83a19cf 100644 --- a/Tag/Tag.m +++ b/Tag/Tag.m @@ -48,10 +48,12 @@ #import "Tag.h" +#import "TagName.h" #import +NSString* const version = @"0.7.2"; -NSString* const version = @"0.7.1"; +NSString* const kMDItemUserTags = @"kMDItemUserTags"; @interface Tag () @@ -257,10 +259,10 @@ - (void)parseTagsArgument:(NSString*)arg { NSString* trimmed = [component stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([trimmed length]) - [uniqueTags addObject:trimmed]; + [uniqueTags addObject:[[TagName alloc] initWithTag:trimmed]]; } - self.tags = [uniqueTags allObjects]; + self.tags = uniqueTags; } @@ -344,12 +346,6 @@ - (void)reportFatalError:(NSError*)error onURL:(NSURL*)URL } -- (BOOL)wildcardInArray:(NSArray*)array -{ - return [array containsObject:@"*"]; -} - - - (NSString*)string:(NSString*)s paddedToMinimumLength:(int)minLength { NSInteger length = [s length]; @@ -360,16 +356,16 @@ - (NSString*)string:(NSString*)s paddedToMinimumLength:(int)minLength } -- (void)emitURL:(NSURL*)URL tags:(NSArray*)tags +- (void)emitURL:(NSURL*)URL tags:(NSArray*)tagArray { NSString* fileName = (_outputFlags & OutputFlagsName) ? [URL relativePath] : nil; NSString* tagString = nil; NSString* tagSeparator; int minFileFieldWidth = 0; - if ((_outputFlags & OutputFlagsTags) && [tags count]) + if ((_outputFlags & OutputFlagsTags) && [tagArray count]) { - NSArray* sortedTags = [tags sortedArrayUsingSelector:@selector(compare:)]; + NSArray* sortedTags = [tagArray sortedArrayUsingSelector:@selector(compare:)]; if (_outputFlags & OutputFlagsGarrulous) { tagSeparator = fileName ? @"\n " : @"\n"; // Don't indent tags if no filename @@ -400,12 +396,42 @@ - (void)emitURL:(NSURL*)URL tags:(NSArray*)tags } +- (BOOL)wildcardInTagSet:(NSSet*)set +{ + static TagName* wildcard; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + wildcard = [[TagName alloc] initWithTag:@"*"]; + }); + return [set containsObject:wildcard]; +} + + +- (NSMutableSet*)tagSetFromArrayOfTags:(NSArray*)tagArray +{ + NSMutableSet* set = [[NSMutableSet alloc] initWithCapacity:[tagArray count]]; + for (NSString* tag in tagArray) + [set addObject:[[TagName alloc] initWithTag:tag]]; + return set; +} + + +- (NSArray*)tagArrayFromTagSet:(NSSet*)tagSet +{ + NSMutableArray* array = [[NSMutableArray alloc] initWithCapacity:[tagSet count]]; + for (TagName* tag in tagSet) + [array addObject:tag.visibleName]; + return array; +} + + - (void)doSet { + NSArray* tagArray = [self tagArrayFromTagSet:self.tags]; for (NSURL* URL in self.URLs) { NSError* error; - if (![URL setResourceValue:self.tags forKey:NSURLTagNamesKey error:&error]) + if (![URL setResourceValue:tagArray forKey:NSURLTagNamesKey error:&error]) [self reportFatalError:error onURL:URL]; } } @@ -413,6 +439,9 @@ - (void)doSet - (void)doAdd { + if (![self.tags count]) + return; + for (NSURL* URL in self.URLs) { @autoreleasepool { @@ -423,12 +452,12 @@ - (void)doAdd if (![URL getResourceValue:&existingTags forKey:NSURLTagNamesKey error:&error]) [self reportFatalError:error onURL:URL]; - // Form the union of the existing tags + new tags - NSMutableSet* tagSet = [[NSMutableSet alloc] initWithArray:existingTags]; - [tagSet addObjectsFromArray:self.tags]; + // Form the union of the existing tags + new tags. + NSMutableSet* tagSet = [self tagSetFromArrayOfTags:existingTags]; + [tagSet unionSet:self.tags]; // Set all the new tags onto the item - if (![URL setResourceValue:[tagSet allObjects] forKey:NSURLTagNamesKey error:&error]) + if (![URL setResourceValue:[self tagArrayFromTagSet:tagSet] forKey:NSURLTagNamesKey error:&error]) [self reportFatalError:error onURL:URL]; } } @@ -437,8 +466,10 @@ - (void)doAdd - (void)doRemove { - BOOL matchAny = [self wildcardInArray:self.tags]; - NSSet* tagsToRemove = [NSSet setWithArray:self.tags]; + if (![self.tags count]) + return; + + BOOL matchAny = [self wildcardInTagSet:self.tags]; for (NSURL* URL in self.URLs) { @@ -451,14 +482,14 @@ - (void)doRemove [self reportFatalError:error onURL:URL]; // Form a set containing difference of the existing tags - tags to remove - NSMutableSet* tagSet = [[NSMutableSet alloc] initWithArray:existingTags]; + NSMutableSet* tagSet = [self tagSetFromArrayOfTags:existingTags]; if (matchAny) [tagSet removeAllObjects]; else - [tagSet minusSet:tagsToRemove]; + [tagSet minusSet:self.tags]; // Set the revised tags onto the item - if (![URL setResourceValue:[tagSet allObjects] forKey:NSURLTagNamesKey error:&error]) + if (![URL setResourceValue:[self tagArrayFromTagSet:tagSet] forKey:NSURLTagNamesKey error:&error]) [self reportFatalError:error onURL:URL]; } } @@ -467,8 +498,7 @@ - (void)doRemove - (void)doMatch { - BOOL matchAny = [self wildcardInArray:self.tags]; - NSSet* requiredTags = [NSSet setWithArray:self.tags]; + BOOL matchAny = [self wildcardInTagSet:self.tags]; // Display only those items containing all the tags listed for (NSURL* URL in self.URLs) @@ -477,14 +507,14 @@ - (void)doMatch NSError* error; // Get the tags on the URL - NSArray* tags; - if (![URL getResourceValue:&tags forKey:NSURLTagNamesKey error:&error]) + NSArray* tagArray; + if (![URL getResourceValue:&tagArray forKey:NSURLTagNamesKey error:&error]) [self reportFatalError:error onURL:URL]; // If the set of existing tags contains all of the required // tags then print the path - if ((matchAny && [tags count]) || [requiredTags isSubsetOfSet:[NSSet setWithArray:tags]]) - [self emitURL:URL tags:tags]; + if ((matchAny && [tagArray count]) || [self.tags isSubsetOfSet:[self tagSetFromArrayOfTags:tagArray]]) + [self emitURL:URL tags:tagArray]; } } } @@ -497,11 +527,11 @@ - (void)doList { @autoreleasepool { NSError* error; - NSArray* tags; - if (![URL getResourceValue:&tags forKey:NSURLTagNamesKey error:&error]) + NSArray* tagArray; + if (![URL getResourceValue:&tagArray forKey:NSURLTagNamesKey error:&error]) [self reportFatalError:error onURL:URL]; - [self emitURL:URL tags:tags]; + [self emitURL:URL tags:tagArray]; } } } @@ -523,25 +553,24 @@ - (void)doFind } -- (NSPredicate*)formQueryPredicateForTags:(NSArray*)tags +- (NSPredicate*)formQueryPredicateForTags:(NSSet*)tagSet { - NSAssert([tags count], @"Assumes there are tags to query for"); + NSAssert([tagSet count], @"Assumes there are tags to query for"); NSPredicate* result; - if ([self wildcardInArray:tags]) + if ([self wildcardInTagSet:tagSet]) { - result = [NSPredicate predicateWithFormat:@"kMDItemUserTags LIKE '*'"]; + result = [NSPredicate predicateWithFormat:@"%K LIKE '*'", kMDItemUserTags]; } - else if ([tags count] == 1) + else if ([tagSet count] == 1) { - result = [NSPredicate predicateWithFormat:@"kMDItemUserTags == %@", tags[0]]; + result = [NSPredicate predicateWithFormat:@"%K ==[c] %@", kMDItemUserTags, ((TagName*)tagSet.anyObject).visibleName]; } else { NSMutableArray* subpredicates = [NSMutableArray new]; - for (NSString* tag in tags) - [subpredicates addObject:[NSPredicate predicateWithFormat:@"kMDItemUserTags == %@", tag]]; - + for (TagName* tag in tagSet) + [subpredicates addObject:[NSPredicate predicateWithFormat:@"%K ==[c] %@", kMDItemUserTags, tag.visibleName]]; result = [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates]; } @@ -568,7 +597,7 @@ - (NSArray*)searchScopesFromSearchScope:(SearchScope)scope } -- (void)initiateMetadataSearchForTags:(NSArray*)tags +- (void)initiateMetadataSearchForTags:(NSSet*)tagSet { // Create the metadata query instance self.metadataQuery=[[NSMetadataQuery alloc] init]; @@ -585,7 +614,7 @@ - (void)initiateMetadataSearchForTags:(NSArray*)tags object:_metadataQuery]; // Configure the search predicate - NSPredicate *searchPredicate = [self formQueryPredicateForTags:tags]; + NSPredicate *searchPredicate = [self formQueryPredicateForTags:tagSet]; [_metadataQuery setPredicate:searchPredicate]; // Set the search scope @@ -593,7 +622,7 @@ - (void)initiateMetadataSearchForTags:(NSArray*)tags [_metadataQuery setSearchScopes:searchScopes]; // Configure the sorting of the results - // (note that the query can't sort by the item path, which likely makes this useless) + // (note that the query can't sort by the item path, which makes sorting less usefull) NSSortDescriptor *sortKeys = [[NSSortDescriptor alloc] initWithKey:(id)kMDItemDisplayName ascending:YES]; [_metadataQuery setSortDescriptors:[NSArray arrayWithObject:sortKeys]]; @@ -624,11 +653,10 @@ - (void)queryComplete:sender; @autoreleasepool { NSMetadataItem* theResult = [_metadataQuery resultAtIndex:i]; - // kMDItemPath, kMDItemDisplayName NSURL* URL = [NSURL fileURLWithPath:[theResult valueForAttribute:(NSString *)kMDItemPath]]; - NSArray* tags = [theResult valueForAttribute:@"kMDItemUserTags"]; + NSArray* tagArray = [theResult valueForAttribute:kMDItemUserTags]; - [self emitURL:URL tags:tags]; + [self emitURL:URL tags:tagArray]; } } diff --git a/Tag/TagName.h b/Tag/TagName.h new file mode 100644 index 0000000..c3a047c --- /dev/null +++ b/Tag/TagName.h @@ -0,0 +1,44 @@ +// +// TagName.h +// Tag +// +// Created by James Berry on 11/1/13. +// +// The MIT License (MIT) +// +// Copyright (c) 2013 James Berry +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import + +// Items of this class are compared for equality in a case-insensitive fashion, +// but retain their original case in visibleName. TagName objects are used +// in forming sets of tags. + +@interface TagName : NSObject + +- (instancetype)initWithTag:(NSString*)tag; + +@property (readonly) NSString* visibleName; +@property (readonly) NSString* comparableName; + +- (BOOL)isEqualToTagName:(TagName*)tagName; + +@end diff --git a/Tag/TagName.m b/Tag/TagName.m new file mode 100644 index 0000000..aad8eeb --- /dev/null +++ b/Tag/TagName.m @@ -0,0 +1,67 @@ +// +// TagName.m +// Tag +// +// Created by James Berry on 11/1/13. +// +// The MIT License (MIT) +// +// Copyright (c) 2013 James Berry +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +#import "TagName.h" + +@implementation TagName + +- (instancetype)initWithTag:(NSString*)tag +{ + self = [super init]; + if (self) + { + _visibleName = [tag copy]; + _comparableName = [_visibleName lowercaseString]; + } + return self; +} + + +- (BOOL)isEqualToTagName:(TagName*)tagName +{ + return [self.comparableName isEqualToString:tagName.comparableName]; +} + + +- (BOOL)isEqual:(id)obj +{ + if (obj == self) + return YES; + if (!obj || ![obj isKindOfClass:[self class]]) + return NO; + return [self isEqualToTagName:obj]; +} + + +- (NSUInteger)hash +{ + return [self.comparableName hash]; +} + + +@end