Loading file list from a folder

Launch Service has a C-style API, not Objective-C, so it’s out of reach of ASObjC.

I’m going back over the code I’ve extracted from the previous posts and would like to ask a question about the variables passed from AppleScript to ObjC routines.

In some code I see a step that converts AppleScript strings to “Cocoa” strings. I believe that this has been used to indicate a conversion to what the Language Reference calls, (NSString *)string

set cocoaString to current application's NSString's stringWithString:theFilename

However; I don’t see this conversions being done in all code examples, nor for other types of AppleScript variables.

I suspect the answer lies in some form of casting of certain AppleScript data types that is going on, but I’m still learning and don’t know for sure.

Can one of the skilled coders shed some light on this?

Meanwhile, I’m also going to go back and read “Everyday AppleScriptObjC” again and see if I can figure out why.

Ok. I’ve gone over the relevant section in Shane’s book and understand that some conversions are done as a result of “Bridging”. (Which I percieved as being “casting”. Close but not the same)

So; I think I can push on with analyzing the code myself.

I would like to ask one question, because the XCode documentation seems to have changed. (I am using the documentation from Version 10.1 (10B61))

The code in one of the previous post entries uses:

+ (NSURL *)fileURLWithPath:(NSString *)path;

e.g.

set sourceAliasOrFile to current application's class "NSURL"'s fileURLWithPath:(cocoaPathString)

The current XCode documentation defines the “same” function as:

class func fileURL(withPath path: String) -> URL

Does this mean that, while old form may still work, it really should be written as:

set sourceAliasOrFile to current application's class "NSURL"'s fileURL(cocoaPathString)

Because, it doesn’t appear to work and returns an error.

The “function” is Swift code, not Objective-C.

Thanks, Shane. I had a look and can see that now. The new XCode documentation is quite different looking from what you described in your book. (No fault on your part. Things get updated by Apple.)
So; I’m just going to have to make sure that, after I do a search, I remember to select the ObjC button.

(I’m really starting to like your book. It takes you through things step by step and you seem to have anticipated many of the questions one might have. So far; between it, the XCode documentation and the Script Debugger, I’ve managed to get almost all the data types aligned in my project. That, and adding copious comments, has made it far easier to follow.)

If I’ve understood your question about why some “conversions” are done explicitly and some simply “bridged”, the bridged ones are invariably parameters to the “methods” used. If an AppleScript object’s passed to a method, and the bridge knows how to convert it to the type the method expects, bridging takes place.

The methods themselves don’t belong to the Objective-C language, but either to particular classes of object or to individual instances (objects) of those classes. So if you want to use “instance” methods, you have to create the instances first, which you do using suitable “class” methods.

In your example in post #63

set cocoaString to current application's NSString's stringWithString:theFilename

stringWithString: is an NSString class method which creates an NSString with the same value as another NSString. If the parameter theFilename is AppleScript text, it’s bridged to NSString at the input to the method because an NSString is what the method expects. So this trick can be used to create an NSString version of an AS text. The NSString to which cocoaString’s set is presumably a copy of the instance created by the bridge. You can use its instance methods to return information about it or to derive other data from it.

Thanks, Nigel. You understood what I was driving at.

I think I’m starting to get the hang of this now.

There’s so much to learn and I really appreciate you and Shane sharing what you know.

Just a thought…
I hope one of you “Code Gurus” (when time permits) will do a tutorial in the “unscripted” section about “The need for speed or how to optimize your code”. This topic needs someone with a wealth of experience with what works and what doesn’t. It’s pretty hard to glean that information from individual posts.

Cheers;
Gary

I started to write such an article years ago, but got bogged down in the details. :wink:

Basically, optimisation for speed involves developing an understanding that:

  1. Things take a certain amount of time to do.
  2. Some things take longer than others.
  3. Some agents do things faster than others.
  4. Commands to external agents take a certain amount of time to be transmitted and acknowledged.
  5. Commands to external agents are one-offs to those agents they’ll have their own housekeeping to do in connection with each one.
  6. Every command and operator written into a script actually does something.
  7. Doing things in a certain order can render some of them unnecessary (actually or statistically) or make less work for the processor.
  8. AppleScript has certain quirks.

