Sort by Tag

TL;DR – Tag files, Hazel sorts them into your Archive folder in a hierarchy of subfolders by tag.

I use the Desktop as a place for working files. Projects that I’m working on, files that I will need this week etc. Once they are no longer needed, I will delete them or file them away. With the help of Hazel and a couple of AppleScripts, I can achieve the latter simply by tagging them.  For example, this years tax return might be tagged: “Finances”, “Tax”, “2017”, “Archive”.

Hazel sees the file, moves it into the Archived folder and then into subfolders Finances > Tax > 2017.

Here’s how to set it up:

  1. First step is to move files that have been tagged “Archive” (I use the grey colour label for this). However, you want that file to have more tags than just “Archive” so the file must first pass the embedded AppleScript as below:

1 Desktop rule

-- SET THIS TO "OFF" TO GET RESULT AS APPLESCRIPT LIST
-- (USEFUL FOR USING TAGS WITH OTHER APPLICATIONS)

set text_List to "OFF"

-- SET THIS TO "OFF" TO REMOVE DIALOG BOX FROM TEXT LIST RESULT
set display_ON to "OFF"

(* 
======================================
//  PROPERTIES (USE CAUTION WHEN CHANGING)
======================================
*)
set tagItems to {}
(* 
======================================
// MAIN PROGRAM 
======================================
*)

--RESET TEXT ITEM DELIMITERS
set AppleScript's text item delimiters to ""


--GET THE FILE

set thePath to quoted form of (POSIX path of theFile) as string

-- GET THE TAG VALUES VIA SHELL SCRIPT
set userTags to (do shell script "mdls -name kMDItemUserTags  " & thePath) as text

-- SET UP THE DELIMITER VALUES TO PULL OUT TAG VALUES…
set theDelim to "("
set finalDelim to ")"

-- SAVE THE OLD TEXT ITEM DELIMITERS
set oldDelim to AppleScript's text item delimiters

-- PROCESS THE KEY INTO A LIST OF TAGS
set AppleScript's text item delimiters to theDelim
set userTags to (userTags as text)
set preProc to (every word of userTags) as string
set procItems to words 3 thru end of preProc
set finalLine to last item of procItems
set AppleScript's text item delimiters to finalDelim
set finalProc to first text item of finalLine
set last item of procItems to finalProc
set AppleScript's text item delimiters to oldDelim

--  CREATE A COMMA-DELIMITED TAG STRING
if text_List is "ON" then
	set AppleScript's text item delimiters to ", "
	set procItems to (procItems as text)
	set AppleScript's text item delimiters to oldDelim
	
	-- DISPLAY AS DIALOG, IF SELECTED
	if display_ON is "ON" then
		tell application "System Events"
			display dialog procItems
		end tell
	end if
	
else
	-- … OR CREATE A CLEAN LIST OF TAGS
	
	-- REMOVE THE COMMAS…
	repeat with procItem in procItems
		set the_String to procItem
		if (the last character of (the_String) is ",") then
			set newItem to (text 1 thru ((length of the_String) - 1)) of the_String as string
			copy newItem to the end of tagItems
		else
			set newItem to (text 1 thru (length of the_String)) of the_String as string
			copy newItem to the end of tagItems
		end if
	end repeat
	
	-- FINAL LIST
	----return tagItems
	
	
	
	--PASSES APPLESCRIPT FOR HAZEL
	if (count of tagItems) is greater than 1 then
		return true
	end if
	
	if (count of tagItems) is 1 then
		return false
	end if
	
	
end if

(Hat tip to Veritrope for the get tags script)

2. Now, we’ll need to create rules in the “Archived” folder.

Sort.png

You will need to save the following AppleScript somewhere and refer to it in the rules (it won’t work as an embedded script due to presence of handlers). Now before you scroll through this monstrosity let me explain what it’s doing: the end result is it exports Hazel tokens for the second “sort into subfolder” rule for Hazel to do it’s sorting business.

However, you may already have an existing hierarchy (e.g. “Computer” > “AppleScript” > “Countdown_Scripts”) and if you tag out of order or omit one of the tags (e.g. “Countdown_Scripts”; “Computer”), you would get a big mess of folders.

