Recursive Handlers

A few recently-posted scripts have used recursive handlers to manipulate files and folders with the Finder. I’ve never had much occasion to use recursive handlers and decided to investigate.

I first wrote a test script that used a recursive handler to get a list of all files in a target folder. My purpose was twofold–to better understand recursive handlers and to create a template of sorts that I might use in the future. My script:

property theFiles : {}

on main()
	set targetFolder to "Store:Test:" as alias
	getFiles(targetFolder)
end main

on getFiles(aFolder)
	tell application "Finder"
		set subfolderFiles to every file of aFolder as alias list
		set subfolderFolders to every folder of aFolder
		repeat with aFolder in subfolderFolders
			my getFiles(aFolder)
		end repeat
		
		set theFiles to theFiles & subfolderFiles
	end tell
end getFiles

set allFiles to main() -- a list of aliases

I was a bit unhappy in that I couldn’t get the script to work without setting ‘theFiles’ as a property. I also wondered if there might be a better way to write this script, and if memory usage might be an isse at some point.

I was also curious as to timing results and compared the above script with the non-recursive Finder script included below. My test folder contained 10 folders each of which contained 10 folders, and every folder including the target folder contained 2 files (202 files total). I also wrote a script utilizing a recursive handler with System Events. The timing results were:

  • Finder script with recursive handler - 2.959 seconds
  • Finder script without recursive handler - 1.086 second
  • System Events script with recursive handler - 535 milliseconds

My non-recursive Finder script was:

on main()
	set targetFolder to "Store:Test:" as alias
	set allFiles to {}
	
	tell application "Finder"
		set subfolderFolders to (every folder of the entire contents of targetFolder) as alias list
		set subfolderFolders to subfolderFolders & targetFolder
		
		repeat with aFolder in subfolderFolders
			set subfolderFiles to every file of aFolder as alias list
			set allFiles to allFiles & subfolderFiles
		end repeat
	end tell
	
	return allFiles
end main

set allFiles to main() -- returns a list of aliases

My recursive System Events script was:

property theFiles : {}

on main()
	set targetFolder to "/Volumes/Store/Test/"
	getFiles(targetFolder)
end main

on getFiles(aFolder)
	tell application "System Events"
		set subfolderFiles to POSIX path of every file of folder aFolder whose visible is true
		set subfolderFolders to POSIX path of every folder of folder aFolder
	end tell
	repeat with aFolder in subfolderFolders
		my getFiles(aFolder)
	end repeat
	set theFiles to theFiles & subfolderFiles
end getFiles

set allFiles to main() -- a list of POSIX paths

Thanks for reading my post.

Hi peavine.

An alternative would be to have a script object within the ‘getFiles()’ handler containing both ‘theFiles’ and the recursive part of the process. ‘theFiles’ would still be a property, but it would be contained within ‘getFiles()’ and, if you’re running a system on which changes to main script properties are persistent, ‘theFiles’ won’t need to be reset explicitly every time ‘getFiles()’ is called:

on main()
	set targetFolder to "Store:Test:" as alias
	getFiles(targetFolder)
end main

on getFiles(aFolder)
	script o
		property theFiles : {}
		
		on recursivePart(aFolder)
			tell application "Finder"
				set subfolderFiles to every file of aFolder as alias list
				set subfolderFolders to every folder of aFolder
			end tell
			repeat with aFolder in subfolderFolders
				recursivePart(aFolder)
			end repeat
			
			set theFiles to theFiles & subfolderFiles
		end recursivePart
	end script
	
	o's recursivePart(aFolder)
	return o's theFiles
end getFiles

set allFiles to main() -- a list of aliases

Thanks Nigel. That’s way better.

FWIW, I changed the System Events script to take the same approach as your script. Also, the format I used with a separate ‘main’ handler for testing might not be best for general use, so I changed that just to show a different approach.

set allFiles to getFiles(POSIX path of (choose folder))

