Make an Array of URLs From a List of POSIX Paths

I get the same result:

[<__NSCFString 0x600000dcb660> valueForUndefinedKey:]: this class is not key value coding-compliant for the key fileURL.

I give up. Looks like there’s some sort of bridging issue here. @Shane Stanley already suggested using Bridge Plus. Perhaps JsObjC does the job, but I only have a rudimentary knowledge of it.

Working JsObjC code can be embedded into AppleScript using osascript command. By the way, there has already been one such case. I’ll raise one of your questions that I was able to solve with JsObjC.

I think the “fileURL” method/property on NSString May only be available for newer OS Foundation Frameworks.

Shane’s bridgeplus uses a custom framework with extensions on NSString.

I’m not quite sure what “the job” is in this case, but the following snippet is working in JS/JXA/JSObjC:

const paths = ["/Users/me/test_directory/Keyboard_Maestro_Test_Folder/A copy 1", "/Users/me/test_directory/Keyboard_Maestro_Test_Folder/A copy 2", "/Users/me/test_directory/Keyboard_Maestro_Test_Folder/ActionXML.plist"];
const nsURLArray = paths.map(p => $.NSURL.fileURLWithPath($(p)));

nsURLArray is a JavaScript Array though. If you need an NSArray object, use $(nsURLArray).

As to

The main issue here is how to return something useful from JSObjC to AS. If you use osascript, there’s nothing but strings (or strings in disguise, like numbers) that you can pass back to your script. For lists, this is not overly practical (but feasible).

Back to the original question: Why not simply URL-encode the file’s path (converting spaces to %20 etc) and then prepend file:/// to the result?

Same here (of course, being on Ventura). I know very little of (AS)ObjC, but I’m wondering what to expect of valueForKey if given the parameter "fileURL" in this context. The array contains NSString objects only, and valueForKey is (if I understand correctly what KVC means) returning a property of these objects. But there is no property fileURL for NSString objects (at least not one that I can see). OTOH, there is a fileURL property for NSURL objects (which only returns true or false, though). And using that as the parameter for valueForKey on an NSArray of NSURL objects (like $(nsURLArray) in the code above) works just fine.

That would appear to require a repeat loop, in which case NSURL’s fileURLWithPath method would seem a better (or at least more commonly used) choice.

Hm. Interestingly, the following also works on Catalina.

Obviously, fileURL here is a property of the array’s NSString objects. Damn it, I can’t find the corresponding entry in the official documentation of the NSString class:
 

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