It’s important not to lose sight of the fact that speed comes only a close second to the script achieving the desired result faultlessly. Many professional scripters don’t have time to look into shaving an odd second or two off a script’s running time (or say they don’t) before delivering it to their client. It’s also desirable that code be written in such a way that anyone seeking to understand it — including the original author months later — can do so with the minimum of effort.

Twenty years ago, if you wanted to use AppleScript, you had to learn the language and do the best you could with it. Nowadays, computers are very much faster and many of the old minor optimisations are practically pointless. The AppleScript philosophy that it can be used by anyone from home users to professionals has been expanded by the ability to run shell scripts and/or to call system Objective-C methods directly. Many home users will be happy just to have written something that does what they want. Many professionals will come to AppleScript already familiar with shell commands (and the languages that can be used in them) or with Objective-C. These people may be happier using what they already know and only learn enough AppleScript to allow them to do so. It could be just we enthusiasts who are still interested in the actual efficiency of our scripts. :wink:

Nigel;

Thanks for your thoughts. I understand what you mean. And, within limits, I likely won’t worry about a few seconds.

That said; I’ve found that there are certain “rules-of-thumb” that result in faster execution. I guess I’ll just try different things and see what works. I don’t expect to find a code optimizer for AppleScriptObjC, but I sure wish there were one. :>)

Regards;
Gary

Nigel,

Thank you for sharing this code (at bottom of post).

Is there a way to have this list files inside folders that are inside the selected top level folder?

So, given a folder structure like this:

Source Folder:
01:
01. 2019-08-14 Membership Interest Purchase Agreement.pdf
02-03:
02. 2019-08-14 Promissory Note.pdf
03. 2019-08-14 Ken Phillips resignation.pdf
05-06:
05. ACACentral.pdf
06. BVA - BCA Valuation 8.7.19 DRAFT 8.14.19.pdf
07:
04. BCA.pdf

The list should be returned in this order:
01. 2019-08-14 Membership Interest Purchase Agreement.pdf
02. 2019-08-14 Promissory Note.pdf
03. 2019-08-14 Ken Phillips resignation.pdf
05. ACACentral.pdf
06. BVA - BCA Valuation 8.7.19 DRAFT 8.14.19.pdf
04. BCA.pdf

This is what I’ve been using in my scripts:

property kFileList : {}

set kFileList to {}

with timeout of 360 seconds
	try
		
		tell application "System Events"
			set source_folder to choose folder with prompt "Please select directory."
			my createList(source_folder)
		end tell
		
	end try
	
end timeout

on createList(mSource_folder)
	set item_list to ""
	
	tell application "System Events"
		set item_list to get the name of every disk item of mSource_folder
	end tell
	
	set item_count to (get count of items in item_list)
	
	repeat with i from 1 to item_count
		set the_properties to ""
		
		set the_item to item i of the item_list
		set the_item to ((mSource_folder & the_item) as string) as alias
		
		set file_info to get info for the_item
		
		
		if visible of file_info is true then
			set file_type to name extension of (info for the_item)
			if file_type is equal to "pdf" then
				
				--set file_name to displayed name of file_info
				set end of kFileList to the_item
				
			end if
			if folder of file_info is true then
				my createList(the_item)
			end if
		end if
		
	end repeat
end createList

But, after 10.12, the list is returned in a seemingly random order, even if everything lives only in the top level folder.

I know I found that createList code somewhere here-I just didn’t document where (which I’ve done extensively since I first wrote that script) and haven’t found it in searching, at least in the form I’m using. The closest seems to be here: https://macscripter.net/viewtopic.php?id=33619

I thought about adding a sort routine to order the list from my code, but that wouldn’t always be appropriately ordered (as in my example where 04… is the last item, not the 4th).

Suggestions, comments, better solutions?