What the script attempts to do is check the hierarchy and adjust the order of the exported tags to match the existing file structure. If it finds a new tag, it will generally put it at the end of the order; if the tag comes up multiple times in the hierarchy the script will return the export tags in the same order as you entered them.

It’s a bit convoluted, let me know in the comments below if that doesn’t make sense. If you don’t care about the order of the tags you could cut that section out I suppose – let me know in the comments below if you’d like the less complicated version of the script.

You’ll need to change the three “/path/to/your/Archived folder/” lines, to where your folder is.

--variables for later...
global thePrimaryFolderList
global theSecondaryFolderList
global theTertiaryFolderList
global theSpecialCaseList
global theFullFolderList

on hazelProcessFile(theFile)
	
	
	(* 
======================================
// USER SWITCHES (YOU CAN CHANGE THESE!)
======================================
*)
	-- SET THIS TO "OFF" TO GET RESULT AS APPLESCRIPT LIST
	-- (USEFUL FOR USING TAGS WITH OTHER APPLICATIONS)
	
	set text_List to "OFF"
	
	-- SET THIS TO "OFF" TO REMOVE DIALOG BOX FROM TEXT LIST RESULT
	set display_ON to "OFF"
	
	(* 
======================================
//  PROPERTIES (USE CAUTION WHEN CHANGING)
======================================
*)
	set tagItems to {}
	(* 
======================================
// MAIN PROGRAM 
======================================
*)
	
	--RESET TEXT ITEM DELIMITERS
	set AppleScript's text item delimiters to ""
	
	
	--GET THE FILE
	set thePath to quoted form of (POSIX path of theFile) as string
	
	-- GET THE TAG VALUES VIA SHELL SCRIPT
	set userTags to (do shell script "mdls -name kMDItemUserTags  " & thePath) as text
	
	-- SET UP THE DELIMITER VALUES TO PULL OUT TAG VALUES…
	set theDelim to "("
	set finalDelim to ")"
	
	-- SAVE THE OLD TEXT ITEM DELIMITERS
	set oldDelim to AppleScript's text item delimiters
	
	-- PROCESS THE KEY INTO A LIST OF TAGS
	set AppleScript's text item delimiters to theDelim
	set userTags to (userTags as text)
	set preProc to (every word of userTags) as string
	set procItems to words 3 thru end of preProc
	set finalLine to last item of procItems
	set AppleScript's text item delimiters to finalDelim
	set finalProc to first text item of finalLine
	set last item of procItems to finalProc
	set AppleScript's text item delimiters to oldDelim
	
	--------------------------------------------------------------------------------
	
	
	--  CREATE A COMMA-DELIMITED TAG STRING
	if text_List is "ON" then
		set AppleScript's text item delimiters to ", "
		set procItems to (procItems as text)
		set AppleScript's text item delimiters to oldDelim
		
		-- DISPLAY AS DIALOG, IF SELECTED
		if display_ON is "ON" then
			tell application "System Events"
				display dialog procItems
			end tell
		end if
		
	else
		-- … OR CREATE A CLEAN LIST OF TAGS
		
		-- REMOVE THE COMMAS…
		repeat with procItem in procItems
			set the_String to procItem
			if (the last character of (the_String) is ",") then
				set newItem to (text 1 thru ((length of the_String) - 1)) of the_String as string
				copy newItem to the end of tagItems
			else
				set newItem to (text 1 thru (length of the_String)) of the_String as string
				copy newItem to the end of tagItems
			end if
		end repeat
		
		-- FINAL LIST
		--return tagItems
		
		
		--------------------------------------------------------------------------------
		--get rid of "Archive" from the tag list
		set cleanTagList to {}
		
		repeat with i from 1 to count of tagItems
			set theItem to item i of tagItems
			if theItem is not "Archive" then
				copy theItem to end of cleanTagList
			end if
		end repeat
		
		
		--get a list of all primary directories
		do shell script "ls -d /path/to/your/Archived folder/*/"
		--transform into AppleScript compatible list
		set theFullList1 to paragraphs of result
		
		--get a list of all secondary directories
		do shell script "ls -d /path/to/your/Archived folder/*/*/"
		--transform into AppleScript compatible list
		set theFullList2 to paragraphs of result
		
		--get a list of all tertiary directories
		do shell script "ls -d /path/to/your/Archived folder/*/*/*/"
		--transform into AppleScript compatible list
		set theFullList3 to paragraphs of result
		
		--make some empty lists for later...
		set theCatchList1 to {}
		set theCatchList2 to {}
		set theCatchList3 to {}
		set thePrimaryFolderList to {}
		set theSecondaryFolderList to {}
		set theTertiaryFolderList to {}
		set theFullFolderList to {}
		set theSpecialCaseList to {}
		
		repeat with eachLine in theFullList1
			set theOffset to offset of "Archived/" in eachLine
			set theLength to ((length of eachLine) - 1)
			set theNewText to text theOffset through theLength of eachLine
			copy theNewText to the end of theCatchList1
		end repeat
		
		--parse the "Archived/*/" into individual items {"Archived", "*"} 
		set AppleScript's text item delimiters to "/"
		repeat with eachItem in theCatchList1
			set theFolders to text items of eachItem
			
			set thePrimaryFolder to item 2 of theFolders
			copy thePrimaryFolder to end of thePrimaryFolderList
			
		end repeat
		
		repeat with eachLine in theFullList2
			set theOffset to offset of "Archived/" in eachLine
			set theLength to ((length of eachLine) - 1)
			set theNewText to text theOffset through theLength of eachLine
			copy theNewText to the end of theCatchList2
		end repeat
		
		--parse the "Archived/*/*/" into individual items {"Archived", "*", "*"} 
		
		repeat with eachItem in theCatchList2
			set theFolders to text items of eachItem
			
			set theSecondaryFolder to item 3 of theFolders
			
			--creat a list of "Special Case" folders, which exist as both primary and secondary directories
			if theSecondaryFolder is in thePrimaryFolderList then
				if theSecondaryFolder is not in theSpecialCaseList then
					copy theSecondaryFolder to end of theSpecialCaseList
				end if
			end if
			
			copy theSecondaryFolder to end of theSecondaryFolderList
			
		end repeat
		
		
		
		
		
		repeat with eachLine in theFullList3
			set theOffset to offset of "Archived/" in eachLine
			set theLength to ((length of eachLine) - 1)
			set theNewText to text theOffset through theLength of eachLine
			copy theNewText to the end of theCatchList3
		end repeat
		
		--parse the "Archived/*/*/*" into individual items {"Archived", "*", "*", "*"} 
		
		repeat with eachItem in theCatchList3
			set theFolders to text items of eachItem
			
			set thePrimaryFolder to item 2 of theFolders
			copy thePrimaryFolder to end of theFullFolderList
			
			set theSecondaryFolder to item 3 of theFolders
			copy theSecondaryFolder to end of theFullFolderList
			
			set theTertiaryFolder to item 4 of theFolders
			copy theTertiaryFolder to end of theTertiaryFolderList
			copy theTertiaryFolder to end of theFullFolderList
			
			--creat a list of "Special Case" folders, which exist as both secondary and tertiary directories
			if theTertiaryFolder is in theSecondaryFolderList then
				if theTertiaryFolder is not in theSpecialCaseList then
					copy theTertiaryFolder to end of theSpecialCaseList
				end if
			end if
			
		end repeat
		
		set AppleScript's text item delimiters to ""
		--------------------------------------------------------------------------------
		--get names of all these damned tags
		set theCount to count of cleanTagList
		
		if theCount is 1 then
			set theTag to item 1 of cleanTagList as text
		end if
		
		if theCount is 2 then
			set theTag to item 1 of cleanTagList as text
			set theTag2 to item 2 of cleanTagList as text
		end if
		
		if theCount is 3 then
			set theTag to item 1 of cleanTagList as text
			set theTag2 to item 2 of cleanTagList as text
			set theTag3 to item 3 of cleanTagList as text
		end if
		
		--------------------------------------------------------------------------------
		--get the order of these tags
		set the123List to {}
		
		repeat with eachItem in cleanTagList
			copy whatkindofTagisit(eachItem) to end of the123List
		end repeat
		
		
		set the123ListLength to length of the123List
		if the123List contains 4 then
			if the123ListLength is 1 then
				return {hazelExportTokens:{Tag:theTag}}
				
			end if
			if the123ListLength is 2 then
				return {hazelExportTokens:{Tag:theTag, Tag2:theTag2}}
				
			end if
			if the123ListLength is 3 then
				return {hazelExportTokens:{Tag:theTag, Tag2:theTag2, Tag3:theTag3}}
				
			end if
		end if
		
		----------------------------------[1 tag]---------------------------------------
		if the123List is {1} then
			return {hazelExportTokens:{Tag:theTag}}
		end if
		
		if the123List is {2} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag}}
		end if
		
		if the123List is {3} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			set theGrandParent to item (theTagPosition - 2) of theFullFolderList
			return {hazelExportTokens:{Tag:theGrandParent, Tag2:theParent, Tag3:theTag}}
		end if
		
		if the123List is {0} then
			return {hazelExportTokens:{Tag:theTag}}
		end if
		
		---------------------------------[2 tags]---------------------------------------
		
		
		
		if the123List is {1, 2} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2}}
		end if
		
		if the123List is {1, 3} then
			set theTagPosition to list_position(theTag2, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag, Tag2:theParent, Tag3:theTag2}}
			
		end if
		
		if the123List is {1, 0} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2}}
		end if
		
		if the123List is {2, 1} then
			return {hazelExportTokens:{Tag:theTag2, Tag2:theTag}}
		end if
		
		if the123List is {2, 3} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag, Tag3:theTag2}}
		end if
		
		if the123List is {2, 0} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag, Tag3:theTag2}}
		end if
		
		if the123List is {3, 1} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag2, Tag2:theParent, Tag3:theTag}}
		end if
		
		if the123List is {3, 2} then
			set theTagPosition to list_position(theTag2, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag2, Tag3:theTag}}
		end if
		
		if the123List is {3, 0} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			set theGrandParent to item (theTagPosition - 2) of theFullFolderList
			return {hazelExportTokens:{Tag:theGrandParent, Tag2:theParent, Tag3:theTag, Tag4:theTag2}}
		end if
		
		if the123List is {0, 1} then
			return {hazelExportTokens:{Tag:theTag2, Tag2:theTag}}
		end if
		if the123List is {0, 2} then
			set theTagPosition to list_position(theTag2, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag2, Tag3:theTag}}
		end if
		
		if the123List is {0, 3} then
			set theTagPosition to list_position(theTag2, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			set theGrandParent to item (theTagPosition - 2) of theFullFolderList
			return {hazelExportTokens:{Tag:theGrandParent, Tag2:theParent, Tag3:theTag2, Tag4:theTag}}
		end if
		
		if the123List is {0, 0} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2}}
		end if
		
		---------------------------------[3 tags]---------------------------------------
		
		if the123List is {1, 2, 3} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2, Tag3:theTag3}}
		end if
		
		if the123List is {1, 2, 0} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2, Tag3:theTag3}}
		end if
		
		if the123List is {1, 3, 2} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag3, Tag3:theTag2}}
		end if
		
		if the123List is {1, 3, 0} then
			set theTagPosition to list_position(theTag2, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag1, Tag2:theParent, Tag3:theTag2, Tag4:theTag3}}
		end if
		
		if the123List is {1, 0, 2} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2, Tag3:theTag3}}
		end if
		
		if the123List is {1, 0, 3} then
			set theTagPosition to list_position(theTag3, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag, Tag2:theParent, Tag3:theTag3, Tag4:theTag2}}
		end if
		
		if the123List is {2, 1, 3} then
			return {hazelExportTokens:{Tag:theTag2, Tag2:theTag, Tag3:theTag3}}
		end if
		
		if the123List is {2, 1, 0} then
			return {hazelExportTokens:{Tag:theTag2, Tag2:theTag, Tag3:theTag3}}
		end if
		
		if the123List is {2, 0, 3} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag, Tag3:theTag3, Tag4:theTag2}}
		end if
		
		if the123List is {2, 0, 1} then
			return {hazelExportTokens:{Tag:theTag3, Tag2:theTag, Tag3:theTag2}}
		end if
		
		if the123List is {2, 3, 0} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag, Tag3:theTag2, Tag4:theTag3}}
		end if
		
		if the123List is {2, 3, 1} then
			return {hazelExportTokens:{Tag:theTag3, Tag2:theTag, Tag3:theTag2}}
		end if
		
		if the123List is {3, 1, 0} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag2, Tag2:theParent, Tag3:theTag, Tag4:theTag3}}
			
		end if
		
		if the123List is {3, 1, 2} then
			return {hazelExportTokens:{Tag:theTag2, Tag2:theTag3, Tag3:theTag}}
		end if
		
		if the123List is {3, 2, 0} then
			set theTagPosition to list_position(theTag2, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag2, Tag3:theTag, Tag4:theTag3}}
		end if
		
		if the123List is {3, 2, 1} then
			return {hazelExportTokens:{Tag:theTag3, Tag2:theTag2, Tag3:theTag}}
		end if
		
		if the123List is {3, 0, 1} then
			set theTagPosition to list_position(theTag, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag3, Tag2:theParent, Tag3:theTag, Tag4:theTag2}}
		end if
		
		if the123List is {3, 0, 2} then
			set theTagPosition to list_position(theTag3, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag3, Tag3:theTag, Tag4:theTag2}}
		end if
		
		if the123List is {0, 1, 2} then
			return {hazelExportTokens:{Tag:theTag2, Tag2:theTag3, Tag3:theTag}}
		end if
		
		if the123List is {0, 1, 3} then
			set theTagPosition to list_position(theTag3, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag2, Tag2:theParent, Tag3:theTag3, Tag4:theTag}}
		end if
		
		if the123List is {0, 2, 1} then
			return {hazelExportTokens:{Tag:theTag3, Tag2:theTag2, Tag3:theTag}}
		end if
		
		if the123List is {0, 2, 3} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2, Tag3:theTag3}}
		end if
		
		if the123List is {0, 3, 1} then
			set theTagPosition to list_position(theTag2, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theTag3, Tag2:theParent, Tag3:theTag2, Tag4:theTag}}
		end if
		
		if the123List is {0, 3, 2} then
			set theTagPosition to list_position(theTag3, theFullFolderList)
			set theParent to item (theTagPosition - 1) of theFullFolderList
			return {hazelExportTokens:{Tag:theParent, Tag2:theTag3, Tag3:theTag2, Tag4:theTag}}
		end if
		
		if the123List is {0, 0, 0} then
			return {hazelExportTokens:{Tag:theTag, Tag2:theTag2, Tag3:theTag3}}
		end if
		
		
		
		
		
	end if
	
end hazelProcessFile

--------------------------------------------------------------------------------
--Functions for later...

on whatkindofTagisit(theTest)
	--returns 1 if primary folder
	--returns 2 if secondary folder
	--returns 3 if tertiary folder
	--returns 4 if the folder is found at multiple levels
	--returns 0 if unknown folder 
	
	if theTest is in theSpecialCaseList then
		return 4
	else if theTest is in thePrimaryFolderList then
		return 1
		
	else if theTest is in theSecondaryFolderList then
		return 2
		
	else if theTest is in theTertiaryFolderList then
		return 3
	else
		return 0
	end if
end whatkindofTagisit



--gets position of item in the list
on list_position(this_item, this_list)
	repeat with i from 1 to the count of this_list
		if item i of this_list is this_item then return i
	end repeat
	return 0
end list_position

--------------------------------------------------------------------------------

 

Update 2017-07-15:

If you would like to get a notification of where your file landed, just add an action at the end with an embedded shell script:

 

exec='open -R ' 

/usr/local/bin/terminal-notifier -message "$1" -title 'your file landed here:' -execute "$exec$1"
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s