TechEarl

find -exec vs xargs: Which to Use (and the {} + Trick That Beats Both)

find -exec ... {} + and find -print0 | xargs -0 are roughly equivalent for batch operations on matched files. find -exec ... {} \; forks once per match and is much slower. The decision matrix: when -exec is enough, when xargs adds value, and the safety rules for filenames with spaces, newlines, and quotes.

Ishan KarunaratneIshan Karunaratne⏱️ 12 min readUpdated
find -exec ... {} + batches arguments into one command (fast). find ... -exec ... {} \; forks per file (slow). xargs adds shell flexibility but needs -0 for safety. The decision matrix and performance comparison.

find . -name '*.log' -exec gzip {} + runs gzip once with all matched files as arguments. find . -name '*.log' -exec gzip {} \; runs gzip once per matched file, which is dramatically slower. find . -name '*.log' -print0 | xargs -0 gzip does the same thing as the first form, with the option to use shell features.

Most "find -exec vs xargs" debates miss the real distinction. The interesting comparison isn't -exec vs xargs. It's -exec ... {} + (batched) vs -exec ... {} \; (one-per-file). The + form is the one you want. Below is the decision matrix, the performance numbers, and the safety rules for filenames with weird characters.

Set your values

Try it with your own values

Set your OS, search path, and the name pattern. Every example below updates with your values.

The decision matrix

Use caseRecommendedWhy
Run one command on a batch of filesfind ... -exec cmd {} +Fastest, no extra processes, handles weird filenames natively
Run one command per file (different behavior per file)find ... -exec cmd {} \;Necessary when the command can't accept multiple paths
Need shell features (pipes, redirection, conditionals)find ... -print0 | xargs -0 sh -c 'cmd ...'Only xargs (via a shell wrapper) gives you a full shell context
Need parallelism (run N jobs in parallel)find ... -print0 | xargs -0 -P N cmdOnly xargs has -P for parallel execution
Building the argument list from a non-find sourcecmd ... | xargs -0 ...xargs doesn't require find at all; it accepts any stdin stream
Maximum safety with filenames containing spaces/newlinesfind ... -exec cmd {} + OR find ... -print0 | xargs -0 cmdBoth forms handle weird filenames; the NUL-delimited pipe is the universal safe form

The shortest takeaway: use -exec ... {} + by default. Reach for xargs when you need a shell feature -exec can't give you, or when you want parallelism.

The three forms compared

Same task, three syntaxes:

bash· Linux (GNU)
# Fast: one command, all files as args
find :search_path -type f -name ':pattern' -exec gzip {} +

# Slow: one command per file
find :search_path -type f -name ':pattern' -exec gzip {} \;

# Equivalent to the first, but uses a pipe
find :search_path -type f -name ':pattern' -print0 | xargs -0 gzip

The first form invokes gzip once with potentially thousands of file paths as arguments. The kernel argument list has a size limit (typically 128 KiB or 2 MiB), so for very large file lists -exec ... {} + may split into multiple invocations, but it's still vastly fewer than one-per-file.

The second form forks once per matched file. On 100,000 files, that's 100,000 fork+exec calls, each taking ~1 ms. The total overhead easily dominates the actual work.

Why -exec ... + is usually the right choice

The {} + form was added to POSIX find specifically to address the per-file fork cost. It does the same thing as xargs: batch arguments into one command invocation. Three reasons it's the default:

  1. No second process needed. -exec is built into find. xargs is a separate binary that has to be in your PATH.
  2. Native handling of weird filenames. -exec passes filenames directly as argv entries without going through the shell, so spaces, newlines, quotes, and backslashes all work without quoting tricks.
  3. One less moving part. No pipe, no NUL-delimited safety mode to remember. Just -exec cmd {} +.

The only thing -exec can't do is feed file paths through additional shell processing (pipes, command substitution, conditional logic). For that you need xargs.

When -exec ... ; is necessary

