How do i iterate through every file including folders?

Nigel. I tested your script, and it works great. I ran some timing tests with my test folder, and my ASObjC script was marginally faster, but the difference is not significant. Your script does throw an error when the target folder is my home folder.

Hi peavine.

Thanks for testing my effort.

I suspect the relative speeds depend on the system and/or distribution of files and folders in the hierarchy. On my own machine so far, my script’s faster than the ASObjC one (rather a surprise!) and they’re both faster in Script Editor than in Script Debugger.

Neither of them can handle my own home folder! :lol: Mine goes on forever and eventually beachballs. Yours throws a “Can’t make some data into the expected type.” error after about half a minute (or a minute-and-a-half in SD), the whole of the repeat loop in getFiles() being highlighted. I’ve trapped the error and ascertained that ‘anItem’ is a ‘missing value’ when the error occurs, but haven’t backtracked further to find out why. I’m guessing it’s something to do with the convoluted system of aliases that goes on in some of the Library subfolders.

Thanks Nigel for all the information. This has been an interesting thread.

I had timed our scripts in Script Geek, and I’m surprised by the differing results reported by Script Editor and Script Debugger. With this type of script, I suspect timing results within a reasonable range are not very important.

It’s unfortunate that my ASObjC script wouldn’t run on your computer’s home folder, as a user might want to do this on occasion. However, I’ve spent enough time on this thread, and the OP has no interest in the ASObjC script, so I’ll leave things as they are.

Hi peavine.

I understand. I’m not inclined myself to spend time on the “entire home folder” problem as it almost certainly involves restricted stuff in the user’s Library folder, which I don’t think is of interest to test123testa anyway.

No further feedback from the OP as yet, but I’ve updated my script this morning to save a text file to the desktop like yours does.

Nigel. I was studying your script to better understand its operation, and I changed code lines 1 and 2 below to code lines 3 and 4 below. I then removed the repeat loop that zapped any paths or files that begin with a “.”. Afterwards, the script ran successfully on my home folder, which wasn’t the case before, although its execution time with my standard test folder increased from 320 to 434 milliseconds (with the write file code disabled). Anyways, a user who has a compelling need to get the visible contents of their home folder might consider this option.

-- existing code
set subfolderPaths to path of thisFolder's folders
set fileNames to name of thisFolder's files

-- new code
set subfolderPaths to path of every folder of thisFolder whose visible is true
set fileNames to name of every file of thisFolder whose visible is true

Thanks, peavine. I’m afraid the ‘whose’ filters are giving me timeout errors on large folders — not necessarily in my Library folder. I’ll look into it further at more leisure. Perhaps not this evening. :slight_smile:

Happy Christmas!

Thanks Nigel, and a Happy Christmas to you as well.

Hi peavine.

It turns out that my System Events script can (often!) negotiate my own home folder without needing to check the disk items’ ‘visibility’ properties. But a couple of my subfolder names run foul of a filing system bug — in Mojave, anyway. These folders were attached to an e-mail I received in 2005 and have names which told me where their contents had to go for the purpose being discussed at the time: eg. “put into ~/Library/Contextual Menu Items/”. The HFS paths to these folders should end with either the trailing slash or with both the trailing slash and a colon. But the ‘path’ returned by System Events only has a colon at the end, so it’s incorrect and can’t be used to identify the folders. This appears to be a file system fault rather than a System Events one, as a similar thing occurs with AppleScript’s own files and aliases:

"iMac HD:blah:blah:put into ~/Library/Contextual Menu Items/:" as «class furl»
result as text
--> "iMac HD:blah:blah:put into ~/Library/Contextual Menu Items:" -- (Trailing slash omitted.)

System Events does have its own quirk though. If it’s used to get just the folders’ names instead of their full paths, all the slashes in the names are replaced with colons! It apparently gets what might be called the “POSIX names”.

An unlikely workaround in the case of the paths is to use System Events to get the folders’ ‘POSIX paths’ (which it gets right), sort on those, and then get the HFS ‘paths’ from folder objects specified using the POSIX paths!

