Why the difference, possible script errors?

I had two scripts posted in this thread, post #18, here Count directory depth

I’ll post them here again. Count Files Script:

tell application "Finder" to display dialog ¬
	("The Total Number of Files (Including Subfolders) is " & ¬
		(count (files of (entire contents of (choose folder)))) as string) & ¬
	"." buttons "OK" default button "OK"

Count Folders Script:

tell application "Finder" to display dialog ¬
	("The Total Number of Folders (Including Subfolders) is " & ¬
		(count (folders of (entire contents of (choose folder)))) as string) & ¬
	"." buttons "OK" default button "OK"

Then, just as a learning exercise, I decided to see if ChatGPT would come up with the same solution, and of course, it didn’t. So now I’m trying to figure out which of the scripts is correct. Here are the ChatGPT scripts.

ChatGPT files script:

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

-- Ask the user to select a folder
set selectedFolder to choose folder with prompt "Select a folder to count all files (including subfolders):"
set posixPath to POSIX path of selectedFolder

-- Use the 'find' shell command to count files
set fileCount to do shell script "find " & quoted form of posixPath & " -type f | wc -l"

-- Show the result
display dialog "The folder contains " & fileCount & " files (including all subfolders)." buttons {"OK"} default button "OK"

ChatGPT folders script:

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

-- Ask the user to select a folder
set selectedFolder to choose folder with prompt "Select a folder to count all folders (including subfolders):"
set posixPath to POSIX path of selectedFolder

-- Use the 'find' shell command to count folders
set folderCount to do shell script "find " & quoted form of posixPath & " -type d | wc -l"

-- Show the result
display dialog "The folder contains " & folderCount & " folders (including all subfolders)." buttons {"OK"} default button "OK"

Here’s why I’m asking for help. The original files script comes up with 2932 files in my Documents folder, the ChatGPT comes up with 6411 files in my Documents folder.

The same issue is in the folders scripts. The original folders script comes up with 575 folders in my Documents folder, the ChatGPT comes up with 3525 folders in my Documents folder.

I’m looking for help in possibly identifying any error in either the original scripts, or the ChatGPT scripts. Thanks.

There are two crucial differences between the Finder and the shell command find.

  1. Unlike the shell the Finder doesn’t see invisible items when they are hidden which is the default
  2. Unlike the Finder the shell treats packages (like applications or the Photos library) as folders and counts everything within.

Actually there is a third difference: The shell is several orders of magnitude faster.

Thank you for your response. So, seeing how I’m not interested in counting invisible items, I’ll stick with the Finder scripts. It’s been a good day, I’ve learned something new yet again.

How fast is this for you?

set aFolder to (choose folder) as text
tell application "System Events" to set c to count files of (folder aFolder)
display dialog "The Total Number of Files (Including Subfolders) is " & c & "." buttons "OK" default button "OK"

Robert, when I tried your script and selected my Documents folder, the total number of files was 2. Are you sure your script includes all sub-folders? And I have no idea what the two files could have been. I toggled the invisible files and there were none in the Documents folder.

My script only does files. You can change it to get folders or all items, like so…

set aFolder to (choose folder) as text
tell application "System Events" to set c to count items of (folder aFolder)
display dialog "The Total Number of Files (Including Subfolders) is " & c & "." buttons "OK" default button "OK"

One of the files is probably the .DS_Store file, which is invisible, whether you toggle or not

try this to see what they are…

local flist, visList, c, i
set aFolder to (choose folder) as text
tell application "System Events"
	tell files of folder aFolder
		set {flist, visList} to {name, visible}
	end tell
	set c to 1
	repeat with i from 1 to count flist
		if item i of visList then
			set item i of visList to item c of visList
			set item i of flist to item c of flist
			set c to c + 1
		end if
	end repeat
end tell
if c > (count flist) then
	set flist to {}
else if c > 1 then
	set flist to items c thru -1 of flist
end if
choose from list flist

System Events does consider invisible files (unlike the Finder) but does not consider subfolders (like the Finder).

