Find Any File Scripts

Starting with FAF version 2.3.3, FAF's search rules can be extended by writing scripts (programming code) in the Lua and JavaScript (short: JS) languages.

This makes it possible to create very specific and complex search rules.

Scripting is an advanced feature for power users. If you don't feel comfortable with writing script code, look below for a list of readily available scripts you can download and use, or ask me for assistance.

Later versions of FAF may add more scripting features, such as for displaying custom columns and more information about listed files and folders, and offering more commands in the contextual menu for found items.

Installing scripts

FAF's AppSupport folder (which you can open by opening the menu bar, under Help, and choosing Open the AppSupport folder) contains a Scripts folder, which in turn contains sub folders for the different types of scripts:

You can also reach the scripts folder by clicking on the rightmost pop-up menu for the "Script" rule:

Open Script Folder

Place the script into the appropriate sub folder (i.e. Matching), and make sure its name ends in either .lua, .js or .javascript.

Then click on the right-most pop-up menu again, where you will now find the newly installed script. Choose the script from the menu to use it with the Script rule.

Writing new scripts

Scripts for matching during search

These are scripts that are placed into the Matching folder. For instance, if you have a file named test.lua in that folder, you can choose it in the Find window as shown:

Script rule

A simple script that imitates the "Name contains good" rule would look like this:

Lua:

function match(diskItem)
    if string.find(diskItem.name, "good") then
        return true
    end
end

JavaScript:

function match(diskItem) {
    if (diskItem.name.includes("good")) {
        return true;
    }
}

Basically, you have to implement a function named match that takes one argument, and returns either true or false (returning nothing is fine, too, and is the same as returning false). The one argument is an object providing access to many properties of the to-be-checked item (i.e. either a file or a folder). The properties will be listed in a section further below.

A script's lifetime and storing data persistently

The script's context, i.e. where it stores its global properties, gets re-initialized after each search operation. That means a script can't keep its values across multiple searches.

It could store some information in FAF's preferences, but if your script attempts this, it should take care to use a unique name for the preference key, ideally based on the name of the script, or on a reverse domain name you own. And the data stored should be kept small; a few 100 bytes is okay, a path list of all found items would not be, as that could easily become megabytes of data, which the prefs system is not suited for.

Another option would be to store data in a text file, using the faf.fileLoad and faf.fileSave functions. If you do not pass an absolute path but only a file name, the file will be located inside a folder named "Storage" next to the script file. That way, you won't have to figure out where best to store the data for the script; just pass a resonable file name.

Configuration options

If the source code has a single-line comment containing "_FAF_Config_" in the form

_FAF_Config_ ( option1, option2 ) 

then the one or more listed options will be used to configure the rule's appearance and/or behavior.

The following options are available:

Debugging

If you use a script, and if the script has an issue, FAF will display a message about the encounted syntax or runtime error in the Find window, and abort the search or otherwise stop using the script until you edit it.

JavaScript

Debugging your JS scripts is fairly easy thanks to Apple having provided the JavaScriptCore engine to run the scripts and linking it with Safari's Web Inspector:

Safari debugger