tell application "System Events"
	set thisFolder to folder folderHFSPath
	set subfolderPOSIXPaths to POSIX path of thisFolder's folders
	-- [snip]
end tell

-- [snip]

repeat with thisPOSIXPath in subfolderPOSIXPaths
	set thisPOSIXPath to thisPOSIXPath's contents
	tell application "System Events" to set subfolderHFSPath to path of folder thisPOSIXPath
	recurse(subfolderHFSPath)
end repeat

I’ve modified the script in post #9 accordingly, but it’s probably overkill for the OP’s purposes.

Nigel. I tested your revised script on a documents folder and my home folder, and it worked great in both instances. The workaround is an interesting one. I agree these scripts will probably be of no use to the OP, but I suspect other forum members and googlers will find these scripts to be helpful in the future.

I had posted a script here, but it did not sort correctly. I have revised my script in post 4, and it works as expected.

I always use “System Events” over Finder.
I find it to be more reliable.

Try this routine on your large folder and see if it craps out.


set folderTree to getFolderTree()

on getFolderTree()
	local theFolders
	set theFolder to (choose folder)
	set folderTree to {theFolder as text}
	
	tell application "System Events"
		set theFiles to (name of every file in theFolder)
		if theFiles ≠ {} then set the end of folderTree to theFiles
		
		try
			set theFolders to (every folder of the theFolder) as list
		on error
			display alert "No folders were found in the selected folder"
			error number -128
		end try
		
		repeat with aFolder in theFolders
			set contents of aFolder to path of aFolder
		end repeat
		--set theFolders to sortFolders(theFolders) of me -- disable to change folder sort
		sortFolders(theFolders) of me
		--=theFolders
		repeat with aFolder in theFolders
			set end of folderTree to linefeed & aFolder
			set theFiles to name of every file in folder aFolder
			if theFiles ≠ {} then set the end of folderTree to theFiles
		end repeat
	end tell
	
	set {ATID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, linefeed}
	set folderTree to folderTree as text
	set AppleScript's text item delimiters to ATID
	return folderTree
end getFolderTree

on sortFolders(a)
	repeat with i from (count a) to 2 by -1
		repeat with j from 1 to i - 1
			if item j of a > item (j + 1) of a then
				set {item j of a, item (j + 1) of a} to {item (j + 1) of a, item j of a}
			end if
		end repeat
	end repeat
	--return a (Not needed since routine sorts the list in-place which was passed by reference by default)
end sortFolders

It only returned folder/files one level deep, and it returned hidden files (mainly .DS_Store).

Here is a recursive version (much neater)

Try this one


on run
	local theFolder
	set theFolder to (choose folder) as text
	set folderTree to getFolderTree(theFolder)
end run

on getFolderTree(theFolder)
	local theFolders, folderTree, theFiles
	set folderTree to (theFolder as text) & linefeed
	set {ATID, text item delimiters} to {text item delimiters, linefeed}
	tell application "System Events"
		set theFiles to (name of every file in folder theFolder whose visible is true)
		sortFolders(theFiles) of me
		if theFiles ≠ {} then set folderTree to folderTree & theFiles as text
		try
			set theFolders to (path of every folder of folder theFolder) --as list
		on error
			tell me to display alert "No folders were found in the selected folder"
			error number -128
		end try
	end tell
	set folderTree to folderTree & linefeed & linefeed
	sortFolders(theFolders)
	repeat with aFolder in theFolders
		set folderTree to folderTree & getFolderTree(contents of aFolder)
	end repeat
	set text item delimiters to ATID
	return folderTree
end getFolderTree

on sortFolders(a)
	repeat with i from (count a) to 2 by -1
		repeat with j from 1 to i - 1
			if item j of a > item (j + 1) of a then
				set {item j of a, item (j + 1) of a} to {item (j + 1) of a, item j of a}
			end if
		end repeat
	end repeat
end sortFolders

I tested the script on my test folder and it worked fine. The timing result was 320 milliseconds.

I was trying my script above with a folder that had over 9000 files.
It took forever and I had to increase the timeout value so that System Events would return the file list.
It seems that the “whose” clause really slows down the result.
If I remove the whose clause, it responded almost instantaneously.
So I decided to parse the list using AppleScript to remove invisible files from the result.
It is much faster!

