Listing all the files in a folder using JavaScript for Automation on Yosemite
Asked Answered
A

3

10

I'm trying to port some old Applescript over to the new JavaScript syntax.

Somethings seem to be pretty straight forward so :

tell application "System Events" to keystroke "t" using command down

becomes :

System = Application('System Events');
System.keystroke("t", {using: "command down"})

However I can't for the life of me work out how to list files at a specific location. In AppleScript, to return me a list of the files in the /usr directory, you would do :

tell application "System Events" to set fileList to name of items in folder "/usr"
-- > {"bin", "include", "lib", "libexec", "local", "sbin", "share", "standalone", "X11"}

However I can't for the life of me work out how to do it in Javascript.

System = Application('System Events')
myPath = Path("/usr")

fileList = System.items(myPath) 
-- > message not understood

fileList = System.folder(myPath)
-- > message not understood

fileList = System.diskItems(myPath)
-- > []

fileList = System.diskItems({at:myPath)
-- > []

I've tried a whole lot of other combinations too, but no luck. Any ideas?

Anywheres answered 10/10, 2014 at 14:23 Comment(0)
L
13

Like Leopard's Scripting Bridge before it, JXA intentionally breaks all sorts of stuff that works perfectly in AppleScript. Here is the translation of your original AppleScript command to JXA syntax:

//tell application "System Events" to name of items in folder "/usr"
Application('System Events').folders.byName('/usr').items.name()

The AS version works perfectly, but the JXA equivalent just throws a completely meaningless Error -1700: Can't convert types.

JXA does seem to work if you write diskItems instead of items:

Application('System Events').folders.byName('/usr').diskItems.name()
// --> ["bin", "lib", "libexec", "local", "sbin", "share", "standalone", "X11", "X11R6"]

which suggests JXA indulges in much the same internal "cleverness" that causes SB to break on so many apps. (Note that I found numerous such design defects in earlier testing, but gave up reporting them once it was clear the AS devs only cared about imposing their own personal ideology and prejudices on everyone else, crippled capabilities and broken compatibility be damned.)

For comparison, here's the JavaScriptOSA (JOSA) prototype I quickly put together for the JXA developers' reference some months back (they promptly ignored it, natch):

app('System Events').folders.named('/usr').items.name()
// -> ["bin", "lib", "libexec", "local", "sbin", "share", "standalone", "X11", "X11R6"]

(While not fully finished or tested, JOSA still works a damn sight better than JXA, is better documented, and even includes an auto-translation tool for converting AS commands to JS syntax. Unfortunately, because Apple have legacied or deprecated the AEM, CM, PM, and OSA Carbon APIs, I cannot recommend it for production use; it's purely there for comparison purposes.)

Similarly:

set myPath to POSIX file "/usr"
tell application "System Events" to name of every disk item of folder named myPath
--> {"bin", "lib", "libexec", "local", "sbin", "share", "standalone", "X11", "X11R6"}

myPath = Path('/usr')
Application('System Events').folders.byName(myPath).diskItems.name()
// Error -1728: Can't get object.

var myPath = Path('/usr')
app('System Events').folders.named(myPath).diskItems.name()
// --> ["bin", "lib", "libexec", "local", "sbin", "share", "standalone", "X11", "X11R6"]

You can work around that particular case by converting the Path object to a string, and using that:

myPath = Path('/usr')
Application('System Events').folders.byName(myPath.toString()).diskItems.name()

Although that workaround won't necessarily help in other situations; e.g. it fails in Finder because Finder doesn't understand POSIX-style path strings, and there's no way to get an HFS-style path string (which Finder does understand) from a JXA Path object:

set myPath to POSIX file "/usr"
tell application "Finder" to name of every item of item myPath
--> {"X11", "X11R6", "bin", "lib", "libexec", "local", "sbin", "share", "standalone"}

myPath = Path('/usr')
Application('Finder').folders.byName(myPath.toString()).items.name()
// Error -1728: Can't get object.

And so it goes. (e.g. Try testing JXA's support for range, filter, relative, and insertion reference forms; it's particularly rotten.)

Liana answered 11/10, 2014 at 17:10 Comment(7)
Wow amazingly comprehensive answer. I guess there is not too much problem in making changes (items to diskItems) if it makes things easier to understand (arguably true in this this case) AND there was decent, clear documentation on it. That doesn't seem to be the case at all (well at least not yet, it is still a beta - he said hopefully). That said, if you are going to make semantic changes like this then surely something like Application('Finder').diskItems({atPath:path, return: "names", includeSubDirectories:true, excludeInvisibles:true ) etc would make a lot more sense, at least top me.Anywheres
JXA isn't making changes to make things easier to understand; it's breaking compatibility because its developers don't like and/or understand how Apple event IPC works. Scriptable applications [i]already define all the behaviors[/i]. As a [i]client[/i] of those applications, it's JXA's responsibility to support those behaviors fully and correctly, not throw a hissy fit just because it offends the JXA developers' personal beliefs. It's pathological backseat driving, nothing else.Liana
OK. So is there any way to work out what methods / properties actually apply at any given time. The documentation isn't a lot of help in this regard. I can find in the documentation Application('System Events').folder but no reference to byName(). And you don't seem to be able to build up commands one bit at a time. Application('System Events').folders.byName('/usr') doesn't return anything so how do you know to apply .diskItems.name()to it? Do you just have to know this stuff? Or how do you work it out?Anywheres
A lot of it is experience and educated guesswork, I'm afraid. This is one of several well-known, longstanding deficiencies with the AppleScript platform; alas, the AS team prefers inventing new problems over solving the ones it already has. Application dictionaries are notoriously vague and incomplete, and Script Editor's dictionary viewer isn't particularly good at presenting what info there is. Script Debugger is far better, though you need to pay for that. I recommend you get a good book on AppleScript; it's not something that can be adequately covered in a couple of comments. :/Liana
Great answer... but again, how did you specifically know of the existence of Application('System Events').folders, or its function byName(), or that byName() returned something with a diskItems property, etc.? Where did this JXA-specific information come from?Weanling
@John: Be aware that Apple event IPC is RPC+simple first-class relational queries, not OOP/DOM. To answer your questions: 1. Read the application's dictionary (File>Open Dictionary in Script Editor). 2. Read the JXA release notes. 3. The byName selector returns an object specifier (i.e. query object) that identifies a single element by name. (Objects that have a name property can [almost always] be selected by name.) 4. Learn AppleScript, then figure out how to transfer that knowledge to JXA. (Also realize that JXA is an obfuscated sack of crap compared to AS, which is saying something.)Liana
DiskItem has path which returns a : delimited path string and posixPath, which returns a / delimited path string. Ex: Application('System Events').folders.byName(myPath.toString()).diskItems.posixPath(); • I'm running on 10.13 High SierraCarmelinacarmelita
B
8

The "System Events" approach certainly does have the merit of simplicity, but it turns out that using $.NSFileManager (now directly available for scripting) gives noticeably faster performance.

On my system, starting for comparison with

var strPath = '/usr';

var appSys = Application('System Events'),
lstHarvest = appSys.folders.byName(strPath).diskItems.name();

A quick test with a few thousand iterations suggests that we can consistently speed it up by a modest 40% with this slightly baroque approach:

var strPath = '/usr';
var fm = $.NSFileManager.defaultManager,
    oURL = $.NSURL.fileURLWithPathIsDirectory(strPath, true),
    lstFiles = ObjC.unwrap(
        fm.contentsOfDirectoryAtURLIncludingPropertiesForKeysOptionsError(
            oURL, [], 1 << 2, null
        )
    ),
    lstHarvest = [];

lstFiles.forEach(function (oItem) {
    lstHarvest.push(
        ObjC.unwrap(oItem.path)
    );
});

and by well over 300% with the rather simpler:

var strPath = '/usr';
var fm = $.NSFileManager.defaultManager,
    lstFiles = ObjC.unwrap(
        fm.contentsOfDirectoryAtPathError(strPath, null)
    ),
    lstHarvest = [];

lstFiles.forEach(function (oItem) {
    lstHarvest.push(
        ObjC.unwrap(oItem)
    );
});
Boyce answered 5/2, 2015 at 21:0 Comment(0)
M
2

I can get it like this

 foldersList =  foldersList =  System.folders.byName("usr").folders.name()

-> ["bin", "lib", "libexec", "sbin", "share", "standalone", "X11"]

And even this works:

   foldersList =  System.folders.byName("/Users/USERName/Documents/").folders.name()

But I so far cannot get the Path command to work on anything but 'open'

Macule answered 11/10, 2014 at 8:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.