Some commands only accept one file at a time. mv, cp, and rm accept many; mediainfo (the metadata tool) accepts one; many older Unix utilities accept one. In those cases you have to fall back to \;:

bash· Linux (GNU)
# Mediainfo accepts one file at a time
find :search_path -type f -name '*.mp4' -exec mediainfo {} \;

The performance cost (one fork per file) is real but unavoidable when the command genuinely can't batch. For tools that do batch (like grep, gzip, tar, chmod, chown, rm), always prefer {} +.

When xargs adds genuine value

-exec runs a command with file paths as arguments. xargs does the same thing, but because there's a pipe in between, you can manipulate the file list with other shell tools before xargs sees it.

Parallelism with xargs -P:

bash· Linux (GNU)
# Compress all matched files in parallel, 4 at a time
find :search_path -type f -name ':pattern' -print0 | xargs -0 -P 4 -n 1 gzip

-P 4 runs up to 4 worker processes in parallel. -n 1 passes one file per invocation (so each worker gets one job). For CPU-bound tasks like compression, this gives near-linear speedup up to the number of CPU cores.

Shell features inside the command:

bash· Linux (GNU)
# Rename matched files using shell parameter expansion
find :search_path -type f -name '*.log' -print0 | xargs -0 -I {} mv {} {}.archived

-I {} replaces {} with the input filename, letting you reference the path multiple times in one command. Useful for renames and copies where the destination is derived from the source.

Limiting batch size:

bash· Linux (GNU)
# Process files 100 at a time, regardless of total
find :search_path -type f -name ':pattern' -print0 | xargs -0 -n 100 echo 'batch:'

-n 100 batches arguments 100 at a time. Useful when the command has its own argument-list limit lower than the kernel's.

The safety rules: handling weird filenames

Unix filenames can contain anything except / and NUL. That includes spaces, newlines, tabs, quotes, and backslashes. Naive pipelines break on all of these.

Unsafe (newline-delimited):

bash
find . -name '*.log' | xargs gzip

If any matched file has a space in its name, xargs splits the path at the space and passes the pieces as separate filenames. gzip fails on "missing file" errors.

Safe (NUL-delimited):

bash· Linux (GNU)
find :search_path -type f -name ':pattern' -print0 | xargs -0 gzip

