Finding Files with find and xargs
By the end of this lesson you'll be able to hunt down files by name, type, size, time, and permissions with find , run a command on every match with -exec , and pipe results into xargs for fast, safe bulk operations — including the all-important -print0 / -0 pairing that survives filenames with spaces.
Learn Finding Files with find and xargs in our free Command Line course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Cli course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
0. Setup: a predictable directory tree
Run this once. It creates a tiny project/ folder with source files, logs, and text files — plus one file deliberately named with a space ( my report.txt ) so we can later prove why -print0 matters. Every block after this assumes this exact layout.
1. Searching by Name and Type
find takes a starting path and walks every directory beneath it, keeping entries that pass your tests . -name "*.txt" matches by filename (case-sensitive); -iname is the case- insensitive version. -type f limits results to regular files and -type d to directories. Tests combine — listing them together means "all of these must be true".
2. Filtering by Size, Time, and Permissions
Three more tests do most of the heavy lifting. -size takes a number with a suffix ( c bytes, k KB, M MB) — +1M means larger than 1 MB, -100k smaller than 100 KB. -mtime counts in days: -7 is "within the last 7 days" and +30 is "older than 30 days". -perm filters by permission bits — -u+x matches anything the owner can execute.
3. Acting on Matches with -exec
Finding files is half the job — usually you want to do something with them. -exec runs a command on each match, with {} standing in for the filename. The action must be terminated, and how you terminate it matters: \; runs the command once per file , while + packs many filenames into fewer invocations (just like xargs ), which is dramatically faster for large result sets.
4. Piping to xargs (and Why -print0 Matters)
xargs reads items from standard input and appends them as arguments to a command — the classic way to feed find 's output into another tool in a single fast call. But plain find | xargs splits on whitespace, so a filename containing a space becomes two broken arguments. The fix is the safe pairing : find … -print0 | xargs -0 , which separates names with a NUL byte that can never appear inside a filename.
5. Placeholders and Parallelism: -I and -P
By default xargs appends arguments at the end of the command. When you need the filename elsewhere — say to build cp file file.bak — use -I {} to define a placeholder that gets substituted in place (this also makes xargs run one item per invocation). And when the work is slow, -P N runs up to N jobs in parallel , putting your CPU cores to use.
🎯 Your Turn: find by name and type
Fill in the one blank marked ___ using the # 👉 hint so the search catches every capitalization of .txt , then run it and check the Output panel matches.
🎯 Your Turn: count lines safely with xargs
Wire up the safe NUL-separated pipeline. Fill in both blanks so a filename with a space could never break the command, then run it.
Pro Tips
- 💡 Quote your patterns. Write find . -name "*.log" , not find . -name *.log — without quotes the shell expands *.log first and find never sees the pattern.
- 💡 Make -print0 | xargs -0 a reflex. It's the only pairing that survives spaces, tabs, and newlines in filenames. The plain pipe is a foot-gun on real data.
- 💡 Prefer {} + (or xargs) over {} \; when the command takes many files. Batching can turn ten thousand process launches into one.
- 💡 find . -name "*.tmp" -delete is the safe, built-in way to remove matches — no pipe to rm needed, and it handles odd filenames for you.
- 💡 Speed up CPU-bound bulk work with xargs -P . For example image conversion: find . -name "*.png" -print0 | xargs -0 -P 4 -I {} convert {} {} .webp .
Common Errors (and the fix)
- "find: paths must precede expression" — your -name pattern wasn't quoted, so the shell expanded it into multiple filenames. Wrap it in quotes: find . -name "*.txt" .
- -exec says "missing argument to `-exec'" — you forgot the terminator. End it with \; (one per file) or + (batched).
- xargs deletes or mangles the wrong files — a filename had a space and the plain pipe split it. Always use find … -print0 | xargs -0 … .
- -mtime +7 didn't match a file you edited yesterday — +7 means older than 7 days. You wanted -7 ("within the last 7 days").
- You meant to search file contents but used find — find matches by attributes, not text. To search inside files, use grep -r , or combine: find . -name "*.py" -exec grep -l TODO {} + .
📋 Quick Reference
Task
Command
Notes
Match by name
find . -name "*.txt"
Quote the pattern
Case-insensitive name
find . -iname "*.txt"
.txt and .TXT
Files / directories
-type f / -type d
Regular file vs dir
By size
-size +1M / -size -100k
+ more, - less
By modified time
-mtime -7 / -mtime +30
Days; - within, + older
Run once per file
-exec cmd {} \;
One invocation each
Batch into fewer calls
-exec cmd {} +
Like xargs, faster
Safe pipe to xargs
-print0 | xargs -0
Handles spaces
Placeholder for arg
xargs -I {} cmd {}
Put arg anywhere
Run in parallel
xargs -P 4
Up to 4 jobs at once
Delete matches
find . -name "*.tmp" -delete
Safe built-in
Frequently Asked Questions
Mini-Challenge: tidy up the project
No blanks this time — just a brief and an outline. Combine name/type tests, -mtime , the safe -print0 | xargs -0 pairing, and -exec … + into a few commands over the project/ tree, and check your results against the hints.
🎉 Lesson Complete!
- ✅ find walks a tree and selects entries by -name / -iname , -type , -size , -mtime , and -perm
- ✅ -exec cmd {} \; runs once per file; -exec cmd {} + batches many files into fewer, faster invocations
- ✅ xargs turns find's output into arguments; find … -print0 | xargs -0 is the safe pairing for names with spaces
- ✅ xargs -I {} places the argument anywhere, and xargs -P N runs N jobs in parallel
- ✅ Bulk operations: count with wc -l , search contents with grep , copy/rename with cp , and remove with -delete
- ✅ Next lesson: HTTP from the Terminal: curl and wget — fetch URLs, download files, and call APIs without leaving the shell
Practice quiz
What is the core job of the find command?
- It searches a directory tree for files and directories matching tests like name, type, size, or time
- It searches the contents of files for text
- It downloads files from the web
- It renames every file in a folder
Answer: It searches a directory tree for files and directories matching tests like name, type, size, or time. find walks a directory tree and selects entries by tests (-name, -type, -size, -mtime, ...). grep is the tool that searches text INSIDE files.
Which test matches only regular files (not directories or symlinks)?
- -file
- -type d
- -type f
- -type l
Answer: -type f. -type f selects regular files; -type d selects directories; -type l selects symbolic links.
How do you match files named with any capitalization of .TXT, like a.txt, B.Txt, and c.TXT?
- find . -type txt
- find . -iname "*.txt"
- find . -name "*.txt"
- find . -match "*.txt"
Answer: find . -iname "*.txt". -iname is the case-insensitive version of -name, so it matches *.txt regardless of letter case. -name is case-sensitive.
What does find . -mtime -7 select?
- The 7 most recently changed files
- Files modified more than 7 days ago
- Files exactly 7 bytes in size
- Files modified within the last 7 days
Answer: Files modified within the last 7 days. -mtime -7 means modified less than 7 days ago (within the last week). A plain 7 means exactly 7 days, and +7 means older than 7 days.
What is the difference between -exec cmd {} (semicolon) and -exec cmd {} + in find?
- The semicolon form runs cmd once per file, while the plus form passes many files to fewer cmd invocations
- The plus form runs once per file, while the semicolon form batches
- The plus form deletes files and the semicolon form copies them
- They are identical
Answer: The semicolon form runs cmd once per file, while the plus form passes many files to fewer cmd invocations. -exec cmd {} (semicolon) runs the command once for each matched file. -exec cmd {} + appends as many filenames as possible per invocation, like xargs, which is far faster for many files.
Why is find . -print0 | xargs -0 safer than find . | xargs?
- It sorts the filenames first
- It separates filenames with a NUL character so names containing spaces or newlines are handled correctly
- It runs commands in parallel
- It compresses the output
Answer: It separates filenames with a NUL character so names containing spaces or newlines are handled correctly. -print0 ends each name with a NUL byte and xargs -0 splits on NUL, so spaces, tabs, and newlines inside filenames no longer break the command.
What does the -I {} option do in xargs?
- It runs jobs in parallel
- It ignores blank input lines
- It reads input separated by NUL
- It defines a placeholder so each input argument is substituted at that spot, not just appended at the end
Answer: It defines a placeholder so each input argument is substituted at that spot, not just appended at the end. xargs -I {} sets {} as a replacement string, letting you place the argument anywhere in the command (for example, in the middle) instead of only at the end.
What does xargs -P 4 do?
- It limits output to 4 lines
- It passes at most 4 arguments per command
- It runs up to 4 command invocations in parallel
- It prints the command before running it
Answer: It runs up to 4 command invocations in parallel. xargs -P 4 runs as many as 4 jobs at the same time, which can dramatically speed up CPU-bound bulk work on multi-core machines.
Which command counts how many .log files exist under the current directory?
- find . -name "*.log" -size
- find . -type f -name "*.log" | wc -l
- grep -c log .
- ls *.log
Answer: find . -type f -name "*.log" | wc -l. find emits one path per line for each match, so piping the .log matches to wc -l counts them. ls *.log would miss files in subdirectories.
You want to delete every .tmp file safely, even ones with spaces in their names. Which is best?
- find . -name "*.tmp" -print0 | xargs -0 rm
- rm *.tmp
- find . -name "*.tmp" | xargs rm
- find . -name "*.tmp" -delete -now
Answer: find . -name "*.tmp" -print0 | xargs -0 rm. Pairing -print0 with xargs -0 NUL-separates the names so spaces and newlines are safe. find . -name "*.tmp" -delete also works, but the plain pipe to xargs breaks on spaces.