Make an Array of URLs From a List of POSIX Paths

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.

I tested both scripts at the same time with five repetitions and got the same results as before.

BTW, the first time Script Debugger is run, theASObjC frameworks have to be loaded in memory, and this can skew the results. For example, the following shows your script as being 55 times slower on the first run.

I run both scripts in Script Degugger too. Several times in succession. So, Script Degugger also confirms that your script is about 20-75 times slower. I don’t know how to explain it.

I suspect this will remain a mystery, but, FWIW, I’ve included a timing script below with my script. It returned 7 milliseconds on my 2023 Mac mini computer.

use framework "Foundation"
use scripting additions

-- start time
set startTime to current application's CACurrentMediaTime()

-- timed code
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 4.txt", "/Users/Robert/Working/New Text File 5.txt", "/Users/Robert/Working/New Text File 6.txt", "/Users/Robert/Working/New Text File 7.txt", "/Users/Robert/Working/New Text File 8.txt", "/Users/Robert/Working/New Text File 9.txt", "/Users/Robert/Working/New Text File 10.txt", "/Users/Robert/Working/New Text File 11.txt", "/Users/Robert/Working/New Text File 12.txt", "/Users/Robert/Working/New Text File 13.txt", "/Users/Robert/Working/New Text File 14.txt", "/Users/Robert/Working/New Text File 15.txt", "/Users/Robert/Working/New Text File 16.txt", "/Users/Robert/Working/New Text File 17.txt", "/Users/Robert/Working/New Text File 18.txt", "/Users/Robert/Working/New Text File 19.txt", "/Users/Robert/Working/New Text File 20.txt", "/Users/Robert/Working/New Text File 21.txt", "/Users/Robert/Working/New Text File 22.txt", "/Users/Robert/Working/New Text File 23.txt", "/Users/Robert/Working/New Text File 24.txt", "/Users/Robert/Working/New Text File 25.txt", "/Users/Robert/Working/New Text File 26.txt", "/Users/Robert/Working/New Text File 27.txt", "/Users/Robert/Working/New Text File 28.txt", "/Users/Robert/Working/New Text File 29.txt", "/Users/Robert/Working/New Text File 30.txt", "/Users/Robert/Working/New Text File 31.txt", "/Users/Robert/Working/New Text File 32.txt", "/Users/Robert/Working/New Text File 33.txt", "/Users/Robert/Working/New Text File 34.txt", "/Users/Robert/Working/New Text File 35.txt", "/Users/Robert/Working/New Text File 36.txt", "/Users/Robert/Working/New Text File 37.txt", "/Users/Robert/Working/New Text File 38.txt", "/Users/Robert/Working/New Text File 39.txt", "/Users/Robert/Working/New Text File 40.txt", "/Users/Robert/Working/New Text File 41.txt", "/Users/Robert/Working/New Text File 42.txt", "/Users/Robert/Working/New Text File 43.txt", "/Users/Robert/Working/New Text File 44.txt", "/Users/Robert/Working/New Text File 45.txt", "/Users/Robert/Working/New Text File 46.txt", "/Users/Robert/Working/New Text File 47.txt", "/Users/Robert/Working/New Text File 48.txt", "/Users/Robert/Working/New Text File 49.txt", "/Users/Robert/Working/New Text File 50.txt", "/Users/Robert/Working/New Text File 51.txt", "/Users/Robert/Working/New Text File 52.txt", "/Users/Robert/Working/New Text File 53.txt", "/Users/Robert/Working/New Text File 54.txt", "/Users/Robert/Working/New Text File 55.txt", "/Users/Robert/Working/New Text File 56.txt", "/Users/Robert/Working/New Text File 57.txt", "/Users/Robert/Working/New Text File 58.txt", "/Users/Robert/Working/New Text File 59.txt", "/Users/Robert/Working/New Text File 60.txt", "/Users/Robert/Working/New Text File 61.txt", "/Users/Robert/Working/New Text File 62.txt", "/Users/Robert/Working/New Text File 63.txt", "/Users/Robert/Working/New Text File 64.txt", "/Users/Robert/Working/New Text File 65.txt", "/Users/Robert/Working/New Text File 66.txt", "/Users/Robert/Working/New Text File 67.txt", "/Users/Robert/Working/New Text File 68.txt", "/Users/Robert/Working/New Text File 69.txt", "/Users/Robert/Working/New Text File 70.txt", "/Users/Robert/Working/New Text File 71.txt", "/Users/Robert/Working/New Text File 72.txt", "/Users/Robert/Working/New Text File 73.txt", "/Users/Robert/Working/New Text File 74.txt", "/Users/Robert/Working/New Text File 75.txt", "/Users/Robert/Working/New Text File 76.txt", "/Users/Robert/Working/New Text File 77.txt", "/Users/Robert/Working/New Text File 78.txt", "/Users/Robert/Working/New Text File 79.txt", "/Users/Robert/Working/New Text File 80.txt", "/Users/Robert/Working/New Text File 81.txt", "/Users/Robert/Working/New Text File 82.txt", "/Users/Robert/Working/New Text File 83.txt", "/Users/Robert/Working/New Text File 84.txt", "/Users/Robert/Working/New Text File 85.txt", "/Users/Robert/Working/New Text File 86.txt", "/Users/Robert/Working/New Text File 87.txt", "/Users/Robert/Working/New Text File 88.txt", "/Users/Robert/Working/New Text File 89.txt", "/Users/Robert/Working/New Text File 90.txt", "/Users/Robert/Working/New Text File 91.txt", "/Users/Robert/Working/New Text File 92.txt", "/Users/Robert/Working/New Text File 93.txt", "/Users/Robert/Working/New Text File 94.txt", "/Users/Robert/Working/New Text File 95.txt", "/Users/Robert/Working/New Text File 96.txt", "/Users/Robert/Working/New Text File 97.txt", "/Users/Robert/Working/New Text File 98.txt", "/Users/Robert/Working/New Text File 99.txt", "/Users/Robert/Working/New Text File 100.txt"}

set theFiles to current application's NSArray's arrayWithArray:theFiles
set theURLs to current application's NSMutableArray's new()
repeat with aFile in theFiles
	set theURL to (current application's |NSURL|'s fileURLWithPath:aFile)
	(theURLs's addObject:theURL)
end repeat

-- elapsed time
set elapsedTime to (current application's CACurrentMediaTime()) - startTime
set numberFormatter to current application's NSNumberFormatter's new()
(numberFormatter's setFormat:"0")
set elapsedTime to ((numberFormatter's stringFromNumber:(elapsedTime * 1000)) as text) & " milliseconds"

-- result
elapsedTime --> 7 milliseconds
# count theURLs --> 100

BTW, the following paragraphs are from the documentation for fileURLWithPath:

fileURLWithPath. This method assumes that path is a directory if it ends with a slash. If path does not end with a slash, the method examines the file system to determine if path is a file or a directory. If path exists in the file system and is a directory, the method appends a trailing slash. If path does not exist in the file system, the method assumes that it represents a file and does not append a trailing slash.

As an alternative, consider using fileURLWithPath:isDirectory:, which allows you to explicitly specify whether the returned NSURL object represents a file or directory.

If the file paths in my test script do not end with a slash, which is the case, I would expect fileURLWithPath to be measurably slower than fileURLWithPath:isDirectory, but that’s not the case on my 2023 Mac mini. So, it appears that the only instance when isDirectory would need to be used is when all of the paths are directories.