Get Folders and Files Recursively with ASObjC

The following script separately returns all folders and files contained within a user-selected folder:

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set {theFolders, theFiles} to getFoldersAndFiles(sourceFolder)

on getFoldersAndFiles(sourceFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set directoryKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set theFiles to ((fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects())'s mutableCopy() -- option 6 skips hidden files and package descendants
	set theFolders to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true
	repeat with anItem in theFiles
		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
	theFiles's removeObjectsInArray:theFolders
	return {theFolders as list, theFiles as list}
end getFoldersAndFiles

The following script is the same as the above except that it only returns folders and files whose names contain a user-specified search string:

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set searchString to text returned of (display dialog "Enter a search string:" default answer "") -- case insensitive
set {theFolders, theFiles} to getFoldersAndFiles(sourceFolder, searchString)

on getFoldersAndFiles(sourceFolder, searchString)
	set fileManager to current application's NSFileManager's defaultManager()
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set directoryKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set theFiles to ((fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects())'s mutableCopy() -- option 6 skips hidden files and package descendants
	set thePredicate to current application's NSPredicate's predicateWithFormat_("lastPathComponent CONTAINS[c] %@", searchString)
	theFiles's filterUsingPredicate:thePredicate
	set theFolders to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true
	repeat with anItem in theFiles
		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
	(theFiles's removeObjectsInArray:theFolders)
	return {theFolders as list, theFiles as list}
end getFoldersAndFiles

The above scripts return a list of file references. Change the last line of the handler to the following to return POSIX paths.

return {(theFolders's valueForKey:"path") as list, (theFiles's valueForKey:"path") as list}

Line 3 below exists in both of the scripts. Insert new lines 1 and 2 above existing line 3 to sort the returned results by path. Replace ā€œpathā€ with ā€œlastPathComponentā€ to sort by folder name and file name.

set theDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:"path" ascending:true selector:"localizedStandardCompare:"
theFiles's sortUsingDescriptors:{theDescriptor}
set theFolders to current application's NSMutableArray's new()

I tested the first script above with large and small folders. The timing results with this script and with a Finder script were:

LOCATION - FOLDERS - FILES - ASObjC RESULT - FINDER RESULT
External SSD - 5264 - 29312 - 995 milliseconds - 39 seconds
Internal SSD - 83 - 389 - 18 milliseconds - 565 milliseconds

The first script in this post was based on a script written by Shane and incorporates suggestions by Nigel and KniazidisR. Thanks for the help.

Sometimes, itā€™s not interesting to just get all the files and directories. I usually strive for automation like ā€œset up and forgotā€. :sweat_smile:
I confess, I donā€™t understand the magic of ā€œ|NSURL|ā€, because my skill are not as powerful as yours.

At the beginning of the year, I wondered about automatically deleting files older than 1 week from the ā€œdownloadā€ directory and found a script to which I made small changes.

(*
Original
https://discussions.apple.com/thread/7230461
*)
use AppleScript version "2.4" -- requires Mac OS X 10.11 (Yosemite)
use scripting additions
use framework "Foundation"
my deleteOlderItems(POSIX path of (path to downloads folder))
on deleteOlderItems(thisPath)
	set thisURL to current application's |NSURL|'s fileURLWithPath:thisPath
	set oldDate to current application's NSDate's dateWithTimeIntervalSinceNow:(-86400 * 7) -- get date of 7 day ago
	set theNSFileManager to current application's NSFileManager's defaultManager()
	set allNSURLs to (theNSFileManager's contentsOfDirectoryAtURL:thisURL includingPropertiesForKeys:{current application's NSURLAddedToDirectoryDateKey} Ā¬
		options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value))
 
	set mylist to {}
	repeat with theUrl in allNSURLs
		-- get the date added
		set {theResult, theValue, theError} to (theUrl's getResourceValue:(reference) forKey:(current application's NSURLAddedToDirectoryDateKey) |error|:(reference))
		-- compare the date
		if theResult and (theValue's compare:oldDate) as integer = -1 then set end of mylist to (theUrl's |path|()) as text as POSIX file as alias
	end repeat
	if mylist is not {} then
		tell application "Finder" to delete mylist
		display notification "ŠžŃ‡ŠøстŠŗŠ° ŠŗŠ°Ń‚Š°Š»Š¾Š³Š° Downloads Š²Ń‹ŠæŠ¾Š»Š½ŠµŠ½Š°.
Š£Š“Š°Š»ŠµŠ½Š¾ Š¾Š±ŃŠŠµŠŗтŠ¾Š²:" & space & (count of mylist)
	end if
end deleteOlderItems

And I created a daemon that runs every day at 10:00 and deletes all old downloaded files. It helps me a lot.

I wrote about it in blog in russian language, but in general everything should be clear there even without an interpreter.

Maybe it will also be useful to some of the forum users. :slight_smile:

Hereā€™s an extra transformation. You can do this:
Ā 

if theResult and (theValue's compare:oldDate) = -1 then set end of mylist to theUrl as alias

Ā 
It also slightly improves speed (by about 15-20%)

1 Like

@V.Yakob. Getting folders and/or files is a common task in AppleScripts, and I wrote my scripts with the idea that they would be used in a larger script that performs some desired task. Perhaps I should have made this clear.

BTW, your script deletes both files and folders in the userā€™s download folder. This may be desired behavior, but it demonstrates why one might want to distinguish folders from files, as is done by my scripts.

@peavine You have great examples. I saved them for myself to take them from the catalog with an example if necessary. I meant that usually need not just a sample of all the files, but a sample with a certain condition or a combination of conditions:

Get all files that were last used a week ago.
Get all files with the specified extension.
Get all files with locked or unlocked.

I have a directory in the iCloud cloud with archives and images of old applications for Mac OS 9, I read that it is a good practice to store these artifacts to install protection. Once I checked the protection box manually, but Iā€™m tired. :roll_eyes:
I may be able to adapt your first script and automate the locking on files with the application library for Mac OS 9.

Your first script ends like this, and does not list files or directories:
{Ā«class ocidĀ» id Ā«data optr0000000070F8D20D00600000Ā», Ā«class ocidĀ» id Ā«data optr0000000060D5D20D00600000Ā»}

What is it? :thinking:

1 Like

@Fredrik71 Thx! It works! In the morning I was inattentive and missed @peavine comments at the bottom of the message.

@V.Yakob. Thanks for mentioning this. I use Script Debugger, which returns the actual URLs, and I forgot that Script Editor does not do this and instead returns a cryptic message. Iā€™ll modify my scripts to return a list of file references as suggested by Fredrik71.

@V.Yakob. I tested your script and it works great.

FWIW, the following script takes a different (but not necessarily better) approach in two respects. First, it uses a somewhat faster method to determine when a file/folder is older than 7 days. Secondly, it uses ASObjC to move old files/folders to the trash.

use framework "Foundation"
use scripting additions

set theFolder to POSIX path of (path to downloads folder)
deleteOlderItems(theFolder)

on deleteOlderItems(theFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set dateKey to current application's NSURLAddedToDirectoryDateKey
	set folderContents to fileManager's contentsOfDirectoryAtURL:theFolder includingPropertiesForKeys:{dateKey} options:4 |error|:(missing value) -- option 4 skips hidden files
	set dateFilter to -86400 * 7 -- must be a negative number
	set deleteCount to 0
	repeat with anItem in folderContents
		set {theResult, aDate} to (anItem's getResourceValue:(reference) forKey:dateKey |error|:(missing value))
		if dateFilter > (aDate's timeIntervalSinceNow as integer) then
			(fileManager's trashItemAtURL:anItem resultingItemURL:(missing value) |error|:(missing value))
			set deleteCount to deleteCount + 1
		end if
	end repeat
	if deleteCount > 0 then display notification "ŠžŃ‡ŠøстŠŗŠ° ŠŗŠ°Ń‚Š°Š»Š¾Š³Š° Downloads Š²Ń‹ŠæŠ¾Š»Š½ŠµŠ½Š°.
Š£Š“Š°Š»ŠµŠ½Š¾ Š¾Š±ŃŠŠµŠŗтŠ¾Š²:" & space & deleteCount
end deleteOlderItems
1 Like

The following script effectively defines a folder as an item without an extension. This is technically incorrect but may do the job when something fast and simple is desired and when the use scenario is tightly defined. As written, the script descends into subdirectories, but this can be changed by substituting option:7 for option:6.

use framework "Foundation"
use scripting additions

set sourceFolder to POSIX path of (choose folder)
set {theFolders, theFiles} to getFoldersAndFiles(sourceFolder)

on getFoldersAndFiles(sourceFolder)
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set fileManager to current application's NSFileManager's defaultManager()
	set theFiles to ((fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects())'s mutableCopy() -- option 6 skips hidden files and package descendants
	(missing value)
	set thePredicate to current application's NSPredicate's predicateWithFormat:"pathExtension == ''"
	set theFolders to (theFiles's filteredArrayUsingPredicate:thePredicate)
	theFiles's removeObjectsInArray:theFolders
	return {theFolders as list, theFiles as list}
end getFoldersAndFiles

Why not simply use find? Itā€™s a built-in command, requires no puritanical at all and comes with a ton of options, as for the age of objects, their size, their type etc? Writing a longish AsObjC script looks a bit like reinventing a perfectly fine wheel here.

@chrillek. I donā€™t believe that a script with the find command will easily do what I want. My script will do all of the following with little or no modification. I would look forward to testing your script with the find command that also does the following:

  • Return a list of folders and a list of files that are contained in a source folder and its subfolders.
  • Include packages in the list of files.
  • Skip hidden files and folders (if desired).
  • Skip files and folders contained within packages.
  • Optionally return file references or URLs or POSIX paths.
  • Optionally return only those folders and files whose names contain a search string or a particular file extension.
  • Sort the returned file and folder lists by their file names, or by their paths, or by both of these with file name taking priority.

BTW, I stated in the title of this thread that my script used ASObjC. I personally prefer to do stuff with ASObjC, which I understand reasonably well. If you prefer the find command Iā€™m fine with that.

find <folder>

As packages are folders, theyā€™re included anyway

Pipe the result of find into a grep or use -iname option with find

Pipe the result of find into grep or sed using an appropriate regular expression

File references are not possible in this context. find returns POSIX paths anyway. Converting them to URL is trivial, just Prozent file:///

Pipe the result of find into grep

Pipe the result of find into sort

Itā€™s all there, and it is all possible with one line of code. Which is not to say that the ASObjC approach is bad. I just find it a bit over-engineered given the CLI tools that are already in place and require no programming at all.

Might well do the trick, but requires additional installation. And then thereā€™s mdfind ā€¦

I wrote the following script with an eye towards brevity. As written, it returns regular files, which excludes packages. Change NSURLIsRegularFileKey to NSURLIsDirectoryKey to return folders and packages. Change NSURLIsRegularFileKey to NSURLIsPackageKey to return packages only.

use framework "Foundation"
use scripting additions

set theFiles to getFiles(choose folder)

on getFiles(sourceFolder) -- sourceFolder requires an alias or file reference
	set fileManager to current application's NSFileManager's defaultManager()
	set regularFileKey to (current application's NSURLIsRegularFileKey) -- does not include packages
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() -- option 6 skips hidden files and package descendants
	set theFiles to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		set {theResult, aRegularFile} to (anItem's getResourceValue:(reference) forKey:regularFileKey |error|:(missing value))
		if aRegularFile as boolean is true then (theFiles's addObject:anItem)
	end repeat
	return theFiles as list -- a list of file references
end getFiles

Iā€™ve never been sure whether to classify packages as files or folders, because in a sense they are both. Four examples of packages on my computer are:

FILE NAME - FILE KIND - COMMENT
Path Copy.app - Application - an AppleScript app
Music Library.musiclibrary - Music Library
8F539E3B-91E5-4ED5-BBD5-928ABB1E332B.rtfd - RTF - contains Stickies documents
Photos Library.photoslibrary - Photos Library

In a nontechnical sense, I would consider the AppleScript app a file and the other packages as folders. I guess the end-purpose of the script will determine how one deals with packages, although I canā€™t think of a circumstance where I would want to alter the contents of a package. My scripts in post 1 include packages as files, but it would be a simple matter to create and return a separate list of packages.

From the point of view of the OS and the filesystem, most packages Iā€™ve seen are folders. Also conceptually, theyā€™re folders, since they contain other files/folders, which a file does not. Ever.
The Finder (or whatever) makes packages appear differently from files, perhaps to hide complexity.

@chrillek I know that many things are easier to do with bash or powershell scripts, but personally Iā€™m more interested in solving automation issues on AppleScript, because itā€™s original for macOS.

I donā€™t know about you, but for me AppleScript is the preferred language for all automation on Mac.

Where this is not possible, I use ā€œdo shell scriptā€, but if over time I manage to solve the issue natively in AppleScript, I always rewriting my decisions.

Forum user @peavine did an excellent job and shared the results on this forum. I think there will be a lot of people who need it.

@Peavine thank you, Iā€™ll test your script of deleting old files.

@Fredrik71 I deleted files manually before, but Iā€™m tired. In the working chat, we often exchange screenshots and other small files that are downloaded to the download catalog. Thatā€™s why I wondered about automating the cleaning of this catalog, initially it seemed easier for me to use Powershell, which is more familiar to me, but I found an AppleScript script on the Apple forum that was able to adapt to my needs.

1 Like

@V.Yakob. Thank you for the kind words. They are greatly appreciated.

Since youā€™re asking: for me, AppleScript just not even a ā€œlanguageā€ given its lack of a formal grammar. Iā€™m coming from Unix, which makes its CL tools a natural choice. Also, because they require a lot less typing then writing AS scripts that handle only one particular case.

And why is Applescript more ā€œoriginalā€ to macOS than the CL tools? Apple didnā€™t do any work on it since ages, itā€™s silently bit-rotting awayā€¦

And then thereā€™s cron to run scripts at certain times

@chrillek Itā€™s a holy war. :slight_smile: A separate topic for discussion is needed about what is more productive, better and simpler.

I like AppleScript, although I donā€™t understand much about it.

However, the AppleScript version changes from time to a new version of macOS.