Also the sort routine was extremely slow, so I replaced it with a combSort which is way faster than the bubble sort that was used before.


on run
	 local theFolder
	 set theFolder to (choose folder) as text
	 set folderTree to getFolderTree(theFolder)
	 saveFolderTree(folderTree)
end run

on getFolderTree(theFolder)
	local theFolders, folderTree, theFiles, visList
	set folderTree to (theFolder as text) & linefeed
	set {ATID, text item delimiters} to {text item delimiters, linefeed}
	try
		tell application "System Events"
			set {theFiles, visList} to {name, visible} of files in folder theFolder --whose visible is true
		end tell
	on error
		return ""
	end try
	repeat with i from (count of theFiles) to 1 by -1
		if item i of visList is false then
			set item i of theFiles to item 1 of theFiles
			set item i of visList to item 1 of visList
			set theFiles to rest of theFiles
			set visList to rest of visList
		end if
	end repeat
	set visList to missing value
	combSort(theFiles)
	if theFiles ≠ {} then set folderTree to folderTree & theFiles as text
	try
		tell application "System Events"
			set {theFolders, visList} to {path, visible} of folders of folder theFolder
		end tell
	on error
		tell me to display alert "No folders were found in the selected folder"
		error number -128
	end try
	repeat with i from (count of theFolders) to 1 by -1
		if item i of visList is false then
			set item i of theFolders to item 1 of theFolders
			set item i of visList to item 1 of visList
			set theFolders to rest of theFolders
			set visList to rest of visList
		end if
	end repeat
	set folderTree to folderTree & linefeed & linefeed
	combSort(theFolders)
	repeat with aFolder in theFolders
		set folderTree to folderTree & getFolderTree(contents of aFolder)
	end repeat
	set text item delimiters to ATID
	return folderTree
end getFolderTree

on saveFolderTree(theText)
	set theFile to (path to desktop as text) & "Folder Contents.txt"
	try
		set fnum to open for access file theFile with write permission
	on error
		display alert "Oops!"
		return
	end try
	set eof fnum to 0
	write theText to fnum starting at 0
	close access fnum
end saveFolderTree

