find and xargs
find walks file trees and matches by name, type, mtime, size, permission. xargs turns stdin into command-line arguments. Together they’re the standard “do something with every file matching X” pattern.
find — basic syntax
find <path> [conditions] [actions]
If no path: defaults to . in some implementations, errors in others. Always pass an explicit path.
find . -name "*.py" # all .py files under .
find /var/log -type f -name "*.log" # files (not dirs) named *.log
find . -mtime -7 # modified in last 7 days
find . -size +100M # larger than 100 MB
find . -empty # empty files or dirs
Common conditions
| Condition | Matches |
|---|---|
-name "pattern" |
name (glob, NOT regex) — quote to prevent shell expansion |
-iname "pattern" |
case-insensitive name |
-path "*/test/*" |
path pattern (matches the whole path) |
-type f |
regular file |
-type d |
directory |
-type l |
symbolic link |
-mtime -N / +N / N |
modified less than / more than / exactly N days ago |
-mmin -N / +N |
modified less than / more than N minutes ago |
-newer file |
newer than the mtime of file |
-size +100M / -100k / 100c |
size: M=megabytes, k=kilobytes, c=bytes |
-empty |
empty file or directory |
-perm -u=x |
has user-execute bit set |
-user alice |
owned by user alice |
-group www-data |
owned by group www-data |
-mtime units are confusing:
| Means | |
|---|---|
-mtime 0 |
modified today (within the last 24 hours) |
-mtime 1 |
modified between 24–48 hours ago |
-mtime -7 |
modified in the last 7 days |
-mtime +30 |
modified more than 30 days ago |
For “modified in the last hour”: -mmin -60.
Combining conditions
find . -name "*.py" -type f -mtime -1 # implicit AND
find . \( -name "*.py" -o -name "*.js" \) # OR (escape parens)
find . -name "*.py" ! -path "./.venv/*" # NOT
find . -type f -name "*.tmp" -size +10M # AND chained
Important: -o has lower precedence than implicit AND. Without parens:
find . -name "*.py" -o -name "*.js" -mtime -1
# means: (-name *.py) OR (-name *.js AND -mtime -1)
# probably NOT what you wanted
find . \( -name "*.py" -o -name "*.js" \) -mtime -1
# means: (*.py OR *.js) AND mtime -1
Actions
| Action | What |
|---|---|
-print |
print the path (default if no action) |
-print0 |
print path followed by NUL — safe for piping to xargs |
-delete |
delete the matched file (or empty dir) |
-exec cmd {} \; |
run cmd for each match, one at a time |
-exec cmd {} + |
run cmd with as many matches as possible per invocation |
-ls |
print details (like ls -l) |
-exec patterns
find . -name "*.tmp" -exec rm {} \; # one rm per file (slow)
find . -name "*.tmp" -exec rm {} + # rm with batched args (fast)
find . -name "*.txt" -exec grep -l "TODO" {} +
find . -type f -exec chmod 644 {} +
{} is the filename. The terminator must be \; (single file per call) or + (batched). Always prefer + for speed.
xargs — turn stdin into args
echo "1 2 3" | xargs echo # echo 1 2 3 (one call with 3 args)
seq 5 | xargs -n 1 echo # echo 1; echo 2; ... (one arg per call)
seq 100 | xargs -n 10 echo # 10 args per call
seq 100 | xargs -P 4 -n 1 ./worker.sh # 4 parallel workers
| Flag | Effect |
|---|---|
-n N |
max N args per command invocation |
-P N |
up to N parallel processes |
-I {} |
replace {} with each input (one per call) |
-0 |
NUL-separated input (paired with find -print0) |
-r |
don’t run if no input (GNU extension; useful with -0) |
find . -name "*.log" | xargs grep -l "error" # grep for "error"; broken on space-in-name
find . -name "*.log" -print0 | xargs -0 grep -l "error" # safe
-I {} replaces the placeholder per call:
ls *.txt | xargs -I {} cp {} {}.bak # cp foo.txt foo.txt.bak (one at a time)
-I implies -n 1 — one arg per call, slower than batched. Use only when you need the placeholder.
find vs xargs
-exec ... + and xargs do similar things. Why both?
find -exec ... + |
find -print0 | xargs -0 |
|
|---|---|---|
| Easier syntax | yes | requires -print0/-0 |
| Parallelism | no | yes via -P |
| Batching | yes (one per arg list) | yes |
| Built-in | requires find |
uses two tools |
For simple “run this on every match”: find -exec. For parallelism or fancier batching: xargs.
The “spaces in filenames” problem
find . -name "*.txt" | xargs cat # breaks on "file with spaces.txt"
find . -name "*.txt" -print0 | xargs -0 cat #
find . -name "*.txt" -exec cat {} + #
-print0 outputs NUL-separated names; -0 reads them. NUL can’t appear in filenames, so this is bulletproof.
For while read loops:
find . -name "*.txt" -print0 | while IFS= read -r -d '' f; do
echo "got: $f"
done
Common one-liners
# Total size of matched files
find . -name "*.log" -exec du -ch {} + | tail -1
# Find and delete files older than 30 days
find /var/log -name "*.log" -mtime +30 -delete
# Find files containing a string
find . -type f -exec grep -l "TODO" {} +
# faster:
grep -rl "TODO" . # ripgrep / grep -r is usually better than find -exec
# Find broken symlinks
find . -xtype l
# Find duplicate files by size (initial filter)
find . -type f -size +1M -printf "%s %p\n" | sort -n
# Recursive chmod on directories only
find . -type d -exec chmod 755 {} +
find . -type f -exec chmod 644 {} +
# Find files modified by a specific user in the last hour
find . -user alice -mmin -60
# Cleanup empty dirs (run multiple times for nested)
find . -type d -empty -delete
When to skip find for ripgrep / fd
Modern alternatives are faster and friendlier:
# fd (https://github.com/sharkdp/fd) — friendlier find
fd ".log$" /var/log
fd -t f -e log /var/log # type=file, extension=log
# ripgrep — when you're using find to grep
rg "TODO" --type py
For interactive use, fd and rg are usually better. For scripts that must work without external dependencies, find is universal.
Common pitfalls
- Forgetting to quote
-name "*.py"— without quotes, the shell expands*.pyfirst. If there are no .py files in the cwd, the glob stays literal and works. If there are, find sees-name foo.py bar.pyand errors. -mtimeconfusion —-mtime 1is “between 24 and 48 hours ago”, not “1 day or less”. Use-mtime -1for “less than 1 day.”- Forgetting
\(\)to group —-obinds tighter than expected without parens. - Piping to
xargswithout-print0/-0— breaks on spaces, newlines, quotes in filenames. -exec ... \;when+would work — N forks instead of 1, drastically slower.
Common interview confusions
- “
find -nameaccepts regex.” — it accepts globs (*,?,[abc]). For regex use-regex(and remember it matches the whole path, not just the basename). - “
xargsalways runs the command once per input.” — by default it batches as many args as fit inARG_MAX. Use-n 1for one-per-call. - “
-execandxargsare interchangeable.” — close, butxargs -P Nadds parallelism that-execlacks;-exec ... +is simpler when you don’t need it.
Interview angle
- “How do you find all
.pyfiles modified in the last day?” —find . -name "*.py" -type f -mtime -1. - “How do you delete files older than 30 days under
/var/log?” —find /var/log -type f -mtime +30 -delete. Or-exec rm {} +if-deleteisn’t supported. - “
-exec ... \;vs-exec ... +— what’s the difference?” —\;runs once per match (one fork each);+batches as many matches as fit (one fork for many). Always prefer+unless you specifically need per-file invocation. - “How do you handle filenames with spaces when piping
findtoxargs?” —find ... -print0 | xargs -0 cmd. NUL bytes can’t appear in filenames, so this is safe. - “How would you run something in parallel for every match?” —
find ... -print0 | xargs -0 -P 4 -n 1 ./worker.shruns up to 4 workers, one file each. - “
-mtime 1matches files modified when?” — between 24 and 48 hours ago.-mtime -1is “in the last 24 hours”;-mtime +30is “more than 30 days ago”.