Thanks for your help.

Cheers,
Jon

AppleScript has always provided lists of files in the order provided to it. What’s changed is that, unlike HFS, APFS does not return names in sorted order.

Shane,

Thank you for the clarification. I thought it was a 10.14 behavior change, but you’re spot on, the source is on an APFS volume. I haven’t tried an AFP or SMB source for comparison, yet.

Going back to Nigel’s code, while I understand some of the workings, I’m not fully conversant on how to adapt it to my needs, if that can be done.

Thanks to everyone who contributes here.

Cheers,
Jon

Hi.

My approach would be to get the full paths to the PDFs, sort those, and then extract the file names. Here’s a hybrid System Events/ASObjC handler. Fully ASObjC would probably be faster, but this is quite interesting:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

set source_folder to choose folder with prompt "Please select directory."
set kFileList to createList(source_folder)

on createList(mSource_folder)
	script o
		property filePaths : {}
		
		-- Recursive handler to build the file-path list.
		on getFiles(srcFolder)
			tell application "System Events"
				-- Concatenate the current folder's file paths to the list.
				set filePaths to filePaths & POSIX path of files of srcFolder
				-- Get the subfolders and call this handler again to get the files from those.
				set subfolders to folders of srcFolder
				repeat with thisSubfolder in subfolders
					my getFiles(thisSubfolder)
				end repeat
			end tell
		end getFiles
	end script
	
	-- Get a list of POSIX paths to all the files in the folder's hierarchy.
	tell o to getFiles(mSource_folder)
	-- Convert it to an NSArray so that we can use ObjC methods.
	set pathArray to current application's class "NSMutableArray"'s arrayWithArray:(o's filePaths)
	-- Filter case-insensitively for paths with a "pdf" extension.
	set PDFPred to current application's class "NSPredicate"'s predicateWithFormat:("pathExtension ==[c] 'pdf'")
	tell pathArray to filterUsingPredicate:(PDFPred)
	-- Sort the PDF paths Finder-style.
	tell pathArray to sortUsingSelector:("localizedStandardCompare:")
	
	-- Return the file names from the sorted paths as an AS list.
	return (pathArray's valueForKey:("lastPathComponent")) as list
end createList

This works, keeping the order, searching in subfolders, and giving files list instead of its base names list:


use AppleScript version "2.5"
use framework "Foundation"
use script "FileManagerLib" version "2.2.2"
-- <https://www.macosxautomation.com/applescript/apps/FileManagerLib_stuff.zip>
use scripting additions

property NSPredicate : a reference to NSPredicate of current application
property NSMutableArray : a reference to NSMutableArray of current application

set theRootFolder to choose folder with prompt "Please select directory."
set theRootFolderPosixPath to POSIX path of theRootFolder

set folderContents to objects of theRootFolderPosixPath ¬
	searching subfolders true ¬
	include invisible items false ¬
	include folders true ¬
	with include files

set pathArray to NSMutableArray's arrayWithArray:folderContents
set PDFPred to NSPredicate's predicateWithFormat:("pathExtension ==[c] 'pdf'")
pathArray's filterUsingPredicate:PDFPred
pathArray's sortUsingSelector:("localizedStandardCompare:")

set theFiles to {}
repeat with theItem in pathArray
	set theFiles to theFiles & ((theItem as text) as POSIX file)
end repeat

And here’s yet another variation. :slight_smile: It’s essentially the same as KniazidisR’s, but it returns just the files’ names as before, doesn’t use a third-party library, and double-checks that none of the items with “pdf” extensions are folders. (KniazidisR’s script can be made to exclude folders explicitly too by changing the include folders parameter value to false in his set folderContents … line.)

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

set source_folder to choose folder with prompt "Please select directory."
set kFileList to createList(source_folder)

on createList(mSource_folder)
	set |⌘| to current application
	
	set sourceURL to |⌘|'s class "NSURL"'s URLWithString:(POSIX path of mSource_folder)
	-- Get the folder's entire contents as a URL array.
	set theManager to |⌘|'s class "NSFileManager"'s defaultManager()
	set fileKey to |⌘|'s NSURLIsRegularFileKey
	set searchOptions to (get |⌘|'s NSDirectoryEnumerationSkipsPackageDescendants) + (get |⌘|'s NSDirectoryEnumerationSkipsHiddenFiles)
	set entireContents to (theManager's enumeratorAtURL:(sourceURL) includingPropertiesForKeys:({fileKey}) options:(searchOptions) errorHandler:(missing value))'s allObjects()
	
	-- Filter case-insensitively for items with "pdf" extensions.
	set PDFPred to |⌘|'s class "NSPredicate"'s predicateWithFormat:("pathExtension ==[c] 'pdf'")
	set URLArray to entireContents's filteredArrayUsingPredicate:(PDFPred)
	-- The result is probably just the files we want, but check for any folders among them while getting their paths.
	set pathArray to |⌘|'s class "NSMutableArray"'s new()
	repeat with thisURL in URLArray
		if ((thisURL's getResourceValue:(reference) forKey:(fileKey) |error|:(missing value))'s end as boolean) then
			tell pathArray to addObject:(thisURL's |path|())
		end if
	end repeat
	-- Sort the qualifying paths Finder-style.
	tell pathArray to sortUsingSelector:("localizedStandardCompare:")
	
	-- Return the file names from the sorted paths as an AS list.
	return (pathArray's valueForKey:("lastPathComponent")) as list
end createList

Ok, Nigel. It is OK that your script doesn’t use a third-party library. And you can return file’s names instead of files, but why double-checking? I know, that folders doesn’t have “pdf” extensions. So, no need double-checking. And I do not specifically filter folders at the beginning, as this speeds up the search in subfolders several times.

If you change the result type of the objects of command to urls array you don’t need to then create an array. It saves the library converting an array of URLs into a list of «class furl» which you then just convert back.

It’s very unlikely, that’s true. :slight_smile: But not impossible. I decided to put in a check just in case, combining this with the initial building of the mutable array.

I’m not surprised. The only way in ASObjC (which both Shane’s library and my script use) to differentiate between folders, files, and other types in an array of NSURLs is with an AS repeat. This has to check the relevant resource value(s) in each individual URL and build a new collection of items which pass the test. If you leave this out, it can save some time.

My script filters for “pdf” extensions before testing the resource values, so the number of items in the AS repeat is reduced — although by how much obviously depends on how many items are eliminated by the “pdf” filter. You could do something similar in your script, although it might take fractionally longer as the resource values won’t have been cached when the URLs were fetched:


use AppleScript version "2.5"
use framework "Foundation"
use script "FileManagerLib" version "2.2.2"
-- <https://www.macosxautomation.com/applescript/apps/FileManagerLib_stuff.zip>
use scripting additions

property NSPredicate : a reference to NSPredicate of current application
property NSMutableArray : a reference to NSMutableArray of current application

set theRootFolder to choose folder with prompt "Please select directory."
set theRootFolderPosixPath to POSIX path of theRootFolder

set folderContents to objects of theRootFolderPosixPath ¬
	result type urls array ¬
	with searching subfolders, include folders and include files without include invisible items

set URLArray to folderContents's mutableCopy()

-- Filter for items with "pdf" extensions.
set PDFPred to NSPredicate's predicateWithFormat:("pathExtension ==[c] 'pdf'")
URLArray's filterUsingPredicate:PDFPred
-- Remove any non-files from the array.
set fileKey to current application's NSURLIsRegularFileKey
repeat with i from (count URLArray) to 1 by -1
	set thisURL to item i of URLArray
	if not ((thisURL's getResourceValue:(reference) forKey:(fileKey) |error|:(missing value))'s end as boolean) then
		(URLArray's removeObjectAtIndex:(i - 1))
	end if
end repeat
-- Sort the remaining URLs on their paths.
set sortDescriptor to current application's NSSortDescriptor's sortDescriptorWithKey:("path") ascending:(true) selector:("localizedStandardCompare:")
URLArray's sortUsingDescriptors:({sortDescriptor})

set theFiles to URLArray as list

Thanks, Shane. Thanks, Nigel. I removed everything unnecessary. My final script:


use AppleScript version "2.5"
use framework "Foundation"
use script "FileManagerLib" version "2.2.2"
-- <https://www.macosxautomation.com/applescript/apps/FileManagerLib_stuff.zip>
use scripting additions

property NSPredicate : a reference to NSPredicate of current application
property NSSortDescriptor : a reference to NSSortDescriptor of current application

set theRootFolder to choose folder with prompt "Please select directory."
set theRootFolderPosixPath to POSIX path of theRootFolder

set URLArray to ¬
	objects of theRootFolderPosixPath ¬
		searching subfolders true ¬
		include invisible items false ¬
		include folders true ¬
		include files true ¬
		result type urls array

set PDFPred to NSPredicate's predicateWithFormat:("pathExtension ==[c] 'pdf'")
URLArray's filterUsingPredicate:PDFPred

set sortDescriptor to NSSortDescriptor's sortDescriptorWithKey:("path") ascending:(true) selector:("localizedStandardCompare:")
URLArray's sortUsingDescriptors:({sortDescriptor})

set theFiles to URLArray as list

Hi, everyone.

My script above works fine, but I would like the user to have a choice without using third-party libraries. In addition, Nigel tried to do this above, but didn’t quite finish the script to the end; his scripts return Posix paths instead of files. And his last script was very close to the goal, but unfortunately, it throws an error when sorting.

I only needed to make 1-2 changes for his version (which is now better from my) to work perfect. 1) It does not use a third-party library, 2) it filters out package files and 3) it filters out “strange” folders with “extensions”.


use framework "Foundation"
use scripting additions

property |⌘| : a reference to current application
property NSPredicate : a reference to NSPredicate of |⌘|
property NSFileManager : a reference to NSFileManager of |⌘|
property |NSURL| : a reference to |NSURL| of |⌘|
property NSMutableArray : a reference to NSMutableArray of |⌘|
property NSURLIsRegularFileKey : a reference to NSURLIsRegularFileKey of |⌘|
property NSSortDescriptor : a reference to NSSortDescriptor of |⌘|
property NSDirectoryEnumerationSkipsPackageDescendants : a reference to 2
property NSDirectoryEnumerationSkipsHiddenFiles : a reference to 4

set sourceFolder to POSIX path of (choose folder with prompt "PLEASE, SELECT FOLDER")
set sourceURL to |NSURL|'s URLWithString:sourceFolder

set fileManager to NSFileManager's |defaultManager|()
set fileKey to NSURLIsRegularFileKey
set searchOptions to (NSDirectoryEnumerationSkipsPackageDescendants) + (NSDirectoryEnumerationSkipsHiddenFiles)

-- Get entire contents of folder, includung contents of subfolders, without packages and hidden files
set entireContents to (fileManager's enumeratorAtURL:(sourceURL) includingPropertiesForKeys:({fileKey}) options:(searchOptions) errorHandler:(missing value))'s allObjects()

-- Filter case-insensitively for items with "pdf" extensions.
set thePredicate to NSPredicate's predicateWithFormat:("pathExtension ==[c] 'pdf'")
set urlArray to entireContents's filteredArrayUsingPredicate:(thePredicate)

-- The result is probably just the files we want, but check for any folders among them while getting their paths.
set theFiles to NSMutableArray's new()
repeat with theURL in urlArray
	if ((theURL's getResourceValue:(reference) forKey:(fileKey) |error|:(missing value))'s end as boolean) then tell theFiles to addObject:(theURL)
end repeat

-- Sort the remaining URLs on their paths.
set sortDescriptor to NSSortDescriptor's sortDescriptorWithKey:("path") ascending:(true) selector:("localizedStandardCompare:")
theFiles's sortUsingDescriptors:({sortDescriptor})

set theFiles to theFiles as list