on getFiles(aFolder)
	script o
		property theFiles : {}
		
		on recursivePart(aFolder)
			tell application "System Events"
				set subfolderFiles to POSIX path of every file of folder aFolder whose visible is true
				set subfolderFolders to POSIX path of every folder of folder aFolder
			end tell
			repeat with aFolder in subfolderFolders
				recursivePart(aFolder)
			end repeat
			
			set theFiles to theFiles & subfolderFiles
		end recursivePart
	end script
	
	o's recursivePart(aFolder)
	return o's theFiles
end getFiles

BTW, I ran a timing test of the above script on a folder that contained 10,000 files evenly spread over 4 folders and subfolders. The script as written took 202 seconds to run, but removing ‘whose visible is true’ reduced that to 269 milliseconds. So, it might be best to deal with hidden files elsewhere in the script.

Hi. Using my does the same thing as the script object. If you have such a low file quantity that you’re reasonably able to use Finder in the first place, the limitation is unlikely to appear, however, you could see stack overflow errors with recursion and several hundred to thousand iterations.

set allFiles to {}
getFiles(alias ((path to desktop folder as text) & "test")) --or wherever
allFiles

on getFiles(theFolder)
	tell application "Finder"
		repeat with aFolder in (get theFolder's folders)
			set my allFiles's end to {aFolder as alias, aFolder's files as alias list}
			my getFiles(aFolder)
		end repeat
	end tell
end getFiles

Marc. Thanks for looking at my post and for the suggestion.

It required a little thought for me to understand why your script worked, but it finally dawned on me that the use of the ‘my’ keyword before allFiles was the reason. I hadn’t seen that before (or didn’t understand its purpose).

BTW, the reason I undertook this study was to understand why recursive handlers worked at all. I finally read the following paragraph in the AppleScript Language Guide, which made matters clear and, additionally, explained why memory usage is an issue.

Anyways, the use of a recursive handler when getting the contents of a folder with Finder would seem unnecessary and best avoided. A recursive handler would seem to have a place with System Events, which does not have an ‘entire contents’ property.

You see the benefits of a System Events recursive handler with larger numbers of files and folders: getting Finder’s ‘entire contents’ property grinds slower and slower as the number of items increases.

As a fairly non-scientific test, I created a random structure of folders, sub-folders and files, with a total of 1691 visible items (251 folders, 1440 visible files). Timing wasn’t very scientific either - I just used the stopwatch on my iPhone.

This script:

tell application "Finder" to count (get entire contents of (choose folder))

took about 41 seconds to return 1691, during which time Finder span a beachball and showed “Not responding” in a Force Quit Applications dialog box. It became responsive again after the script had finished running.

This script:


global itemcount
set theFolder to (choose folder)
set itemcount to 0
set countedItems to my countItems(theFolder, itemcount)
on countItems(eachFolder)
	tell application "System Events"
		set theItems to every item of eachFolder
		repeat with eachItem in theItems
			if visible of eachItem is true then set itemcount to itemcount + 1
			if class of eachItem is folder then
				my countItems(eachItem)
			end if
		end repeat
	end tell
	return itemcount
end countItems

took just over four seconds to return the same result, with no noticeable effect on system performance and no obvious hit on memory usage.

Horses for courses, I guess…

I forgot that Nigel and I had participated in a thread in which Nigel used System Events and a recursive handler to get a large quantity of files:

https://macscripter.net/viewtopic.php?id=48810

After reviewing Nigel’s script in the above thread, I modified my System Events script in post 3 above to use Nigel’s approach to filter out hidden files. This script is easily edited to accomplish other goals–for example to filter out all but text files. I reran timing tests with a folder with 10,000 files spread over 4 folders:

System Events script from post 3 with no filter - 263 milliseconds
System Events script from post 3 with whose filter - 194 seconds
System Events script below with repeat-loop filter - 289 milliseconds

The revised script is:

