diff --git a/README.md b/README.md index 92eacc1..4b56c1c 100644 --- a/README.md +++ b/README.md @@ -67,23 +67,27 @@ The *match* operation prints the file names that match the specified tags. Matc You can use a wildcard (*) character in the tags list to match against any/all tags. Note, however, that you'll need to quote that * against shell expansion. To display all files in the current directory that have any combination of tags (but not _no_ tags), use: - tag --match \* * + tag --match '*' * + +Conversely, to match against paths that have _no_ tags, use an empty tag expression: + + tag --match '' * Turn on --tags display mode for this operation to additionally show the tags on the file: - tag --match \* --tags * + tag --match '*' --tags * Turn on garrulous output to format those tags onto multiple lines: - tag --match \* --tags --garrulous * + tag --match '*' --tags --garrulous * You may use short options as well. The following is equivalent to the previous command: - tag -tgm \* * + tag -tgm '*' * You may use the --enter or --descend options to display the contents of, or recursively descend through, any directories provided. This is similar to the --find operation, but operates recursively. There may be differences in performance and/or output ordering in particular cases: - tag --match \* --descend . + tag --match '*' --descend . If no file arguments are given, *match* will enumerate and match against the contents of the current directory: @@ -128,11 +132,15 @@ The *find* operation searches across your filesystem for all files that contain You can use the wildcard here too to find all files that contain a tag of any name: - tag --find \* + tag --find '*' +Use an empty tag expression here too to find all files that have _no_ tag: + + tag --find '' + And of course you could turn on display of tag names, and even ask it to be garrulous, which displays all files on your system with tags, listing the tags independently on lines below the file names. - tag -tgf \* + tag -tgf '*' *find* by default will search everywhere that it can. You may supply options to specify a search scope of the user home directory, local disks, or to include attached network file systems. @@ -184,6 +192,7 @@ Advanced Usage ---- * Wherever a "tagname" is expected, a list of tags may be provided. They must be comma-separated. * Tagnames may include spaces, but the entire tag list must be provided as one parameter: "tag1,a multiword tag name,tag3". +* For *match* and *find*, a tag name of '*' is the wildcard and will match any tag. An empty tag expression '' will match only files with no tags. * 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 --descend 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 --descend, then the operation will apply to the directory itself, rather than to its contents. @@ -199,6 +208,6 @@ The following features are contemplated for future enhancement: * A man page * Regex or glob matching of tags -* The ability to match or find for "" * More complicated boolean matching criteria + diff --git a/Tag/Tag.m b/Tag/Tag.m index 4380d69..be6f079 100644 --- a/Tag/Tag.m +++ b/Tag/Tag.m @@ -64,10 +64,7 @@ foo,bar OR baz foo,bar AND NOT biz,baz * -- Some tag - NOT * -- No tag? - ^ -- No tag? - - -- No tag? - "" -- No tag? + -- No tag support glob patterns? support queries for both match and find? @@ -80,7 +77,7 @@ #import "TagName.h" #import -NSString* const version = @"0.8"; +NSString* const version = @"0.8.1"; NSString* const kMDItemUserTags = @"kMDItemUserTags"; @@ -685,7 +682,8 @@ - (void)doRemove - (void)doMatch { BOOL matchAny = [self wildcardInTagSet:self.tags]; - + BOOL matchNone = [self.tags count] == 0; + // Enumerate the provided URLs or current directory, listing all paths that match the specified tags // --all, --enter, and --descend apply [self enumerateURLsWithBlock:^(NSURL *URL) { @@ -695,10 +693,14 @@ - (void)doMatch NSArray* tagArray; if (![URL getResourceValue:&tagArray forKey:NSURLTagNamesKey error:&error]) [self reportFatalError:error onURL:URL]; + NSUInteger tagCount = [tagArray count]; // If the set of existing tags contains all of the required // tags then emit - if ((matchAny && [tagArray count]) || [self.tags isSubsetOfSet:[self tagSetFromTagArray:tagArray]]) + if ( (matchAny && tagCount > 0) + || (matchNone && tagCount == 0) + || (!matchNone && [self.tags isSubsetOfSet:[self tagSetFromTagArray:tagArray]]) + ) [self emitURL:URL tags:tagArray]; }]; } @@ -723,10 +725,6 @@ - (void)doList - (void)doFind { - // Don't do a search for no tags - if (![self.tags count]) - return; - // Start a metadata search for files containing all of the given tags [self initiateMetadataSearchForTags:self.tags]; @@ -739,21 +737,23 @@ - (void)doFind - (NSPredicate*)formQueryPredicateForTags:(NSSet*)tagSet { - NSAssert([tagSet count], @"Assumes there are tags to query for"); - - // Note for future: the following can be used to search for files that have - // no tags: [NSPredicate predicateWithFormat:@"NOT %K LIKE '*'", kMDItemUserTags] - + BOOL matchAny = [self wildcardInTagSet:tagSet]; + BOOL matchNone = [tagSet count] == 0; + NSPredicate* result; - if ([self wildcardInTagSet:tagSet]) + if (matchAny) { result = [NSPredicate predicateWithFormat:@"%K LIKE '*'", kMDItemUserTags]; } + else if (matchNone) + { + result = [NSPredicate predicateWithFormat:@"NOT %K LIKE '*'", kMDItemUserTags]; + } else if ([tagSet count] == 1) { result = [NSPredicate predicateWithFormat:@"%K ==[c] %@", kMDItemUserTags, ((TagName*)tagSet.anyObject).visibleName]; } - else + else // if tagSet count > 0 { NSMutableArray* subpredicates = [NSMutableArray new]; for (TagName* tag in tagSet)