find -print0 outputs filenames separated by NUL bytes (the only character that can't appear in a Unix filename). xargs -0 reads NUL-separated input. The combination is the only universally safe way to pipe filenames between find and another command.

Also safe (no pipe at all):

bash· Linux (GNU)
find :search_path -type f -name ':pattern' -exec gzip {} +

-exec passes filenames as argv entries with no shell involvement, so weird characters are never an issue. This is the simplest safe form.

Performance: -exec ... + vs -exec ... ;

A quick benchmark on 50,000 small files matched by -name '*.tmp':

FormTime
find ... -exec rm {} +~0.3 seconds
find ... -delete~0.3 seconds
find ... -print0 | xargs -0 rm -f~0.4 seconds (pipe overhead)
find ... -exec rm {} \;~12 seconds (40× slower)

The 40× difference is the fork+exec overhead. Each \; invocation costs ~250 microseconds even when the command itself does almost nothing. Multiplied across 50,000 files, that's the bulk of the runtime.

For commands that genuinely need one invocation per file (like mediainfo), the cost is unavoidable. For commands that batch (rm, gzip, grep, chmod, chown), always use + or xargs.

macOS BSD vs GNU find -exec

FeatureGNU findBSD find (macOS default)
-exec ... {} \;SupportedSupported
-exec ... {} +SupportedSupported
-execdir ... {} \;SupportedSupported
-execdir ... {} +SupportedSupported (macOS 10.x+)
-ok ... {} \; (confirm per match)SupportedSupported
xargs -0 (NUL delimiter)SupportedSupported
xargs -P N (parallelism)SupportedSupported
xargs -I {} (replacement)SupportedSupported

The behavior matches closely. The one practical difference: BSD xargs -0 is slightly stricter about how the input is terminated (a trailing NUL is required; GNU is more permissive about missing trailing NULs).

Common mistakes

1. Forgetting to escape the \;. The semicolon is a shell metacharacter that ends commands. find . -exec rm {} ; runs find with no -exec action and then runs an empty command. Always escape: \; or quote: ';'.

2. Mixing {} + and \; in the same command. find allows only one action; you can't combine + and \; in a single -exec. To run different commands per match, use separate -exec clauses.

3. Using -exec ... \; when + works. The single most common reason find scripts are slow. If the command can accept multiple file paths (most commands can), use +.

4. Unsafe find | xargs without -print0 -0. Pipelines that don't use NUL delimiters break on filenames with spaces. Either use -print0 | xargs -0 or skip the pipe entirely and use -exec ... {} +.

5. Forgetting xargs -r to skip empty input. If find matches nothing, plain xargs cmd will run cmd with no arguments, which often has unexpected behavior (e.g., rm with no args is an error, grep with no args reads from stdin). xargs -r (-no-run-if-empty on BSD) skips the command when there's no input.

6. Using xargs -I {} for everything. -I switches xargs into one-per-invocation mode, which loses the batching benefit. Use -I only when you need to reference the input multiple times in one command; for plain batching, just xargs cmd (or xargs -0 cmd).

7. Quoting the {} placeholder. Both find and xargs recognize {} as a replacement token; you don't need to quote it ('{}' is unnecessary). On rare shells, quoting can actually break the substitution.

When NOT to use either

Some situations are better handled by other tools:

  • Atomic bulk operations. Neither -exec nor xargs is atomic. If the command is interrupted halfway, you get partial results. For atomic deletes/renames, use a script that lists, validates, then commits.
  • Complex per-file logic. If you need conditional logic per file ("delete only if larger than X and older than Y and not symlinked"), wrap the loop in bash: find ... -print0 | while IFS= read -r -d '' f; do ...; done gives you full shell control per file.
  • Network-mounted filesystems with slow stat. find calls stat on every entry. On slow network mounts, this is the bottleneck. find -mount or -xdev to stay on local disk; alternately, use locate (database-backed, no per-file stat).

See also

FAQ

TagsfindexecxargsCLILinuxmacOSBSDPerformance
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

find -name uses shell globs on the basename; find -regex matches a full regular expression against the whole path. The -regextype flavors, the GNU emacs vs BSD basic default drift, and when each one is the right tool.

find -regex vs -name: When to Use Regex in find

find -name takes a shell glob and matches the basename; find -regex takes a full regular expression and matches the whole path. That whole-path detail is the number one surprise: -regex '.*\.txt' works but -regex '.txt' matches nothing. The flag reference, -regextype flavors, the GNU vs BSD default-flavor drift, and when -name is the better tool.

find walks the live filesystem every time; locate and plocate query a prebuilt updatedb database. Compare freshness, speed, permission-awareness, and filtering, plus the mlocate vs plocate split and the macOS mdfind alternative.

find vs locate vs mlocate: Which File Search Tool to Use

find walks the live filesystem every time it runs: always current, sometimes slow. locate queries a prebuilt database: instant, but stale until the next updatedb. This breaks down the locate family (mlocate, plocate, slocate), the macOS situation, and exactly when to reach for each one.

Why find scripts break between macOS and Linux: -printf and -regextype are GNU only, regex flavors and stat format strings differ. The portable find subset, the gotchas, and brew install findutils for gfind.

BSD find vs GNU find: Every macOS vs Linux Difference That Matters

macOS ships BSD find; Linux ships GNU find. The two share a name and most of an interface, but -printf, -regextype, and the stat format strings diverge hard enough to break scripts shipped between platforms. The full divergence list, the portable subset that works on both, and how to get GNU find on a Mac.