set targetFolder to POSIX path of (choose folder)
set allFiles to getFiles(targetFolder)

on getFiles(aFolder)
	script o
		property theFiles : {}
		on recursivePart(aFolder)
			tell application "System Events"
				set subfolderFiles to POSIX path of every file of folder aFolder
				set subfolderFolders to POSIX path of every folder of folder aFolder
			end tell
			repeat with aFolder in subfolderFolders
				recursivePart(aFolder)
			end repeat
			
			set theFiles to theFiles & subfolderFiles
		end recursivePart
	end script
	o's recursivePart(aFolder)
	
	set TID to text item delimiters
	set text item delimiters to {"/"}
	repeat with i from 1 to (count o's theFiles)
		if text item -1 of (item i of o's theFiles) begins with "." then set (item i of o's theFiles) to missing value
	end repeat
	set text item delimiters to TID
	
	return text of o's theFiles
end getFiles

The above script returns POSIX paths but can be made to return HFS paths by changing four lines as follows:

set targetFolder to (choose folder) as text
set subfolderFiles to path of every file of folder aFolder
set subfolderFolders to path of every folder of folder aFolder
set text item delimiters to {":"}

Hello,

I would like to note that the latest recursive scripts are very fast, but they do not use the full capabilities of System Events. Because using HFS paths for files (and Alias or System Events folder references for folders) instead of Posix paths will give a speed gain of almost one and a half times (about 1.4 times):


set allFiles to getFiles(path to movies folder)


on getFiles(aFolder)
	
	script o -- get all files
		property theFiles : {}
		on recursivePart(aFolder)
			tell application "System Events" to set {subfolderFiles, subfolders} to ¬
				{(path of files of aFolder), (folders of aFolder)}
			repeat with aFolder in subfolders
				recursivePart(aFolder)
			end repeat
			set theFiles to theFiles & subfolderFiles
		end recursivePart
	end script
	
	o's recursivePart(aFolder)
	set {TID, text item delimiters} to {text item delimiters, ":"}
	tell (o's theFiles) to repeat with i from 1 to (count)
		if text item -1 of item i begins with "." then set item i to missing value
	end repeat
	set text item delimiters to TID
	return text of o's theFiles
	
end getFiles
1 Like

KniazidisR. Thanks for posting your script, which works great.

I ran some additional timing tests on a folder that contains 414 files spread over 109 folders and subfolders, and I confirmed your findings. So I modified my script to use HFS paths for folders but to return POSIX paths for files. Then, I further edited this script to return HFS paths. The timing results were:

KniazidisR’s script (HFS paths): 284 milliseconds
Peavine’s script from post 7 (POSIX paths): 466 milliseconds
Peavine’s new script (POSIX paths): 224 milliseconds
Peavine’s new script (HFS paths): 289 milliseconds
ASObjC script (to set a baseline): 87 milliseconds

My new script which returns POSIX paths is:

set targetFolder to (choose folder) as text
set allFiles to getFiles(targetFolder)

on getFiles(aFolder)
	script o
		property theFiles : {}
		on recursivePart(aFolder)
			tell application "System Events"
				set subfolderFiles to POSIX path of every file of folder aFolder
				set subfolderFolders to path of every folder of folder aFolder
			end tell
			repeat with aFolder in subfolderFolders
				recursivePart(aFolder)
			end repeat
			
			set theFiles to theFiles & subfolderFiles
		end recursivePart
	end script
	o's recursivePart(aFolder)
	
	set TID to text item delimiters
	set text item delimiters to {"/"}
	repeat with i from 1 to (count o's theFiles)
		if text item -1 of (item i of o's theFiles) begins with "." then set (item i of o's theFiles) to missing value
	end repeat
	set text item delimiters to TID
	
	return text of o's theFiles
end getFiles

To make this script return HFS paths, two lines need to be changed as follows:

set subfolderFiles to path of every file of folder aFolder
set text item delimiters to {":"}
1 Like