@Homer712 The two files in your documents folder are the .DS_Store and .localized files which are invisible regardless of the AppleShowAllFiles state.

I checked Google AI, and it also recommended using the find shell command to recursively get all folders and files. For reasons explained by Stefan, this isn’t a good solution in many instances.

I tested the Finder script on my Tahoe computer, and it returned folders in my ~/Library folder when the target folder was my home folder. It’s probably a bad idea to use Finder for this task, so this probably isn’t much of an issue.

An ASObjC script might be used when the number of folders or files is very large. This script returns packages with files but that’s easily changed. The timing result with my smallish home folder was 60 milliseconds. This assumes that the Foundation framework is in memory, which would often be the case.

use framework "Foundation"
use scripting additions

set theFolder to (choose folder)
set theFolder to current application's |NSURL|'s fileURLWithPath:(POSIX path of theFolder)
set folderName to theFolder's lastPathComponent()
set {itemCount, folderCount} to getCount(theFolder)
set dialogText to "The " & quote & folderName & quote & " folder contains " & (itemCount) & " items consisting of " & (itemCount - folderCount) & " files and " & folderCount & " folders."
display dialog dialogText buttons {"OK"} cancel button 1 default button 1

on getCount(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()
	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
	return {folderContents's |count|(), theFolders's |count|()}
end getCount

Peavine, I tested your script against the two original scripts that I posted in my original question, and your script gave me the same exact count in my Documents folder, 2932 files and 575 folders. You always seem to come up with a unique solution, this time combining the file and folder counts. Thanks for posting your script.

You can speed up the script even a bit more by passing the resource keys in the includingPropertiesForKeys parameter which prefetches and caches the values.

… includingPropertiesForKeys:{folderKey, packageKey}

I’m interested in how you got Script Geek to time the Peavine’s script. I entered the script into Script Geek and ended up with an error, please see attachment.

Screenshot

Homer. I edited the script in Script Debugger for timing purposes (see below) I then copied the script in Script Debugger and pasted it into Script Geek. This worked fine. If the script is edited in Script Editor, you may have to compile before copying the script.

use framework "Foundation"
use scripting additions

set theFolder to "/Users/robert/" --set to desired value
set theFolder to current application's |NSURL|'s fileURLWithPath:(POSIX path of theFolder)
set folderName to theFolder's lastPathComponent()
set {itemCount, folderCount} to getCount(theFolder)
# set dialogText to "The " & quote & folderName & quote & " folder contains " & (itemCount) & " items consisting of " & (itemCount - folderCount) & " files and " & folderCount & " folders."
# display dialog dialogText buttons {"OK"} cancel button 1 default button 1

on getCount(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()
	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
	return {folderContents's |count|(), theFolders's |count|()}
end getCount

EDIT: I just copied the script from within the MacScripter site and pasted into Script Geek and that also worked.

Peavine, thank you for the revised script, and, I believe that now I understand why it didn’t work with Script Geek on my first try, correct me if I’m mistaken.

When I first ran the script in Script Geek it required me to make a selection, which I believe was the error. I took a look at your revised script and picked up that the folder selection was hard coded in the script. I just revised your script to Users/homer/Documents and pasted that into Script Geek and it ran perfectly.

So when StefanK said his “smallish” home folder in 60 milliseconds, I suspect that “smallish” must be in the eye of the beholder. My Documents folder ran in 0.166 milliseconds.

Thanks for posting the revised script. And, as I said above, it’s always a good day when I learn something new.

Stefan. Thanks for looking at my script and for the suggestion.

Several years ago I ran some timing tests and prefetching the resource keys made no difference. I retested using the script included below and, once again, saw no difference. I set reqKeys to an array because that’s how Shane handled this in one post I found. I also set reqKeys to a list as you suggest, and that also made no difference.

One enhancement I didn’t use was to set boolean true to a variable outside the repeat loop, and that reduced the timing results by about 8 percent.

use framework "Foundation"
use scripting additions

set theFolder to "/Users/robert/" --set to desired value
set theFolder to current application's |NSURL|'s fileURLWithPath:(POSIX path of theFolder)
set folderName to theFolder's lastPathComponent()
set {itemCount, folderCount} to getCount(theFolder)

on getCount(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 reqKeys to current application's NSArray's arrayWithArray:{folderKey, packageKey}
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:reqKeys options:6 errorHandler:(missing value))'s allObjects()
	set theFolders to current application's NSMutableArray's new()
	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
	return {folderContents's |count|(), theFolders's |count|()}
end getCount

You can speed up this script if you’re using macOS 10.11 or later.
There is a property you can use to narrow the folder content: hasDirectoryPath.
Here is how:

use framework "Foundation"
use scripting additions

set theFolder to (current application's NSURL's fileURLWithPath:"/Applications/")
set fileManager to current application's NSFileManager's defaultManager()
set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()
set thePredicate to current application's NSPredicate's predicateWithFormat:"(self.hasDirectoryPath == true)"
set theResult to (folderContents's filteredArrayUsingPredicate:thePredicate)

set theArray to current application's NSMutableArray's new()
repeat with aResult in theResult
	set {hasResult, isDirectory} to (aResult's getResourceValue:(reference) forKey:"NSURLIsPackageKey" |error|:(missing value))
	if not (isDirectory as boolean) then (theArray's addObject:aResult)
end repeat

{folderContents's |count|(), theResult's |count|(), theArray's |count|()}
1 Like

I edited my earlier script to use the boolean-true optimization and tested this on my home folder. The timing result was 50 milliseconds. I tested ionah’s suggestion on my home folder and the result was 25 milliseconds. Can’t beat that.

Given the approach used in ionah’s script, I had to wonder if there might be a circumstance where it would return incorrect results. In preliminary testing I couldn’t find any.

The same approach could be used to get regular files (not folders and packages) without using a repeat loop. I’ll look into this.

I edited ionah’s script to return regular files, and the timing result on my home folder was 14 milliseconds. The otherwise similar script that used resource value keys to achieve this same result took 58 milliseconds. Thanks ionah.

use framework "Foundation"
use scripting additions

set theFolder to (current application's NSURL's fileURLWithPath:"/Users/robert/")
set fileManager to current application's NSFileManager's defaultManager()
set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()
set thePredicate to current application's NSPredicate's predicateWithFormat:"(self.hasDirectoryPath == false)"
set regularFiles to (folderContents's filteredArrayUsingPredicate:thePredicate)
set regularFilesCount to regularFiles's |count|()

You’re welcome.

Be aware that your last script will not return files that are also packages (like scptd).
You should add them in the predicate format.

1 Like

Just for general forum information, the following is from a Google AI description of packages.

In macOS, a package is a directory (folder) that the Finder presents to the user as a single, opaque file. This prevents the user from accidentally modifying or deleting important internal files that an application or document needs to work correctly. The contents can be viewed by right-clicking the package and selecting “Show Package Contents”…

While the terms are often used interchangeably, “package” and “bundle” have slightly different technical meanings. A package is a file system directory that the Finder displays as a single file to improve the user experience. A bundle is a type of package with a specific, standardized directory structure that contains executable code and its resources.

Some file extensions that are packages include .app, .bundle, .plugin, .kext, .scptd, .rtfd, .pkg, .photoslibrary. Files created by macOS Pages, Numbers, and Keynote apps may also be packages (I’m not sure of this).

The following is an example of a script that works as ionah suggests. The specified file extensions are not case sensitive.

use framework "Foundation"
use scripting additions

set theFolder to current application's NSURL's fileURLWithPath:(POSIX path of (choose folder))
set fileManager to current application's NSFileManager's defaultManager()
set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()
set thePredicates to current application's NSPredicate's predicateWithFormat:"self.hasDirectoryPath == false OR pathExtension.lowercaseString IN {'app', 'scptd', 'pkg'}" --lowercase package extensions
set theFiles to (folderContents's filteredArrayUsingPredicate:thePredicates) as list