faf-big

Find Any File (FAF)

Key Features

  • Convenient folder and icon views for results
  • Can search in other users' home folders ("root" mode)
  • Queries can be saved for easy re-use
  • Can be launched with a self defined keyboard shortcut

Find Any File is Shareware

You may try it out without buying first. Simply download it.

If you keep using it you are expected to pay for it, though.

New in version 2.4:

New in version 2.3.2:

  • Fixes search issues around macOS Catalina, Big Sur and Monterey.
  • Can now search on new Google Drive.
  • Search for inodes and diacritics-insensitive.
  • Customizable Dock icon (ctrl-click on it!).

New in version 2.3.1:

  • Fixes some critical search issues with macOS Catalina and El Capitan.

New in version 2.3:

  • Native Apple Silicon (M1) code.
  • Search for Tags
  • Search for and display Date Last Opened and Date Added
  • Works with Alfred, Keyboard Maestro, PopClip etc.

New in version 2.2.1:

  • Fixes a potential crash.
  • The Find window doesn't get excessively wide any more.

New in version 2.2:

  • Ready for macOS Big Sur.
  • You can now save and re-open the results.

New in version 2.1.1:

  • Icons in Preview Grid should look correct again.
  • Does not remove Volumes from Login Items any more.

New in version 2.1:

  • Includes Spotlight for even faster results.
  • Many bug fixes.

New in version 2.0:

  • FAF is now a 64 bit app.
  • Shows results as soon as they're found.
  • Can search by Kind (Images, Audio, etc.).
  • Can search with regular expressions.

New in version 1.9.4:

  • Compatible with version 2 in regards to preferences and .faf files.
  • Several bug fixes.

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:

  • Matching – These are used as search rules, which you can then choose from once you select the Script matches rule.

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:

  • input or input=single – will add a single-line input field to the rule in the Find window. The input value can be queried by the script using currentSearch.input, which will be a string. The rule will only be usable if the user has input something into the text field.
  • input=multi – will add a multi-line input field. currentSearch.input will return an array of strings in this case. An empty input will disable the rule.
  • inputOptional – The rule doesn't require input, i.e. even with no text in the input field the rule will be run. Only applies if input is also declared.
  • persistentContext – The lifetime (see above) will be extended as long as possible, meaning that it won't create a new context for each search (but will still renew the context if the script was stopped due to an error or when clicking Stop in faf.showAlert() or whenever the script's source file gets updated).

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, or macOS will tell you that the app is damaged. Instead, you first need to remove the quarantine state from the app by issuing this command in Terminal.app: xattr -d com.apple.quarantine /path/to/FindAnyFile.app (you need to enter the correct path to the app, of course). Then ctrl-click on the app's icon and choose "Open" from the menu, and 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
finderComment: string

exists: boolean
isDirectory: boolean
isRegularFile: boolean
isSymlink: boolean		// true for symlinks only
isAlias: boolean		// true for symlinks and Finder Aliases
isHidden: boolean		// true only if the item itself is invisible or its name starts with a "."
isItemOrParentItemHidden: boolean	// true if hidden or inside a hidden folder
isVolume: boolean		// true if it's the root folder of a volume
isPackage: boolean
isTrashed: boolean
isTrashFolder: boolean
isLocked: boolean
isSystemProtected: boolean
isContentOfPackage: boolean		// true if inside a package (bundle)
isDataless: boolean		// true if the file or folder is offline, i.e. its data is only in the cloud

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 (`input=single` or `input=multi`).

// 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")`.

addMatch(diskItem)
	adds the disk item to the results.

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 purgeable and sparse files

Works on macOS 11 and later. See Which files are purgeable? for details.

purgeable file test

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

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

sparse file test

Lua version (save this as "Is sparse file.lua" to the Scripts/Matching folder):

function match(f)
    if not f.isRegularFile then -- sorts out directories and symlinks
        return false
    end
    return f.resourceValueNamed ("NSURLIsSparseKey")
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 "Name length.lua"):

-- a Lua program, see https://findanyfile.app/scripting.php
--
-- _FAF_Config_ (input=single)
--
--
-- The user may enter a comparator symbol (=, <, >) and then a number to compare against.
--
-- Examples:
--   >80   -- checks for names longer than 80 chars. Same as >=81 (which you cannot use).
--   =12   -- checks for names of exactly 12 chars in length.
--   <5    -- checks for names shorter than 4 chars. Same as <=4 (which you cannot use).

name_length = 0
comparator = ""

function searchHasStarted ()
	-- Fetch the user's input and make sure it's valid
	local input = currentSearch.input
	comparator = string.sub (input, 1, 1)
	if not (comparator == "=" or comparator == "<" or comparator == ">") then
		currentSearch.statusMessage = "Input must start with =, < or >"
	else
		name_length = tonumber (string.sub (input, 2, -1))
		if name_length <= 0 then
			currentSearch.statusMessage = "Input must provide a number > 0"
		end
	end
end

function match(f)
	local s = f.name
	if comparator == "=" then
		return string.len (s) == name_length
	elseif comparator == "<" then
		return string.len (s) < name_length
	elseif comparator == ">" then
		return string.len (s) > name_length
	else
		currentSearch.statusMessage = "Invalid comparator in script's match function"
	end
end