on combSort(alist as list) -- FASTEST
	local i, j, cc, sf, ns, js, gap, pgap, sw -- ns means No Swap
	script mL
		property nlist : alist
	end script
	set sf to 1.5
	set cc to count mL's nlist
	set gap to cc div sf
	repeat until gap = 0
		repeat with i from 1 to gap
			set js to cc - gap
			repeat until js < 1 -- do each gap till nor more swaps
				set ns to gap
				repeat with j from i to js by gap
					if (item j of mL's nlist) > (item (j + gap) of mL's nlist) then
						set sw to (item j of mL's nlist)
						set (item j of mL's nlist) to (item (j + gap) of mL's nlist)
						set (item (j + gap) of mL's nlist) to sw
						set ns to j
					end if
				end repeat
				set js to ns - gap
			end repeat
		end repeat
		set pgap to gap
		set gap to gap div sf
		if gap = 0 then -- no while using as integer
			if pgap ≠ 1 then set gap to 1
		end if
	end repeat
end combSort

If you don’t mind using script libraries, this can be done with Shane’s fileManagerLib.

https://latenightsw.com/freeware/

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"

use script "FileManagerLib" version "2.3.5"
on main()
	set theFolder to POSIX path of (choose folder)
	
	set filesFolders to objects of theFolder ¬
		searching subfolders true ¬
		include invisible items false ¬
		include folders true ¬
		include files true ¬
		result type files list
	set AppleScript's text item delimiters to {return}
	set theText to filesFolders as text
	saveFolderTree(theText)
end main

on saveFolderTree(theText)
	set theFile to (current application's NSHomeDirectory()'s stringByAppendingPathComponent:"Desktop")'s stringByAppendingPathComponent:"Folder Contents.txt"
	(current application's NSString's stringWithString:theText)'s writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end saveFolderTree

main()

This script is similar to my script in post 4 above, differing in that it only returns file names that contain a specified search string. The script creates a text file on the desktop with the matching files.

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder with prompt "Select a source folder:" default location (path to home folder))
set searchString to text returned of (display dialog "Enter a search string:" default answer "")
set theFiles to getFiles(sourceFolder, searchString)
if theFiles's |count|() = 0 then display dialog "No file names containing the search string were found." buttons {"OK"} cancel button 1 default button 1 with icon stop
set foldersAndFiles to getFoldersAndFiles(theFiles)
saveFoldersAndFiles(foldersAndFiles, sourceFolder)

on getFiles(sourceFolder, searchString)
	set fileManager to current application's NSFileManager's defaultManager()
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set theKeys to {current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey}
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:theKeys options:6 errorHandler:(missing value))'s allObjects()
	set thePredicate to current application's NSPredicate's predicateWithFormat_("lastPathComponent CONTAINS[c] %@", searchString)
	set folderContents to folderContents's filteredArrayUsingPredicate:thePredicate
	set theFolders to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		set {theResult, aDirectory} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(missing value))
		if aDirectory as boolean is true then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(missing value))
			if aPackage as boolean is false then (theFolders's addObject:anItem)
		end if
	end repeat
	set theFiles to folderContents's mutableCopy()
	theFiles's removeObjectsInArray:theFolders
	set theDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"path" ascending:true selector:"localizedStandardCompare:"
	return (theFiles's sortedArrayUsingDescriptors:{theDescriptor})
end getFiles

on getFoldersAndFiles(theFiles)
	set foldersAndFiles to current application's NSMutableArray's new()
	set oldFolder to (theFiles's objectAtIndex:0)'s URLByDeletingLastPathComponent()
	(foldersAndFiles's addObject:(oldFolder as text))
	repeat with aFile in theFiles
		set newFolder to aFile's URLByDeletingLastPathComponent()
		if not (newFolder's isEqual:oldFolder) then (foldersAndFiles's addObject:(linefeed & (newFolder as text)))
		(foldersAndFiles's addObject:(aFile's lastPathComponent()))
		set oldFolder to newFolder's |copy|()
	end repeat
	return foldersAndFiles's componentsJoinedByString:linefeed
end getFoldersAndFiles

on saveFoldersAndFiles(theText, theFolder)
	set theFolder to (current application's NSString's stringWithString:theFolder)'s lastPathComponent()
	set fileName to theFolder's stringByAppendingString:" Folder Contents.txt"
	set theFile to (current application's NSHomeDirectory()'s stringByAppendingPathComponent:"Desktop")'s stringByAppendingPathComponent:fileName
	theText's writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end saveFoldersAndFiles

In order not to make users guess about your intentions, please be more clear. It is not clear in what form you want to get the hierarchy structure. I will give examples of saving the hierarchy as tabbed text.

I did some clean-up and optimization of my script in post 4. The timing results were 9 milliseconds when run on a folder containing 99 files in 10 folders and 110 milliseconds when run on my home folder.

use framework "Foundation"
use scripting additions

on main()
	set theFolder to POSIX path of (choose folder)
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	try
		set theFolders to getFolders(theFolder)
		set folderTree to getFolderTree(theFolders)
	on error
		display dialog "An error occurred while getting files" buttons {"OK"} cancel button 1 default button 1
	end try
	saveFolderTree(folderTree, theFolder)
end main

on getFolders(theFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set folderKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()
	set theFolders to current application's NSMutableArray's new()
	(theFolders's addObject:theFolder)
	repeat with anItem in folderContents
		set {theResult, aFolder} to (anItem's getResourceValue:(reference) forKey:folderKey |error|:(missing value))
		if aFolder as boolean is true then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
			if aPackage as boolean is false then (theFolders's addObject:anItem)
		end if
	end repeat
	set theDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"path" ascending:true selector:"localizedStandardCompare:"
	return (theFolders's sortedArrayUsingDescriptors:{theDescriptor})
end getFolders

on getFolderTree(theFolders)
	set fileManager to current application's NSFileManager's defaultManager()
	set folderTree to current application's NSMutableArray's new()
	repeat with aFolder in theFolders
		set aFolderPath to ((aFolder's |path|())'s stringByAppendingString:"/")
		(folderTree's addObject:aFolderPath)
		(folderTree's addObjectsFromArray:getFiles(aFolder, theFolders, fileManager))
		(folderTree's addObject:"")
	end repeat
	return (folderTree's componentsJoinedByString:linefeed)
end getFolderTree

on getFiles(theFolder, theFolders, fileManager)
	set theFiles to (fileManager's contentsOfDirectoryAtURL:theFolder includingPropertiesForKeys:{} options:4 |error|:(missing value))'s mutableCopy()
	theFiles's removeObjectsInArray:theFolders
	return ((theFiles's valueForKey:"lastPathComponent")'s sortedArrayUsingSelector:"localizedStandardCompare:")
end getFiles

on saveFolderTree(theString, theFolder)
	set fileName to (theFolder's lastPathComponent)'s stringByAppendingString:" Folder Contents.txt"
	set theFile to (current application's NSHomeDirectory()'s stringByAppendingPathComponent:"Desktop")'s stringByAppendingPathComponent:fileName
	(current application's NSString's stringWithString:theString)'s writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end saveFolderTree

main()

I rewrote the above script utilizing a somewhat different approach. I thought it might be faster, but it was actually about 10 percent slower. It does not return empty folders and might be considered for this reason.

use framework "Foundation"
use scripting additions

on main()
	set theFolder to POSIX path of (choose folder)
	set theFiles to getFiles(theFolder)
	if theFiles's |count|() is 0 then display dialog "No files found" buttons {"OK"} cancel button 1 default button 1
	set folderTree to getFolderTree(theFiles)
	saveFolderTree(folderTree, theFolder)
end main

on getFiles(theFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set directoryKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() --option 6 skips hidden files and package contents
	set theFolders to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true
	repeat with anItem in folderContents
		set {theResult, aDirectory} to (anItem's getResourceValue:(reference) forKey:directoryKey |error|:(missing value))
		if aDirectory is booleanTrue then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
			if not (aPackage is booleanTrue) then (theFolders's addObject:anItem)
		end if
	end repeat
	set theFiles to folderContents's mutableCopy()
	theFiles's removeObjectsInArray:theFolders
	set pathDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"stringByDeletingLastPathComponent" ascending:true selector:"localizedStandardCompare:"
	set nameDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"lastPathComponent" ascending:true selector:"localizedStandardCompare:"
	return (theFiles's valueForKey:"path")'s sortedArrayUsingDescriptors:{pathDescriptor, nameDescriptor}
end getFiles

on getFolderTree(theFiles)
	set fileTree to current application's NSMutableArray's new()
	set previousFolder to (theFiles's objectAtIndex:0)'s stringByDeletingLastPathComponent()
	fileTree's addObject:(previousFolder's stringByAppendingString:"/")
	repeat with aFile in theFiles
		set aFolder to aFile's stringByDeletingLastPathComponent()
		set aFileName to aFile's lastPathComponent()
		if (aFolder's isEqualToString:previousFolder) is false then
			(fileTree's addObject:"")
			(fileTree's addObject:(aFolder's stringByAppendingString:"/"))
			(fileTree's addObject:aFileName)
		else
			(fileTree's addObject:aFileName)
		end if
		set previousFolder to aFolder
	end repeat
	return fileTree
end getFolderTree

on saveFolderTree(theString, theFolder)
	set theString to theString's componentsJoinedByString:linefeed
	set fileName to ((current application's NSString's stringWithString:theFolder)'s lastPathComponent())'s stringByAppendingString:" Folder Contents.txt"
	set theFile to ((current application's NSHomeDirectory())'s stringByAppendingPathComponent:"Desktop")'s stringByAppendingPathComponent:fileName
	(current application's NSString's stringWithString:theString)'s writeToFile:theFile atomically:true encoding:(current application's NSUTF8StringEncoding) |error|:(missing value)
end saveFolderTree

main()