FileSystem Convert Objects To NSURLs

I’m working on a handler that converts any type of filesystem object reference into an NSURL.

Requirements:
•Accept any single filesystem object [alias, file, nsurl, posix file, posix path, hfs path] returning a single NSURL.
•Accept [list or NSArray] of any combination of [aliases, files, nsurls, posix files, posix paths, hfs paths] returning an NSArray of NSURLs.

I would greatly appreciate if anyone with more ASOBJC chops than I would take a critical look at this handler and offer any suggestions! Code or logic optimizations, either would be awesome. I wouldn’t be surprised if there is a completely different methodology I could be using here. TIA!

I try to keep code to a minimum in handlers, but processing efficiency for this handler is extremely important to me as it’s called by numerous other handlers, and often repeatedly. Handling filesystem objects that are already NSURLs is crucial as this condition occurs most frequently, so at the beginning of the handler I test for a single NSURL parameter and immediately return it. Same for NSArrays of NSURLs.

For a more intense test, change “path to the desktop folder” to “path to the home folder”. 12.39 seconds here ( old intel box / Tahoe ) for 320,629 items.

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


set fileSystemObjectList to FileSystem_List_Contents(path to the desktop folder)
set NSURLList to FileSystem_Convert_Objects_To_NSURLs(fileSystemObjectList)
set NSURLListLength to (NSURLList's valueForKey:"@count") as integer

on FileSystem_Convert_Objects_To_NSURLs(fileSystemObjectList)
	--THIS HANDLER IS USED EXTENSIVELY IN OTHER HANDLERS. ANY OPTIMIZATIONS HERE WOULD BE VERY VALUABLE 
	try
		set singularParameter to true
		--IF FILESYSTEMOBJECTLIST IS A SINGLE NSURL THEN  RETURN IT UNMODIFIED
		try
			if ((fileSystemObjectList's isKindOfClass:(current application's NSURL))) then
				return fileSystemObjectList
			end if
		end try
		
		--IF PARAMETER IS ARRAY OF NSURLS THEN RETURN IT UNMODIFIED
		try
			if (fileSystemObjectList's isKindOfClass:(current application's NSArray)) then
				set {parameterIsNSArray, singularParameter} to {true, false}
				set classNames to (fileSystemObjectList's valueForKey:"className") --as list
				set classNamesSet to (current application's NSSet's setWithArray:classNames)
				set classNames to (classNamesSet's allObjects()) as list
				if length of classNames is 1 and (item 1 of classNames) is "NSURL" then return fileSystemObjectList
			end if
		on error
			set parameterIsNSArray to false
		end try
		
		--CONVERT APPLESCRIPT LIST PARAMETERS TO NSARRAYS
		if class of fileSystemObjectList is in {list} then
			set fileSystemObjectList to (current application's NSArray's arrayWithArray:fileSystemObjectList)
			set singularParameter to false
		end if
		
		--CONVERT SINGLE ITEM PARAMETERS TO SINGLE ITEM NSARRAYS
		if singularParameter then set fileSystemObjectList to (current application's NSArray's arrayWithObject:fileSystemObjectList)
		
		--REGARDLESS OF INPUT PARAMETER fileSystemObjectList SHOULD NOW BE AN NSARRAY
		set fileSystemObjectListLength to (fileSystemObjectList's valueForKey:"@count") as integer
		
		repeat with currentIndex from 1 to fileSystemObjectListLength
			set fileSystemObject to item currentIndex of fileSystemObjectList
			try
				if ((fileSystemObject's isKindOfClass:(current application's NSURL))) then
					--SKIP IT
				else --NOT AN NSURL. CONVERT IT.
					try
						set fileSystemObject to POSIX path of fileSystemObject
					on error
						try
							tell application "System Events"
								set fileSystemObject to POSIX path of ((path of disk item (fileSystemObject as string)) as alias)
							end tell
						on error
							set fileSystemObject to POSIX path of (fileSystemObject as alias)
						end try
					end try
					set fileSystemObject to (current application's class "NSURL"'s fileURLWithPath:fileSystemObject)
					set item currentIndex of fileSystemObjectList to fileSystemObject
				end if
			end try
		end repeat
		
		if singularParameter then return item 1 of fileSystemObjectList
		return fileSystemObjectList
		
	on error errorText number errornumber partial result errorResults from errorObject to errorExpectedType
		error "<FileSystem_Convert_Objects_To_NSURLs>" & space & errorText number errornumber partial result errorResults from errorObject to errorExpectedType
	end try
end FileSystem_Convert_Objects_To_NSURLs

on FileSystem_List_Contents(folderObject)
	--FULL RECURSIVE LISTING NOT INCLUDING INVISIBLES OR CONTENTS OF PACKAGES
	try
		set folderObject to FileSystem_Convert_Objects_To_NSURLs(folderObject)
		set theEntireContents to ((current application's NSFileManager's defaultManager's enumeratorAtURL:folderObject includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects())'s mutableCopy()
		return theEntireContents
	on error errorText number errornumber partial result errorResults from errorObject to errorExpectedType
		error "<FileSystem_List_Contents>" & errorText number errornumber partial result errorResults from errorObject to errorExpectedType
	end try
end FileSystem_List_Contents

Hi Paul.

Here’s another take on your handler. I don’t think it’s any better, just a bit shorter. :wink:

The timings for your posted script are almost entirely for the other handler, since the conversion handler itself is initially called with just a single file system object and then with the other handler’s result, which is an array of NSURLs anyway and quickly dealt with.

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

set fileSystemObjectList to FileSystem_List_Contents(path to desktop) -- Returns an array of NSURLs anyway.
--set NSURLList to FileSystem_Convert_Objects_To_NSURLs(fileSystemObjectList)
set NSURLListLength to fileSystemObjectList's |count|()

on FileSystem_Convert_Objects_To_NSURLs(fileSystemObjectList)
	set |⌘| to current application
	
	-- Get the input in Objective-C form.
	set fileSystemObjectList to (|⌘|'s class "NSArray"'s arrayWithObject:(fileSystemObjectList))'s firstObject()
	-- Script object containing a potentially recursive private handler.
	script o
		on convert(obj)
			if (obj's isKindOfClass:(|⌘|'s class "NSURL")) then
				return obj
			else if (obj's isKindOfClass:(|⌘|'s class "NSString")) then
				-- This assumes that any NSString is a valid POSIX or HFS path.
				if not (obj's hasPrefix:("/")) then set obj to (obj as text as «class furl»)'s POSIX path
				return |⌘|'s class "NSURL"'s fileURLWithPath:(obj)
			else if (obj's isKindOfClass:(|⌘|'s class "NSArray")) then
				-- Quick check to see if an array contains anything other than NSURL-related classes.
				set classNameSet to |⌘|'s class "NSSet"'s setWithArray:(obj's valueForKey:("className"))
				set filter to |⌘|'s class "NSPredicate"'s predicateWithFormat:("! self CONTAINS 'URL'")
				-- If it does, work through each of its objects in turn.
				if ((classNameSet's filteredSetUsingPredicate:(filter))'s |count|() > 0) then
					set obj to obj's mutableCopy()
					repeat with this in obj
						set this's contents to convert(this)
					end repeat
				end if
				return obj
			end if
			return false -- If none of the above. 
		end convert
	end script
	
	-- Perform the conversion and return the result.
	return o's convert(fileSystemObjectList)
end FileSystem_Convert_Objects_To_NSURLs

on FileSystem_List_Contents(folderObject)
	--FULL RECURSIVE LISTING NOT INCLUDING INVISIBLES OR CONTENTS OF PACKAGES
	try
		set folderObject to FileSystem_Convert_Objects_To_NSURLs(folderObject)
		set theEntireContents to ((current application's NSFileManager's defaultManager's enumeratorAtURL:folderObject includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()) -- 's mutableCopy()
		return theEntireContents
	on error errorText number errornumber partial result errorResults from errorObject to errorExpectedType
		error "<FileSystem_List_Contents>" & errorText number errornumber partial result errorResults from errorObject to errorExpectedType
	end try
end FileSystem_List_Contents

I didn’t review your script in detail, as my initial feeling was that it appears quite cumbersome and verbose. Instead, I drafted an attempt at something shorter, leveraging specific class coercions that are valid across different types of file references. These get applied in an order dictated by the following:

  • All AppleScript file references, apart from those belonging to System Events, can be coerced to a string;

  • NSURL-class file URL’s can be coerced to a string, yielding a file URL string of the form "file:///...";

  • String paths may coerce to an alias (i.e. HFS paths), but any that don’t (posix paths and file URL strings) will coerce to POSIX file references;

  • Both alias and POSIX file classes coerce to «class furl».

use framework "Foundation"
use scripting additions

to __NSURL__(fileRef)
        fileRef as string as {alias, POSIX file} as «class furl»
        return my (NSURL's fileURLWithPath:(result's POSIX path))
end __NSURL__

to __NSURLs__ of current application from list
        local fileRefs, _from
        if class of fileRefs ≠ list then set fileRefs to _from
        
        script objects
                property list : fileRefs
        end script
        
        repeat with object in (a reference to the list of objects)
                set the object's contents to __NSURL__(object)
        end repeat
        
        return arrayWithArray_(the list of objects) of my NSArray
end __NSURLs__

A quick field-test using various types of file references that all point to the home directory:

tell (path to home folder) to get [¬
        it, POSIX path, it as text, ¬
        POSIX file POSIX path, ¬
        it as «class furl», ¬
        it as «class fsrf»]

tell application id "com.apple.finder" to set ¬
        fRefs to the result & home's [URL, it]

On my machine, fRefs evaluates to the following:

{alias "CK-AIR:Users:CK:", "/Users/CK/", "CK-AIR:Users:CK:", ¬
        file "CK-AIR:Users:CK:", file "CK-AIR:Users:CK:", ¬
        file "CK-AIR:Users:CK:", "file:///Users/CK/", ¬
        folder "CK" of folder "Users" of startup disk ¬
        of application "Finder"}

This can be passed to the __NSURLs__ handler like so:

__NSURLs__ from fRefs
__NSURLs__ from result -- testing an array of NSURLs 

yielding an opaque objc reference, which should be an NSArray of NSURL objects. If so, we will be able to retrieve an NSURL property value for every object in the NSArray:

result's |path| as list

Output:

{"/Users/CK", "/Users/CK", "/Users/CK", "/Users/CK", ¬
 "/Users/CK", "/Users/CK", "/Users/CK", "/Users/CK"}

  • Admittedly, this is a fairly cursory fieldtest, and more in-depth testing may be required to look for other fail conditions.

  • As alluded to at the beginning, System Events file references are not currently handled by the above script. This can be incorporated reasonably easily, but does require a separate bit of processing as System Events file references cannot generally be coerced into non-System Events classes. They will first need to be evaluated against a suitable property, for which the URL property lends itself well by being accessible outside of a System Events block.

Thanks Nigel, this is interesting and much more compact! I’m digesting it. Already see lots of bits I want to incorporate! Same timings so all good there!

Yes, the provided test is just passing along the NSURLS delivered. And, while that is actually most important to me in many cases, a comprehensive test of various types would have been more polite to post. I assumed folk would pass it their own various local references.

However, in fabricating proper test data, my posted code fails on some filesystem objects, returning NSStrings. Yours avoided this error.

Damn right. I’m not happy about it. The original handler was ~10 lines. But I am willing to budget some extra bytes here if it increases efficiency. And there’s quite a bit of code just to prevent wasted effort and processing time, list and single object handling etc. I’m fine with this so long as the handler remains quick and flexible.

Some of your handlers are inscrutable to me. Cool. I’ll dig in and try to follow.

I appreciate the actual coercion logic suggestions! The meat of my handler that actually coerces objects is quite old code and yours is more concise and direct. In my testing it appears to handle all specced filesystem objects, setting system events file references aside.

Nigel, could you explain to me why this line, if passed an NSArray of multiple items, will return the array as opposed to a new NSArray containing only the first item of that NSArray? I like this construct, but I don’t understand. I thought arrayWithObject would error if fed an array.

Hi Paul.

It’s a trick from Shane’s book, I think. It gets the Objective-C version of the object by creating an NSArray containing it and recovering the Objective-C equivalent from the array. If the original object was an AppleScript list containing AppleScript items, the equivalent will be an NSArray containing Objective-C objects. Hope this makes sense! :woozy_face:

1 Like

I’ve been looking at @CJK’s handlers today. The first line’s interesting in the first one:

I’ve not seen this sort of thing before and I don’t think it’s documented behaviour. It apparently coerces to alias any text that can be so coerced, otherwise the text’s treated as a POSIX path and coerced to a POSIX file. Whichever happens, the result’s then coerced to «class furl». It works with everything I’ve tried so far except for HFS paths with fictitious disk names. As far as I can to tell, it can be reduced to the following with no loss of effectiveness. (Corrected here and in post 10 below following pjh’s comment in post 13):

fileRef as text as {alias, POSIX file}

I’ve not so far been able to fathom why the other handler’s written the way it is, but I’ll study it in more detail this weekend. Meanwhile, here’s another version of Paul’s script which uses CJK’s insight above. There is of course no difference in speed when the input’s already an array containing only NSURLs, but it does seem to be quite a bit faster than my earlier effort in other cases.

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

set fileSystemObjectList to FileSystem_List_Contents(path to desktop)
-- set fileSystemObjectList to fileSystemObjectList's valueForKey:("path") -- For testing.
set NSURLList to FileSystem_Convert_Objects_To_NSURLs(fileSystemObjectList)
set NSURLListLength to NSURLList's |count|()

on FileSystem_Convert_Objects_To_NSURLs(fileSystemObjectList)
	script o -- Script object containing a potentially recursive private handler.
		on convert(objList)
			script p -- Script object holding a list for speed of access to the list's items.
				property lst : objList
			end script
			repeat with this in (a reference to p's lst)
				if (this's class is list) then
					convert(this's contents)
				else
					set this's contents to this as text as {alias, POSIX file} -- After CJK.
				end if
			end repeat
		end convert
	end script
	
	-- Perform the conversion and return the result.
	set |⌘| to current application
	-- (*
	-- See if the input can be returned immediately as an NSArray of just NSURLs. (Not incompatible with Finder specifiers. )
	set fileSystemObjectList to (|⌘|'s class "NSArray"'s arrayWithObject:(fileSystemObjectList))'s firstObject()
	if (fileSystemObjectList's isKindOfClass:(|⌘|'s class "NSArray")) then
		set classNameSet to |⌘|'s class "NSSet"'s setWithArray:(fileSystemObjectList's valueForKey:("className"))
		set filter to |⌘|'s class "NSPredicate"'s predicateWithFormat:("! self CONTAINS 'URL'")
		if ((classNameSet's filteredSetUsingPredicate:(filter))'s |count|() = 0) then return fileSystemObjectList
	end if
	-- *)
	-- Get the input as an AppleScript list and coerce any non-«class furl» items to «class furl».
	set fileSystemObjectList to fileSystemObjectList as list
	o's convert(fileSystemObjectList)
	-- Derive an NSArray from the list.
	set URLArray to |⌘|'s class "NSArray"'s arrayWithArray:(fileSystemObjectList)
	-- Output a single item or the array as appropriate.
	if (URLArray's |count|() = 1) then return URLArray's firstObject()
	return URLArray
end FileSystem_Convert_Objects_To_NSURLs

on FileSystem_List_Contents(folderObject)
	--FULL RECURSIVE LISTING NOT INCLUDING INVISIBLES OR CONTENTS OF PACKAGES
	try
		set folderObject to FileSystem_Convert_Objects_To_NSURLs(folderObject)
		set theEntireContents to ((current application's NSFileManager's defaultManager's enumeratorAtURL:folderObject includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()) -- 's mutableCopy()
		return theEntireContents
	on error errorText number errornumber partial result errorResults from errorObject to errorExpectedType
		error "<FileSystem_List_Contents>" & errorText number errornumber partial result errorResults from errorObject to errorExpectedType
	end try
end FileSystem_List_Contents

I thought so too! Several constructs I wasn’t familiar with had me both confused and excited. Cool to learn new things!

I really appreciate your work on this handler! You’ve made some great improvements! I do have one question though. I’m finding that this line…

	if (fileSystemObjectList's isKindOfClass:("NSArray")) then

Fails to correctly identify NSArrays. My timings doubled for home folder evaluation or I might not have noticed.

	if (fileSystemObjectList's isKindOfClass:(current application's NSArray)) then

works correctly here. Is testing for the class name as text supposed to work?

Ah. No. Sorry. You’re right. That was an experiment that somehow survived into the posted version. I’ve just corrected it.

Right. Continuing my analysis of CFK’s handlers with the second one.

It turns out that if a handler’s on/to line has constants (those that are their own values) where there should be parameter variables, the values passed when the handler’s called are instead assigned in order to the first few local variables declared inside the handler — one each per missing parameter variable.

CFK’s second handler is a labelled-parameter handler with the optional of parameter and one using the from label. The constants current application and list have been used instead of parameter variables in the to line, so the incoming values are assigned instead to the local variables declared in the line below it. There’s nothing special about the constants or the variable names. They’re just illustrative. The to line could be to __NSURLs__ of Tuesday from ask and it would still work.

While the of parameter’s optional, it’s only optional at the calling end. It’s specified in the handler definition, so the system helpfully supplies a default value if the call doesn’t provide one. This value’s a reference to the script itself and is assigned to the fileRefs variable. But fileRefs needs to get the from parameter’s (presumed list) of file references, hence the list check and variable reassignment after the local declarations. I think this has been done through some misunderstanding (or out-of-date understanding) of how the optionality works. If the of parameter’s simply omitted from the handler definition, no value, default or otherwise, will be received for it, regardless of whether or not one’s included in the call. The handler only uses the from value, so the of parameter may as well be omitted from the to line. And there’s no need for the constants-instead-of-variables trick.

The use of the list constant as a property label in the script object is a naughtiness intended to make the following code more English-like. It has no functional value.

Since the second handler’s concluding action is to create an NSArray from the edited list, there’s no need for the first handler to produce the individual NSURLs. It can simply return the aliases or «class furl»s and they’ll be converted in bulk along with the list.

The handlers were written with the assumption that the input would be a list rather than an individual item. One way round this for Paul’s use would be to set the property in the script object to fileRefs as list. Another way would be to do this in the to line, which I believe is an intentional feature in AppleScript, but not well known.

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

to __NSURL__(fileRef)
	return fileRef as text as {alias, POSIX file}
end __NSURL__

to __NSURLs__ from fileRefs as list -- Coercion to list here.
	script objects
		property lst : fileRefs
	end script
	
	repeat with object in (a reference to the lst of objects)
		set the object's contents to __NSURL__(object)
	end repeat
	
	return current application's NSArray's arrayWithArray:(lst of objects)
end __NSURLs__

__NSURLs__ from (path to pictures folder) -- Single alias for this demo.

I’d had it in my head that coercing a handler’s input in the parameter list was only possible with labelled parameters. But fooling around with it this morning, it seems to work with positional and interleaved parameters too. It even works when the item in the parameter list is a constant instead of a variable! :wink:

on test(application responses as list)
	local fred
	return fred
end test

my test("Hello!") --> {"Hello!"}

Hello everyone,
Very interesting topic!

The CJK method is clever and concise, but it can be very slow because it tests every coercion for each element.

Nigel, I’m not sure if it’s thanks to my MacStudio M4’s processor or Sequoia’s optimization, but I’ve noticed that using a subscript to speed up processing long lists has become unnecessary.

Here is my contribution: (I’m curious to see what results you’re getting…)

use framework "Foundation"
use scripting additions

tell (path to documents folder from user domain) to set theRefs to {it, POSIX path, "~/Documents", it as text, POSIX file POSIX path, it as «class furl», it as «class fsrf»}
tell application id "com.apple.finder" to set theRefs to theRefs & (folder "Documents" of home)'s [URL]

repeat 5 times
	set theRefs to theRefs & theRefs
end repeat

set theDate to current application's NSDate's new()
set theResult1 to my makeURLList:theRefs
set a to {theResult1's |count|(), (current application's NSDate's new())'s timeIntervalSinceDate:theDate}

set folderObject to (current application's NSURL's fileURLWithPath:"/Applications/")
set theRefs to ((current application's NSFileManager's defaultManager's enumeratorAtURL:folderObject includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects())'s mutableCopy()
set theDate to current application's NSDate's new()
set theResult2 to my makeURLList:(theRefs's valueForKey:"path")
set b to {theResult2's |count|(), (current application's NSDate's new())'s timeIntervalSinceDate:theDate}

{a, b} --> {{256, 0.028068065643}, {11884, 0.606459021568}}

on makeURL:oneRef
	if class of oneRef = class "NSURL" then return oneRef -- it's already a NSURL
	
	set theString to (oneRef as text)
	if (theString starts with "/") then -- full POSIX path
		return current application's NSURL's fileURLWithPath:theString
	else if (theString starts with "~") then -- abreviated POSIX path
		return current application's NSURL's fileURLWithPath:((current application's NSString's stringWithString:theString)'s |stringByExpandingTildeInPath|())
	else if (theString starts with "file:///") then -- finder URL
		return current application's NSURL's fileURLWithPath:(POSIX path of (get POSIX file theString))
	else -- HFS path
		return current application's NSURL's fileURLWithPath:(POSIX path of oneRef)
	end if
end makeURL:

on makeURLList:theRefs
	set theArray to current application's NSMutableArray's new()
	repeat with aRef in theRefs
		(theArray's addObject:(my makeURL:aRef))
	end repeat
	return theArray
end makeURLList:

Hi ionah.

It’s still necessary with long AppleScript lists in Tahoe on my M3 MBP. But I’m not sure, off-hand, about ASObjC arrays. If you were experimenting with my script in post 7 above, it’s possible the list wasn’t worked through at all, because the conversion handler (based on Paul’s original) contains a check to find out quickly if the input’s already an array of NSURLs or a list of AS equivalents. The input in the script is an array of NSURLs returned by the other handler, so it’s just output as such without the individual items needing to be checked.

The result of your script on my MBP is:

{{256, 0.058376073837}, {14642, 1.138298988342}}

… although the machine’s running tasks in the background, so the timings vary slightly from run to run. On my older Intel iMac running Ventura, the result is:

{{256, 0.102644920349}, {14641, 3.263147450172}}

Hi,
I doubt that the shortened NSURL handler is any good for Finder URLs:

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

to __NSURL__(fileRef)
    -- return fileRef as string as {alias, POSIX file} as «class furl»
	fileRef as {«class furl», POSIX file}
end __NSURL__

to __NSURLs__ from fileRefs as list -- Coercion to list here.
	script objects
		property lst : fileRefs
	end script
	
	repeat with object in (a reference to the lst of objects)
		set the object's contents to __NSURL__(object)
	end repeat
	
	return current application's NSArray's arrayWithArray:(lst of objects)
end __NSURLs__

tell application id "com.apple.finder" to set fRefs to home's [URL, it]

__NSURLs__ from fRefs
result's |path| as list

This returns

(NSArray) {
	(NSURL) file:///file/:::Users:aUser:,
	(NSURL) file:///.file/id=6571367.2438739/
}

and respectivly

{"/file/:::Users:aUser:", "/Users/aUser"}

Unquote the commented-out line to compare with the correct value.
Am I wrong?

No. Thanks for pointing that out. I get similar results to your own. It seems that, while Finder URLs can be coerced directly to «class furl», the result’s incorrect, like the “fictitious disk name” exception I mentioned somewhere above. The “file:” prefix is taken as a folder name and the system prepends the name of the startup disk to it. The original hack works because a Finder URL can’t be coerced to alias and so it’s coerced to POSIX file instead. It may still be possible to get away with:

fileRef as {alias, POSIX file}

… since the NSURL creation process seems to be equally happy with aliases and «class furl»s. But I’ll have to test this more thoroughly in the morning. :slightly_smiling_face:

The following day:
Yes and no. It works with CJK’s original test list, an NSArray derived from the test list minus the Finder folder specifier, and on the results generated. But it doesn’t work with NSStrings, so the original as text is still needed. I’ve corrected my posts above. The “~/Documents” string in ionah’s test list would have to be expanded separately.

Nigel,
I think I copied the code a bit too quickly into my library. What do you think about this:

use framework "Foundation"
use scripting additions

tell (path to documents folder from user domain) to set theEntry1 to {it, POSIX path, "~/Documents", it as text, POSIX file POSIX path, it as «class furl», it as «class fsrf»}
tell application id "com.apple.finder" to set theEntry1 to theEntry1 & (folder "Documents" of home)'s [URL]

repeat 5 times
	set theEntry1 to theEntry1 & theEntry1
end repeat

set theDate to current application's NSDate's new()
set theResult1 to my makeURLList:theEntry1
set theTimer1 to {theResult1's |count|(), (current application's NSDate's new())'s timeIntervalSinceDate:theDate}

set folderObject to (current application's NSURL's fileURLWithPath:"/Applications/")
set theURLs to ((current application's NSFileManager's defaultManager's enumeratorAtURL:folderObject includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects())'s mutableCopy()

-- array of NSURLs
set theDate to current application's NSDate's new()sss
set theEntry2 to theURLs -- just for the result panel
set theResult2 to my makeURLList:theURLs
set theTimer2 to {theResult2's |count|(), (current application's NSDate's new())'s timeIntervalSinceDate:theDate}

-- list of hfs
set theDate to current application's NSDate's new()
set theEntry3 to (theURLs as list)
set theResult3 to my makeURLList:theEntry3
set theTimer3 to {theResult3's |count|(), (current application's NSDate's new())'s timeIntervalSinceDate:theDate}

-- array of paths
set theDate to current application's NSDate's new()
set theEntry4 to (theURLs's valueForKey:"path")
set theResult4 to my makeURLList:theEntry4
set theTimer4 to {theResult4's |count|(), (current application's NSDate's new())'s timeIntervalSinceDate:theDate}

-- list of paths
set theDate to current application's NSDate's new()
set theEntry5 to ((theURLs's valueForKey:"path") as list)
set theResult5 to my makeURLList:theEntry5
set theTimer5 to {theResult2's |count|(), (current application's NSDate's new())'s timeIntervalSinceDate:theDate}

{theTimer1, theTimer2, theTimer3, theTimer4, theTimer5} --> {{256, 0.021373033524}, {11878, 0.261879920959}, {11878, 0.401418924332}, {11878, 0.566382050514}, {11878, 0.579731941223}}

on makeURL:oneRef
	if class of oneRef = class "NSURL" then return oneRef -- it's already a NSURL
	
	if (oneRef's hasPrefix:"/") then -- full POSIX path
		return current application's NSURL's fileURLWithPath:oneRef
	else if (oneRef's hasPrefix:"~") then -- abreviated POSIX path
		return current application's NSURL's fileURLWithPath:((current application's NSString's stringWithString:oneRef)'s |stringByExpandingTildeInPath|())
	else if (oneRef's hasPrefix:"file:///") then -- finder URL
		return current application's NSURL's fileURLWithPath:(POSIX path of (get POSIX file (oneRef as string)))
	else -- HFS path
		return current application's NSURL's fileURLWithPath:(POSIX path of (oneRef as string))
	end if
end makeURL:

on makeURLList:multiRefs
	set multiRefs to current application's NSArray's arrayWithArray:multiRefs
	
	set theArray to current application's NSMutableArray's new()
	repeat with aRef in multiRefs
		(theArray's addObject:(my makeURL:aRef))
	end repeat
	return theArray
end makeURLList:

The first test is used to verify that all the reference types return the correct result.
Tests 2 and 3 show a slight difference in processing time between an array and a list (NSURL vs HFS).
Tests 4 and 5 do the same for POSIX paths.
As you can see, the differences are not really significant.

Why the deprecated and old fashioned square brackets here. {} seem to work as well and without apparent issue. I am stressing the verb “seem to” here.
Very interesting posting by Shane Stanley here:
https://web.archive.org/web/20140115104144/http://lists.apple.com/archives/applescript-users/2009/Dec/msg00318.html

Thanks for any elucidation!

Hi ionah.

Further to my reply last night, changing the items in a long, existing list that’s referenced through a script object is definitely faster than building a long, new NSMutableArray that isn’t. On both my machines, your already fast script is made faster if the makeURLList: handler is rendered thus:

on makeURLList:theRefs
	script o
		property lst : (theRefs as list)'s items -- List from NSArray or new list with another's items.
	end script
	
	repeat with aRef in (a reference to o's lst)
		set aRef's contents to (my makeURL:(aRef's contents))
	end repeat
	
	return current application's class "NSArray"'s arrayWithArray:(o's lst)
end makeURLList:

Adding «class furl» items to an NSMutableArray, or adding them to a list and making an NSArray from the list, automatically produces NSURLs, so there’s no need for your makeURL: handler to do the work for such items individually:

on makeURL:oneRef
	if class of oneRef = class "NSURL" then return oneRef -- it's already a NSURL
	
	set theString to (oneRef as text)
	if (theString starts with "/") then -- full POSIX path
		return POSIX file theString
	else if (theString starts with "~") then -- abreviated POSIX path
		return current application's NSURL's fileURLWithPath:((current application's NSString's stringWithString:theString)'s |stringByExpandingTildeInPath|())
	else if (theString starts with "file:///") then -- finder URL
		return POSIX file theString
	else -- HFS path
		return theString as «class furl»
	end if
end makeURL:

PS. Your latest post has appeared while I’ve been composing this. I’ll take a look at it now.

CJK and I have had words about this before. How scripters choose to code for their own use at home is entirely up to them. What they tell or imply to others on public help fora is the way to do something requires more “academic rigour”. :slightly_smiling_face:

OK, I assume: no special reason.

Hi ionah.

The version of makeURLList: I suggested in my previous reply had an error which I’ve since corrected. Apologies if it caught you out.

The timings for the four “long” tests in your latest script include the times taken to set up the source lists/arrays.! But the difference probably isn’t critical. :wink: I’ve moved the start time lines for my own tests.

The results in Tahoe on my M3 MBP are:

{{256, 0.022356987}, {14642, 0.434098958969}, {14642, 0.434849977493}, {14642, 0.867901086807}, {14642, 0.87541103363}}

These show your script to be marginally faster with NSURLs and «class furl»s than with paths. With the “faster” handlers I suggested in my previous reply, the path tests were faster still, but the NSURL and «class furl» tests took quite a bit longer! I edited makeURL: so that «class furl» objects were returned immediately, like NSURLs, and this fixed the problem for both classes! I eventually realised it was because arrays of NSURLs were turned into lists of «class furl»s at the top of makeURLList:. Duh! :roll_eyes:

Anyway. I’ve now performed a couple more tweaks in both handlers:

on makeURL:oneRef
	set theString to (oneRef as text)
	if (theString starts with "/") then -- full POSIX path
		return POSIX file theString
	else if (theString starts with "~") then -- abreviated POSIX path
		return current application's NSURL's fileURLWithPath:((current application's NSString's stringWithString:theString)'s |stringByExpandingTildeInPath|())
	else if (theString starts with "file:///") then -- finder URL
		return POSIX file theString
	else -- HFS path
		return theString as «class furl»
	end if
end makeURL:

on makeURLList:theRefs
	script o
		property lst : (theRefs as list)'s items -- List from NSArray or new list with another's items.
	end script
	
	repeat with aRef in (a reference to o's lst)
		set refContents to aRef's contents
		if (refContents's class is in {«class furl», alias, «class fsrf», class "NSURL"}) then
		else
			set aRef's contents to (my makeURL:(refContents))
		end if
	end repeat
	
	return current application's class "NSArray"'s arrayWithArray:(o's lst)
end makeURLList: