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
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
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.
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 {":"}
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
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 {":"}