set posixPaths to {"/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt"}
set NSStrings to current application's NSArray's arrayWithArray:posixPaths
set theURLs to (NSStrings's fileURL) -- as list, optional

 

I don’t think so. NSStrings (not a very good choice for a variable name in that context) is an NSArray object. So NSStrings's fileURL would be a property of NSArray, not of NSString (you’re not using valueForKey here). And the code throws an error on Ventura.

What exactly is theURLs in your environment?

1 Like

NSURLs without coercion as list

Odd…

That works in Script Debugger 5.0.12 but not in Script Debugger 8.0.6 or Apple’s Script Editor.

  • macOS 10.14.6
  • MacBookAir5,2
AppleScript
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set posixPathList to {"/Users/chris/Desktop/FastScripts ⇢ Search & Replace Evaluation.scpt", "/Users/chris/Desktop/REBUTTAL.scpt", "/Users/chris/Desktop/test.txt"}

set theNSArray to current application's NSArray's arrayWithArray:posixPathList

set theURLs to theNSArray's fileURL --<<-- Script Debugger 8.0.6 (8A66) on OSX 10.14.6 fails here.

return theURLs as list

--> Error: Can’t get fileURL of «class ocid» id «data optr00000000609E070000600000».

FWIW, Shane has explained that using a property name without parentheses is the same as using value for key. The following script demonstrates this.

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

set posixPaths to {"/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt"}
set NSStrings to current application's NSArray's arrayWithArray:posixPaths
set theURLs to (NSStrings's lastPathComponent) --> (NSArray) {"New Text File 1.txt",  "New Text File 2.txt", 	"New Text File 3.txt"}

It works for me in both editors without problems.

As I noted above, the fileURL property appears to be a property of the NSString class that is not documented anywhere, and is poorly implemented or not implemented at all in most OS X versions. This is another secret of the Apple team, which can be revealed by asking them a question.

@Shane_Stanley had some informative insight about this:

4 Likes

I just had a look for fileURL in all ObjC headers in …Foundation – the only one found was the property in NSURL. Given that I’m on Ventura and almost everybody else here seems to be on older OS versions, the property seems to have gone down the drain meanwhile. Relying on undocumented features is never a good idea, I think.

1 Like

 
As we have already found out, it is impossible to obtain a universal solution without a repeat loop.

Your script works, but it can be improved. I suggest that you don’t create an extra mutable array, but replace POSIX paths with NSURLs directly on the existing array:

 

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

on posixPaths2NSURLs(posixPaths)
	set theURLs to (current application's NSMutableArray's arrayWithArray:posixPaths)
	repeat with anItem in theURLs
		set contents of anItem to (current application's |NSURL|'s fileURLWithPath:anItem)
	end repeat
	return theURLs
end posixPaths2NSURLs

posixPaths2NSURLs({"/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt"})

 

The script above is little faster only then yours (about 1.15 times). If you want to get a large speed improvement, then you need to use a hybrid solution. NOTE: with a large list, the difference in speed will increase significantly. For example, for 96 posix paths I get speed improvement = 35 times.
 

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

on posixPaths2NSURLs(theFiles) -- list of Posix paths
	repeat with anItem in theFiles
		set contents of anItem to anItem as POSIX file
	end repeat
	return current application's NSArray's arrayWithArray:theFiles
end posixPaths2NSURLs

posixPaths2NSURLs({"/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt"})

 

Well … Sensibly, anyway. :wink:

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

on join(lst, delim)
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to delim
	set txt to lst as text
	set AppleScript's text item delimiters to astid
	return txt
end join

set theFiles to {"/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt"}
set theFiles to "{posix file \"" & join(theFiles, "\", posix file \"") & "\"}"
set theFiles to current application's class "NSArray"'s arrayWithArray:(run script theFiles)
2 Likes

An interesting solution. Unfortunately it is 2 times slower than my last script. I tested with 96 posix paths.

Thanks Fredrik71 and Nigel for the fine suggestions. Because computers and macOS versions vary greatly, and because absolute timing results can be more important than relative results, I ran some timing tests using Script Geek with 100 POSIX path files. My computer is a 2023 Mac mini running Ventura.

FORUM MEMBER - IN POST – RESULT
Peavine - 5 - 4 milliseconds
KniazidisR (first) - 32 - 3 milliseconds
KniazidisR (second) - 32 - 3 milliseconds
Nigel - 33 - 5 milliseconds

It’s not clear why we both test at Script Geek and get different tests. I have your script (and my 1st script) at least 20 times slower than my 2nd script.

Also, your script (and my 1st script) is at least 10 times slower than Nigel Garvey’s script. Are you building a list of Posix paths in a repeat loop? And measured time with it? You need to provide a list of 100 paths for the purity of the experiment. Here is list of 96 posix paths to test:
 

set theFiles to {"/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt", "/Users/Robert/Working/New Text File 1.txt", "/Users/Robert/Working/New Text File 2.txt", "/Users/Robert/Working/New Text File 3.txt"}

 

My test script contained an error, and I have revised my timing results above. The following is a screenshot of Script Geek with one of the tested scripts for review.

And, here is a screenshot that seems to verify that the script works as expected:

Again it is not clear. It is also not clear how you are testing my script. The main value of Script Geek lies in the simultaneous launch and comparison of 2 tested scripts, and even with the possibility of repetition. Click the image bellow to zoom it.