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
Set your OS, search path, and the name pattern. Every example below updates with your values.
The decision matrix
| Use case | Recommended | Why |
|---|---|---|
| Run one command on a batch of files | find ... -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 cmd | Only xargs has -P for parallel execution |
| Building the argument list from a non-find source | cmd ... | xargs -0 ... | xargs doesn't require find at all; it accepts any stdin stream |
| Maximum safety with filenames containing spaces/newlines | find ... -exec cmd {} + OR find ... -print0 | xargs -0 cmd | Both 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:
# 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 gzipThe 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:
- No second process needed.
-execis built into find.xargsis a separate binary that has to be in your PATH. - Native handling of weird filenames.
-execpasses filenames directly asargventries without going through the shell, so spaces, newlines, quotes, and backslashes all work without quoting tricks. - 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 \;:
# 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:
# 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:
# 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:
# 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):
find . -name '*.log' | xargs gzipIf 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):
find :search_path -type f -name ':pattern' -print0 | xargs -0 gzipfind -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):
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':
| Form | Time |
|---|---|
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
| Feature | GNU find | BSD find (macOS default) |
|---|---|---|
-exec ... {} \; | Supported | Supported |
-exec ... {} + | Supported | Supported |
-execdir ... {} \; | Supported | Supported |
-execdir ... {} + | Supported | Supported (macOS 10.x+) |
-ok ... {} \; (confirm per match) | Supported | Supported |
xargs -0 (NUL delimiter) | Supported | Supported |
xargs -P N (parallelism) | Supported | Supported |
xargs -I {} (replacement) | Supported | Supported |
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
-execnor 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 ...; donegives you full shell control per file. - Network-mounted filesystems with slow stat.
findcallsstaton every entry. On slow network mounts, this is the bottleneck.find -mountor-xdevto stay on local disk; alternately, uselocate(database-backed, no per-file stat).
See also
- find Command Cheat Sheet: the full find reference
- Find files modified in the last N days:
-mtimefilter for time-based scope - Find and delete files safely: when the command is
rm, prefer-deleteover-exec rm - Find files containing text (find + grep): the most common find-then-tool pipeline
- Bash while loop: the
while IFS= read -r -d '' fpattern for per-file shell logic - External: GNU find -exec docs, BSD find(1) man page, xargs(1) man page





