How Do I Get List of Only Normal Files in Folder?

asobjc

(Jim Underwood) #1

I want to get a list of only normal, visible, non-alias, non-symlink files (no folders) in a folder. I’ve done some research and testing with variations of the below script, but have not been successful.

I have reviewed @ShaneStanley’s book Everyday AppleScriptObjC, Third Edition, particularly Script 15-7, 15-8, pp 68-69, but don’t see how to add the extra filters/options to exclude folders, aliases, symlinks.

Any ideas?

Script by @estockly, based on SD Clipping


Update and Solution

As indicated below, @ShaneStanley answered my question in post #2. However, his post is a lengthly one to help us understand his ultimate script. So, in the interest of brevity and clarity, here is Shane’s script that specifically met my need:

Now to your question. You want the files, excluding aliases and symlinks. Two potentially relevant keys are NSURLIsSymbolicLinkKey and NSURLIsAliasFileKey. But there’s also NSURLIsRegularFileKey, which defines “whether the resource is a regular file, as opposed to a directory or a symbolic link”. And once you start dealing with more keys, the second form is more efficient. So:

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

on listFilesNotAliasesOrSymlinksIn:sourceFolder
  set regKey to current application's NSURLIsRegularFileKey
  set packageKey to current application's NSURLIsPackageKey
  set aliasKey to current application's NSURLIsAliasFileKey
  set pathKey to current application's NSURLPathKey
  set fileManager to current application's NSFileManager's defaultManager()
  set aURL to current application's |NSURL|'s fileURLWithPath:sourceFolder
  set reqKeys to current application's NSArray's arrayWithArray:{regKey, packageKey, aliasKey, pathKey}
  
  set theURLs to fileManager's contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:reqKeys options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
  set theResults to current application's NSMutableArray's array() -- to hold result
  repeat with aURL in theURLs
    set {theValues, theError} to (aURL's resourceValuesForKeys:reqKeys |error|:(reference))
    (theResults's addObject:theValues)
  end repeat
  
  set thePred to current application's NSPredicate's predicateWithFormat:" (%K == YES AND %K == NO) OR %K == YES" argumentArray:{regKey, aliasKey, packageKey}
  theResults's filterUsingPredicate:thePred
  return (theResults's valueForKey:pathKey) as list
end listFilesNotAliasesOrSymlinksIn:


(Shane Stanley) #2

Excuse the long answer…

There’s no simple built-in filtering. You can get information about NSURLs, such as whether they represent directories or aliases, but the values aren’t simple properties — they use methods that themselves require keys, so you can’t use shortcuts like valueForKey:. You have to use use a repeat loop.

So first consider the simpler task, getting all the files — that is, everything that’s not a folder (like Finder’s files of). There are two relevant keys: NSURLIsDirectoryKey, which tells you if a URL is a directory (that is, a folder or a package), and NSURLIsPackageKey, which tells you whether a URL is a package. So you typically check if a URL is a directory, and if it is, whether it’s a package. So something like this:

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

on listFilesIn:sourceFolder
	set dirKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set fileManager to current application's NSFileManager's defaultManager()
	set aURL to current application's |NSURL|'s fileURLWithPath:sourceFolder
	
	set theURLs to fileManager's contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:{} options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	set theResults to current application's NSMutableArray's array() -- to hold list
	repeat with aURL in theURLs
		set isFile to true
		-- is it a directory?
		set {theResult, theValue, theError} to (aURL's getResourceValue:(reference) forKey:dirKey |error|:(reference))
		if theValue as boolean then
			-- is it a package?
			set {theResult, theValue, theError} to (aURL's getResourceValue:(reference) forKey:packageKey |error|:(reference))
			if not theValue as boolean then
				set isFile to false
			end if
		end if
		if isFile then
			(theResults's addObject:aURL)
		end if
	end repeat
	
	return (theResults's valueForKey:"path") as list
end listFilesIn:

You can also get the value for several keys at once, in the form of a dictionary, then use a predicate to filter the items you want. Like this:

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

on listFilesIn:sourceFolder
	set dirKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set pathKey to current application's NSURLPathKey
	set fileManager to current application's NSFileManager's defaultManager()
	set aURL to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set reqKeys to current application's NSArray's arrayWithArray:{dirKey, packageKey, pathKey}
	
	set theURLs to fileManager's contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:reqKeys options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	
	set theResults to current application's NSMutableArray's array() -- to hold result
	repeat with aURL in theURLs
		set {theValues, theError} to (aURL's resourceValuesForKeys:reqKeys |error|:(reference))
		(theResults's addObject:theValues)
	end repeat
	
	set thePred to current application's NSPredicate's predicateWithFormat:"%K == NO OR %K == YES" argumentArray:{dirKey, packageKey}
	theResults's filterUsingPredicate:thePred
	return (theResults's valueForKey:pathKey) as list
end listFilesIn:

This is a little quicker, although not much.

Now to your question. You want the files, excluding aliases and symlinks. Two potentially relevant keys are NSURLIsSymbolicLinkKey and NSURLIsAliasFileKey. But there’s also NSURLIsRegularFileKey, which defines “whether the resource is a regular file, as opposed to a directory or a symbolic link”. And once you start dealing with more keys, the second form is more efficient. So:

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

on listFilesNotAliasesOrSymlinksIn:sourceFolder
	set regKey to current application's NSURLIsRegularFileKey
	set packageKey to current application's NSURLIsPackageKey
	set aliasKey to current application's NSURLIsAliasFileKey
	set pathKey to current application's NSURLPathKey
	set fileManager to current application's NSFileManager's defaultManager()
	set aURL to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set reqKeys to current application's NSArray's arrayWithArray:{regKey, packageKey, aliasKey, pathKey}
	
	set theURLs to fileManager's contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:reqKeys options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	set theResults to current application's NSMutableArray's array() -- to hold result
	repeat with aURL in theURLs
		set {theValues, theError} to (aURL's resourceValuesForKeys:reqKeys |error|:(reference))
		(theResults's addObject:theValues)
	end repeat
	
	set thePred to current application's NSPredicate's predicateWithFormat:" (%K == YES AND %K == NO) OR %K == YES" argumentArray:{regKey, aliasKey, packageKey}
	theResults's filterUsingPredicate:thePred
	return (theResults's valueForKey:pathKey) as list
end listFilesNotAliasesOrSymlinksIn:

Edited as discussed below.


(Nigel Garvey) #3

Hi Shane.

The second script’s actually a bit slower than the first on my machine when run against my Preferences folder. But it can be speeded up by presetting reqKeys as an NSArray instead of a list and also by using it as the includingPropertiesForKeys: value when the URLs are fetched.


(Shane Stanley) #4

I found the speed ratio varied depending on the composition of the folder, in terms of ratio of folders and packages. In some cases one was faster, in others it was slower. I was generalising on the basis of a few I tried, which were probably not representative of much at all. I didn’t see any particularly significant differences, though — just a slight trend.

Interestingly I could never discern any difference with or without the includingPropertiesForKeys: value.

To my mind the bigger issue is that the predicate method comes into its own as the number of parameters rises (which is also where nested if clauses otherwise also start to get gnarly).


(Nigel Garvey) #5

Yeah. What’s in the folder determines what the script actually does and therefore how long it takes to do it.

I don’t see much difference myself with your first script, which only uses two resources per item. The second uses three, and passing the reqKeys list as the includingPropertiesForKeys: value reduces the running time (with my folder) from around 0.066 seconds to around 0.056 seconds — ie. by about a sixth. Presetting reqKeys as an array saves the scripting bridge having to convert it to one every time round the repeat and further reduces the running time to 0.048 seconds. There are similar savings when these modifications are applied to your third script.


(Shane Stanley) #6

I thought I tried it with that case, but I must have missed it. It does make a difference.

Yes, that makes sense.

I’ll edit the above scripts accordingly, for future browsers.


(Shane Stanley) #7

I’d also add that if what you want is an array of URLs or list of «furl»s rather than paths, using predicates is likely to be be slower.


(Nigel Garvey) #8

Maybe these would do for «furls»:

use AppleScript version "2.5" -- El Capitan (10.11) or later
use framework "Foundation"
use scripting additions

on listFilesIn:sourceFolder -- POSIX path parameter.
	set regKey to current application's NSURLIsRegularFileKey
	set packageKey to current application's NSURLIsPackageKey
	set fileManager to current application's NSFileManager's defaultManager()
	set aURL to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set reqKeys to current application's NSArray's arrayWithArray:{regKey, packageKey}
	
	set theURLs to fileManager's contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:reqKeys options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	set theURLs to theURLs's mutableCopy() -- The code below works without this in El Capitan and High Sierra!
	repeat with aURL in theURLs
		set {theValues, theError} to (aURL's resourceValuesForKeys:reqKeys |error|:(reference))
		if (theValues as record as list) does not contain true then set aURL's contents to Wednesday
	end repeat
	
	return (theURLs as list)'s every «class furl»
end listFilesIn:

And:

use AppleScript version "2.5" -- El Capitan (10.11) or later
use framework "Foundation"
use scripting additions

on listFilesNotAliasesOrSymlinksIn:sourceFolder -- POSIX path parameter.
	set regKey to current application's NSURLIsRegularFileKey
	set packageKey to current application's NSURLIsPackageKey
	set aliasKey to current application's NSURLIsAliasFileKey
	set fileManager to current application's NSFileManager's defaultManager()
	set aURL to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set reqKeys to current application's NSArray's arrayWithArray:{regKey, packageKey, aliasKey}
	set acceptableResults to {(current application's NSDictionary's dictionaryWithObjects:{true, false, false} forKeys:reqKeys) as record, (current application's NSDictionary's dictionaryWithObjects:{false, true, false} forKeys:reqKeys) as record}
	
	set theURLs to fileManager's contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:reqKeys options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	set theURLs to theURLs's mutableCopy() -- The code below works without this in El Capitan and High Sierra!
	repeat with aURL in theURLs
		set {theValues, theError} to (aURL's resourceValuesForKeys:reqKeys |error|:(reference))
		if ({(theValues as record)} is not in acceptableResults) then set aURL's contents to Wednesday
	end repeat
	
	return (theURLs as list)'s every «class furl»
end listFilesNotAliasesOrSymlinksIn:

Edit: I’ve just realised that both scripts change the contents of a supposedly immutable array! They work in both El Capitan and High Sierra though.
Further edit: As a precaution, theURLs is now explicitly a mutable copy in both scripts.


(Jim Underwood) #9

Thanks, Shane. This is perfect. Exactly what I was looking for.
Now that I see your code, I realize I would never have been able to figure that out.

And thanks @NigelGarvey – I always appreciate, and learn from, your discussion and scripts.


(Shane Stanley) #10

You could insert the following to be on the safe side:

	set theURLs to theURLs's mutableCopy()

It’s not like it will make any appreciable difference in performance.


(Nigel Garvey) #11

Yes. I did realise that. I was simply marvelling that it’s possible to alter the contents of an NSArray from vanilla AppleScript. It works with set item i of theURLs to Wednesday too.

But I’ve now made theURLs explicitly mutable in both scripts. :slight_smile:


(Shane Stanley) #12

An NSMutableArray is still an NSArray. When a method says it returns an NSArray, it can return an object of any subclass of NSArray, including mutable ones.

We looked at this in terms of what to label results in Script Debugger. Something that, surprisingly, Objective-C doesn’t support is any method for checking if an object is mutable (short of trying it).