FileManagerLib tags performance, lists, automator

Hello all,
OSX 12.7.2

I’m working on an Automator quick action which runs an Applescript to peak inside a directory and see if there are any files (not folders), and if so, apply a tag (Red) to the parent directory.

My minimum version used labels, but since I’d like to expand on the idea and tag for multiple conditions, after much research on the weirdness which is tags I found the lovely FileManagerLib!

So far, I’ve been successful in getting the list of tags and updating it with new tags. However, I’ve run into problems removing tags. Basically, I want to filter for a tag to remove (ie “Red”) and keep the others in place.

Thank you in advance for anyone who might have some thoughts on what is going on!

Basically, something like the following doesn’t work:

 set target_tag to "Red" as string
 -- Initialize label index with existing labels
 set existingTags to tags of folderAlias -- {"Orange", "Blue"}

if target_tag is in existingTags then --this works
    set existingTags to (existingTags whose item is not equal to target_tag) --this does not work
-- "Error: Can’t get {"Orange", "Red"} whose item != "Red"."
    -- do stuff
end if

I’ve tried all manner of looping over the list to filter out the target_tag, including a helper method like:

 on list_position(this_item, this_list)
    repeat with i from 1 to count of this_list
        if item i of this_list is this_item then return i
    end repeat
    return 0
 end list_position
 -- ERROR: The action “Run AppleScript” encountered an error: “Finder got an error: Can’t continue list_position.”

But this fails too. When I try to identify the classes of the individual list elements using something like the below, I get an empty result for each list element.

-- Get the class name as a string
set variableType to class of myVariable as string
-- Display the type in an alert dialog
display dialog "The variable is of type: " & variableType buttons {"OK"} default button "OK"

In the end, I am converting the tag list into a long string, and then filtering the string representation for the target_tag, and then recreating the list from the results, and feeding that into set tags

Here is my result, and its slooooooooow:

use AppleScript version "2.4"
use scripting additions
use script "FileManagerLib" version "2.3.5"

on run {input, parameters}
	-- The tag that will be applied
	set target_tag to "Red" as string
	
	repeat with folderAlias in input
		tell application "Finder"
			set folderContents to list folder folderAlias without invisibles
			set hasFiles to false
			repeat with fileItem in folderContents
				set filePath to (folderAlias as text) & fileItem
				set itemKind to kind of (info for alias filePath)
				
				if itemKind is not equal to "Folder" then
					set hasFiles to true
					exit repeat
				end if
			end repeat
			
			-- Initialize label index with existing labels
			set existingTags to tags of folderAlias
			
			if hasFiles then
				try
					if "Red" is not in existingTags then
						set end of existingTags to target_tag
						add tags existingTags to folderAlias
					end if
				on error errMsg
					display dialog "Error: " & errMsg
				end try
			else
				if (count of existingTags) is not 0 then
					try
						if target_tag is in existingTags then
							beep 2
							
							-- All efforts to loop over the list have resulted in an error :/
							-- The individual 'tag' indexes are not strings or any other named class that I can find. . . 
							-- set existingTags to (existingTags whose item is not equal to "Red") -- doesn't work
							-- So we will stringify the list and filter that way
							
							-- Set the text item delimiters to ", "
							set oldDelimiters to AppleScript's text item delimiters
							set AppleScript's text item delimiters to ", "
							
							-- Turn the list into a string
							set existingTagsString to existingTags as text
							
							-- Set the target_tag as the new delimiter
							set AppleScript's text item delimiters to target_tag
							-- filter the strinifyed tag list with the target_tag to remove
							set existingTagsString to existingTagsString as text
							-- Create a new list from the results
							set filteredTags to text items of existingTagsString
							
							-- Reset AppleScript's delimiters
							set AppleScript's text item delimiters to oldDelimiters
							-- remove all tags
							remove tags from folderAlias
							-- add only the filtered tags
							add tags filteredTags to folderAlias
						end if
					on error errMsg
						display dialog "Error: " & errMsg
					end try
				end if
			end if
		end tell
	end repeat
	beep 1
	return input
end run

It would be nice if FileManagerLib had a remove tag method, but is there a more robust approach to this?

Thanks again

I don’t quite follow your code, but try something like this:

my removeItem:"Red" fromList:{"Blue", "Red", "Green"}

on removeItem:theItem fromList:theList
	if theList does not contain theItem then return
	set newList to {}
	repeat with anItem in theList
		if contents of anItem is not theItem then
			set end of newList to contents of anItem
		end if
	end repeat
	return newList
end removeItem:fromList:
1 Like

@ShaneStanley

Many thanks!

I think the secret sauce I was missing was ‘my’ to set the scope. Im coming from JS and PHP so thats an -a-ha moment. My inital approach was to find the index of the key and drop it from the list. But your apporach is easier to understand.

I’ve also had a stab at hacking FileManagerLib to get the ability to drop keys from the list.
It preserves the original behaviour - if you pass nothing or an empty list, it flushes all tags; however, if you provide a list, it only drops items found in the list.

Modification to FileManagerLib:

-- Remove tags; pass a file, alias, or POSIX path
on remove tags from theFileOrPathInput given tagList:tagList
	return my removeTags:tagList forItem:theFileOrPathInput
end remove tags from

on removeTags:tagList forItem:theFileOrPathInput
	if tagList is {} or tagList is missing value then
		return my setTags:{} forItem:theFileOrPathInput
	else
		set thisURL to my makeURLFromFileOrPath:theFileOrPathInput -- make URL
		-- get existing tags
		set {theResult, theTags, theError} to thisURL's getResourceValue:(reference) forKey:NSURLTagNamesKey |error|:(reference)
		if theResult as boolean is false then error (theError's |localizedDescription|() as text)
		
		-- Convert NSArray to a list
		set tagListArray to (theTags as list)
		
		-- Filter out tags to be removed
		set filteredTags to (current application's NSArray's arrayWithArray:tagListArray)'s filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat_("NOT (SELF IN %@)", tagList))
		
		-- Convert NSArray back to a list
		set filteredTagsList to filteredTags as list
		
		set {theResult, theError} to thisURL's setResourceValue:filteredTagsList forKey:NSURLTagNamesKey |error|:(reference)
		if theResult as boolean is false then error (theError's |localizedDescription|() as text)
		return true
	end if
end removeTags:forItem:

and in your script

remove tags from folderAlias given tagList:{target_tag}

Its usage is not as elegant as the other methods available, i.e. ... given tag list:{target_tag} feels awkward. Perhaps this can be modified by someone @ShaneStanley ? to provide better ergonomics? I had been trying to keep the same API, but it appears that there isn’t an elegant way for tagList to be an optional parameter.

Obviously, I’d rather not hack FileManagerLib directly. It would be nice if it made its way into the package, though!

Realistically, it’s unlikely to happen. I wouldn’t be doing any more than the code above, and it raises questions about what to do if the tag isn’t found – someone’s bound to want that reported too.

If you’re writing an Automator action, surely the best solution is to incorporate the code, or as much of it as you need, in the action. That’s neater, self-contained, and you can then modify it to your heart’s content.

@ShaneStanley, Totally understood; thanks for your help.