A find script that works perfectly on my Linux box fails on my Mac, and the error message is rarely helpful. The cause is almost always the same: macOS ships BSD find, Linux ships GNU find (part of the findutils package), and the two implementations agree on the common cases but diverge on exactly the flags people reach for in real scripts. -printf is the headline offender. It does not exist on BSD, so the moment a script crosses from a CI runner to a developer's MacBook (or the reverse), it breaks.
This is the deep dive on every divergence that has cost me time. The find command cheat sheet has a short version of this in its "macOS BSD vs GNU find" section; this article is the full reference. I cover each difference with a worked failure and the fix, the portable subset of flags that behave identically on both platforms, and how to install GNU find on a Mac when you just want one consistent tool.
Set your values
Set your OS and search path. Every find example below rewrites itself with your values, and swapping the OS shows the BSD vs GNU variant side by side.
Why there are two finds
find is a POSIX-specified utility, so a baseline set of behavior is standardized: -name, -type, -size, -mtime, -perm, -print, -exec, the boolean operators. POSIX does not standardize anything beyond that baseline, which leaves every implementation free to add extensions.
GNU findutils added a large pile of extensions over the decades: -printf, -regextype, -maxdepth, -mindepth, -iname, -delete, -execdir, -newerXY. BSD find added its own, overlapping but not identical, set. macOS inherits BSD find from FreeBSD's lineage. Linux distributions ship GNU find. So a script that uses a GNU-only extension runs fine on Linux and dies on macOS, and a script tuned to BSD quirks misbehaves on Linux.
The fix is never "hope it works". It is either to stick to the portable subset, branch on uname, or install GNU find on the Mac. The rest of this article makes each of those concrete.
Divergence 1: -printf is GNU only
This is the single most common cross-platform break. GNU find -printf formats each match with a custom string: file size, path, modification time, permissions, depth, anything. BSD find has no -printf at all.
The classic use is listing files with their size for a sort:
find :search_path -type f -printf '%s %p\n' | sort -rn | head -20On Linux, -printf '%s %p\n' prints the byte size and path. On macOS that command fails immediately with find: -printf: unknown primary or operator. The fix is -exec stat with BSD stat's format flags: %z is the size, %N is the name. There is no clean one-to-one BSD replacement for -printf, because -printf is a formatting engine and stat is a separate program with its own (different) format language.
The same break hits the epoch-timestamp pattern from the find files modified deep dive:
find :search_path -type f -printf '%T@ %p\n' | sort -rn | head -20GNU %T@ is the modification time as a Unix timestamp; BSD stat -f '%m' is the same value. The -exec stat variant forks one stat per file, which is slower on large trees, but it is the only portable answer short of installing GNU find.
Divergence 2: -regextype is GNU only, and the default flavors differ
GNU find lets you pick the regex dialect for -regex and -iregex with -regextype:
find . -regextype posix-extended -regex '.*/[0-9]{4}-[0-9]{2}\.log'BSD find has no -regextype flag. There is no way to change the regex flavor; you get whatever BSD's default is. Worse, the defaults differ between the two implementations:
- GNU find defaults to the
emacsregex flavor (a quirky dialect), not POSIX extended. - BSD find uses basic POSIX regular expressions (BRE).
So a -regex pattern written and tested on one platform can match differently, or fail to compile, on the other. A pattern with {4} repetition works under POSIX extended but is literal text under basic POSIX unless you escape it as \{4\}. The + and ? quantifiers similarly need backslashes under BRE.
My rule: do not use find -regex in any script meant to run on both platforms. Use -name and -path with glob patterns instead (those are portable), or filter with a grep -E downstream where the regex dialect is explicit and consistent:
find :search_path -type f -name '*.log' | grep -E '/[0-9]{4}-[0-9]{2}\.log$'grep -E is POSIX extended on both Linux and macOS, so the regex behaves the same. The find regex vs name article covers the wider tradeoff between -name globs and -regex.
Divergence 3: -maxdepth and -mindepth (both support them)
Good news for once. -maxdepth and -mindepth are not POSIX, but both GNU and BSD find implement them with identical semantics. -maxdepth 1 limits the search to the start directory's immediate children; -mindepth 1 skips the start directory itself.
find :search_path -maxdepth 2 -type f -name '*.conf'This is one of the rare non-POSIX flags that is safe to use cross-platform. Put -maxdepth and -mindepth before the other tests; both implementations evaluate the expression left to right, and a depth restriction placed after -name can produce surprising results.
Divergence 4: -delete behaves the same on both
-delete is a GNU extension that BSD also adopted, and the behavior matches: it removes each matched path, and it implies depth-first traversal so a directory's contents are deleted before the directory itself. No divergence here.
find :search_path -type f -name '*.tmp' -deleteThe safety habit is the same on both platforms: write the command with -print first, eyeball the list, then swap -print for -delete. There is no confirmation and no undo on either implementation. The find and delete files article has the full safe-delete playbook.
Divergence 5: -mtime fractional rounding edge cases
Both implementations accept -mtime, -mmin, -atime, and -ctime, and for the common cases they agree. The divergence is in how fractional days are handled at the boundary.
GNU find floors a file's age to whole 24-hour units before comparing, and documents that rounding precisely. BSD find handles the fractional remainder slightly differently in some edge cases, so a file modified very close to exactly N days ago can match -mtime -N on one platform and not the other.
For most scripts this is invisible: a log rotation that fires on "older than 30 days" does not care about a few seconds of boundary drift. When you do need exact behavior, drop -mtime for -mmin with explicit minutes (the minute math is unambiguous on both), or use -newer FILE against a touch-set marker file:
touch -t 202605120000 /tmp/cutoff
find :search_path -type f ! -newer /tmp/cutoff-newer compares exact mtimes, so it sidesteps the day-rounding question entirely. The find files modified deep dive covers the rounding rule in full.
Divergence 6: GNU long options do not exist on BSD
GNU tools accept double-dash long options. find --help and find --version work on Linux. On macOS, BSD find rejects them: find --version produces an error because BSD treats --version as a path argument (or an unknown primary). BSD find has no --help either; the documentation lives in man find.
This bites scripts that probe for find's version, or CI steps that log find --version for the build record. The portable way to check which find you have:
find --version | head -1In practice I detect the platform with uname rather than asking find about itself. More on that below.
Divergence 7: -execdir is supported on both
-execdir runs the command from the directory containing the matched file, passing the basename rather than the full path. It started as a GNU extension and a security improvement over -exec, and modern BSD find (including the version macOS ships) supports it with the same semantics.
find :search_path -type f -name '*.bak' -execdir rm {} \;Safe to use cross-platform. The one caveat: very old BSD find (pre-2005-era systems you will almost never meet today) lacked it. On any current macOS, -execdir is fine.
Divergence 8: stat itself uses completely different format strings
This one is adjacent to find rather than part of it, but it is critical because the -printf workaround leans on stat, and stat is where the platforms diverge hardest.
- GNU stat (Linux) uses
-c(or--format) with%-codes:%ssize,%nname,%Ymtime epoch,%aoctal permissions. - BSD stat (macOS) uses
-fwith a different set of%-codes:%zsize,%Nname,%mmtime epoch,%Lpoctal permissions.
The format letters are not just different flags, they are entirely different languages. A script that does stat -c '%s %n' on Linux must become stat -f '%z %N' on macOS. There is no overlap to lean on.
| What you want | GNU stat (Linux) | BSD stat (macOS) |
|---|---|---|
| File size in bytes | stat -c '%s' file | stat -f '%z' file |
| File name | stat -c '%n' file | stat -f '%N' file |
| Modification time (epoch) | stat -c '%Y' file | stat -f '%m' file |
| Octal permissions | stat -c '%a' file | stat -f '%Lp' file |
| Owner user name | stat -c '%U' file | stat -f '%Su' file |
This is why "just use stat" is not a clean answer to the -printf problem. You trade one platform divergence for another. The genuinely portable fix is to install GNU find (and GNU stat) on the Mac.
The portable subset
If a script must run unchanged on both Linux and macOS, restrict yourself to flags that behave identically on both. This is the intersection I trust:
| Flag | What it does |
|---|---|
-name | Match basename against a glob |
-iname | Case-insensitive -name |
-type | Filter by f, d, l, etc. |
-size | Filter by size with a unit suffix |
-mtime | Filter by modification age in days |
-mmin | Filter by modification age in minutes |
-perm | Filter by permission bits |
-newer | Compare against a reference file's mtime |
-print | Print the path (the default action) |
-print0 | Print NUL-delimited paths for safe piping |
-exec | Run a command per match ({} and \; or +) |
-execdir | Run a command from the match's directory |
-delete | Delete each match (depth-first) |
-prune | Skip a subtree |
-path | Match the full path against a glob |
-maxdepth / -mindepth | Limit recursion depth |
Everything outside that set risks BSD/GNU drift. The known offenders to avoid in cross-platform scripts: -printf, -regextype, -regex (default-flavor dependent), -newerXY letter variants, and GNU long options.
Getting GNU find on macOS
When I want one consistent tool everywhere instead of branching, I install GNU findutils on the Mac via Homebrew:
brew install findutilsHomebrew installs the GNU tools with a g prefix so they do not shadow the system BSD versions: gfind, gxargs, and (from coreutils) gstat. So gfind -printf '%s %p\n' works on macOS exactly as find -printf works on Linux.
To make the GNU versions the default in an interactive shell, alias them in ~/.zshrc (or ~/.bashrc):
alias find='gfind'
alias xargs='gxargs'
alias stat='gstat'Two cautions. First, aliases only affect interactive shells; scripts run with #!/bin/bash do not see them, so a script that needs GNU find must call gfind explicitly or set its own alias. Second, aliasing find to gfind system-wide can surprise other tools that expect BSD behavior on macOS. I prefer to leave the system find alone and call gfind deliberately where I need it.
Writing find scripts that run on both
Three strategies, in the order I reach for them.
1. Stick to the portable subset. The cleanest option. If the script only uses the flags in the table above, it runs unchanged on Linux and macOS with no detection logic. Most find scripts can be written this way; the -printf urge is usually satisfiable with -exec plus a portable command.
2. Detect the platform and branch. When a script genuinely needs -printf-style formatting, branch on uname:
if [ "$(uname)" = "Darwin" ]; then
find . -type f -exec stat -f '%z %N' {} \; | sort -rn | head
else
find . -type f -printf '%s %p\n' | sort -rn | head
fiThis is explicit and self-documenting. The downside is two code paths to maintain and test.
3. Require GNU findutils. For internal tooling where I control the environment, I document brew install findutils as a prerequisite and call gfind everywhere. The script is then a single GNU-only code path. This is the right call for a team's shared scripts where pinning the toolchain beats supporting two dialects.
Common mistakes
1. Assuming -printf exists. It is GNU only. A script that uses -printf fails on macOS with unknown primary or operator. Either branch on uname or use gfind.
2. Assuming the -regex flavor. GNU defaults to the emacs dialect; BSD uses basic POSIX. A pattern with unescaped {n}, +, or ? behaves differently. Avoid -regex in portable scripts; filter with grep -E downstream instead.
3. Assuming stat format strings match. GNU stat -c and BSD stat -f are different languages. stat -c '%s' on macOS fails; the BSD form is stat -f '%z'. The -printf workaround inherits this trap.
4. Logging find --version in CI. Works on Linux, errors on macOS. Use uname to identify the platform instead of interrogating find.
5. Testing on only one platform. A script written and tested on Linux can ship a GNU-only flag without anyone noticing until a macOS user hits it. If a script is meant to be cross-platform, test it on both, or pin it to GNU findutils and document the dependency.
When NOT to worry about this
The BSD/GNU divergence only matters when a script crosses platforms. It is a non-issue when:
- The script is single-platform. A deploy script that only ever runs on a Linux server can use every GNU extension freely. There is no Mac in the picture.
- CI pins the OS. If the pipeline always runs
ubuntu-latest, the build environment is GNU find, full stop. Write to GNU and move on. - It is a one-off command, not a committed script. A find command typed once into a terminal does not need to be portable. Use whatever your current shell gives you.
- You have already standardized on
gfind. If the Mac developers all runbrew install findutilsand the scripts callgfind, the divergence is closed by construction.
Portability has a cost: it constrains you to the intersection of two feature sets. Pay that cost only when the script actually needs to run in both places. For everything else, write to whichever find you have and keep the GNU extensions.
See also
- find Command Cheat Sheet: the full find reference covering name, type, size, permissions, and exec patterns
- find -regex vs -name: when to use glob patterns versus regex, and why
-regexportability is a trap - Find the largest files on disk: the
-sizeand biggest-files patterns, with the-printfvsstatsplit - Find files modified in the last 7 days: the
-mtimeand-mminreference, including the BSD rounding gotcha - External: GNU findutils manual, FreeBSD find(1) man page, Homebrew findutils formula.



![Bash arrays reference: declaration, indexing, [@] vs [*] quoting, iteration, appending, slicing, mapfile/readarray for lines, IFS-based string splitting, plus macOS Bash 3.2 limits.](https://techearl.com/cdn-cgi/image/width=1536,format=auto,quality=90/https://images.techearl.com/bash-arrays/bash-arrays.jpg?v=2026-02-12T14%3A18%3A00Z)

![Bash while loop reference: read files with while IFS= read -r, retry-with-backoff, wait-for-service polling, the subshell-scoping bug fix, the until and select siblings, plus [[ ]] vs [ ] vs (( )) test contexts.](https://techearl.com/cdn-cgi/image/width=1536,format=auto,quality=90/https://images.techearl.com/bash-while-loop/bash-while-loop.jpg?v=2026-04-25T13%3A18%3A00Z)