For this to work, you need a special version of FAF that allows the debugger to be enabled. You should find this special version either here for release versions and here for beta versions. These versions have a reduced safety state (i.e. they're not notarized). This means that you cannot launch these versions with a double click after downloading them. Instead, you need to ctrl-click on the app's icon and choose "Open" from the menu, then confirm to open it (you'll have to do this only once). After that, this version will work just like the regular version, with the added benefit of being able to use the JavaScript debugger.

But note that this debug-enabled version can't automatically be updated by FAF's self-update mechanism. To update FAF, you will need to run the regular version instead. Then, if you want to debug JavaScript again, you need to download the new special version manually again, as explained above.

Now, to enable JS debugging (which includes support for the console.log() command), launch Safari, open its Preferences window, switch to the Advanced tab and check the "Show Developer menu in menu bar" option. With that, you'll find a new Develop item in the menu bar. In that, find the row titled after your Mac's name (3rd from the top, usually), and check the options "Automatically Show Web Inspector for JSContexts" and "Automatically Pause Connecting to JSContexts" inside the submenu.

Now, if you use a Script in FAF, Safari will open a Web Inspector window in which you can see the Console output, view the source code, set breakpoints into the match function, view the diskInfo object's properties and their values, and single-step through the code to see what it does.

Turn the options in the Develop menu off again once you're finished with debugging your script, or the Web Inspector windows will keep popping up when FAF runs any scripts.

Lua

Unfortunately, there's no advanced debugging support for Lua with FAF. Therefore, consider using JavaScript and Safari's Web Inspector if you want to write more complex scripts, as it'll probably make it easier.

You can, however, use faf.logInfo("some text") to write a line of text to the FAF.log file, which you can view by opening the menu bar, under Help, and choose Show Log

Objects and operations (events) provided to the scripts

function match (diskItem)

The optional match function, when implemented by the script, is called during a search repeatedly, being passed every disk item (file or directory) that has been left after matching by any other previous rules in the search. The function then has to return true to keep the item matched. If all rules agree that an item matches, it will be added to the "found results". Since the scripts usually run a bit slower than FAF's built-in rules, scripts will therefore always be invoked last (but before file content rules), in order to sort out any misses (non-matches) more quickly before invoking the script with the remaining items that the other rules have matched. If the match function is not declared in the script, then its rule will always match.

function searchHasStarted ()

The optional searchHasStarted function, when implemented by the script, is called once a search starts. The global variable currentSearch will be set at this point (and remains available until after the search has ended). The function gets a ruleInfo passed, which has information about the rule the script is running under.

function searchHasEnded (completed, foundItems)

The optional searchHasEnded function, when implemented by the script, receives two arguments: The first is a boolean stating whether the search was completed (as opposed to being stopped by the user), and the second provides all the found disk items in an array.

function skipDirectory (diskItem)

The optional searchHasEnded function, when implemented by the script, gets called during a "slow" search for every directory it encounters. If this function returns true, that directory's contents won't be searched.

function finishedDirectory (diskItem)

The optional finishedDirectory function, when implemented by the script, gets called during a "slow" and recursive search (set currentSearch.searchMode to 3 to enable) for every directory it had traversed into.

The "disk item" object

The properties of the disk item objects can be seen in the Web Inspector (see above, Debugging). Here's a list (which is manually collected and thus may be outdated or incorrect):

volumeName: string
name: string                // normalized, POSIX format (using ":" in names, not "/")
localizedName: string       // as shown by Finder (using "/" in names, not ":") 

path: string
canonicalPath: string       // normalized & canonical
normalizedPath: string      // normalized
parentPath: string          // normalized

fileSize: number            // combined data and resource fork size
dataSize: number            // data fork size only
resourceSize: number

fileType: string            // the UTI
kind: string

tagsArray: array of string 

typeCode: number
typeCodeString: string
creatorCode: number
creatorCodeString: string

ownerID: number
groupID: number

fileIDNumber: number
inodeNumber: number

creationDate: date
modificationDate: date
lastContentAccessDate: date
addedToDirectoryDate: date

labelNumber: number
localizedLabel: string

exists: boolean
isDirectory: boolean
isRegularFile: boolean
isSymlink: boolean
isAlias: boolean
isHidden: boolean
isVolume: boolean
isPackage: boolean
isTrashed: boolean
isTrashFolder: boolean
isLocked: boolean
isSystemProtected: boolean
isContentOfPackage: boolean
isItemOrParentItemHidden: boolean

parentItem: DiskItem    // returns the parent object, or nil (undefined) for the root dir

function resourceValueNamed(name): any type

The date type is actually a number in Lua. You can then use the os.date() function to extract day and time from it.

For resourceValueNamed(), pass any of the names listed as Key under NSURLResourceKey.

For now, all these properties are read-only.

The "faf" global

faf is a global object that provides general functions and properties:

// these log functions write to the "FAF.log" file; see FAF's Help menu
function logInfo(msg)
function logWarning(msg)
function logError(msg)

// get and set FAF's preferences
function prefsValue(key): any type of value
function setPrefsValue(key, value)

// execute a command and wait for it to end. For example, in JavaScript:
//   let output = faf.runCommandWithArgs("/bin/ls", ["-l","/"])
// or to run a shell with a free command string:
//   let cmd = "ls -la ~"
//   let output = faf.runCommandWithArgs("/bin/sh", ["-c",cmd])
function runCommandWithArgs(cmd, args): string // args is an array of strings

// Load from and write to text files; passing an empty string to the
// path parameter will use the script's file name, and passing no
// absolute path but only a file name will put the file into a folder
// named "Storage" next to the script file.
function fileLoad(path): string
function fileSave(path, content): boolean
function fileAppend(path, content): boolean

function showAlert(title, subtext)
	// shows a modal dialog with an "OK" and a "Stop" button

function showNotification(title, subtext)
   // shows a notification in the top right screen corner
   // (if not disabled in System Preferences by the user)

function beep()             // plays the system alert sound
function playSound(name)    // e.g.: faf.playSound("Frog")

scriptFileName: (read-only) returns the file name of the script

For example, to write a line to the log file, use:

faf.logInfo ("some info")

The "currentSearch" global

This is an object that only exists during a search.

// Set and retrieve named values that persist across
// all involved matching scripts during a search:
function setSharedValue(name, value)
function sharedValue(name): any type of value

currentSearchTarget:
   (read-only) A disk item that specifies which volume or folder
   is currently searched.

statusMessage:
   (string, r/w) When assigning a string to this, the search will be
   stopped and the text be shown in the Find window. This can be used
   to show an error message as well as to show extra information at
   the end of a successful search (when set from `searchHasEnded`).

input:
   (read-only) If the script has requested to show an input field
   with the rule, this property will contain the text the user has
   typed in. It's either a string or an array of strings, depending
   on the config value (`wantsInput` or `wantsInputMultiLine`).

// These settings are to be applied to all comparisons
// and can be changed with related rules:
caseSensitiveNames (boolean, read-only)
diacriticsSensitiveNames (boolean, read-only)
caseSensitiveContent (boolean, read-only)
negateConditions (boolean, read-only)

// The following properties can only be changed from
// within the `searchHasStarted` function:
searchMode (integer, r/w):
   0: default (prefers fast search), 1: forced slow mode,
   2: prefer recursive in slow mode, 3: force recursive mode
   (slower than 1 but needed for `finishedDirectory` callback).
spotlightMode (integer, r/w):
   0: no Spotlight use, 1: include Spotlight query, 33: Spotlight only.
calculateFolderSizes (boolean, r/w):
   only used in recursive search mode, calculates the size of every
   searched folder, and can be fetched in the `finishedDirectory`
   callback with `f.resourceValueNamed("NSURLTotalFileSizeKey")`.

Example scripts

Find executable files

JavaScript version (save this as "is executable.js" to the Scripts/Matching folder):

function match(f) {
    if (! f.isRegularFile) { // sorts out directories and symlinks
        return false
    }
    isExecutable = f.resourceValueNamed ("NSURLIsExecutableKey")
    return isExecutable
}

Lua version (save this as "is executable.lua" to the Scripts/Matching folder):

function match(f)
    if not f.isRegularFile then -- sorts out directories and symlinks
        return false
    end
    isExecutable = f.resourceValueNamed ("NSURLIsExecutableKey")
    return isExecutable
end

Find files and folders by length of their name

Here is a lua script that also shows how to specify that an input value is required (save as "minimum name length.lua"):

-- a Lua program, see https://findanyfile.app/scripting.php
--
-- _FAF_Config_ (input=single)

minlen = 0

function searchHasStarted ()
	minlen = tonumber (currentSearch.input)
	if minlen <= 0 then
		currentSearch.statusMessage = "input must be a number > 0"
	end
end

function match(f)
	return string.len (f.name) >= minlen
end

BTW, you could also do this name length check with a Regex rule:

Regex length rule

The above rule finds any name that is at least 25 bytes long (plain "ASCII" characters such as A-Z and digits count as one byte, whereas non-latin characters may count as 2 to 5 bytes, though, so using regex may not work well if you're using non-latin scripts for your file names).

Find files whose names are not allowed in Microsoft OneDrive

Certain file names cannot be used with OneDrive. To identify them, use this script (save it as "OneDrive Name Issues.lua"):

-- Finds file names that will cause an issue if moved to Microsoft OneDrive.
-- Save this to FAF's Scripts/Matching folder with a name ending in ".lua".
-- For more info, see https://findanyfile.app/scripting.php

function match(f)
	name = f.name
	firstchar = string.byte (name, 1)
	lastchar = string.byte (name, -1)
	
	-- these chars may not appear in file names: /\:"*?
	if name:find "[/\\:\"%*%?]" then
		return true
	end
	
	-- names may not start nor end with blanks
	if firstchar == 32 then
		return true
	end
	if lastchar == 32 then
		return true
	end
	
	-- names may not end with a period
	if lastchar == 46 then
		return true
	end
end

To fix the file names of these items, you may use renamer programs such as NameChanger: Install the program, then select the found items in FAF, open the "Services" menu (e.g. by right-clicking on the selection) and choose "Rename with NameChanger" and then enter various replacement rules for "Original Text" and "New Text", such as replacing each of the invalid chars with an underscore ("_") or a dash ("-") character.

Find folders with a minimum amount of files and folders inside

(Save as "minimum item count.lua")

-- a Lua program, see https://findanyfile.app/scripting.php
--
-- _FAF_Config_ (input=single)

targetItemCount = 0

function searchHasStarted ()
	-- Fetch the user's input and make sure it's a sensible number, i.e. > 0
	targetItemCount = tonumber (currentSearch.input)
	if targetItemCount <= 0 then
		currentSearch.statusMessage = "input must be a number > 0"
	else
		currentSearch.searchMode = 3 -- forces recursive search that performs the folder calculations we need below
	end
end

function match(f)
	-- let's not match anything by default (we'll do it below)
	return false
end

function finishedDirectory(f)
	-- determine the currently searched folder's item count
	currentItemCount = (f.resourceValueNamed("dirFileCount") or 0) + (f.resourceValueNamed("dirFolderCount") or 0)
	-- if the count reaches the input value, then make this folder a match that'll appear in the results
	if currentItemCount >= targetItemCount then
		currentSearch.addMatch (f)
	end
end

Find folders with a minimum amount of files and folders inside

(Save as "minimum folder size.lua")

-- a Lua program, see https://findanyfile.app/scripting.php
--
-- _FAF_Config_ (input=single)

targetSize = 0

function searchHasStarted ()
	-- Fetch the user's input and make sure it's a sensible number, i.e. > 0
	targetSize = tonumber (currentSearch.input)
	if targetSize <= 0 then
		currentSearch.statusMessage = "input must be a number > 0"
	else
		currentSearch.searchMode = 3 -- forces recursive search that performs folder calculations
		currentSearch.calculateFolderSizes = true -- needed for getting "NSURLTotalFileSizeKey" set
	end
end

function match(f)
	-- let's not match anything by default (we'll do it below)
	return false
end

function finishedDirectory(f)
	-- determine the currently searched folder's content size
	currentSize = (f.resourceValueNamed("NSURLTotalFileSizeKey") or 0)
	-- if the size reaches the input value, then make this folder a match that'll appear in the results
	if currentSize >= targetSize then
		currentSearch.addMatch (f)
	end
	-- optional logging of values:
	--   faf.logInfo (f.name .. ": " .. tonumber(currentSize))
end