BTW, you could also perform a 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 of a minimum size (of the total files 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

Find the parent folders of specific files

Use this rule as the last rule with other rules that match specifc files. That way, if the other (previous) rules find a match, this script will then drop those matches and instead mark the parent folder as a match.

(Save as "Parent folders of matched items.lua")

-- a Lua program, see https://findanyfile.app/scripting.php

function searchHasStarted ()
	currentSearch.searchMode = 3 -- forces a recursive search
end

function match(f)
	-- if we get here, previous rules have already matched - which means we drop
	-- this match and instead match its parent directory
	currentSearch.addMatch (f.parentItem)
	return false
end

Find folders that do not contain specific files

Use this rule as the last rule with other rules that match specifc files. That way, if the other (previous) rules find a match, this script will then drop those matches and remember that the folder had at least one match. It will then instead mark those folder as matched that had no matches inside.

(Save as "Folders with no matches.lua")

-- a Lua program, see https://findanyfile.app/scripting.php

function searchHasStarted ()
	currentSearch.searchMode = 3 -- forces a recursive search
end

matchStack = {}

function skipDirectory (diskItem)
	-- we're entering a new directory - put a fresh "has no match" flag onto the stack
	table.insert (matchStack, false)
	return false
end

function finishedDirectory (diskItem)
	-- directory has been searched - did we have matches inside?
	local hadMatch = table.remove (matchStack)
	if not hadMatch then
		currentSearch.addMatch (diskItem)
	end
end

function match (diskItem)
	-- if we get here, previous rules have already matched - which means we drop
	-- this match and instead remember that this directory had a match
	matchStack[#matchStack] = true
	return false
end

Find folders that do not contain files with specific extensions

Use this rule to locate folders that are missing files with the given extensions, any levels deep.

-- See https://findanyfile.app/scripting.php
--
-- Save as "Folders without extensions.lua" to FAF's "Scripts/Matching" folder.
--
-- FAF script for listing folders that do not contain files with a set of specified extensions.
-- To specify multiple extensions, enter them with a blank (space) as a separator.
--
-- _FAF_Config_ (input=single)

-- "deep" setting: Choose true or false. "false" means to look for the extensions only in the
-- same folder as the matched file, "true" will also search all deeper folders.
deep = true

exts = {}

if deep then
	ls_cmd = "find ."
else
	ls_cmd = "ls -1"
end

function searchHasStarted ()
	-- Build a shell command that lists all files with the given extensions
	-- This turns an input like "png jpeg" into the cmd "ls -1 *.png *.jpeg"
	local input = currentSearch.input
	for ext in input:gmatch("%w+") do table.insert(exts, ext) end
	for idx = 1, #exts do
		local ext = exts[idx]
		if ext ~= "" then
			if not startswith(ext, ".") then
				ext = "." .. ext
			end
			if deep then
				if idx > 1 then
					ls_cmd = ls_cmd .. " -or"
				end
				ls_cmd = ls_cmd .. " -iname '*" .. ext .. "'"
			else
				ls_cmd = ls_cmd .. " *" .. ext
			end
		end
	end
	-- faf.logInfo ("Script cmd: " .. ls_cmd)
end

visitedDirs = {}

function match (diskItem)
	-- if we get here, previous rules have already matched - which means we drop
	-- this match and instead remember that this directory if there's none of the exts inside
	local parentItem = diskItem.parentItem
	local path = parentItem.path
	local visited = visitedDirs[path]
	if not visited then -- we need to check each dir only once
		visitedDirs[path] = true
		-- Are there any files of the input's extensions in the same
		-- directory as the currently matched file?
		local cmd = 'cd "' .. path .. '" ; ' .. ls_cmd .. " 2>/dev/null"
		local res = faf.runCommandWithArgs ("/bin/sh", {"-c",cmd})
		res = trim(res)
		if res == "" then
			-- there were no matching files in the dir
			currentSearch.addMatch (parentItem)
		else
			-- there were matching files in the dir
		end
	end
	return false
end

function startswith(text, prefix)
    return string.sub(text, 1, #prefix) == prefix
end

function trim(text)
	return string.gsub(text, "%s+", "")
end

Find empty folders

Use this rule to identify directories (which includes folders and bundles) that contain no files apart from the insignificant .DS_Store files or contain only directories that in turn are determined to be empty as well.

This is effectively the same operation that you can also get when using my free tool Find Empty Folders.

(Save as "Is empty directory.lua")

-- "Is empty directory.lua"
--
-- a Lua program, see https://findanyfile.app/scripting.php

function searchHasStarted ()
	currentSearch.searchMode = 3 -- forces a recursive search
end

isEmptyDirectoryStack = {}	-- an array of booleans for the currently scanned folder and all its parents

-- The calling order of the following functions is as follows:
--
-- match() - can be a file or directory. If it's a directory, then:
--   skipDirectory() if it's a directory
--   recursion (i.e. calling match, skipDirectory, finishedDirectory for all items inside)
--   finishedDirectory() if it's a directory

function match(f)
	-- remember whether this directory has a visible item inside
	if f.isDirectory then
		-- we'll let finishedDirectory() handle this later, after its contents have been checked
	else
		-- check a file inside the current directory
		if f.name == ".DS_Store" then
			-- let's ignore this often occuring file inside otherwise empty directories
		else
			--faf.logInfo("dir not empty bc of file: "..f.path)
			isEmptyDirectoryStack[#isEmptyDirectoryStack] = false
		end
	end
	return false
end

function skipDirectory (f)
	-- we're entering a new directory - put a fresh "is empty" flag onto the stack
	table.insert (isEmptyDirectoryStack, true)
	return false
end

function finishedDirectory(f)
	-- directory has been searched - is it still marked as empty?
	local isEmpty = table.remove (isEmptyDirectoryStack)
	if isEmpty then
		currentSearch.addMatch (f)
	else
		-- mark the content of the parent directory as non-empty, too
		--faf.logInfo("dir not empty bc of non-empty subdir: "..f.parentItem.path)
		isEmptyDirectoryStack[#isEmptyDirectoryStack] = false
	end
end